diff --git a/runtime/pb/codec.lua b/runtime/pb/codec.lua index 5453043e7eaf04a858292257377428c0c695bfd3..9b8c796fb24a5d3624a7c1df470161fff21eaa78 100644 --- a/runtime/pb/codec.lua +++ b/runtime/pb/codec.lua @@ -224,15 +224,107 @@ -- in LuaJIT 2.1 when the trace recorder sees mixed shapes across fields. -- -- Shapes that fall through to encode_field (no writer set): -- - map fields (need `pairs()` over user data; can't be helped) --- - repeated fields (TODO — common shape, worth specializing) -- - fields inside a oneof (active-branch dispatch happens in encode_message) -- --------------------------------------------------------------------------- +local function build_repeated_writer(f) + local fname = f.name + local kind = f.kind + + if kind == 'scalar' then + local handler = scalar[f.proto_type] + if not handler then return nil end + local encode_value = handler.encode + + if f.packed and handler.packable then + local tag_bytes = wire.encode_tag(f.id, wire.WIRE_LEN) + return function(data, out) + local v = data[fname] + if v == nil then return end + local nv = #v + if nv == 0 then return end + local parts = {} + for i = 1, nv do parts[i] = encode_value(v[i]) end + local payload = table.concat(parts) + local n = #out + out[n + 1] = tag_bytes + out[n + 2] = wire.encode_len(payload) + end + end + + -- Unpacked repeated scalar (also covers string/bytes — LEN + -- wire type, which is non-packable by proto3 rules). + local tag_bytes = wire.encode_tag(f.id, handler.wire) + return function(data, out) + local v = data[fname] + if v == nil then return end + local nv = #v + if nv == 0 then return end + local n = #out + for i = 1, nv do + n = n + 1; out[n] = tag_bytes + n = n + 1; out[n] = encode_value(v[i]) + end + end + end + + if kind == 'message' then + local sub_desc = f.message + local tag_bytes = wire.encode_tag(f.id, wire.WIRE_LEN) + return function(data, out) + local v = data[fname] + if v == nil then return end + local nv = #v + if nv == 0 then return end + local n = #out + for i = 1, nv do + n = n + 1; out[n] = tag_bytes + n = n + 1; out[n] = wire.encode_len(encode_msg(sub_desc, v[i])) + end + end + end + + if kind == 'enum' then + local enum_desc = f.enum + -- proto3 default: repeated enums are packed unless explicitly disabled. + if f.packed ~= false then + local tag_bytes = wire.encode_tag(f.id, wire.WIRE_LEN) + return function(data, out) + local v = data[fname] + if v == nil then return end + local nv = #v + if nv == 0 then return end + local parts = {} + for i = 1, nv do + parts[i] = wire.encode_varint(encode_enum_value(enum_desc, v[i])) + end + local n = #out + out[n + 1] = tag_bytes + out[n + 2] = wire.encode_len(table.concat(parts)) + end + end + local tag_bytes = wire.encode_tag(f.id, wire.WIRE_VARINT) + return function(data, out) + local v = data[fname] + if v == nil then return end + local nv = #v + if nv == 0 then return end + local n = #out + for i = 1, nv do + n = n + 1; out[n] = tag_bytes + n = n + 1; out[n] = wire.encode_varint(encode_enum_value(enum_desc, v[i])) + end + end + end + + return nil +end + local function build_writer(f) -- Maps and oneof branches keep going through encode_field. if f.kind == 'map' or f.oneof then return nil end - -- Repeated fields fall through for now. - if f.repeated then return nil end + + if f.repeated then return build_repeated_writer(f) end local fname = f.name local kind = f.kind