~bigbes/tarantool

tarantool-protobuf

3235f34af486e3ef0043f8cb9d9230e29b919980 — Eugene Blikh 2 months ago c57b013
c_runtime: proto2 — required, defaults, groups, extensions (ra6 3i)

Plumb proto2 semantic surface through the C plan and codec:

  * required: encode-time `required field missing` error with full path
    (`<msg>.<field>`); set scalars/enums force-emit so zero still reaches
    the wire (matches build_required_writer).
  * groups: kind='group' compiles to PB_KIND_MESSAGE with is_group=1; tag
    uses SGROUP and a pre-encoded EGROUP closer; encode walks regular
    body bytes into a sub-buf and brackets with SGROUP+body+EGROUP (no
    length prefix). Decode adds a stop_group_id to decode_body so the
    inner walk terminates on the matching EGROUP, with id-mismatch as a
    hard error per spec. Unknown-tag skip (`dec_skip_with_id`) recurses
    through SGROUP bodies to the matching EGROUP.
  * extensions: walk `desc.extensions_list` at plan-compile time, cache
    each ext's full_name; encode iterates the cached array and emits any
    present in `data._extensions[full_name]`; decode probes unregistered
    tags against `plan->extensions` before falling through to
    `_unknown_fields`, routing matched bytes into result._extensions.

Defaults: presence-tracked optional fields stay nil-on-absent in the
decoded table; the descriptor's `default_value` is surfaced for callers
(JSON, text) but never auto-materialized at decode — same as codec.lua.

36 new tests in test/c_runtime_proto2_test.lua exercise required missing
+ zero-emit, presence-tracked defaults, singular/repeated groups, and
extension round-trip — each asserts byte-equality with mode=full pure-Lua
output across both codegen modes.

ra6 3i
2 files changed, 684 insertions(+), 24 deletions(-)

M runtime/pb/c/c_runtime.c
A test/c_runtime_proto2_test.lua
M runtime/pb/c/c_runtime.c => runtime/pb/c/c_runtime.c +420 -24
@@ 55,11 55,13 @@ enum {
	PB_KIND_MAP,
};

/* Wire types per proto3 spec. */
/* Wire types per proto3 spec. SGROUP/EGROUP are proto2-only legacy. */
enum {
	PB_WIRE_VARINT = 0,
	PB_WIRE_I64    = 1,
	PB_WIRE_LEN    = 2,
	PB_WIRE_SGROUP = 3,
	PB_WIRE_EGROUP = 4,
	PB_WIRE_I32    = 5,
};



@@ 121,14 123,19 @@ typedef struct pb_plan_field {
	uint8_t  packed;
	uint8_t  repeated;
	uint8_t  optional;
	uint8_t  required;           /* proto2 required — missing-on-encode errors, no zero suppression */
	uint8_t  is_group;           /* proto2 group — SGROUP/EGROUP framing instead of LEN */
	uint8_t  tag_len;
	uint8_t  tag_bytes[5];
	uint8_t  egroup_tag_len;     /* groups only: pre-encoded EGROUP tag */
	uint8_t  egroup_tag_bytes[5];
	int      sub_plan_idx;       /* 1-based into sub_plans table; 0 if none */
	uint8_t  map_key_kind;
	uint8_t  map_value_kind;
	int      map_value_sub_plan_idx; /* 1-based; 0 if value is scalar */
	int      oneof_idx;          /* 0-based into plan->oneofs; -1 if none */
	int      enum_ref;           /* LUA_REGISTRYINDEX ref for enum desc; LUA_NOREF if none */
	char    *full_name;          /* extensions: "<package>.<ext_name>" key in data._extensions; NULL for regular fields */
} pb_plan_field;

typedef struct pb_plan_oneof {


@@ 145,6 152,8 @@ typedef struct pb_plan {
	pb_plan_oneof *oneofs;
	int   extension_range_start;
	int   extension_range_end;
	int   n_extensions;          /* proto2 extensions registered on this message */
	pb_plan_field *extensions;   /* extension field shapes; keyed by full_name */
	uint8_t has_override;
	int   override_encode_ref;   /* LUA_NOREF if absent */
	int   override_decode_ref;


@@ 187,9 196,21 @@ plan_free(lua_State *L, pb_plan *p)
		for (int i = 0; i < p->n_fields; i++) {
			if (p->fields[i].enum_ref != LUA_NOREF)
				luaL_unref(L, LUA_REGISTRYINDEX, p->fields[i].enum_ref);
			if (p->fields[i].full_name != NULL)
				free(p->fields[i].full_name);
		}
		free(p->fields);
	}
	if (p->extensions != NULL) {
		for (int i = 0; i < p->n_extensions; i++) {
			if (p->extensions[i].enum_ref != LUA_NOREF)
				luaL_unref(L, LUA_REGISTRYINDEX,
				           p->extensions[i].enum_ref);
			if (p->extensions[i].full_name != NULL)
				free(p->extensions[i].full_name);
		}
		free(p->extensions);
	}
	if (p->oneofs != NULL) {
		for (int i = 0; i < p->n_oneofs; i++) {
			free(p->oneofs[i].name);


@@ 304,7 325,7 @@ compile_field(lua_State *L, int f_desc_idx, pb_plan_field *field,
	lua_rawseti(L, field_names_idx, field_idx_1based);
	lua_pop(L, 1);

	/* repeated / packed / optional */
	/* repeated / packed / optional / required (proto2) */
	lua_getfield(L, f_desc_idx, "repeated");
	field->repeated = lua_toboolean(L, -1) ? 1 : 0;
	lua_pop(L, 1);


@@ 314,6 335,9 @@ compile_field(lua_State *L, int f_desc_idx, pb_plan_field *field,
	lua_getfield(L, f_desc_idx, "optional");
	field->optional = lua_toboolean(L, -1) ? 1 : 0;
	lua_pop(L, 1);
	lua_getfield(L, f_desc_idx, "required");
	field->required = lua_toboolean(L, -1) ? 1 : 0;
	lua_pop(L, 1);

	/* kind dispatch on desc.kind */
	lua_getfield(L, f_desc_idx, "kind");


@@ 344,14 368,19 @@ compile_field(lua_State *L, int f_desc_idx, pb_plan_field *field,
		} else {
			lua_pop(L, 1);
		}
	} else if (strcmp(kind_str, "message") == 0) {
	} else if (strcmp(kind_str, "message") == 0 ||
	           strcmp(kind_str, "group") == 0) {
		int is_group = (strcmp(kind_str, "group") == 0);
		lua_pop(L, 1);
		field->kind = PB_KIND_MESSAGE;
		element_wire_type = PB_WIRE_LEN;
		field->is_group = is_group ? 1 : 0;
		/* Group wire-type is SGROUP at the field's tag; EGROUP is the
		 * closing bracket, pre-encoded separately for the encoder. */
		element_wire_type = is_group ? PB_WIRE_SGROUP : PB_WIRE_LEN;
		lua_getfield(L, f_desc_idx, "message");
		if (!lua_istable(L, -1))
			luaL_error(L, "message field '%s' missing 'message' descriptor",
			           "?");
			luaL_error(L, "%s field missing 'message' descriptor",
			           is_group ? "group" : "message");
		int sub_desc = lua_gettop(L);
		field->sub_plan_idx = resolve_sub_plan(L, sub_desc, sub_plans_idx);
		lua_pop(L, 1); /* sub-desc table */


@@ 407,7 436,8 @@ compile_field(lua_State *L, int f_desc_idx, pb_plan_field *field,
	}

	/* Wire type: repeated+packed → LEN regardless of element type;
	 * repeated unpacked → element type per tag; singular → element. */
	 * repeated unpacked → element type per tag; singular → element.
	 * Groups can't be packed (LEN form), so this branch never fires for them. */
	if (field->repeated && field->packed) {
		field->wire_type = PB_WIRE_LEN;
	} else {


@@ 416,6 446,10 @@ compile_field(lua_State *L, int f_desc_idx, pb_plan_field *field,

	encode_tag(field->field_number, field->wire_type,
	           field->tag_bytes, &field->tag_len);
	if (field->is_group) {
		encode_tag(field->field_number, PB_WIRE_EGROUP,
		           field->egroup_tag_bytes, &field->egroup_tag_len);
	}
}

/* ---------------------------------------------------------------- *


@@ 616,6 650,41 @@ compile_plan_impl(lua_State *L, int desc_idx)
	/* Compile oneofs after fields so oneof_idx back-pointers can be set. */
	compile_oneofs(L, p, desc_idx, field_names_idx);

	/* Compile proto2 extensions (registered on this descriptor's
	 * extensions_list array). Each extension shares the field shape; we
	 * additionally cache its full_name string so encode can find the
	 * value in data._extensions[full_name] and decode can stash it back. */
	lua_getfield(L, desc_idx, "extensions_list");
	if (lua_istable(L, -1)) {
		int elist_idx = lua_gettop(L);
		int n_ext = (int)lua_objlen(L, elist_idx);
		if (n_ext > 0) {
			p->n_extensions = n_ext;
			p->extensions = (pb_plan_field *)calloc(
				n_ext, sizeof(pb_plan_field));
			for (int i = 0; i < n_ext; i++) {
				lua_rawgeti(L, elist_idx, i + 1);
				int e_desc_idx = lua_gettop(L);
				/* Reuse compile_field. Pass a throwaway 1-based
				 * index into field_names_idx — extension name
				 * caching there is harmless; we never look it
				 * up since we cache full_name separately. */
				compile_field(L, e_desc_idx,
				              &p->extensions[i],
				              field_names_idx, sub_plans_idx,
				              n_fields + i + 1);
				/* Cache full_name for _extensions[key] lookup. */
				lua_getfield(L, e_desc_idx, "full_name");
				const char *fn = lua_tostring(L, -1);
				p->extensions[i].full_name =
					strdup(fn != NULL ? fn : "");
				lua_pop(L, 1);
				lua_pop(L, 1); /* extension desc */
			}
		}
	}
	lua_pop(L, 1); /* extensions_list (table or nil) */

	/* Stash the field-names + sub-plans tables in the registry. */
	lua_pushvalue(L, sub_plans_idx);
	p->sub_plans_ref = luaL_ref(L, LUA_REGISTRYINDEX);


@@ 1371,8 1440,12 @@ push_default_for_kind(lua_State *L, uint8_t kind)
static void encode_body(lua_State *L, enc_buf *b, pb_plan *plan, int msg_idx);
static void encode_submessage_field(lua_State *L, enc_buf *b, pb_plan *plan,
                                    pb_plan_field *f, int val_idx);
static void encode_group_field(lua_State *L, enc_buf *b, pb_plan *plan,
                                pb_plan_field *f, int val_idx);
static void encode_map_field(lua_State *L, enc_buf *b, pb_plan *plan,
                              pb_plan_field *f, int val_idx);
static void encode_extension(lua_State *L, enc_buf *b, pb_plan *plan,
                              pb_plan_field *ext, int val_idx);

/* Encode a repeated field's elements into `b`. Dispatches on element
 * kind and the `packed` plan flag:


@@ 1401,6 1474,15 @@ encode_repeated_field(lua_State *L, enc_buf *b, pb_plan *plan,
		return;

	if (f->kind == PB_KIND_MESSAGE) {
		if (f->is_group) {
			for (int i = 1; i <= n; i++) {
				lua_rawgeti(L, val_idx, i);
				int elem = lua_gettop(L);
				encode_group_field(L, b, plan, f, elem);
				lua_pop(L, 1);
			}
			return;
		}
		for (int i = 1; i <= n; i++) {
			lua_rawgeti(L, val_idx, i);
			int elem = lua_gettop(L);


@@ 1519,6 1601,77 @@ encode_submessage_field(lua_State *L, enc_buf *b, pb_plan *plan,
	lua_settop(L, saved_top);
}

/* Encode one singular proto2 group field: SGROUP tag, nested body bytes
 * verbatim (no length prefix), EGROUP tag. The body is built into a fresh
 * sub-buffer so the SGROUP/EGROUP bracket lands on the parent in one shot;
 * the lifecycle contract is identical to encode_submessage_field. */
static void
encode_group_field(lua_State *L, enc_buf *b, pb_plan *plan,
                    pb_plan_field *f, int val_idx)
{
	if (plan->sub_plans_ref == LUA_NOREF)
		luaL_error(L, "plan '%s' has no sub-plans table",
		           plan->name != NULL ? plan->name : "?");

	if (b->heap_idx == 0)
		ebuf_grow(L, b, 1);

	val_idx = abs_idx(L, val_idx);
	int saved_top = lua_gettop(L);

	lua_rawgeti(L, LUA_REGISTRYINDEX, plan->sub_plans_ref);
	lua_rawgeti(L, -1, f->sub_plan_idx);
	pb_plan *subplan = (pb_plan *)lua_touserdata(L, -1);
	if (subplan == NULL)
		luaL_error(L, "group sub-plan at index %d is not a userdata",
		           f->sub_plan_idx);

	if (lua_type(L, val_idx) != LUA_TTABLE)
		luaL_error(L, "group field requires a table value");

	enc_buf sub;
	ebuf_init(&sub);
	encode_body(L, &sub, subplan, val_idx);

	/* SGROUP tag + body + EGROUP tag (no length prefix). */
	ebuf_reserve(L, b, f->tag_len + sub.used + f->egroup_tag_len);
	ebuf_put_tag(b, f);
	if (sub.used > 0)
		ebuf_put_bytes(b, ebuf_base(&sub), sub.used);
	memcpy(ebuf_base(b) + b->used, f->egroup_tag_bytes, f->egroup_tag_len);
	b->used += f->egroup_tag_len;

	lua_settop(L, saved_top);
}

/* Encode one proto2 extension value into the parent buffer `b`. The
 * extension field shape mirrors a regular field; for the encode dispatch
 * we route through the same singular/repeated/message/group paths used
 * by the field-walk. Always force-emit (proto2 extensions are
 * presence-tracked: a user-set zero must reach the wire). */
static void
encode_extension(lua_State *L, enc_buf *b, pb_plan *plan,
                  pb_plan_field *ext, int val_idx)
{
	val_idx = abs_idx(L, val_idx);
	if (ext->repeated) {
		if (lua_type(L, val_idx) != LUA_TTABLE)
			luaL_error(L,
				"repeated extension '%s' requires a table value",
				ext->full_name != NULL ? ext->full_name : "?");
		encode_repeated_field(L, b, plan, ext, val_idx);
		return;
	}
	if (ext->kind == PB_KIND_MESSAGE) {
		if (ext->is_group)
			encode_group_field(L, b, plan, ext, val_idx);
		else
			encode_submessage_field(L, b, plan, ext, val_idx);
		return;
	}
	encode_one_field(L, b, ext, val_idx, /* force_emit */ 1);
}

/* Encode a map<K,V> field into `b`.
 *
 * Wire shape: each (k, v) pair becomes a length-delimited entry sub-


@@ 1704,6 1857,16 @@ encode_body(lua_State *L, enc_buf *b, pb_plan *plan, int msg_idx)
		int val_idx = lua_gettop(L);

		if (lua_isnil(L, val_idx)) {
			/* Proto2 required: missing → hard error with full path,
			 * matching codec.lua's build_required_writer. */
			if (f->required) {
				lua_rawgeti(L, names_idx, i + 1);
				const char *fname = lua_tostring(L, -1);
				luaL_error(L,
					"required field missing on encode: %s.%s",
					plan->name != NULL ? plan->name : "?",
					fname != NULL ? fname : "?");
			}
			lua_pop(L, 1);
			continue;
		}


@@ 1716,14 1879,42 @@ encode_body(lua_State *L, enc_buf *b, pb_plan *plan, int msg_idx)
					"repeated field requires a table value");
			encode_repeated_field(L, b, plan, f, val_idx);
		} else if (f->kind == PB_KIND_MESSAGE) {
			encode_submessage_field(L, b, plan, f, val_idx);
			if (f->is_group)
				encode_group_field(L, b, plan, f, val_idx);
			else
				encode_submessage_field(L, b, plan, f, val_idx);
		} else {
			int force = f->oneof_idx >= 0 ? 1 : 0;
			/* Force emit when the field has presence: oneof member,
			 * proto2 required, or proto2 explicit-optional. The
			 * non-forced path proto3-elides zeros. */
			int force = (f->oneof_idx >= 0 || f->required) ? 1 : 0;
			encode_one_field(L, b, f, val_idx, force);
		}
		lua_pop(L, 1);
	}

	/* Proto2 extensions: walk plan->extensions and emit each present
	 * entry from data._extensions[ext.full_name]. Registration order
	 * == iteration order (matches codec.lua's extensions_list walk). */
	if (plan->n_extensions > 0) {
		lua_getfield(L, msg_idx, "_extensions");
		if (lua_type(L, -1) == LUA_TTABLE) {
			int exts_idx = lua_gettop(L);
			for (int i = 0; i < plan->n_extensions; i++) {
				pb_plan_field *ext = &plan->extensions[i];
				lua_getfield(L, exts_idx,
				             ext->full_name != NULL ?
				             ext->full_name : "");
				if (!lua_isnil(L, -1)) {
					encode_extension(L, b, plan, ext,
					                 lua_gettop(L));
				}
				lua_pop(L, 1);
			}
		}
		lua_pop(L, 1); /* _extensions (table or nil) */
	}

	/* Re-emit captured unknown bytes at the tail (bd-wyp / ra6 3j).
	 * Mirrors codec.lua's `encode_message`: nil or "" are no-ops; any
	 * non-empty string is appended verbatim. */


@@ 1845,9 2036,20 @@ dec_fixed64(dec_ctx *c)
	return v;
}

/* For SGROUP recursion we need to thread the opening field id so the closing
 * EGROUP can be id-matched per proto2 spec. Mirrors runtime/pb/wire.lua's
 * skip_field(buf, pos, wt, field_id). */
static void dec_skip_with_id(dec_ctx *c, uint8_t wt, uint32_t field_id);

static void
dec_skip(dec_ctx *c, uint8_t wt)
{
	dec_skip_with_id(c, wt, 0);
}

static void
dec_skip_with_id(dec_ctx *c, uint8_t wt, uint32_t field_id)
{
	switch (wt) {
	case PB_WIRE_VARINT:
		(void)dec_varint(c);


@@ 1865,6 2067,31 @@ dec_skip(dec_ctx *c, uint8_t wt)
		c->pos += (size_t)plen;
		break;
	}
	case PB_WIRE_SGROUP: {
		if (field_id == 0)
			luaL_error(c->L,
				"skip SGROUP requires field id for EGROUP match");
		while (c->pos < c->len) {
			uint64_t itag = dec_varint(c);
			uint32_t iid  = (uint32_t)(itag >> 3);
			uint8_t  iwt  = (uint8_t)(itag & 0x07);
			if (iwt == PB_WIRE_EGROUP) {
				if (iid != field_id)
					luaL_error(c->L,
						"EGROUP id %d does not match SGROUP id %d",
						(int)iid, (int)field_id);
				return;
			}
			dec_skip_with_id(c, iwt, iid);
		}
		luaL_error(c->L,
			"unterminated SGROUP for field id %d", (int)field_id);
		break;
	}
	case PB_WIRE_EGROUP:
		luaL_error(c->L, "unexpected EGROUP for field id %d",
		           (int)field_id);
		break;
	default:
		luaL_error(c->L, "unsupported wire type %d for skip", (int)wt);
	}


@@ 1992,12 2219,22 @@ dec_push_one(dec_ctx *c, pb_plan_field *f)
	dec_push_kind(c, f->kind);
}

/* Forward decl for the recursive decode. */
static void decode_body(dec_ctx *c, pb_plan *plan, int result_idx);
/* Forward decl for the recursive decode.
 *
 * `stop_group_id`: 0 means decode to end of c->len (normal message); non-zero
 * means we're inside a proto2 group body and the loop terminates on the
 * matching EGROUP tag. After EGROUP, c->pos sits just past the closing tag.
 * Mirrors codec.lua's decode_group. */
static void decode_body(dec_ctx *c, pb_plan *plan, int result_idx,
                        uint32_t stop_group_id);
static void decode_submessage_field(dec_ctx *c, pb_plan_field *f,
                                    int sub_plans_idx);
static void decode_group_field(dec_ctx *c, pb_plan_field *f, int sub_plans_idx);
static void decode_extension_into(dec_ctx *c, pb_plan_field *ext, uint8_t wt,
                                  int sub_plans_idx, int result_idx);
static void decode_map_entry(dec_ctx *c, pb_plan_field *f, int sub_plans_idx,
                              int map_idx);
static inline int field_is_packable(const pb_plan_field *f);

/* Decode one singular sub-message field. On entry, `c->pos` points at
 * the length-varint byte; on exit, `c->pos == c->pos + plen`. Pushes


@@ 2038,7 2275,7 @@ decode_submessage_field(dec_ctx *c, pb_plan_field *f, int sub_plans_idx)
	 * spill past it. */
	size_t saved_len = c->len;
	c->len = c->pos + (size_t)plen;
	decode_body(c, subplan, sub_result_idx);
	decode_body(c, subplan, sub_result_idx, /* stop_group_id */ 0);
	if (c->pos != c->len)
		luaL_error(L,
			"nested message body underflow at offset %d (expected %d)",


@@ 2046,6 2283,118 @@ decode_submessage_field(dec_ctx *c, pb_plan_field *f, int sub_plans_idx)
	c->len = saved_len;
}

/* Decode one proto2 group field. On entry, c->pos sits just past the
 * SGROUP tag — we walk the body via decode_body with stop_group_id set
 * to the field's id; decode_body terminates on EGROUP with matching id
 * and leaves c->pos just past the closing tag. The decoded sub-table
 * is left on top of the Lua stack (mirrors decode_submessage_field). */
static void
decode_group_field(dec_ctx *c, pb_plan_field *f, int sub_plans_idx)
{
	lua_State *L = c->L;
	lua_rawgeti(L, sub_plans_idx, f->sub_plan_idx);
	pb_plan *subplan = (pb_plan *)lua_touserdata(L, -1);
	if (subplan == NULL)
		luaL_error(L, "group sub-plan at index %d is not a userdata",
		           f->sub_plan_idx);
	lua_pop(L, 1);

	lua_createtable(L, 0, subplan->n_fields);
	int sub_result_idx = lua_gettop(L);

	/* Groups have no length prefix; decode_body walks raw bytes until
	 * the matching EGROUP tag. The outer c->len bound still applies
	 * (unterminated group ⇒ error). */
	decode_body(c, subplan, sub_result_idx, f->field_number);
}

/* Decode one proto2 extension's wire bytes into result._extensions[full_name].
 * Mirrors codec.lua's decode_extension: scalar/enum/message/group, singular
 * vs repeated, packed-payload handling. */
static void
decode_extension_into(dec_ctx *c, pb_plan_field *ext, uint8_t wt,
                      int sub_plans_idx, int result_idx)
{
	lua_State *L = c->L;

	/* Find or create result._extensions; leave it on top of the stack
	 * as `exts_idx`. */
	lua_getfield(L, result_idx, "_extensions");
	if (!lua_istable(L, -1)) {
		lua_pop(L, 1);
		lua_createtable(L, 0, 4);
		lua_pushvalue(L, -1);
		lua_setfield(L, result_idx, "_extensions");
	}
	int exts_idx = lua_gettop(L);
	const char *key = ext->full_name != NULL ? ext->full_name : "";

	if (ext->repeated) {
		lua_getfield(L, exts_idx, key);
		if (!lua_istable(L, -1)) {
			lua_pop(L, 1);
			lua_createtable(L, 0, 0);
			lua_pushvalue(L, -1);
			lua_setfield(L, exts_idx, key);
		}
		int list_idx = lua_gettop(L);

		if (ext->kind == PB_KIND_MESSAGE) {
			if (ext->is_group) {
				if (wt != PB_WIRE_SGROUP)
					luaL_error(L,
						"repeated group extension '%s' expected wire 3, got %d",
						key, (int)wt);
				decode_group_field(c, ext, sub_plans_idx);
			} else {
				if (wt != PB_WIRE_LEN)
					luaL_error(L,
						"repeated message extension '%s' expected wire 2, got %d",
						key, (int)wt);
				decode_submessage_field(c, ext, sub_plans_idx);
			}
			lua_rawseti(L, list_idx,
			            (int)lua_objlen(L, list_idx) + 1);
		} else if (wt == PB_WIRE_LEN && field_is_packable(ext)) {
			/* Packed payload for a packable extension element. */
			uint64_t plen = dec_varint(c);
			if (c->len - c->pos < plen)
				luaL_error(L,
					"truncated packed extension '%s' payload", key);
			size_t saved_len = c->len;
			c->len = c->pos + (size_t)plen;
			while (c->pos < c->len) {
				dec_push_one(c, ext);
				lua_rawseti(L, list_idx,
				            (int)lua_objlen(L, list_idx) + 1);
			}
			if (c->pos != c->len)
				luaL_error(L,
					"packed extension '%s' underflow", key);
			c->len = saved_len;
		} else {
			dec_push_one(c, ext);
			lua_rawseti(L, list_idx,
			            (int)lua_objlen(L, list_idx) + 1);
		}
		lua_pop(L, 2); /* list, _extensions */
		return;
	}

	/* Singular extension. Scalars/enums use last-wins; messages merge. */
	if (ext->kind == PB_KIND_MESSAGE) {
		if (ext->is_group)
			decode_group_field(c, ext, sub_plans_idx);
		else
			decode_submessage_field(c, ext, sub_plans_idx);
		lua_setfield(L, exts_idx, key);
	} else {
		dec_push_one(c, ext);
		lua_setfield(L, exts_idx, key);
	}
	lua_pop(L, 1); /* _extensions */
}

/* Decode one map<K,V> entry from the wire and lua_rawset it into the
 * map table at absolute stack index `map_idx`.
 *


@@ 2122,7 2471,8 @@ decode_map_entry(dec_ctx *c, pb_plan_field *f, int sub_plans_idx,
					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);
					decode_body(c, vsub, new_val,
					            /* stop_group_id */ 0);
					if (c->pos != c->len)
						luaL_error(L,
							"nested map<,message> body underflow");


@@ 2172,7 2522,8 @@ field_is_packable(const pb_plan_field *f)
}

static void
decode_body(dec_ctx *c, pb_plan *plan, int result_idx)
decode_body(dec_ctx *c, pb_plan *plan, int result_idx,
            uint32_t stop_group_id)
{
	lua_State *L = c->L;
	if (plan->override_decode_ref != LUA_NOREF) {


@@ 2235,6 2586,22 @@ decode_body(dec_ctx *c, pb_plan *plan, int result_idx)
		uint32_t field_number = (uint32_t)(tag >> 3);
		uint8_t  wt           = (uint8_t)(tag & 0x07);

		/* Proto2 group body: EGROUP with matching id terminates this
		 * decode_body call. A mismatched id is a hard error per spec. */
		if (wt == PB_WIRE_EGROUP) {
			if (stop_group_id == 0)
				luaL_error(L,
					"unexpected EGROUP for field id %d at top level",
					(int)field_number);
			if (field_number != stop_group_id)
				luaL_error(L,
					"EGROUP id %d does not match SGROUP id %d",
					(int)field_number, (int)stop_group_id);
			/* Successful close — drop into the unknown-fields
			 * tail handling below. */
			break;
		}

		/* Linear scan over plan->fields. n_fields is typically small;
		 * tag-keyed dispatch table is a future optimization. */
		pb_plan_field *f = NULL;


@@ 2247,9 2614,25 @@ decode_body(dec_ctx *c, pb_plan *plan, int result_idx)
			}
		}

		/* Unknown tag — capture tag+payload verbatim into `unknown`. */
		/* Unknown tag — proto2 extensions get a second chance before
		 * the bytes are stashed verbatim as result._unknown_fields. */
		if (f == NULL) {
			dec_skip(c, wt);
			if (plan->n_extensions > 0) {
				pb_plan_field *ext = NULL;
				for (int i = 0; i < plan->n_extensions; i++) {
					if (plan->extensions[i].field_number
					        == field_number) {
						ext = &plan->extensions[i];
						break;
					}
				}
				if (ext != NULL) {
					decode_extension_into(c, ext, wt,
					    sub_plans_idx, result_idx);
					continue;
				}
			}
			dec_skip_with_id(c, wt, field_number);
			size_t chunk = c->pos - tag_start;
			ebuf_reserve(L, &unknown, chunk);
			ebuf_put_bytes(&unknown, c->buf + tag_start, chunk);


@@ 2301,13 2684,23 @@ decode_body(dec_ctx *c, pb_plan *plan, int result_idx)
				list_count[f_idx] = 0;
			}

			/* Repeated message: per-element length-delimited body. */
			/* Repeated message: per-element length-delimited body.
			 * Repeated proto2 group: SGROUP-framed body, one per
			 * element. */
			if (f->kind == PB_KIND_MESSAGE) {
				if (wt != PB_WIRE_LEN)
					luaL_error(L,
						"repeated message field %d expected wire 2, got %d",
						(int)field_number, (int)wt);
				decode_submessage_field(c, f, sub_plans_idx);
				if (f->is_group) {
					if (wt != PB_WIRE_SGROUP)
						luaL_error(L,
							"repeated group field %d expected wire 3, got %d",
							(int)field_number, (int)wt);
					decode_group_field(c, f, sub_plans_idx);
				} else {
					if (wt != PB_WIRE_LEN)
						luaL_error(L,
							"repeated message field %d expected wire 2, got %d",
							(int)field_number, (int)wt);
					decode_submessage_field(c, f, sub_plans_idx);
				}
				/* Stack top is the decoded sub-table. */
				list_count[f_idx]++;
				lua_rawseti(L, list_idx, list_count[f_idx]);


@@ 2351,7 2744,10 @@ decode_body(dec_ctx *c, pb_plan *plan, int result_idx)
		 *  Singular dispatch                                      *
		 * ------------------------------------------------------ */
		if (f->kind == PB_KIND_MESSAGE) {
			decode_submessage_field(c, f, sub_plans_idx);
			if (f->is_group)
				decode_group_field(c, f, sub_plans_idx);
			else
				decode_submessage_field(c, f, sub_plans_idx);
			/* stack: ..., names, sub_plans, [lists...], sub_result */
			lua_rawgeti(L, names_idx, f_idx + 1);
			lua_insert(L, -2);            /* name, sub_result */


@@ 2423,7 2819,7 @@ decode_lua(lua_State *L)
	c.len = buf_len;
	c.pos = 0;

	decode_body(&c, plan, result_idx);
	decode_body(&c, plan, result_idx, /* stop_group_id */ 0);
	return 1;
}


A test/c_runtime_proto2_test.lua => test/c_runtime_proto2_test.lua +264 -0
@@ 0,0 1,264 @@
-- Tests for bd-m7u / ra6 3i: proto2 — required, defaults, groups, extensions.
--
-- The proto2 semantics (required-missing on encode, presence-tracked
-- optionals, SGROUP/EGROUP framing, registered extensions) must match the
-- pure-Lua codec byte-for-byte. We exercise the same fixtures as
-- test/proto2_test.lua but route encode/decode through the C runtime, and
-- compare against the full-mode pure-Lua output for parity.

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 function hex(s)
    local out = {}
    for i = 1, #s do out[i] = string.format('%02x', s:byte(i)) end
    return table.concat(out)
end

for _, mode in ipairs({'full', 'runtime'}) do
    local g = t.group('c_runtime_proto2.' .. mode)
    local pb2
    local full

    g.before_all(function()
        skip_if_no_c()
        pb2  = require(mode .. '.proto2_basic.proto2_basic_pb')
        full = require('full.proto2_basic.proto2_basic_pb')
    end)

    g.before_each(skip_if_no_c)

    -- ---------- Required: error on missing, force-emit at zero ----------

    function g.test_required_missing_errors()
        local plan = c_runtime.compile_plan(pb2.Cardinality_descriptor)
        local ok, err = pcall(c_runtime.encode, plan, {})
        t.assert_equals(ok, false)
        t.assert_str_contains(err, 'required field missing on encode')
        t.assert_str_contains(err, 'proto2_basic.Cardinality.r')
    end

    function g.test_required_zero_emitted_byte_equal()
        local plan = c_runtime.compile_plan(pb2.Cardinality_descriptor)
        local c_bytes = c_runtime.encode(plan, {r = 0})
        -- tag 1 / wire VARINT (0x08) + varint 0.
        t.assert_equals(hex(c_bytes), '0800')
        t.assert_equals(c_bytes, full.Cardinality_encode({r = 0}))
    end

    function g.test_required_set_round_trip()
        local plan = c_runtime.compile_plan(pb2.Cardinality_descriptor)
        local bytes = c_runtime.encode(plan, {r = 7})
        t.assert_equals(bytes, full.Cardinality_encode({r = 7}))
        local dec = c_runtime.decode(plan, bytes)
        t.assert_equals(dec.r, 7)
    end

    function g.test_nested_required_message_missing_errors()
        local plan = c_runtime.compile_plan(pb2.Nested_descriptor)
        local ok, err = pcall(c_runtime.encode, plan, {})
        t.assert_equals(ok, false)
        t.assert_str_contains(err, 'proto2_basic.Nested.inner')
    end

    function g.test_nested_required_inner_required_errors()
        -- Outer .inner is present (table) but inner.x is missing.
        local plan = c_runtime.compile_plan(pb2.Nested_descriptor)
        local ok, err = pcall(c_runtime.encode, plan, {inner = {}})
        t.assert_equals(ok, false)
        t.assert_str_contains(err, 'proto2_basic.Nested.Inner.x')
    end

    function g.test_nested_required_filled_round_trips_byte_equal()
        local plan = c_runtime.compile_plan(pb2.Nested_descriptor)
        local val = {inner = {x = 5}, inner_opt = {x = 9}}
        local c_bytes = c_runtime.encode(plan, val)
        t.assert_equals(c_bytes, full.Nested_encode(val))
        local dec = c_runtime.decode(plan, c_bytes)
        t.assert_equals(dec.inner.x, 5)
        t.assert_equals(dec.inner_opt.x, 9)
    end

    -- ---------- Defaults: presence-tracked, not auto-emitted ----------

    function g.test_empty_message_round_trips_to_empty_bytes()
        local plan = c_runtime.compile_plan(pb2.Defaults_descriptor)
        local c_bytes = c_runtime.encode(plan, {})
        t.assert_equals(c_bytes, '', 'no defaults on the wire')
        t.assert_equals(c_runtime.decode(plan, c_bytes), {})
    end

    function g.test_set_to_proto_default_still_serializes()
        local plan = c_runtime.compile_plan(pb2.Defaults_descriptor)
        local c_bytes = c_runtime.encode(plan, {i = 17})
        t.assert_not_equals(c_bytes, '')
        t.assert_equals(c_bytes, full.Defaults_encode({i = 17}))
        local dec = c_runtime.decode(plan, c_bytes)
        t.assert_equals(dec.i, 17)
    end

    function g.test_defaults_set_value_round_trip()
        local plan = c_runtime.compile_plan(pb2.Defaults_descriptor)
        local val = {i = 42, s = 'world', b = false, f = -1.5}
        local c_bytes = c_runtime.encode(plan, val)
        t.assert_equals(c_bytes, full.Defaults_encode(val))
        local dec = c_runtime.decode(plan, c_bytes)
        t.assert_equals(dec.i, 42)
        t.assert_equals(dec.s, 'world')
        t.assert_equals(dec.b, false)
        t.assert_equals(dec.f, -1.5)
        -- Defaults are NOT auto-filled on decode for absent fields.
        t.assert_equals(dec.d, nil)
        t.assert_equals(dec.color, nil)
    end

    -- ---------- Groups: SGROUP/EGROUP framing ----------

    function g.test_group_singular_wire_bytes_byte_equal()
        local plan = c_runtime.compile_plan(pb2.WithGroup_descriptor)
        local val = {singlegroup = {a = 7, s = 'ok'}}
        local c_bytes = c_runtime.encode(plan, val)
        -- field 1 SGROUP (tag 0x0b), a=7 (0x10 0x07), s='ok' (0x1a 0x02 'ok'),
        -- EGROUP (tag 0x0c).
        t.assert_equals(hex(c_bytes),
            '0b' .. '10' .. '07' .. '1a' .. '02' .. '6f' .. '6b' .. '0c')
        t.assert_equals(c_bytes, full.WithGroup_encode(val))
    end

    function g.test_group_round_trip()
        local plan = c_runtime.compile_plan(pb2.WithGroup_descriptor)
        local val = {singlegroup = {a = 7, s = 'ok'}}
        local bytes = c_runtime.encode(plan, val)
        local dec = c_runtime.decode(plan, bytes)
        t.assert_equals(dec.singlegroup.a, 7)
        t.assert_equals(dec.singlegroup.s, 'ok')
    end

    function g.test_repeated_group_byte_equal()
        local plan = c_runtime.compile_plan(pb2.WithGroup_descriptor)
        local val = {repgroup = {{n = 1}, {n = 2}}}
        local c_bytes = c_runtime.encode(plan, val)
        -- Each rep wraps its own SGROUP(4)/EGROUP(4) bracket.
        t.assert_equals(hex(c_bytes),
            '23' .. '28' .. '01' .. '24' ..
            '23' .. '28' .. '02' .. '24')
        t.assert_equals(c_bytes, full.WithGroup_encode(val))

        local dec = c_runtime.decode(plan, c_bytes)
        t.assert_equals(#dec.repgroup, 2)
        t.assert_equals(dec.repgroup[1].n, 1)
        t.assert_equals(dec.repgroup[2].n, 2)
    end

    function g.test_group_decode_of_full_emit_bytes()
        -- Decode wire bytes produced by the pure-Lua encoder (which is the
        -- conformance reference). Catches any SGROUP/EGROUP framing skew.
        local plan = c_runtime.compile_plan(pb2.WithGroup_descriptor)
        local val = {
            singlegroup = {a = 11, s = 'wkt'},
            repgroup = {{n = 100}, {n = 200}, {n = 300}},
        }
        local bytes = full.WithGroup_encode(val)
        local dec = c_runtime.decode(plan, bytes)
        t.assert_equals(dec.singlegroup.a, 11)
        t.assert_equals(dec.singlegroup.s, 'wkt')
        t.assert_equals(dec.repgroup[1].n, 100)
        t.assert_equals(dec.repgroup[2].n, 200)
        t.assert_equals(dec.repgroup[3].n, 300)
    end

    -- ---------- Extensions: registered into extendee._extensions ----------

    function g.test_extension_round_trip_byte_equal()
        local plan = c_runtime.compile_plan(pb2.BenchPayload_descriptor)
        local msg = {
            id = 7,
            _extensions = {
                ['proto2_basic.ext_count'] = 42,
                ['proto2_basic.ext_label'] = 'tag',
            },
        }
        local c_bytes = c_runtime.encode(plan, msg)
        t.assert_equals(c_bytes, full.BenchPayload_encode(msg),
            'C encode of extensions must match full-mode byte-for-byte')
        local dec = c_runtime.decode(plan, c_bytes)
        t.assert_equals(dec.id, 7)
        t.assert_equals(dec._extensions['proto2_basic.ext_count'], 42)
        t.assert_equals(dec._extensions['proto2_basic.ext_label'], 'tag')
    end

    function g.test_extension_absent_emits_nothing()
        local plan = c_runtime.compile_plan(pb2.BenchPayload_descriptor)
        local msg = {id = 1}
        local c_bytes = c_runtime.encode(plan, msg)
        t.assert_equals(c_bytes, full.BenchPayload_encode(msg))
        t.assert_equals(hex(c_bytes), '0801')
    end

    function g.test_extension_decode_from_full_emit_bytes()
        -- Wire bytes for a registered extension must land in _extensions,
        -- not in _unknown_fields, when the extension is registered on the
        -- descriptor at plan-compile time.
        local plan = c_runtime.compile_plan(pb2.BenchPayload_descriptor)
        local msg = {
            id = 9,
            _extensions = {['proto2_basic.ext_count'] = 17},
        }
        local bytes = full.BenchPayload_encode(msg)
        local dec = c_runtime.decode(plan, bytes)
        t.assert_equals(dec.id, 9)
        t.assert_equals(dec._extensions['proto2_basic.ext_count'], 17)
        t.assert_equals(dec._unknown_fields, nil,
            'registered extensions must not fall through to _unknown_fields')
    end

    -- ---------- BenchPayload: combined required + group + extensions ----------

    function g.test_benchpayload_full_round_trip_byte_equal()
        local plan = c_runtime.compile_plan(pb2.BenchPayload_descriptor)
        local val = {
            id = 1,
            name = 'x',
            retries = 5,
            lucky_numbers = {1, 2, 3},
            tags = {'a', 'b'},
            inner = {key = 'k', weight = 9},
            stats = {latency_ns = 1234, attempts = 2},
            _extensions = {
                ['proto2_basic.ext_count'] = 11,
                ['proto2_basic.ext_label'] = 'lbl',
            },
        }
        local c_bytes = c_runtime.encode(plan, val)
        t.assert_equals(c_bytes, full.BenchPayload_encode(val),
            'BenchPayload byte-equality with full mode')

        local dec = c_runtime.decode(plan, c_bytes)
        t.assert_equals(dec.id, 1)
        t.assert_equals(dec.name, 'x')
        t.assert_equals(dec.retries, 5)
        t.assert_equals(dec.lucky_numbers, {1, 2, 3})
        t.assert_equals(dec.tags, {'a', 'b'})
        t.assert_equals(dec.inner.key, 'k')
        t.assert_equals(dec.inner.weight, 9)
        t.assert_equals(dec.stats.latency_ns, 1234)
        t.assert_equals(dec.stats.attempts, 2)
        t.assert_equals(dec._extensions['proto2_basic.ext_count'], 11)
        t.assert_equals(dec._extensions['proto2_basic.ext_label'], 'lbl')
    end

    function g.test_benchpayload_required_missing_errors()
        local plan = c_runtime.compile_plan(pb2.BenchPayload_descriptor)
        local ok, err = pcall(c_runtime.encode, plan, {name = 'no_id'})
        t.assert_equals(ok, false)
        t.assert_str_contains(err, 'proto2_basic.BenchPayload.id')
    end
end