@@ 19,7 19,7 @@
{"_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":"open","priority":2,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-18T20:20:57Z","created_by":"Eugene Blikh","updated_at":"2026-05-18T20:20:57Z","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":"open","priority":2,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-18T20:20:40Z","created_by":"Eugene Blikh","updated_at":"2026-05-18T20:20:40Z","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}
-{"_type":"issue","id":"tarantool-protobuf-w3u","title":"ra6 3g: oneofs","description":"Oneof groups: on encode, only the field with the current 'which' set is written; on decode, last-write-wins per the oneof group. Plan carries oneof grouping as a parallel array (matches the runtime/pb/codec.lua desc.oneofs_list pattern — required for JIT-friendly iteration since pairs() on a hash is NYI). Track 'which one is set' per group during decode by storing field number in a small array. Depends on 3b + 3c. Acceptance: hello.Result (which uses oneof outcome with string/int32/Address branches) round-trips byte-equal across all three branches; the decoded table includes the active branch and excludes the others; pb.lazy view's :which() returns the correct branch name.","status":"open","priority":2,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-18T20:20:36Z","created_by":"Eugene Blikh","updated_at":"2026-05-18T20:20:36Z","dependencies":[{"issue_id":"tarantool-protobuf-w3u","depends_on_id":"tarantool-protobuf-mz6","type":"blocks","created_at":"2026-05-18T23:22:08Z","created_by":"Eugene Blikh","metadata":"{}"},{"issue_id":"tarantool-protobuf-w3u","depends_on_id":"tarantool-protobuf-y1n","type":"blocks","created_at":"2026-05-18T23:22:08Z","created_by":"Eugene Blikh","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0}
+{"_type":"issue","id":"tarantool-protobuf-w3u","title":"ra6 3g: oneofs","description":"Oneof groups: on encode, only the field with the current 'which' set is written; on decode, last-write-wins per the oneof group. Plan carries oneof grouping as a parallel array (matches the runtime/pb/codec.lua desc.oneofs_list pattern — required for JIT-friendly iteration since pairs() on a hash is NYI). Track 'which one is set' per group during decode by storing field number in a small array. Depends on 3b + 3c. Acceptance: hello.Result (which uses oneof outcome with string/int32/Address branches) round-trips byte-equal across all three branches; the decoded table includes the active branch and excludes the others; pb.lazy view's :which() returns the correct branch name.","status":"closed","priority":2,"issue_type":"task","assignee":"Eugene Blikh","owner":"bigbes@gmail.com","created_at":"2026-05-18T20:20:36Z","created_by":"Eugene Blikh","updated_at":"2026-05-23T13:17:51Z","started_at":"2026-05-23T13:15:43Z","closed_at":"2026-05-23T13:17:51Z","close_reason":"Closed","dependencies":[{"issue_id":"tarantool-protobuf-w3u","depends_on_id":"tarantool-protobuf-mz6","type":"blocks","created_at":"2026-05-18T23:22:08Z","created_by":"Eugene Blikh","metadata":"{}"},{"issue_id":"tarantool-protobuf-w3u","depends_on_id":"tarantool-protobuf-y1n","type":"blocks","created_at":"2026-05-18T23:22:08Z","created_by":"Eugene Blikh","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0}
{"_type":"issue","id":"tarantool-protobuf-21d","title":"runtime/Person_encode emits 2.5–3× more traces than full mode (codec dispatch fragments)","description":"Trace-topology measurement explaining the 10–25% gap between mode=runtime and mode=full reported in bench/COMPARISON.md.\n\nMeasured on Tarantool 3.8.0-entrypoint / LuaJIT 2.1.0-beta3 / macOS arm64 via bench/jit_trace.lua (5-run median):\n\n full/Person_encode stops=8–10\n runtime/Person_encode stops=24–27 (3.0× more traces)\n full/Person_decode stops=19–24\n runtime/Person_decode stops=12–23\n full/Person_encode multi-byte stops=6\n runtime/Person_encode multi-byte stops=18–20 (3.0×)\n\nThe runtime-side encode in runtime/pb/codec.lua dispatches on field.kind / field.proto_type per field per iteration. Each distinct (kind, proto_type) combination becomes its own side trace because the dispatch chain inside encode_field is megamorphic from LuaJIT's view. With ~7 fields and ~5 proto_types per Person fixture, this multiplies out to ~25–30 trace fragments, most of which carry the full per-field call setup overhead.\n\nThe compile_writers / compile_readers hooks in runtime/pb/init.lua already attach per-field monomorphic functions (codec.compile_writers(desc) / compile_readers(desc) called from finalize_message). They handle the singular scalar/enum/message and repeated scalar/enum/message shapes — but the outer encode loop in codec.encode_message still iterates desc.fields and branches per-shape before calling the writer. The branch is what fragments.\n\nConcrete fix paths (mutually compatible):\n- Codegen at finalize_message time: emit a single closure 'encode_body' that calls writer(data, out) for each field in declaration order, no per-iteration kind/type test. The body is monomorphic because each writer is monomorphic.\n- Same for the decoder: walk fields once at compile time to produce a single if/elseif chain over field ids whose bodies are direct reader(buf, pos, result) calls.\n\nExpected impact: most of the runtime-vs-full gap in COMPARISON.md collapses (current: full 281–582 MB/s vs runtime 148–593 MB/s for encode; decode 136–192 vs 129–175). Likely also retires the 'runtime mode is 10-25% slower' caveat documented at the top of COMPARISON.md.\n\nValidation:\n- bench/jit_trace.lua should show runtime/Person_encode stops \u003c= full/Person_encode stops + 4.\n- bench/bench.lua --compare against fresh baseline should show runtime mode at within 5% of full for all Person sizes.","status":"open","priority":2,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-18T17:27:19Z","created_by":"Eugene Blikh","updated_at":"2026-05-18T17:27:19Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"tarantool-protobuf-3o2","title":"bench/jit_trace.lua: harden mcode arena + jit.off the listener","description":"Two infra fixes for the trace-stability gate, both confirmed by repro on Tarantool 3.8.0-entrypoint / LuaJIT 2.1.0-beta3 / macOS arm64.\n\n1) On macOS arm64 the default JIT mcode arena (sizemcode=32K, maxmcode=512K) is too small for the codegen footprint of our hot paths. Empirically the gate fails roughly 1 in 10 runs with every check reporting 'stops=0' and jit.v showing 'failed to allocate mcode memory at hello_pb.lua:890' (the packed lucky_numbers varint loop). The gate currently has no diagnostic for this — it just reports 'no trace was compiled', which is indistinguishable from a real JIT topology regression. Add 'jit.opt.start(\"sizemcode=64\",\"maxmcode=4096\")' to the top of bench/jit_trace.lua so the arena can hold the full encoder/decoder bodies for the proto3 + proto2 fixtures with headroom.\n\n2) The trace listener callback itself can become hot enough to be JIT-compiled, which then interferes with recording the function under test (recording-while-recording). Reproduced with a fat callback that appends raw event tuples to a table: starts go up but stops drop to ~0. Calling 'jit.off(cb)' on the listener function immediately after defining it prevents this. The gate's current callback happens to dodge this because its branches keep the call sites polymorphic enough to avoid trace promotion — but that's fragile; future extension (e.g. collecting per-event timing or pc context) would re-trigger the bug. Add the jit.off as a defensive measure.\n\nAcceptance: 50 consecutive runs of 'tarantool bench/jit_trace.lua' all report 37/37 passing with no 'stops=0' failure mode.","status":"closed","priority":2,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-18T17:27:04Z","created_by":"Eugene Blikh","updated_at":"2026-05-18T17:28:24Z","closed_at":"2026-05-18T17:28:24Z","close_reason":"Applied both fixes to bench/jit_trace.lua: jit.opt.start('sizemcode=64','maxmcode=4096') at top, jit.off(cb) on the trace listener. Verified 20/20 consecutive runs report 37/37 passing (was intermittently 0/37 before). Acceptance criterion exceeded (asked for 50, did 20 — bump if needed).","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"tarantool-protobuf-qwt","title":"Codegen: emit per-extension writers for proto2 extensions (skip pb.codec.encode_field dispatch)","description":"proto2 extensions are the slowest line in the bench: proto2_basic.BenchPayload 'min' fixture encodes at 11.0 MB/s — vs 281 MB/s for the 100B Person. Cause: generated _extensions walk in mode=full falls through to pb.codec.encode_field, the slow runtime dispatch that mode=full otherwise avoids.\n\nFix: at codegen time, when 'extend Foo { ... ext_count = 100; ... }' is seen, emit a dedicated writer per extension. The Foo_encode body's extensions block becomes:\n\n local _ev = _exts['proto2_basic.ext_count']\n if _ev ~= nil then\n -- direct inline write for the int32 ext, just like a regular int32 field\n n = n + 1; out[n] = '\u003cprecomputed tag bytes\u003e'\n n = n + 1; out[n] = wire.encode_int32(_ev)\n end\n -- repeat per extension\n\nDecoder side: extensions_by_id[id] dispatch in the else branch currently calls pb.codec.decode_extension; can similarly be replaced by inline-emitted per-extension decoder blocks alongside the regular field branches.\n\nConcretely closes the proto2 min throughput gap (3.4x vs Go apiv2) which is bench's worst data point. Has zero impact on payloads without extensions.","notes":"Generated code grows by one if-block per registered extension. For a message with 50 extensions this could be significant — but proto2 extensions are typically used sparingly. Worth gating on extension count if size becomes a problem.\n\nExisting extensions_by_id / extensions_list / extensions_by_full_name registries can stay (they're used by text/json/runtime dispatch); the fast path just bypasses them in mode=full.","status":"open","priority":2,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-18T17:14:38Z","created_by":"Eugene Blikh","updated_at":"2026-05-18T17:14:38Z","dependency_count":0,"dependent_count":0,"comment_count":0}
@@ 1323,12 1323,39 @@ encode_body(lua_State *L, enc_buf *b, pb_plan *plan, int msg_idx)
lua_rawgeti(L, LUA_REGISTRYINDEX, plan->field_names_ref);
int names_idx = lua_gettop(L);
+ /* Oneof active-member resolution. For each oneof group, walk its
+ * member_indices in declaration order and check msg[member_name];
+ * the last non-nil member wins, matching codec.lua encode_message.
+ * active_member_idx[o] stores the plan->fields index of the active
+ * branch, or -1 if no member is set. Sized for n_oneofs >= 1; the
+ * dummy slot when n_oneofs == 0 avoids zero-length-VLA UB. */
+ int oo_vla_n = plan->n_oneofs > 0 ? plan->n_oneofs : 1;
+ int active_member_idx[oo_vla_n];
+ for (int o = 0; o < oo_vla_n; o++) active_member_idx[o] = -1;
+ for (int o = 0; o < plan->n_oneofs; o++) {
+ pb_plan_oneof *oo = &plan->oneofs[o];
+ for (int k = 0; k < oo->n_members; k++) {
+ int m_idx = oo->member_indices[k];
+ lua_rawgeti(L, names_idx, m_idx + 1);
+ lua_rawget(L, msg_idx);
+ if (!lua_isnil(L, -1))
+ active_member_idx[o] = m_idx;
+ lua_pop(L, 1);
+ }
+ }
+
for (int i = 0; i < plan->n_fields; i++) {
pb_plan_field *f = &plan->fields[i];
/* Map dispatches in 3h. */
if (f->kind == PB_KIND_MAP) continue;
+ /* Oneof: skip every non-active member. The active member
+ * encodes with force_emit so default values (e.g. text="")
+ * still carry presence. */
+ if (f->oneof_idx >= 0 && active_member_idx[f->oneof_idx] != i)
+ continue;
+
lua_rawgeti(L, names_idx, i + 1); /* push field name */
lua_rawget(L, msg_idx); /* push msg[name] */
int val_idx = lua_gettop(L);
@@ 1346,7 1373,8 @@ encode_body(lua_State *L, enc_buf *b, pb_plan *plan, int msg_idx)
} else if (f->kind == PB_KIND_MESSAGE) {
encode_submessage_field(L, b, plan, f, val_idx);
} else {
- encode_one_field(L, b, f, val_idx, /* force_emit */ 0);
+ int force = f->oneof_idx >= 0 ? 1 : 0;
+ encode_one_field(L, b, f, val_idx, force);
}
lua_pop(L, 1);
}
@@ 1788,14 1816,28 @@ decode_body(dec_ctx *c, pb_plan *plan, int result_idx)
lua_rawgeti(L, names_idx, f_idx + 1);
lua_insert(L, -2); /* name, sub_result */
lua_rawset(L, result_idx); /* result[name] = sub_result */
- continue;
- }
+ } else {
+ dec_push_one(c, f); /* stack: ..., names, sub_plans, [lists...], value */
- dec_push_one(c, f); /* stack: ..., names, sub_plans, [lists...], value */
+ lua_rawgeti(L, names_idx, f_idx + 1);
+ lua_insert(L, -2); /* name, value */
+ lua_rawset(L, result_idx); /* result[name] = value */
+ }
- lua_rawgeti(L, names_idx, f_idx + 1);
- lua_insert(L, -2); /* name, value */
- lua_rawset(L, result_idx); /* result[name] = value */
+ /* Oneof: clear sibling branches in the result table. Wire-order
+ * last-wins semantics — the most-recently-decoded member is the
+ * one whose value remains. Mirrors codec.lua's
+ * `oneof_siblings` clearing. */
+ if (f->oneof_idx >= 0) {
+ pb_plan_oneof *oo = &plan->oneofs[f->oneof_idx];
+ for (int k = 0; k < oo->n_members; k++) {
+ int m_idx = oo->member_indices[k];
+ if (m_idx == f_idx) continue;
+ lua_rawgeti(L, names_idx, m_idx + 1);
+ lua_pushnil(L);
+ lua_rawset(L, result_idx);
+ }
+ }
}
/* Pop everything we pushed: per-field list tables (one per repeated
@@ 0,0 1,134 @@
+-- Test for bd-w3u / ra6 3g: C-side oneof encode/decode.
+--
+-- Acceptance per bd-w3u:
+-- hello.Result (oneof outcome { string text=2; int32 code=3;
+-- Address details=4; }) round-trips byte-equal across all three
+-- branches; decoded table includes the active branch and excludes
+-- the others. Mirrors test/protobuf_test.lua g.test_oneof_* but
+-- drives encode/decode through pb.c_runtime.
+--
+-- Gated on PB_ENABLE_C=1 + a loadable c_runtime module, same as the
+-- sibling c_runtime_*_test.lua files.
+
+local t = require('luatest')
+
+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_oneof.' .. mode)
+ local hello
+ local plan
+
+ g.before_all(function()
+ skip_if_no_c()
+ hello = require(mode .. '.hello.hello_pb')
+ full_hello = require('full.hello.hello_pb')
+ plan = c_runtime.compile_plan(hello.Result_descriptor)
+ end)
+
+ g.before_each(skip_if_no_c)
+
+ function g.test_oneof_text_branch_encode_byte_equal()
+ local msg = {id = 1, text = 'hello'}
+ local c_bytes = c_runtime.encode(plan, msg)
+ local lua_bytes = full_hello.Result_encode(msg)
+ t.assert_equals(c_bytes, lua_bytes)
+ end
+
+ function g.test_oneof_code_branch_encode_byte_equal()
+ local msg = {id = 2, code = 42}
+ local c_bytes = c_runtime.encode(plan, msg)
+ local lua_bytes = full_hello.Result_encode(msg)
+ t.assert_equals(c_bytes, lua_bytes)
+ end
+
+ function g.test_oneof_message_branch_encode_byte_equal()
+ local msg = {id = 3, details = {street = 'X', zip = 99}}
+ local c_bytes = c_runtime.encode(plan, msg)
+ local lua_bytes = full_hello.Result_encode(msg)
+ t.assert_equals(c_bytes, lua_bytes)
+ end
+
+ function g.test_oneof_text_branch_round_trip()
+ local msg = {id = 1, text = 'hello'}
+ local dec = c_runtime.decode(plan, c_runtime.encode(plan, msg))
+ t.assert_equals(dec.id, 1)
+ t.assert_equals(dec.text, 'hello')
+ t.assert_equals(dec.code, nil)
+ t.assert_equals(dec.details, nil)
+ end
+
+ function g.test_oneof_code_branch_round_trip()
+ local msg = {id = 2, code = 42}
+ local dec = c_runtime.decode(plan, c_runtime.encode(plan, msg))
+ t.assert_equals(dec.id, 2)
+ t.assert_equals(dec.code, 42)
+ t.assert_equals(dec.text, nil)
+ t.assert_equals(dec.details, nil)
+ end
+
+ function g.test_oneof_message_branch_round_trip()
+ local msg = {id = 3, details = {street = 'X', zip = 99}}
+ local dec = c_runtime.decode(plan, c_runtime.encode(plan, msg))
+ t.assert_equals(dec.id, 3)
+ t.assert_equals(dec.details.street, 'X')
+ t.assert_equals(dec.details.zip, 99)
+ t.assert_equals(dec.text, nil)
+ t.assert_equals(dec.code, nil)
+ end
+
+ function g.test_oneof_emits_default_value_when_active()
+ -- text='' is the proto3 string default. Outside a oneof it would
+ -- elide; inside, presence is meaningful. Active branch must emit.
+ local c_bytes = c_runtime.encode(plan, {text = ''})
+ t.assert_equals(c_bytes, '\x12\x00')
+ local dec = c_runtime.decode(plan, c_bytes)
+ t.assert_equals(dec.text, '')
+ end
+
+ function g.test_oneof_decode_clears_siblings()
+ -- Wire bytes carry text first, then code. Decoder must end with
+ -- code set and text cleared (last-wins per spec).
+ local raw = '\x12\x03foo' -- field 2 (text) LEN=3, "foo"
+ .. '\x18\x07' -- field 3 (code) varint 7
+ local dec = c_runtime.decode(plan, raw)
+ t.assert_equals(dec.code, 7)
+ t.assert_equals(dec.text, nil)
+ end
+
+ function g.test_oneof_last_set_wins_on_encode()
+ -- Caller sets multiple branches; encoder picks the last in
+ -- declaration order (details = field 4).
+ local msg = {text = 'first', code = 9, details = {street = 'last'}}
+ local c_bytes = c_runtime.encode(plan, msg)
+ -- Byte-equal to Lua reference encode, which also resolves to
+ -- details-only.
+ local lua_bytes = full_hello.Result_encode(msg)
+ t.assert_equals(c_bytes, lua_bytes)
+ local dec = c_runtime.decode(plan, c_bytes)
+ t.assert_equals(dec.details.street, 'last')
+ t.assert_equals(dec.text, nil)
+ t.assert_equals(dec.code, nil)
+ end
+
+ function g.test_oneof_message_branch_decode_clears_scalar_sibling()
+ -- Sibling-clear must also fire when the active branch is a
+ -- sub-message (exercises the PB_KIND_MESSAGE arm of the
+ -- singular dispatch).
+ local raw = '\x18\x07' -- code = 7
+ .. '\x22\x03\x0a\x01X' -- details {street="X"}
+ local dec = c_runtime.decode(plan, raw)
+ t.assert_equals(dec.details.street, 'X')
+ t.assert_equals(dec.code, nil)
+ t.assert_equals(dec.text, nil)
+ end
+end