~bigbes/tarantool

tarantool-protobuf

2656c977e43bd649d9ac766cec637c6733706478 — Eugene Blikh 3 months ago 79efcdc
bench,codec: pin proto2 paths, kill pairs() on extension hot path

JIT trace gate gains 12 proto2-specific checks (required encode/decode,
groups singular + repeated, extension encode/decode). The extension-encode
check fired NYI on bytecode 72 (ISNEXT) — pairs() over the new
extensions_by_full_name hash is the same trace-abort that gates map
encode. Fix: register_extension now also appends to extensions_list,
an array view. codec.encode_message, text.emit_message and
json.encode_message all switch to ipairs over the list. Hash tables
stay around for O(1) lookups in decode (extensions_by_id) and bracket-
name resolution (extensions_by_full_name).

Inline (full-mode) codegen learns to walk extensions too: before this
commit the inline encoder skipped t._extensions entirely (only the
runtime codec walked it), so a generated _encode silently dropped any
extension data set on the message. Add the array-walk after the field
loop and a decode_extension dispatch in the unknown-tag branch — the
inline path now matches runtime byte-for-byte.

bench/bench.lua parameterizes over a FIXTURES list so the baseline can
cover both hello.Person and a new proto2_basic.BenchPayload fixture
(required + group + extension + repeated). baseline.json restructured
under "schemas": [{schema, results}, …]; compare walks both. Fresh
numbers committed.

740/740 luatest cases pass; JIT gate 37/37; conformance 2806 binary+JSON
and 434 text-format, both 0 failures.
M bench/baseline.json => bench/baseline.json +49 -28
@@ 1,35 1,56 @@
{
  "schema_message": "hello.Person",
  "results": [
  "schemas": [
    {
      "size_label": "10B",
      "size_bytes": 10,
      "encode": {"alloc_kb_per_op_full": 0.367, "alloc_kb_per_op_runtime": 0.633},
      "decode": {"alloc_kb_per_op_full": 0.508, "alloc_kb_per_op_runtime": 0.508}
      "schema": "hello.Person",
      "results": [
        {
          "size_label": "10B",
          "size_bytes": 10,
          "encode": {"alloc_kb_per_op_full": 0.133, "alloc_kb_per_op_runtime": 0.133},
          "decode": {"alloc_kb_per_op_full": 0.109, "alloc_kb_per_op_runtime": 0.109}
        },
        {
          "size_label": "100B",
          "size_bytes": 94,
          "encode": {"alloc_kb_per_op_full": 0.133, "alloc_kb_per_op_runtime": 0.133},
          "decode": {"alloc_kb_per_op_full": 0.109, "alloc_kb_per_op_runtime": 0.109}
        },
        {
          "size_label": "1KB",
          "size_bytes": 930,
          "encode": {"alloc_kb_per_op_full": 1.336, "alloc_kb_per_op_runtime": 1.336},
          "decode": {"alloc_kb_per_op_full": 0.977, "alloc_kb_per_op_runtime": 0.977}
        },
        {
          "size_label": "10KB",
          "size_bytes": 9634,
          "encode": {"alloc_kb_per_op_full": 8.340, "alloc_kb_per_op_runtime": 8.340},
          "decode": {"alloc_kb_per_op_full": 4.727, "alloc_kb_per_op_runtime": 4.727}
        },
        {
          "size_label": "100KB",
          "size_bytes": 96674,
          "encode": {"alloc_kb_per_op_full": 128.521, "alloc_kb_per_op_runtime": 128.521},
          "decode": {"alloc_kb_per_op_full": 32.727, "alloc_kb_per_op_runtime": 32.727}
        }
      ]
    },
    {
      "size_label": "100B",
      "size_bytes": 94,
      "encode": {"alloc_kb_per_op_full": 0.367, "alloc_kb_per_op_runtime": 0.633},
      "decode": {"alloc_kb_per_op_full": 0.508, "alloc_kb_per_op_runtime": 0.508}
    },
    {
      "size_label": "1KB",
      "size_bytes": 930,
      "encode": {"alloc_kb_per_op_full": 5.978, "alloc_kb_per_op_runtime": 7.040},
      "decode": {"alloc_kb_per_op_full": 7.750, "alloc_kb_per_op_runtime": 7.883}
    },
    {
      "size_label": "10KB",
      "size_bytes": 9634,
      "encode": {"alloc_kb_per_op_full": 47.489, "alloc_kb_per_op_runtime": 48.551},
      "decode": {"alloc_kb_per_op_full": 59.500, "alloc_kb_per_op_runtime": 59.633}
    },
    {
      "size_label": "100KB",
      "size_bytes": 96674,
      "encode": {"alloc_kb_per_op_full": 444.126, "alloc_kb_per_op_runtime": 445.189},
      "decode": {"alloc_kb_per_op_full": 567.500, "alloc_kb_per_op_runtime": 567.633}
      "schema": "proto2_basic.BenchPayload",
      "results": [
        {
          "size_label": "min",
          "size_bytes": 5,
          "encode": {"alloc_kb_per_op_full": 0.102, "alloc_kb_per_op_runtime": 0.102},
          "decode": {"alloc_kb_per_op_full": 0.359, "alloc_kb_per_op_runtime": 0.359}
        },
        {
          "size_label": "mid",
          "size_bytes": 609,
          "encode": {"alloc_kb_per_op_full": 1.438, "alloc_kb_per_op_runtime": 1.438},
          "decode": {"alloc_kb_per_op_full": 2.313, "alloc_kb_per_op_runtime": 2.313}
        }
      ]
    }
  ]
}

M bench/bench.lua => bench/bench.lua +193 -84
@@ 22,20 22,30 @@ local json  = require('json')
local fio   = require('fio')

local MODES = {'full', 'runtime'}
local SIZES = {

-- Per-fixture size lists. The proto3 Person fixture sweeps five decades to
-- characterize behavior across allocation regimes; proto2 only needs two
-- sizes (one fits-in-cache, one larger) because its purpose is to gate
-- the proto2-specific shapes (required/group/extension), not to recharacterize
-- alloc scaling — that's already covered by Person.
local PERSON_SIZES = {
    {label = '10B',   target = 10},
    {label = '100B',  target = 100},
    {label = '1KB',   target = 1024},
    {label = '10KB',  target = 10240},
    {label = '100KB', target = 102400},
}
local PROTO2_SIZES = {
    {label = 'min',  target = 0},     -- minimal: required + extension only
    {label = 'mid',  target = 1024},  -- with group + repeated + extension
}

-- Build a `Person` payload whose encoded size is close to `target` bytes.
--
-- Strategy: pick one knob per decade so each size still exercises the
-- full encoder (varints, packed repeated, length-delimited strings,
-- nested messages) — not just one giant byte-string.
local function build_payload(target)
local function build_person_payload(target)
    if target <= 10 then
        -- name(6) + age(1) ⇒ 10 bytes encoded.
        return {name = 'bigbes', age = 42}


@@ 65,6 75,65 @@ local function build_payload(target)
    return p
end

-- Build a `BenchPayload` proto2 message. Two sizes:
--   * `min` — required + one extension (~12 bytes): tightest measure of the
--     per-message overhead the new code paths add.
--   * `mid` — required + group + repeated + extension (~1 KB): exercises
--     SGROUP/EGROUP framing, the packed-int32 path under proto2 semantics
--     (NOT packed unless explicitly marked), the `pairs`-free
--     extensions_list walk, and the nested message branch.
local function build_proto2_payload(target)
    if target <= 0 then
        return {
            id = 7,
            _extensions = {
                ['proto2_basic.ext_count'] = 42,
            },
        }
    end
    local n_tags    = 16
    local per_tag   = 30
    local tags      = {}
    for i = 1, n_tags do
        tags[i] = string.rep('t', per_tag - 2) .. string.format('%02d', i)
    end
    local lucky = {}
    for i = 1, 8 do lucky[i] = 1000 + i end
    return {
        id            = 7,
        name          = 'bench',
        retries       = 9,
        lucky_numbers = lucky,
        tags          = tags,
        inner         = {key = string.rep('k', 16), weight = 3},
        stats         = {latency_ns = 1234567, attempts = 4},
        _extensions = {
            ['proto2_basic.ext_count'] = 99,
            ['proto2_basic.ext_label'] = string.rep('x', 32),
        },
    }
end

-- Each fixture binds a schema + payload builder + sizes to bench.
local FIXTURES = {
    {
        name = 'hello.Person',
        module_path = '.hello.hello_pb',
        encode_field = 'Person_encode',
        decode_field = 'Person_decode',
        build_payload = build_person_payload,
        sizes = PERSON_SIZES,
    },
    {
        name = 'proto2_basic.BenchPayload',
        module_path = '.proto2_basic.proto2_basic_pb',
        encode_field = 'BenchPayload_encode',
        decode_field = 'BenchPayload_decode',
        build_payload = build_proto2_payload,
        sizes = PROTO2_SIZES,
    },
}

-- Pick iteration count adaptively: smaller messages need more iters to
-- amortize loop + clock overhead; larger messages need fewer to keep
-- wall time bounded.


@@ 142,12 211,12 @@ local function bench_alloc(fn, expected_bytes)
    }
end

local function bench_one(mode, size)
    local hello_pb = require(mode .. '.hello.hello_pb')
    local encode   = hello_pb.Person_encode
    local decode   = hello_pb.Person_decode
local function bench_one(fixture, mode, size)
    local mod    = require(mode .. fixture.module_path)
    local encode = mod[fixture.encode_field]
    local decode = mod[fixture.decode_field]

    local payload = build_payload(size.target)
    local payload = fixture.build_payload(size.target)
    local bytes   = encode(payload)
    local n       = iter_count(#bytes)
    local runs    = 5


@@ 168,6 237,7 @@ local function bench_one(mode, size)
    dec_throughput.alloc_bytes_per_op = dec_alloc.bytes_per_op

    return {
        schema     = fixture.name,
        mode       = mode,
        size_label = size.label,
        size_bytes = #bytes,


@@ 177,24 247,28 @@ local function bench_one(mode, size)
end

local function run_all()
    local results = {}
    for _, mode in ipairs(MODES) do
        for _, size in ipairs(SIZES) do
            io.stderr:write(string.format('  bench %s/%s ... ', mode, size.label))
            io.stderr:flush()
            local r = bench_one(mode, size)
            io.stderr:write(string.format(
                'enc %.0f msgs/s (%.1f MB/s)  dec %.0f msgs/s (%.1f MB/s)\n',
                r.encode.msgs_per_s, r.encode.mb_per_s,
                r.decode.msgs_per_s, r.decode.mb_per_s))
            results[#results + 1] = r
    local schemas = {}
    for _, fixture in ipairs(FIXTURES) do
        io.stderr:write(string.format('schema %s\n', fixture.name))
        local results = {}
        for _, mode in ipairs(MODES) do
            for _, size in ipairs(fixture.sizes) do
                io.stderr:write(string.format('  bench %s/%s ... ', mode, size.label))
                io.stderr:flush()
                local r = bench_one(fixture, mode, size)
                io.stderr:write(string.format(
                    'enc %.0f msgs/s (%.1f MB/s)  dec %.0f msgs/s (%.1f MB/s)\n',
                    r.encode.msgs_per_s, r.encode.mb_per_s,
                    r.decode.msgs_per_s, r.decode.mb_per_s))
                results[#results + 1] = r
            end
        end
        schemas[#schemas + 1] = {schema = fixture.name, results = results}
    end
    return {
        tarantool       = _TARANTOOL,
        jit             = jit and jit.version or nil,
        schema_message  = 'hello.Person',
        results         = results,
        tarantool = _TARANTOOL,
        jit       = jit and jit.version or nil,
        schemas   = schemas,
    }
end



@@ 216,17 290,24 @@ local function render(doc)
    lines[#lines + 1] = '{'
    lines[#lines + 1] = string.format('  "tarantool": %s,', json.encode(doc.tarantool))
    lines[#lines + 1] = string.format('  "jit": %s,', json.encode(doc.jit or json.NULL))
    lines[#lines + 1] = string.format('  "schema_message": %s,', json.encode(doc.schema_message))
    lines[#lines + 1] = '  "results": ['
    for i, r in ipairs(doc.results) do
        local sep = (i == #doc.results) and '' or ','
    lines[#lines + 1] = '  "schemas": ['
    for si, schema in ipairs(doc.schemas) do
        local schema_sep = (si == #doc.schemas) and '' or ','
        lines[#lines + 1] = '    {'
        lines[#lines + 1] = string.format('      "mode": %s,', json.encode(r.mode))
        lines[#lines + 1] = string.format('      "size_label": %s,', json.encode(r.size_label))
        lines[#lines + 1] = string.format('      "size_bytes": %d,', r.size_bytes)
        lines[#lines + 1] = string.format('      "encode": %s,', render_metric(r.encode))
        lines[#lines + 1] = string.format('      "decode": %s', render_metric(r.decode))
        lines[#lines + 1] = '    }' .. sep
        lines[#lines + 1] = string.format('      "schema": %s,', json.encode(schema.schema))
        lines[#lines + 1] = '      "results": ['
        for i, r in ipairs(schema.results) do
            local sep = (i == #schema.results) and '' or ','
            lines[#lines + 1] = '        {'
            lines[#lines + 1] = string.format('          "mode": %s,', json.encode(r.mode))
            lines[#lines + 1] = string.format('          "size_label": %s,', json.encode(r.size_label))
            lines[#lines + 1] = string.format('          "size_bytes": %d,', r.size_bytes)
            lines[#lines + 1] = string.format('          "encode": %s,', render_metric(r.encode))
            lines[#lines + 1] = string.format('          "decode": %s', render_metric(r.decode))
            lines[#lines + 1] = '        }' .. sep
        end
        lines[#lines + 1] = '      ]'
        lines[#lines + 1] = '    }' .. schema_sep
    end
    lines[#lines + 1] = '  ]'
    lines[#lines + 1] = '}'


@@ 239,50 320,69 @@ end
-- bytes, not time — so that's all we commit. Throughput is in --print
-- output for human inspection only.
local function reduce_for_baseline(doc)
    local by_key = {}
    for _, r in ipairs(doc.results) do
        by_key[r.mode .. '/' .. r.size_label] = r
    end
    local out = {}
    for _, size in ipairs(SIZES) do
        local full    = by_key['full/'    .. size.label]
        local runtime = by_key['runtime/' .. size.label]
        out[#out + 1] = {
            size_label = size.label,
            size_bytes = full.size_bytes,
            encode = {
                alloc_kb_per_op_full    = full.encode.alloc_kb_per_op,
                alloc_kb_per_op_runtime = runtime.encode.alloc_kb_per_op,
            },
            decode = {
                alloc_kb_per_op_full    = full.decode.alloc_kb_per_op,
                alloc_kb_per_op_runtime = runtime.decode.alloc_kb_per_op,
            },
        }
    local out_schemas = {}
    for _, schema in ipairs(doc.schemas) do
        local by_key = {}
        for _, r in ipairs(schema.results) do
            by_key[r.mode .. '/' .. r.size_label] = r
        end
        -- Recover size order from the first mode's run (declaration order).
        local seen, sizes = {}, {}
        for _, r in ipairs(schema.results) do
            if not seen[r.size_label] then
                seen[r.size_label] = true
                sizes[#sizes + 1] = r.size_label
            end
        end
        local results = {}
        for _, label in ipairs(sizes) do
            local full    = by_key['full/'    .. label]
            local runtime = by_key['runtime/' .. label]
            results[#results + 1] = {
                size_label = label,
                size_bytes = full.size_bytes,
                encode = {
                    alloc_kb_per_op_full    = full.encode.alloc_kb_per_op,
                    alloc_kb_per_op_runtime = runtime.encode.alloc_kb_per_op,
                },
                decode = {
                    alloc_kb_per_op_full    = full.decode.alloc_kb_per_op,
                    alloc_kb_per_op_runtime = runtime.decode.alloc_kb_per_op,
                },
            }
        end
        out_schemas[#out_schemas + 1] = {schema = schema.schema, results = results}
    end
    return {schema_message = doc.schema_message, results = out}
    return {schemas = out_schemas}
end

local function render_baseline(reduced)
    local lines = {'{'}
    lines[#lines + 1] = string.format('  "schema_message": %s,', json.encode(reduced.schema_message))
    lines[#lines + 1] = '  "results": ['
    for i, r in ipairs(reduced.results) do
        local sep = (i == #reduced.results) and '' or ','
    lines[#lines + 1] = '  "schemas": ['
    for si, schema in ipairs(reduced.schemas) do
        local schema_sep = (si == #reduced.schemas) and '' or ','
        lines[#lines + 1] = '    {'
        lines[#lines + 1] = string.format('      "size_label": %s,', json.encode(r.size_label))
        lines[#lines + 1] = string.format('      "size_bytes": %d,', r.size_bytes)
        lines[#lines + 1] = string.format(
            '      "encode": {"alloc_kb_per_op_full": %.3f, '
            .. '"alloc_kb_per_op_runtime": %.3f},',
            r.encode.alloc_kb_per_op_full,
            r.encode.alloc_kb_per_op_runtime)
        lines[#lines + 1] = string.format(
            '      "decode": {"alloc_kb_per_op_full": %.3f, '
            .. '"alloc_kb_per_op_runtime": %.3f}',
            r.decode.alloc_kb_per_op_full,
            r.decode.alloc_kb_per_op_runtime)
        lines[#lines + 1] = '    }' .. sep
        lines[#lines + 1] = string.format('      "schema": %s,', json.encode(schema.schema))
        lines[#lines + 1] = '      "results": ['
        for i, r in ipairs(schema.results) do
            local sep = (i == #schema.results) and '' or ','
            lines[#lines + 1] = '        {'
            lines[#lines + 1] = string.format('          "size_label": %s,', json.encode(r.size_label))
            lines[#lines + 1] = string.format('          "size_bytes": %d,', r.size_bytes)
            lines[#lines + 1] = string.format(
                '          "encode": {"alloc_kb_per_op_full": %.3f, '
                .. '"alloc_kb_per_op_runtime": %.3f},',
                r.encode.alloc_kb_per_op_full,
                r.encode.alloc_kb_per_op_runtime)
            lines[#lines + 1] = string.format(
                '          "decode": {"alloc_kb_per_op_full": %.3f, '
                .. '"alloc_kb_per_op_runtime": %.3f}',
                r.decode.alloc_kb_per_op_full,
                r.decode.alloc_kb_per_op_runtime)
            lines[#lines + 1] = '        }' .. sep
        end
        lines[#lines + 1] = '      ]'
        lines[#lines + 1] = '    }' .. schema_sep
    end
    lines[#lines + 1] = '  ]'
    lines[#lines + 1] = '}'


@@ 299,27 399,36 @@ end
local function compare(current, baseline, tolerance)
    local function index(b)
        local m = {}
        for _, r in ipairs(b.results) do m[r.size_label] = r end
        for _, schema in ipairs(b.schemas or {}) do
            for _, r in ipairs(schema.results) do
                m[schema.schema .. '|' .. r.size_label] = r
            end
        end
        return m
    end
    local cur = index(current)
    local cur  = index(current)
    local base = index(baseline)
    local regressions = {}
    for _, size in ipairs(SIZES) do
        local c = cur[size.label]
        local b = base[size.label]
        if not c or not b then goto continue end
        for _, op in ipairs({'encode', 'decode'}) do
            for _, key in ipairs({'alloc_kb_per_op_full', 'alloc_kb_per_op_runtime'}) do
                local bv, cv = b[op][key], c[op][key]
                if bv > 0 and cv > bv * (1 + tolerance) then
                    regressions[#regressions + 1] = string.format(
                        '%s/%s %s: %.3f -> %.3f KB/op (+%.1f%%)',
                        size.label, op, key, bv, cv, (cv / bv - 1) * 100)
    -- Walk in baseline declaration order so the report is stable.
    for _, schema in ipairs(baseline.schemas or {}) do
        for _, r in ipairs(schema.results) do
            local key = schema.schema .. '|' .. r.size_label
            local c = cur[key]
            local b = base[key]
            if c and b then
                for _, op in ipairs({'encode', 'decode'}) do
                    for _, ak in ipairs({'alloc_kb_per_op_full', 'alloc_kb_per_op_runtime'}) do
                        local bv, cv = b[op][ak], c[op][ak]
                        if bv > 0 and cv > bv * (1 + tolerance) then
                            regressions[#regressions + 1] = string.format(
                                '%s %s/%s %s: %.3f -> %.3f KB/op (+%.1f%%)',
                                schema.schema, r.size_label, op, ak,
                                bv, cv, (cv / bv - 1) * 100)
                        end
                    end
                end
            end
        end
        ::continue::
    end
    return regressions
end

M bench/jit_trace.lua => bench/jit_trace.lua +70 -0
@@ 269,6 269,76 @@ for _, mode in ipairs({'full', 'runtime'}) do
        function() hello.Person_decode(big_person_bytes) end)
end

-- ---------------------------------------------------------------------------
-- Proto2-specific shapes: required, groups, extensions, closed enums.
--
-- The new code paths share most of their machinery with proto3 message and
-- scalar fields, but introduce three distinct shapes worth pinning:
--
--   1. The `required` writer wraps the regular scalar writer with a missing-
--      value error. Should stay JIT-stable when the value is set.
--   2. Group fields use SGROUP/EGROUP tags and a per-element body without a
--      length prefix. Different wire shape than messages; new writer/reader.
--   3. Extensions are walked at encode time via a `pairs()` over an array
--      view (`extensions_list`), and looked up at decode time via the
--      `extensions_by_id` hash. Both sides need to stay on trace; the
--      hash lookup is one cdata read so should be fine, but the encode
--      walk would abort if we used `pairs()` over a non-array — see the
--      list-view layer in pb.codec.
-- ---------------------------------------------------------------------------

for _, mode in ipairs({'full', 'runtime'}) do
    local p2 = require(mode .. '.proto2_basic.proto2_basic_pb')

    -- Required: encode emits a clear error on missing, but the happy path
    -- where the value is set should compile and stay on trace.
    local card = {r = 7}
    local card_bytes = p2.Cardinality_encode(card)
    check(mode .. '/Cardinality_encode (required)',
        function() p2.Cardinality_encode(card) end)
    check(mode .. '/Cardinality_decode (required)',
        function() p2.Cardinality_decode(card_bytes) end)

    -- Groups: SGROUP/EGROUP framed body, no length prefix. Singular path.
    local with_group = {singlegroup = {a = 7, s = 'ok'}}
    local with_group_bytes = p2.WithGroup_encode(with_group)
    check(mode .. '/WithGroup_encode (group)',
        function() p2.WithGroup_encode(with_group) end)
    check(mode .. '/WithGroup_decode (group)',
        function() p2.WithGroup_decode(with_group_bytes) end)

    -- Repeated groups: same shape, exercises the per-element bracket loop.
    local with_rep = {repgroup = {{n = 1}, {n = 2}, {n = 3}, {n = 4}}}
    local with_rep_bytes = p2.WithGroup_encode(with_rep)
    check(mode .. '/WithGroup_encode (repeated group)',
        function() p2.WithGroup_encode(with_rep) end)
    check(mode .. '/WithGroup_decode (repeated group)',
        function() p2.WithGroup_decode(with_rep_bytes) end)
end

-- Extensions: register a small proto2 fixture inline so the trace gate
-- doesn't depend on the bulky conformance schema. Exercises both encode
-- (walks _extensions and routes through encode_field) and decode (routes
-- an unknown tag through extensions_by_id → decode_extension).
do
    local p2tests = require('full.protobuf_test_messages.proto2.test_messages_proto2_pb')
    local Foo     = p2tests.TestAllTypesProto2_descriptor
    local msg = {
        _extensions = {
            ['protobuf_test_messages.proto2.extension_int32']  = 42,
            ['protobuf_test_messages.proto2.extension_string'] = 'hi',
        },
    }
    -- Encode-prime via the descriptor-driven path; tracing the inline
    -- emitted code wouldn't cover the extension loop (it's in codec.lua).
    local pb = require('pb')
    local ext_bytes = pb.encode(Foo, msg)
    check('full/extension_encode',
        function() pb.encode(Foo, msg) end)
    check('full/extension_decode',
        function() pb.decode(Foo, ext_bytes) end)
end

-- Pin the known map limitation: pairs() over a hash compiles to bytecode
-- ISNEXT, which Tarantool LuaJIT 2.1 can't trace. If this stops triggering,
-- upstream lifted the restriction and our scope claim can broaden.

M cmd/protoc-gen-tarantool/internal/gen/inline.go => cmd/protoc-gen-tarantool/internal/gen/inline.go +39 -3
@@ 53,6 53,31 @@ func emitInlineEncode(w *writer, name string, m *protogen.Message, file *protoge
	for _, f := range m.Fields {
		emitInlineEncodeField(w, f, file, selfPath, imports, prefix)
	}
	// Proto2 extensions: delegate to pb.codec.encode_field once per set
	// entry. We don't inline an extension-specific writer here because
	// extensions are runtime-mutable — a user can register more after the
	// module loads — so the canonical iteration source is the descriptor's
	// extensions_list. The walk is JIT-stable (array, not pairs over a
	// hash). Skip entirely when extensions_list is nil (most proto3 messages
	// and proto2 messages without `extensions` ranges) so the hot path
	// pays nothing.
	w.line("    local _exts = t._extensions")
	w.line("    if _exts ~= nil then")
	w.line("        local _elist = M.%s_descriptor.extensions_list", name)
	w.line("        if _elist ~= nil then")
	w.line("            for _i = 1, #_elist do")
	w.line("                local _ext = _elist[_i]")
	w.line("                local _ev = _exts[_ext.full_name]")
	w.line("                if _ev ~= nil then")
	w.line("                    pb.codec.encode_field(_ext, _ev, out, true)")
	w.line("                end")
	w.line("            end")
	// encode_field uses out[#out + 1] = … and doesn't know about our
	// local `n` cursor — resync so the unknown-fields append below lands
	// after the extension bytes, not on top of them.
	w.line("            n = #out")
	w.line("        end")
	w.line("    end")
	// Preserve unknown fields captured at decode time.
	w.line("    local _uf = t._unknown_fields")
	w.line("    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end")


@@ 342,9 367,20 @@ func emitInlineDecode(w *writer, name string, m *protogen.Message, file *protoge
		w.line("        if true then")
	}
	w.line("        else")
	w.line("            pos = wire.skip_field(buf, pos, wt, id)")
	w.line("            if _uf == nil then _uf = {} end")
	w.line("            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)")
	// Proto2 extensions: an unknown tag may belong to a registered
	// extension on this descriptor. Route through pb.codec's
	// decode_extension which mirrors the in-line dispatch on field
	// kind (scalar/enum/message/group, singular/repeated) and stores
	// into result._extensions[full_name].
	w.line("            local _ebid = M.%s_descriptor.extensions_by_id", name)
	w.line("            local _ext = _ebid and _ebid[id] or nil")
	w.line("            if _ext ~= nil then")
	w.line("                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)")
	w.line("            else")
	w.line("                pos = wire.skip_field(buf, pos, wt, id)")
	w.line("                if _uf == nil then _uf = {} end")
	w.line("                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)")
	w.line("            end")
	w.line("        end")
	w.line("    end")
	w.line("    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end")

M examples/expected/full/conformance/conformance_pb.lua => examples/expected/full/conformance/conformance_pb.lua +115 -15
@@ 225,6 225,20 @@ function M.TestStatus_encode(t)
        n = n + 1; out[n] = wire.encode_varint(#v)
        n = n + 1; out[n] = v
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.TestStatus_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 256,9 270,15 @@ function M.TestStatus_decode(buf)
            val, pos = wire.decode_string(buf, pos)
            result.matched_name = val
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.TestStatus_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 296,6 316,20 @@ function M.FailureSet_encode(t)
            n = n + 1; out[n] = _b
        end
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.FailureSet_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 321,9 355,15 @@ function M.FailureSet_decode(buf)
            payload, pos = wire.decode_len(buf, pos)
            list[#list + 1] = M.TestStatus_decode(payload)
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.FailureSet_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 430,6 470,20 @@ function M.ConformanceRequest_encode(t)
        n = n + 1; out[n] = "\x48"
        n = n + 1; out[n] = wire.encode_bool(v)
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.ConformanceRequest_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 502,9 556,15 @@ function M.ConformanceRequest_decode(buf)
            val, pos = wire.decode_bool(buf, pos)
            result.print_unknown_fields = val
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.ConformanceRequest_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 604,6 664,20 @@ function M.ConformanceResponse_encode(t)
        n = n + 1; out[n] = wire.encode_varint(#v)
        n = n + 1; out[n] = v
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.ConformanceResponse_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 731,9 805,15 @@ function M.ConformanceResponse_decode(buf)
            result.skipped = nil
            result.jspb_payload = nil
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.ConformanceResponse_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 766,6 846,20 @@ function M.JspbEncodingConfig_encode(t)
        n = n + 1; out[n] = "\x08"
        n = n + 1; out[n] = wire.encode_bool(v)
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.JspbEncodingConfig_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 789,9 883,15 @@ function M.JspbEncodingConfig_decode(buf)
            val, pos = wire.decode_bool(buf, pos)
            result.use_jspb_array_any_format = val
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.JspbEncodingConfig_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end

M examples/expected/full/hello/hello_pb.lua => examples/expected/full/hello/hello_pb.lua +138 -18
@@ 246,6 246,20 @@ function M.Result_encode(t)
        n = n + 1; out[n] = wire.encode_varint(#_b)
        n = n + 1; out[n] = _b
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.Result_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 292,9 306,15 @@ function M.Result_decode(buf)
            result.text = nil
            result.code = nil
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.Result_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 328,6 348,20 @@ function M.HelloRequest_encode(t)
        n = n + 1; out[n] = wire.encode_varint(#v)
        n = n + 1; out[n] = v
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.HelloRequest_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 351,9 385,15 @@ function M.HelloRequest_decode(buf)
            val, pos = wire.decode_string(buf, pos)
            result.name = val
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.HelloRequest_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 387,6 427,20 @@ function M.HelloReply_encode(t)
        n = n + 1; out[n] = wire.encode_varint(#v)
        n = n + 1; out[n] = v
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.HelloReply_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 410,9 464,15 @@ function M.HelloReply_decode(buf)
            val, pos = wire.decode_string(buf, pos)
            result.greeting = val
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.HelloReply_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 534,6 594,20 @@ function M.Event_encode(t)
        n = n + 1; out[n] = wire.encode_varint(#_b)
        n = n + 1; out[n] = _b
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.Event_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 601,9 675,15 @@ function M.Event_decode(buf)
            payload, pos = wire.decode_len(buf, pos)
            result.update_mask = pb.wkt.FieldMask_decode(payload)
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.Event_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 657,6 737,20 @@ function M.Address_encode(t)
        n = n + 1; out[n] = wire.encode_varint(#v)
        n = n + 1; out[n] = v
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.Address_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 692,9 786,15 @@ function M.Address_decode(buf)
            val, pos = wire.decode_string(buf, pos)
            result.apartment = val
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.Address_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 873,6 973,20 @@ function M.Person_encode(t)
            n = n + 1; out[n] = wire.encode_len(table.concat(entry))
        end
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.Person_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 1017,9 1131,15 @@ function M.Person_decode(buf)
            end
            map[_key] = _val
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.Person_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end

M examples/expected/full/proto2_basic/proto2_basic_pb.lua => examples/expected/full/proto2_basic/proto2_basic_pb.lua +624 -21
@@ 27,6 27,9 @@ M.Nested_Inner_descriptor = {name = "proto2_basic.Nested.Inner"}
M.WithGroup_descriptor = {name = "proto2_basic.WithGroup"}
M.WithGroup_SingleGroup_descriptor = {name = "proto2_basic.WithGroup.SingleGroup"}
M.WithGroup_RepGroup_descriptor = {name = "proto2_basic.WithGroup.RepGroup"}
M.BenchPayload_descriptor = {name = "proto2_basic.BenchPayload"}
M.BenchPayload_Stats_descriptor = {name = "proto2_basic.BenchPayload.Stats"}
M.BenchPayload_Inner_descriptor = {name = "proto2_basic.BenchPayload.Inner"}

-- Message: proto2_basic.Defaults
M.Defaults_descriptor.fields = {


@@ 121,6 124,49 @@ M.WithGroup_RepGroup_fields = pb.field_names({
    n = "n",
})

-- Message: proto2_basic.BenchPayload
M.BenchPayload_descriptor.fields = {
    {name="id", id=1, kind='scalar', proto_type="int32", required=true},
    {name="name", id=2, kind='scalar', proto_type="string", optional=true, default_value="anonymous"},
    {name="retries", id=3, kind='scalar', proto_type="int32", optional=true, default_value=3},
    {name="lucky_numbers", id=4, kind='scalar', proto_type="int32", repeated=true, packed=true, options={packed = true}},
    {name="tags", id=5, kind='scalar', proto_type="string", repeated=true},
    {name="inner", id=6, kind='message', message=M.BenchPayload_Inner_descriptor, optional=true},
    {name="stats", id=7, kind='group', message=M.BenchPayload_Stats_descriptor, optional=true},
}
pb.finalize_message(M.BenchPayload_descriptor)
M.BenchPayload_fields = pb.field_names({
    id = "id",
    name = "name",
    retries = "retries",
    lucky_numbers = "lucky_numbers",
    tags = "tags",
    inner = "inner",
    stats = "stats",
})

-- Message: proto2_basic.BenchPayload.Stats
M.BenchPayload_Stats_descriptor.fields = {
    {name="latency_ns", id=8, kind='scalar', proto_type="int32", optional=true},
    {name="attempts", id=9, kind='scalar', proto_type="int32", optional=true},
}
pb.finalize_message(M.BenchPayload_Stats_descriptor)
M.BenchPayload_Stats_fields = pb.field_names({
    latency_ns = "latency_ns",
    attempts = "attempts",
})

-- Message: proto2_basic.BenchPayload.Inner
M.BenchPayload_Inner_descriptor.fields = {
    {name="key", id=1, kind='scalar', proto_type="string", required=true},
    {name="weight", id=2, kind='scalar', proto_type="int32", optional=true},
}
pb.finalize_message(M.BenchPayload_Inner_descriptor)
M.BenchPayload_Inner_fields = pb.field_names({
    key = "key",
    weight = "weight",
})

-- EmmyLua / lua-language-server type annotations.
-- These are comments — no runtime effect. They give editors
-- autocomplete and type-checking for the generated wrappers.


@@ 164,6 210,26 @@ M.WithGroup_RepGroup_fields = pb.field_names({
---@class proto2_basic.WithGroup.RepGroup
---@field n? integer

--- Bench fixture exercising the proto2-only shapes: required scalar,
--- optional repeated, custom default, a `group` field, and a nested
--- message. Used by bench/bench.lua against both modes.
---@class proto2_basic.BenchPayload
---@field id integer
---@field name? string
---@field retries? integer
---@field lucky_numbers integer[]
---@field tags string[]
---@field inner? proto2_basic.BenchPayload.Inner
---@field stats? proto2_basic.BenchPayload.Stats

---@class proto2_basic.BenchPayload.Stats
---@field latency_ns? integer
---@field attempts? integer

---@class proto2_basic.BenchPayload.Inner
---@field key string
---@field weight? integer

---@param t? proto2_basic.Defaults
---@return proto2_basic.Defaults
function M.Defaults_new(t) return t or {} end


@@ 237,6 303,20 @@ function M.Defaults_encode(t)
        n = n + 1; out[n] = "\x48"
        n = n + 1; out[n] = wire.encode_int32(nv)
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.Defaults_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 292,9 372,15 @@ function M.Defaults_decode(buf)
            u, pos = wire.decode_varint(buf, pos)
            result.color = wire.varint_to_int32(u)
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.Defaults_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 409,6 495,20 @@ function M.Cardinality_encode(t)
            n = n + 1; out[n] = wire.encode_int32(v[_i])
        end
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.Cardinality_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 487,9 587,15 @@ function M.Cardinality_decode(buf)
                list[#list + 1] = val
            end
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.Cardinality_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 538,6 644,20 @@ function M.Nested_encode(t)
        n = n + 1; out[n] = wire.encode_varint(#_b)
        n = n + 1; out[n] = _b
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.Nested_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 575,9 695,15 @@ function M.Nested_decode(buf)
                pb.codec.merge_message(M.Nested_Inner_descriptor, prev, M.Nested_Inner_decode(payload))
            end
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.Nested_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 616,6 742,20 @@ function M.Nested_Inner_encode(t)
    end
    n = n + 1; out[n] = "\x08"
    n = n + 1; out[n] = wire.encode_int32(v)
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.Nested_Inner_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 639,9 779,15 @@ function M.Nested_Inner_decode(buf)
            val, pos = wire.decode_int32(buf, pos)
            result.x = val
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.Nested_Inner_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 686,6 832,20 @@ function M.WithGroup_encode(t)
            n = n + 1; out[n] = _etag
        end
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.WithGroup_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 720,9 880,15 @@ function M.WithGroup_decode(buf)
            payload, pos = pb.codec.decode_group(M.WithGroup_RepGroup_descriptor, buf, pos, 4)
            list[#list + 1] = payload
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.WithGroup_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 767,6 933,20 @@ function M.WithGroup_SingleGroup_encode(t)
        n = n + 1; out[n] = wire.encode_varint(#v)
        n = n + 1; out[n] = v
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.WithGroup_SingleGroup_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 794,9 974,15 @@ function M.WithGroup_SingleGroup_decode(buf)
            val, pos = wire.decode_string(buf, pos)
            result.s = val
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.WithGroup_SingleGroup_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 839,6 1025,20 @@ function M.WithGroup_RepGroup_encode(t)
        n = n + 1; out[n] = "\x28"
        n = n + 1; out[n] = wire.encode_int32(v)
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.WithGroup_RepGroup_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 862,9 1062,15 @@ function M.WithGroup_RepGroup_decode(buf)
            val, pos = wire.decode_int32(buf, pos)
            result.n = val
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.WithGroup_RepGroup_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 884,4 1090,401 @@ function M.WithGroup_RepGroup_has_n(t) return t.n ~= nil end
---@param t proto2_basic.WithGroup.RepGroup
function M.WithGroup_RepGroup_clear_n(t) t.n = nil end

---@param t? proto2_basic.BenchPayload
---@return proto2_basic.BenchPayload
function M.BenchPayload_new(t) return t or {} end

---@param t proto2_basic.BenchPayload
---@return string
function M.BenchPayload_encode(t)
    if type(t) ~= 'table' then
        error("expected table for proto2_basic.BenchPayload, got " .. type(t), 0)
    end
    local out, n = {}, 0
    local v
    -- field 1: id
    v = t.id
    if v == nil then
        error("required field missing on encode: proto2_basic.BenchPayload.id", 0)
    end
    n = n + 1; out[n] = "\x08"
    n = n + 1; out[n] = wire.encode_int32(v)
    -- field 2: name
    v = t.name
    if v ~= nil then
        n = n + 1; out[n] = "\x12"
        n = n + 1; out[n] = wire.encode_varint(#v)
        n = n + 1; out[n] = v
    end
    -- field 3: retries
    v = t.retries
    if v ~= nil then
        n = n + 1; out[n] = "\x18"
        n = n + 1; out[n] = wire.encode_int32(v)
    end
    -- field 4: lucky_numbers
    v = t.lucky_numbers
    if v ~= nil and #v > 0 then
        local parts, m = {}, 0
        for _i = 1, #v do
            m = m + 1; parts[m] = wire.encode_int32(v[_i])
        end
        local _b = table.concat(parts)
        n = n + 1; out[n] = "\x22"
        n = n + 1; out[n] = wire.encode_varint(#_b)
        n = n + 1; out[n] = _b
    end
    -- field 5: tags
    v = t.tags
    if v ~= nil and #v > 0 then
        local _tag = "\x2a"
        for _i = 1, #v do
            local _b = v[_i]
            n = n + 1; out[n] = _tag
            n = n + 1; out[n] = wire.encode_varint(#_b)
            n = n + 1; out[n] = _b
        end
    end
    -- field 6: inner
    v = t.inner
    if v ~= nil or type(v) == 'cdata' then
        local _b = M.BenchPayload_Inner_encode(v)
        n = n + 1; out[n] = "\x32"
        n = n + 1; out[n] = wire.encode_varint(#_b)
        n = n + 1; out[n] = _b
    end
    -- field 7: stats
    v = t.stats
    if v ~= nil or type(v) == 'cdata' then
        n = n + 1; out[n] = "\x3b"
        n = n + 1; out[n] = M.BenchPayload_Stats_encode(v)
        n = n + 1; out[n] = "\x3c"
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.BenchPayload_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)
end

---@param b string
---@return proto2_basic.BenchPayload
function M.BenchPayload_decode(buf)
    if type(buf) ~= 'string' then
        error("expected string for proto2_basic.BenchPayload decode, got " .. type(buf), 0)
    end
    local result = {}
    local pos, len = 1, #buf
    local _uf
    while pos <= len do
        local _tag_start = pos
        local id, wt
        id, wt, pos = wire.decode_tag(buf, pos)
        if id == 1 then
            local val
            val, pos = wire.decode_int32(buf, pos)
            result.id = val
        elseif id == 2 then
            local val
            val, pos = wire.decode_string(buf, pos)
            result.name = val
        elseif id == 3 then
            local val
            val, pos = wire.decode_int32(buf, pos)
            result.retries = val
        elseif id == 4 then
            local list = result.lucky_numbers
            if list == nil then list = {}; result.lucky_numbers = list end
            if wt == 2 then
                local payload
                payload, pos = wire.decode_len(buf, pos)
                local p2, lim = 1, #payload
                while p2 <= lim do
                    local val
                    val, p2 = wire.decode_int32(payload, p2)
                    list[#list + 1] = val
                end
            else
                local val
                val, pos = wire.decode_int32(buf, pos)
                list[#list + 1] = val
            end
        elseif id == 5 then
            local list = result.tags
            if list == nil then list = {}; result.tags = list end
            local val
            val, pos = wire.decode_string(buf, pos)
            list[#list + 1] = val
        elseif id == 6 then
            local payload
            payload, pos = wire.decode_len(buf, pos)
            local prev = result.inner
            if prev == nil then
                result.inner = M.BenchPayload_Inner_decode(payload)
            else
                pb.codec.merge_message(M.BenchPayload_Inner_descriptor, prev, M.BenchPayload_Inner_decode(payload))
            end
        elseif id == 7 then
            local payload
            payload, pos = pb.codec.decode_group(M.BenchPayload_Stats_descriptor, buf, pos, 7)
            local prev = result.stats
            if prev == nil then
                result.stats = payload
            else
                pb.codec.merge_message(M.BenchPayload_Stats_descriptor, prev, payload)
            end
        else
            local _ebid = M.BenchPayload_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end
    return result
end

---@param b string
---@return pb.MessageView
function M.BenchPayload_decode_lazy(b) return pb.decode_lazy(M.BenchPayload_descriptor, b) end
---@param t proto2_basic.BenchPayload
---@param opts? {single_line: boolean?, indent: string?}
---@return string
function M.BenchPayload_text(t, opts) return pb.text.encode(M.BenchPayload_descriptor, t, opts) end
---@param t proto2_basic.BenchPayload
---@return boolean
function M.BenchPayload_has_name(t) return t.name ~= nil end
---@param t proto2_basic.BenchPayload
function M.BenchPayload_clear_name(t) t.name = nil end
---@param t proto2_basic.BenchPayload
---@return boolean
function M.BenchPayload_has_retries(t) return t.retries ~= nil end
---@param t proto2_basic.BenchPayload
function M.BenchPayload_clear_retries(t) t.retries = nil end
---@param t proto2_basic.BenchPayload
---@return boolean
function M.BenchPayload_has_inner(t) return t.inner ~= nil end
---@param t proto2_basic.BenchPayload
function M.BenchPayload_clear_inner(t) t.inner = nil end
---@param t proto2_basic.BenchPayload
---@return boolean
function M.BenchPayload_has_stats(t) return t.stats ~= nil end
---@param t proto2_basic.BenchPayload
function M.BenchPayload_clear_stats(t) t.stats = nil end

---@param t? proto2_basic.BenchPayload.Stats
---@return proto2_basic.BenchPayload.Stats
function M.BenchPayload_Stats_new(t) return t or {} end

---@param t proto2_basic.BenchPayload.Stats
---@return string
function M.BenchPayload_Stats_encode(t)
    if type(t) ~= 'table' then
        error("expected table for proto2_basic.BenchPayload.Stats, got " .. type(t), 0)
    end
    local out, n = {}, 0
    local v
    -- field 8: latency_ns
    v = t.latency_ns
    if v ~= nil then
        n = n + 1; out[n] = "\x40"
        n = n + 1; out[n] = wire.encode_int32(v)
    end
    -- field 9: attempts
    v = t.attempts
    if v ~= nil then
        n = n + 1; out[n] = "\x48"
        n = n + 1; out[n] = wire.encode_int32(v)
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.BenchPayload_Stats_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)
end

---@param b string
---@return proto2_basic.BenchPayload.Stats
function M.BenchPayload_Stats_decode(buf)
    if type(buf) ~= 'string' then
        error("expected string for proto2_basic.BenchPayload.Stats decode, got " .. type(buf), 0)
    end
    local result = {}
    local pos, len = 1, #buf
    local _uf
    while pos <= len do
        local _tag_start = pos
        local id, wt
        id, wt, pos = wire.decode_tag(buf, pos)
        if id == 8 then
            local val
            val, pos = wire.decode_int32(buf, pos)
            result.latency_ns = val
        elseif id == 9 then
            local val
            val, pos = wire.decode_int32(buf, pos)
            result.attempts = val
        else
            local _ebid = M.BenchPayload_Stats_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end
    return result
end

---@param b string
---@return pb.MessageView
function M.BenchPayload_Stats_decode_lazy(b) return pb.decode_lazy(M.BenchPayload_Stats_descriptor, b) end
---@param t proto2_basic.BenchPayload.Stats
---@param opts? {single_line: boolean?, indent: string?}
---@return string
function M.BenchPayload_Stats_text(t, opts) return pb.text.encode(M.BenchPayload_Stats_descriptor, t, opts) end
---@param t proto2_basic.BenchPayload.Stats
---@return boolean
function M.BenchPayload_Stats_has_latency_ns(t) return t.latency_ns ~= nil end
---@param t proto2_basic.BenchPayload.Stats
function M.BenchPayload_Stats_clear_latency_ns(t) t.latency_ns = nil end
---@param t proto2_basic.BenchPayload.Stats
---@return boolean
function M.BenchPayload_Stats_has_attempts(t) return t.attempts ~= nil end
---@param t proto2_basic.BenchPayload.Stats
function M.BenchPayload_Stats_clear_attempts(t) t.attempts = nil end

---@param t? proto2_basic.BenchPayload.Inner
---@return proto2_basic.BenchPayload.Inner
function M.BenchPayload_Inner_new(t) return t or {} end

---@param t proto2_basic.BenchPayload.Inner
---@return string
function M.BenchPayload_Inner_encode(t)
    if type(t) ~= 'table' then
        error("expected table for proto2_basic.BenchPayload.Inner, got " .. type(t), 0)
    end
    local out, n = {}, 0
    local v
    -- field 1: key
    v = t.key
    if v == nil then
        error("required field missing on encode: proto2_basic.BenchPayload.Inner.key", 0)
    end
    n = n + 1; out[n] = "\x0a"
    n = n + 1; out[n] = wire.encode_varint(#v)
    n = n + 1; out[n] = v
    -- field 2: weight
    v = t.weight
    if v ~= nil then
        n = n + 1; out[n] = "\x10"
        n = n + 1; out[n] = wire.encode_int32(v)
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.BenchPayload_Inner_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)
end

---@param b string
---@return proto2_basic.BenchPayload.Inner
function M.BenchPayload_Inner_decode(buf)
    if type(buf) ~= 'string' then
        error("expected string for proto2_basic.BenchPayload.Inner decode, got " .. type(buf), 0)
    end
    local result = {}
    local pos, len = 1, #buf
    local _uf
    while pos <= len do
        local _tag_start = pos
        local id, wt
        id, wt, pos = wire.decode_tag(buf, pos)
        if id == 1 then
            local val
            val, pos = wire.decode_string(buf, pos)
            result.key = val
        elseif id == 2 then
            local val
            val, pos = wire.decode_int32(buf, pos)
            result.weight = val
        else
            local _ebid = M.BenchPayload_Inner_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end
    return result
end

---@param b string
---@return pb.MessageView
function M.BenchPayload_Inner_decode_lazy(b) return pb.decode_lazy(M.BenchPayload_Inner_descriptor, b) end
---@param t proto2_basic.BenchPayload.Inner
---@param opts? {single_line: boolean?, indent: string?}
---@return string
function M.BenchPayload_Inner_text(t, opts) return pb.text.encode(M.BenchPayload_Inner_descriptor, t, opts) end
---@param t proto2_basic.BenchPayload.Inner
---@return boolean
function M.BenchPayload_Inner_has_weight(t) return t.weight ~= nil end
---@param t proto2_basic.BenchPayload.Inner
function M.BenchPayload_Inner_clear_weight(t) t.weight = nil end

-- Extension: proto2_basic.ext_count extends proto2_basic.BenchPayload (tag 100)
pb.register_extension(M.BenchPayload_descriptor, {name="ext_count", full_name="proto2_basic.ext_count", id=100, kind='scalar', proto_type="int32", optional=true})
-- Extension: proto2_basic.ext_label extends proto2_basic.BenchPayload (tag 101)
pb.register_extension(M.BenchPayload_descriptor, {name="ext_label", full_name="proto2_basic.ext_label", id=101, kind='scalar', proto_type="string", optional=true})

return M

M examples/expected/full/protobuf_test_messages/proto2/test_messages_proto2_pb.lua => examples/expected/full/protobuf_test_messages/proto2/test_messages_proto2_pb.lua +483 -63
@@ 2259,6 2259,20 @@ function M.TestAllTypesProto2_encode(t)
        n = n + 1; out[n] = "\x90\x1a"
        n = n + 1; out[n] = wire.encode_int32(v)
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.TestAllTypesProto2_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 3835,9 3849,15 @@ function M.TestAllTypesProto2_decode(buf)
            val, pos = wire.decode_int32(buf, pos)
            result.Field_name18__ = val
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.TestAllTypesProto2_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 4163,6 4183,20 @@ function M.TestAllTypesProto2_NestedMessage_encode(t)
        n = n + 1; out[n] = wire.encode_varint(#_b)
        n = n + 1; out[n] = _b
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.TestAllTypesProto2_NestedMessage_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 4195,9 4229,15 @@ function M.TestAllTypesProto2_NestedMessage_decode(buf)
                pb.codec.merge_message(M.TestAllTypesProto2_descriptor, prev, M.TestAllTypesProto2_decode(payload))
            end
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.TestAllTypesProto2_NestedMessage_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 4246,6 4286,20 @@ function M.TestAllTypesProto2_Data_encode(t)
        n = n + 1; out[n] = "\xd8\x0c"
        n = n + 1; out[n] = wire.encode_uint32(v)
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.TestAllTypesProto2_Data_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 4273,9 4327,15 @@ function M.TestAllTypesProto2_Data_decode(buf)
            val, pos = wire.decode_uint32(buf, pos)
            result.group_uint32 = val
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.TestAllTypesProto2_Data_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 4324,6 4384,20 @@ function M.TestAllTypesProto2_MultiWordGroupField_encode(t)
        n = n + 1; out[n] = "\xf0\x0c"
        n = n + 1; out[n] = wire.encode_uint32(v)
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.TestAllTypesProto2_MultiWordGroupField_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 4351,9 4425,15 @@ function M.TestAllTypesProto2_MultiWordGroupField_decode(buf)
            val, pos = wire.decode_uint32(buf, pos)
            result.group_uint32 = val
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.TestAllTypesProto2_MultiWordGroupField_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 4396,6 4476,20 @@ function M.ForeignMessageProto2_encode(t)
        n = n + 1; out[n] = "\x08"
        n = n + 1; out[n] = wire.encode_int32(v)
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.ForeignMessageProto2_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 4419,9 4513,15 @@ function M.ForeignMessageProto2_decode(buf)
            val, pos = wire.decode_int32(buf, pos)
            result.c = val
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.ForeignMessageProto2_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 4465,6 4565,20 @@ function M.GroupField_encode(t)
        n = n + 1; out[n] = "\xd8\x07"
        n = n + 1; out[n] = wire.encode_uint32(v)
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.GroupField_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 4492,9 4606,15 @@ function M.GroupField_decode(buf)
            val, pos = wire.decode_uint32(buf, pos)
            result.group_uint32 = val
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.GroupField_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 4574,6 4694,20 @@ function M.UnknownToTestAllTypes_encode(t)
            n = n + 1; out[n] = wire.encode_int32(v[_i])
        end
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.UnknownToTestAllTypes_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 4640,9 4774,15 @@ function M.UnknownToTestAllTypes_decode(buf)
                list[#list + 1] = val
            end
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.UnknownToTestAllTypes_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 4700,6 4840,20 @@ function M.UnknownToTestAllTypes_OptionalGroup_encode(t)
        n = n + 1; out[n] = "\x08"
        n = n + 1; out[n] = wire.encode_int32(v)
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.UnknownToTestAllTypes_OptionalGroup_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 4723,9 4877,15 @@ function M.UnknownToTestAllTypes_OptionalGroup_decode(buf)
            val, pos = wire.decode_int32(buf, pos)
            result.a = val
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.UnknownToTestAllTypes_OptionalGroup_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 4757,6 4917,20 @@ function M.NullHypothesisProto2_encode(t)
    end
    local out, n = {}, 0
    local v
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.NullHypothesisProto2_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 4777,9 4951,15 @@ function M.NullHypothesisProto2_decode(buf)
        id, wt, pos = wire.decode_tag(buf, pos)
        if true then
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.NullHypothesisProto2_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 4806,6 4986,20 @@ function M.EnumOnlyProto2_encode(t)
    end
    local out, n = {}, 0
    local v
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.EnumOnlyProto2_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 4826,9 5020,15 @@ function M.EnumOnlyProto2_decode(buf)
        id, wt, pos = wire.decode_tag(buf, pos)
        if true then
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.EnumOnlyProto2_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 4862,6 5062,20 @@ function M.OneStringProto2_encode(t)
        n = n + 1; out[n] = wire.encode_varint(#v)
        n = n + 1; out[n] = v
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.OneStringProto2_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 4885,9 5099,15 @@ function M.OneStringProto2_decode(buf)
            val, pos = wire.decode_string(buf, pos)
            result.data = val
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.OneStringProto2_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 4943,6 5163,20 @@ function M.ProtoWithKeywords_encode(t)
            n = n + 1; out[n] = _b
        end
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.ProtoWithKeywords_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 4976,9 5210,15 @@ function M.ProtoWithKeywords_decode(buf)
            val, pos = wire.decode_string(buf, pos)
            list[#list + 1] = val
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.ProtoWithKeywords_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 5317,6 5557,20 @@ function M.TestAllRequiredTypesProto2_encode(t)
    n = n + 1; out[n] = "\xfa\x0f"
    n = n + 1; out[n] = wire.encode_varint(#v)
    n = n + 1; out[n] = v
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.TestAllRequiredTypesProto2_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 5517,9 5771,15 @@ function M.TestAllRequiredTypesProto2_decode(buf)
            val, pos = wire.decode_bytes(buf, pos)
            result.default_bytes = val
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.TestAllRequiredTypesProto2_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 5575,6 5835,20 @@ function M.TestAllRequiredTypesProto2_NestedMessage_encode(t)
        n = n + 1; out[n] = wire.encode_varint(#_b)
        n = n + 1; out[n] = _b
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.TestAllRequiredTypesProto2_NestedMessage_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 5616,9 5890,15 @@ function M.TestAllRequiredTypesProto2_NestedMessage_decode(buf)
                pb.codec.merge_message(M.TestAllRequiredTypesProto2_descriptor, prev, M.TestAllRequiredTypesProto2_decode(payload))
            end
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.TestAllRequiredTypesProto2_NestedMessage_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 5664,6 5944,20 @@ function M.TestAllRequiredTypesProto2_Data_encode(t)
    end
    n = n + 1; out[n] = "\xd8\x0c"
    n = n + 1; out[n] = wire.encode_uint32(v)
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.TestAllRequiredTypesProto2_Data_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 5691,9 5985,15 @@ function M.TestAllRequiredTypesProto2_Data_decode(buf)
            val, pos = wire.decode_uint32(buf, pos)
            result.group_uint32 = val
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.TestAllRequiredTypesProto2_Data_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 5766,6 6066,20 @@ function M.TestLargeOneof_encode(t)
        n = n + 1; out[n] = wire.encode_varint(#_b)
        n = n + 1; out[n] = _b
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.TestLargeOneof_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 5850,9 6164,15 @@ function M.TestLargeOneof_decode(buf)
            result.a3 = nil
            result.a4 = nil
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.TestLargeOneof_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 5879,6 6199,20 @@ function M.TestLargeOneof_A1_encode(t)
    end
    local out, n = {}, 0
    local v
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.TestLargeOneof_A1_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 5899,9 6233,15 @@ function M.TestLargeOneof_A1_decode(buf)
        id, wt, pos = wire.decode_tag(buf, pos)
        if true then
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.TestLargeOneof_A1_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 5928,6 6268,20 @@ function M.TestLargeOneof_A2_encode(t)
    end
    local out, n = {}, 0
    local v
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.TestLargeOneof_A2_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 5948,9 6302,15 @@ function M.TestLargeOneof_A2_decode(buf)
        id, wt, pos = wire.decode_tag(buf, pos)
        if true then
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.TestLargeOneof_A2_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 5977,6 6337,20 @@ function M.TestLargeOneof_A3_encode(t)
    end
    local out, n = {}, 0
    local v
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.TestLargeOneof_A3_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 5997,9 6371,15 @@ function M.TestLargeOneof_A3_decode(buf)
        id, wt, pos = wire.decode_tag(buf, pos)
        if true then
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.TestLargeOneof_A3_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 6026,6 6406,20 @@ function M.TestLargeOneof_A4_encode(t)
    end
    local out, n = {}, 0
    local v
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.TestLargeOneof_A4_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 6046,9 6440,15 @@ function M.TestLargeOneof_A4_decode(buf)
        id, wt, pos = wire.decode_tag(buf, pos)
        if true then
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.TestLargeOneof_A4_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 6075,6 6475,20 @@ function M.TestLargeOneof_A5_encode(t)
    end
    local out, n = {}, 0
    local v
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.TestLargeOneof_A5_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 6095,9 6509,15 @@ function M.TestLargeOneof_A5_decode(buf)
        id, wt, pos = wire.decode_tag(buf, pos)
        if true then
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.TestLargeOneof_A5_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end

M examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua => examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua +115 -15
@@ 2207,6 2207,20 @@ function M.TestAllTypesProto3_encode(t)
        n = n + 1; out[n] = "\x90\x1a"
        n = n + 1; out[n] = wire.encode_int32(v)
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.TestAllTypesProto3_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 3861,9 3875,15 @@ function M.TestAllTypesProto3_decode(buf)
            val, pos = wire.decode_int32(buf, pos)
            result.Field_name18__ = val
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.TestAllTypesProto3_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 3904,6 3924,20 @@ function M.TestAllTypesProto3_NestedMessage_encode(t)
        n = n + 1; out[n] = wire.encode_varint(#_b)
        n = n + 1; out[n] = _b
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.TestAllTypesProto3_NestedMessage_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 3936,9 3970,15 @@ function M.TestAllTypesProto3_NestedMessage_decode(buf)
                pb.codec.merge_message(M.TestAllTypesProto3_descriptor, prev, M.TestAllTypesProto3_decode(payload))
            end
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.TestAllTypesProto3_NestedMessage_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 3971,6 4011,20 @@ function M.ForeignMessage_encode(t)
        n = n + 1; out[n] = "\x08"
        n = n + 1; out[n] = wire.encode_int32(v)
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.ForeignMessage_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 3994,9 4048,15 @@ function M.ForeignMessage_decode(buf)
            val, pos = wire.decode_int32(buf, pos)
            result.c = val
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.ForeignMessage_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 4023,6 4083,20 @@ function M.NullHypothesisProto3_encode(t)
    end
    local out, n = {}, 0
    local v
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.NullHypothesisProto3_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 4043,9 4117,15 @@ function M.NullHypothesisProto3_decode(buf)
        id, wt, pos = wire.decode_tag(buf, pos)
        if true then
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.NullHypothesisProto3_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end


@@ 4072,6 4152,20 @@ function M.EnumOnlyProto3_encode(t)
    end
    local out, n = {}, 0
    local v
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.EnumOnlyProto3_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 4092,9 4186,15 @@ function M.EnumOnlyProto3_decode(buf)
        id, wt, pos = wire.decode_tag(buf, pos)
        if true then
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.EnumOnlyProto3_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end

M examples/expected/full/quickstart/quickstart_pb.lua => examples/expected/full/quickstart/quickstart_pb.lua +23 -3
@@ 96,6 96,20 @@ function M.User_encode(t)
            n = n + 1; out[n] = _b
        end
    end
    local _exts = t._extensions
    if _exts ~= nil then
        local _elist = M.User_descriptor.extensions_list
        if _elist ~= nil then
            for _i = 1, #_elist do
                local _ext = _elist[_i]
                local _ev = _exts[_ext.full_name]
                if _ev ~= nil then
                    pb.codec.encode_field(_ext, _ev, out, true)
                end
            end
            n = #out
        end
    end
    local _uf = t._unknown_fields
    if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
    return table.concat(out)


@@ 133,9 147,15 @@ function M.User_decode(buf)
            val, pos = wire.decode_string(buf, pos)
            list[#list + 1] = val
        else
            pos = wire.skip_field(buf, pos, wt, id)
            if _uf == nil then _uf = {} end
            _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            local _ebid = M.User_descriptor.extensions_by_id
            local _ext = _ebid and _ebid[id] or nil
            if _ext ~= nil then
                pos = pb.codec.decode_extension(_ext, buf, pos, wt, result)
            else
                pos = wire.skip_field(buf, pos, wt, id)
                if _uf == nil then _uf = {} end
                _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
            end
        end
    end
    if _uf ~= nil then result._unknown_fields = table.concat(_uf) end

M examples/expected/runtime/proto2_basic/proto2_basic_pb.lua => examples/expected/runtime/proto2_basic/proto2_basic_pb.lua +157 -0
@@ 27,6 27,9 @@ M.Nested_Inner_descriptor = {name = "proto2_basic.Nested.Inner"}
M.WithGroup_descriptor = {name = "proto2_basic.WithGroup"}
M.WithGroup_SingleGroup_descriptor = {name = "proto2_basic.WithGroup.SingleGroup"}
M.WithGroup_RepGroup_descriptor = {name = "proto2_basic.WithGroup.RepGroup"}
M.BenchPayload_descriptor = {name = "proto2_basic.BenchPayload"}
M.BenchPayload_Stats_descriptor = {name = "proto2_basic.BenchPayload.Stats"}
M.BenchPayload_Inner_descriptor = {name = "proto2_basic.BenchPayload.Inner"}

-- Message: proto2_basic.Defaults
M.Defaults_descriptor.fields = {


@@ 121,6 124,49 @@ M.WithGroup_RepGroup_fields = pb.field_names({
    n = "n",
})

-- Message: proto2_basic.BenchPayload
M.BenchPayload_descriptor.fields = {
    {name="id", id=1, kind='scalar', proto_type="int32", required=true},
    {name="name", id=2, kind='scalar', proto_type="string", optional=true, default_value="anonymous"},
    {name="retries", id=3, kind='scalar', proto_type="int32", optional=true, default_value=3},
    {name="lucky_numbers", id=4, kind='scalar', proto_type="int32", repeated=true, packed=true, options={packed = true}},
    {name="tags", id=5, kind='scalar', proto_type="string", repeated=true},
    {name="inner", id=6, kind='message', message=M.BenchPayload_Inner_descriptor, optional=true},
    {name="stats", id=7, kind='group', message=M.BenchPayload_Stats_descriptor, optional=true},
}
pb.finalize_message(M.BenchPayload_descriptor)
M.BenchPayload_fields = pb.field_names({
    id = "id",
    name = "name",
    retries = "retries",
    lucky_numbers = "lucky_numbers",
    tags = "tags",
    inner = "inner",
    stats = "stats",
})

-- Message: proto2_basic.BenchPayload.Stats
M.BenchPayload_Stats_descriptor.fields = {
    {name="latency_ns", id=8, kind='scalar', proto_type="int32", optional=true},
    {name="attempts", id=9, kind='scalar', proto_type="int32", optional=true},
}
pb.finalize_message(M.BenchPayload_Stats_descriptor)
M.BenchPayload_Stats_fields = pb.field_names({
    latency_ns = "latency_ns",
    attempts = "attempts",
})

-- Message: proto2_basic.BenchPayload.Inner
M.BenchPayload_Inner_descriptor.fields = {
    {name="key", id=1, kind='scalar', proto_type="string", required=true},
    {name="weight", id=2, kind='scalar', proto_type="int32", optional=true},
}
pb.finalize_message(M.BenchPayload_Inner_descriptor)
M.BenchPayload_Inner_fields = pb.field_names({
    key = "key",
    weight = "weight",
})

-- EmmyLua / lua-language-server type annotations.
-- These are comments — no runtime effect. They give editors
-- autocomplete and type-checking for the generated wrappers.


@@ 164,6 210,26 @@ M.WithGroup_RepGroup_fields = pb.field_names({
---@class proto2_basic.WithGroup.RepGroup
---@field n? integer

--- Bench fixture exercising the proto2-only shapes: required scalar,
--- optional repeated, custom default, a `group` field, and a nested
--- message. Used by bench/bench.lua against both modes.
---@class proto2_basic.BenchPayload
---@field id integer
---@field name? string
---@field retries? integer
---@field lucky_numbers integer[]
---@field tags string[]
---@field inner? proto2_basic.BenchPayload.Inner
---@field stats? proto2_basic.BenchPayload.Stats

---@class proto2_basic.BenchPayload.Stats
---@field latency_ns? integer
---@field attempts? integer

---@class proto2_basic.BenchPayload.Inner
---@field key string
---@field weight? integer

---@param t? proto2_basic.Defaults
---@return proto2_basic.Defaults
function M.Defaults_new(t) return t or {} end


@@ 358,4 424,95 @@ function M.WithGroup_RepGroup_has_n(t) return t.n ~= nil end
---@param t proto2_basic.WithGroup.RepGroup
function M.WithGroup_RepGroup_clear_n(t) t.n = nil end

---@param t? proto2_basic.BenchPayload
---@return proto2_basic.BenchPayload
function M.BenchPayload_new(t) return t or {} end
---@param t proto2_basic.BenchPayload
---@return string
function M.BenchPayload_encode(t) return pb.encode(M.BenchPayload_descriptor, t) end
---@param b string
---@return proto2_basic.BenchPayload
function M.BenchPayload_decode(b) return pb.decode(M.BenchPayload_descriptor, b) end
---@param b string
---@return pb.MessageView
function M.BenchPayload_decode_lazy(b) return pb.decode_lazy(M.BenchPayload_descriptor, b) end
---@param t proto2_basic.BenchPayload
---@param opts? {single_line: boolean?, indent: string?}
---@return string
function M.BenchPayload_text(t, opts) return pb.text.encode(M.BenchPayload_descriptor, t, opts) end
---@param t proto2_basic.BenchPayload
---@return boolean
function M.BenchPayload_has_name(t) return t.name ~= nil end
---@param t proto2_basic.BenchPayload
function M.BenchPayload_clear_name(t) t.name = nil end
---@param t proto2_basic.BenchPayload
---@return boolean
function M.BenchPayload_has_retries(t) return t.retries ~= nil end
---@param t proto2_basic.BenchPayload
function M.BenchPayload_clear_retries(t) t.retries = nil end
---@param t proto2_basic.BenchPayload
---@return boolean
function M.BenchPayload_has_inner(t) return t.inner ~= nil end
---@param t proto2_basic.BenchPayload
function M.BenchPayload_clear_inner(t) t.inner = nil end
---@param t proto2_basic.BenchPayload
---@return boolean
function M.BenchPayload_has_stats(t) return t.stats ~= nil end
---@param t proto2_basic.BenchPayload
function M.BenchPayload_clear_stats(t) t.stats = nil end

---@param t? proto2_basic.BenchPayload.Stats
---@return proto2_basic.BenchPayload.Stats
function M.BenchPayload_Stats_new(t) return t or {} end
---@param t proto2_basic.BenchPayload.Stats
---@return string
function M.BenchPayload_Stats_encode(t) return pb.encode(M.BenchPayload_Stats_descriptor, t) end
---@param b string
---@return proto2_basic.BenchPayload.Stats
function M.BenchPayload_Stats_decode(b) return pb.decode(M.BenchPayload_Stats_descriptor, b) end
---@param b string
---@return pb.MessageView
function M.BenchPayload_Stats_decode_lazy(b) return pb.decode_lazy(M.BenchPayload_Stats_descriptor, b) end
---@param t proto2_basic.BenchPayload.Stats
---@param opts? {single_line: boolean?, indent: string?}
---@return string
function M.BenchPayload_Stats_text(t, opts) return pb.text.encode(M.BenchPayload_Stats_descriptor, t, opts) end
---@param t proto2_basic.BenchPayload.Stats
---@return boolean
function M.BenchPayload_Stats_has_latency_ns(t) return t.latency_ns ~= nil end
---@param t proto2_basic.BenchPayload.Stats
function M.BenchPayload_Stats_clear_latency_ns(t) t.latency_ns = nil end
---@param t proto2_basic.BenchPayload.Stats
---@return boolean
function M.BenchPayload_Stats_has_attempts(t) return t.attempts ~= nil end
---@param t proto2_basic.BenchPayload.Stats
function M.BenchPayload_Stats_clear_attempts(t) t.attempts = nil end

---@param t? proto2_basic.BenchPayload.Inner
---@return proto2_basic.BenchPayload.Inner
function M.BenchPayload_Inner_new(t) return t or {} end
---@param t proto2_basic.BenchPayload.Inner
---@return string
function M.BenchPayload_Inner_encode(t) return pb.encode(M.BenchPayload_Inner_descriptor, t) end
---@param b string
---@return proto2_basic.BenchPayload.Inner
function M.BenchPayload_Inner_decode(b) return pb.decode(M.BenchPayload_Inner_descriptor, b) end
---@param b string
---@return pb.MessageView
function M.BenchPayload_Inner_decode_lazy(b) return pb.decode_lazy(M.BenchPayload_Inner_descriptor, b) end
---@param t proto2_basic.BenchPayload.Inner
---@param opts? {single_line: boolean?, indent: string?}
---@return string
function M.BenchPayload_Inner_text(t, opts) return pb.text.encode(M.BenchPayload_Inner_descriptor, t, opts) end
---@param t proto2_basic.BenchPayload.Inner
---@return boolean
function M.BenchPayload_Inner_has_weight(t) return t.weight ~= nil end
---@param t proto2_basic.BenchPayload.Inner
function M.BenchPayload_Inner_clear_weight(t) t.weight = nil end

-- Extension: proto2_basic.ext_count extends proto2_basic.BenchPayload (tag 100)
pb.register_extension(M.BenchPayload_descriptor, {name="ext_count", full_name="proto2_basic.ext_count", id=100, kind='scalar', proto_type="int32", optional=true})
-- Extension: proto2_basic.ext_label extends proto2_basic.BenchPayload (tag 101)
pb.register_extension(M.BenchPayload_descriptor, {name="ext_label", full_name="proto2_basic.ext_label", id=101, kind='scalar', proto_type="string", optional=true})

return M

M runtime/pb/codec.lua => runtime/pb/codec.lua +21 -9
@@ 909,15 909,21 @@ encode_message = function(desc, data)
        end
    end
    -- Proto2 extensions: data._extensions = { [full_name] = value, ... }.
    -- Walk known extensions in declaration order via extensions_by_full_name
    -- so the wire bytes are stable across runs (pairs() ordering otherwise
    -- depends on hash). Unknown extensions stay in _unknown_fields.
    -- Walk via the parallel array `extensions_list` rather than `pairs()`
    -- over a hash — pairs() compiles to bytecode ISNEXT, which is NYI in
    -- Tarantool's LuaJIT 2.1 fork (same limitation that gates map encode).
    -- pb.register_extension keeps the array in registration order, which
    -- is also the deterministic on-wire order.
    local exts = data._extensions
    if exts ~= nil and desc.extensions_by_full_name ~= nil then
        for full_name, ext in pairs(desc.extensions_by_full_name) do
            local v = exts[full_name]
            if v ~= nil then
                encode_field(ext, v, out, true)
    if exts ~= nil then
        local elist = desc.extensions_list
        if elist ~= nil then
            for i = 1, #elist do
                local ext = elist[i]
                local v = exts[ext.full_name]
                if v ~= nil then
                    encode_field(ext, v, out, true)
                end
            end
        end
    end


@@ 1037,7 1043,13 @@ M.decode_group = decode_group
-- result._extensions[ext.full_name]. Mirrors the in-line decode dispatch on
-- field kind (scalar/enum/message/group, singular/repeated). Returns the
-- new buffer position after the value bytes.
local function decode_extension(ext, buf, pos, wt, result)
-- Exposed for inline-mode generated code: the per-message decoder calls
-- decode_extension when it sees an unknown tag that matches a registered
-- extension on the descriptor.
local decode_extension
function M.decode_extension(...) return decode_extension(...) end

decode_extension = function(ext, buf, pos, wt, result)
    local exts = result._extensions
    if exts == nil then exts = {}; result._extensions = exts end
    local key = ext.full_name

M runtime/pb/init.lua => runtime/pb/init.lua +11 -2
@@ 190,14 190,23 @@ return {
    -- Proto2 extension registration. Generated code emits one call per
    -- `extend Foo { ... }` field, attaching the extension's field shape
    -- to the extendee's descriptor. The codec consults `extensions_by_id`
    -- when decoding an unrecognized tag and walks `extensions_by_full_name`
    -- when encoding `data._extensions`.
    -- when decoding an unrecognized tag, walks `extensions_list` (array)
    -- when encoding `data._extensions`, and uses
    -- `extensions_by_full_name` for text/JSON encoder lookups.
    --
    -- `extensions_list` is the JIT-stable iteration source — pairs() over
    -- a hash compiles to bytecode ISNEXT which is NYI in LuaJIT 2.1, so
    -- the hot encode loop iterates the array instead. The hash tables
    -- stay around for O(1) name/id lookups.
    register_extension = function(extendee_desc, ext)
        if extendee_desc.extensions_by_id == nil then
            extendee_desc.extensions_by_id = {}
            extendee_desc.extensions_by_full_name = {}
            extendee_desc.extensions_list = {}
        end
        extendee_desc.extensions_by_id[ext.id] = ext
        extendee_desc.extensions_by_full_name[ext.full_name] = ext
        local list = extendee_desc.extensions_list
        list[#list + 1] = ext
    end,
}

M runtime/pb/json.lua => runtime/pb/json.lua +11 -7
@@ 940,17 940,21 @@ encode_message = function(desc, t)
        end
    end
    -- Proto2 extensions: surface set entries under their bracketed
    -- fully-qualified name (`[pkg.ext_name]`). Repeated extensions
    -- emit as JSON arrays per the proto2 JSON spec.
    -- fully-qualified name (`[pkg.ext_name]`). Walk the array
    -- (`extensions_list`) rather than the hash — pairs() compiles to
    -- ISNEXT which is NYI on the JIT. Repeated extensions emit as JSON
    -- arrays per the proto2 JSON spec.
    local exts = t._extensions
    if exts ~= nil and desc.extensions_by_full_name ~= nil then
        for full_name, ext in pairs(desc.extensions_by_full_name) do
            local v = exts[full_name]
    local elist = exts ~= nil and desc.extensions_list or nil
    if elist ~= nil then
        for i = 1, #elist do
            local ext = elist[i]
            local v = exts[ext.full_name]
            if v ~= nil then
                local k = '[' .. full_name .. ']'
                local k = '[' .. ext.full_name .. ']'
                if ext.repeated then
                    local arr = setmetatable({}, {__serialize='seq'})
                    for i = 1, #v do arr[i] = encode_field_value(ext, v[i]) end
                    for j = 1, #v do arr[j] = encode_field_value(ext, v[j]) end
                    out[k] = arr
                else
                    out[k] = encode_field_value(ext, v)

M runtime/pb/text.lua => runtime/pb/text.lua +12 -8
@@ 370,19 370,23 @@ emit_message = function(buf, desc, t, depth)
        end
    end
    -- Proto2 extensions: emit each set entry as `[full.name]: value` in
    -- declaration order via the registry. Repeated extensions emit one
    -- entry per element to keep the round-trip lossless.
    -- registration order via the array view (`extensions_list`). Same
    -- ISNEXT-NYI reasoning as the wire encoder — walk the array, not the
    -- hash. Repeated extensions emit one entry per element to keep the
    -- round-trip lossless.
    local exts = t._extensions
    if exts ~= nil and desc.extensions_by_full_name ~= nil then
        for full_name, ext in pairs(desc.extensions_by_full_name) do
            local v = exts[full_name]
    local elist = exts ~= nil and desc.extensions_list or nil
    if elist ~= nil then
        for i = 1, #elist do
            local ext = elist[i]
            local v = exts[ext.full_name]
            if v ~= nil then
                if ext.repeated then
                    for i = 1, #v do
                        emit_extension_entry(buf, ext, full_name, v[i], depth)
                    for j = 1, #v do
                        emit_extension_entry(buf, ext, ext.full_name, v[j], depth)
                    end
                else
                    emit_extension_entry(buf, ext, full_name, v, depth)
                    emit_extension_entry(buf, ext, ext.full_name, v, depth)
                end
            end
        end

M test/proto/proto2_basic.proto => test/proto/proto2_basic.proto +29 -0
@@ 48,3 48,32 @@ message WithGroup {
    optional int32 n = 5;
  }
}

// Bench fixture exercising the proto2-only shapes: required scalar,
// optional repeated, custom default, a `group` field, and a nested
// message. Used by bench/bench.lua against both modes.
message BenchPayload {
  required int32 id            = 1;
  optional string name         = 2 [default = "anonymous"];
  optional int32 retries       = 3 [default = 3];
  repeated int32 lucky_numbers = 4 [packed = true];
  repeated string tags         = 5;
  optional Inner inner         = 6;
  optional group Stats = 7 {
    optional int32 latency_ns = 8;
    optional int32 attempts   = 9;
  }

  message Inner {
    required string key = 1;
    optional int32 weight = 2;
  }

  // Reserve a range for the file-level extensions declared below.
  extensions 100 to 199;
}

extend BenchPayload {
  optional int32  ext_count   = 100;
  optional string ext_label   = 101;
}

M test/proto2_test.lua => test/proto2_test.lua +46 -0
@@ 188,6 188,52 @@ for _, mode in ipairs(MODES) do
        t.assert(s:find('SingleGroup'), 'expected SingleGroup label in: ' .. s)
        t.assert_not(s:find('singlegroup'), 'lowercase field name must not appear')
    end

    -- ----- Proto2 extensions -----

    g.test_extension_registry_uses_array_view = function()
        -- The hot encode loop iterates extensions_list (array) rather than
        -- pairs() over extensions_by_full_name to stay JIT-stable. If a
        -- future refactor drops the array view, encode silently falls off
        -- the JIT — pin both indices here.
        local d = pb.BenchPayload_descriptor
        t.assert(type(d.extensions_list) == 'table',
            'extensions_list array view must exist')
        t.assert(#d.extensions_list >= 2)
        t.assert(d.extensions_by_id[100])
        t.assert(d.extensions_by_full_name['proto2_basic.ext_count'])
    end

    g.test_extension_round_trip = function()
        local msg = {
            id = 7,
            _extensions = {
                ['proto2_basic.ext_count'] = 42,
                ['proto2_basic.ext_label'] = 'tag',
            },
        }
        local dec = pb.BenchPayload_decode(pb.BenchPayload_encode(msg))
        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

    g.test_extension_inline_codegen_walks_them = function()
        -- Regression pin: the inline (full-mode) generated code added an
        -- explicit extensions walk after the field loop. Verify that the
        -- bytes-on-wire match what the runtime codec produces for the
        -- same input.
        local full    = require('full.proto2_basic.proto2_basic_pb')
        local runtime = require('runtime.proto2_basic.proto2_basic_pb')
        local msg = {
            id = 9,
            _extensions = {['proto2_basic.ext_count'] = 17},
        }
        t.assert_equals(
            hex(full.BenchPayload_encode(msg)),
            hex(runtime.BenchPayload_encode(msg)),
            'inline and runtime extension emission must match byte-for-byte')
    end
end

-- Dynamic (source-parsed) proto2: load test/proto/proto2_basic.proto at