diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index 7fad4382bc1b635cd058d9450bc3f888af4aec95..38e723f79fee98a4eb1661557f14c5926e22c5e6 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -22,3 +22,4 @@ {"id":"int-b6e91276","kind":"field_change","created_at":"2026-05-19T04:46:39.175032Z","actor":"Eugene Blikh","issue_id":"tarantool-protobuf-mq7","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Descriptor -> C plan compiler landed in runtime/pb/c/c_runtime.c. Compiles a finalized Lua descriptor into an opaque pb_plan userdata with: per-field records (field_number, wire_type, kind, repeated/packed/optional, pre-encoded tag bytes, sub_plan_idx, oneof_idx, enum_ref), oneof grouping (parallel array with member indices + field oneof_idx back-pointers), WKT override pointers (has_override + override_encode_ref/decode_ref skipping field-walk), extension range hooks (proto2 scaffolding), field-name luaL_ref cache, sub-plan table for cycle-safe recursion. Self-references (Person.friends -> Person) handled by stashing c_plan on desc BEFORE recursing, so resolve_sub_plan returns the same userdata. Idempotent: re-compiling returns cached plan. Build via 'just build-c'. Smoke test at test/c_runtime_plan_test.lua: 26 assertions across both codegen modes (full + runtime) verify module surface, scalar/enum/message/map/repeated/packed shapes, sub-plan resolution, self-reference cycle break, oneof grouping (Result.outcome), idempotency, WKT override detection (Timestamp). Full suite: 771/771 with PB_ENABLE_C=1, 745+26 skipped without (silent fallback verified). Unblocks 3b/3c/3l/3k (y1n, mz6, awv, rmf)."}} {"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"}} diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 8ccd24b1350418d670617ed8f6ac78edc406c3f8..b7a7e7a0d53fc3c65cdfbaf9f9c14f17c71fd243 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -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} diff --git a/examples/expected/full/c_repeated/c_repeated_pb.lua b/examples/expected/full/c_repeated/c_repeated_pb.lua new file mode 100644 index 0000000000000000000000000000000000000000..ee6fc82145e0013dbcbf37d11bd952c03f1388fe --- /dev/null +++ b/examples/expected/full/c_repeated/c_repeated_pb.lua @@ -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 diff --git a/examples/expected/runtime/c_repeated/c_repeated_pb.lua b/examples/expected/runtime/c_repeated/c_repeated_pb.lua new file mode 100644 index 0000000000000000000000000000000000000000..693680e1fba9caacc0eb92024ca135288429eb63 --- /dev/null +++ b/examples/expected/runtime/c_repeated/c_repeated_pb.lua @@ -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 diff --git a/runtime/pb/c/c_runtime.c b/runtime/pb/c/c_runtime.c index f82aa520416edbd4a6c6d816268c335567dfbbdd..3b301b68586f6bc4bfb265bdf7b66cd29963f148 100644 --- a/runtime/pb/c/c_runtime.c +++ b/runtime/pb/c/c_runtime.c @@ -990,19 +990,24 @@ } 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 @@ return 1; } 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 @@ return 1; } 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 @@ return 1; } 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 @@ case PB_KIND_FIXED32: 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 @@ } 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 @@ double d = lua_tonumber(L, 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 @@ } 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 @@ return 1; } 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 @@ if (lua_type(L, val_idx) != LUA_TSTRING) 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 @@ ebuf_put_bytes(b, (const uint8_t *)s, len); 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 @@ 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 @@ lua_pop(L, 1); 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 @@ (int)c->pos, (int)c->len); 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 @@ lua_pushnil(L); } 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 @@ /* 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 diff --git a/test/c_runtime_decode_test.lua b/test/c_runtime_decode_test.lua index 5a04bade2cce8794dd0ad73f56165ccc03375a66..a78fc2267110eb7ed8a51fafacb345ff28512943 100644 --- a/test/c_runtime_decode_test.lua +++ b/test/c_runtime_decode_test.lua @@ -129,20 +129,26 @@ local c_decoded = c_runtime.decode(plan, bytes) 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) ---------- @@ -227,5 +233,150 @@ -- string bytes that the length claims. t.assert_error_msg_contains('truncated', function() 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 diff --git a/test/c_runtime_encode_test.lua b/test/c_runtime_encode_test.lua index fc844e0c9442591cfb64fb0db8efa8efa0bbdb99..d9cfe7fb8eba67dec13aa2539b0e5ec861264d81 100644 --- a/test/c_runtime_encode_test.lua +++ b/test/c_runtime_encode_test.lua @@ -144,19 +144,24 @@ t.assert_equals(c_runtime.encode(plan, msg), 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 @@ -243,5 +248,179 @@ local plan = c_runtime.compile_plan(pb.wkt.Timestamp_descriptor) t.assert_error_msg_contains('override', function() 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 diff --git a/test/proto/c_repeated.proto b/test/proto/c_repeated.proto new file mode 100644 index 0000000000000000000000000000000000000000..ff204c873f5e10c5fa19d2f2c7f1d67093bd99ac --- /dev/null +++ b/test/proto/c_repeated.proto @@ -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; +}