M .beads/interactions.jsonl => .beads/interactions.jsonl +1 -0
@@ 22,3 22,4 @@
{"id":"int-59f687bd","kind":"field_change","created_at":"2026-05-23T09:41:30.983997Z","actor":"Eugene Blikh","issue_id":"tarantool-protobuf-y1n","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"C-side singular-scalar encode landed in runtime/pb/c/c_runtime.c. New entry pb.c_runtime.encode(plan, msg) -> string. Buffer strategy: 4KB stack scratch promoted to lua_newuserdata on overflow — mid-encode luaL_error doesn't leak (userdata GC'd at unwind). Covered kinds: int32/64, uint32/64, sint32/64, bool, fixed32/64, sfixed32/64, float, double, enum (number/string lookup via field->enum_ref's by_name), string, bytes. Repeated/map/message silently skipped (3d/3e scope). Proto3 zero-suppression mirrors mode=full exactly: empty strings, zero ints/fixed, +0.0 double skipped; -0.0 emitted via type-pun byte equality (matches Lua's 1/v == -math.huge guard); proto3-optional fields bypass suppression. WKT override plans rejected (3k/bd-rmf scope). New test test/c_runtime_encode_test.lua: 16 cases per mode (full + runtime) = 32 tests covering acceptance (Person {name='x', age=42, balance=-7, user_id=0xDEADBEEFCAFEBABEULL, weight_kg=3.14} byte-equal to mode=full), per-kind sweep, edge cases (proto3-optional empty string, -0.0 double, enum-string lookup + unknown-name error, cdata uint64 fixed64, heap-buffer grow at 8KB string, repeated/message-skip silent). Full suite: 806/806 with PB_ENABLE_C=1, 748/748 + 58 skipped without (silent fallback intact). Unblocks 3d/3e/3g/3i/3j."}}
{"id":"int-32f1e096","kind":"field_change","created_at":"2026-05-23T10:00:25.652311Z","actor":"Eugene Blikh","issue_id":"tarantool-protobuf-mz6","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"C-side singular scalar/enum/string/bytes decode. pb.c_runtime.decode(plan, bytes) -> table mirrors mode=full output: int64 family pushes cdata via luaL_pushint64/pushuint64, others Lua number/string/boolean. Linear field_number scan over plan->fields per tag; repeated/map/message and unknown tags skipped by wire type — 3d/3e/3i extend later. 26 tests pass (full + runtime modes); full suite 832/832 with PB_ENABLE_C=1, 748/748+84 skipped without."}}
{"id":"int-634d0065","kind":"field_change","created_at":"2026-05-23T10:24:19.417709Z","actor":"Eugene Blikh","issue_id":"tarantool-protobuf-hwe","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"ra6 3d: sub-message encode/decode (recursion) — refactored encode_lua/decode_lua into reusable encode_body/decode_body, added singular sub-message encode (force-promoted parent heap_idx before recursion to make lua_settop safe) and decode (temporarily shrunk c->len for bounded inner read). 22 new tests cover round-trip, empty sub-message presence, proto3-optional fields, 5-level depth (new test/proto/c_nested.proto), parent-buffer regrow path, truncated-input errors. Full suite 854/854 with PB_ENABLE_C=1."}}
+{"id":"int-869e5402","kind":"field_change","created_at":"2026-05-23T10:39:55.349195Z","actor":"Eugene Blikh","issue_id":"tarantool-protobuf-jc9","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
M .beads/issues.jsonl => .beads/issues.jsonl +1 -1
@@ 1,5 1,5 @@
{"_type":"issue","id":"tarantool-protobuf-exy","title":"ra6 3f: repeated strings + repeated messages (cached stack-idx)","description":"Repeated string fields and repeated message fields. Encode: iterate the Lua array via cached stack idx, write each element (string field or recursive submsg encode). Decode: first hit lazy-creates the result array and stashes its stack index in a per-field slot (sized by plan-\u003en_fields, cap 16 for the spike fixed-size); subsequent hits lua_rawseti directly without re-lookup; one lua_setfield at decode_message exit attaches the array to the result table. This is the SPIKE-VALIDATED PATTERN — naive lazy-getfield is 2x slower at 100KB. Depends on 3d (needs sub-message machinery for repeated messages). Acceptance: Person.emails (repeated string) and Person.friends (repeated Person, self-reference) both round-trip byte-equal to mode=full at 1KB, 10KB, 100KB sizes.","status":"open","priority":1,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-18T20:20:52Z","created_by":"Eugene Blikh","updated_at":"2026-05-18T20:20:52Z","dependencies":[{"issue_id":"tarantool-protobuf-exy","depends_on_id":"tarantool-protobuf-hwe","type":"blocks","created_at":"2026-05-18T23:22:18Z","created_by":"Eugene Blikh","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
-{"_type":"issue","id":"tarantool-protobuf-jc9","title":"ra6 3e: repeated/packed scalar encode/decode","description":"Repeated scalar fields: encode-time, walk the Lua array via cached stack idx; decode-time, lazy-create the array on first hit and append via lua_rawseti with a cached stack idx (NOT lazy lua_getfield per element — that's 2x slower at 100KB per the spike). Packed repeated: length-prefixed varint blob; tight loop in C. Covers int32/sint32/uint32/int64/uint64/fixed32/fixed64/double/bool/enum. Depends on 3b + 3c (need the scalar primitives). Acceptance: Person.lucky_numbers (packed int32) round-trips byte-equal; a fixture with both packed and unpacked repeated scalars round-trips against mode=full at 10/100/1000-element counts.","status":"open","priority":1,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-18T20:20:33Z","created_by":"Eugene Blikh","updated_at":"2026-05-18T20:20:33Z","dependencies":[{"issue_id":"tarantool-protobuf-jc9","depends_on_id":"tarantool-protobuf-mz6","type":"blocks","created_at":"2026-05-18T23:22:07Z","created_by":"Eugene Blikh","metadata":"{}"},{"issue_id":"tarantool-protobuf-jc9","depends_on_id":"tarantool-protobuf-y1n","type":"blocks","created_at":"2026-05-18T23:22:07Z","created_by":"Eugene Blikh","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0}
+{"_type":"issue","id":"tarantool-protobuf-jc9","title":"ra6 3e: repeated/packed scalar encode/decode","description":"Repeated scalar fields: encode-time, walk the Lua array via cached stack idx; decode-time, lazy-create the array on first hit and append via lua_rawseti with a cached stack idx (NOT lazy lua_getfield per element — that's 2x slower at 100KB per the spike). Packed repeated: length-prefixed varint blob; tight loop in C. Covers int32/sint32/uint32/int64/uint64/fixed32/fixed64/double/bool/enum. Depends on 3b + 3c (need the scalar primitives). Acceptance: Person.lucky_numbers (packed int32) round-trips byte-equal; a fixture with both packed and unpacked repeated scalars round-trips against mode=full at 10/100/1000-element counts.","status":"closed","priority":1,"issue_type":"task","assignee":"Eugene Blikh","owner":"bigbes@gmail.com","created_at":"2026-05-18T20:20:33Z","created_by":"Eugene Blikh","updated_at":"2026-05-23T10:39:55Z","started_at":"2026-05-23T10:29:02Z","closed_at":"2026-05-23T10:39:55Z","close_reason":"Closed","dependencies":[{"issue_id":"tarantool-protobuf-jc9","depends_on_id":"tarantool-protobuf-mz6","type":"blocks","created_at":"2026-05-18T23:22:07Z","created_by":"Eugene Blikh","metadata":"{}"},{"issue_id":"tarantool-protobuf-jc9","depends_on_id":"tarantool-protobuf-y1n","type":"blocks","created_at":"2026-05-18T23:22:07Z","created_by":"Eugene Blikh","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0}
{"_type":"issue","id":"tarantool-protobuf-hwe","title":"ra6 3d: sub-message encode/decode (recursion)","description":"Nested-message support. Encode: open a sub-buffer, recurse with the submsg plan, prefix parent buffer with tag + length. Decode: bound the byte range, recurse, lua_setfield the resulting table. Sub-buffer stack-backed (4KB) and malloc-promoted, same pattern as the outer buffer. Recursion is by C function call into the same encode/decode routine with a different plan, so depth is bounded by C stack. Depends on 3b + 3c. Acceptance: Person.address (1-level) round-trips; deeper nesting via a 5-level test fixture round-trips byte-equal to mode=full.","status":"closed","priority":1,"issue_type":"task","assignee":"Eugene Blikh","owner":"bigbes@gmail.com","created_at":"2026-05-18T20:20:24Z","created_by":"Eugene Blikh","updated_at":"2026-05-23T10:24:19Z","started_at":"2026-05-23T10:11:41Z","closed_at":"2026-05-23T10:24:19Z","close_reason":"ra6 3d: sub-message encode/decode (recursion) — refactored encode_lua/decode_lua into reusable encode_body/decode_body, added singular sub-message encode (force-promoted parent heap_idx before recursion to make lua_settop safe) and decode (temporarily shrunk c-\u003elen for bounded inner read). 22 new tests cover round-trip, empty sub-message presence, proto3-optional fields, 5-level depth (new test/proto/c_nested.proto), parent-buffer regrow path, truncated-input errors. Full suite 854/854 with PB_ENABLE_C=1.","dependencies":[{"issue_id":"tarantool-protobuf-hwe","depends_on_id":"tarantool-protobuf-mz6","type":"blocks","created_at":"2026-05-18T23:22:06Z","created_by":"Eugene Blikh","metadata":"{}"},{"issue_id":"tarantool-protobuf-hwe","depends_on_id":"tarantool-protobuf-y1n","type":"blocks","created_at":"2026-05-18T23:22:05Z","created_by":"Eugene Blikh","metadata":"{}"}],"dependency_count":2,"dependent_count":4,"comment_count":0}
{"_type":"issue","id":"tarantool-protobuf-awv","title":"ra6 3l: 64-bit cdata fidelity","description":"int64/uint64/fixed64/sfixed64 must round-trip as LuaJIT cdata in both encode and decode — never narrowed to a Lua double. Same convention as msgpackffi, net.box, box.tuple, the built-in protobuf. Use luaT_pushuint64/luaT_pushint64 from Tarantool's module.h for the C side. Independent transverse concern — can land in parallel with 3b/3c since it's a per-kind fix rather than a new pipeline stage. Acceptance: a value \u003e 2^53 round-trips byte-equal and remains cdata after decode; luatest test/protobuf_test.lua int64-cdata cases pass with PB_ENABLE_C=1.","status":"open","priority":1,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-18T20:20:10Z","created_by":"Eugene Blikh","updated_at":"2026-05-18T20:20:10Z","dependencies":[{"issue_id":"tarantool-protobuf-awv","depends_on_id":"tarantool-protobuf-mq7","type":"blocks","created_at":"2026-05-18T23:21:55Z","created_by":"Eugene Blikh","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
{"_type":"issue","id":"tarantool-protobuf-mz6","title":"ra6 3c: decode scalars","description":"C-side decode for scalar wire types. Pre-sized result table via lua_createtable(0, n_fields) using descriptor stats; per-field plan-driven dispatch on wire type and kind; cached luaL_ref-based lua_setfield. Wire→table output must equal mode=full Lua output for the same input bytes. Depends on 3a. Acceptance: Person decode round-trip works for the bytes produced by 3b; the decoded table is shape-identical to pure-Lua decode (verified by running test/interop tests with PB_ENABLE_C=1).","status":"closed","priority":1,"issue_type":"task","assignee":"Eugene Blikh","owner":"bigbes@gmail.com","created_at":"2026-05-18T20:20:06Z","created_by":"Eugene Blikh","updated_at":"2026-05-23T10:00:26Z","started_at":"2026-05-23T09:54:01Z","closed_at":"2026-05-23T10:00:26Z","close_reason":"C-side singular scalar/enum/string/bytes decode. pb.c_runtime.decode(plan, bytes) -\u003e table mirrors mode=full output: int64 family pushes cdata via luaL_pushint64/pushuint64, others Lua number/string/boolean. Linear field_number scan over plan-\u003efields per tag; repeated/map/message and unknown tags skipped by wire type — 3d/3e/3i extend later. 26 tests pass (full + runtime modes); full suite 832/832 with PB_ENABLE_C=1, 748/748+84 skipped without.","dependencies":[{"issue_id":"tarantool-protobuf-mz6","depends_on_id":"tarantool-protobuf-mq7","type":"blocks","created_at":"2026-05-18T23:21:54Z","created_by":"Eugene Blikh","metadata":"{}"}],"dependency_count":1,"dependent_count":6,"comment_count":0}
A examples/expected/full/c_repeated/c_repeated_pb.lua => examples/expected/full/c_repeated/c_repeated_pb.lua +737 -0
@@ 0,0 1,737 @@
+-- Code generated by protoc-gen-tarantool. DO NOT EDIT.
+-- source: c_repeated.proto
+-- syntax: proto3
+-- package: c_repeated
+
+local pb = require("pb")
+local wire = pb.wire
+local string_byte = string.byte
+local band = bit.band
+local rshift = bit.rshift
+
+local M = {}
+
+M.options = {go_package = "tarantoolpb_synthetic/c_repeated"}
+
+-- Pre-declare message descriptors so cross-references resolve.
+M.Inner_descriptor = {name = "c_repeated.Inner"}
+M.Holder_descriptor = {name = "c_repeated.Holder"}
+
+-- Message: c_repeated.Inner
+M.Inner_descriptor.fields = {
+ {name="v", id=1, kind='scalar', proto_type="int32"},
+ {name="s", id=2, kind='scalar', proto_type="string"},
+}
+pb.finalize_message(M.Inner_descriptor)
+M.Inner_fields = pb.field_names({
+ v = "v",
+ s = "s",
+})
+
+-- Message: c_repeated.Holder
+M.Holder_descriptor.fields = {
+ {name="packed_int32", id=1, kind='scalar', proto_type="int32", repeated=true, packed=true},
+ {name="packed_int64", id=2, kind='scalar', proto_type="int64", repeated=true, packed=true},
+ {name="packed_sint32", id=3, kind='scalar', proto_type="sint32", repeated=true, packed=true},
+ {name="packed_uint32", id=4, kind='scalar', proto_type="uint32", repeated=true, packed=true},
+ {name="packed_fixed32", id=5, kind='scalar', proto_type="fixed32", repeated=true, packed=true},
+ {name="packed_fixed64", id=6, kind='scalar', proto_type="fixed64", repeated=true, packed=true},
+ {name="packed_double", id=7, kind='scalar', proto_type="double", repeated=true, packed=true},
+ {name="packed_float", id=8, kind='scalar', proto_type="float", repeated=true, packed=true},
+ {name="packed_bool", id=9, kind='scalar', proto_type="bool", repeated=true, packed=true},
+ {name="unpacked_int32", id=20, kind='scalar', proto_type="int32", repeated=true, packed=false, options={packed = false}},
+ {name="unpacked_fixed64", id=21, kind='scalar', proto_type="fixed64", repeated=true, packed=false, options={packed = false}},
+ {name="unpacked_sint32", id=22, kind='scalar', proto_type="sint32", repeated=true, packed=false, options={packed = false}},
+ {name="strings", id=30, kind='scalar', proto_type="string", repeated=true},
+ {name="blobs", id=31, kind='scalar', proto_type="bytes", repeated=true},
+ {name="messages", id=40, kind='message', message=M.Inner_descriptor, repeated=true},
+}
+pb.finalize_message(M.Holder_descriptor)
+M.Holder_fields = pb.field_names({
+ packed_int32 = "packed_int32",
+ packed_int64 = "packed_int64",
+ packed_sint32 = "packed_sint32",
+ packed_uint32 = "packed_uint32",
+ packed_fixed32 = "packed_fixed32",
+ packed_fixed64 = "packed_fixed64",
+ packed_double = "packed_double",
+ packed_float = "packed_float",
+ packed_bool = "packed_bool",
+ unpacked_int32 = "unpacked_int32",
+ unpacked_fixed64 = "unpacked_fixed64",
+ unpacked_sint32 = "unpacked_sint32",
+ strings = "strings",
+ blobs = "blobs",
+ messages = "messages",
+})
+
+-- EmmyLua / lua-language-server type annotations.
+-- These are comments — no runtime effect. They give editors
+-- autocomplete and type-checking for the generated wrappers.
+---@class c_repeated.Inner
+---@field v integer
+---@field s string
+
+---@class c_repeated.Holder
+---@field packed_int32 integer[] @ Packed scalars (proto3 default). One per encoder branch.
+---@field packed_int64 integer[]
+---@field packed_sint32 integer[]
+---@field packed_uint32 integer[]
+---@field packed_fixed32 integer[]
+---@field packed_fixed64 integer[]
+---@field packed_double number[]
+---@field packed_float number[]
+---@field packed_bool boolean[]
+---@field unpacked_int32 integer[] @ Explicit unpacked — emits one tag per element.
+---@field unpacked_fixed64 integer[]
+---@field unpacked_sint32 integer[]
+---@field strings string[] @ Length-delimited element kinds — never packable.
+---@field blobs string[]
+---@field messages c_repeated.Inner[] @ Repeated sub-message — per-element tag + length-prefix + body.
+
+---@param t? c_repeated.Inner
+---@return c_repeated.Inner
+function M.Inner_new(t) return t or {} end
+
+---@param t c_repeated.Inner
+---@return string
+function M.Inner_encode(t)
+ if type(t) ~= 'table' then
+ error("expected table for c_repeated.Inner, got " .. type(t), 0)
+ end
+ local out, n = {}, 0
+ local v
+ -- field 1: v
+ v = t.v
+ if v ~= nil and v ~= 0 then
+ n = n + 1; out[n] = "\x08"
+ n = n + 1; out[n] = wire.encode_int32(v)
+ end
+ -- field 2: s
+ v = t.s
+ if v ~= nil and v ~= '' then
+ n = n + 1; out[n] = "\x12"
+ local _len = #v
+ if _len < 128 then
+ n = n + 1; out[n] = string.char(_len)
+ else
+ n = n + 1; out[n] = wire.encode_varint(_len)
+ end
+ n = n + 1; out[n] = v
+ end
+ local _exts = t._extensions
+ if _exts ~= nil then
+ local _elist = M.Inner_descriptor.extensions_list
+ if _elist ~= nil then
+ for _i = 1, #_elist do
+ local _ext = _elist[_i]
+ local _ev = _exts[_ext.full_name]
+ if _ev ~= nil then
+ pb.codec.encode_field(_ext, _ev, out, true)
+ end
+ end
+ n = #out
+ end
+ end
+ local _uf = t._unknown_fields
+ if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
+ return table.concat(out)
+end
+
+---@param b string
+---@return c_repeated.Inner
+function M.Inner_decode(buf)
+ if type(buf) ~= 'string' then
+ error("expected string for c_repeated.Inner decode, got " .. type(buf), 0)
+ end
+ local result = {}
+ local pos, len = 1, #buf
+ local _uf
+ while pos <= len do
+ local _tag_start = pos
+ local id, wt
+ local _b = string_byte(buf, pos)
+ if _b ~= nil and _b < 0x80 then
+ wt = band(_b, 7)
+ if wt >= 6 then error("illegal wire type " .. wt, 0) end
+ id = rshift(_b, 3)
+ if id == 0 then error("illegal field number 0", 0) end
+ pos = pos + 1
+ else
+ id, wt, pos = wire.decode_tag(buf, pos)
+ end
+ if id == 1 then
+ local val
+ val, pos = wire.decode_int32(buf, pos)
+ result.v = val
+ elseif id == 2 then
+ local val
+ val, pos = wire.decode_string(buf, pos)
+ result.s = val
+ else
+ local _ebid = M.Inner_descriptor.extensions_by_id
+ local _ext = _ebid and _ebid[id] or nil
+ if _ext ~= nil then
+ pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
+ else
+ pos = wire.skip_field(buf, pos, wt, id)
+ if _uf == nil then _uf = {} end
+ _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
+ end
+ end
+ end
+ if _uf ~= nil then result._unknown_fields = table.concat(_uf) end
+ return result
+end
+
+---@param b string
+---@return pb.MessageView
+function M.Inner_decode_lazy(b) return pb.decode_lazy(M.Inner_descriptor, b) end
+---@param t c_repeated.Inner
+---@param opts? {single_line: boolean?, indent: string?}
+---@return string
+function M.Inner_text(t, opts) return pb.text.encode(M.Inner_descriptor, t, opts) end
+
+---@param t? c_repeated.Holder
+---@return c_repeated.Holder
+function M.Holder_new(t) return t or {} end
+
+---@param t c_repeated.Holder
+---@return string
+function M.Holder_encode(t)
+ if type(t) ~= 'table' then
+ error("expected table for c_repeated.Holder, got " .. type(t), 0)
+ end
+ local out, n = {}, 0
+ local v
+ -- field 1: packed_int32
+ v = t.packed_int32
+ if v ~= nil and #v > 0 then
+ local parts, m = {}, 0
+ for _i = 1, #v do
+ m = m + 1; parts[m] = wire.encode_int32(v[_i])
+ end
+ local _b = table.concat(parts)
+ n = n + 1; out[n] = "\x0a"
+ local _len = #_b
+ if _len < 128 then
+ n = n + 1; out[n] = string.char(_len)
+ else
+ n = n + 1; out[n] = wire.encode_varint(_len)
+ end
+ n = n + 1; out[n] = _b
+ end
+ -- field 2: packed_int64
+ v = t.packed_int64
+ if v ~= nil and #v > 0 then
+ local parts, m = {}, 0
+ for _i = 1, #v do
+ m = m + 1; parts[m] = wire.encode_int64(v[_i])
+ end
+ local _b = table.concat(parts)
+ n = n + 1; out[n] = "\x12"
+ local _len = #_b
+ if _len < 128 then
+ n = n + 1; out[n] = string.char(_len)
+ else
+ n = n + 1; out[n] = wire.encode_varint(_len)
+ end
+ n = n + 1; out[n] = _b
+ end
+ -- field 3: packed_sint32
+ v = t.packed_sint32
+ if v ~= nil and #v > 0 then
+ local parts, m = {}, 0
+ for _i = 1, #v do
+ m = m + 1; parts[m] = wire.encode_sint32(v[_i])
+ end
+ local _b = table.concat(parts)
+ n = n + 1; out[n] = "\x1a"
+ local _len = #_b
+ if _len < 128 then
+ n = n + 1; out[n] = string.char(_len)
+ else
+ n = n + 1; out[n] = wire.encode_varint(_len)
+ end
+ n = n + 1; out[n] = _b
+ end
+ -- field 4: packed_uint32
+ v = t.packed_uint32
+ if v ~= nil and #v > 0 then
+ local parts, m = {}, 0
+ for _i = 1, #v do
+ m = m + 1; parts[m] = wire.encode_uint32(v[_i])
+ end
+ local _b = table.concat(parts)
+ n = n + 1; out[n] = "\x22"
+ local _len = #_b
+ if _len < 128 then
+ n = n + 1; out[n] = string.char(_len)
+ else
+ n = n + 1; out[n] = wire.encode_varint(_len)
+ end
+ n = n + 1; out[n] = _b
+ end
+ -- field 5: packed_fixed32
+ v = t.packed_fixed32
+ if v ~= nil and #v > 0 then
+ local parts, m = {}, 0
+ for _i = 1, #v do
+ m = m + 1; parts[m] = wire.encode_fixed32(v[_i])
+ end
+ local _b = table.concat(parts)
+ n = n + 1; out[n] = "\x2a"
+ local _len = #_b
+ if _len < 128 then
+ n = n + 1; out[n] = string.char(_len)
+ else
+ n = n + 1; out[n] = wire.encode_varint(_len)
+ end
+ n = n + 1; out[n] = _b
+ end
+ -- field 6: packed_fixed64
+ v = t.packed_fixed64
+ if v ~= nil and #v > 0 then
+ local parts, m = {}, 0
+ for _i = 1, #v do
+ m = m + 1; parts[m] = wire.encode_fixed64(v[_i])
+ end
+ local _b = table.concat(parts)
+ n = n + 1; out[n] = "\x32"
+ local _len = #_b
+ if _len < 128 then
+ n = n + 1; out[n] = string.char(_len)
+ else
+ n = n + 1; out[n] = wire.encode_varint(_len)
+ end
+ n = n + 1; out[n] = _b
+ end
+ -- field 7: packed_double
+ v = t.packed_double
+ if v ~= nil and #v > 0 then
+ local parts, m = {}, 0
+ for _i = 1, #v do
+ m = m + 1; parts[m] = wire.encode_double(v[_i])
+ end
+ local _b = table.concat(parts)
+ n = n + 1; out[n] = "\x3a"
+ local _len = #_b
+ if _len < 128 then
+ n = n + 1; out[n] = string.char(_len)
+ else
+ n = n + 1; out[n] = wire.encode_varint(_len)
+ end
+ n = n + 1; out[n] = _b
+ end
+ -- field 8: packed_float
+ v = t.packed_float
+ if v ~= nil and #v > 0 then
+ local parts, m = {}, 0
+ for _i = 1, #v do
+ m = m + 1; parts[m] = wire.encode_float(v[_i])
+ end
+ local _b = table.concat(parts)
+ n = n + 1; out[n] = "\x42"
+ local _len = #_b
+ if _len < 128 then
+ n = n + 1; out[n] = string.char(_len)
+ else
+ n = n + 1; out[n] = wire.encode_varint(_len)
+ end
+ n = n + 1; out[n] = _b
+ end
+ -- field 9: packed_bool
+ v = t.packed_bool
+ if v ~= nil and #v > 0 then
+ local parts, m = {}, 0
+ for _i = 1, #v do
+ m = m + 1; parts[m] = wire.encode_bool(v[_i])
+ end
+ local _b = table.concat(parts)
+ n = n + 1; out[n] = "\x4a"
+ local _len = #_b
+ if _len < 128 then
+ n = n + 1; out[n] = string.char(_len)
+ else
+ n = n + 1; out[n] = wire.encode_varint(_len)
+ end
+ n = n + 1; out[n] = _b
+ end
+ -- field 20: unpacked_int32
+ v = t.unpacked_int32
+ if v ~= nil and #v > 0 then
+ local _tag = "\xa0\x01"
+ for _i = 1, #v do
+ n = n + 1; out[n] = _tag
+ n = n + 1; out[n] = wire.encode_int32(v[_i])
+ end
+ end
+ -- field 21: unpacked_fixed64
+ v = t.unpacked_fixed64
+ if v ~= nil and #v > 0 then
+ local _tag = "\xa9\x01"
+ for _i = 1, #v do
+ n = n + 1; out[n] = _tag
+ n = n + 1; out[n] = wire.encode_fixed64(v[_i])
+ end
+ end
+ -- field 22: unpacked_sint32
+ v = t.unpacked_sint32
+ if v ~= nil and #v > 0 then
+ local _tag = "\xb0\x01"
+ for _i = 1, #v do
+ n = n + 1; out[n] = _tag
+ n = n + 1; out[n] = wire.encode_sint32(v[_i])
+ end
+ end
+ -- field 30: strings
+ v = t.strings
+ if v ~= nil and #v > 0 then
+ local _tag = "\xf2\x01"
+ for _i = 1, #v do
+ local _b = v[_i]
+ n = n + 1; out[n] = _tag
+ local _len = #_b
+ if _len < 128 then
+ n = n + 1; out[n] = string.char(_len)
+ else
+ n = n + 1; out[n] = wire.encode_varint(_len)
+ end
+ n = n + 1; out[n] = _b
+ end
+ end
+ -- field 31: blobs
+ v = t.blobs
+ if v ~= nil and #v > 0 then
+ local _tag = "\xfa\x01"
+ for _i = 1, #v do
+ local _b = v[_i]
+ n = n + 1; out[n] = _tag
+ local _len = #_b
+ if _len < 128 then
+ n = n + 1; out[n] = string.char(_len)
+ else
+ n = n + 1; out[n] = wire.encode_varint(_len)
+ end
+ n = n + 1; out[n] = _b
+ end
+ end
+ -- field 40: messages
+ v = t.messages
+ if v ~= nil and #v > 0 then
+ local _tag = "\xc2\x02"
+ for _i = 1, #v do
+ local _b = M.Inner_encode(v[_i])
+ n = n + 1; out[n] = _tag
+ local _len = #_b
+ if _len < 128 then
+ n = n + 1; out[n] = string.char(_len)
+ else
+ n = n + 1; out[n] = wire.encode_varint(_len)
+ end
+ n = n + 1; out[n] = _b
+ end
+ end
+ local _exts = t._extensions
+ if _exts ~= nil then
+ local _elist = M.Holder_descriptor.extensions_list
+ if _elist ~= nil then
+ for _i = 1, #_elist do
+ local _ext = _elist[_i]
+ local _ev = _exts[_ext.full_name]
+ if _ev ~= nil then
+ pb.codec.encode_field(_ext, _ev, out, true)
+ end
+ end
+ n = #out
+ end
+ end
+ local _uf = t._unknown_fields
+ if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
+ return table.concat(out)
+end
+
+---@param b string
+---@return c_repeated.Holder
+function M.Holder_decode(buf)
+ if type(buf) ~= 'string' then
+ error("expected string for c_repeated.Holder decode, got " .. type(buf), 0)
+ end
+ local result = {}
+ local pos, len = 1, #buf
+ local _uf
+ local _n_packed_int32 = 0
+ local _n_packed_int64 = 0
+ local _n_packed_sint32 = 0
+ local _n_packed_uint32 = 0
+ local _n_packed_fixed32 = 0
+ local _n_packed_fixed64 = 0
+ local _n_packed_double = 0
+ local _n_packed_float = 0
+ local _n_packed_bool = 0
+ local _n_unpacked_int32 = 0
+ local _n_unpacked_fixed64 = 0
+ local _n_unpacked_sint32 = 0
+ local _n_strings = 0
+ local _n_blobs = 0
+ local _n_messages = 0
+ while pos <= len do
+ local _tag_start = pos
+ local id, wt
+ local _b = string_byte(buf, pos)
+ if _b ~= nil and _b < 0x80 then
+ wt = band(_b, 7)
+ if wt >= 6 then error("illegal wire type " .. wt, 0) end
+ id = rshift(_b, 3)
+ if id == 0 then error("illegal field number 0", 0) end
+ pos = pos + 1
+ else
+ id, wt, pos = wire.decode_tag(buf, pos)
+ end
+ if id == 1 then
+ local list = result.packed_int32
+ if list == nil then list = {}; result.packed_int32 = list end
+ if wt == 2 then
+ local payload
+ payload, pos = wire.decode_len(buf, pos)
+ local p2, lim = 1, #payload
+ while p2 <= lim do
+ local val
+ val, p2 = wire.decode_int32(payload, p2)
+ _n_packed_int32 = _n_packed_int32 + 1; list[_n_packed_int32] = val
+ end
+ else
+ local val
+ val, pos = wire.decode_int32(buf, pos)
+ _n_packed_int32 = _n_packed_int32 + 1; list[_n_packed_int32] = val
+ end
+ elseif id == 2 then
+ local list = result.packed_int64
+ if list == nil then list = {}; result.packed_int64 = list end
+ if wt == 2 then
+ local payload
+ payload, pos = wire.decode_len(buf, pos)
+ local p2, lim = 1, #payload
+ while p2 <= lim do
+ local val
+ val, p2 = wire.decode_int64(payload, p2)
+ _n_packed_int64 = _n_packed_int64 + 1; list[_n_packed_int64] = val
+ end
+ else
+ local val
+ val, pos = wire.decode_int64(buf, pos)
+ _n_packed_int64 = _n_packed_int64 + 1; list[_n_packed_int64] = val
+ end
+ elseif id == 3 then
+ local list = result.packed_sint32
+ if list == nil then list = {}; result.packed_sint32 = list end
+ if wt == 2 then
+ local payload
+ payload, pos = wire.decode_len(buf, pos)
+ local p2, lim = 1, #payload
+ while p2 <= lim do
+ local val
+ val, p2 = wire.decode_sint32(payload, p2)
+ _n_packed_sint32 = _n_packed_sint32 + 1; list[_n_packed_sint32] = val
+ end
+ else
+ local val
+ val, pos = wire.decode_sint32(buf, pos)
+ _n_packed_sint32 = _n_packed_sint32 + 1; list[_n_packed_sint32] = val
+ end
+ elseif id == 4 then
+ local list = result.packed_uint32
+ if list == nil then list = {}; result.packed_uint32 = list end
+ if wt == 2 then
+ local payload
+ payload, pos = wire.decode_len(buf, pos)
+ local p2, lim = 1, #payload
+ while p2 <= lim do
+ local val
+ val, p2 = wire.decode_uint32(payload, p2)
+ _n_packed_uint32 = _n_packed_uint32 + 1; list[_n_packed_uint32] = val
+ end
+ else
+ local val
+ val, pos = wire.decode_uint32(buf, pos)
+ _n_packed_uint32 = _n_packed_uint32 + 1; list[_n_packed_uint32] = val
+ end
+ elseif id == 5 then
+ local list = result.packed_fixed32
+ if list == nil then list = {}; result.packed_fixed32 = list end
+ if wt == 2 then
+ local payload
+ payload, pos = wire.decode_len(buf, pos)
+ local p2, lim = 1, #payload
+ while p2 <= lim do
+ local val
+ val, p2 = wire.decode_fixed32(payload, p2)
+ _n_packed_fixed32 = _n_packed_fixed32 + 1; list[_n_packed_fixed32] = val
+ end
+ else
+ local val
+ val, pos = wire.decode_fixed32(buf, pos)
+ _n_packed_fixed32 = _n_packed_fixed32 + 1; list[_n_packed_fixed32] = val
+ end
+ elseif id == 6 then
+ local list = result.packed_fixed64
+ if list == nil then list = {}; result.packed_fixed64 = list end
+ if wt == 2 then
+ local payload
+ payload, pos = wire.decode_len(buf, pos)
+ local p2, lim = 1, #payload
+ while p2 <= lim do
+ local val
+ val, p2 = wire.decode_fixed64(payload, p2)
+ _n_packed_fixed64 = _n_packed_fixed64 + 1; list[_n_packed_fixed64] = val
+ end
+ else
+ local val
+ val, pos = wire.decode_fixed64(buf, pos)
+ _n_packed_fixed64 = _n_packed_fixed64 + 1; list[_n_packed_fixed64] = val
+ end
+ elseif id == 7 then
+ local list = result.packed_double
+ if list == nil then list = {}; result.packed_double = list end
+ if wt == 2 then
+ local payload
+ payload, pos = wire.decode_len(buf, pos)
+ local p2, lim = 1, #payload
+ while p2 <= lim do
+ local val
+ val, p2 = wire.decode_double(payload, p2)
+ _n_packed_double = _n_packed_double + 1; list[_n_packed_double] = val
+ end
+ else
+ local val
+ val, pos = wire.decode_double(buf, pos)
+ _n_packed_double = _n_packed_double + 1; list[_n_packed_double] = val
+ end
+ elseif id == 8 then
+ local list = result.packed_float
+ if list == nil then list = {}; result.packed_float = list end
+ if wt == 2 then
+ local payload
+ payload, pos = wire.decode_len(buf, pos)
+ local p2, lim = 1, #payload
+ while p2 <= lim do
+ local val
+ val, p2 = wire.decode_float(payload, p2)
+ _n_packed_float = _n_packed_float + 1; list[_n_packed_float] = val
+ end
+ else
+ local val
+ val, pos = wire.decode_float(buf, pos)
+ _n_packed_float = _n_packed_float + 1; list[_n_packed_float] = val
+ end
+ elseif id == 9 then
+ local list = result.packed_bool
+ if list == nil then list = {}; result.packed_bool = list end
+ if wt == 2 then
+ local payload
+ payload, pos = wire.decode_len(buf, pos)
+ local p2, lim = 1, #payload
+ while p2 <= lim do
+ local val
+ val, p2 = wire.decode_bool(payload, p2)
+ _n_packed_bool = _n_packed_bool + 1; list[_n_packed_bool] = val
+ end
+ else
+ local val
+ val, pos = wire.decode_bool(buf, pos)
+ _n_packed_bool = _n_packed_bool + 1; list[_n_packed_bool] = val
+ end
+ elseif id == 20 then
+ local list = result.unpacked_int32
+ if list == nil then list = {}; result.unpacked_int32 = list end
+ if wt == 2 then
+ local payload
+ payload, pos = wire.decode_len(buf, pos)
+ local p2, lim = 1, #payload
+ while p2 <= lim do
+ local val
+ val, p2 = wire.decode_int32(payload, p2)
+ _n_unpacked_int32 = _n_unpacked_int32 + 1; list[_n_unpacked_int32] = val
+ end
+ else
+ local val
+ val, pos = wire.decode_int32(buf, pos)
+ _n_unpacked_int32 = _n_unpacked_int32 + 1; list[_n_unpacked_int32] = val
+ end
+ elseif id == 21 then
+ local list = result.unpacked_fixed64
+ if list == nil then list = {}; result.unpacked_fixed64 = list end
+ if wt == 2 then
+ local payload
+ payload, pos = wire.decode_len(buf, pos)
+ local p2, lim = 1, #payload
+ while p2 <= lim do
+ local val
+ val, p2 = wire.decode_fixed64(payload, p2)
+ _n_unpacked_fixed64 = _n_unpacked_fixed64 + 1; list[_n_unpacked_fixed64] = val
+ end
+ else
+ local val
+ val, pos = wire.decode_fixed64(buf, pos)
+ _n_unpacked_fixed64 = _n_unpacked_fixed64 + 1; list[_n_unpacked_fixed64] = val
+ end
+ elseif id == 22 then
+ local list = result.unpacked_sint32
+ if list == nil then list = {}; result.unpacked_sint32 = list end
+ if wt == 2 then
+ local payload
+ payload, pos = wire.decode_len(buf, pos)
+ local p2, lim = 1, #payload
+ while p2 <= lim do
+ local val
+ val, p2 = wire.decode_sint32(payload, p2)
+ _n_unpacked_sint32 = _n_unpacked_sint32 + 1; list[_n_unpacked_sint32] = val
+ end
+ else
+ local val
+ val, pos = wire.decode_sint32(buf, pos)
+ _n_unpacked_sint32 = _n_unpacked_sint32 + 1; list[_n_unpacked_sint32] = val
+ end
+ elseif id == 30 then
+ local list = result.strings
+ if list == nil then list = {}; result.strings = list end
+ local val
+ val, pos = wire.decode_string(buf, pos)
+ _n_strings = _n_strings + 1; list[_n_strings] = val
+ elseif id == 31 then
+ local list = result.blobs
+ if list == nil then list = {}; result.blobs = list end
+ local val
+ val, pos = wire.decode_bytes(buf, pos)
+ _n_blobs = _n_blobs + 1; list[_n_blobs] = val
+ elseif id == 40 then
+ local list = result.messages
+ if list == nil then list = {}; result.messages = list end
+ local payload
+ payload, pos = wire.decode_len(buf, pos)
+ _n_messages = _n_messages + 1; list[_n_messages] = M.Inner_decode(payload)
+ else
+ local _ebid = M.Holder_descriptor.extensions_by_id
+ local _ext = _ebid and _ebid[id] or nil
+ if _ext ~= nil then
+ pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
+ else
+ pos = wire.skip_field(buf, pos, wt, id)
+ if _uf == nil then _uf = {} end
+ _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
+ end
+ end
+ end
+ if _uf ~= nil then result._unknown_fields = table.concat(_uf) end
+ return result
+end
+
+---@param b string
+---@return pb.MessageView
+function M.Holder_decode_lazy(b) return pb.decode_lazy(M.Holder_descriptor, b) end
+---@param t c_repeated.Holder
+---@param opts? {single_line: boolean?, indent: string?}
+---@return string
+function M.Holder_text(t, opts) return pb.text.encode(M.Holder_descriptor, t, opts) end
+
+return M
A examples/expected/runtime/c_repeated/c_repeated_pb.lua => examples/expected/runtime/c_repeated/c_repeated_pb.lua +126 -0
@@ 0,0 1,126 @@
+-- Code generated by protoc-gen-tarantool. DO NOT EDIT.
+-- source: c_repeated.proto
+-- syntax: proto3
+-- package: c_repeated
+
+local pb = require("pb")
+local wire = pb.wire
+local string_byte = string.byte
+local band = bit.band
+local rshift = bit.rshift
+
+local M = {}
+
+M.options = {go_package = "tarantoolpb_synthetic/c_repeated"}
+
+-- Pre-declare message descriptors so cross-references resolve.
+M.Inner_descriptor = {name = "c_repeated.Inner"}
+M.Holder_descriptor = {name = "c_repeated.Holder"}
+
+-- Message: c_repeated.Inner
+M.Inner_descriptor.fields = {
+ {name="v", id=1, kind='scalar', proto_type="int32"},
+ {name="s", id=2, kind='scalar', proto_type="string"},
+}
+pb.finalize_message(M.Inner_descriptor)
+M.Inner_fields = pb.field_names({
+ v = "v",
+ s = "s",
+})
+
+-- Message: c_repeated.Holder
+M.Holder_descriptor.fields = {
+ {name="packed_int32", id=1, kind='scalar', proto_type="int32", repeated=true, packed=true},
+ {name="packed_int64", id=2, kind='scalar', proto_type="int64", repeated=true, packed=true},
+ {name="packed_sint32", id=3, kind='scalar', proto_type="sint32", repeated=true, packed=true},
+ {name="packed_uint32", id=4, kind='scalar', proto_type="uint32", repeated=true, packed=true},
+ {name="packed_fixed32", id=5, kind='scalar', proto_type="fixed32", repeated=true, packed=true},
+ {name="packed_fixed64", id=6, kind='scalar', proto_type="fixed64", repeated=true, packed=true},
+ {name="packed_double", id=7, kind='scalar', proto_type="double", repeated=true, packed=true},
+ {name="packed_float", id=8, kind='scalar', proto_type="float", repeated=true, packed=true},
+ {name="packed_bool", id=9, kind='scalar', proto_type="bool", repeated=true, packed=true},
+ {name="unpacked_int32", id=20, kind='scalar', proto_type="int32", repeated=true, packed=false, options={packed = false}},
+ {name="unpacked_fixed64", id=21, kind='scalar', proto_type="fixed64", repeated=true, packed=false, options={packed = false}},
+ {name="unpacked_sint32", id=22, kind='scalar', proto_type="sint32", repeated=true, packed=false, options={packed = false}},
+ {name="strings", id=30, kind='scalar', proto_type="string", repeated=true},
+ {name="blobs", id=31, kind='scalar', proto_type="bytes", repeated=true},
+ {name="messages", id=40, kind='message', message=M.Inner_descriptor, repeated=true},
+}
+pb.finalize_message(M.Holder_descriptor)
+M.Holder_fields = pb.field_names({
+ packed_int32 = "packed_int32",
+ packed_int64 = "packed_int64",
+ packed_sint32 = "packed_sint32",
+ packed_uint32 = "packed_uint32",
+ packed_fixed32 = "packed_fixed32",
+ packed_fixed64 = "packed_fixed64",
+ packed_double = "packed_double",
+ packed_float = "packed_float",
+ packed_bool = "packed_bool",
+ unpacked_int32 = "unpacked_int32",
+ unpacked_fixed64 = "unpacked_fixed64",
+ unpacked_sint32 = "unpacked_sint32",
+ strings = "strings",
+ blobs = "blobs",
+ messages = "messages",
+})
+
+-- EmmyLua / lua-language-server type annotations.
+-- These are comments — no runtime effect. They give editors
+-- autocomplete and type-checking for the generated wrappers.
+---@class c_repeated.Inner
+---@field v integer
+---@field s string
+
+---@class c_repeated.Holder
+---@field packed_int32 integer[] @ Packed scalars (proto3 default). One per encoder branch.
+---@field packed_int64 integer[]
+---@field packed_sint32 integer[]
+---@field packed_uint32 integer[]
+---@field packed_fixed32 integer[]
+---@field packed_fixed64 integer[]
+---@field packed_double number[]
+---@field packed_float number[]
+---@field packed_bool boolean[]
+---@field unpacked_int32 integer[] @ Explicit unpacked — emits one tag per element.
+---@field unpacked_fixed64 integer[]
+---@field unpacked_sint32 integer[]
+---@field strings string[] @ Length-delimited element kinds — never packable.
+---@field blobs string[]
+---@field messages c_repeated.Inner[] @ Repeated sub-message — per-element tag + length-prefix + body.
+
+---@param t? c_repeated.Inner
+---@return c_repeated.Inner
+function M.Inner_new(t) return t or {} end
+---@param t c_repeated.Inner
+---@return string
+function M.Inner_encode(t) return pb.encode(M.Inner_descriptor, t) end
+---@param b string
+---@return c_repeated.Inner
+function M.Inner_decode(b) return pb.decode(M.Inner_descriptor, b) end
+---@param b string
+---@return pb.MessageView
+function M.Inner_decode_lazy(b) return pb.decode_lazy(M.Inner_descriptor, b) end
+---@param t c_repeated.Inner
+---@param opts? {single_line: boolean?, indent: string?}
+---@return string
+function M.Inner_text(t, opts) return pb.text.encode(M.Inner_descriptor, t, opts) end
+
+---@param t? c_repeated.Holder
+---@return c_repeated.Holder
+function M.Holder_new(t) return t or {} end
+---@param t c_repeated.Holder
+---@return string
+function M.Holder_encode(t) return pb.encode(M.Holder_descriptor, t) end
+---@param b string
+---@return c_repeated.Holder
+function M.Holder_decode(b) return pb.decode(M.Holder_descriptor, b) end
+---@param b string
+---@return pb.MessageView
+function M.Holder_decode_lazy(b) return pb.decode_lazy(M.Holder_descriptor, b) end
+---@param t c_repeated.Holder
+---@param opts? {single_line: boolean?, indent: string?}
+---@return string
+function M.Holder_text(t, opts) return pb.text.encode(M.Holder_descriptor, t, opts) end
+
+return M
M runtime/pb/c/c_runtime.c => runtime/pb/c/c_runtime.c +299 -30
@@ 990,19 990,24 @@ enum_value_at(lua_State *L, pb_plan_field *f, int idx)
return (int32_t)to_int64_at(L, idx);
}
-/* Encode one singular scalar/enum/string field. Returns 1 if bytes
- * were written, 0 if the value collapsed to its proto3 default and
- * was suppressed. Zero-suppression skipped for proto3-optional. */
+/* Encode one scalar/enum/string field. Returns 1 if bytes were
+ * written, 0 if the value collapsed to its proto3 default and was
+ * suppressed. Zero-suppression is skipped for proto3-optional AND
+ * when `force_emit` is non-zero (used by repeated-unpacked, where
+ * every element must reach the wire regardless of value). */
static int
-encode_one_field(lua_State *L, enc_buf *b, pb_plan_field *f, int val_idx)
+encode_one_field(lua_State *L, enc_buf *b, pb_plan_field *f, int val_idx,
+ int force_emit)
{
+ int suppress = !force_emit && !f->optional;
+
switch (f->kind) {
case PB_KIND_INT32:
case PB_KIND_INT64:
case PB_KIND_UINT32:
case PB_KIND_UINT64: {
uint64_t u = to_uint64_at(L, val_idx);
- if (!f->optional && u == 0) return 0;
+ if (suppress && u == 0) return 0;
ebuf_reserve(L, b, f->tag_len + 10);
ebuf_put_tag(b, f);
ebuf_put_varint(b, u);
@@ 1010,7 1015,7 @@ encode_one_field(lua_State *L, enc_buf *b, pb_plan_field *f, int val_idx)
}
case PB_KIND_SINT32: {
int32_t s = to_int32_at(L, val_idx);
- if (!f->optional && s == 0) return 0;
+ if (suppress && s == 0) return 0;
ebuf_reserve(L, b, f->tag_len + 5);
ebuf_put_tag(b, f);
ebuf_put_varint(b, zigzag32(s));
@@ 1018,7 1023,7 @@ encode_one_field(lua_State *L, enc_buf *b, pb_plan_field *f, int val_idx)
}
case PB_KIND_SINT64: {
int64_t s = to_int64_at(L, val_idx);
- if (!f->optional && s == 0) return 0;
+ if (suppress && s == 0) return 0;
ebuf_reserve(L, b, f->tag_len + 10);
ebuf_put_tag(b, f);
ebuf_put_varint(b, zigzag64(s));
@@ 1026,7 1031,7 @@ encode_one_field(lua_State *L, enc_buf *b, pb_plan_field *f, int val_idx)
}
case PB_KIND_BOOL: {
int truthy = lua_toboolean(L, val_idx);
- if (!f->optional && !truthy) return 0;
+ if (suppress && !truthy) return 0;
ebuf_reserve(L, b, f->tag_len + 1);
ebuf_put_tag(b, f);
ebuf_put_byte(b, truthy ? 1 : 0);
@@ 1036,7 1041,7 @@ encode_one_field(lua_State *L, enc_buf *b, pb_plan_field *f, int val_idx)
case PB_KIND_SFIXED32: {
uint64_t u = to_uint64_at(L, val_idx);
uint32_t u32 = (uint32_t)u;
- if (!f->optional && u32 == 0) return 0;
+ if (suppress && u32 == 0) return 0;
ebuf_reserve(L, b, f->tag_len + 4);
ebuf_put_tag(b, f);
ebuf_put_fixed32(b, u32);
@@ 1045,7 1050,7 @@ encode_one_field(lua_State *L, enc_buf *b, pb_plan_field *f, int val_idx)
case PB_KIND_FIXED64:
case PB_KIND_SFIXED64: {
uint64_t u = to_uint64_at(L, val_idx);
- if (!f->optional && u == 0) return 0;
+ if (suppress && u == 0) return 0;
ebuf_reserve(L, b, f->tag_len + 8);
ebuf_put_tag(b, f);
ebuf_put_fixed64(b, u);
@@ 1056,7 1061,7 @@ encode_one_field(lua_State *L, enc_buf *b, pb_plan_field *f, int val_idx)
uint32_t u = f32_to_u32((float)d);
/* +0.0 -> u==0 (skip); -0.0 -> u==0x80000000 (emit). Matches the
* Lua-side `(v ~= 0 or 1/v == -math.huge)` guard. */
- if (!f->optional && u == 0) return 0;
+ if (suppress && u == 0) return 0;
ebuf_reserve(L, b, f->tag_len + 4);
ebuf_put_tag(b, f);
ebuf_put_fixed32(b, u);
@@ 1065,7 1070,7 @@ encode_one_field(lua_State *L, enc_buf *b, pb_plan_field *f, int val_idx)
case PB_KIND_DOUBLE: {
double d = lua_tonumber(L, val_idx);
uint64_t u = f64_to_u64(d);
- if (!f->optional && u == 0) return 0;
+ if (suppress && u == 0) return 0;
ebuf_reserve(L, b, f->tag_len + 8);
ebuf_put_tag(b, f);
ebuf_put_fixed64(b, u);
@@ 1073,7 1078,7 @@ encode_one_field(lua_State *L, enc_buf *b, pb_plan_field *f, int val_idx)
}
case PB_KIND_ENUM: {
int32_t e = enum_value_at(L, f, val_idx);
- if (!f->optional && e == 0) return 0;
+ if (suppress && e == 0) return 0;
ebuf_reserve(L, b, f->tag_len + 10);
ebuf_put_tag(b, f);
/* enum is wire-equivalent to int32: sign-extend to uint64 then varint. */
@@ 1086,7 1091,7 @@ encode_one_field(lua_State *L, enc_buf *b, pb_plan_field *f, int val_idx)
luaL_error(L, "string/bytes field requires a string value");
size_t len;
const char *s = lua_tolstring(L, val_idx, &len);
- if (!f->optional && len == 0) return 0;
+ if (suppress && len == 0) return 0;
ebuf_reserve(L, b, f->tag_len + 10 + len);
ebuf_put_tag(b, f);
ebuf_put_varint(b, (uint64_t)len);
@@ 1095,13 1100,161 @@ encode_one_field(lua_State *L, enc_buf *b, pb_plan_field *f, int val_idx)
return 1;
}
default:
- /* Repeated/map/message are 3d/3e scope — silently skip. */
+ /* Map is 3h scope; message singular goes through
+ * encode_submessage_field; repeated dispatches at the
+ * field-walk level. */
return 0;
}
}
+/* Encode one element of a packed scalar/enum/bool field into `b` —
+ * raw value bytes only, no tag. Mirrors encode_one_field's per-kind
+ * value emission with suppression always off. Length-delimited kinds
+ * (STRING/BYTES/MESSAGE) are not packable per spec — caller must
+ * dispatch them elsewhere. */
+static void
+encode_packed_element_at(lua_State *L, enc_buf *b, pb_plan_field *f,
+ int val_idx)
+{
+ switch (f->kind) {
+ case PB_KIND_INT32:
+ case PB_KIND_INT64:
+ case PB_KIND_UINT32:
+ case PB_KIND_UINT64: {
+ uint64_t u = to_uint64_at(L, val_idx);
+ ebuf_reserve(L, b, 10);
+ ebuf_put_varint(b, u);
+ return;
+ }
+ case PB_KIND_SINT32: {
+ int32_t s = to_int32_at(L, val_idx);
+ ebuf_reserve(L, b, 5);
+ ebuf_put_varint(b, zigzag32(s));
+ return;
+ }
+ case PB_KIND_SINT64: {
+ int64_t s = to_int64_at(L, val_idx);
+ ebuf_reserve(L, b, 10);
+ ebuf_put_varint(b, zigzag64(s));
+ return;
+ }
+ case PB_KIND_BOOL: {
+ int truthy = lua_toboolean(L, val_idx);
+ ebuf_reserve(L, b, 1);
+ ebuf_put_byte(b, truthy ? 1 : 0);
+ return;
+ }
+ case PB_KIND_FIXED32:
+ case PB_KIND_SFIXED32: {
+ uint64_t u = to_uint64_at(L, val_idx);
+ ebuf_reserve(L, b, 4);
+ ebuf_put_fixed32(b, (uint32_t)u);
+ return;
+ }
+ case PB_KIND_FIXED64:
+ case PB_KIND_SFIXED64: {
+ uint64_t u = to_uint64_at(L, val_idx);
+ ebuf_reserve(L, b, 8);
+ ebuf_put_fixed64(b, u);
+ return;
+ }
+ case PB_KIND_FLOAT: {
+ double d = lua_tonumber(L, val_idx);
+ ebuf_reserve(L, b, 4);
+ ebuf_put_fixed32(b, f32_to_u32((float)d));
+ return;
+ }
+ case PB_KIND_DOUBLE: {
+ double d = lua_tonumber(L, val_idx);
+ ebuf_reserve(L, b, 8);
+ ebuf_put_fixed64(b, f64_to_u64(d));
+ return;
+ }
+ case PB_KIND_ENUM: {
+ int32_t e = enum_value_at(L, f, val_idx);
+ ebuf_reserve(L, b, 10);
+ ebuf_put_varint(b, (uint64_t)(int64_t)e);
+ return;
+ }
+ default:
+ luaL_error(L, "kind %d not packable", (int)f->kind);
+ }
+}
+
/* Forward decls for the recursive encode pair. */
static void encode_body(lua_State *L, enc_buf *b, pb_plan *plan, int msg_idx);
+static void encode_submessage_field(lua_State *L, enc_buf *b, pb_plan *plan,
+ pb_plan_field *f, int val_idx);
+
+/* Encode a repeated field's elements into `b`. Dispatches on element
+ * kind and the `packed` plan flag:
+ * - packed scalar/enum/bool → single tag(LEN) + varint(len) + tight
+ * payload built in a stack-backed sub-buffer
+ * - unpacked scalar/enum/bool → per-element tag(elem_wire) + value
+ * - string/bytes → per-element tag(LEN) + varint(len) + bytes
+ * (string/bytes are never packable per spec)
+ * - message → per-element tag(LEN) + len-prefix + nested body via
+ * encode_submessage_field
+ *
+ * Empty arrays produce nothing — proto3 wire spec treats an absent
+ * repeated field and an empty one identically.
+ *
+ * Sub-buffer cleanup follows encode_submessage_field's contract: the
+ * parent's heap is force-established before allocating the sub-buffer
+ * so the parent's heap_idx sits below saved_top and survives the final
+ * lua_settop. */
+static void
+encode_repeated_field(lua_State *L, enc_buf *b, pb_plan *plan,
+ pb_plan_field *f, int val_idx)
+{
+ val_idx = abs_idx(L, val_idx);
+ int n = (int)lua_objlen(L, val_idx);
+ if (n == 0)
+ return;
+
+ if (f->kind == PB_KIND_MESSAGE) {
+ for (int i = 1; i <= n; i++) {
+ lua_rawgeti(L, val_idx, i);
+ int elem = lua_gettop(L);
+ encode_submessage_field(L, b, plan, f, elem);
+ lua_pop(L, 1);
+ }
+ return;
+ }
+
+ if (f->packed) {
+ if (b->heap_idx == 0)
+ ebuf_grow(L, b, 1);
+ int saved_top = lua_gettop(L);
+
+ enc_buf sub;
+ ebuf_init(&sub);
+ for (int i = 1; i <= n; i++) {
+ lua_rawgeti(L, val_idx, i);
+ encode_packed_element_at(L, &sub, f, lua_gettop(L));
+ lua_pop(L, 1);
+ }
+
+ ebuf_reserve(L, b, f->tag_len + 10 + sub.used);
+ ebuf_put_tag(b, f);
+ ebuf_put_varint(b, (uint64_t)sub.used);
+ if (sub.used > 0)
+ ebuf_put_bytes(b, ebuf_base(&sub), sub.used);
+
+ lua_settop(L, saved_top);
+ return;
+ }
+
+ /* Unpacked: per-element tag + value. string/bytes flow through
+ * encode_one_field too — its STRING/BYTES branch already emits
+ * `tag + varint(len) + bytes`, which is exactly the unpacked
+ * length-delimited element shape. */
+ for (int i = 1; i <= n; i++) {
+ lua_rawgeti(L, val_idx, i);
+ encode_one_field(L, b, f, lua_gettop(L), /* force_emit */ 1);
+ lua_pop(L, 1);
+ }
+}
/* Encode one singular sub-message field into the parent buffer `b`.
* Lifecycle / stack-management contract:
@@ 1173,9 1326,7 @@ encode_body(lua_State *L, enc_buf *b, pb_plan *plan, int msg_idx)
for (int i = 0; i < plan->n_fields; i++) {
pb_plan_field *f = &plan->fields[i];
- /* Scope for now: singular scalar/enum/string/bytes/message.
- * Repeated and map are 3e/3h territory — still skipped. */
- if (f->repeated) continue;
+ /* Map dispatches in 3h. */
if (f->kind == PB_KIND_MAP) continue;
lua_rawgeti(L, names_idx, i + 1); /* push field name */
@@ 1187,10 1338,15 @@ encode_body(lua_State *L, enc_buf *b, pb_plan *plan, int msg_idx)
continue;
}
- if (f->kind == PB_KIND_MESSAGE) {
+ if (f->repeated) {
+ if (lua_type(L, val_idx) != LUA_TTABLE)
+ luaL_error(L,
+ "repeated field requires a table value");
+ encode_repeated_field(L, b, plan, f, val_idx);
+ } else if (f->kind == PB_KIND_MESSAGE) {
encode_submessage_field(L, b, plan, f, val_idx);
} else {
- encode_one_field(L, b, f, val_idx);
+ encode_one_field(L, b, f, val_idx, /* force_emit */ 0);
}
lua_pop(L, 1);
}
@@ 1468,6 1624,27 @@ decode_submessage_field(dec_ctx *c, pb_plan_field *f, int sub_plans_idx)
c->len = saved_len;
}
+/* Return 1 if the field's element wire type is varint/i32/i64 — i.e.
+ * the field is eligible for packed encoding. Length-delimited kinds
+ * (STRING/BYTES/MESSAGE) and MAP are never packable. Used on decode
+ * to detect a wt==LEN payload for a repeated scalar field as a packed
+ * blob even when the schema declares packed=false (proto3 readers MUST
+ * accept both). */
+static inline int
+field_is_packable(const pb_plan_field *f)
+{
+ switch (f->kind) {
+ case PB_KIND_STRING:
+ case PB_KIND_BYTES:
+ case PB_KIND_MESSAGE:
+ case PB_KIND_MAP:
+ case PB_KIND_NONE:
+ return 0;
+ default:
+ return 1;
+ }
+}
+
static void
decode_body(dec_ctx *c, pb_plan *plan, int result_idx)
{
@@ 1494,6 1671,26 @@ decode_body(dec_ctx *c, pb_plan *plan, int result_idx)
}
sub_plans_idx = lua_gettop(L);
+ /* Per-field stack-slot cache for repeated fields. On first hit for
+ * a given field, we lua_createtable + store into result[name] and
+ * dup-push the table onto the stack; subsequent hits reuse the
+ * cached absolute stack index and lua_rawseti the new element
+ * directly. Avoids the per-element lua_getfield(result, name) round
+ * trip that the c-accel spike measured at 2x slower at 100KB.
+ *
+ * list_count[i] tracks length without calling lua_objlen per append
+ * — a Lua-side O(log n) probe that adds up fast on the 1000-element
+ * acceptance path.
+ *
+ * VLA size guarded against n_fields == 0 (UB for zero-length VLA).
+ * Both arrays live on the C stack; n_fields is bounded by message
+ * shape — for any realistic schema this is well under 1KB. */
+ int vla_n = plan->n_fields > 0 ? plan->n_fields : 1;
+ int list_stack_idx[vla_n];
+ int list_count[vla_n];
+ memset(list_stack_idx, 0, sizeof(list_stack_idx));
+ memset(list_count, 0, sizeof(list_count));
+
while (c->pos < c->len) {
uint64_t tag = dec_varint(c);
uint32_t field_number = (uint32_t)(tag >> 3);
@@ 1502,39 1699,111 @@ decode_body(dec_ctx *c, pb_plan *plan, int result_idx)
/* Linear scan over plan->fields. n_fields is typically small;
* tag-keyed dispatch table is a future optimization. */
pb_plan_field *f = NULL;
+ int f_idx = -1;
for (int i = 0; i < plan->n_fields; i++) {
if (plan->fields[i].field_number == field_number) {
f = &plan->fields[i];
+ f_idx = i;
break;
}
}
- /* Out-of-scope shapes for current bd-hwe: repeated, map.
- * 3e / 3h will replace this skip. */
- if (f == NULL || f->repeated || f->kind == PB_KIND_MAP) {
+ /* Unknown tag, or map (3h territory). */
+ if (f == NULL || f->kind == PB_KIND_MAP) {
dec_skip(c, wt);
continue;
}
+ /* ------------------------------------------------------ *
+ * Repeated dispatch *
+ * ------------------------------------------------------ */
+ if (f->repeated) {
+ /* Lazy-create the list table on first hit. */
+ int list_idx = list_stack_idx[f_idx];
+ if (list_idx == 0) {
+ lua_createtable(L, 0, 0);
+ /* Stack: ..., new_list. Dup, write name → list
+ * into result, leave list on top as our cache. */
+ lua_pushvalue(L, -1);
+ lua_rawgeti(L, names_idx, f_idx + 1);
+ lua_insert(L, -2); /* name, list_copy */
+ lua_rawset(L, result_idx); /* result[name] = list */
+ list_idx = lua_gettop(L);
+ list_stack_idx[f_idx] = list_idx;
+ list_count[f_idx] = 0;
+ }
+
+ /* Repeated message: per-element length-delimited body. */
+ if (f->kind == PB_KIND_MESSAGE) {
+ if (wt != PB_WIRE_LEN)
+ luaL_error(L,
+ "repeated message field %d expected wire 2, got %d",
+ (int)field_number, (int)wt);
+ decode_submessage_field(c, f, sub_plans_idx);
+ /* Stack top is the decoded sub-table. */
+ list_count[f_idx]++;
+ lua_rawseti(L, list_idx, list_count[f_idx]);
+ continue;
+ }
+
+ /* Packed payload: a single LEN-prefixed blob carrying
+ * N elements end-to-end. Proto3 readers must accept a
+ * packed payload for any packable scalar regardless of
+ * the schema's packed flag — that's why this check is
+ * `field_is_packable`, not `f->packed`. */
+ if (wt == PB_WIRE_LEN && field_is_packable(f)) {
+ uint64_t plen = dec_varint(c);
+ if (c->len - c->pos < plen)
+ luaL_error(L,
+ "truncated packed payload for field %d",
+ (int)field_number);
+ size_t saved_len = c->len;
+ c->len = c->pos + (size_t)plen;
+ while (c->pos < c->len) {
+ dec_push_one(c, f);
+ list_count[f_idx]++;
+ lua_rawseti(L, list_idx, list_count[f_idx]);
+ }
+ if (c->pos != c->len)
+ luaL_error(L,
+ "packed payload underflow for field %d",
+ (int)field_number);
+ c->len = saved_len;
+ continue;
+ }
+
+ /* Unpacked single element. */
+ dec_push_one(c, f);
+ list_count[f_idx]++;
+ lua_rawseti(L, list_idx, list_count[f_idx]);
+ continue;
+ }
+
+ /* ------------------------------------------------------ *
+ * Singular dispatch *
+ * ------------------------------------------------------ */
if (f->kind == PB_KIND_MESSAGE) {
decode_submessage_field(c, f, sub_plans_idx);
- /* stack: ..., names, sub_plans, sub_result */
- int field_idx_1based = (int)(f - plan->fields) + 1;
- lua_rawgeti(L, names_idx, field_idx_1based);
+ /* stack: ..., names, sub_plans, [lists...], sub_result */
+ lua_rawgeti(L, names_idx, f_idx + 1);
lua_insert(L, -2); /* name, sub_result */
lua_rawset(L, result_idx); /* result[name] = sub_result */
continue;
}
- dec_push_one(c, f); /* stack: ..., names, sub_plans, value */
+ dec_push_one(c, f); /* stack: ..., names, sub_plans, [lists...], value */
- int field_idx_1based = (int)(f - plan->fields) + 1;
- lua_rawgeti(L, names_idx, field_idx_1based);
+ lua_rawgeti(L, names_idx, f_idx + 1);
lua_insert(L, -2); /* name, value */
lua_rawset(L, result_idx); /* result[name] = value */
}
- lua_pop(L, 2); /* sub_plans, names */
+ /* Pop everything we pushed: per-field list tables (one per repeated
+ * field that appeared), then sub_plans and names. Walk list_stack_idx
+ * to count list-table pushes — equals lua_gettop(L) - sub_plans_idx. */
+ int top = lua_gettop(L);
+ int to_pop = top - names_idx + 1;
+ lua_pop(L, to_pop);
}
static int
M test/c_runtime_decode_test.lua => test/c_runtime_decode_test.lua +157 -6
@@ 129,20 129,26 @@ for _, mode in ipairs({'full', 'runtime'}) do
t.assert_equals(c_decoded.avatar, '\x00\x01\xff\xfe')
end
- function g.test_skips_repeated_and_map_tags()
- -- 3d scope: singular message decoded into a sub-table.
- -- Repeated and map remain skipped (3e/3h replace this).
+ function g.test_skips_map_tags()
+ -- 3e scope: singular message + repeated now decode. Map tags
+ -- (synthetic entry sub-messages) remain skipped — 3h replaces
+ -- the gate.
local plan = c_runtime.compile_plan(hello.Person_descriptor)
local rich = {
name = 'x',
- emails = {'a@b'}, -- repeated string (skip)
+ emails = {'a@b'}, -- repeated string (decode)
address = {street = 'Main'}, -- message (decode)
- lucky_numbers = {1, 2, 3}, -- repeated packed (skip)
+ lucky_numbers = {1, 2, 3}, -- repeated packed (decode)
ages_by_nickname = {alice = 30}, -- map (skip)
}
local bytes = full_hello.Person_encode(rich)
local c_decoded = c_runtime.decode(plan, bytes)
- t.assert_equals(c_decoded, {name = 'x', address = {street = 'Main'}})
+ t.assert_equals(c_decoded, {
+ name = 'x',
+ emails = {'a@b'},
+ address = {street = 'Main'},
+ lucky_numbers = {1, 2, 3},
+ })
end
-- ---------- Sub-message decode (bd-hwe / ra6 3d) ----------
@@ 228,4 234,149 @@ for _, mode in ipairs({'full', 'runtime'}) do
c_runtime.decode(plan, '\x0a\x05ab')
end)
end
+
+ -- ---------- Repeated + packed decode (bd-jc9 / ra6 3e) ----------
+
+ function g.test_acceptance_lucky_numbers_packed_int32_round_trip()
+ local plan = c_runtime.compile_plan(hello.Person_descriptor)
+ local msg = {lucky_numbers = {1, 2, 3, 4, 5, -1, 0x7fffffff}}
+ local bytes = full_hello.Person_encode(msg)
+ local c_decoded = c_runtime.decode(plan, bytes)
+ local lua_decoded = full_hello.Person_decode(bytes)
+ t.assert_equals(c_decoded, lua_decoded)
+ t.assert_equals(c_decoded.lucky_numbers, {1, 2, 3, 4, 5, -1, 0x7fffffff})
+ end
+
+ function g.test_repeated_string_round_trip()
+ local plan = c_runtime.compile_plan(hello.Person_descriptor)
+ local msg = {emails = {'a@b', 'c@d', '', 'last'}}
+ local bytes = full_hello.Person_encode(msg)
+ t.assert_equals(c_runtime.decode(plan, bytes),
+ full_hello.Person_decode(bytes))
+ end
+
+ function g.test_repeated_message_self_reference_round_trip()
+ local plan = c_runtime.compile_plan(hello.Person_descriptor)
+ local msg = {
+ name = 'root',
+ friends = {
+ {name = 'alice', age = 30},
+ {name = 'bob', friends = {{name = 'carol'}}},
+ {},
+ },
+ }
+ local bytes = full_hello.Person_encode(msg)
+ t.assert_equals(c_runtime.decode(plan, bytes),
+ full_hello.Person_decode(bytes))
+ end
+
+ function g.test_packed_payload_accepted_for_unpacked_schema()
+ -- Wire-format invariant: proto3 decoders accept a packed payload
+ -- for ANY packable scalar, regardless of the schema's packed flag.
+ -- Build a synthetic packed payload for Holder.unpacked_int32 and
+ -- confirm the C decoder concatenates its elements correctly.
+ local cr = require(mode .. '.c_repeated.c_repeated_pb')
+ local full_cr = require('full.c_repeated.c_repeated_pb')
+ local wire = require('pb.wire')
+ -- field 20, wire LEN: tag = (20<<3)|2 = 162 = 0xA2 0x01
+ local payload = wire.encode_varint(1) .. wire.encode_varint(2)
+ .. wire.encode_varint(3)
+ local bytes = '\xa2\x01' .. wire.encode_varint(#payload) .. payload
+ local plan = c_runtime.compile_plan(cr.Holder_descriptor)
+ local c_decoded = c_runtime.decode(plan, bytes)
+ local lua_decoded = full_cr.Holder_decode(bytes)
+ t.assert_equals(c_decoded, lua_decoded)
+ t.assert_equals(c_decoded.unpacked_int32, {1, 2, 3})
+ end
+
+ function g.test_unpacked_payload_accepted_for_packed_schema()
+ -- Symmetric: a per-element-tagged stream for Holder.packed_int32
+ -- must also concatenate. Build it by hand.
+ local cr = require(mode .. '.c_repeated.c_repeated_pb')
+ local full_cr = require('full.c_repeated.c_repeated_pb')
+ local wire = require('pb.wire')
+ -- field 1, wire VARINT: tag = (1<<3)|0 = 8 = 0x08
+ local elem = function(v)
+ return '\x08' .. wire.encode_varint(v)
+ end
+ local bytes = elem(7) .. elem(8) .. elem(9)
+ local plan = c_runtime.compile_plan(cr.Holder_descriptor)
+ local c_decoded = c_runtime.decode(plan, bytes)
+ local lua_decoded = full_cr.Holder_decode(bytes)
+ t.assert_equals(c_decoded, lua_decoded)
+ t.assert_equals(c_decoded.packed_int32, {7, 8, 9})
+ end
+
+ local function counts() return {10, 100, 1000} end
+
+ function g.test_fixture_packed_int32_round_trip_at_counts()
+ local cr = require(mode .. '.c_repeated.c_repeated_pb')
+ local full_cr = require('full.c_repeated.c_repeated_pb')
+ local plan = c_runtime.compile_plan(cr.Holder_descriptor)
+ for _, n in ipairs(counts()) do
+ local arr = {}
+ for i = 1, n do arr[i] = i end
+ local bytes = full_cr.Holder_encode({packed_int32 = arr})
+ t.assert_equals(c_runtime.decode(plan, bytes),
+ full_cr.Holder_decode(bytes),
+ ('packed_int32 n=%d'):format(n))
+ end
+ end
+
+ function g.test_fixture_unpacked_scalars_round_trip_at_counts()
+ local cr = require(mode .. '.c_repeated.c_repeated_pb')
+ local full_cr = require('full.c_repeated.c_repeated_pb')
+ local plan = c_runtime.compile_plan(cr.Holder_descriptor)
+ for _, n in ipairs(counts()) do
+ local i32, sx, fx = {}, {}, {}
+ for i = 1, n do
+ i32[i] = i
+ sx[i] = -i
+ fx[i] = ffi.new('uint64_t', i)
+ end
+ local bytes = full_cr.Holder_encode({
+ unpacked_int32 = i32,
+ unpacked_sint32 = sx,
+ unpacked_fixed64 = fx,
+ })
+ t.assert_equals(c_runtime.decode(plan, bytes),
+ full_cr.Holder_decode(bytes),
+ ('unpacked scalars n=%d'):format(n))
+ end
+ end
+
+ function g.test_fixture_repeated_messages_round_trip_at_counts()
+ local cr = require(mode .. '.c_repeated.c_repeated_pb')
+ local full_cr = require('full.c_repeated.c_repeated_pb')
+ local plan = c_runtime.compile_plan(cr.Holder_descriptor)
+ for _, n in ipairs(counts()) do
+ local arr = {}
+ for i = 1, n do arr[i] = {v = i, s = 'name' .. i} end
+ local bytes = full_cr.Holder_encode({messages = arr})
+ t.assert_equals(c_runtime.decode(plan, bytes),
+ full_cr.Holder_decode(bytes),
+ ('messages n=%d'):format(n))
+ end
+ end
+
+ function g.test_fixture_mixed_round_trip()
+ local cr = require(mode .. '.c_repeated.c_repeated_pb')
+ local full_cr = require('full.c_repeated.c_repeated_pb')
+ local plan = c_runtime.compile_plan(cr.Holder_descriptor)
+ local p, u, s, m = {}, {}, {}, {}
+ for i = 1, 100 do
+ p[i] = i
+ u[i] = -i
+ s[i] = 'k' .. i
+ m[i] = {v = i, s = 's' .. i}
+ end
+ local bytes = full_cr.Holder_encode({
+ packed_int32 = p,
+ unpacked_int32 = u,
+ strings = s,
+ messages = m,
+ })
+ t.assert_equals(c_runtime.decode(plan, bytes),
+ full_cr.Holder_decode(bytes))
+ end
end
M test/c_runtime_encode_test.lua => test/c_runtime_encode_test.lua +186 -7
@@ 144,19 144,24 @@ for _, mode in ipairs({'full', 'runtime'}) do
full_hello.Person_encode(msg))
end
- function g.test_skips_repeated_and_map_fields()
- -- 3d scope: singular message now encoded. Repeated and map
- -- are still silently skipped — 3e/3h will replace this.
+ function g.test_skips_map_fields()
+ -- 3e scope: singular message + repeated (packed and unpacked)
+ -- now encode. Map fields remain silently skipped — 3h replaces
+ -- the gate.
local plan = c_runtime.compile_plan(hello.Person_descriptor)
local msg = {
name = 'x',
- emails = {'a@b'}, -- repeated string (skip)
+ emails = {'a@b'}, -- repeated string (encode)
address = {street = 'Main'}, -- message (encode)
- lucky_numbers = {1, 2, 3}, -- repeated packed (skip)
+ lucky_numbers = {1, 2, 3}, -- repeated packed (encode)
ages_by_nickname = {alice = 30}, -- map (skip)
}
- local expected = full_hello.Person_encode(
- {name = 'x', address = {street = 'Main'}})
+ local expected = full_hello.Person_encode({
+ name = 'x',
+ emails = {'a@b'},
+ address = {street = 'Main'},
+ lucky_numbers = {1, 2, 3},
+ })
t.assert_equals(c_runtime.encode(plan, msg), expected)
end
@@ 244,4 249,178 @@ for _, mode in ipairs({'full', 'runtime'}) do
c_runtime.encode(plan, {seconds = 1})
end)
end
+
+ -- ---------- Repeated + packed (bd-jc9 / ra6 3e) ----------
+
+ -- Acceptance per bd-jc9: Person.lucky_numbers (packed int32) round-
+ -- trips byte-equal, and a fixture mixing packed + unpacked at 10/
+ -- 100/1000 elements matches mode=full byte-for-byte.
+
+ function g.test_acceptance_lucky_numbers_packed_int32()
+ local plan = c_runtime.compile_plan(hello.Person_descriptor)
+ local msg = {lucky_numbers = {1, 2, 3, 4, 5, -1, 0x7fffffff}}
+ t.assert_equals(c_runtime.encode(plan, msg),
+ full_hello.Person_encode(msg))
+ end
+
+ function g.test_repeated_empty_array_omits_field()
+ local plan = c_runtime.compile_plan(hello.Person_descriptor)
+ -- Empty repeated must be wire-equivalent to absent.
+ t.assert_equals(c_runtime.encode(plan, {lucky_numbers = {}}), '')
+ t.assert_equals(c_runtime.encode(plan, {emails = {}}), '')
+ end
+
+ function g.test_repeated_emits_zero_elements_no_suppression()
+ -- Unlike singular scalars, repeated elements are NOT zero-
+ -- suppressed — every element reaches the wire.
+ local plan = c_runtime.compile_plan(hello.Person_descriptor)
+ local msg = {lucky_numbers = {0, 0, 0}}
+ local bytes = c_runtime.encode(plan, msg)
+ t.assert_equals(bytes, full_hello.Person_encode(msg))
+ t.assert(#bytes > 0, 'zero elements still emit')
+ end
+
+ function g.test_repeated_string_per_element_tag()
+ local plan = c_runtime.compile_plan(hello.Person_descriptor)
+ local msg = {emails = {'a@b', 'c@d', '', 'long' .. string.rep('x', 200)}}
+ t.assert_equals(c_runtime.encode(plan, msg),
+ full_hello.Person_encode(msg))
+ end
+
+ function g.test_repeated_message_self_reference()
+ -- Person.friends is `repeated Person` — a self-referencing
+ -- sub-message exercising recursive plan dispatch via the
+ -- c_plan stash for cycle-breaking.
+ local plan = c_runtime.compile_plan(hello.Person_descriptor)
+ local msg = {
+ name = 'root',
+ friends = {
+ {name = 'alice', age = 30},
+ {name = 'bob', friends = {{name = 'carol'}}},
+ {}, -- empty friend → tag + len(0) per proto3 presence
+ },
+ }
+ t.assert_equals(c_runtime.encode(plan, msg),
+ full_hello.Person_encode(msg))
+ end
+
+ function g.test_repeated_non_table_value_errors()
+ local plan = c_runtime.compile_plan(hello.Person_descriptor)
+ t.assert_error_msg_contains('repeated', function()
+ c_runtime.encode(plan, {lucky_numbers = 'not an array'})
+ end)
+ end
+
+ -- ---------- c_repeated fixture: every dispatch branch ----------
+
+ local function counts() return {10, 100, 1000} end
+
+ function g.test_fixture_packed_int32_at_counts()
+ local cr = require(mode .. '.c_repeated.c_repeated_pb')
+ local full_cr = require('full.c_repeated.c_repeated_pb')
+ local plan = c_runtime.compile_plan(cr.Holder_descriptor)
+ for _, n in ipairs(counts()) do
+ local arr = {}
+ for i = 1, n do arr[i] = i end
+ local msg = {packed_int32 = arr}
+ t.assert_equals(c_runtime.encode(plan, msg),
+ full_cr.Holder_encode(msg),
+ ('packed_int32 n=%d'):format(n))
+ end
+ end
+
+ function g.test_fixture_packed_all_numeric_kinds()
+ local cr = require(mode .. '.c_repeated.c_repeated_pb')
+ local full_cr = require('full.c_repeated.c_repeated_pb')
+ local plan = c_runtime.compile_plan(cr.Holder_descriptor)
+ local msg = {
+ packed_int32 = {1, -1, 0x7fffffff},
+ packed_int64 = {ffi.new('int64_t', 1), ffi.new('int64_t', -1)},
+ packed_sint32 = {-3, 0, 3},
+ packed_uint32 = {7, 8, 9},
+ packed_fixed32 = {100, 200},
+ packed_fixed64 = {ffi.new('uint64_t', 0x1234567890ABCDEFULL)},
+ packed_double = {1.5, -2.25, 0},
+ packed_float = {0.5, -0.25},
+ packed_bool = {true, false, true, true, false},
+ }
+ t.assert_equals(c_runtime.encode(plan, msg),
+ full_cr.Holder_encode(msg))
+ end
+
+ function g.test_fixture_unpacked_scalars_at_counts()
+ local cr = require(mode .. '.c_repeated.c_repeated_pb')
+ local full_cr = require('full.c_repeated.c_repeated_pb')
+ local plan = c_runtime.compile_plan(cr.Holder_descriptor)
+ for _, n in ipairs(counts()) do
+ local i32, sx, fx = {}, {}, {}
+ for i = 1, n do
+ i32[i] = i
+ sx[i] = -i
+ fx[i] = ffi.new('uint64_t', i)
+ end
+ local msg = {
+ unpacked_int32 = i32,
+ unpacked_sint32 = sx,
+ unpacked_fixed64 = fx,
+ }
+ t.assert_equals(c_runtime.encode(plan, msg),
+ full_cr.Holder_encode(msg),
+ ('unpacked scalars n=%d'):format(n))
+ end
+ end
+
+ function g.test_fixture_repeated_strings_and_bytes()
+ local cr = require(mode .. '.c_repeated.c_repeated_pb')
+ local full_cr = require('full.c_repeated.c_repeated_pb')
+ local plan = c_runtime.compile_plan(cr.Holder_descriptor)
+ for _, n in ipairs(counts()) do
+ local s, b = {}, {}
+ for i = 1, n do
+ s[i] = 'str' .. tostring(i)
+ b[i] = string.char(i % 256) .. '\0\xff'
+ end
+ local msg = {strings = s, blobs = b}
+ t.assert_equals(c_runtime.encode(plan, msg),
+ full_cr.Holder_encode(msg),
+ ('strings/blobs n=%d'):format(n))
+ end
+ end
+
+ function g.test_fixture_repeated_messages_at_counts()
+ local cr = require(mode .. '.c_repeated.c_repeated_pb')
+ local full_cr = require('full.c_repeated.c_repeated_pb')
+ local plan = c_runtime.compile_plan(cr.Holder_descriptor)
+ for _, n in ipairs(counts()) do
+ local arr = {}
+ for i = 1, n do arr[i] = {v = i, s = 'name' .. i} end
+ local msg = {messages = arr}
+ t.assert_equals(c_runtime.encode(plan, msg),
+ full_cr.Holder_encode(msg),
+ ('messages n=%d'):format(n))
+ end
+ end
+
+ function g.test_fixture_mixed_packed_and_unpacked()
+ -- All branches simultaneously: packed + unpacked + string/bytes
+ -- + message, on the same message instance.
+ local cr = require(mode .. '.c_repeated.c_repeated_pb')
+ local full_cr = require('full.c_repeated.c_repeated_pb')
+ local plan = c_runtime.compile_plan(cr.Holder_descriptor)
+ local p, u, s, m = {}, {}, {}, {}
+ for i = 1, 100 do
+ p[i] = i
+ u[i] = -i
+ s[i] = 'k' .. i
+ m[i] = {v = i, s = 's' .. i}
+ end
+ local msg = {
+ packed_int32 = p,
+ unpacked_int32 = u,
+ strings = s,
+ messages = m,
+ }
+ t.assert_equals(c_runtime.encode(plan, msg),
+ full_cr.Holder_encode(msg))
+ end
end
A test/proto/c_repeated.proto => test/proto/c_repeated.proto +43 -0
@@ 0,0 1,43 @@
+syntax = "proto3";
+
+package c_repeated;
+
+// Fixture for bd-jc9 (ra6 3e): repeated + packed C-runtime acceptance.
+//
+// Carries packed (proto3 default), explicit [packed=false], length-
+// delimited (string/bytes — never packable), and repeated sub-message
+// shapes so the C runtime exercises every repeated dispatch branch
+// against the same mode=full reference.
+//
+// Field numbers cluster by encoding shape so a hand-decode of a small
+// encoded payload stays readable when debugging fixture drift.
+
+message Inner {
+ int32 v = 1;
+ string s = 2;
+}
+
+message Holder {
+ // Packed scalars (proto3 default). One per encoder branch.
+ repeated int32 packed_int32 = 1;
+ repeated int64 packed_int64 = 2;
+ repeated sint32 packed_sint32 = 3;
+ repeated uint32 packed_uint32 = 4;
+ repeated fixed32 packed_fixed32 = 5;
+ repeated fixed64 packed_fixed64 = 6;
+ repeated double packed_double = 7;
+ repeated float packed_float = 8;
+ repeated bool packed_bool = 9;
+
+ // Explicit unpacked — emits one tag per element.
+ repeated int32 unpacked_int32 = 20 [packed = false];
+ repeated fixed64 unpacked_fixed64 = 21 [packed = false];
+ repeated sint32 unpacked_sint32 = 22 [packed = false];
+
+ // Length-delimited element kinds — never packable.
+ repeated string strings = 30;
+ repeated bytes blobs = 31;
+
+ // Repeated sub-message — per-element tag + length-prefix + body.
+ repeated Inner messages = 40;
+}