From dbbfaf3555b5cd0fbc76ec1c9e69c5c87072dc97 Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Sun, 24 May 2026 20:21:24 +0300 Subject: [PATCH] codegen: inline 1-byte varint fast path for packed scalar elements (aah) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-element wire.encode_(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_ 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). --- .beads/interactions.jsonl | 1 + .beads/issues.jsonl | 2 +- bench/PERF_LOG.md | 121 ++++++++ bench/packed_bench.lua | 61 ++++ .../internal/gen/inline.go | 74 ++++- .../full/c_repeated/c_repeated_pb.lua | 75 +++-- examples/expected/full/hello/hello_pb.lua | 12 +- .../full/proto2_basic/proto2_basic_pb.lua | 24 +- .../proto2/test_messages_proto2_pb.lua | 157 ++++++++--- .../proto3/test_messages_proto3_pb.lua | 266 +++++++++++++----- 10 files changed, 625 insertions(+), 168 deletions(-) create mode 100644 bench/packed_bench.lua diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index 8ba3108972ac3226f87bed45a6298fa108d145f6..511ba0badae1236281b4bac0377e37f68e9a4439 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -47,3 +47,4 @@ {"id":"int-a3f84d37","kind":"field_change","created_at":"2026-05-24T16:53:33.608711Z","actor":"Eugene Blikh","issue_id":"tarantool-protobuf-lkz","extra":{"field":"status","new_value":"closed","old_value":"open"}} {"id":"int-643d6ba3","kind":"field_change","created_at":"2026-05-24T16:53:43.706654Z","actor":"Eugene Blikh","issue_id":"tarantool-protobuf-86g","extra":{"field":"status","new_value":"closed","old_value":"open"}} {"id":"int-7db2afd6","kind":"field_change","created_at":"2026-05-24T17:05:10.538707Z","actor":"Eugene Blikh","issue_id":"tarantool-protobuf-ch2","extra":{"field":"status","new_value":"closed","old_value":"in_progress"}} +{"id":"int-b981997e","kind":"field_change","created_at":"2026-05-24T17:20:53.622551Z","actor":"Eugene Blikh","issue_id":"tarantool-protobuf-aah","extra":{"field":"status","new_value":"closed","old_value":"in_progress"}} diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 43d929c12f265332c8fb7bb32288f1fd62638d51..654c42d5de29c57359e1ac65c98a0e4f1690ab49 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -44,7 +44,7 @@ {"_type":"issue","id":"tarantool-protobuf-b12","title":"Encoder: parallel-array iteration for proto2 extensions","description":"Mirror the desc.oneofs_list finalize-time pattern: build desc.extensions_list once in pb.finalize_message, iterate with 'for i=1,#' in encode instead of walking the descriptor's hash table on every call. The 4-7x proto2 gap (vs 2-4x proto3 in bench/starwing_bench.lua) is partly explained by hash-walked extensions on the hot path. Expected: brings proto2 BenchPayload encode closer to proto3 Person encode at the same shape.","status":"open","priority":2,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-17T15:47:12Z","created_by":"Eugene Blikh","updated_at":"2026-05-17T15:47:12Z","labels":["encoder","perf","proto2"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"tarantool-protobuf-bgu","title":"Decoder: ffi.string(ptr+off, len) for string/bytes decode","description":"Replace buf:sub(npos, npos+len-1) in decode_string/decode_bytes with ffi.string against a cached ffi.cast('const uint8_t*', buf). Removes method-call dispatch and lets the JIT fuse the read with surrounding code. Hits hardest on repeated-string fields (26 emails strings in 1KB Person -\u003e 26 sub calls today). Watch for LuaJIT string-intern collision behavior to stay identical to sub().","status":"closed","priority":2,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-17T15:47:10Z","created_by":"Eugene Blikh","updated_at":"2026-05-24T04:20:11Z","closed_at":"2026-05-24T04:20:11Z","close_reason":"Spike (bench/c_accel/bgu_probe.lua, removed) measured ffi.string vs buf:sub for the 1-byte LEN string fast path on Tarantool 3.8.0/LuaJIT 2.1.0-beta3 / Apple M-series. Results (ns/call):\n\n size buf:sub per-call-cast amortized/26 amortized/4\n 10B 89.7 280.5 (3.1x) 224.9 (2.5x) 246.2 (2.7x)\n 32B 107.6 299.4 (2.8x) 243.8 (2.3x) 260.9 (2.4x)\n 80B 154.3 362.4 (2.3x) 300.1 (1.9x) 314.9 (2.0x)\n\nV1 (per-call ffi.cast(U8CP, buf) inside decode_string): 2.3-3.1x slower.\nV2 (cast hoisted to caller, threaded as ptr arg, amortized over 26 emails — the Person 1KB peak shape): still 1.9-2.5x slower. Cast-once-amortize-many fails because 'ptr + np - 1' mints a fresh cdata wrapper per call that the JIT cannot sink across the decode_string frame — same root cause as a6n's failed top-of-_decode cast. The amortization helps a little (V2 \u003c V1) but does not close the gap.\n\nConclusion: bgu's premise (ffi.string lets the JIT fuse the string read with surrounding code) does not hold on the current LuaJIT fork — the cdata allocation cost dominates any reduction in dispatch. Closing as superseded by a6n-decode-inline-len-fast-path-2026-05-23. Revival criteria: a measurable cdata-sink win on a future LuaJIT (likely needs upstream allocation-sink improvements for pointer-arith cdata), OR an API redesign where the decoder works on a cdata buffer end-to-end (not a Lua string).","labels":["decoder","perf","wire"],"dependencies":[{"issue_id":"tarantool-protobuf-bgu","depends_on_id":"tarantool-protobuf-0an","type":"blocks","created_at":"2026-05-17T18:47:21Z","created_by":"Eugene Blikh","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"tarantool-protobuf-u39","title":"Decoder: emit table.new(0, N) for result tables","description":"Codegen knows the field count of every message at emission time. Emit table.new(0, N) instead of {} so the result table is sized correctly from the start. Cheap, broad win — especially closes the 1KB cliff where hello.Person rehashes as nested address + repeated fields populate. LuaJIT-only (table.new is in require('table.new')).","status":"closed","priority":2,"issue_type":"task","assignee":"Eugene Blikh","owner":"bigbes@gmail.com","created_at":"2026-05-17T15:47:08Z","created_by":"Eugene Blikh","updated_at":"2026-05-18T19:06:00Z","started_at":"2026-05-18T19:01:45Z","closed_at":"2026-05-18T19:06:00Z","close_reason":"Attempted and reverted. table_new(0, N) for decode result tables passed tests + JIT but regressed small-payload decode (10B -15.4%, 100B -17.7%) because the call cost exceeds rehash savings at small scale, and the small case doesn't reach the first rehash. Large-payload decode flat. Confirms drm bug: alloc is not the throughput bottleneck. See bench/PERF_LOG.md.","labels":["codegen","decoder","perf"],"dependency_count":0,"dependent_count":1,"comment_count":0} -{"_type":"issue","id":"tarantool-protobuf-aah","title":"Encoder: codegen-emitted packed-scalar tight loops","description":"Replace per-element wire.encode_int32 calls with one inlined varint-emitting loop per packed field. Applies to packed int32, int64, sint32, sint64, bool, enum. The 1KB Person has 5 packed lucky_numbers + 26 emails — current per-element function-call boundary costs add up. Expected: 30%+ on packed-heavy payloads.","status":"open","priority":2,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-17T15:47:07Z","created_by":"Eugene Blikh","updated_at":"2026-05-17T15:47:07Z","labels":["codegen","encoder","perf"],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"tarantool-protobuf-aah","title":"Encoder: codegen-emitted packed-scalar tight loops","description":"Replace per-element wire.encode_int32 calls with one inlined varint-emitting loop per packed field. Applies to packed int32, int64, sint32, sint64, bool, enum. The 1KB Person has 5 packed lucky_numbers + 26 emails — current per-element function-call boundary costs add up. Expected: 30%+ on packed-heavy payloads.","notes":"Shipped. work.lab.local 3-trial medians: packed_int32 +90/+113/+106%, packed_sint32 +66/+236/+156%, packed_uint32 +73/+105/+102%, packed_bool +44/+51/+39% (at 10/100/1000 elements). packed_int64 +22% (cdata path; fast path skipped, gain from table_new pre-sizing). Headline Person 1KB +3.6%, proto2 mid +10.5%.","status":"closed","priority":2,"issue_type":"task","assignee":"Eugene Blikh","owner":"bigbes@gmail.com","created_at":"2026-05-17T15:47:07Z","created_by":"Eugene Blikh","updated_at":"2026-05-24T17:20:54Z","started_at":"2026-05-24T17:08:14Z","closed_at":"2026-05-24T17:20:54Z","labels":["codegen","encoder","perf"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"tarantool-protobuf-4ql","title":"Encoder: optional caller-owned ffi.cdata ibuf API","description":"Separate API surface, not the default. ibuf-based encoder that writes into a caller-owned ffi.cdata buffer instead of returning a fresh Lua string. Targets hot RPC paths where the caller already owns a reusable buffer (e.g. net.box send path). Independent of the codegen rewrite — different API contract. Earlier attempt parked in stash@{0}; revisit after the codegen-time emission lands so we can compare apples-to-apples.","status":"closed","priority":2,"issue_type":"feature","owner":"bigbes@gmail.com","created_at":"2026-05-17T15:47:06Z","created_by":"Eugene Blikh","updated_at":"2026-05-24T04:30:27Z","closed_at":"2026-05-24T04:30:27Z","close_reason":"Spike (bench/c_accel/ibuf_probe.lua, removed) implemented a hand-coded Person_encode_ibuf mirroring what protoc-gen-tarantool would emit: stable pre-allocated cdata scratch buffer, direct p[i] byte writes, single-pass with backpatched length for nested Address + packed lucky_numbers, ffi.copy(p+i, lua_str, n) for strings. Same-byte correctness verified across 10B/100B/1KB/10KB.\n\nBench (ns/op, Tarantool 3.8.0 / Apple M-series), Person_encode vs ibuf scratch-only:\n\n size Person_encode ibuf scratch ibuf+ffi.string speedup\n 10B 462.8 239.1 273.7 1.69x (win)\n 100B 470.1 243.1 286.3 1.64x (win)\n 1KB 3693.8 4872.6 5141.6 0.72x (loss)\n 10KB 17146 42807 43023 0.40x (loss)\n 100KB 159081 422510 432701 0.37x (loss)\n\nCrossover ~26 emails. Root cause: each email pays an ffi.copy(p+i, lua_str, n) boundary (~50 ns/call). At 26 emails = ~1.3 us pure boundary; at 2800 emails (100KB) = ~140 us pure boundary. Meanwhile Person_encode appends Lua-string refs to an out table (no FFI boundary) and pays ONE bulk table.concat memcpy at the end regardless of count. Per-field boundary work beats per-message bulk work only when field count is very small.\n\nFor 4ql's stated use case (net.box send path), typical Tarantool RPC payloads are \u003e=1 KB — exactly the regression zone (1.4-2.7x slower). Win window (\u003c100B) is too narrow to justify a separate API surface, especially since pb.encode is already 462-470ns at that size — saving 200ns on a sub-microsecond operation is not a meaningful net.box gain.\n\nThree abandoned attempts now (per-byte b:alloc cliff, v2 two-pass bulk reserve, this single-pass backpatch). All hit the same per-field FFI boundary tax. Closing as 'not viable on current LuaJIT'. REVIVAL CRITERIA: a future LuaJIT that can sink FFI calls into traces (so per-field ffi.copy stops paying the boundary cost), OR a use case where caller passes pre-cdata-cast strings (no per-email lua-string -\u003e cdata copy), OR a hand-written C encoder behind the c_runtime FFI surface (different path entirely — see c0i).","labels":["api","encoder","perf"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"tarantool-protobuf-86g","title":"Encoder: two-pass with exact size precomputation","description":"Walk fields once to sum byte sizes, allocate the final string at exact size, walk again to write. Eliminates buffer-grow realloc; one lua_pushlstring. Same pattern as vtprotobuf's Size() + MarshalToVT. Stacks with the codegen-time inline writes (depends-on). Expected: additional 30-50% on large-message encode beyond the inline-FFI baseline.","notes":"2026-05-24 — verdict: not viable as a pure-Lua path. Same evidence as lkz close.\n\nHand-spike of the two-pass exact-size+emit shape (ffi.new uint8_t[?],\ninlined per-message sizer + writer, recursive) regressed encode 0.31x-0.77x\nacross all 5 size buckets vs the current `out` table + `table.concat` shape.\nProfile (jit.p) showed 84% of time in the inlined message bodies, not in\nbuffering — the perceived buffering inefficiency simply isn't there.\n\nThe single profile-hottest line (39% of Person_encode share) was replaced\nin 2ri (CHARS[_len] lookup) for +17%-34% encode throughput on 1KB+ payloads.\n\nBeating the pure-Lua ceiling requires escaping it — c0i (C-runtime backend)\nis the remaining path. Closing 86g as superseded.","status":"closed","priority":2,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-17T15:47:05Z","created_by":"Eugene Blikh","updated_at":"2026-05-24T16:53:44Z","closed_at":"2026-05-24T16:53:44Z","labels":["codegen","encoder","perf"],"dependencies":[{"issue_id":"tarantool-protobuf-86g","depends_on_id":"tarantool-protobuf-h8v","type":"blocks","created_at":"2026-05-17T18:47:19Z","created_by":"Eugene Blikh","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"tarantool-protobuf-drm","title":"Inspect: ~120 B/op fixed encode/decode overhead in Lua hot path","description":"Cross-runtime bench (bench/COMPARISON.md) shows a fixed per-call\nallocation floor on both encode and decode in `mode=full`:\n\n- encode: 136 B/op even at 10 B payload (output is 10 bytes; ~126 B overhead)\n- decode: 112 B/op even at 10 B payload (top-level table = 2 hash-tables)\n\nFor comparison, Go vtproto runs encode at 16 B/op and decode at 8 B/op\non the same 10 B fixture.\n\nInspect:\n 1. What's the source of the 100+ B encode overhead? Suspect candidates:\n - per-call buffer.ibuf workspace allocation in wire.lua\n - Lua string concat building the result\n - GCstr header on the output string itself (~24-32 B)\n 2. Decode floor: 112 B/op. Likely 2x table headers + small alloc for\n the top-level message table.\n 3. For large payloads (100 KB encode = 131605 B/op vs output 96674 B)\n the overhead is ~28 KB. Is that one big realloc tail, or many small?\n\nOutcome: a writeup pointing to specific lines and a recommendation\non whether the M6 ibuf path would actually help.","notes":"Findings from bench/alloc_probe.lua:\n\nENCODE 10B floor = 136 B/op is the `out` Lua table:\n- `local out, n = {}, 0` alone: 64 B (Lua GCtab base)\n- After 5 array entries: 136 B (matches the encode floor exactly)\n- table.concat: 0 B in the bench because output bytes intern\n- Varints: 0 B per call because 1-byte string.char outputs intern\n globally (small string dedup)\n- 2-byte varints DO allocate: ~33 B per fresh value (encode_varint_slow\n path returns a fresh string from string.char + bit.bor)\n\nDECODE 10B floor = 112 B/op is the top-level result table\n{name=..., age=...} — same shape across iters means it'd allocate the\nsame in a real workload.\n\nLARGE PAYLOAD (1KB Person):\n- same input (output interned): 1368 B/op\n- varying age (output unique): 2368 B/op\n- Delta = ~1000 B is the result string for 930 bytes of output (the\n GCstr header + 930 content; the extra ~70 B is presumably padding /\n alignment / the Address's nested concat).\n- So real cost per call has TWO components: (a) the small fixed tables\n for the encoder workspace, (b) the output bytes themselves.\n\nKEY INSIGHT: bench numbers UNDERSTATE real allocation. The bench\niterates the SAME input → output string interns → bench reads only\nthe table cost. Real workloads where every message is unique pay\noutput-size + table-cost.\n\nOPTIMIZATION RANKING:\n1. Highest ROI: encoder workspace table. ~136 B per top-level encode\n + ~136 B per nested message encode (Address adds its own). For a\n 1KB Person we have ~5 nested encoders → ~700 B of tables. Replacing\n the array-of-string-pieces with a single growable `buffer.ibuf` cuts\n this to ~0. This is the M6 ibuf path.\n2. Medium: encode_varint_slow returns a fresh string on each call for\n non-fast-path values. Tag bytes are precomputed as literals; only\n payload varints hit this. Inlining the slow path into wire.lua's\n hot caller (or returning into a passed-in buffer) drops this.\n3. Lowest: result string. Unavoidable for the encoder's API contract\n (returns a string). Only the lazy path avoids it.\n\nDECISION POINT: M6 ibuf path (already prototyped, deferred per\nmemory/decode_perf_deferred.md) is the right intervention.\nConservatively halves encode B/op on small messages, larger savings\non nested-heavy payloads.\nCORRECTION to earlier note.\n\nRecommendation to \"use M6 ibuf path to cut the 136 B/op floor\" was\nwrong. Per memory/tarantool_ibuf_perf.md (verified against current\nrepo state — feature NOT in HEAD):\n\n- Naive per-byte b:alloc(1): ~20× slower realistic, ~36× synthetic\n- Two-pass bulk-reserve: byte-equal correct, ~2× slower at every\n payload size. Sitting in git stash@{0}.\n\nBoth prototypes lose on wall time because:\n (a) Bench harness runs with jit.off, closure dispatch interpreted\n (b) Realistic encoder cost is dominated by per-field closures\n (sizer + writer + emit_tag + pwrite_*), NOT byte writes\n (c) Two-pass adds an extra walk on top\n\nAlso: the 136 B/op encode floor is NOT the throughput bottleneck.\nCross-runtime gap (5× apiv2, 13-20× vtproto) is JIT/dispatch\noverhead per field, not allocator pressure. Cutting 136 B doesn't\nclose that gap.\n\nVIABLE PATHS (neither tried):\n1. Single-pass with backpatched length varints. Walk once. For nested\n msgs: write tag + 1-byte placeholder, recurse, fill in (or memmove\n if final length ≥ 128). Eliminates the size pass (~3 µs at 1 KB).\n2. Codegen-time `_encode_ibuf` per message in protoc-gen-tarantool\n (mode=full). Straight-line ibuf writes, no descriptor walk at\n runtime. Mirrors what pb.encode already does for the table path.\n Probably the only approach that actually closes the throughput gap.\n\nEither has to keep per-field closure count ≤ pb.encode's writers,\notherwise we re-introduce the dispatch cost both stashed prototypes\nfell on.\n2026-05-24 re-verification (post-21d/qwt):\n\nAllocation floor unchanged. Fresh `tarantool bench/alloc_probe.lua`:\n Person_encode 10B (same input) 136.0 B/op\n Person_encode 100B (same input) 136.0 B/op\n Person_decode 10B (same input) 112.0 B/op\n\nDrill-down isolates the source line-by-line — `local out, n = {}, 0` (64 B base) + 5 array entries (8 B each at LuaJIT tab growth = +72 B) = 136 B exactly. Lines: examples/expected/full/hello/hello_pb.lua:1044 (the `out` table) and 1049/1056/1061+ (the `n = n + 1; out[n] = ...` writes that populate it).\n\n21d closed dispatch fragmentation (small-encode +42% throughput per memory/21d-encode-dispatch-codegen-2026-05-24) and the alloc floor did not move — consistent with the CORRECTION note: the 5x cross-runtime throughput gap is not allocator pressure, and naive byte-buffer rewrites already lost in two stashed prototypes.\n\nInvestigation complete. Actionable interventions live in:\n- lkz (single-pass with backpatched length varints into one buffer)\n- 86g (two-pass with exact-size precompute; vtproto's Size+MarshalTo)\n\nBoth depend on closed h8v (codegen FFI direct writes). Closing drm.","status":"closed","priority":2,"issue_type":"bug","owner":"bigbes@gmail.com","created_at":"2026-05-17T15:13:38Z","created_by":"Eugene Blikh","updated_at":"2026-05-24T15:24:55Z","closed_at":"2026-05-24T15:24:55Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/bench/PERF_LOG.md b/bench/PERF_LOG.md index 845f08fc76f6cd670135cb1ca2432fd10623b4e6..2fb865c987510f950c6bc8bb7fded70be4a6ead9 100644 --- a/bench/PERF_LOG.md +++ b/bench/PERF_LOG.md @@ -84,6 +84,127 @@ macOS arm64. +## 2026-05-24 — aah: inline 1-byte varint fast path for packed scalar elements + +**Task:** [tarantool-protobuf-aah] Encoder: codegen-emitted packed-scalar +tight loops. Per-element `wire.encode_(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 `string.char(n)` (or — post-2ri +— a `CHARS[n]` lookup). Inlining the check + lookup at codegen time +removes the call entirely for the dominant small-value case. + +**Change.** New helper `emitPackedVarintElem` in `inline.go` emits the +per-element write with a type-conditional fast path: + +```lua +-- int32 / int64 / uint32 / uint64: +local _e = v[_i] +if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] +else + m = m + 1; parts[m] = wire.encode_(_e) +end + +-- sint32 / sint64 (zigzag inlined; -64 <= v <= 63 → 7-bit zigzag): +local _e = v[_i] +if type(_e) == 'number' and _e >= -64 and _e <= 63 then + m = m + 1; parts[m] = CHARS[bit.bxor(bit.lshift(_e, 1), bit.arshift(_e, 31))] +else + m = m + 1; parts[m] = wire.encode_(_e) +end + +-- bool (always 1 byte, no fast/slow split): +m = m + 1; parts[m] = CHARS[(v[_i]) and 1 or 0] + +-- fixed-width scalars (fixed32/64/sfixed32/64/float/double): unchanged +m = m + 1; parts[m] = wire.encode_(v[_i]) +``` + +Applied at every packed emit site: +1. Repeated packed scalar (mode=full, `emitInlineEncodeRepeated`). +2. Repeated packed enum (after string→int resolve, treated as int32). +3. Proto2 extension packed scalar (`emitInlineEncodeExtensionRepeated`). +4. Proto2 extension packed enum. + +Also pre-sizes the `parts` accumulator: `local parts, m = table_new(#v, 0), 0` +instead of `local parts, m = {}, 0`. Same pattern qwt+2sn used for the +decode-side packed result table — eliminates rehashes as elements push. + +**Tests:** 752/752 pass. JIT trace gate: 37/37 (no regressions). + +**Bench (work.lab.local, median of 3 trials, c_repeated.Holder packed +field × N elements, msgs/s):** + +| field | N | BEFORE | AFTER | Δ | +|----------------|----:|-------------|-------------|-------------| +| packed_int32 | 10 | 1,230,747 | 2,340,246 | **+90%** | +| packed_int32 | 100 | 337,800 | 719,580 | **+113%** | +| packed_int32 |1000 | 48,827 | 100,661 | **+106%** | +| packed_sint32 | 10 | 1,237,886 | 2,048,630 | **+66%** | +| packed_sint32 | 100 | 130,687 | 439,797 | **+236%** | +| packed_sint32 |1000 | 18,511 | 47,453 | **+156%** | +| packed_uint32 | 10 | 1,094,003 | 1,890,418 | **+73%** | +| packed_uint32 | 100 | 329,853 | 676,333 | **+105%** | +| packed_uint32 |1000 | 48,415 | 97,969 | **+102%** | +| packed_bool | 10 | 1,176,470 | 1,689,881 | **+44%** | +| packed_bool | 100 | 393,085 | 593,550 | **+51%** | +| packed_bool |1000 | 62,660 | 87,037 | **+39%** | +| packed_int64* | 10 | 415,289 | 507,112 | +22% | +| packed_int64* | 100 | 56,831 | 68,695 | +21% | +| packed_int64* |1000 | 5,981 | 7,331 | +23% | + +`* packed_int64` payloads use `int64_t` cdata elements; the +`type(_e) == 'number'` check fails, so the fast path is skipped and +`wire.encode_int64` is still called. The +22% there comes from the +`table_new(#v, 0)` pre-sizing alone — cdata varints still go through +the FFI boundary tax that drm/[[luajit_cdata_perf_patterns]] document. + +**Bench (work.lab.local, median of 3 trials, headline hello.Person — +encode-only deltas vs the 2ri AFTER medians):** + +| size | post-2ri | post-aah | Δ | +|-------|-------------|-------------|----------| +| 10B | 4,624,460 | 4,367,697 | ±noise | +| 100B | 4,520,564 | 4,486,250 | ±noise | +| 1KB | 489,681 | 507,275 | +3.6% | +| 10KB | 108,144 | 105,391 | ±noise | +| 100KB | 8,579 | 8,529 | ±noise | +| proto2_basic mid | 480,281 | 530,580 | **+10.5%** | + +The Person fixture has 5 packed lucky_numbers — too few elements for +aah to dominate. proto2 BenchPayload mid carries a heavier packed mix +and picks up the bigger lift. The headline gains are real but small; +**packed_bench.lua is where this change shows its true magnitude.** + +**Reading the table.** sint32 mid-size (+236%) is the biggest single +win because `encode_sint32` previously dispatched through three +function layers (`encode_sint32` → `zigzag_encode32` → `encode_varint` +→ `string.char`). aah collapses all of that into a 7-bit-zigzag check +plus a CHARS lookup. int32/uint32 land at +90-115% (one layer skipped). +bool at +39-51% (the loop overhead becomes a larger relative share at +this throughput). + +**Notes / caveats:** + +- The fast path uses `type(_e) == 'number'` — Lua numbers only. LuaJIT + trace-specializes on the hot type so the check is a single guard. Any + non-number element (cdata, string, nil) falls through to + `wire.encode_`, preserving every prior behavior including error + messages. +- For sint32/sint64 the 32-bit zigzag is safe in the fast-path range + (-64..63 fits in int8). The fallback `wire.encode_sint64` handles + larger values + cdata correctly. +- The packed enum path inherits the same fast path after string→int + resolve, since post-resolve values are always Lua numbers from the + enum lookup table. +- Repeated *unpacked* scalars (the `else` branch at the end of + `emitInlineEncodeRepeated`) emit one tag per element via a different + shape and are out of scope here. + +**Commit:** see the aah commit immediately following this entry. + +--- + ## 2026-05-24 — 2ri: CHARS[_len] lookup table replaces string.char(_len) at length-prefix sites **Task:** [tarantool-protobuf-2ri] Codegen: replace `string.char(_len)` with diff --git a/bench/packed_bench.lua b/bench/packed_bench.lua new file mode 100644 index 0000000000000000000000000000000000000000..6a4a2abb9bd94b16f1a8c9a59a948b5cb5dafa34 --- /dev/null +++ b/bench/packed_bench.lua @@ -0,0 +1,61 @@ +#!/usr/bin/env tarantool +-- Packed-scalar encode microbench for aah verification. +-- c_repeated.Holder has packed_int32 / packed_int64 / packed_sint32 / +-- packed_uint32 / packed_bool — exercises every packed varint flavor. +-- Element values are kept in the 1-byte fast-path range so the CHARS +-- lookup branch dominates (the realistic hot case). + +package.path = './runtime/?.lua;./runtime/?/init.lua;' + .. './examples/expected/?.lua;./examples/expected/?/init.lua;' + .. package.path + +local clock = require('clock') +local ffi = require('ffi') + +local holder_full = require('full.c_repeated.c_repeated_pb') + +local INT64 = ffi.typeof('int64_t') + +local function build_payload(n_elems, field) + local arr = {} + for i = 1, n_elems do + -- Cycle through 0-127 to stay on the 1-byte varint fast path. + arr[i] = i % 128 + end + if field == 'packed_int64' then + -- Convert to int64_t cdata to match the documented convention. + for i = 1, n_elems do arr[i] = INT64(arr[i]) end + elseif field == 'packed_bool' then + for i = 1, n_elems do arr[i] = (i % 2 == 0) end + end + return {[field] = arr} +end + +local FIELDS = {'packed_int32', 'packed_sint32', 'packed_uint32', 'packed_bool', 'packed_int64'} +local SIZES = {10, 100, 1000} + +local function bench_one(label, fn, p, encoded_bytes) + for _ = 1, 1000 do fn(p) end + local probe = 5000 + local t0 = clock.proc() + for _ = 1, probe do fn(p) end + local dt = clock.proc() - t0 + local rate = probe / dt + local iters = math.max(probe, math.min(2000000, math.floor(rate * 0.5))) + t0 = clock.proc() + for _ = 1, iters do fn(p) end + dt = clock.proc() - t0 + local msgs = iters / dt + local mb = msgs * encoded_bytes / 1024 / 1024 + print(string.format(' %-30s %10.0f msgs/s %8.1f MB/s', label, msgs, mb)) +end + +print('=== Packed scalar encode bench (c_repeated.Holder) ===') +for _, field in ipairs(FIELDS) do + for _, n in ipairs(SIZES) do + local p = build_payload(n, field) + local bytes = #holder_full.Holder_encode(p) + print(string.format('\n[%s × %d, encoded %d bytes]', field, n, bytes)) + bench_one('Holder_encode', holder_full.Holder_encode, p, bytes) + end +end diff --git a/cmd/protoc-gen-tarantool/internal/gen/inline.go b/cmd/protoc-gen-tarantool/internal/gen/inline.go index 292674343119628ed2e912f01c1269f10eb87e59..fe0dd3560e930a061f7825d88fd4470756dd4631 100644 --- a/cmd/protoc-gen-tarantool/internal/gen/inline.go +++ b/cmd/protoc-gen-tarantool/internal/gen/inline.go @@ -211,6 +211,52 @@ func emitInlineLenPrefix(w *writer, indent, bodyExpr string) { w.line("%send", indent) } +// emitPackedVarintElem emits the per-element write inside a packed-varint +// loop with an inlined 1-byte CHARS-lookup fast path matching +// wire.encode_varint's hot case. Falls through to wire.encode_ for +// negatives, large values, and cdata. For fixed-width scalars (no varint +// fast path) it just emits a straight wire.encode_ call. +// +// - indent is the Lua indentation prefix (e.g. " "). +// - list/idx are the parts-table and counter variable names. +// - valExpr is the source expression for the element (e.g. "v[_i]" +// or "nv" after enum-string resolve). +// - st is the scalar name from scalarName(). +// +// For sint32/sint64 the 7-bit zigzag range is -64..63 and is encoded +// inline with bit ops. For bool the emit is always 1 byte. For +// int32/int64/uint32/uint64 the fast path triggers when v is a Lua +// number in [0, 128) — covers the dominant small-positive case that +// pure-Lua wire.encode_varint already optimizes for. +func emitPackedVarintElem(w *writer, indent, list, idx, valExpr, st string) { + switch st { + case "bool": + w.line("%s%s = %s + 1; %s[%s] = CHARS[(%s) and 1 or 0]", + indent, idx, idx, list, idx, valExpr) + case "sint32", "sint64": + w.line("%slocal _e = %s", indent, valExpr) + w.line("%sif type(_e) == 'number' and _e >= -64 and _e <= 63 then", indent) + w.line("%s %s = %s + 1; %s[%s] = CHARS[bit.bxor(bit.lshift(_e, 1), bit.arshift(_e, 31))]", + indent, idx, idx, list, idx) + w.line("%selse", indent) + w.line("%s %s = %s + 1; %s[%s] = wire.encode_%s(_e)", + indent, idx, idx, list, idx, st) + w.line("%send", indent) + case "int32", "int64", "uint32", "uint64": + w.line("%slocal _e = %s", indent, valExpr) + w.line("%sif type(_e) == 'number' and _e >= 0 and _e < 128 then", indent) + w.line("%s %s = %s + 1; %s[%s] = CHARS[_e]", + indent, idx, idx, list, idx) + w.line("%selse", indent) + w.line("%s %s = %s + 1; %s[%s] = wire.encode_%s(_e)", + indent, idx, idx, list, idx, st) + w.line("%send", indent) + default: + w.line("%s%s = %s + 1; %s[%s] = wire.encode_%s(%s)", + indent, idx, idx, list, idx, st, valExpr) + } +} + // realOneofs returns the message's non-synthetic oneofs (skips the ones // proto3 expands explicit `optional` into). func realOneofs(m *protogen.Message) []*protogen.Oneof { @@ -412,15 +458,16 @@ func emitInlineEncodeRepeated(w *writer, f *protogen.Field, tag, fname string, f enumLocal := typeRef(file, f.Enum.Desc, selfPath, imports, "", prefix) fullName := string(f.Enum.Desc.FullName()) w.line(" if v ~= nil and #v > 0 then") - w.line(" local parts, m = {}, 0") - w.line(" for _i = 1, #v do") + w.line(" local _n = #v") + w.line(" local parts, m = table_new(_n, 0), 0") + w.line(" for _i = 1, _n do") w.line(" local elem = v[_i]") w.line(" local nv = elem") w.line(" if type(elem) == 'string' then") w.line(" nv = %s[elem]", enumLocal) w.line(" if nv == nil then error(\"unknown enum value '\" .. elem .. \"' for %s\", 0) end", fullName) w.line(" end") - w.line(" m = m + 1; parts[m] = wire.encode_int32(nv)") + emitPackedVarintElem(w, " ", "parts", "m", "nv", "int32") w.line(" end") w.line(" local _b = table.concat(parts)") w.line(" n = n + 1; out[n] = %s", tag) @@ -432,9 +479,10 @@ func emitInlineEncodeRepeated(w *writer, f *protogen.Field, tag, fname string, f packable := st != "string" && st != "bytes" if packable && f.Desc.IsPacked() { w.line(" if v ~= nil and #v > 0 then") - w.line(" local parts, m = {}, 0") - w.line(" for _i = 1, #v do") - w.line(" m = m + 1; parts[m] = wire.encode_%s(v[_i])", st) + w.line(" local _n = #v") + w.line(" local parts, m = table_new(_n, 0), 0") + w.line(" for _i = 1, _n do") + emitPackedVarintElem(w, " ", "parts", "m", "v[_i]", st) w.line(" end") w.line(" local _b = table.concat(parts)") w.line(" n = n + 1; out[n] = %s", tag) @@ -565,15 +613,16 @@ func emitInlineEncodeExtensionRepeated(w *writer, ext *protogen.Extension, tag, enumFull := string(ext.Enum.Desc.FullName()) w.line(" if _ev ~= nil and #_ev > 0 then") if ext.Desc.IsPacked() { - w.line(" local parts, m = {}, 0") - w.line(" for _i = 1, #_ev do") + w.line(" local _n = #_ev") + w.line(" local parts, m = table_new(_n, 0), 0") + w.line(" for _i = 1, _n do") w.line(" local elem = _ev[_i]") w.line(" local nv = elem") w.line(" if type(elem) == 'string' then") w.line(" nv = %s[elem]", enumLocal) w.line(" if nv == nil then error(\"unknown enum value '\" .. elem .. \"' for %s\", 0) end", enumFull) w.line(" end") - w.line(" m = m + 1; parts[m] = wire.encode_int32(nv)") + emitPackedVarintElem(w, " ", "parts", "m", "nv", "int32") w.line(" end") w.line(" local _b = table.concat(parts)") w.line(" n = n + 1; out[n] = %s", tag) @@ -598,9 +647,10 @@ func emitInlineEncodeExtensionRepeated(w *writer, ext *protogen.Extension, tag, packable := st != "string" && st != "bytes" if packable && ext.Desc.IsPacked() { w.line(" if _ev ~= nil and #_ev > 0 then") - w.line(" local parts, m = {}, 0") - w.line(" for _i = 1, #_ev do") - w.line(" m = m + 1; parts[m] = wire.encode_%s(_ev[_i])", st) + w.line(" local _n = #_ev") + w.line(" local parts, m = table_new(_n, 0), 0") + w.line(" for _i = 1, _n do") + emitPackedVarintElem(w, " ", "parts", "m", "_ev[_i]", st) w.line(" end") w.line(" local _b = table.concat(parts)") w.line(" n = n + 1; out[n] = %s", tag) diff --git a/examples/expected/full/c_repeated/c_repeated_pb.lua b/examples/expected/full/c_repeated/c_repeated_pb.lua index caac20aee1abb5de2e8a7ed7845e1ce66720acc5..c7b5d6a2b54d1ef2e931d4a969a07d42104ff670 100644 --- a/examples/expected/full/c_repeated/c_repeated_pb.lua +++ b/examples/expected/full/c_repeated/c_repeated_pb.lua @@ -226,9 +226,15 @@ function M.Holder_encode(t) -- field 1: packed_int32 v = t.packed_int32 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = encode_int32(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + local _e = v[_i] + if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] + else + m = m + 1; parts[m] = encode_int32(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\x0a" @@ -243,9 +249,15 @@ function M.Holder_encode(t) -- field 2: packed_int64 v = t.packed_int64 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = wire.encode_int64(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + local _e = v[_i] + if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] + else + m = m + 1; parts[m] = wire.encode_int64(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\x12" @@ -260,9 +272,15 @@ function M.Holder_encode(t) -- field 3: packed_sint32 v = t.packed_sint32 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = encode_sint32(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + local _e = v[_i] + if type(_e) == 'number' and _e >= -64 and _e <= 63 then + m = m + 1; parts[m] = CHARS[bit.bxor(bit.lshift(_e, 1), bit.arshift(_e, 31))] + else + m = m + 1; parts[m] = encode_sint32(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\x1a" @@ -277,9 +295,15 @@ function M.Holder_encode(t) -- field 4: packed_uint32 v = t.packed_uint32 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = wire.encode_uint32(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + local _e = v[_i] + if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] + else + m = m + 1; parts[m] = wire.encode_uint32(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\x22" @@ -294,8 +318,9 @@ function M.Holder_encode(t) -- field 5: packed_fixed32 v = t.packed_fixed32 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do m = m + 1; parts[m] = wire.encode_fixed32(v[_i]) end local _b = table.concat(parts) @@ -311,8 +336,9 @@ function M.Holder_encode(t) -- field 6: packed_fixed64 v = t.packed_fixed64 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do m = m + 1; parts[m] = encode_fixed64(v[_i]) end local _b = table.concat(parts) @@ -328,8 +354,9 @@ function M.Holder_encode(t) -- field 7: packed_double v = t.packed_double if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do m = m + 1; parts[m] = wire.encode_double(v[_i]) end local _b = table.concat(parts) @@ -345,8 +372,9 @@ function M.Holder_encode(t) -- field 8: packed_float v = t.packed_float if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do m = m + 1; parts[m] = wire.encode_float(v[_i]) end local _b = table.concat(parts) @@ -362,9 +390,10 @@ function M.Holder_encode(t) -- field 9: packed_bool v = t.packed_bool if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = wire.encode_bool(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + m = m + 1; parts[m] = CHARS[(v[_i]) and 1 or 0] end local _b = table.concat(parts) n = n + 1; out[n] = "\x4a" diff --git a/examples/expected/full/hello/hello_pb.lua b/examples/expected/full/hello/hello_pb.lua index 11a973dc3a3b9eb2378f6c696a03957aa6934e89..0c1874d07f3dcea7e0efd83e53e47a3f3161c96f 100644 --- a/examples/expected/full/hello/hello_pb.lua +++ b/examples/expected/full/hello/hello_pb.lua @@ -1123,9 +1123,15 @@ function M.Person_encode(t) -- field 7: 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] = encode_int32(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + local _e = v[_i] + if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] + else + m = m + 1; parts[m] = encode_int32(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\x3a" diff --git a/examples/expected/full/proto2_basic/proto2_basic_pb.lua b/examples/expected/full/proto2_basic/proto2_basic_pb.lua index 227a068b8b9111cd67406972beca37051dae986d..3e40529265347455a26b7c97f3362a894578f8a2 100644 --- a/examples/expected/full/proto2_basic/proto2_basic_pb.lua +++ b/examples/expected/full/proto2_basic/proto2_basic_pb.lua @@ -527,9 +527,15 @@ function M.Cardinality_encode(t) -- field 4: explicitly_packed v = t.explicitly_packed if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = encode_int32(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + local _e = v[_i] + if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] + else + m = m + 1; parts[m] = encode_int32(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\x22" @@ -1257,9 +1263,15 @@ function M.BenchPayload_encode(t) -- 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] = encode_int32(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + local _e = v[_i] + if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] + else + m = m + 1; parts[m] = encode_int32(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\x22" diff --git a/examples/expected/full/protobuf_test_messages/proto2/test_messages_proto2_pb.lua b/examples/expected/full/protobuf_test_messages/proto2/test_messages_proto2_pb.lua index cb3e3e3905f80d3675d60c8faf49a328994391ce..d3c395788a1fa320f69d11707297031d353a595d 100644 --- a/examples/expected/full/protobuf_test_messages/proto2/test_messages_proto2_pb.lua +++ b/examples/expected/full/protobuf_test_messages/proto2/test_messages_proto2_pb.lua @@ -1312,15 +1312,21 @@ function M.TestAllTypesProto2_encode(t) -- field 51: repeated_nested_enum v = t.repeated_nested_enum if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do local elem = v[_i] local nv = elem if type(elem) == 'string' then nv = M.TestAllTypesProto2_NestedEnum[elem] if nv == nil then error("unknown enum value '" .. elem .. "' for protobuf_test_messages.proto2.TestAllTypesProto2.NestedEnum", 0) end end - m = m + 1; parts[m] = encode_int32(nv) + local _e = nv + if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] + else + m = m + 1; parts[m] = encode_int32(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\x98\x03" @@ -1335,15 +1341,21 @@ function M.TestAllTypesProto2_encode(t) -- field 52: repeated_foreign_enum v = t.repeated_foreign_enum if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do local elem = v[_i] local nv = elem if type(elem) == 'string' then nv = M.ForeignEnumProto2[elem] if nv == nil then error("unknown enum value '" .. elem .. "' for protobuf_test_messages.proto2.ForeignEnumProto2", 0) end end - m = m + 1; parts[m] = encode_int32(nv) + local _e = nv + if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] + else + m = m + 1; parts[m] = encode_int32(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\xa0\x03" @@ -1390,9 +1402,15 @@ function M.TestAllTypesProto2_encode(t) -- field 75: packed_int32 v = t.packed_int32 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = encode_int32(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + local _e = v[_i] + if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] + else + m = m + 1; parts[m] = encode_int32(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\xda\x04" @@ -1407,9 +1425,15 @@ function M.TestAllTypesProto2_encode(t) -- field 76: packed_int64 v = t.packed_int64 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = encode_int64(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + local _e = v[_i] + if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] + else + m = m + 1; parts[m] = encode_int64(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\xe2\x04" @@ -1424,9 +1448,15 @@ function M.TestAllTypesProto2_encode(t) -- field 77: packed_uint32 v = t.packed_uint32 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = encode_uint32(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + local _e = v[_i] + if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] + else + m = m + 1; parts[m] = encode_uint32(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\xea\x04" @@ -1441,9 +1471,15 @@ function M.TestAllTypesProto2_encode(t) -- field 78: packed_uint64 v = t.packed_uint64 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = encode_uint64(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + local _e = v[_i] + if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] + else + m = m + 1; parts[m] = encode_uint64(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\xf2\x04" @@ -1458,9 +1494,15 @@ function M.TestAllTypesProto2_encode(t) -- field 79: packed_sint32 v = t.packed_sint32 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = encode_sint32(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + local _e = v[_i] + if type(_e) == 'number' and _e >= -64 and _e <= 63 then + m = m + 1; parts[m] = CHARS[bit.bxor(bit.lshift(_e, 1), bit.arshift(_e, 31))] + else + m = m + 1; parts[m] = encode_sint32(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\xfa\x04" @@ -1475,9 +1517,15 @@ function M.TestAllTypesProto2_encode(t) -- field 80: packed_sint64 v = t.packed_sint64 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = encode_sint64(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + local _e = v[_i] + if type(_e) == 'number' and _e >= -64 and _e <= 63 then + m = m + 1; parts[m] = CHARS[bit.bxor(bit.lshift(_e, 1), bit.arshift(_e, 31))] + else + m = m + 1; parts[m] = encode_sint64(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\x82\x05" @@ -1492,8 +1540,9 @@ function M.TestAllTypesProto2_encode(t) -- field 81: packed_fixed32 v = t.packed_fixed32 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do m = m + 1; parts[m] = encode_fixed32(v[_i]) end local _b = table.concat(parts) @@ -1509,8 +1558,9 @@ function M.TestAllTypesProto2_encode(t) -- field 82: packed_fixed64 v = t.packed_fixed64 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do m = m + 1; parts[m] = encode_fixed64(v[_i]) end local _b = table.concat(parts) @@ -1526,8 +1576,9 @@ function M.TestAllTypesProto2_encode(t) -- field 83: packed_sfixed32 v = t.packed_sfixed32 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do m = m + 1; parts[m] = encode_sfixed32(v[_i]) end local _b = table.concat(parts) @@ -1543,8 +1594,9 @@ function M.TestAllTypesProto2_encode(t) -- field 84: packed_sfixed64 v = t.packed_sfixed64 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do m = m + 1; parts[m] = encode_sfixed64(v[_i]) end local _b = table.concat(parts) @@ -1560,8 +1612,9 @@ function M.TestAllTypesProto2_encode(t) -- field 85: packed_float v = t.packed_float if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do m = m + 1; parts[m] = encode_float(v[_i]) end local _b = table.concat(parts) @@ -1577,8 +1630,9 @@ function M.TestAllTypesProto2_encode(t) -- field 86: packed_double v = t.packed_double if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do m = m + 1; parts[m] = encode_double(v[_i]) end local _b = table.concat(parts) @@ -1594,9 +1648,10 @@ function M.TestAllTypesProto2_encode(t) -- field 87: packed_bool v = t.packed_bool if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = encode_bool(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + m = m + 1; parts[m] = CHARS[(v[_i]) and 1 or 0] end local _b = table.concat(parts) n = n + 1; out[n] = "\xba\x05" @@ -1611,15 +1666,21 @@ function M.TestAllTypesProto2_encode(t) -- field 88: packed_nested_enum v = t.packed_nested_enum if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do local elem = v[_i] local nv = elem if type(elem) == 'string' then nv = M.TestAllTypesProto2_NestedEnum[elem] if nv == nil then error("unknown enum value '" .. elem .. "' for protobuf_test_messages.proto2.TestAllTypesProto2.NestedEnum", 0) end end - m = m + 1; parts[m] = encode_int32(nv) + local _e = nv + if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] + else + m = m + 1; parts[m] = encode_int32(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\xc2\x05" @@ -1751,15 +1812,21 @@ function M.TestAllTypesProto2_encode(t) -- field 102: unpacked_nested_enum v = t.unpacked_nested_enum if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do local elem = v[_i] local nv = elem if type(elem) == 'string' then nv = M.TestAllTypesProto2_NestedEnum[elem] if nv == nil then error("unknown enum value '" .. elem .. "' for protobuf_test_messages.proto2.TestAllTypesProto2.NestedEnum", 0) end end - m = m + 1; parts[m] = encode_int32(nv) + local _e = nv + if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] + else + m = m + 1; parts[m] = encode_int32(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\xb0\x06" diff --git a/examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua b/examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua index 0d3057c4ab9dad6e28302c6692be6e849f21e969..3add91ac2ca5105dc1e50cbe468d23415040f43b 100644 --- a/examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua +++ b/examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua @@ -843,9 +843,15 @@ function M.TestAllTypesProto3_encode(t) -- field 31: repeated_int32 v = t.repeated_int32 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = encode_int32(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + local _e = v[_i] + if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] + else + m = m + 1; parts[m] = encode_int32(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\xfa\x01" @@ -860,9 +866,15 @@ function M.TestAllTypesProto3_encode(t) -- field 32: repeated_int64 v = t.repeated_int64 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = encode_int64(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + local _e = v[_i] + if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] + else + m = m + 1; parts[m] = encode_int64(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\x82\x02" @@ -877,9 +889,15 @@ function M.TestAllTypesProto3_encode(t) -- field 33: repeated_uint32 v = t.repeated_uint32 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = encode_uint32(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + local _e = v[_i] + if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] + else + m = m + 1; parts[m] = encode_uint32(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\x8a\x02" @@ -894,9 +912,15 @@ function M.TestAllTypesProto3_encode(t) -- field 34: repeated_uint64 v = t.repeated_uint64 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = encode_uint64(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + local _e = v[_i] + if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] + else + m = m + 1; parts[m] = encode_uint64(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\x92\x02" @@ -911,9 +935,15 @@ function M.TestAllTypesProto3_encode(t) -- field 35: repeated_sint32 v = t.repeated_sint32 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = encode_sint32(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + local _e = v[_i] + if type(_e) == 'number' and _e >= -64 and _e <= 63 then + m = m + 1; parts[m] = CHARS[bit.bxor(bit.lshift(_e, 1), bit.arshift(_e, 31))] + else + m = m + 1; parts[m] = encode_sint32(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\x9a\x02" @@ -928,9 +958,15 @@ function M.TestAllTypesProto3_encode(t) -- field 36: repeated_sint64 v = t.repeated_sint64 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = encode_sint64(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + local _e = v[_i] + if type(_e) == 'number' and _e >= -64 and _e <= 63 then + m = m + 1; parts[m] = CHARS[bit.bxor(bit.lshift(_e, 1), bit.arshift(_e, 31))] + else + m = m + 1; parts[m] = encode_sint64(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\xa2\x02" @@ -945,8 +981,9 @@ function M.TestAllTypesProto3_encode(t) -- field 37: repeated_fixed32 v = t.repeated_fixed32 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do m = m + 1; parts[m] = encode_fixed32(v[_i]) end local _b = table.concat(parts) @@ -962,8 +999,9 @@ function M.TestAllTypesProto3_encode(t) -- field 38: repeated_fixed64 v = t.repeated_fixed64 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do m = m + 1; parts[m] = encode_fixed64(v[_i]) end local _b = table.concat(parts) @@ -979,8 +1017,9 @@ function M.TestAllTypesProto3_encode(t) -- field 39: repeated_sfixed32 v = t.repeated_sfixed32 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do m = m + 1; parts[m] = encode_sfixed32(v[_i]) end local _b = table.concat(parts) @@ -996,8 +1035,9 @@ function M.TestAllTypesProto3_encode(t) -- field 40: repeated_sfixed64 v = t.repeated_sfixed64 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do m = m + 1; parts[m] = encode_sfixed64(v[_i]) end local _b = table.concat(parts) @@ -1013,8 +1053,9 @@ function M.TestAllTypesProto3_encode(t) -- field 41: repeated_float v = t.repeated_float if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do m = m + 1; parts[m] = encode_float(v[_i]) end local _b = table.concat(parts) @@ -1030,8 +1071,9 @@ function M.TestAllTypesProto3_encode(t) -- field 42: repeated_double v = t.repeated_double if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do m = m + 1; parts[m] = encode_double(v[_i]) end local _b = table.concat(parts) @@ -1047,9 +1089,10 @@ function M.TestAllTypesProto3_encode(t) -- field 43: repeated_bool v = t.repeated_bool if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = encode_bool(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + m = m + 1; parts[m] = CHARS[(v[_i]) and 1 or 0] end local _b = table.concat(parts) n = n + 1; out[n] = "\xda\x02" @@ -1128,15 +1171,21 @@ function M.TestAllTypesProto3_encode(t) -- field 51: repeated_nested_enum v = t.repeated_nested_enum if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do local elem = v[_i] local nv = elem if type(elem) == 'string' then nv = M.TestAllTypesProto3_NestedEnum[elem] if nv == nil then error("unknown enum value '" .. elem .. "' for protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum", 0) end end - m = m + 1; parts[m] = encode_int32(nv) + local _e = nv + if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] + else + m = m + 1; parts[m] = encode_int32(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\x9a\x03" @@ -1151,15 +1200,21 @@ function M.TestAllTypesProto3_encode(t) -- field 52: repeated_foreign_enum v = t.repeated_foreign_enum if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do local elem = v[_i] local nv = elem if type(elem) == 'string' then nv = M.ForeignEnum[elem] if nv == nil then error("unknown enum value '" .. elem .. "' for protobuf_test_messages.proto3.ForeignEnum", 0) end end - m = m + 1; parts[m] = encode_int32(nv) + local _e = nv + if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] + else + m = m + 1; parts[m] = encode_int32(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\xa2\x03" @@ -1206,9 +1261,15 @@ function M.TestAllTypesProto3_encode(t) -- field 75: packed_int32 v = t.packed_int32 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = encode_int32(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + local _e = v[_i] + if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] + else + m = m + 1; parts[m] = encode_int32(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\xda\x04" @@ -1223,9 +1284,15 @@ function M.TestAllTypesProto3_encode(t) -- field 76: packed_int64 v = t.packed_int64 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = encode_int64(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + local _e = v[_i] + if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] + else + m = m + 1; parts[m] = encode_int64(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\xe2\x04" @@ -1240,9 +1307,15 @@ function M.TestAllTypesProto3_encode(t) -- field 77: packed_uint32 v = t.packed_uint32 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = encode_uint32(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + local _e = v[_i] + if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] + else + m = m + 1; parts[m] = encode_uint32(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\xea\x04" @@ -1257,9 +1330,15 @@ function M.TestAllTypesProto3_encode(t) -- field 78: packed_uint64 v = t.packed_uint64 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = encode_uint64(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + local _e = v[_i] + if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] + else + m = m + 1; parts[m] = encode_uint64(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\xf2\x04" @@ -1274,9 +1353,15 @@ function M.TestAllTypesProto3_encode(t) -- field 79: packed_sint32 v = t.packed_sint32 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = encode_sint32(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + local _e = v[_i] + if type(_e) == 'number' and _e >= -64 and _e <= 63 then + m = m + 1; parts[m] = CHARS[bit.bxor(bit.lshift(_e, 1), bit.arshift(_e, 31))] + else + m = m + 1; parts[m] = encode_sint32(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\xfa\x04" @@ -1291,9 +1376,15 @@ function M.TestAllTypesProto3_encode(t) -- field 80: packed_sint64 v = t.packed_sint64 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = encode_sint64(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + local _e = v[_i] + if type(_e) == 'number' and _e >= -64 and _e <= 63 then + m = m + 1; parts[m] = CHARS[bit.bxor(bit.lshift(_e, 1), bit.arshift(_e, 31))] + else + m = m + 1; parts[m] = encode_sint64(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\x82\x05" @@ -1308,8 +1399,9 @@ function M.TestAllTypesProto3_encode(t) -- field 81: packed_fixed32 v = t.packed_fixed32 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do m = m + 1; parts[m] = encode_fixed32(v[_i]) end local _b = table.concat(parts) @@ -1325,8 +1417,9 @@ function M.TestAllTypesProto3_encode(t) -- field 82: packed_fixed64 v = t.packed_fixed64 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do m = m + 1; parts[m] = encode_fixed64(v[_i]) end local _b = table.concat(parts) @@ -1342,8 +1435,9 @@ function M.TestAllTypesProto3_encode(t) -- field 83: packed_sfixed32 v = t.packed_sfixed32 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do m = m + 1; parts[m] = encode_sfixed32(v[_i]) end local _b = table.concat(parts) @@ -1359,8 +1453,9 @@ function M.TestAllTypesProto3_encode(t) -- field 84: packed_sfixed64 v = t.packed_sfixed64 if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do m = m + 1; parts[m] = encode_sfixed64(v[_i]) end local _b = table.concat(parts) @@ -1376,8 +1471,9 @@ function M.TestAllTypesProto3_encode(t) -- field 85: packed_float v = t.packed_float if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do m = m + 1; parts[m] = encode_float(v[_i]) end local _b = table.concat(parts) @@ -1393,8 +1489,9 @@ function M.TestAllTypesProto3_encode(t) -- field 86: packed_double v = t.packed_double if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do m = m + 1; parts[m] = encode_double(v[_i]) end local _b = table.concat(parts) @@ -1410,9 +1507,10 @@ function M.TestAllTypesProto3_encode(t) -- field 87: packed_bool v = t.packed_bool if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do - m = m + 1; parts[m] = encode_bool(v[_i]) + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do + m = m + 1; parts[m] = CHARS[(v[_i]) and 1 or 0] end local _b = table.concat(parts) n = n + 1; out[n] = "\xba\x05" @@ -1427,15 +1525,21 @@ function M.TestAllTypesProto3_encode(t) -- field 88: packed_nested_enum v = t.packed_nested_enum if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do local elem = v[_i] local nv = elem if type(elem) == 'string' then nv = M.TestAllTypesProto3_NestedEnum[elem] if nv == nil then error("unknown enum value '" .. elem .. "' for protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum", 0) end end - m = m + 1; parts[m] = encode_int32(nv) + local _e = nv + if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] + else + m = m + 1; parts[m] = encode_int32(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\xc2\x05" @@ -1567,15 +1671,21 @@ function M.TestAllTypesProto3_encode(t) -- field 102: unpacked_nested_enum v = t.unpacked_nested_enum if v ~= nil and #v > 0 then - local parts, m = {}, 0 - for _i = 1, #v do + local _n = #v + local parts, m = table_new(_n, 0), 0 + for _i = 1, _n do local elem = v[_i] local nv = elem if type(elem) == 'string' then nv = M.TestAllTypesProto3_NestedEnum[elem] if nv == nil then error("unknown enum value '" .. elem .. "' for protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum", 0) end end - m = m + 1; parts[m] = encode_int32(nv) + local _e = nv + if type(_e) == 'number' and _e >= 0 and _e < 128 then + m = m + 1; parts[m] = CHARS[_e] + else + m = m + 1; parts[m] = encode_int32(_e) + end end local _b = table.concat(parts) n = n + 1; out[n] = "\xb0\x06"