json: M.encode(desc, t, opts) for use_proto_names / emit_defaults / indent
Canonical protojson options were silently ignored. Plumb a single opts
table through encode_message via a CURRENT_ENCODE_OPTS state (mirroring
the decode-side CURRENT_OPTS):
* use_proto_names — emit snake_case field names (proto wire
names) instead of the spec-default
lowerCamelCase.
* emit_defaults — emit zero-valued implicit-presence scalars,
(alias: always_emit_zero_value) empty repeated lists, and empty
maps. Explicit-presence fields (optional /
oneof) and singular message fields remain
absent — matches protobuf-go's behavior.
* indent — pretty-print with the given indent string;
empty arrays/objects stay compact.
Tests parameterize over both codegen modes (descriptor-table contract is
shared between full and runtime) plus a dedicated parity group that
asserts byte-identical JSON across modes for each option.
codegen: resolve (tarantool.lua_package) via global type registry
The custom file option was being dropped silently: protoc encoded it
correctly into the FileDescriptorProto, but protobuf-go parked the
unknown extension in the message's unknown-fields tail because
E_LuaPackage was never registered with protoregistry.GlobalTypes. As a
result proto.GetExtension returned "" and every override the README
documented was a no-op — every caller fell through to the default
"<pkg>.<file>_pb" path resolution.
Register E_LuaPackage in init() and add a luatest regression that
asserts both the output path and the cross-file require strings honor
the option, parameterized over both codegen modes.
json: emit unbroken base64 for bytes fields ({nowrap = true})
Canonical proto3 JSON expects RFC 4648 unwrapped base64, but Tarantool's
`digest.base64_encode` defaults to RFC 2045 MIME-style 76-char line
wrapping. Any `bytes` payload past ~57 bytes used to land in the JSON
string with an embedded `\n`, which breaks every spec-compliant
consumer (grpc-gateway, protojson, protobuf-go's JSON, ...).
Pass `{nowrap = true}` at the two encode sites: the per-field bytes
encoder and the `google.protobuf.Any` opaque-fallback `value` encoder.
Surfaced by tarantool-etcd's `TestJSONGatewayBytesUnwrapped` over Range
responses whose `value` is >=57 bytes; its reference grpc-gateway never
emits the wrapped form.
build: add scm-1 rockspec for the pb runtime
Installs the 12 modules under pb.* from runtime/pb/ via builtin build
type; lua >= 5.1 is the only declared dependency since every other
import (bit, datetime, digest, ffi, fiber, json, protobuf, utf8) is a
Tarantool builtin. Plugin stays out of scope — it's a Go binary built
via the Justfile, not a luarocks artifact. Source URL points at the
planned sourcecraft.dev home that matches go.mod's module path.
docs: track protobuf editions support as a deferred non-goal
PLAN gained a §4.6 design note: what FEATURE_SUPPORTS_EDITIONS would
buy us (per-field packed opt-out is the only proto3 gap), what it
would cost across plugin + parser + descriptor contract, and the
conditions that would force a revisit. Section 8 non-goals now points
at the analysis instead of the bare bullet.
docs: full reference + how-to set, migrate Makefile to Justfile
Documentation overhaul that adds the missing user-facing surface:
four reference pages (runtime-api, generated-api, cli, grpc-contract),
twelve how-tos walking from first-message through custom transports,
a troubleshooting page, and a docs/index map. Every how-to references
a runnable artifact under examples/, all of them verified end-to-end.
Build system migration: the Makefile is gone; the Justfile is now
the canonical entry point and absorbs every target. examples/Justfile
ships one recipe per runnable example, forwarded via top-level
'just examples <name>'. The 'examples are part of the documented
surface' convention is pinned in CLAUDE.md, alongside a dedicated
section on updating the conformance harness (PROTOBUF_TAG bumps,
libjsoncpp path drift, new test-category wiring).
Stale-number sweep across README/PLAN/CLAUDE: fixture count 18→10,
test count 613/130→639, wire.lua LOC dropped, M7 marked done.
Descriptor-shape block deduplicated against codegen.md as the
canonical source. gRPC transports spec status reframed from
'draft / decision deferred' to 'shipped contract; external
transports deferred'.
.gitignore picks up *.snap / *.xlog / *.vylog / *.run / *.pid /
512.lock so example state can't leak into the working tree.
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.
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).
docs: drop text-format parser handoff brief
The pb.text.decode slice landed (7ec8f2b); the handoff brief is no
longer load-bearing. Removes the file and unlinks references from
README.md and PLAN.md.
docs: spec out msgpack encoding and gRPC transport story
Two design slices captured under docs/specs/ for later pickup:
- msgpack_encoding.md: proto schemas as IDL, msgpack as wire format.
Default mp_map keyed by field number; ARRAY/MP_TUPLE opt-ins for
box.space feeders; WKT bridging to MP_DATETIME / MP_INTERVAL.
- grpc_transports.md: Connect-JSON over HTTP/1.1 as default external
transport, net.box tunnel for in-cluster, explicit no on HTTP/2
termination (recommend Envoy). Conformance anchored on
connectrpc/conformance — same framed-runner shape as the existing
protobuf conformance harness.
PLAN.md cross-links both specs from M4, Open Questions, and Non-goals.
docs: refresh README + PLAN; add api-modes and codegen notes
README.md
* Status table reflects current state: text-format encode + decode,
-0.0 preservation, strict FieldMask, all-green proto3 conformance.
* Conformance baseline jumped to 1493 / 416 / 0 failures (was
1389 / 0 / 79 in the old table); explains the 1313+18 skipped
tests are all TestAllTypesProto2, deferred separately.
* Layout walks the full runtime/pb/ tree (lazy, text, json, wkt,
grpc, parser, dynamic, fileset, descriptor_pb).
* Generated API section calls out the three-mode design and links
to docs/api-modes.md and docs/codegen.md.
PLAN.md
* Section 2 ("Current state") rewritten — no longer claims M0;
lists what's actually in the codebase.
* M1, M2, M3 marked done with [x] checkboxes (had stale [ ]
markers across items that have been shipping for months).
* M5 conformance numbers updated to current baseline; calls out
the three commits that closed the proto3 suite (text decode,
-0 codec, JSON strict pass).
* M7 text-format parser entry updated: 416 ✓ / 0 failures (was
406 / 10 expected before the codec -0 fix).
* Added an M7 entry for the JSON strict-validation pass (six
classes of relaxation now enforced).
docs/api-modes.md (new)
* When to use full vs runtime (descriptor / reflect) vs lazy.
* Concrete code shapes for each, plus what the generated
Person_encode actually looks like in full mode.
* Descriptor-shape contract that ties all three together.
* Lazy: SoA index rationale, sparse-read vs dense-read trade-offs,
cross-over points from bench numbers.
docs/codegen.md (new)
* Pipeline diagram, CLI options, what gets emitted per .proto.
* Walk through the inline (full) mode emission with annotated
generated code.
* Runtime mode: pb.finalize_message's per-field writer/reader
closures and why they exist.
* Hot-path rules the generated code observes (no pairs(), 64-bit
as cdata, SoA over AoS for large index structures, keep hot
helpers small).
* Plugin source map; where to extend for a new wire type.
* Proto2 deferral section: what it would take and why we punt.
json: strict validation pass closes the proto3 conformance suite
Six classes of relaxation that the proto3 JSON conformance corpus
flagged are now enforced. All as Recommended.* tests; combined with
the -0.0 codec fix this empties known_failures.txt and brings the
proto3 binary+JSON suite to 1493 ✓ / 1313 skipped / 0 expected
failures / 0 unexpected failures.
1. Duplicate JSON keys. Tarantool's json.decode is hash-backed and
silently collapses `{"foo":1,"foo":2}` to one entry. A small
byte-walker `find_duplicate_json_keys` runs before json.decode,
tracks per-object brace frames and key sets, errors on the
second occurrence. Closes Recommended.FieldNameDuplicate.
2. camelCase / snake_case aliases of the same proto field appearing
side-by-side. Detected inside decode_message via a `field_seen`
set keyed by proto-name; second hit errors. Closes
FieldNameDuplicateDifferentCasing{1,2}.
3. JSON null inside repeated arrays and map values. Previously
silently dropped; now errors before decode_field_value. Closes
RepeatedField{Message,Primitive}ElementIsNull and
MapFieldValueIsNull.
4. Unknown enum *names* (not integers). decode_enum used to return
nil so callers silently dropped them; now raises by default and
returns nil only when M.decode's `ignore_unknown_fields=true`
opt is set. Conformance dispatch in cmd/conformance/core.lua
forwards this flag when req.test_category ==
JSON_IGNORE_UNKNOWN_PARSING_TEST. Closes
RejectUnknownEnumStringValueIn{Optional,Repeated,Map} and the
paired IgnoreUnknownEnumStringValueIn* tests.
5. google.protobuf.NullValue JSON canonical form. The single enum
value renders as the literal JSON `null` (not the string
"NULL_VALUE"); decode accepts either, encode emits null. The
decode_field_value null-handling path also treats a JSON null
on a NullValue-typed field as "set" rather than "absent" so a
oneof gets marked active. Closes
NullValueInOtherOneof{New,Old}Format.Validator.
6. FieldMask strict round-trip. Path validity is checked on both
sides: the snake_case wire form rejects uppercase letters,
consecutive underscores, trailing underscore, and underscore
followed by anything other than a lowercase letter — these
break the snake↔camel round-trip. The JSON form rejects any
underscore in the input (must be lowerCamelCase). Closes
FieldMask{TooManyUnderscore,PathsDontRoundTrip,
NumbersDontRoundTrip}.JsonOutput and JsonInput.FieldMaskInvalidCharacter.
The pre-existing "drop unknown enum strings" unit regressions in
test/conformance_test.lua were inverted to assert the new error
shape. New strict-validation regressions in test/json_test.lua pin
all six categories so they don't regress; the `json.strict` group
runs across both codegen modes via the shared descriptor table.
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.
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.
license: BSD 2-Clause
Matches Tarantool's own license. Closes the "TBD" placeholder in the
README.
wkt: split length-prefix emission to drop per-field concat
Apply the split-emit pattern from ac9b9c1 (inline.go + codec.lua) to the
WKT hand-rolled encoders. Replace `wire.encode_len(body)` — which
allocates `varint(#body) .. body` — with three out slots (tag, varint
length, body) wherever the result feeds a `table.concat(out)`
accumulator. For StringValue/BytesValue (whose encoders return a single
string and can't be split), inline encode_len's chain so LuaJIT folds
tag + varint + body into one multi-concat instead of two sequential
concats. Same final wire bytes.
Touched sites: struct_encode (split the outer entry wrap, inline the
inner key/value chain), list_encode, any_encode (type_url, value),
fieldmask_encode, and the WRAPPERS LEN-typed encoders.
The struct_encode inner entry stays a single chain rather than fanning
out into more `out` slots — eight slots per Struct entry regressed
B/op and gave no encode win on the wkt-event shape; three slots match
the codec.lua nested-message pattern and land the speedup.
bench/shapes (wkt-event encode, median across 4 runs):
full: 125895 -> 129000 msgs/s (+2.5%)
runtime: 115231 -> 120000 msgs/s (+4%)
make test 509/509, make jit-trace 23/23, docker conformance binary
1478/0 (unexpected), text suite clean.
docs: handoff brief for the pb.text.decode slice
Self-contained brief targeting the 408 proto3 TextFormatInput tests
still skipped after the encoder + SGROUP work. Covers existing
surface (text.lua encode-only, parser.lua schema parser, conformance
dispatch), grammar buckets the suite exercises (10 categories with
upstream test counts), files to touch, LuaJIT/cdata/box.NULL
conventions, and a verifying-locally checklist. Suggested
recursive-descent shape mirrors runtime/pb/parser.lua's cursor
mechanics.
No code changes; positions the next slice without committing to a
specific implementation schedule.
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.
test: pin unknown-fields text-format conformance regressions
Adds eight tests under conformance.core mirroring the
Recommended.Proto3.ProtobufInput.*UnknownFields_*.TextFormatOutput tests
in the Google harness. Each sends the byte-exact upstream payload (field
IDs 1001..1011 from UnknownToTestAllTypes) through cmd/conformance/core
and pins current output, with the target assertion + needed runtime fix
documented in each failure message.
Two blockers surface:
1. wire.skip_field rejects SGROUP/EGROUP (wire 3/4), so the four
Group/Repeated tests fail at decode.
2. pb.text.encode doesn't walk _unknown_fields, so the four *_Print
tests serialize empty.
Lets us iterate on those fixes locally without the Docker round-trip.
text: wire pb.text into conformance runner TEXT_FORMAT output
The runner short-circuited every TEXT_FORMAT request to `skipped`, even
though pb.text.encode has been encode-capable since M7. Plug it into
cmd/conformance/core.lua so protobuf/JSON input → text output exercises
the existing encoder end-to-end. Text-format input remains deferred
(pb.text is encode-only).
Text-format suite: 0 ✓ / 430 skipped / 4 expected fails →
2 ✓ / 426 skipped / 6 expected fails (Scalar/Message
*_Print added — unknown-field rendering still missing).