diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index 38e723f79fee98a4eb1661557f14c5926e22c5e6..0ed9abe45d1b0f9ec5adb8bacf5dd1dfa7b6d95e 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -23,3 +23,5 @@ {"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"}} +{"id":"int-92bcd95a","kind":"field_change","created_at":"2026-05-23T11:07:38.503706Z","actor":"Eugene Blikh","issue_id":"tarantool-protobuf-exy","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Acceptance tests landed: Person.emails (repeated string) and Person.friends (repeated Person, self-reference) round-trip byte-equal to mode=full at 1KB/10KB/100KB. Repeated string + message dispatch was already implemented as part of 3e (encode_repeated_field handles MESSAGE + string/bytes branches alongside scalars, decode_body uses cached list_stack_idx[] per-field); this ticket adds the formal size-scaled acceptance coverage."}} +{"id":"int-e3eda42e","kind":"field_change","created_at":"2026-05-23T11:17:06.762081Z","actor":"Eugene Blikh","issue_id":"tarantool-protobuf-awv","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}} diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index f87e9ca80666b19c05cacb7316c58eb46f267f44..1f065be49cd594567907a39663cb7ee6e474333a 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,7 +1,7 @@ {"_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":"closed","priority":1,"issue_type":"task","assignee":"Eugene Blikh","owner":"bigbes@gmail.com","created_at":"2026-05-18T20:20:52Z","created_by":"Eugene Blikh","updated_at":"2026-05-23T11:07:38Z","started_at":"2026-05-23T11:05:14Z","closed_at":"2026-05-23T11:07:38Z","close_reason":"Acceptance tests landed: Person.emails (repeated string) and Person.friends (repeated Person, self-reference) round-trip byte-equal to mode=full at 1KB/10KB/100KB. Repeated string + message dispatch was already implemented as part of 3e (encode_repeated_field handles MESSAGE + string/bytes branches alongside scalars, decode_body uses cached list_stack_idx[] per-field); this ticket adds the formal size-scaled acceptance coverage.","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":"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-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":"closed","priority":1,"issue_type":"task","assignee":"Eugene Blikh","owner":"bigbes@gmail.com","created_at":"2026-05-18T20:20:10Z","created_by":"Eugene Blikh","updated_at":"2026-05-23T11:17:07Z","started_at":"2026-05-23T11:13:35Z","closed_at":"2026-05-23T11:17:07Z","close_reason":"Closed","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} {"_type":"issue","id":"tarantool-protobuf-y1n","title":"ra6 3b: encode scalars (varint, fixed32/64, double, bool, enum, sint)","description":"C-side encode for scalar wire types into the message's output buffer. Reads field values via cached luaL_ref + lua_rawgeti, dispatches on plan field kind, writes varint/fixed32/fixed64/double/bool/enum/sint into the 4KB stack-backed buffer with malloc-promotion on overflow. Wire-byte output must be byte-equal to mode=full Lua output. Depends on 3a (needs the plan). Acceptance: Person encode round-trip works for {name='x', age=42, balance=-7, user_id=0xDEADBEEFCAFEBABEULL, weight_kg=3.14} with byte-equal output to mode=full pure Lua.","status":"closed","priority":1,"issue_type":"task","assignee":"Eugene Blikh","owner":"bigbes@gmail.com","created_at":"2026-05-18T20:20:03Z","created_by":"Eugene Blikh","updated_at":"2026-05-23T09:41:31Z","started_at":"2026-05-23T09:32:14Z","closed_at":"2026-05-23T09:41:31Z","close_reason":"C-side singular-scalar encode landed in runtime/pb/c/c_runtime.c. New entry pb.c_runtime.encode(plan, msg) -\u003e 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-\u003eenum_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.","dependencies":[{"issue_id":"tarantool-protobuf-y1n","depends_on_id":"tarantool-protobuf-mq7","type":"blocks","created_at":"2026-05-18T23:21:53Z","created_by":"Eugene Blikh","metadata":"{}"}],"dependency_count":1,"dependent_count":6,"comment_count":0} {"_type":"issue","id":"tarantool-protobuf-mq7","title":"ra6 3a: descriptor → C plan compiler","description":"Compile finalized Lua descriptors into a C-side plan userdata, stashed on desc.c_plan. The plan carries per-field records (field number, wire type, kind tag, presence offset, default index), pre-encoded tag bytes, luaL_ref slots for cached field names, sub-descriptor pointers, oneof grouping metadata (parallel array, matches desc.oneofs_list), extension range hooks, and WKT override pointers (desc.encode/desc.decode passthrough). Rebuilt at pb.finalize_message time when PB_ENABLE_C=1. Not serialized, not shared across processes. Foundation for all other ra6 sub-issues — no other 3* issue can start until this lands. Acceptance: pb.c_runtime.compile_plan(desc) returns a userdata; plan-\u003en_fields and plan-\u003efields[i].tag are readable from a C-level smoke test; plans for hello.Person and Address build without error.","status":"closed","priority":1,"issue_type":"task","assignee":"Eugene Blikh","owner":"bigbes@gmail.com","created_at":"2026-05-18T20:19:58Z","created_by":"Eugene Blikh","updated_at":"2026-05-19T04:46:39Z","started_at":"2026-05-19T04:26:53Z","closed_at":"2026-05-19T04:46:39Z","close_reason":"Descriptor -\u003e 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 -\u003e 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).","dependencies":[{"issue_id":"tarantool-protobuf-mq7","depends_on_id":"tarantool-protobuf-47e","type":"blocks","created_at":"2026-05-18T23:21:52Z","created_by":"Eugene Blikh","metadata":"{}"},{"issue_id":"tarantool-protobuf-mq7","depends_on_id":"tarantool-protobuf-pf6","type":"blocks","created_at":"2026-05-18T23:21:52Z","created_by":"Eugene Blikh","metadata":"{}"},{"issue_id":"tarantool-protobuf-mq7","depends_on_id":"tarantool-protobuf-z7x","type":"blocks","created_at":"2026-05-18T23:21:53Z","created_by":"Eugene Blikh","metadata":"{}"}],"dependency_count":3,"dependent_count":5,"comment_count":0} diff --git a/examples/expected/full/c_int64/c_int64_pb.lua b/examples/expected/full/c_int64/c_int64_pb.lua new file mode 100644 index 0000000000000000000000000000000000000000..a45f7d301bb4ec4e4c5b20cca304e7fb73bad8e7 --- /dev/null +++ b/examples/expected/full/c_int64/c_int64_pb.lua @@ -0,0 +1,173 @@ +-- Code generated by protoc-gen-tarantool. DO NOT EDIT. +-- source: c_int64.proto +-- syntax: proto3 +-- package: c_int64 + +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_int64"} + +-- Pre-declare message descriptors so cross-references resolve. +M.Wide_descriptor = {name = "c_int64.Wide"} + +-- Message: c_int64.Wide +M.Wide_descriptor.fields = { + {name="a_int64", id=1, kind='scalar', proto_type="int64"}, + {name="a_uint64", id=2, kind='scalar', proto_type="uint64"}, + {name="a_sint64", id=3, kind='scalar', proto_type="sint64"}, + {name="a_fixed64", id=4, kind='scalar', proto_type="fixed64"}, + {name="a_sfixed64", id=5, kind='scalar', proto_type="sfixed64"}, +} +pb.finalize_message(M.Wide_descriptor) +M.Wide_fields = pb.field_names({ + a_int64 = "a_int64", + a_uint64 = "a_uint64", + a_sint64 = "a_sint64", + a_fixed64 = "a_fixed64", + a_sfixed64 = "a_sfixed64", +}) + +-- 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_int64.Wide +---@field a_int64 integer +---@field a_uint64 integer +---@field a_sint64 integer +---@field a_fixed64 integer +---@field a_sfixed64 integer + +---@param t? c_int64.Wide +---@return c_int64.Wide +function M.Wide_new(t) return t or {} end + +---@param t c_int64.Wide +---@return string +function M.Wide_encode(t) + if type(t) ~= 'table' then + error("expected table for c_int64.Wide, got " .. type(t), 0) + end + local out, n = {}, 0 + local v + -- field 1: a_int64 + v = t.a_int64 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x08" + n = n + 1; out[n] = wire.encode_int64(v) + end + -- field 2: a_uint64 + v = t.a_uint64 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x10" + n = n + 1; out[n] = wire.encode_uint64(v) + end + -- field 3: a_sint64 + v = t.a_sint64 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x18" + n = n + 1; out[n] = wire.encode_sint64(v) + end + -- field 4: a_fixed64 + v = t.a_fixed64 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x21" + n = n + 1; out[n] = wire.encode_fixed64(v) + end + -- field 5: a_sfixed64 + v = t.a_sfixed64 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x29" + n = n + 1; out[n] = wire.encode_sfixed64(v) + end + local _exts = t._extensions + if _exts ~= nil then + local _elist = M.Wide_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_int64.Wide +function M.Wide_decode(buf) + if type(buf) ~= 'string' then + error("expected string for c_int64.Wide 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_int64(buf, pos) + result.a_int64 = val + elseif id == 2 then + local val + val, pos = wire.decode_uint64(buf, pos) + result.a_uint64 = val + elseif id == 3 then + local val + val, pos = wire.decode_sint64(buf, pos) + result.a_sint64 = val + elseif id == 4 then + local val + val, pos = wire.decode_fixed64(buf, pos) + result.a_fixed64 = val + elseif id == 5 then + local val + val, pos = wire.decode_sfixed64(buf, pos) + result.a_sfixed64 = val + else + local _ebid = M.Wide_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.Wide_decode_lazy(b) return pb.decode_lazy(M.Wide_descriptor, b) end +---@param t c_int64.Wide +---@param opts? {single_line: boolean?, indent: string?} +---@return string +function M.Wide_text(t, opts) return pb.text.encode(M.Wide_descriptor, t, opts) end + +return M diff --git a/examples/expected/runtime/c_int64/c_int64_pb.lua b/examples/expected/runtime/c_int64/c_int64_pb.lua new file mode 100644 index 0000000000000000000000000000000000000000..df51cb81c349d1aecdcb8a65baec7067d9ed0d2f --- /dev/null +++ b/examples/expected/runtime/c_int64/c_int64_pb.lua @@ -0,0 +1,63 @@ +-- Code generated by protoc-gen-tarantool. DO NOT EDIT. +-- source: c_int64.proto +-- syntax: proto3 +-- package: c_int64 + +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_int64"} + +-- Pre-declare message descriptors so cross-references resolve. +M.Wide_descriptor = {name = "c_int64.Wide"} + +-- Message: c_int64.Wide +M.Wide_descriptor.fields = { + {name="a_int64", id=1, kind='scalar', proto_type="int64"}, + {name="a_uint64", id=2, kind='scalar', proto_type="uint64"}, + {name="a_sint64", id=3, kind='scalar', proto_type="sint64"}, + {name="a_fixed64", id=4, kind='scalar', proto_type="fixed64"}, + {name="a_sfixed64", id=5, kind='scalar', proto_type="sfixed64"}, +} +pb.finalize_message(M.Wide_descriptor) +M.Wide_fields = pb.field_names({ + a_int64 = "a_int64", + a_uint64 = "a_uint64", + a_sint64 = "a_sint64", + a_fixed64 = "a_fixed64", + a_sfixed64 = "a_sfixed64", +}) + +-- 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_int64.Wide +---@field a_int64 integer +---@field a_uint64 integer +---@field a_sint64 integer +---@field a_fixed64 integer +---@field a_sfixed64 integer + +---@param t? c_int64.Wide +---@return c_int64.Wide +function M.Wide_new(t) return t or {} end +---@param t c_int64.Wide +---@return string +function M.Wide_encode(t) return pb.encode(M.Wide_descriptor, t) end +---@param b string +---@return c_int64.Wide +function M.Wide_decode(b) return pb.decode(M.Wide_descriptor, b) end +---@param b string +---@return pb.MessageView +function M.Wide_decode_lazy(b) return pb.decode_lazy(M.Wide_descriptor, b) end +---@param t c_int64.Wide +---@param opts? {single_line: boolean?, indent: string?} +---@return string +function M.Wide_text(t, opts) return pb.text.encode(M.Wide_descriptor, t, opts) end + +return M diff --git a/test/c_runtime_int64_test.lua b/test/c_runtime_int64_test.lua new file mode 100644 index 0000000000000000000000000000000000000000..ef0052e052f0797b34d452060cad25bff1cda476 --- /dev/null +++ b/test/c_runtime_int64_test.lua @@ -0,0 +1,188 @@ +-- Test for bd-awv / ra6 3l: 64-bit cdata fidelity. +-- +-- int64/uint64/sint64/fixed64/sfixed64 must round-trip as LuaJIT +-- int64_t/uint64_t cdata through the C runtime — never narrowed to +-- a Lua double. Same convention as Tarantool's msgpackffi, net.box, +-- box.tuple, and the built-in protobuf. The C decode path pushes +-- cdata via luaL_pushint64 / luaL_pushuint64; the C encode path +-- accepts cdata via luaL_toint64 / luaL_touint64 alongside plain +-- Lua numbers. +-- +-- Acceptance per bd-awv: +-- A value > 2^53 round-trips byte-equal to mode=full pure-Lua, and +-- the decoded value remains cdata. Covers all five 64-bit kinds in +-- both codegen modes. Only runs when PB_ENABLE_C=1 is set and the +-- C runtime module is loadable. + +local t = require('luatest') +local ffi = require('ffi') + +local pb = require('pb') +local c_runtime = pb.c_runtime + +local function skip_if_no_c() + if c_runtime == nil then + t.skip('PB_ENABLE_C not set or pb.c_runtime not available') + end +end + +-- Values past Lua's 2^53 double-precision boundary, picked so the +-- top bits would silently drop if any code path narrowed to double. +-- int64_max - 1, uint64_max - 1, and high bit patterns for fixed. +local U64_BIG = 0xDEADBEEFCAFEBABEULL +local U64_MAX_ISH = 0xFFFFFFFFFFFFFFFEULL -- one off all-ones +local I64_NEG_BIG = -0x0123456789ABCDEFLL -- past -2^53 +local I64_POS_BIG = 0x0123456789ABCDEFLL -- past 2^53 + +local full_wide + +for _, mode in ipairs({'full', 'runtime'}) do + local g = t.group('c_runtime_int64.' .. mode) + local wide + + g.before_all(function() + skip_if_no_c() + wide = require(mode .. '.c_int64.c_int64_pb') + full_wide = require('full.c_int64.c_int64_pb') + end) + + g.before_each(skip_if_no_c) + + -- ---------- Acceptance per bd-awv ---------- + + function g.test_acceptance_all_kinds_round_trip_past_2pow53() + -- One message carries every 64-bit kind with a value that + -- would lose precision if narrowed through a Lua double. + -- Bytes must match mode=full, and every decoded field must + -- remain cdata. + local plan = c_runtime.compile_plan(wide.Wide_descriptor) + local msg = { + a_int64 = ffi.new('int64_t', I64_POS_BIG), + a_uint64 = U64_BIG, + a_sint64 = ffi.new('int64_t', I64_NEG_BIG), + a_fixed64 = U64_MAX_ISH, + a_sfixed64 = ffi.new('int64_t', I64_NEG_BIG), + } + local c_bytes = c_runtime.encode(plan, msg) + local lua_bytes = full_wide.Wide_encode(msg) + t.assert_equals(c_bytes, lua_bytes, 'encode bytes match mode=full') + + local c_decoded = c_runtime.decode(plan, c_bytes) + local lua_decoded = full_wide.Wide_decode(c_bytes) + t.assert_equals(c_decoded, lua_decoded, 'decoded table matches mode=full') + + -- Every value must surface as cdata, not double. + for _, k in ipairs({'a_int64', 'a_uint64', 'a_sint64', + 'a_fixed64', 'a_sfixed64'}) do + t.assert_equals(type(c_decoded[k]), 'cdata', + ('field %s decoded as %s, expected cdata') + :format(k, type(c_decoded[k]))) + end + + -- Value comparison done via cdata equality so the top bits + -- are checked, not just the low 53. + t.assert_equals(c_decoded.a_int64, msg.a_int64) + t.assert_equals(c_decoded.a_uint64, msg.a_uint64) + t.assert_equals(c_decoded.a_sint64, msg.a_sint64) + t.assert_equals(c_decoded.a_fixed64, msg.a_fixed64) + t.assert_equals(c_decoded.a_sfixed64, msg.a_sfixed64) + end + + -- ---------- Per-kind: encode from cdata + decode preserves cdata -- + + function g.test_int64_cdata_round_trip() + local plan = c_runtime.compile_plan(wide.Wide_descriptor) + local v = ffi.new('int64_t', I64_POS_BIG) + local bytes = c_runtime.encode(plan, {a_int64 = v}) + t.assert_equals(bytes, full_wide.Wide_encode({a_int64 = v})) + local d = c_runtime.decode(plan, bytes) + t.assert_equals(type(d.a_int64), 'cdata') + t.assert_equals(d.a_int64, v) + end + + function g.test_int64_negative_cdata_round_trip() + local plan = c_runtime.compile_plan(wide.Wide_descriptor) + -- Negative int64 is sign-extended to 10-byte varint on the wire. + local v = ffi.new('int64_t', I64_NEG_BIG) + local bytes = c_runtime.encode(plan, {a_int64 = v}) + t.assert_equals(bytes, full_wide.Wide_encode({a_int64 = v})) + local d = c_runtime.decode(plan, bytes) + t.assert_equals(type(d.a_int64), 'cdata') + t.assert_equals(d.a_int64, v) + end + + function g.test_uint64_cdata_round_trip() + local plan = c_runtime.compile_plan(wide.Wide_descriptor) + local v = U64_BIG + local bytes = c_runtime.encode(plan, {a_uint64 = v}) + t.assert_equals(bytes, full_wide.Wide_encode({a_uint64 = v})) + local d = c_runtime.decode(plan, bytes) + t.assert_equals(type(d.a_uint64), 'cdata') + t.assert_equals(d.a_uint64, v) + end + + function g.test_uint64_near_max_cdata_round_trip() + local plan = c_runtime.compile_plan(wide.Wide_descriptor) + local v = U64_MAX_ISH + local bytes = c_runtime.encode(plan, {a_uint64 = v}) + t.assert_equals(bytes, full_wide.Wide_encode({a_uint64 = v})) + local d = c_runtime.decode(plan, bytes) + t.assert_equals(type(d.a_uint64), 'cdata') + t.assert_equals(d.a_uint64, v) + end + + function g.test_sint64_cdata_round_trip() + local plan = c_runtime.compile_plan(wide.Wide_descriptor) + local v = ffi.new('int64_t', I64_NEG_BIG) + local bytes = c_runtime.encode(plan, {a_sint64 = v}) + t.assert_equals(bytes, full_wide.Wide_encode({a_sint64 = v})) + local d = c_runtime.decode(plan, bytes) + t.assert_equals(type(d.a_sint64), 'cdata') + t.assert_equals(d.a_sint64, v) + end + + function g.test_fixed64_cdata_round_trip() + local plan = c_runtime.compile_plan(wide.Wide_descriptor) + local v = U64_BIG + local bytes = c_runtime.encode(plan, {a_fixed64 = v}) + t.assert_equals(bytes, full_wide.Wide_encode({a_fixed64 = v})) + -- fixed64 is always 8 bytes + 1 tag byte; spot-check the wire size. + t.assert_equals(#bytes, 9, 'fixed64 wire size = tag + 8B') + local d = c_runtime.decode(plan, bytes) + t.assert_equals(type(d.a_fixed64), 'cdata') + t.assert_equals(d.a_fixed64, v) + end + + function g.test_sfixed64_cdata_round_trip() + local plan = c_runtime.compile_plan(wide.Wide_descriptor) + local v = ffi.new('int64_t', I64_NEG_BIG) + local bytes = c_runtime.encode(plan, {a_sfixed64 = v}) + t.assert_equals(bytes, full_wide.Wide_encode({a_sfixed64 = v})) + local d = c_runtime.decode(plan, bytes) + t.assert_equals(type(d.a_sfixed64), 'cdata') + t.assert_equals(d.a_sfixed64, v) + end + + -- ---------- Encode also accepts plain Lua numbers (in-range) ------ + + function g.test_int64_from_lua_number_within_double_range() + -- Plain Lua numbers below 2^53 must encode identically to + -- the cdata path. This is the common case for IDs/counters + -- that happen to fit a double exactly. Tarantool's + -- luaL_pushint64 surfaces small values as Lua numbers (not + -- cdata) by convention — same as msgpackffi, net.box, the + -- built-in protobuf — so the decoded type tracks that, and + -- equality compares numerically across number/cdata. + local plan = c_runtime.compile_plan(wide.Wide_descriptor) + local msg_num = {a_int64 = 42} + local msg_cdata = {a_int64 = ffi.new('int64_t', 42)} + local b_num = c_runtime.encode(plan, msg_num) + local b_cd = c_runtime.encode(plan, msg_cdata) + t.assert_equals(b_num, b_cd, 'Lua number encodes same as cdata') + t.assert_equals(b_num, full_wide.Wide_encode(msg_num)) + local d = c_runtime.decode(plan, b_num) + t.assert_equals(d.a_int64, 42) + t.assert_equals(d, full_wide.Wide_decode(b_num), + 'C decode matches mode=full shape-for-shape') + end +end diff --git a/test/proto/c_int64.proto b/test/proto/c_int64.proto new file mode 100644 index 0000000000000000000000000000000000000000..d7079928a62e84e745f1cb417bbe4813caf40413 --- /dev/null +++ b/test/proto/c_int64.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package c_int64; + +// Fixture for bd-awv (ra6 3l): 64-bit cdata fidelity. +// +// Carries one singular field per 64-bit proto kind. The C runtime +// must encode/decode each through LuaJIT int64_t/uint64_t cdata — +// never narrow to a Lua double — so values past 2^53 round-trip +// byte-equal and remain cdata after decode. +// +// Same convention as Tarantool's msgpackffi, net.box, box.tuple, +// and the built-in protobuf. See runtime/pb/wire.lua decoder +// comments for the per-kind type mapping. + +message Wide { + int64 a_int64 = 1; // varint, sign-extended + uint64 a_uint64 = 2; // varint + sint64 a_sint64 = 3; // zigzag varint + fixed64 a_fixed64 = 4; // 8-byte LE unsigned + sfixed64 a_sfixed64 = 5; // 8-byte LE signed +}