private IReadOnlyList <object> GenerateValues(Item endItem, IProduction production)
    {
        // Small shortcircuit: If this production has one symbol, and that symbol is a
        // terminal, just return those derivation results directly without any buffering.
        if (production.Symbols.Count == 1 && production.Symbols[0] is IParser)
        {
            _statistics.DerivationSingleSymbolShortcircuits++;
            return(endItem.Derivations !
                   .Select(d => production.Apply(new[] { d }))
                   .Where(o => o.Success)
                   .Select(o => o.Value)
                   .ToList());
        }

        Item current = endItem;
        var  count   = production.Symbols.Count;
        var  values  = ArrayPool <IReadOnlyList <object> > .Shared.Rent(count);

        if (!GetValuesArray(values, current))
        {
            return(Array.Empty <object>());
        }

        // Small shortcircuit: If there is only one item in the production, and that one item
        // has only one possible value after recursing, just return that without buffering.
        if (count == 1 && values[0].Count == 1)
        {
            var result = production.Apply(new[] { values[0][0] });
            _statistics.ItemsWithSingleDerivation++;
            return(result.Success ? new object[] { result.Value } : Array.Empty <object>());
        }

        // Allocate a buffer and fill with initial values
        var buffer = ArrayPool <object> .Shared.Rent(count);

        InitializeBuffer(count, values, buffer);

        // Get an array to hold indices and initialize all values (they might not be 0
        // coming out of the array pool)
        var indices = ArrayPool <int> .Shared.Rent(count);

        Array.Clear(indices, 0, count);

        // We traverse from Item to Item.ParentItem, which forms a unique chain from the end
        // back to the beginning. Even if Item.ParentItem has multiple children of different
        // lengths we can traverse those branches separately and there is no ambiguity in
        // derivation of results.

        // Start getting values. On each loop we call the production callback with what we
        // have in the buffer already, then we update values to the next index. When position
        // 1 gets to the last value, we reset it to 0 and increment position 2, etc. When
        // we've incremented the last position past the number of items in the last column, we
        // break from the loop and are done.
        var results = new List <object>();

        do
        {
            _statistics.ProductionRuleAttempts++;
            var result = production.Apply(buffer);
            if (result.Success)
            {
                results.Add(result.Value);
                _statistics.ProductionRuleSuccesses++;
            }
        }while (IncrementBufferItems(count, indices, values, buffer));

        ArrayPool <IReadOnlyList <object> > .Shared.Return(values);

        ArrayPool <object> .Shared.Return(buffer);

        ArrayPool <int> .Shared.Return(indices);

        return(results);
    }