@@ 2,7 2,7 @@
{"_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-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":"open","priority":1,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-18T20:20:24Z","created_by":"Eugene Blikh","updated_at":"2026-05-18T20:20:24Z","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":"open","priority":1,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-18T20:20:06Z","created_by":"Eugene Blikh","updated_at":"2026-05-18T20:20:06Z","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-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}
{"_type":"issue","id":"tarantool-protobuf-1eq","title":"C accel spike: generic one-call C codec variant (strategy 3)","description":"Sub-issue of tarantool-protobuf-04c. Implement strategy 3: a generic C codec that gets a descriptor + Lua table once per top-level encode/decode call and owns the inner loop. Internally dispatches per-field on descriptor kind (the work pf6 / ra6 ends up shipping in production). Measure with bench/c_accel/spike_bench.lua. Acceptance: numbers in bench/c_accel/README.md showing the gap to strategy 4 (hand-written, no dispatch).","notes":"Phase A measured strategy 4 ceiling at 3-10x over pure Lua. The 4 vs 3 gap is the key signal for pf6 architecture: \u003c20-40% gap means generic C runtime (ra6) alone is sufficient; \u003e40% gap means we need codegen-emitted C (c0i). For the spike, dispatch can be simple (switch on protoreflect.Kind); no need to design the compiled-plan format here.","status":"closed","priority":1,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-18T19:28:08Z","created_by":"Eugene Blikh","updated_at":"2026-05-18T19:44:07Z","closed_at":"2026-05-18T19:44:07Z","close_reason":"Measured as part of the 04c spike; numbers and architecture conclusion captured in 04c close, bench/c_accel/README.md, and bd memory c-accel-spike-04c-final-2026-05-18.","dependencies":[{"issue_id":"tarantool-protobuf-1eq","depends_on_id":"tarantool-protobuf-04c","type":"blocks","created_at":"2026-05-18T22:28:15Z","created_by":"Eugene Blikh","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
@@ 1144,6 1144,281 @@ encode_lua(lua_State *L)
}
/* ---------------------------------------------------------------- *
+ * Decode (bd-mz6 / ra6 3c): singular scalar fields. *
+ * *
+ * Symmetric scope with 3b. Repeated/map/message-typed fields are *
+ * skipped over by wire-type — 3d/3e/3i will overwrite this with *
+ * real handling. Unknown tags are skipped silently too; bd-wyp *
+ * will add round-trip capture. *
+ * *
+ * Result-table shape must match mode=full pure-Lua decode. That *
+ * pins per-kind value types: int64/uint64/sint64/fixed64/sfixed64 *
+ * push Tarantool int64_t/uint64_t cdata via luaL_pushint64 / *
+ * luaL_pushuint64; everything else pushes a Lua number, boolean, *
+ * or string. See runtime/pb/wire.lua decoder comments. *
+ * ---------------------------------------------------------------- */
+
+typedef struct dec_ctx {
+ lua_State *L;
+ const uint8_t *buf;
+ size_t len;
+ size_t pos;
+} dec_ctx;
+
+static uint64_t
+dec_varint(dec_ctx *c)
+{
+ uint64_t v = 0;
+ int shift = 0;
+ for (int i = 0; i < 10; i++) {
+ if (c->pos >= c->len)
+ luaL_error(c->L, "truncated varint at offset %d",
+ (int)c->pos);
+ uint8_t b = c->buf[c->pos++];
+ v |= ((uint64_t)(b & 0x7f)) << shift;
+ if ((b & 0x80) == 0)
+ return v;
+ shift += 7;
+ }
+ luaL_error(c->L, "varint exceeds 10 bytes at offset %d",
+ (int)c->pos);
+ return 0;
+}
+
+static uint32_t
+dec_fixed32(dec_ctx *c)
+{
+ if (c->len - c->pos < 4)
+ luaL_error(c->L, "truncated fixed32 at offset %d",
+ (int)c->pos);
+ uint32_t v = (uint32_t)c->buf[c->pos]
+ | ((uint32_t)c->buf[c->pos + 1] << 8)
+ | ((uint32_t)c->buf[c->pos + 2] << 16)
+ | ((uint32_t)c->buf[c->pos + 3] << 24);
+ c->pos += 4;
+ return v;
+}
+
+static uint64_t
+dec_fixed64(dec_ctx *c)
+{
+ if (c->len - c->pos < 8)
+ luaL_error(c->L, "truncated fixed64 at offset %d",
+ (int)c->pos);
+ uint64_t v = 0;
+ for (int i = 0; i < 8; i++)
+ v |= ((uint64_t)c->buf[c->pos + i]) << (i * 8);
+ c->pos += 8;
+ return v;
+}
+
+static void
+dec_skip(dec_ctx *c, uint8_t wt)
+{
+ switch (wt) {
+ case PB_WIRE_VARINT:
+ (void)dec_varint(c);
+ break;
+ case PB_WIRE_I32:
+ (void)dec_fixed32(c);
+ break;
+ case PB_WIRE_I64:
+ (void)dec_fixed64(c);
+ break;
+ case PB_WIRE_LEN: {
+ uint64_t plen = dec_varint(c);
+ if (c->len - c->pos < plen)
+ luaL_error(c->L, "truncated length-delimited payload");
+ c->pos += (size_t)plen;
+ break;
+ }
+ default:
+ luaL_error(c->L, "unsupported wire type %d for skip", (int)wt);
+ }
+}
+
+static inline int32_t
+zigzag32_dec(uint32_t u)
+{
+ return (int32_t)((u >> 1) ^ (~(u & 1) + 1));
+}
+
+static inline int64_t
+zigzag64_dec(uint64_t u)
+{
+ return (int64_t)((u >> 1) ^ (~(u & 1) + 1));
+}
+
+/* Decode a single value for field `f` and push it onto the Lua stack. */
+static void
+dec_push_one(dec_ctx *c, pb_plan_field *f)
+{
+ switch (f->kind) {
+ case PB_KIND_INT32: {
+ uint64_t u = dec_varint(c);
+ int32_t v = (int32_t)(uint32_t)u;
+ lua_pushinteger(c->L, v);
+ return;
+ }
+ case PB_KIND_INT64: {
+ uint64_t u = dec_varint(c);
+ luaL_pushint64(c->L, (int64_t)u);
+ return;
+ }
+ case PB_KIND_UINT32: {
+ uint64_t u = dec_varint(c);
+ uint32_t v = (uint32_t)u;
+ lua_pushnumber(c->L, (double)v);
+ return;
+ }
+ case PB_KIND_UINT64: {
+ uint64_t u = dec_varint(c);
+ luaL_pushuint64(c->L, u);
+ return;
+ }
+ case PB_KIND_SINT32: {
+ uint64_t u = dec_varint(c);
+ int32_t v = zigzag32_dec((uint32_t)u);
+ lua_pushinteger(c->L, v);
+ return;
+ }
+ case PB_KIND_SINT64: {
+ uint64_t u = dec_varint(c);
+ luaL_pushint64(c->L, zigzag64_dec(u));
+ return;
+ }
+ case PB_KIND_BOOL: {
+ uint64_t u = dec_varint(c);
+ lua_pushboolean(c->L, u != 0);
+ return;
+ }
+ case PB_KIND_ENUM: {
+ uint64_t u = dec_varint(c);
+ int32_t v = (int32_t)(uint32_t)u;
+ lua_pushinteger(c->L, v);
+ return;
+ }
+ case PB_KIND_FIXED32: {
+ uint32_t u = dec_fixed32(c);
+ lua_pushnumber(c->L, (double)u);
+ return;
+ }
+ case PB_KIND_SFIXED32: {
+ uint32_t u = dec_fixed32(c);
+ lua_pushinteger(c->L, (int32_t)u);
+ return;
+ }
+ case PB_KIND_FLOAT: {
+ uint32_t u = dec_fixed32(c);
+ union { uint32_t u; float f; } pun;
+ pun.u = u;
+ lua_pushnumber(c->L, (double)pun.f);
+ return;
+ }
+ case PB_KIND_FIXED64: {
+ uint64_t u = dec_fixed64(c);
+ luaL_pushuint64(c->L, u);
+ return;
+ }
+ case PB_KIND_SFIXED64: {
+ uint64_t u = dec_fixed64(c);
+ luaL_pushint64(c->L, (int64_t)u);
+ return;
+ }
+ case PB_KIND_DOUBLE: {
+ uint64_t u = dec_fixed64(c);
+ union { uint64_t u; double d; } pun;
+ pun.u = u;
+ lua_pushnumber(c->L, pun.d);
+ return;
+ }
+ case PB_KIND_STRING:
+ case PB_KIND_BYTES: {
+ uint64_t plen = dec_varint(c);
+ if (c->len - c->pos < plen)
+ luaL_error(c->L, "truncated string/bytes payload");
+ lua_pushlstring(c->L, (const char *)(c->buf + c->pos),
+ (size_t)plen);
+ c->pos += (size_t)plen;
+ return;
+ }
+ default:
+ luaL_error(c->L, "dec_push_one: unsupported kind %d",
+ (int)f->kind);
+ }
+}
+
+static int
+decode_lua(lua_State *L)
+{
+ pb_plan *plan = (pb_plan *)luaL_checkudata(L, 1, PB_PLAN_MT);
+ size_t buf_len;
+ const char *buf = luaL_checklstring(L, 2, &buf_len);
+
+ if (plan->has_override) {
+ return luaL_error(L,
+ "C decode on plan with override is unsupported (bd-rmf)");
+ }
+
+ /* Pre-size the result table: at most n_fields singular entries. */
+ lua_createtable(L, 0, plan->n_fields);
+ int result_idx = lua_gettop(L);
+
+ /* Pin field-names so the loop can rawgeti by 1-based field index. */
+ if (plan->field_names_ref != LUA_NOREF)
+ lua_rawgeti(L, LUA_REGISTRYINDEX, plan->field_names_ref);
+ else
+ lua_pushnil(L);
+ int names_idx = lua_gettop(L);
+
+ dec_ctx c;
+ c.L = L;
+ c.buf = (const uint8_t *)buf;
+ c.len = buf_len;
+ c.pos = 0;
+
+ while (c.pos < c.len) {
+ uint64_t tag = dec_varint(&c);
+ uint32_t field_number = (uint32_t)(tag >> 3);
+ uint8_t wt = (uint8_t)(tag & 0x07);
+
+ /* Linear scan over plan->fields. n_fields is typically small;
+ * tag-keyed dispatch table is a future optimization. */
+ pb_plan_field *f = NULL;
+ for (int i = 0; i < plan->n_fields; i++) {
+ if (plan->fields[i].field_number == field_number) {
+ f = &plan->fields[i];
+ break;
+ }
+ }
+
+ /* Out-of-scope shapes for 3c: repeated, map, message. Skip
+ * by wire type so 3d/3e/3i can replace this branch later. */
+ if (f == NULL
+ || f->repeated
+ || f->kind == PB_KIND_MAP
+ || f->kind == PB_KIND_MESSAGE) {
+ dec_skip(&c, wt);
+ continue;
+ }
+
+ dec_push_one(&c, f); /* stack: ..., names, value */
+
+ /* result[name] = value. Field index within plan = pointer
+ * arithmetic against plan->fields[0]; +1 for the 1-based
+ * field_names_ref convention used elsewhere. */
+ int field_idx_1based = (int)(f - plan->fields) + 1;
+ lua_rawgeti(L, names_idx, field_idx_1based); /* push name */
+ lua_insert(L, -2); /* name, value */
+ lua_rawset(L, result_idx); /* result[name] = value */
+ }
+
+ /* Drop names ref; leave result on top. */
+ lua_pop(L, 1);
+ return 1;
+}
+
+/* ---------------------------------------------------------------- *
* Module entry. *
* ---------------------------------------------------------------- */
@@ 1157,6 1432,7 @@ static const struct luaL_Reg c_runtime_methods[] = {
{"plan_has_override", plan_has_override},
{"plan_sub_plan", plan_sub_plan},
{"encode", encode_lua},
+ {"decode", decode_lua},
{NULL, NULL},
};
@@ 0,0 1,178 @@
+-- Test for bd-mz6 / ra6 3c: C-side scalar decode.
+--
+-- Only runs when PB_ENABLE_C=1 is set in the environment AND the C
+-- runtime module is loadable. Otherwise the group is skipped, mirroring
+-- the gate on c_runtime_encode_test.lua.
+--
+-- Acceptance per bd-mz6:
+-- Person decode round-trip works for the bytes produced by 3b; the
+-- decoded table is shape-identical to pure-Lua decode for the same
+-- input bytes (both codegen modes).
+
+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
+
+-- mode=full is the byte-equal reference for both encode and decode.
+local full_hello
+
+for _, mode in ipairs({'full', 'runtime'}) do
+ local g = t.group('c_runtime_decode.' .. mode)
+ local hello
+
+ g.before_all(function()
+ skip_if_no_c()
+ hello = require(mode .. '.hello.hello_pb')
+ full_hello = require('full.hello.hello_pb')
+ end)
+
+ g.before_each(skip_if_no_c)
+
+ -- ---------- Acceptance per bd-mz6 ----------
+
+ function g.test_acceptance_person_round_trip_from_3b_bytes()
+ local msg = {
+ name = 'x',
+ age = 42,
+ balance = -7,
+ user_id = 0xDEADBEEFCAFEBABEULL,
+ weight_kg = 3.14,
+ }
+ local plan = c_runtime.compile_plan(hello.Person_descriptor)
+ local bytes = c_runtime.encode(plan, msg)
+ local c_decoded = c_runtime.decode(plan, bytes)
+ local lua_decoded = full_hello.Person_decode(bytes)
+ t.assert_equals(c_decoded, lua_decoded,
+ 'C decode matches mode=full pure-Lua decode shape-for-shape')
+ end
+
+ -- ---------- Per-kind coverage ----------
+
+ function g.test_empty_input_produces_empty_table()
+ local plan = c_runtime.compile_plan(hello.Address_descriptor)
+ t.assert_equals(c_runtime.decode(plan, ''), {})
+ end
+
+ function g.test_address_strings_and_int32()
+ local plan = c_runtime.compile_plan(hello.Address_descriptor)
+ local msg = {street = 'Main', city = 'Springfield', zip = 12345}
+ local bytes = full_hello.Address_encode(msg)
+ t.assert_equals(c_runtime.decode(plan, bytes),
+ full_hello.Address_decode(bytes))
+ end
+
+ function g.test_proto3_optional_empty_string_present()
+ -- Address.apartment is proto3-optional; presence beats default.
+ -- 3b emits the tag for apartment='' (optional bypasses suppression),
+ -- so decode must surface the empty string in the result table.
+ local plan = c_runtime.compile_plan(hello.Address_descriptor)
+ local bytes = full_hello.Address_encode({apartment = ''})
+ local lua_decoded = full_hello.Address_decode(bytes)
+ local c_decoded = c_runtime.decode(plan, bytes)
+ t.assert_equals(c_decoded, lua_decoded)
+ t.assert_equals(c_decoded.apartment, '')
+ end
+
+ function g.test_double_negative_zero_decodes()
+ local plan = c_runtime.compile_plan(hello.Person_descriptor)
+ -- LuaJIT constant-folds the literal -0.0 to +0.0, so build the
+ -- sign-bit-set zero at runtime via a cdata round-trip. Bytes
+ -- encoded from this value must round-trip to a value whose 1/x
+ -- is -inf — the only way to distinguish -0.0 from +0.0 in Lua.
+ local neg_zero = ffi.new('double[1]', 0)[0] * -1
+ local bytes = full_hello.Person_encode({weight_kg = neg_zero})
+ t.assert(#bytes > 0, '-0.0 should not be zero-suppressed')
+ local c_decoded = c_runtime.decode(plan, bytes)
+ t.assert_equals(1 / c_decoded.weight_kg, -math.huge)
+ end
+
+ function g.test_enum_as_number_value()
+ local plan = c_runtime.compile_plan(hello.Person_descriptor)
+ local bytes = full_hello.Person_encode({status = 2}) -- ERROR
+ 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.status, 2)
+ end
+
+ function g.test_fixed64_returns_uint64_cdata()
+ local plan = c_runtime.compile_plan(hello.Person_descriptor)
+ local msg = {user_id = ffi.new('uint64_t', 0x123456789ABCDEF0)}
+ local bytes = full_hello.Person_encode(msg)
+ local c_decoded = c_runtime.decode(plan, bytes)
+ t.assert_equals(type(c_decoded.user_id), 'cdata')
+ t.assert_equals(c_decoded.user_id, msg.user_id)
+ end
+
+ function g.test_sint32_negative_round_trip()
+ local plan = c_runtime.compile_plan(hello.Person_descriptor)
+ local bytes = full_hello.Person_encode({balance = -1})
+ t.assert_equals(c_runtime.decode(plan, bytes),
+ full_hello.Person_decode(bytes))
+ bytes = full_hello.Person_encode({balance = 0x7fffffff})
+ t.assert_equals(c_runtime.decode(plan, bytes),
+ full_hello.Person_decode(bytes))
+ end
+
+ function g.test_bytes_field_preserves_high_bytes()
+ local plan = c_runtime.compile_plan(hello.Person_descriptor)
+ local bytes = full_hello.Person_encode({avatar = '\x00\x01\xff\xfe'})
+ 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_message_tags()
+ -- 3c scope: repeated/map/message tags are skipped over by wire
+ -- type rather than decoded (3d/3e/3i extend this). The result
+ -- table contains only the singular scalar fields, matching what
+ -- a 3b-encoded input would have produced anyway.
+ local plan = c_runtime.compile_plan(hello.Person_descriptor)
+ -- Build bytes via the full Lua codec, then decode through C.
+ local rich = {
+ name = 'x',
+ emails = {'a@b'}, -- repeated string
+ address = {street = 'Main'}, -- message
+ lucky_numbers = {1, 2, 3}, -- repeated packed
+ ages_by_nickname = {alice = 30}, -- map
+ }
+ local bytes = full_hello.Person_encode(rich)
+ local c_decoded = c_runtime.decode(plan, bytes)
+ t.assert_equals(c_decoded, {name = 'x'})
+ end
+
+ function g.test_skips_unknown_field_tags()
+ -- Append a synthetic unknown-field tag (id=999, wire varint=0)
+ -- to a valid encoding. The C decoder should skip it, not error.
+ local good = full_hello.Person_encode({name = 'x'})
+ -- field 999, wire 0 => tag varint = (999<<3) | 0 = 7992 => 0xF8 0x3E
+ local unknown_tag = '\xf8\x3e\x05' -- tag + varint value 5
+ local plan = c_runtime.compile_plan(hello.Person_descriptor)
+ local c_decoded = c_runtime.decode(plan, good .. unknown_tag)
+ t.assert_equals(c_decoded, {name = 'x'})
+ end
+
+ function g.test_wkt_override_rejected()
+ -- has_override plans don't expose field arrays; mirror 3b's gate.
+ local plan = c_runtime.compile_plan(pb.wkt.Timestamp_descriptor)
+ t.assert_error_msg_contains('override', function()
+ c_runtime.decode(plan, '')
+ end)
+ end
+
+ function g.test_truncated_input_errors()
+ local plan = c_runtime.compile_plan(hello.Person_descriptor)
+ -- Tag for field 1 (string) followed by length-prefix without the
+ -- string bytes that the length claims.
+ t.assert_error_msg_contains('truncated', function()
+ c_runtime.decode(plan, '\x0a\x05ab')
+ end)
+ end
+end