~bigbes/tarantool

tarantool-protobuf

ref: ff1ce60f7c0d09476ff860eded449af87071fb66 tarantool-protobuf/examples/expected/full/protobuf_test_messages/proto3 d---------
ff1ce60f — Eugene Blikh 2 months ago
c_runtime: decode_unsafe(plan, buf) entrypoint, skip_utf8 gate on dec_ctx (kyt)

Before this change, M.<Msg>_decode_unsafe stayed on the inline Lua path even
when PB_ENABLE_C=1, because c_runtime.decode validated UTF-8 unconditionally.
Now both safe and unsafe decoders dispatch to the C runtime; the unsafe path
calls c_runtime.decode_unsafe, which sets dec_ctx.skip_utf8 and gates the
is_valid_utf8 call on every PB_KIND_STRING payload across the decode tree.

pb.decode_unsafe in init.lua mirrors pb.decode: tries desc.c_plan first, falls
back to the pure-Lua decode_msg_unsafe. Full-mode codegen emits the same
pb.c_runtime check at the unsafe prologue.

Perf on string-heavy Person (418B, 16 emails + 16 nicknames):
  Lua  unsafe  142 MB/s
  C    safe    364 MB/s
  C    unsafe  455 MB/s   (+25% over C-safe, 3.2x over Lua-unsafe)

Suites: test 766/766, test-c 1057/1057, examples all green.
53840866 — Eugene Blikh 2 months ago
codegen: emit <Msg>_decode_unsafe for trusted-source decoding (6bb)

Full-mode codegen now emits a sister <Msg>_decode_unsafe(buf) alongside
<Msg>_decode that drops the per-string utf8_len check (singular,
repeated, map keys/values, extensions, and the >=128-byte fallback all
route through wire.decode_bytes). Sub-messages recurse into their own
_decode_unsafe so nested strings also bypass; WKTs continue to call the
normal pb.wkt.<Name>_decode (no _unsafe twin, no string-validation hot
path). C runtime dispatch is skipped because it validates today (kyt).

Use this when re-decoding bytes from a trusted producer — your own
encoder over typed RPC, JSON/text round-trips, in-process pipelines —
where the spec-required utf8.len check on every string is duplicate
work. Microbench on a string-heavy 1KB Person (26 emails) shows
~20% throughput vs _decode; conformance suite still passes (3240/3240)
because _decode itself is unchanged.

Runtime mode does not yet expose _decode_unsafe (compiled f._reader
closures capture handler.decode by value, so a runtime swap wouldn't
reach them); tracked in 58u.
dbbfaf35 — Eugene Blikh 2 months ago
codegen: inline 1-byte varint fast path for packed scalar elements (aah)

Per-element wire.encode_<type>(v) calls in packed-repeated fields paid a
full function-call boundary even though encode_varint's small-positive-int
hot path is a single CHARS[n] lookup. Inline the check + lookup at
codegen time at every packed emit site:

  - Repeated packed scalar (mode=full)
  - Repeated packed enum (after string->int resolve)
  - Proto2 extension packed scalar + enum

For varint scalars (int32/int64/uint32/uint64): fast path triggers when
v is a Lua number in [0, 128). For sint32/sint64: 7-bit zigzag range
-64..63 is inlined with bit ops. For bool: always 1 byte via
CHARS[v and 1 or 0] (no fast/slow split). Fixed-width scalars keep the
wire.encode_<type> call shape — already optimal.

Also pre-sizes the parts accumulator with table_new(#v, 0) instead of
{}; same pattern qwt+2sn used decode-side. Eliminates rehash cascade
as elements push.

Tests: 752/752 pass. JIT trace gate: 37/37.

Bench (work.lab.local, median of 3, c_repeated.Holder packed N elems):
  packed_int32:  +90% / +113% / +106%  (N=10/100/1000)
  packed_sint32: +66% / +236% / +156%
  packed_uint32: +73% / +105% / +102%
  packed_bool:   +44% / +51% / +39%
  packed_int64:  +22% / +21% / +23%  (cdata; gain from table_new only)

Headline hello.Person 1KB +3.6%, proto2 BenchPayload mid +10.5%.

See bench/PERF_LOG.md 2026-05-24 aah entry for the full breakdown
including the sint32 +236% mid-size analysis (three function layers
collapsed into one CHARS lookup).
035b2adb — Eugene Blikh 2 months ago
codegen: CHARS[_len] lookup replaces string.char(_len) at length-prefix sites (2ri)

Profile attributed 39% of Person_encode's trace share (~28% of total) to a
single line emitting `string.char(_len)` at every inlined length-prefix
site. The `_len` argument is rarely a compile-time constant (lengths come
from user data), so the JIT can't fold the C-function call, and the cost
compounds — the 1KB Person fixture fires ~30 length-prefix sites per
encode.

Replace with a 256-entry lookup table `wire.CHARS` (built once at module
load, byte i -> string.char(i)). Codegen header now emits
`local CHARS = wire.CHARS` alongside the other hot-path localizers; the
single emit site in `emitInlineLenPrefix` swaps `string.char(_len)` for
`CHARS[_len]`. Parity is by construction — both return the same interned
1-byte string.

Tests: 752/752 pass. Bench (work.lab.local, median of 3, hello.Person
encode): 10B +4.7%, 1KB +16.8%, 10KB +21.1%, 100KB +34.2%, proto2 mid
+11.1%. Decode unchanged. See bench/PERF_LOG.md 2026-05-24 2ri entry.

Closes lkz and 86g (ffi.new buffer rewrite paths) — separate hand-spike
of that shape regressed 0.31x-0.77x across all sizes; the perceived
buffering inefficiency wasn't there, and 2ri captured the single hottest
line. Remaining encode-perf headroom is c0i (C-runtime backend).
ed383c92 — Eugene Blikh 2 months ago
codegen: inline proto2 extension writers/readers (qwt) + table.new(N,0) for packed lists (2sn)

qwt closes the proto2_basic.BenchPayload `min` bench's worst data point:
encode 1.89M → 3.30M msgs/s (+75%), decode 1.02M → 1.26M msgs/s (+23%).
Mechanism: collectExtsByExtendee groups all `extend Foo { ... }`
declarations by extendee FullName across input files; emitInlineEncode
emits one dedicated writer per extension instead of dispatching through
pb.codec.encode_field, and emitInlineDecode adds an elseif arm per
extension id straight into result._extensions[full_name]. Dynamic
extensions_list walk preserved for forward compat, gated on
#_elist > N so it pays one int compare when no runtime extension was
registered.

2sn pre-sizes packed-scalar repeated lists via table.new(N, 0) where
the count is recoverable from the LEN payload — exact (lim >> 2 or
lim >> 3) for fixed-width, upper bound (lim) for varint-packed. The
allocation is deferred into the `if wt == 2 then` branch so the
per-element fallback and non-packable types keep the bare-`{}`
alloc. Avoids u39's regression mode because the call cost is paid
once per repeated-field-first-occurrence, not per message decode.

Bench summary for hello.Person full mode: encode +3-5% across sizes,
decode +1-3% across sizes. Tests: 752/752 pure-Lua, 1043/1043 with
PB_ENABLE_C=1, JIT 37/37. PERF_LOG entry covers the rationale and
caveats.
0fb62135 — Eugene Blikh 2 months ago
codegen: localize wire.* upvalues per generated message function

Capture each _encode/_decode body, scan for wire.<name> refs, and
rewrite refs that appear >=2 times to bare locals with a
"local X = wire.X" prelude. Single-use refs stay as wire.X — without
the threshold the prelude TGETS outweighed the in-body saving on
sparse small-message decode.

Measured (median-of-3, shapes bench full mode):
- scalar-heavy: enc +38.5%, dec +39.2%
- packed-int32x100: enc +51.1%, dec +38.2%
- map-strxi32-*: enc +5.5%, dec +6-7%
- nested/oneof/wkt: +1-5% (within ~5% variance band)
- bench.lua Person small sizes: neutral

closes kot
b09c7213 — Eugene Blikh 2 months ago
codegen: inline 1-byte LEN fast path for string/bytes decode

Singular and repeated string/bytes fields in generated full-mode _decode
now read the length byte and dispatch in-line instead of calling
wire.decode_string / wire.decode_bytes. For length < 128 (the common
short-string RPC case), the path stays inside the parent JIT trace:
no child-trace stitch, no per-element function frame, utf8_len lookup
hoisted to a generated-file upvalue.

The original a6n design — one ffi.cast(U8CP, buf) at the top of each
_decode — was abandoned: the cdata wrapper is 24 bytes per call and
LuaJIT can't sink the allocation because the pointer local lives
across wire.decode_* call frames. Net regression at small sizes
(+11-16%) overwhelmed the single-byte-read savings.

Bench (hello.Person full-mode decode):

      size    ns/op before   ns/op after   delta
      10B          419            390     -6.9%
      100B         515            478     -7.2%
      1KB         9004           8042    -10.7%
      10KB       64644          56745    -12.2%
      100KB     625638         537750    -14.0%

Zero allocation impact across all sizes. Tests: 1043 pass, 37 JIT
trace gates pass. (a6n)
1eb062c1 — Eugene Blikh 2 months ago
c_runtime: wire encode/decode dispatch + conformance-c harness

pb.encode / pb.decode lazy-compile desc.c_plan on first call and route
to pb.c_runtime.encode/decode when PB_ENABLE_C=1 loaded the module.
Eager compile at finalize_message time fails on codegen's
forward-declared descriptors — sub-messages don't have .fields yet —
so compilation is deferred until first encode/decode, by which time
the whole module table is populated and sub-plan chase resolves.

Full-mode codegen wrappers (M.<Type>_{encode,decode}) gain the same
lazy-compile prologue, bypassing the inline body when the C runtime
is loaded. Runtime-mode wrappers already call pb.encode and pick up
dispatch centrally.

Harness side: the conformance Docker image now installs tarantool-dev
+ build-essential so the C runtime can be built in-container; a new
`just conformance-c` recipe builds runtime/pb/c_runtime.so inside the
container, runs the suite with PB_ENABLE_C=1, then cleans the .so to
keep the bind mount free of foreign-platform binaries. The runner
script also pre-populates package.loaded.pb (avoids
.rocks/lib/tarantool/pb.so from starwing lua-protobuf masking ours)
and orders package.cpath by jit.os so mixed .dylib/.so trees from
host/container interleavings don't cross-load.

Verified:
- just test                 → 748 pass, 291 C-conditional skipped
- PB_ENABLE_C=1 just test   → 1026/1039 pass; 13 fails are strict-decode
                              gaps in the C decoder (bd-rc8)
- just conformance-c        → 2729/2806 binary suite pass; 77 unexpected
                              failures match the same gap categories
                              (illegal wire-type 6/7, field-num 0/over,
                              overlong tag varint, UTF-8 rejection,
                              message merge for oneof/repeated)

Strict-decode parity tracked in bd-rc8; this commit closes the wiring
half of bd-43t (conformance gate for C paths).
a6eebdb3 — Eugene Blikh 3 months ago
codegen: local counter per repeated field on decode

Replaces `list[#list + 1] = val` with `_n_<fname> = _n_<fname> + 1;
list[_n_<fname>] = val` in every generated M.X_decode repeated-field
append site. One counter local per repeated non-map field, declared at
function entry. Counters survive across loop iterations so out-of-order
wire entries for the same field continue past the existing length
without re-scanning.

Profile attributed 6.2% of hello.Person 1KB decode to the `#list + 1`
re-traversal (26-email Person paid 26 list scans per decode).

Bench (Person full decode, msgs/s, median-of-3 vs post-4kj):
1KB +5.0%, 10KB +5.0%, 100KB +7.1%. Encode flat. Tests 745/745.
JIT 37/37, 0 bridges.

beads-tarantool-protobuf-cch
feecf8e0 — Eugene Blikh 3 months ago
codegen: inline 1-byte tag fast path at decode call sites

Hoists wire.decode_tag's 1-byte fast path into every generated
M.X_decode while-loop, falling back to the helper for multi-byte
tags (field ids > 15). The 1-byte case covers every protobuf field
with id 1..15 and is the dominant decode dispatch in real payloads.

Header now localizes string.byte, bit.band, and bit.rshift so the
inlined ops compile to straight local calls.

Bench (Person full decode, msgs/s, median-of-3 vs proper post-h8v
3-run baseline): 10B +14.7%, 100B +8.9%, 1KB +7.5%, 10KB +7.0%,
100KB +8.5%. Full encode is flat to small (-0.1% to -2.6%) at large
sizes, plausibly from header-upvalue layout. JIT trace gate: 37/37,
all bridges still 0. Tests: 745/745.

Also documented in bench/PERF_LOG.md, including the methodology
note that h8v's earlier numbers used single-run baselines and are
therefore ~3-5% optimistic; medians-of-3 are the standard now.

beads-tarantool-protobuf-4kj
9ee21c09 — Eugene Blikh 3 months ago
codegen: inline 1-byte varint length prefix at every LEN emit site

Eliminates the wire.encode_varint(#body) call + dispatch for every
length-delimited field in mode=full codegen. Profile flagged the
out[n] = wire.encode_varint(#_b) line as ~33% of hello.Person 1KB
encode time, with another ~17% in encode_varint dispatch — together
~50% of encode time. Lifting the 1-byte fast path (the dominant case
for proto strings and small message bodies) to the call site removes
the function frame entirely for lengths < 128.

Applied at every LEN emit site: singular/repeated message body,
singular/repeated string|bytes, packed scalar bundle, packed enum
bundle, map entry. Map value pieces (emitMapPiece message branch)
left as-is — they sit inside a single slot assignment that would
require a deeper restructure, and maps are not on the current hot
benchmark.

Results (hello.Person full encode, msgs/s): 10B +6.9%, 100B +7.9%,
1KB +25.7%, 10KB +48.1%, 100KB +31.9%. Decode flat (unchanged path).
Runtime mode flat (descriptor dispatch still calls encode_varint).
JIT trace gate: 37/37. Test suite: 745/745.

Bench history saved to bench/PERF_LOG.md with full numbers and the
workflow this iteration follows.

beads-tarantool-protobuf-h8v
2656c977 — Eugene Blikh 3 months ago
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.
7676fdc2 — Eugene Blikh 3 months ago
codegen: preserve descriptor options as `options = { ... }`

Every populated *Options message — FileOptions, MessageOptions,
FieldOptions, OneofOptions, EnumOptions, EnumValueOptions,
ServiceOptions, MethodOptions — surfaces on the generated descriptor as
a plain Lua sub-table named `options` (or `oneof_options` /
`value_options` for the per-member shapes). Standard fields use their
proto name as a bare Lua key; extensions use their fully-qualified
extension name as a bracket-quoted string key.

The walker is generic — no extension-specific code paths. Consumers
pull whatever they care about: `(google.api.http)` for REST routing,
`(versionpb.etcd_version_*)` for compatibility gates, `[deprecated =
true]` for migration tooling, and any in-house extension without pb
knowing about them. Standard fields sort alphabetically before
extensions (also alphabetical by full name) so codegen output stays
byte-identical across runs.

The `options` key is only emitted when at least one field is populated,
so option-free protos produce zero-diff output to before. Resolver
re-links *Options* messages so in-file extensions surface via the
protoreflect walker — protogen builds f.Desc before in-file extensions
are registered, and only f.Proto gets the post-pass fix-up, so we
rebuild the resolver manually.

UninterpretedOption is treated as a codegen-time error: a populated
entry means protoc couldn't resolve the extension, and emitting
opaque parser state would hide the problem.
ab214a39 — Eugene Blikh 3 months ago
codegen: emit strict <Type>_fields / _oneofs constants for lazy view

Lazy-view callers passing a typo'd field name to :get / :has / :set /
:clear / :which got `nil` back, indistinguishable from a legitimately-
absent optional field. Failures surfaced as missing data downstream.

Each generated message now exports a M.<Type>_fields table mapping
each field name to itself (and M.<Type>_oneofs for oneof groups),
wrapped by a new pb.field_names() helper that errors on unknown-key
reads and on any write. Routing field-name arguments through these
tables turns a typo into a load-time error at the read site.

Eager _encode / _decode keep round-tripping plain Lua tables — the
constants table is a lazy-view contract, documented in
docs/api-modes.md. README and lazy_test.lua converted to the new
pattern; three new tests cover typo / read-only / oneof-typo errors.
428ee1f6 — Eugene Blikh 3 months ago
codegen: propagate proto comments into generated _pb.lua

Leading `//` comments on messages, enums, enum values, fields, services,
and RPC methods now surface in the generated Lua:

- Message / enum descriptions become `---` blocks above `---@class` /
  `---@alias`, so LuaLS shows them on hover.
- Per-field comments collapse onto a trailing `@ description` on the
  matching `---@field` line.
- Enum values, service banners, and per-method entries get plain `--`
  lines inside the table literals so readers without an LSP still see
  the proto-side context.

Covers both `mode=full` and `mode=runtime` (emission is mode-independent
but the new luatest group runs against both).
f5c5ee6c — Eugene Blikh 3 months ago
codec: preserve -0.0 for proto3 float/double scalars

Proto3 default-elision dropped any singular float/double whose value
compared equal to 0 — `-0.0 == 0.0` in IEEE so the `if v ~= 0` guard
silently elided negative zero. The wire bytes for -0 differ from +0
and the TextFormatInput conformance corpus pins that -0 must survive
a round-trip; the failure surfaced as 10 unexpected text-suite
regressions covering FloatFieldNegativeZero (3 spellings × 2 outputs)
and Neg{Float,Double}FieldLargeNegativeExponentParsesAsNegZero
(2 types × 2 outputs).

Sign-bit guard via `1/v == math.huge` (positive zero yields +inf,
negative zero yields -inf). Applied in three layers:
  * runtime/pb/codec.lua: is_default_scalar + the specialized
    monomorphic writer for non-optional numeric scalars
  * runtime/pb/text.lua: is_proto3_default (text encoder elision)
  * inline codegen: scalarNotDefaultExpr emits the guard for
    float/double fields in full-mode `_encode` functions

Proto3 text-format conformance now sits at 416 ✓ / 18 skipped / 0
expected failures; binary+JSON holds at 1478 ✓. Unit-test regressions
cover both directions (preserved on encode + decode round-trip, +0
still elided) and use a runtime-computed -0.0 sentinel because LuaJIT
can constant-fold the literal `-0.0` to a sign-less zero in some
load paths.
7ec8f2b4 — Eugene Blikh 3 months ago
text: add pb.text.decode and wire it into conformance dispatch

Hand-written recursive-descent parser for the textproto grammar
covering every bucket the proto3 conformance suite exercises:
decimal/hex/octal integer literals with full 32/64-bit range checks,
float specials (inf/infinity/nan any case, oversize exponents
saturating to ±inf, underflows to ±0), C-style + \u/\U string escapes
with adjacent-literal concat and surrogate rejection, aggregate {} /
<> bodies, repeated short-form `[a, b, c]`, `key: K value: V` map
entries, the `[type.googleapis.com/...]` inline Any form alongside
the direct `type_url:`/`value:` form, enum-by-name-or-number,
reserved-name silent drop, numeric-field-ID tolerance, and
duplicate-singular-field rejection.

Plugin gains a small reserved_names emitter so the parser can match
mainline TextFormat::Parser's "silently drop reserved" rule. The Any
WKT descriptor advertises its real fields (type_url + string,
value + bytes) so the generic body walker can populate it directly
when the input doesn't use the inline-URL form.

cmd/conformance/core.lua stops short-circuiting text_payload to
`skipped` and runs it through pb.text.decode. The proto3 TextFormat
input suite climbs from 8 ✓ / 426 skipped to 406 ✓ / 18 skipped / 10
expected failures. The 10 surviving failures all share one cause
(proto3 -0.0 elision in the codec, not a parser bug — documented in
test/conformance/known_failures_text.txt). Binary+JSON conformance
holds at 1478 ✓. 591 unit tests pass across both codegen modes.

Closes the text-conformance-output branch.
343e4ba4 — Eugene Blikh 3 months ago
text: render captured unknown fields, tolerate SGROUP in skip

Two changes close the proto3 text-format conformance suite:

  1. `wire.skip_field` learns SGROUP/EGROUP. Wire 3 recurses through
     inner tags until a matching EGROUP, with field_id checked against
     the SGROUP's id. Callers (codec.lua, lazy.lua, wkt.lua, generated
     full-mode `_pb.lua`) now pass the tag's field_id so groups inside
     unknown-field skips don't error.

  2. `pb.text.encode` walks the captured `_unknown_fields` buffer when
     `opts.print_unknown_fields=true` and emits each entry in
     TextFormat numeric-field form:
       VARINT  -> "<id>: <uint64>"
       I64/I32 -> "<id>: 0x<hex>"
       LEN     -> speculative "<id> { <recurse> }"; rolls back to
                  byte-string form if the inner bytes don't parse as a
                  sub-message
       SGROUP  -> "<id> { <recurse> }" through matching EGROUP

`cmd/conformance/core.lua` threads `req.print_unknown_fields` into
`pb.text.encode` so `_Drop` tests drop unknowns and `_Print` tests
render them.

Conformance: text-format suite goes from 2 ✓ / 6 expected fails to
8 ✓ / 0 expected fails. All eight regression tests in
`conformance_test.lua` (one per upstream test, plus the fixed field-1011
tag bytes that were miscomputed earlier) now assert the target output.
ac9b9c13 — Eugene Blikh 3 months ago
codegen+wire: split length-prefix emission + 2-byte varint fast path

Two complementary encode optimizations: codegen + runtime writers no
longer pay a per-field string concat for length-prefixed fields, and
encode_varint_slow now handles 2/3/4-byte values without dropping into
the uint64 cdata path.

Composite bench/bench.lua, full mode:
  1KB encode:    229 K -> 332 K msgs/s  (1.45x)
  10KB encode:   38 K  -> 56.5 K        (1.49x)
  100KB encode:  4.1 K -> 6.7 K         (1.64x)

Runtime mode now matches full mode at 10KB+:
  1KB encode:    203 K -> 276 K  (1.36x)
  10KB encode:   35 K  -> 55 K   (1.56x)

bench/shapes_bench.lua biggest swings (both modes):
  packed-int32x1000 enc:  1.7 K  -> 13 K  (7.0-7.6x)
  packed-int32x100  enc:  21 K   -> 113 K (5.3x)
  scalar-heavy      enc:  582 K  -> 1.0 M (1.7x)

Per-helper, bench/wire_bench.lua:
  encode_varint(150)  [2-byte]:   529 ns -> 68 ns  (7.8x)
  encode_varint(20K)  [3-byte]:   841 ns -> 88 ns  (9.6x)
  encode_tag(16, LEN) [2-byte]:   531 ns -> 78 ns  (6.8x)
  encode_string(200B) [2-byte L]: 560 ns -> 109 ns (5.1x)
  encode_varint(127)  [1-byte]:    29 ns -> 28 ns  (no regression)

1. Split length-prefix emission. Length-delimited fields (string/bytes
   scalars, nested messages, packed scalars/enums) used to emit
   `tag → encode_len(body)` where `encode_len(body)` returns
   `encode_varint(#body) .. body`. The string concat allocated a copy
   of the body per field. Now the codegen and runtime writers emit
   three separate `out` slots — `tag`, `varint(#body)`, `body` — and
   let `table.concat(out)` join them in one pass at the end of encode.

   Applied to inline.go (full-mode codegen) for: singular and repeated
   nested messages, singular and repeated string/bytes scalars, packed
   scalars, packed enums. Applied to codec.lua build_writer / build_
   repeated_writer for the same set. Maps still go through encode_len
   pending a separate pass.

2. encode_varint_slow Lua-number fast paths. The outer encode_varint
   stays at the tiny `1-byte check + tail call` shape that LuaJIT
   inlines into hot traces. The slow function (not inlined into hot
   traces, so its body size is unconstrained) now handles non-negative
   Lua numbers up to 2^28 directly via bit.rshift / string.char with no
   cdata allocation. Values in [2^28, 2^53) emit one byte and recurse
   on the smaller residue. Only cdata inputs, negative Lua numbers
   (sign-extended to 10-byte varint), and the rare > 2^53 case still
   take the uint64 cdata loop. Net effect: every multi-byte varint
   encode that fits in a Lua number — including the length prefix for
   any string >= 128 bytes, every tag for field IDs >= 16, and every
   negative-zigzag sint — drops from ~500 ns to ~70 ns.

497/497 luatest pass.  23/23 jit-trace gates pass (the two new
multi-byte varint gates added in the bench infra commit confirm
encode_varint_slow JIT-compiles cleanly).  Conformance suite (binary
+ text) shows 1478 expected passes, 0 unexpected failures.
96ed328e — Eugene Blikh 3 months ago
json: treat null fields as absent (and Value's null as a real value)

Per the proto3 JSON spec, a null on any field means "use the field's
default" — encoded as missing — with the lone exception of
google.protobuf.Value, where JSON null is itself a Value carrying
NullValue.NULL_VALUE.

Three coupled bugs surfaced together:

1. decode_field_value used to fall through with v = box.NULL, leaving a
   useless box.NULL sitting in the result table for scalars. Now it
   returns nil for non-Value fields, PB_NULL for Value fields.

2. decode_message's repeated and map branches called `#jv` and
   `pairs(jv)` unconditionally; a JSON-null on either type crashed with
   "attempt to get length of 'void *'". Now both branches short-circuit
   when jv is box.NULL.

3. The nil-skip checks in the decode loop (`if dv ~= nil`) and in the
   codec / inline message encoders (`if v == nil then return end`)
   evaluated TRUE on box.NULL because Tarantool's cdata __eq aliases
   it to nil. Decode now uses rawequal(dv, nil); encode special-cases
   message kind by also accepting cdata, so the Value field's
   box.NULL sentinel survives all the way through to value_encode.

Drops 3 entries from test/conformance/known_failures.txt
(AllFieldAcceptNull, WrapperTypesWithNullValue, ValueAcceptNull).
Adds 5 regression tests covering scalar / repeated / map / wrapper
null treatment and the Value-NULL_VALUE exception.
Next