~bigbes/tarantool

tarantool-protobuf

ref: a5ba47c793d7f1177e823122c06ce8c290fa4603 tarantool-protobuf/runtime/pb d---------
8b5bfc0e — Eugene Blikh 2 months ago
codegen: int64_as_number opt-in flag for 64-bit decode as Lua number (5y9)

Adds a plugin generator option:

    protoc --tarantool_opt=int64_as_number=true ...

mode=full only, default off, error on mode=runtime. When set, 64-bit
scalar decoders (int64/uint64/sint64/fixed64/sfixed64) return a Lua
number for values that fit [-2^53, 2^53] (inclusive — both endpoints
are powers of two and exact as doubles), cdata otherwise. The decoded
type becomes value-dependent under this option; `+`/`-`/`*`/`==`
work transparently on both, so most callers don't have to care.

Wire side: decode_int64_n / decode_uint64_n / decode_sint64_n /
decode_fixed64_n / decode_sfixed64_n added alongside the existing
decoders. Bounds use LL/ULL cdata literals so the threshold compares
compile to plain 64-bit integer compares on a hot trace.

Codegen side: cfg.Int64AsNumber threads to writer.int64AsNumber;
decodeFnSuffix() emits "_n" only when the flag is set and the scalar
is one of the affected kinds. Every wire.decode_<st> emit site picks
it up — singular, repeated, packed, oneof, extension, map value.

The flag is a no-op under PB_ENABLE_C=1: the C runtime decides
number-vs-cdata on its own via luaL_pushint64 (Tarantool's small-fits-
in-double convention). The test gates on PB_ENABLE_C accordingly.

Workload-specific tradeoff measured on c_int64.Wide decode (full mode,
no PB_ENABLE_C, 5 fields per message):
  tiny   (all 1-byte vars)   2050 -> 1700 ns/op   (-17%, cdata avoided)
  medium (3-byte vars)       3220 -> 3600 ns/op   (+11%, extra cmp+tonumber)
  huge   (past 2^53)         7575 -> 7750 ns/op   (+2%, noise)

Enable when fields are dominated by small IDs/counters/small
timestamps that hit the 1-byte varint path; leave off otherwise.
The plugin flag help documents the tradeoff.

Test fixture: examples/expected/full_n/c_int64/c_int64_pb.lua via
the new gen-int64-as-number Justfile recipe. test/int64_as_number_test.lua
covers byte-identical encoding, small-value Lua number returns, default
cdata returns, 2^53 boundary inclusive, past-2^53 cdata fallback, and
Lua-number-input round-trip.

Suites: test 771/771, test-c 1057/1057 (5 skipped under PB_ENABLE_C=1).
117c6ef9 — Eugene Blikh 2 months ago
codegen: type-elision for sint64/fixed64/sfixed64 encode (a7l)

For mode=full the field type is statically known, so the codegen can
skip wire.to_int64 / wire.to_uint64's runtime type dispatch by pre-
casting at the call site:

  -- before
  out[n] = wire.encode_sint64(v)        -- calls to_int64(v) inside
  out[n] = wire.encode_fixed64(v)       -- calls to_uint64(v) inside

  -- after
  out[n] = wire.encode_sint64_i(INT64(v))   -- direct cdata path
  out[n] = wire.encode_fixed64_u(UINT64(v))
  out[n] = wire.encode_sfixed64_u(UINT64(v))

Wire-level: added encode_sint64_i / encode_fixed64_u / encode_sfixed64_u
alongside the existing encoders; encode_fixed64 is now a thin wrapper
that calls to_uint64 + encode_fixed64_u so the generic API surface stays
intact. INT64/UINT64 added as file-header upvalues in generated modules.

int32/int64/uint32/uint64 unchanged: those alias to encode_varint, which
only dispatches through to_uint64 on the slow-slow cdata path (large
negatives) — not worth the codegen surface.

Applied at every encode emit site that took a scalar: singular,
required, repeated non-packed, extension singular, extension repeated,
packed slow path, proto2 extension repeated. Map-value path also picks
it up via the shared helper.

c_int64.Wide encode (full mode, 4-run median):
  lua-num inputs   2769 -> 2520 ns/op   (+9.9% throughput)
  cdata inputs    12993 -> 12935 ns/op  (unchanged; to_uint64 already
                                         takes the cdata fast path)

Just below the issue's optimistic 10-15% but exactly the case that
matters — Lua-number inputs are the common case for user code on
sint64/fixed64 fields. Suites: test 766/766, test-c 1057/1057.
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.
e3f06741 — Eugene Blikh 2 months ago
runtime: pb.decode_unsafe + M.<Name>_decode_unsafe in runtime mode (58u)

Completes the unsafe-decode story 6bb started in full mode. Runtime mode
now exposes the same API via parallel `f._reader_unsafe` closures
compiled in pb.finalize_message against a swapped scalar table where
`string` maps to the bytes handler (no utf8_len). build_reader /
build_repeated_reader / decode_one are now parameterized on
(scalar_tbl, decode_msg_fn, decode_group_fn) so the same builders emit
both reader shapes. decode_message_unsafe, decode_group_unsafe, and
decode_extension_unsafe are literal clones of their safe twins with
three substitutions (documented in codec.lua): the _reader field, the
scalar table in slow paths, and the sub-message / group / extension
dispatchers. Tests in test/decode_unsafe_test.lua are now parameterized
over both modes (14 cases, including a map<string, int32> invalid-key
case that exercises the decode_one map-fallback path).

Runtime-mode microbench shows ~8% throughput vs validating decode on
the string-heavy 1KB Person; smaller than full mode's ~20% because the
descriptor dispatch + closure indirection swamp utf8_len, but still a
net win and the perf-cost-of-validating story is now consistent across
modes. Conformance 3240/3240 + JIT trace 37/37 still pass.

Closes 58u, also closes b12 (already fixed in 2656c97; never closed).
kyt still tracks unifying _decode_unsafe with C accel.
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).
b93adbf0 — Eugene Blikh 2 months ago
codec: emit per-descriptor encode body for monomorphic dispatch (21d)

The runtime-mode encode loop in codec.encode_message iterated
desc.fields and called `writer(data, out)` per field. Each iteration
saw a different closure, making the call site megamorphic from the
JIT's view: trace topology fragmented into 25 stops for Person_encode
vs full mode's 8 (~3x).

compile_encode_body emits a generated function at pb.finalize_message
time with one literal call site per field, all closing over a single
`_u` table upvalue indexed by constant int. TGETI on a stable array
with a literal key specializes on trace just like direct upvalue
access — and dodges LuaJIT's 60-upvalue function limit, which
TestAllTypesProto2 (~140 fields) would otherwise hit.

Trace topology (bench/jit_trace.lua):
  runtime/Person_encode             25 -> 10  (full 8)
  runtime/Person_encode multi-byte  18 ->  7  (full 6)
  runtime/Address_encode             6 ->  5  (full 2)
  runtime/Cardinality required       6 ->  2  (full 2)

Encode throughput (hello.Person, bench/bench.lua):
  10B    10.9 -> 15.5 MB/s  (+42%)
  100B  101.1 -> 144.4 MB/s (+43%)
  1KB   170.5 -> 183.5 MB/s (+8%)
  10KB  355.5 -> 362.3 MB/s (+2%)
  100KB 373.5 -> 407.6 MB/s (+9%)

Decode untouched. Full mode unaffected (its inline _encode doesn't
go through codec.encode_message). 752+1043 tests pass; examples
unchanged.

The issue's secondary goal — runtime within 5% of full on encode at
all sizes — is not met. Residual gap is per-closure call overhead;
closing it would need writer bodies inlined into the generated body
(not just call sites), which is a larger codegen-at-runtime change.

Closes tarantool-protobuf-21d.
Files tarantool-protobuf-h8x (decode_group bimodal trace flake
surfaced during validation; pre-existing on master).
0666a648 — Eugene Blikh 2 months ago
lsp: annotate codec / grpc / json / wkt public surface (74c)

Phase-2 of the LSP / LLM affordance work (phase-1 landed annotations
on init.lua + lazy.lua). Adds ---@param / ---@return on the
public-surface entry points so editors and LLM assistants see typed
signatures on hover.

- codec.lua: encode_message, decode_message, encode_field,
  compile_writers, compile_readers, merge_message.
- grpc.lua: loopback, multiplex, new_stream_pair, wrap_call,
  wrap_server_stream, wrap_server_view.
- json.lua: M.encode, M.decode. New pb.JsonEncodeOpts and
  pb.JsonDecodeOpts @class blocks in _types.lua document the opts
  fields the implementation actually consults (use_proto_names,
  emit_defaults / always_emit_zero_value alias, indent;
  ignore_unknown_fields on decode).
- wkt.lua: register, lookup, any_pack, any_unpack. New
  pb.AnyMessage @class for the {type_url, value} shape.

Also corrects two pre-existing signature lies in _types.lua:

- pb.register is `(desc): pb.Descriptor`, not `(full_name, desc)` —
  the implementation has always derived the key from desc.name and
  all callers pass a single arg.
- pb.any.pack is `(desc, t, type_url_prefix?)`, not `(t, type_url)`.

Pure metadata — `just test` stays at 752 and `just test-c` at 1043.

bd-74c
11b9ebde — Eugene Blikh 2 months ago
c_runtime: strict-decode parity with pure-Lua codec (rc8)

The C decoder accepted wire types 6/7, field number 0, field numbers
beyond 29 bits, overlong tag varints, invalid UTF-8 in string fields,
and unmatched proto2 SGROUPs — each of which the pure-Lua decoder
already rejected. It also replaced (instead of merged) when a singular
message field appeared more than once on the wire, dropping
sub-message scalars from the prior occurrence.

decode_body: reject wt 6/7 / field 0 / fn > 2^29-1 / overlong tag
varint up front; track egroup_seen so a group body that runs off the
end of the buffer fails loudly. dec_push_kind STRING: port wire.lua's
RFC 3629 validator (utf8.len equivalent) — covers singular, repeated,
oneof, map-key, and map-value via the same code path. Singular message
dispatch in decode_body and decode_extension_into: look up an existing
prev table at result[name] and merge via the new
merge_subresult_into, which ports codec.lua's merge_message (recursive
sub-message, repeated concat, map last-wins per key, skip for WKT
custom-decode).

Regression coverage in test/conformance_test.lua's conformance.core
group: pre-existing tests already pinned the wire-type, tag, UTF-8
(singular/repeated/oneof), and merge cases — those now also exercise
the C path under PB_ENABLE_C=1. Two gaps remained — map-key/value
UTF-8 and proto2 group balancing — both covered now via four new
tests, with proto2 routed through a TestAllTypesProto2 helper.

PB_ENABLE_C=1 just test: 1043/1043 (baseline 1039 + 4 new).
PB_ENABLE_C=1 just conformance-c: 0 unexpected failures (was 77).
just conformance (pure-Lua path): unchanged — no regression.

Closes rc8
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).
3235f34a — Eugene Blikh 2 months ago
c_runtime: proto2 — required, defaults, groups, extensions (ra6 3i)

Plumb proto2 semantic surface through the C plan and codec:

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

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

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

ra6 3i
cf281e26 — Eugene Blikh 2 months ago
c_runtime: WKT override-hook passthrough (ra6 3k)

A plan whose descriptor carries desc.encode / desc.decode now
dispatches through those overrides instead of erroring. Top-level
encode/decode, sub-message fields, and map<,message> values all
check the override refs and call them with the same contract the
pure-Lua codec uses: encode(value) -> body bytes; decode(buf) ->
value. runtime/pb/wkt.lua is unmodified.

ra6 3k
7edbf973 — Eugene Blikh 2 months ago
c_runtime: unknown-fields capture + re-emission (ra6 3j)
8c1e6078 — Eugene Blikh 2 months ago
c_runtime: map<K,V> encode + decode dispatch (ra6 3h)

Encode walks the user map with lua_next (the documented JIT exception
per CLAUDE.md — map hot paths can't avoid hash iteration). Each entry
goes into a stack-backed sub-buffer with synthetic tag(1,key) +
tag(2,value); proto3-elides default key and default value independently.
Map<,message> resolves its value sub-plan once and recurses through
encode_body. Decode reads the entry payload bounded, dispatches inner
id=1/id=2 (skipping anything else per spec), and lua_rawsets into a
lazy-created result map table; missing key or value defaults to the
proto3 zero. Reuses the existing list_stack_idx[] slot for the lazy
map cache since a field is either repeated or map, never both.

12 new tests cover round-trip for ages_by_nickname (string->int32),
nickname_by_age (int32->string), and addresses_by_label (string->
message), plus default-elision, multi-key correctness, empty maps,
and unknown-inner-id tolerance. Full suite 964/964 with PB_ENABLE_C=1.

bd-asz
b840992a — Eugene Blikh 2 months ago
c_runtime: oneof encode + decode dispatch (ra6 3g)

Encode resolves the active member per oneof group (last-non-nil wins in
declaration order via plan->oneofs[].member_indices), skips non-active
members, and force-emits the active branch so default values like
text="" still carry presence.

Decode clears sibling result-table entries after writing any field with
oneof_idx >= 0, mirroring codec.lua's oneof_siblings handling — wire
last-wins. Sibling-clear runs for both the scalar/string/enum/bytes arm
and the sub-message arm.

20 new tests in test/c_runtime_oneof_test.lua cover all three Result
branches across both codegen modes: byte-equal vs full.hello reference,
round-trip preservation of the active branch and absence of siblings,
default-value emission for active branches, and last-wins on both
encode (multi-branch input) and decode (multi-occurrence wire bytes).

Acceptance per bd-w3u
e875519e — Eugene Blikh 2 months ago
c_runtime: repeated + packed scalar encode/decode (ra6 3e)

Add repeated dispatch to the C-runtime encode/decode loop. Encode
side: encode_repeated_field walks Lua arrays via lua_objlen + per-
index rawgeti, dispatches on element kind. Packed numerics build
their payload in a stack-backed sub-buffer then emit `tag(LEN) +
varint(len) + body`; unpacked emit `tag + value` per element via
encode_one_field with force_emit=1 to bypass zero-suppression;
strings/bytes flow through the same path (never packable); repeated
messages reuse encode_submessage_field per element.

Decode side: per-field stack-slot cache (list_stack_idx[]) tied to
list_count[] avoids the per-element lua_getfield(result, name) round
trip the c-accel spike measured at 2x slower at 100KB. On first hit
for a repeated field we lua_createtable + write result[name] AND dup-
push the list onto the stack; subsequent hits lua_rawseti through the
cached absolute stack index. Lists stay valid across recursive sub-
message decodes because each child decode_body cleans up its own
scratch back to the caller's frame.

Packed/unpacked symmetry on read: a wt==LEN payload for any packable
scalar is decoded as a packed blob regardless of the schema's packed
flag, and a per-element-tagged stream is decoded element-by-element
even on a schema that defaults to packed — per proto3 reader rules.

New test/proto/c_repeated.proto fixture carries packed + explicit-
unpacked + repeated string/bytes + repeated message branches. The
encode and decode tests round-trip at 10/100/1000 elements per
branch. The two existing "skip repeated and map" marker tests
collapse to "skip map" — only map fields remain out of scope for
3e (bd-asz / 3h lands them next). 854 → 896 passing tests.

bd-jc9
6e7835a2 — Eugene Blikh 2 months ago
c_runtime: encode/decode singular sub-messages (ra6 3d)

Refactor encode_lua/decode_lua into encode_body/decode_body so the
field-walk loop is callable recursively, then dispatch the MESSAGE
kind into a per-side sub-handler. Repeated and map fields still skip
at the field-walk level — 3e (jc9) and 3h (asz) land them next.

encode_submessage_field force-establishes the parent enc_buf's
heap_idx via a no-op ebuf_grow before recursing. Without that the
final ebuf_reserve on the parent could land its new userdata above
sub-encode's leaked stack slots, making the closing lua_settop drop
the parent's heap.

decode_submessage_field bounds the inner read by temporarily
shrinking c->len to the sub-message end offset; the wire-prim
helpers already bounds-check against c->len, so a malformed inner
payload can't over-read into the outer message's bytes.

New fixture test/proto/c_nested.proto carries a 5-level singular
chain (L1->L2->L3->L4->L5) for the depth test. The two existing
"skip message" tests are renamed to "skip repeated and map" — sub-
messages now encode and decode end-to-end.

bd-hwe
e67a90f2 — Eugene Blikh 2 months ago
c_runtime: decode singular scalars (ra6 3c)

New entry pb.c_runtime.decode(plan, bytes) -> table, symmetric in
scope with 3b: int32/64, uint32/64, sint32/64, bool, fixed32/64,
sfixed32/64, float, double, enum, string, bytes. Repeated, map,
message-typed, and unknown tags skip by wire type — 3d/3e/3i/wyp
will extend later.

Result shape mirrors mode=full pure-Lua decode byte-for-byte: int64
family (int64/uint64/sint64/fixed64/sfixed64) push Tarantool cdata
via luaL_pushint64 / luaL_pushuint64; everything else lands as Lua
number/string/boolean. Pre-sized via lua_createtable(0, n_fields).
Field lookup is a linear scan over plan->fields by field_number; a
tag-keyed dispatch table is a future optimization.

Tests: 26 cases per codegen mode (full + runtime), covering the
bd-mz6 acceptance (round-trip of bd-y1n's Person payload matches
mode=full Person_decode shape-for-shape) plus per-kind coverage,
proto3-optional presence, fixed64 cdata, -0.0 sign preservation
(built via cdata to dodge LuaJIT literal-folding to +0.0),
skip-by-wire-type for out-of-scope shapes, unknown-tag skip, WKT
override rejection, and truncated input. Suite: 832/832 with
PB_ENABLE_C=1, 748/748 + 84 skipped without.

Closes bd-mz6
5e8f5f60 — Eugene Blikh 2 months ago
c_runtime: encode singular scalars (ra6 3b)

New entry pb.c_runtime.encode(plan, msg) -> string. Covers int32/64,
uint32/64, sint32/64, bool, fixed32/64, sfixed32/64, float, double,
enum (number or by_name lookup), string, bytes. Repeated, map, and
message-typed fields are silently skipped — those land in 3d/3e.

Buffer is a 4KB stack scratch promoted to a lua_newuserdata on
overflow, so a mid-encode luaL_error doesn't leak: the userdata is
GC'd at the unwind point.

Proto3 zero-suppression mirrors mode=full byte-for-byte: empty strings,
zero ints/fixed/enum skipped, +0.0 double skipped while -0.0 is emitted
via type-pun u64 equality (matches the `1/v == -math.huge` guard on
the Lua side). Proto3-optional fields bypass suppression. Plans with
desc.encode overrides (WKT) are rejected here — bd-rmf scope.

Tests: 16 cases per codegen mode (full + runtime), covering the bd-y1n
acceptance (Person {name='x', age=42, balance=-7,
user_id=0xDEADBEEFCAFEBABEULL, weight_kg=3.14} byte-equal to mode=full)
plus per-kind sweep and heap-grow path at 8KB. Suite: 806/806 with
PB_ENABLE_C=1, 748/748 + 58 skipped without.

Closes bd-y1n
8721548d — Eugene Blikh 2 months ago
lsp: foundational types + annotate pb runtime entry + lazy views

Add lua-language-server scaffolding so editors and LLM assistants get
parameter / return types for the public pb runtime. Pure metadata --
no runtime behavior change. 748 tests still pass.

- .luarc.json at repo root: declare LuaJIT runtime, project require
  paths (so require('full.hello.hello_pb') resolves), ignored
  directories (.rocks, generated dylibs, bench/starwing third-party).

- runtime/pb/_types.lua (@meta): foundational @class declarations
  for the descriptor contract (pb.Descriptor, pb.Field, pb.Enum-
  Descriptor, pb.OneofDescriptor), the public pb module surface
  (pb.Module), lazy view classes (pb.MessageView / pb.ArrayView /
  pb.MapView), gRPC transport contract (pb.GrpcTransport plus
  Service / Method descriptors), and the json / text / wire
  sub-modules. Mirrors docs/codegen.md's descriptor shape.

- runtime/pb/init.lua: @type pb.Module annotation on the returned
  table so hover on pb.encode / pb.decode_lazy / pb.parse picks up
  the signatures from _types.lua.

- runtime/pb/lazy.lua: @class annotations on the three view locals
  so lua-language-server merges them with the pb.MessageView /
  pb.ArrayView / pb.MapView declarations in _types.lua.

Next phases: annotate codec/grpc/json (phase 2), emit per-message
@class blocks from the plugin codegen (phase 3).
569bc3c9 — Eugene Blikh 2 months ago
fix(wire): cap the Lua-number varint fast path at 2^51 (x86_64 corruption)

encode_varint had a fast path for Lua numbers in [2^28, 2^53) that emitted
each byte via bit.band(n, 0x7f) / math.floor(n / 128). bit.band routes
through LuaJIT's number->int32 conversion, which on x86_64 uses the
magic-number trick (add 2^52 + 2^51, read the low bits). That is exact only
while n + 2^52 + 2^51 < 2^53, i.e. n < 2^51; above it the addition rounds to
an even double and silently drops low bits, corrupting the varint.

arm64 LuaJIT uses an exact FP->int instruction, so the bug was invisible on
Apple-Silicon dev machines and only surfaced on x86_64 (a 64-bit lease ID in
tarantool-etcd round-tripped 3041234677171912 -> 3041234677171940 over gRPC,
breaking lease lookups). Cap the fast path at 2^51; values in [2^51, 2^53)
now fall through to the exact uint64 cdata loop.

Adds test/wire_varint_test.lua pinning the round-trip at the boundaries.
Next