c_runtime: map<K,V> encode + decode dispatch (ra6 3h) Encode walks the user map with lua_next (the documented JIT exception per CLAUDE.md — map hot paths can't avoid hash iteration). Each entry goes into a stack-backed sub-buffer with synthetic tag(1,key) + tag(2,value); proto3-elides default key and default value independently. Map<,message> resolves its 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 defaults to the proto3 zero. Reuses the existing list_stack_idx[] slot for the lazy map cache since 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. bd-asz
c_runtime: repeated string + repeated message acceptance at 1KB/10KB/100KB (ra6 3f) The repeated string and repeated message (self-reference) dispatch landed implicitly with ra6 3e — encode_repeated_field already routes PB_KIND_MESSAGE through encode_submessage_field and unpacked string/bytes through encode_one_field, and decode_body's cached list_stack_idx[] handles every repeated element type. What was missing was the formal 3f acceptance: explicit-size byte-equality at 1KB / 10KB / 100KB for Person.emails and Person.friends, against mode=full. Adds 4 acceptance tests (encode + decode × emails + friends), each running both codegen modes. Sizing pins each case into a ±30% band around the named target so a future schema/wire shift fails loudly instead of silently drifting. bd-exy
c_runtime: repeated + packed scalar encode/decode (ra6 3e) Add repeated dispatch to the C-runtime encode/decode loop. Encode side: encode_repeated_field walks Lua arrays via lua_objlen + per- index rawgeti, dispatches on element kind. Packed numerics build their payload in a stack-backed sub-buffer then emit `tag(LEN) + varint(len) + body`; unpacked emit `tag + value` per element via encode_one_field with force_emit=1 to bypass zero-suppression; strings/bytes flow through the same path (never packable); repeated messages reuse encode_submessage_field per element. Decode side: per-field stack-slot cache (list_stack_idx[]) tied to list_count[] avoids the per-element lua_getfield(result, name) round trip the c-accel spike measured at 2x slower at 100KB. On first hit for a repeated field we lua_createtable + write result[name] AND dup- push the list onto the stack; subsequent hits lua_rawseti through the cached absolute stack index. Lists stay valid across recursive sub- message decodes because each child decode_body cleans up its own scratch back to the caller's frame. Packed/unpacked symmetry on read: a wt==LEN payload for any packable scalar is decoded as a packed blob regardless of the schema's packed flag, and a per-element-tagged stream is decoded element-by-element even on a schema that defaults to packed — per proto3 reader rules. New test/proto/c_repeated.proto fixture carries packed + explicit- unpacked + repeated string/bytes + repeated message branches. The encode and decode tests round-trip at 10/100/1000 elements per branch. The two existing "skip repeated and map" marker tests collapse to "skip map" — only map fields remain out of scope for 3e (bd-asz / 3h lands them next). 854 → 896 passing tests. bd-jc9
c_runtime: encode/decode singular sub-messages (ra6 3d) Refactor encode_lua/decode_lua into encode_body/decode_body so the field-walk loop is callable recursively, then dispatch the MESSAGE kind into a per-side sub-handler. Repeated and map fields still skip at the field-walk level — 3e (jc9) and 3h (asz) land them next. encode_submessage_field force-establishes the parent enc_buf's heap_idx via a no-op ebuf_grow before recursing. Without that the final ebuf_reserve on the parent could land its new userdata above sub-encode's leaked stack slots, making the closing lua_settop drop the parent's heap. decode_submessage_field bounds the inner read by temporarily shrinking c->len to the sub-message end offset; the wire-prim helpers already bounds-check against c->len, so a malformed inner payload can't over-read into the outer message's bytes. New fixture test/proto/c_nested.proto carries a 5-level singular chain (L1->L2->L3->L4->L5) for the depth test. The two existing "skip message" tests are renamed to "skip repeated and map" — sub- messages now encode and decode end-to-end. bd-hwe
c_runtime: decode singular scalars (ra6 3c) New entry pb.c_runtime.decode(plan, bytes) -> table, symmetric in scope with 3b: int32/64, uint32/64, sint32/64, bool, fixed32/64, sfixed32/64, float, double, enum, string, bytes. Repeated, map, message-typed, and unknown tags skip by wire type — 3d/3e/3i/wyp will extend later. Result shape mirrors mode=full pure-Lua decode byte-for-byte: int64 family (int64/uint64/sint64/fixed64/sfixed64) push Tarantool cdata via luaL_pushint64 / luaL_pushuint64; everything else lands as Lua number/string/boolean. Pre-sized via lua_createtable(0, n_fields). Field lookup is a linear scan over plan->fields by field_number; a tag-keyed dispatch table is a future optimization. Tests: 26 cases per codegen mode (full + runtime), covering the bd-mz6 acceptance (round-trip of bd-y1n's Person payload matches mode=full Person_decode shape-for-shape) plus per-kind coverage, proto3-optional presence, fixed64 cdata, -0.0 sign preservation (built via cdata to dodge LuaJIT literal-folding to +0.0), skip-by-wire-type for out-of-scope shapes, unknown-tag skip, WKT override rejection, and truncated input. Suite: 832/832 with PB_ENABLE_C=1, 748/748 + 84 skipped without. Closes bd-mz6