M .beads/issues.jsonl => .beads/issues.jsonl +1 -1
@@ 15,7 15,7 @@
{"_type":"issue","id":"tarantool-protobuf-h8v","title":"Encoder: codegen-time inline FFI writes (mode=full)","description":"Replace per-field wire.encode_* calls + ../table.concat chain with directly-emitted FFI byte writes at every field site in mode=full. Today every encode_len(s) is 'encode_varint(#s) .. s' — two allocations and a concat per string. Sidesteps the per-byte b:alloc(1) cliff that sank the earlier ibuf attempt (memory: tarantool_ibuf_perf). Expected: 1.5-2x encode throughput; alloc/op drops from ~130 KB at 100 KB Person to near zero. Biggest single perf move. Bench reference: bench/starwing_bench.lua + bench/COMPARISON.md show starwing C encoder at 2.3-3x ours across all sizes.","status":"closed","priority":1,"issue_type":"task","assignee":"Eugene Blikh","owner":"bigbes@gmail.com","created_at":"2026-05-17T15:47:04Z","created_by":"Eugene Blikh","updated_at":"2026-05-18T18:31:54Z","started_at":"2026-05-18T18:25:39Z","closed_at":"2026-05-18T18:31:54Z","close_reason":"Inlined 1-byte varint length prefix at every LEN emit site; +25-48% encode at 1KB+ Person. Full FFI-buffer rewrite deferred — this slice captured the dominant profile target without that level of disruption. See bench/PERF_LOG.md entry for details.","labels":["codegen","encoder","perf"],"dependency_count":0,"dependent_count":2,"comment_count":0}
{"_type":"issue","id":"tarantool-protobuf-e4t","title":"LSP: plugin emits @class type stubs for generated messages","description":"Phase 3 of the LSP / LLM affordance work. Follows phase 1 (commit 8721548 — runtime annotations) and phase 2 (bd-74c — annotate codec/grpc/json/wkt).\n\nHighest-payoff phase for LLM grounding. Modify the protoc plugin (cmd/protoc-gen-tarantool/internal/gen) so each generated _pb.lua module emits ---@class blocks describing every message, plus typed @param/@return on the _encode/_decode/_descriptor surface. After this lands, anyone (human or LLM) who require('full.hello.hello_pb') gets full type info on Person, Address, etc., without reading the .proto.\n\nEmit shape (decision: inline at top of generated file; one file per .proto already includes all messages from that file):\n\n ---@class hello.Person\n ---@field name? string\n ---@field user_id? ffi.cdata* # uint64\n ---@field age? integer # int32\n ---@field weight_kg? number # double\n ---@field emails? string[]\n ---@field address? hello.Address\n ---@field lucky_numbers? integer[]\n ---@field favorite_color? hello.Color # enum alias\n ---@field tags? table\u003cstring,string\u003e # map\u003cstring,string\u003e\n ---@field unknown_fields? string\n\n ---@param tbl hello.Person\n ---@return string\n function M.Person_encode(tbl) ... end\n\n ---@param bytes string\n ---@return hello.Person\n function M.Person_decode(bytes) ... end\n\nField-kind -\u003e Lua-type mapping:\n- scalar int32/uint32/sint32/fixed32/sfixed32/bool → integer/boolean\n- scalar int64/uint64/sint64/fixed64/sfixed64 → ffi.cdata* (LuaJIT cdata, per project convention)\n- scalar float/double → number\n- scalar string/bytes → string\n- enum → alias of integer (emit ---@alias hello.Color integer)\n- message → another @class reference\n- repeated T → T[]\n- map\u003cK,V\u003e → table\u003cK_lua, V_lua\u003e\n- oneof — all members are optional, generator should NOT emit a discriminator field; user calls view:which() / inspects which value is non-nil\n\nAll fields are optional (? suffix) since proto3 default-elision means absence-on-wire is indistinguishable from default value. Required fields in proto2 omit the ?.\n\nBoth mode=full and mode=runtime emit the same _encode/_decode wrapper signatures, so the stubs apply uniformly.\n\nVerify by:\n1. Regenerating examples/expected/ via just gen\n2. Confirming the generated _pb.lua files load and pass tests (no behavior change)\n3. Opening examples/expected/full/hello/hello_pb.lua in an LSP-aware editor and checking that hover on hello.Person, M.Person_encode shows the @class + typed signature\n4. Sanity: require a generated module from spike code and confirm autocomplete on the result-table field names","acceptance_criteria":"Plugin emits ---@class blocks + typed _encode/_decode signatures into every generated _pb.lua; just gen + just test pass; LSP hover on require('full.hello.hello_pb').Person_encode shows ---@param tbl hello.Person ---@return string; ---@class hello.Person is reachable via hover on a decoded value","status":"open","priority":2,"issue_type":"feature","owner":"bigbes@gmail.com","created_at":"2026-05-23T09:30:19Z","created_by":"Eugene Blikh","updated_at":"2026-05-23T09:30:19Z","labels":["codegen","lsp"],"dependencies":[{"issue_id":"tarantool-protobuf-e4t","depends_on_id":"tarantool-protobuf-74c","type":"blocks","created_at":"2026-05-23T12:30:18Z","created_by":"Eugene Blikh","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"tarantool-protobuf-74c","title":"LSP: annotate codec/grpc/json/wkt with EmmyLua types","description":"Phase 2 of the LSP / LLM affordance work (phase 1 landed in commit 8721548 — .luarc.json, runtime/pb/_types.lua, init.lua + lazy.lua annotations).\n\nAdd ---@param / ---@return annotations to the remaining public-surface modules so editor hover and LLM context show types beyond just init.lua and the lazy views:\n\n- runtime/pb/codec.lua — encode(desc, t) / decode(desc, b); compile_writers / compile_readers (called from pb.finalize_message)\n- runtime/pb/grpc.lua — loopback / multiplex transport factories; the service client (factory(transport) -\u003e client_methods) and server (impl -\u003e {service, methods}) shapes\n- runtime/pb/json.lua — pb.json.encode(desc, t, opts) / pb.json.decode(desc, s, opts); opts shape (preserve_proto_field_names, emit_unpopulated, ...)\n- runtime/pb/wkt.lua — register(full_name, desc), lookup, any_pack/any_unpack, NULL sentinel\n\nFoundational @class declarations (pb.Descriptor, pb.Field, pb.GrpcTransport, pb.Json, pb.Wkt, pb.Module) already live in runtime/pb/_types.lua — extend as needed for opts shapes / map\u003cK,V\u003e generics.\n\nPure metadata change — no runtime behavior. Verify by running just test (must stay at 748 passing) and by opening a generated _pb.lua in an LSP-aware editor and confirming hover on require('pb').encode / .grpc.loopback / .json.encode shows the right signatures.","acceptance_criteria":"just test still green (748+ tests); hover in lua-language-server-aware editor shows typed signatures for pb.encode, pb.decode, grpc.loopback, json.encode, json.decode, wkt.register","status":"open","priority":2,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-23T09:29:55Z","created_by":"Eugene Blikh","updated_at":"2026-05-23T09:29:55Z","labels":["docs","lsp"],"dependency_count":0,"dependent_count":1,"comment_count":0}
-{"_type":"issue","id":"tarantool-protobuf-rmf","title":"ra6 3k: WKT override-hook passthrough","description":"If a plan's descriptor has desc.encode and desc.decode set (the WKT pattern from runtime/pb/wkt.lua), the C runtime must call those Lua functions instead of walking fields. Plan compiler stores luaL_ref to those functions; encode/decode entry points check first. This is also the extension point for future per-message codegen C (c0i) — same hook. Depends on 3a only (just adds a check at entry to the encode/decode loop). Acceptance: hello.Event (uses Timestamp, Duration, Any, FieldMask, Wrappers, Struct) round-trips with PB_ENABLE_C=1 producing byte-equal output to mode=full; the WKT module is unmodified.","status":"open","priority":2,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-18T20:21:13Z","created_by":"Eugene Blikh","updated_at":"2026-05-18T20:21:13Z","dependencies":[{"issue_id":"tarantool-protobuf-rmf","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-rmf","title":"ra6 3k: WKT override-hook passthrough","description":"If a plan's descriptor has desc.encode and desc.decode set (the WKT pattern from runtime/pb/wkt.lua), the C runtime must call those Lua functions instead of walking fields. Plan compiler stores luaL_ref to those functions; encode/decode entry points check first. This is also the extension point for future per-message codegen C (c0i) — same hook. Depends on 3a only (just adds a check at entry to the encode/decode loop). Acceptance: hello.Event (uses Timestamp, Duration, Any, FieldMask, Wrappers, Struct) round-trips with PB_ENABLE_C=1 producing byte-equal output to mode=full; the WKT module is unmodified.","status":"in_progress","priority":2,"issue_type":"task","assignee":"Eugene Blikh","owner":"bigbes@gmail.com","created_at":"2026-05-18T20:21:13Z","created_by":"Eugene Blikh","updated_at":"2026-05-23T18:17:19Z","started_at":"2026-05-23T18:17:19Z","dependencies":[{"issue_id":"tarantool-protobuf-rmf","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-m7u","title":"ra6 3i: proto2 — required, defaults, groups, extensions","description":"Proto2 semantic differences from proto3 that the C runtime must honor: (1) Required fields: decode-time enforcement when explicitly opted in (or default to skipping per existing pure-Lua behavior — match exactly); encode-time enforcement same. (2) Field defaults: present in the plan; emit them when missing on encode and substitute on decode where proto2 semantics require. (3) Groups (SGROUP/EGROUP wire format): encode/decode the framed group syntax. (4) Extensions: walk extension ranges, use plan-cached extension descriptor pointers, store in result._extensions. (5) Closed enum semantics for proto2. Depends on 3b + 3c + 3d + 3e + 3g + 3j (it's the join point — proto2 touches scalars, sub-messages, repeated, oneofs, unknown-fields). Acceptance: proto2_basic.BenchPayload and test_messages_proto2 round-trip byte-equal to mode=full at all bench sizes; conformance proto2 suite passes under PB_ENABLE_C=1.","status":"open","priority":2,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-18T20:21:03Z","created_by":"Eugene Blikh","updated_at":"2026-05-18T20:21:03Z","dependencies":[{"issue_id":"tarantool-protobuf-m7u","depends_on_id":"tarantool-protobuf-hwe","type":"blocks","created_at":"2026-05-18T23:22:21Z","created_by":"Eugene Blikh","metadata":"{}"},{"issue_id":"tarantool-protobuf-m7u","depends_on_id":"tarantool-protobuf-jc9","type":"blocks","created_at":"2026-05-18T23:22:22Z","created_by":"Eugene Blikh","metadata":"{}"},{"issue_id":"tarantool-protobuf-m7u","depends_on_id":"tarantool-protobuf-mz6","type":"blocks","created_at":"2026-05-18T23:22:20Z","created_by":"Eugene Blikh","metadata":"{}"},{"issue_id":"tarantool-protobuf-m7u","depends_on_id":"tarantool-protobuf-w3u","type":"blocks","created_at":"2026-05-18T23:22:22Z","created_by":"Eugene Blikh","metadata":"{}"},{"issue_id":"tarantool-protobuf-m7u","depends_on_id":"tarantool-protobuf-wyp","type":"blocks","created_at":"2026-05-18T23:22:23Z","created_by":"Eugene Blikh","metadata":"{}"},{"issue_id":"tarantool-protobuf-m7u","depends_on_id":"tarantool-protobuf-y1n","type":"blocks","created_at":"2026-05-18T23:22:20Z","created_by":"Eugene Blikh","metadata":"{}"}],"dependency_count":6,"dependent_count":1,"comment_count":0}
{"_type":"issue","id":"tarantool-protobuf-asz","title":"ra6 3h: maps (entry-as-pseudo-message)","description":"map\u003cK,V\u003e fields are wire-encoded as repeated messages with synthetic Entry { key=1; value=2 } shape. Compile a map plan: outer field is K_REPEATED_MESSAGE with a synthesized Entry sub-plan. Encode: walk the Lua map via pairs(), emit each entry (this is the documented JIT exception — map fields are allowed pairs() in the hot path per CLAUDE.md). Decode: each entry yields a key + value, lua_settable into the result map. Depends on 3d (uses sub-message machinery). Acceptance: Person.ages_by_nickname / nickname_by_age / addresses_by_label round-trip; multi-key map fixtures behave correctly even though encode byte order differs (the existing map_test pattern).","status":"closed","priority":2,"issue_type":"task","assignee":"Eugene Blikh","owner":"bigbes@gmail.com","created_at":"2026-05-18T20:20:57Z","created_by":"Eugene Blikh","updated_at":"2026-05-23T17:07:06Z","started_at":"2026-05-23T16:45:29Z","closed_at":"2026-05-23T17:07:06Z","close_reason":"ra6 3h: maps (entry-as-pseudo-message) — encode walks the user map with lua_next (CLAUDE.md JIT exception), builds each entry payload in a stack-backed sub-buffer with synthetic tag(1,key) + tag(2,value), proto3-elides defaults independently for key and value, and emits outer tag + len + payload into the parent buffer. Map\u003c,message\u003e resolves the value sub-plan once and recurses through encode_body. Decode reads the entry payload bounded, dispatches inner id=1/id=2 (skipping anything else per spec), and lua_rawsets into a lazy-created result map table; missing key or value falls back to the proto3 zero. Reuses the existing list_stack_idx[] slot for the lazy map cache (a field is either repeated or map, never both). 12 new tests cover round-trip for ages_by_nickname (string→int32), nickname_by_age (int32→string), and addresses_by_label (string→message), plus default-elision, multi-key correctness, empty maps, and unknown-inner-id tolerance. Full suite 964/964 with PB_ENABLE_C=1.","dependencies":[{"issue_id":"tarantool-protobuf-asz","depends_on_id":"tarantool-protobuf-hwe","type":"blocks","created_at":"2026-05-18T23:22:19Z","created_by":"Eugene Blikh","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
{"_type":"issue","id":"tarantool-protobuf-wyp","title":"ra6 3j: unknown-fields capture","description":"A C-runtime decoder that meets fields not in the plan must capture their raw bytes into result._unknown_fields, and a subsequent C encode must re-emit them verbatim — same contract as test/unknown_test.lua against the pure-Lua paths. Bytes go in as a Lua string keyed by field number (mirror the existing convention). Depends on 3b + 3c (need the basic decode loop to know where to splice the unknown bytes). Acceptance: test/unknown_test.lua passes under PB_ENABLE_C=1; an evolved schema decoding bytes written by an old schema preserves the unknown fields through a C-side re-encode.","status":"closed","priority":2,"issue_type":"task","assignee":"Eugene Blikh","owner":"bigbes@gmail.com","created_at":"2026-05-18T20:20:40Z","created_by":"Eugene Blikh","updated_at":"2026-05-23T18:04:38Z","started_at":"2026-05-23T17:54:53Z","closed_at":"2026-05-23T18:04:38Z","close_reason":"Closed","dependencies":[{"issue_id":"tarantool-protobuf-wyp","depends_on_id":"tarantool-protobuf-mz6","type":"blocks","created_at":"2026-05-18T23:22:10Z","created_by":"Eugene Blikh","metadata":"{}"},{"issue_id":"tarantool-protobuf-wyp","depends_on_id":"tarantool-protobuf-y1n","type":"blocks","created_at":"2026-05-18T23:22:09Z","created_by":"Eugene Blikh","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0}
M runtime/pb/c/c_runtime.c => runtime/pb/c/c_runtime.c +126 -36
@@ 1459,8 1459,6 @@ static void
encode_submessage_field(lua_State *L, enc_buf *b, pb_plan *plan,
pb_plan_field *f, int val_idx)
{
- if (lua_type(L, val_idx) != LUA_TTABLE)
- luaL_error(L, "message field requires a table value");
if (plan->sub_plans_ref == LUA_NOREF)
luaL_error(L, "plan '%s' has no sub-plans table",
plan->name != NULL ? plan->name : "?");
@@ 1479,6 1477,32 @@ encode_submessage_field(lua_State *L, enc_buf *b, pb_plan *plan,
luaL_error(L, "sub-plan at index %d is not a userdata",
f->sub_plan_idx);
+ /* WKT override: call desc.encode(value) for the body bytes; emit
+ * tag + len + body verbatim. Accepts any Lua type (datetime cdata,
+ * number, string, table) per the override's contract. */
+ if (subplan->override_encode_ref != LUA_NOREF) {
+ lua_rawgeti(L, LUA_REGISTRYINDEX,
+ subplan->override_encode_ref);
+ lua_pushvalue(L, val_idx);
+ lua_call(L, 1, 1);
+ if (lua_type(L, -1) != LUA_TSTRING)
+ luaL_error(L,
+ "WKT encode for '%s' returned non-string",
+ subplan->name != NULL ? subplan->name : "?");
+ size_t blen;
+ const char *bp = lua_tolstring(L, -1, &blen);
+ ebuf_reserve(L, b, f->tag_len + 10 + blen);
+ ebuf_put_tag(b, f);
+ ebuf_put_varint(b, (uint64_t)blen);
+ if (blen > 0)
+ ebuf_put_bytes(b, (const uint8_t *)bp, blen);
+ lua_settop(L, saved_top);
+ return;
+ }
+
+ if (lua_type(L, val_idx) != LUA_TTABLE)
+ luaL_error(L, "message field requires a table value");
+
enc_buf sub;
ebuf_init(&sub);
encode_body(L, &sub, subplan, val_idx);
@@ 1570,19 1594,41 @@ encode_map_field(lua_State *L, enc_buf *b, pb_plan *plan,
/* Value. Messages always emit (presence is meaningful); other
* kinds proto3-elide on default. */
if (f->map_value_kind == PB_KIND_MESSAGE) {
- if (lua_type(L, v_idx) != LUA_TTABLE)
- luaL_error(L,
- "map<,message> value must be a table");
- enc_buf vbody;
- ebuf_init(&vbody);
- encode_body(L, &vbody, value_subplan, v_idx);
- ebuf_reserve(L, &entry,
- 1 + 10 + vbody.used);
- ebuf_put_byte(&entry, val_tag);
- ebuf_put_varint(&entry, (uint64_t)vbody.used);
- if (vbody.used > 0)
- ebuf_put_bytes(&entry, ebuf_base(&vbody),
- vbody.used);
+ if (value_subplan->override_encode_ref != LUA_NOREF) {
+ /* WKT-typed map value: override owns body. */
+ lua_rawgeti(L, LUA_REGISTRYINDEX,
+ value_subplan->override_encode_ref);
+ lua_pushvalue(L, v_idx);
+ lua_call(L, 1, 1);
+ if (lua_type(L, -1) != LUA_TSTRING)
+ luaL_error(L,
+ "WKT encode for '%s' returned non-string",
+ value_subplan->name != NULL
+ ? value_subplan->name : "?");
+ size_t blen;
+ const char *bp = lua_tolstring(L, -1, &blen);
+ ebuf_reserve(L, &entry, 1 + 10 + blen);
+ ebuf_put_byte(&entry, val_tag);
+ ebuf_put_varint(&entry, (uint64_t)blen);
+ if (blen > 0)
+ ebuf_put_bytes(&entry,
+ (const uint8_t *)bp, blen);
+ lua_pop(L, 1);
+ } else {
+ if (lua_type(L, v_idx) != LUA_TTABLE)
+ luaL_error(L,
+ "map<,message> value must be a table");
+ enc_buf vbody;
+ ebuf_init(&vbody);
+ encode_body(L, &vbody, value_subplan, v_idx);
+ ebuf_reserve(L, &entry,
+ 1 + 10 + vbody.used);
+ ebuf_put_byte(&entry, val_tag);
+ ebuf_put_varint(&entry, (uint64_t)vbody.used);
+ if (vbody.used > 0)
+ ebuf_put_bytes(&entry, ebuf_base(&vbody),
+ vbody.used);
+ }
} else if (!value_is_default_kind(L, f->map_value_kind,
v_idx)) {
ebuf_reserve(L, &entry, 1);
@@ 1609,9 1655,12 @@ encode_body(lua_State *L, enc_buf *b, pb_plan *plan, int msg_idx)
{
msg_idx = abs_idx(L, msg_idx);
- if (plan->has_override) {
+ if (plan->override_encode_ref != LUA_NOREF) {
+ /* Defensive: callers must dispatch via the override Lua-ref
+ * directly. Reaching encode_body here means a code-path bug. */
luaL_error(L,
- "C encode on plan with override is unsupported (bd-rmf)");
+ "internal: encode_body invoked on override plan '%s'",
+ plan->name != NULL ? plan->name : "?");
}
if (plan->field_names_ref == LUA_NOREF || plan->n_fields == 0)
@@ 1696,13 1745,23 @@ static int
encode_lua(lua_State *L)
{
pb_plan *plan = (pb_plan *)luaL_checkudata(L, 1, PB_PLAN_MT);
- luaL_checktype(L, 2, LUA_TTABLE);
- if (plan->has_override) {
- return luaL_error(L,
- "C encode on plan with override is unsupported (bd-rmf)");
+ /* WKT override: desc.encode(value) returns the body bytes verbatim.
+ * The value is any Lua type the override accepts (datetime cdata,
+ * Lua number, string, table, etc.) — don't pre-check for TTABLE. */
+ if (plan->override_encode_ref != LUA_NOREF) {
+ lua_rawgeti(L, LUA_REGISTRYINDEX, plan->override_encode_ref);
+ lua_pushvalue(L, 2);
+ lua_call(L, 1, 1);
+ if (lua_type(L, -1) != LUA_TSTRING)
+ return luaL_error(L,
+ "WKT encode for '%s' returned non-string",
+ plan->name != NULL ? plan->name : "?");
+ return 1;
}
+ luaL_checktype(L, 2, LUA_TTABLE);
+
enc_buf b;
ebuf_init(&b);
encode_body(L, &b, plan, 2);
@@ 1959,6 2018,18 @@ decode_submessage_field(dec_ctx *c, pb_plan_field *f, int sub_plans_idx)
f->sub_plan_idx);
lua_pop(L, 1);
+ /* WKT override: feed the body slice to desc.decode(buf), push whatever
+ * Lua representation the override returns. */
+ if (subplan->override_decode_ref != LUA_NOREF) {
+ lua_rawgeti(L, LUA_REGISTRYINDEX,
+ subplan->override_decode_ref);
+ lua_pushlstring(L, (const char *)(c->buf + c->pos),
+ (size_t)plen);
+ lua_call(L, 1, 1);
+ c->pos += (size_t)plen;
+ return;
+ }
+
lua_createtable(L, 0, subplan->n_fields);
int sub_result_idx = lua_gettop(L);
@@ 2036,16 2107,28 @@ decode_map_entry(dec_ctx *c, pb_plan_field *f, int sub_plans_idx,
if (c->len - c->pos < sub_len)
luaL_error(L,
"truncated nested map<,message> value");
- lua_createtable(L, 0, vsub->n_fields);
- int new_val = lua_gettop(L);
- size_t saved2 = c->len;
- c->len = c->pos + (size_t)sub_len;
- decode_body(c, vsub, new_val);
- if (c->pos != c->len)
- luaL_error(L,
- "nested map<,message> body underflow");
- c->len = saved2;
- lua_replace(L, val_idx);
+ if (vsub->override_decode_ref != LUA_NOREF) {
+ /* WKT-typed map value: override consumes body. */
+ lua_rawgeti(L, LUA_REGISTRYINDEX,
+ vsub->override_decode_ref);
+ lua_pushlstring(L,
+ (const char *)(c->buf + c->pos),
+ (size_t)sub_len);
+ lua_call(L, 1, 1);
+ c->pos += (size_t)sub_len;
+ lua_replace(L, val_idx);
+ } else {
+ lua_createtable(L, 0, vsub->n_fields);
+ int new_val = lua_gettop(L);
+ size_t saved2 = c->len;
+ c->len = c->pos + (size_t)sub_len;
+ decode_body(c, vsub, new_val);
+ if (c->pos != c->len)
+ luaL_error(L,
+ "nested map<,message> body underflow");
+ c->len = saved2;
+ lua_replace(L, val_idx);
+ }
} else {
dec_push_kind(c, f->map_value_kind);
lua_replace(L, val_idx);
@@ 2092,9 2175,12 @@ static void
decode_body(dec_ctx *c, pb_plan *plan, int result_idx)
{
lua_State *L = c->L;
- if (plan->has_override) {
+ if (plan->override_decode_ref != LUA_NOREF) {
+ /* Defensive: callers must dispatch via the override Lua-ref
+ * directly. Reaching decode_body here means a code-path bug. */
luaL_error(L,
- "C decode on plan with override is unsupported (bd-rmf)");
+ "internal: decode_body invoked on override plan '%s'",
+ plan->name != NULL ? plan->name : "?");
}
result_idx = abs_idx(L, result_idx);
@@ 2319,9 2405,13 @@ decode_lua(lua_State *L)
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)");
+ /* WKT override: desc.decode(buf) consumes the entire body and returns
+ * whatever Lua representation the override picks (e.g. datetime). */
+ if (plan->override_decode_ref != LUA_NOREF) {
+ lua_rawgeti(L, LUA_REGISTRYINDEX, plan->override_decode_ref);
+ lua_pushlstring(L, buf, buf_len);
+ lua_call(L, 1, 1);
+ return 1;
}
lua_createtable(L, 0, plan->n_fields);
M test/c_runtime_decode_test.lua => test/c_runtime_decode_test.lua +5 -5
@@ 275,12 275,12 @@ for _, mode in ipairs({'full', 'runtime'}) do
{name = 'x', _unknown_fields = unknown_tag})
end
- function g.test_wkt_override_rejected()
- -- has_override plans don't expose field arrays; mirror 3b's gate.
+ function g.test_wkt_override_passthrough()
+ -- bd-rmf / ra6 3k: has_override plans dispatch to desc.decode(buf).
local plan = c_runtime.compile_plan(pb.wkt.Timestamp_descriptor)
- t.assert_error_msg_contains('override', function()
- c_runtime.decode(plan, '')
- end)
+ local bytes = pb.wkt.Timestamp_encode({seconds = 1700000000, nanos = 42})
+ t.assert_equals(c_runtime.decode(plan, bytes),
+ pb.wkt.Timestamp_decode(bytes))
end
function g.test_truncated_input_errors()
M test/c_runtime_encode_test.lua => test/c_runtime_encode_test.lua +6 -5
@@ 292,12 292,13 @@ for _, mode in ipairs({'full', 'runtime'}) do
full_hello.Person_encode(msg))
end
- function g.test_wkt_override_rejected()
- -- has_override plans skip field-walk; 3b does not handle them.
+ function g.test_wkt_override_passthrough()
+ -- bd-rmf / ra6 3k: has_override plans now dispatch to
+ -- desc.encode(value) and return its bytes verbatim.
local plan = c_runtime.compile_plan(pb.wkt.Timestamp_descriptor)
- t.assert_error_msg_contains('override', function()
- c_runtime.encode(plan, {seconds = 1})
- end)
+ local v = {seconds = 1, nanos = 2}
+ t.assert_equals(c_runtime.encode(plan, v),
+ pb.wkt.Timestamp_encode(v))
end
-- ---------- Repeated + packed (bd-jc9 / ra6 3e) ----------
A test/c_runtime_wkt_test.lua => test/c_runtime_wkt_test.lua +158 -0
@@ 0,0 1,158 @@
+-- Tests for bd-rmf / ra6 3k: WKT override-hook passthrough.
+--
+-- A plan whose descriptor carries desc.encode / desc.decode dispatches
+-- straight to those overrides instead of walking fields. The Event
+-- message exercises every WKT shape we ship — Timestamp, Duration,
+-- Empty, the <T>Value wrappers, Struct, Value, ListValue, Any,
+-- FieldMask — so byte-equality with mode=full proves the override is
+-- live at both the top level and when nested as a sub-message field.
+--
+-- Acceptance per bd-rmf: hello.Event with WKT sub-messages round-trips
+-- byte-equal to mode=full pure-Lua, runtime/pb/wkt.lua is unmodified.
+
+local t = require('luatest')
+local datetime = require('datetime')
+
+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
+
+local full_hello
+
+for _, mode in ipairs({'full', 'runtime'}) do
+ local g = t.group('c_runtime_wkt.' .. 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)
+
+ -- ---------- Top-level override dispatch ----------
+
+ function g.test_timestamp_top_level_encode_decode()
+ local plan = c_runtime.compile_plan(pb.wkt.Timestamp_descriptor)
+ local dt = datetime.new({timestamp = 1700000000, nsec = 123})
+ local c_bytes = c_runtime.encode(plan, dt)
+ t.assert_equals(c_bytes, pb.wkt.Timestamp_encode(dt))
+ local out = c_runtime.decode(plan, c_bytes)
+ t.assert(datetime.is_datetime(out))
+ t.assert_equals(out.epoch, 1700000000)
+ t.assert_equals(out.nsec, 123)
+ end
+
+ function g.test_duration_top_level_round_trip()
+ local plan = c_runtime.compile_plan(pb.wkt.Duration_descriptor)
+ local v = {seconds = 7200, nanos = 500}
+ local c_bytes = c_runtime.encode(plan, v)
+ t.assert_equals(c_bytes, pb.wkt.Duration_encode(v))
+ t.assert_equals(c_runtime.decode(plan, c_bytes),
+ pb.wkt.Duration_decode(c_bytes))
+ end
+
+ function g.test_empty_top_level_encode_is_zero_bytes()
+ local plan = c_runtime.compile_plan(pb.wkt.Empty_descriptor)
+ t.assert_equals(c_runtime.encode(plan, {}), '')
+ end
+
+ function g.test_int32value_wrapper_top_level()
+ local plan = c_runtime.compile_plan(pb.wkt.Int32Value_descriptor)
+ -- The wrapper accepts the bare scalar; presence is meaningful.
+ t.assert_equals(c_runtime.encode(plan, 42),
+ pb.wkt.Int32Value_encode(42))
+ end
+
+ -- ---------- Nested override dispatch (sub-message field) ----------
+
+ function g.test_event_with_timestamp_byte_equal()
+ local plan = c_runtime.compile_plan(hello.Event_descriptor)
+ local msg = {
+ title = 'hi',
+ created_at = datetime.new(
+ {timestamp = 1700000000, nsec = 123456789}),
+ }
+ t.assert_equals(c_runtime.encode(plan, msg),
+ full_hello.Event_encode(msg))
+ end
+
+ function g.test_event_with_duration_and_empty_byte_equal()
+ local plan = c_runtime.compile_plan(hello.Event_descriptor)
+ local msg = {
+ title = 'hi',
+ duration = {seconds = 60, nanos = 0},
+ ack = {},
+ }
+ t.assert_equals(c_runtime.encode(plan, msg),
+ full_hello.Event_encode(msg))
+ end
+
+ function g.test_event_with_wrappers_byte_equal()
+ local plan = c_runtime.compile_plan(hello.Event_descriptor)
+ local msg = {
+ retry_count = 5,
+ note = 'remember',
+ is_admin = true,
+ }
+ t.assert_equals(c_runtime.encode(plan, msg),
+ full_hello.Event_encode(msg))
+ end
+
+ function g.test_event_with_wrapper_zero_preserved()
+ local plan = c_runtime.compile_plan(hello.Event_descriptor)
+ -- Wrappers preserve presence at zero — the override must still
+ -- emit tag + len(0). A naive "skip default" path would lose it.
+ local msg = {retry_count = 0}
+ t.assert_equals(c_runtime.encode(plan, msg),
+ full_hello.Event_encode(msg))
+ end
+
+ function g.test_event_with_fieldmask_byte_equal()
+ local plan = c_runtime.compile_plan(hello.Event_descriptor)
+ local msg = {update_mask = {paths = {'foo', 'bar.baz'}}}
+ t.assert_equals(c_runtime.encode(plan, msg),
+ full_hello.Event_encode(msg))
+ end
+
+ -- ---------- Round-trip parity through the C path ----------
+ --
+ -- Encode in C, decode in C, compare against mode=full's decode of
+ -- the same wire bytes. Asserts the override decode wrapper hands
+ -- back the same Lua representation the pure-Lua codec does.
+
+ function g.test_event_round_trip_through_c()
+ local plan = c_runtime.compile_plan(hello.Event_descriptor)
+ local dt = datetime.new({timestamp = 42, nsec = 500})
+ local msg = {
+ title = 'x',
+ created_at = dt,
+ duration = {seconds = 1, nanos = 2},
+ ack = {},
+ retry_count = 7,
+ note = 'hello',
+ update_mask = {paths = {'a', 'b'}},
+ }
+ local bytes = c_runtime.encode(plan, msg)
+ local c_dec = c_runtime.decode(plan, bytes)
+ local lua_dec = full_hello.Event_decode(bytes)
+
+ t.assert_equals(c_dec.title, lua_dec.title)
+ t.assert(datetime.is_datetime(c_dec.created_at))
+ t.assert_equals(c_dec.created_at.epoch, lua_dec.created_at.epoch)
+ t.assert_equals(c_dec.created_at.nsec, lua_dec.created_at.nsec)
+ t.assert_equals(tonumber(c_dec.duration.seconds),
+ tonumber(lua_dec.duration.seconds))
+ t.assert_equals(c_dec.duration.nanos, lua_dec.duration.nanos)
+ t.assert_equals(type(c_dec.ack), 'table')
+ t.assert_equals(c_dec.retry_count, lua_dec.retry_count)
+ t.assert_equals(c_dec.note, lua_dec.note)
+ t.assert_equals(c_dec.update_mask.paths, lua_dec.update_mask.paths)
+ end
+end