@@ 8,13 8,16 @@
-- The underlying Lua string is kept GC-anchored on the view table so
-- substring-based passthrough remains valid for the view's lifetime.
--
--- Phase 1: read-only API (:get / :has / :which / :iter / :names plus
--- ArrayView :len/:at/:iter and MapView :get/:has/:keys/:iter). Mutation
--- and passthrough re-encode arrive in phase 2.
+-- Storage layout: SoA (struct-of-arrays). Per wire-entry we hold four
+-- integer offsets in parallel arrays, not one Lua table per entry.
+-- For an emails-heavy message at 100 KB that's 4 tables of 2800 ints
+-- instead of 2800 tables of 4 keys — ~5× fewer table allocations on
+-- decode_lazy, which is the difference between losing sparse-read 0.6×
+-- and breaking even.
--
-- WKT and any descriptor carrying `desc.decode` are eagerly decoded and
--- wrapped in an EagerView with the same getter surface, so callers don't
--- have to special-case Timestamp/Duration/Struct/etc.
+-- wrapped in an EagerView with the same getter surface, so callers
+-- don't have to special-case Timestamp/Duration/Struct/etc.
local wire = require('pb.wire')
local codec = require('pb.codec')
@@ 27,72 30,75 @@ local build_array_view
local build_map_view
-- ---------------------------------------------------------------------------
--- Index build
+-- Index build (SoA)
--
--- Single pass over `bytes`. Each tag+value occupies one `segment`:
--- {id = <field id>,
--- tag_start = <1-based offset of tag's first byte>,
--- val_start = <1-based offset of value's first byte>,
--- next_start = <1-based offset just past this segment>,
--- wt = <wire type>}
+-- Single pass over `bytes`. Returns:
+-- segs = {
+-- n = <entry count>,
+-- id = {<field id>, ...}, -- 1-based, by entry
+-- tag_start = {...}, -- 1-based byte offset, tag's first byte
+-- val_start = {...}, -- , value's first byte
+-- next_start = {...}, -- , just past this entry
+-- }
+-- by_id = { [field_id] = {seg_idx, seg_idx, ...} } -- indices into segs
--
--- `segments` is the wire-order list (used by iter and by passthrough re-encode
--- in phase 2). `by_id` maps id -> list-of-segments, even for singular fields
--- (multiple wire entries for the same singular id are legal per spec: scalar
--- last-wins, message-merge — both consumers can walk the list).
+-- We deliberately don't keep wire_type: consumers know it from the
+-- descriptor for known fields, and we never re-decode the tag for
+-- unknown fields after the index pass (we only splice their bytes).
-- ---------------------------------------------------------------------------
local function index_bytes(desc, bytes)
local pos, lim = 1, #bytes
- local segments = {}
+ local s_id, s_tag, s_val, s_next = {}, {}, {}, {}
local by_id = {}
local fbi = desc.field_by_id
+ local n = 0
while pos <= lim do
local tag_start = pos
local id, wt, npos = wire.decode_tag(bytes, pos)
local val_start = npos
local next_start = wire.skip_field(bytes, npos, wt)
- local seg = {
- id = id,
- tag_start = tag_start,
- val_start = val_start,
- next_start = next_start,
- wt = wt,
- }
- segments[#segments + 1] = seg
+ n = n + 1
+ s_id[n] = id
+ s_tag[n] = tag_start
+ s_val[n] = val_start
+ s_next[n] = next_start
if fbi[id] ~= nil then
local list = by_id[id]
if list == nil then
- by_id[id] = {seg}
+ by_id[id] = {n}
else
- list[#list + 1] = seg
+ list[#list + 1] = n
end
end
pos = next_start
end
- return segments, by_id
+ return {
+ n = n,
+ id = s_id,
+ tag_start = s_tag,
+ val_start = s_val,
+ next_start = s_next,
+ }, by_id
end
-- ---------------------------------------------------------------------------
-- Per-field materialization
-- ---------------------------------------------------------------------------
--- Decode a single value from one segment, given the singular field shape.
--- For message fields this returns another lazy view; for scalar/enum it
--- returns the materialized Lua/cdata value.
-local function read_singular(field, bytes, seg)
+-- Decode a single value at `val_start`. For message fields returns a
+-- lazy sub-view (or eager value via WKT desc.decode override).
+local function read_singular(field, bytes, val_start)
local kind = field.kind
if kind == 'scalar' then
- local h = wire.TYPE_INFO[field.proto_type]
- local v = h.decode(bytes, seg.val_start)
+ local v = wire.TYPE_INFO[field.proto_type].decode(bytes, val_start)
return v
elseif kind == 'enum' then
- local u = wire.decode_varint(bytes, seg.val_start)
+ local u = wire.decode_varint(bytes, val_start)
return tonumber(u)
elseif kind == 'message' then
- local payload = wire.decode_len(bytes, seg.val_start)
+ local payload = wire.decode_len(bytes, val_start)
if field.message.decode ~= nil then
- -- WKT (or any custom-decode override). Eager.
return field.message.decode(payload)
end
return build_msg_view(field.message, payload)
@@ 102,20 108,16 @@ end
-- For singular fields, semantics for multiple wire entries with the same id:
-- - scalar/enum: last wins.
--- - message: merged. We delegate to the eager codec by concatenating the
--- per-entry payloads and feeding them to pb.codec.decode, which honors
--- proto3 merge rules. (Multi-entry singular messages are rare; this is
--- the off-fast-path correctness branch.)
-local function read_singular_list(field, bytes, list)
- if field.kind ~= 'message' or #list == 1 then
- return read_singular(field, bytes, list[#list])
- end
- -- Multi-segment message: concatenate the inner payloads and eager-decode.
- -- This loses the lazy sub-view but is the right thing semantically.
+-- - message: merged. We delegate to the eager codec by concatenating
+-- per-entry payloads. (Multi-entry singular messages are rare; this
+-- is the off-fast-path correctness branch.)
+local function read_singular_list(field, bytes, segs, idx_list)
+ if field.kind ~= 'message' or #idx_list == 1 then
+ return read_singular(field, bytes, segs.val_start[idx_list[#idx_list]])
+ end
local parts = {}
- for i = 1, #list do
- local seg = list[i]
- local payload = wire.decode_len(bytes, seg.val_start)
+ for i = 1, #idx_list do
+ local payload = wire.decode_len(bytes, segs.val_start[idx_list[i]])
parts[i] = payload
end
local merged = table.concat(parts)
@@ 128,110 130,90 @@ end
-- ---------------------------------------------------------------------------
-- ArrayView: lazy view over a repeated field.
--
--- For unpacked repeated, each wire entry is one element (one segment).
--- For packed repeated (scalars/enums), one segment contains a length-delimited
--- payload with all elements; we walk the payload on demand. We never
--- materialize the full element array unless the user iterates it all.
+-- Stores a flat int array `_starts` of value offsets (one int per element).
+-- For unpacked, those come from the parent's val_start array (subset).
+-- For packed, they're scanned out of the packed payload at construct time.
+-- :at(i) decodes from bytes[_starts[i]] using the field's known kind.
-- ---------------------------------------------------------------------------
local ArrayView = {}
ArrayView.__index = ArrayView
--- Pre-expand packed payloads into a uniform per-element segment list at
--- construct time. Cheaper to do once than to re-scan on every :at(i).
-local function expand_packed(field, bytes, packed_seg)
+-- Walk a packed payload, emitting one val_start per element. Cheaper as
+-- a one-pass scan than re-walking on every :at — packed payloads are
+-- contiguous so each step is just a wire.skip_field with the element's
+-- known wire type.
+local function expand_packed(field, bytes, val_start)
local h = field.kind == 'scalar' and wire.TYPE_INFO[field.proto_type] or nil
- -- Decode the length prefix to know where elements live.
- local payload_start = packed_seg.val_start
- local b = bytes:byte(payload_start)
+ local b = bytes:byte(val_start)
local payload_len, hdr_end
if b < 0x80 then
- payload_len = b
- hdr_end = payload_start + 1
+ payload_len = b; hdr_end = val_start + 1
else
- local v, npos = wire.decode_varint(bytes, payload_start)
- payload_len = tonumber(v)
- hdr_end = npos
+ local v, npos = wire.decode_varint(bytes, val_start)
+ payload_len = tonumber(v); hdr_end = npos
end
local lim = hdr_end + payload_len
- local elems = {}
+ local elem_wire = (field.kind == 'scalar') and h.wire or wire.WIRE_VARINT
+ local starts, n = {}, 0
local p = hdr_end
while p < lim do
- local v_start = p
- local v_next
- if field.kind == 'scalar' then
- -- Skip according to the scalar's actual wire type.
- v_next = wire.skip_field(bytes, p, h.wire)
- else
- -- Packed enums: varint per element.
- v_next = wire.skip_field(bytes, p, wire.WIRE_VARINT)
- end
- elems[#elems + 1] = {val_start = v_start, val_next = v_next}
- p = v_next
+ n = n + 1
+ starts[n] = p
+ p = wire.skip_field(bytes, p, elem_wire)
end
- return elems
+ return starts, n
end
-local function build_array_view_impl(field, bytes, segs)
- -- For packed payloads (single segment, WIRE_LEN, but scalar field uses
- -- a non-LEN wire type), elements live inside that one segment.
- -- For unpacked, each segment is one element.
- local mode -- 'packed' | 'unpacked'
- local elements -- list of {val_start = ...} entries
- if field.kind == 'scalar' then
- local h = wire.TYPE_INFO[field.proto_type]
- if h.wire ~= wire.WIRE_LEN and #segs == 1 and segs[1].wt == wire.WIRE_LEN then
- mode = 'packed'
- elements = expand_packed(field, bytes, segs[1])
- else
- mode = 'unpacked'
- elements = segs
- end
- elseif field.kind == 'enum' then
- if #segs == 1 and segs[1].wt == wire.WIRE_LEN then
- mode = 'packed'
- elements = expand_packed(field, bytes, segs[1])
- else
- mode = 'unpacked'
- elements = segs
- end
+local function build_array_view_impl(field, bytes, segs, idx_list)
+ -- Detect packed vs unpacked from the wire type of the entries.
+ -- For repeated scalars/enums with a single LEN-typed wire entry
+ -- when the element type is non-LEN, that's a packed payload.
+ local mode, starts, n
+ local first_idx = idx_list[1]
+ local tag_byte = bytes:byte(segs.tag_start[first_idx])
+ local first_wt = tag_byte % 8 -- low 3 bits
+
+ if field.kind == 'message' then
+ -- Repeated messages never pack.
+ mode = 'unpacked'
+ n = #idx_list
+ starts = {}
+ for i = 1, n do starts[i] = segs.val_start[idx_list[i]] end
+ elseif (field.kind == 'scalar' or field.kind == 'enum')
+ and #idx_list == 1 and first_wt == wire.WIRE_LEN
+ and not (field.kind == 'scalar'
+ and wire.TYPE_INFO[field.proto_type].wire == wire.WIRE_LEN) then
+ mode = 'packed'
+ starts, n = expand_packed(field, bytes, segs.val_start[first_idx])
else
- -- repeated message: always one segment per element, never packed.
mode = 'unpacked'
- elements = segs
+ n = #idx_list
+ starts = {}
+ for i = 1, n do starts[i] = segs.val_start[idx_list[i]] end
end
+
return setmetatable({
- _field = field,
- _bytes = bytes,
- _mode = mode,
- _elements = elements,
+ _field = field,
+ _bytes = bytes,
+ _starts = starts,
+ _n = n,
}, ArrayView)
end
build_array_view = build_array_view_impl
-function ArrayView:len()
- return #self._elements
-end
+function ArrayView:len() return self._n end
function ArrayView:at(i)
- local entry = self._elements[i]
- if entry == nil then return nil end
+ if i < 1 or i > self._n then return nil end
local field, bytes = self._field, self._bytes
- if self._mode == 'packed' then
- if field.kind == 'enum' then
- local u = wire.decode_varint(bytes, entry.val_start)
- return tonumber(u)
- end
- local h = wire.TYPE_INFO[field.proto_type]
- return (h.decode(bytes, entry.val_start))
- end
- return read_singular(field, bytes, entry)
+ local val_start = self._starts[i]
+ return read_singular(field, bytes, val_start)
end
function ArrayView:iter()
- local view, i = self, 0
- local n = #self._elements
+ local view, i, n = self, 0, self._n
return function()
i = i + 1
if i > n then return nil end
@@ 241,34 223,29 @@ end
function ArrayView:tolist()
local out = {}
- for i = 1, #self._elements do out[i] = self:at(i) end
+ for i = 1, self._n do out[i] = self:at(i) end
return out
end
-- ---------------------------------------------------------------------------
-- MapView: lazy view over a map<K,V> field.
--
--- Each wire entry is one length-delimited submessage with two fields:
--- id=1 (key), id=2 (value). The submessage may omit either when the
--- value equals its proto3 default.
---
--- For O(1) `:get(k)`, we decode keys lazily but cache the key->entry map on
--- first key-lookup or first :iter. Until then, only the per-entry payload
--- offsets are known.
+-- Stores a flat int array `_starts` of LEN-prefix offsets — one per map
+-- entry. Each entry contains the (key, value) sub-fields and is decoded
+-- on first :get/:has/:iter to build a key->value cache.
-- ---------------------------------------------------------------------------
local MapView = {}
MapView.__index = MapView
-local function decode_map_entry(field, bytes, seg)
+local function decode_map_entry(field, bytes, val_start)
local key_field, val_field = field.key, field.value
- -- Read the outer LEN to find the entry payload.
- local b = bytes:byte(seg.val_start)
+ local b = bytes:byte(val_start)
local payload_len, hdr_end
if b < 0x80 then
- payload_len = b; hdr_end = seg.val_start + 1
+ payload_len = b; hdr_end = val_start + 1
else
- local v, npos = wire.decode_varint(bytes, seg.val_start)
+ local v, npos = wire.decode_varint(bytes, val_start)
payload_len = tonumber(v); hdr_end = npos
end
local lim = hdr_end + payload_len
@@ 320,11 297,15 @@ local function decode_map_entry(field, bytes, seg)
return key, val
end
-local function build_map_view_impl(field, bytes, segs)
+local function build_map_view_impl(field, bytes, segs, idx_list)
+ local n = #idx_list
+ local starts = {}
+ for i = 1, n do starts[i] = segs.val_start[idx_list[i]] end
return setmetatable({
- _field = field,
- _bytes = bytes,
- _segs = segs,
+ _field = field,
+ _bytes = bytes,
+ _starts = starts,
+ _n = n,
-- _by_key populated lazily on first :get/:has/:iter call.
}, MapView)
end
@@ 333,11 314,11 @@ build_map_view = build_map_view_impl
local function map_ensure_index(self)
if self._by_key ~= nil then return end
- local field, bytes, segs = self._field, self._bytes, self._segs
+ local field, bytes, starts, n = self._field, self._bytes, self._starts, self._n
local by_key = {}
local keys = {}
- for i = 1, #segs do
- local k, v = decode_map_entry(field, bytes, segs[i])
+ for i = 1, n do
+ local k, v = decode_map_entry(field, bytes, starts[i])
if by_key[k] == nil then keys[#keys + 1] = k end
by_key[k] = v -- duplicate keys: last wins (matches eager decode)
end
@@ 398,11 379,11 @@ local function build_msg_view_impl(desc, bytes)
_eager_only = true,
}, MessageView)
end
- local segments, by_id = index_bytes(desc, bytes)
+ local segs, by_id = index_bytes(desc, bytes)
return setmetatable({
_desc = desc,
_bytes = bytes,
- _segments = segments,
+ _segs = segs,
_by_id = by_id,
_cache = {},
-- Parallel array of cached sub-MessageViews so :is_dirty can
@@ 415,7 396,7 @@ end
build_msg_view = build_msg_view_impl
M.build = build_msg_view_impl
--- :get(name) -> decoded value, or nil if not on wire (and no _cache entry).
+-- :get(name) -> decoded value, or nil if not on wire.
function MessageView:get(name)
if self._eager_only then return self._eager[name] end
local cache = self._cache
@@ 423,18 404,16 @@ function MessageView:get(name)
if v ~= nil then return v end
local field = self._desc.field_by_name[name]
if field == nil then return nil end
- local segs = self._by_id[field.id]
- if segs == nil then return nil end
+ local idx_list = self._by_id[field.id]
+ if idx_list == nil then return nil end
if field.kind == 'map' then
- v = build_map_view(field, self._bytes, segs)
+ v = build_map_view(field, self._bytes, self._segs, idx_list)
elseif field.repeated then
- v = build_array_view(field, self._bytes, segs)
+ v = build_array_view(field, self._bytes, self._segs, idx_list)
else
- v = read_singular_list(field, self._bytes, segs)
+ v = read_singular_list(field, self._bytes, self._segs, idx_list)
end
cache[name] = v
- -- Track sub-MessageViews on a flat array so :is_dirty can walk
- -- with ipairs (see _sub_msg_views note above).
if type(v) == 'table' and getmetatable(v) == MessageView then
local s = self._sub_msg_views
s[#s + 1] = v
@@ 452,7 431,7 @@ end
-- :which(oneof_name) -> name of the active branch, or nil.
-- Proto3 last-wins: if multiple branches appeared on the wire, the one
--- whose final segment came last in wire order is active.
+-- whose final entry came last in wire order is active.
function MessageView:which(oneof_name)
if self._eager_only then
local oneofs = self._desc.oneofs
@@ 470,15 449,16 @@ function MessageView:which(oneof_name)
if members == nil then return nil end
local member_set = {}
for i = 1, #members do member_set[members[i]] = true end
- local fbn = self._desc.field_by_name
+ local segs = self._segs
+ local fbi = self._desc.field_by_id
local active, active_pos
- local segs = self._segments
- for i = 1, #segs do
- local f = self._desc.field_by_id[segs[i].id]
+ for i = 1, segs.n do
+ local f = fbi[segs.id[i]]
if f ~= nil and member_set[f.name] then
- if active_pos == nil or segs[i].tag_start > active_pos then
+ local ts = segs.tag_start[i]
+ if active_pos == nil or ts > active_pos then
active = f.name
- active_pos = segs[i].tag_start
+ active_pos = ts
end
end
end
@@ 500,16 480,15 @@ function MessageView:names()
end
end
end
- local segments = self._segments
+ local segs = self._segs
local fbi = self._desc.field_by_id
local emitted = {}
local i = 0
return function()
while true do
i = i + 1
- local seg = segments[i]
- if seg == nil then return nil end
- local f = fbi[seg.id]
+ if i > segs.n then return nil end
+ local f = fbi[segs.id[i]]
if f ~= nil and emitted[f.name] == nil then
emitted[f.name] = true
return f.name
@@ 534,7 513,7 @@ function MessageView:iter()
end
end
end
- local segments = self._segments
+ local segs = self._segs
local fbi = self._desc.field_by_id
local emitted = {}
local view = self
@@ 542,9 521,8 @@ function MessageView:iter()
return function()
while true do
i = i + 1
- local seg = segments[i]
- if seg == nil then return nil end
- local f = fbi[seg.id]
+ if i > segs.n then return nil end
+ local f = fbi[segs.id[i]]
if f ~= nil and emitted[f.name] == nil then
emitted[f.name] = true
return f.name, view:get(f.name)
@@ 559,9 537,6 @@ end
-- :set(name, value) marks a field dirty. Subsequent :encode() emits the
-- new value via the codec; other fields passthrough their original bytes.
--- The provided value can be any shape the eager encoder accepts (Lua
--- table/string/number/cdata/array). Passing a MessageView as `value` is
--- supported but materializes it via :totable() on encode.
function MessageView:set(name, value)
if self._eager_only then
self._eager[name] = value
@@ 578,14 553,8 @@ function MessageView:set(name, value)
end
-- :is_dirty() returns true if this view has had :set called, OR if any
--- cached sub-view (e.g. a nested MessageView accessed via :get) has been
--- mutated. Used by parent :encode to decide whether to splice or re-encode.
---
--- We walk _sub_msg_views (a flat array of cached sub-MessageViews) with
--- ipairs instead of pairs(_cache). Hash-keyed pairs compiles to ISNEXT
--- which is NYI in Tarantool's LuaJIT 2.1 — keeping is_dirty on an array
--- lets the encode-time JIT trace stay attached. Array fields and map
--- fields aren't recursed into: phase 2 mutation is on MessageView only.
+-- cached sub-view has been mutated. Walks _sub_msg_views (flat array)
+-- with ipairs to stay JIT-stable.
function MessageView:is_dirty()
if self._eager_only then return self._eager_dirty == true end
local d = self._dirty
@@ 598,7 567,6 @@ function MessageView:is_dirty()
end
-- Materialize a possibly-view value so encode_field can consume it.
--- Lazy sub-views are converted to plain tables; raw values pass through.
local function materialize(value)
if type(value) ~= 'table' then return value end
local m = getmetatable(value)
@@ 612,24 580,21 @@ local function materialize(value)
return value
end
--- Recursively materialize a value to a plain Lua structure suitable for
--- the eager codec. For a MessageView, we walk every present field —
--- this is the cost of mutation; pure-read messages stay lazy.
function MessageView:totable()
if self._eager_only then return self._eager end
local out = {}
for name in self:names() do
- local v = self:get(name)
- out[name] = materialize(v)
+ out[name] = materialize(self:get(name))
end
-- Preserve unknown fields for round-trip.
- if self._segments then
+ local segs = self._segs
+ if segs then
local unknown = {}
local fbi = self._desc.field_by_id
- for i = 1, #self._segments do
- local seg = self._segments[i]
- if fbi[seg.id] == nil then
- unknown[#unknown + 1] = self._bytes:sub(seg.tag_start, seg.next_start - 1)
+ for i = 1, segs.n do
+ if fbi[segs.id[i]] == nil then
+ unknown[#unknown + 1] =
+ self._bytes:sub(segs.tag_start[i], segs.next_start[i] - 1)
end
end
if #unknown > 0 then out._unknown_fields = table.concat(unknown) end
@@ 641,8 606,8 @@ end
-- 1. WKT eager-wrap: delegate to desc.encode on the materialized table.
-- 2. Untouched (no dirty fields, no dirty sub-views): emit the original
-- bytes verbatim — perfect byte-for-byte passthrough.
--- 3. Mixed: walk fields in id order, splice clean segments, encode dirty
--- values fresh. Unknown segments are emitted at the end.
+-- 3. Mixed: walk fields in id order, splice clean segments, encode
+-- dirty values fresh. Unknown segments are emitted at the end.
function MessageView:encode()
if self._eager_only then
return self._desc.encode(self._eager)
@@ 654,12 619,13 @@ function MessageView:encode()
local out = {}
local fields = self._desc.fields
local bytes = self._bytes
+ local segs = self._segs
local by_id = self._by_id
local dirty = self._dirty or {}
local cache = self._cache
- -- Active-oneof resolution mirrors codec.encode_message:
- -- a oneof field is only emitted if it's the active branch.
+ -- Active-oneof resolution mirrors codec.encode_message: a oneof
+ -- field is only emitted if it's the active branch.
local active
local oneofs_list = self._desc.oneofs_list
if oneofs_list then
@@ 669,7 635,8 @@ function MessageView:encode()
local members = oo.members
for j = 1, #members do
local fname = members[j]
- if dirty[fname] or by_id[self._desc.field_by_name[fname].id] then
+ if dirty[fname]
+ or by_id[self._desc.field_by_name[fname].id] then
active[oo.name] = fname
end
end
@@ 690,28 657,25 @@ function MessageView:encode()
end
if f.oneof and active and active[f.oneof] ~= fname then
- -- Inactive oneof branch: skip entirely.
+ -- Inactive oneof branch: skip.
elseif is_dirty or is_sub_dirty then
- local v = cache[fname]
- v = materialize(v)
- codec.encode_field(f, v, out, f.optional or (f.oneof ~= nil))
+ codec.encode_field(f, materialize(cache[fname]), out,
+ f.optional or (f.oneof ~= nil))
elseif by_id[f.id] then
- local segs = by_id[f.id]
- for j = 1, #segs do
- local seg = segs[j]
- out[#out + 1] = bytes:sub(seg.tag_start, seg.next_start - 1)
+ local idx_list = by_id[f.id]
+ for j = 1, #idx_list do
+ local idx = idx_list[j]
+ out[#out + 1] = bytes:sub(segs.tag_start[idx],
+ segs.next_start[idx] - 1)
end
end
end
- -- Unknown segments preserved at the end (matches codec's _unknown_fields
- -- trailer convention).
+ -- Unknown segments preserved at the end.
local fbi = self._desc.field_by_id
- local segments = self._segments
- for i = 1, #segments do
- local seg = segments[i]
- if fbi[seg.id] == nil then
- out[#out + 1] = bytes:sub(seg.tag_start, seg.next_start - 1)
+ for i = 1, segs.n do
+ if fbi[segs.id[i]] == nil then
+ out[#out + 1] = bytes:sub(segs.tag_start[i], segs.next_start[i] - 1)
end
end