@sofa-buffers/corelib 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -70,6 +70,28 @@ declare const U64_MAX = 18446744073709551615n;
70
70
  declare const I64_MIN = -9223372036854775808n;
71
71
  /** Largest signed 64-bit value (`2^63 - 1`). */
72
72
  declare const I64_MAX = 9223372036854775807n;
73
+ /**
74
+ * The smallest output buffer this port accepts **for streaming** (CORELIB_PLAN
75
+ * §5.1, normative): the number a caller sizes a streaming buffer from.
76
+ *
77
+ * `1`, because the encoder splits every *atomic unit* — a field header varint, a
78
+ * fixlen word, an element count, a scalar or array element varint, an `fp32` /
79
+ * `fp64` element — across a flush, so no write needs contiguous room and the
80
+ * bytes produced are identical at any buffer size. (A port that required atomic
81
+ * units to land contiguously would declare the largest run it reserves instead;
82
+ * §5.1 caps any declaration at `20`.)
83
+ *
84
+ * It binds a buffer installed **with** a flush sink, at construction and at
85
+ * every mid-stream `OStream.setBuffer`: such a buffer must satisfy
86
+ * `buffer.length - offset >= MIN_OUTPUT_BUFFER` and is rejected right there
87
+ * with `SofabErrorCode.Argument`, never partway through a message.
88
+ *
89
+ * A buffer installed **without** a sink is subject to no minimum: no flush can
90
+ * occur, so nothing can be split, and the buffer either holds the whole message
91
+ * or reports `SofabErrorCode.BufferFull`. That is the one-shot `MAX_SIZE` case
92
+ * and it stays exact — a two-byte message encodes into a two-byte buffer.
93
+ */
94
+ declare const MIN_OUTPUT_BUFFER = 1;
73
95
  /**
74
96
  * Maximum nested-sequence depth (§4.9 / §6.2). An encoder must not open more
75
97
  * than this many nested sequences, and a decoder must reject a message that
@@ -87,13 +109,22 @@ declare const MAX_DEPTH = 255;
87
109
  * payload shorter than its declared length, an array that runs off the end, or
88
110
  * a nested sequence never closed). **Not an error** — more bytes could
89
111
  * complete it, and the caller owns end-of-input.
90
- * - `Invalid` — the bytes are malformed regardless of what follows.
112
+ * - `Invalid` — the bytes are malformed regardless of what follows. **Terminal**
113
+ * (CORELIB_PLAN §5.2): no later bytes can undo it.
114
+ *
115
+ * **Each of the three leaves by exactly one channel.** {@link IStream.feed}
116
+ * *returns* `Complete` or `Incomplete` — that is the {@link FeedStatus} pair, and
117
+ * the caller needs no end step (CORELIB_PLAN §6). `Invalid` is never returned by
118
+ * anything: it is the name of the outcome the throw carries, a {@link SofabError}
119
+ * whose `code` is {@link SofabErrorCode.InvalidMsg}, and the verdict latches, so
120
+ * every further `feed` re-throws without consuming input. There is no accessor
121
+ * that re-reports any of the three, because a fact with two ways to learn it is a
122
+ * fact that can be learned two different ways.
91
123
  *
92
- * {@link IStream.end} returns `Complete` or `Incomplete` (an `Invalid` message
93
- * has already thrown from {@link IStream.feed}); the one-shot {@link decode} /
94
- * {@link Cursor} path signals `Incomplete` and `Invalid` by throwing a
95
- * {@link SofabError} whose `code` is {@link SofabErrorCode.Incomplete} or
96
- * {@link SofabErrorCode.InvalidMsg}, and `Complete` by returning normally.
124
+ * The one-shot {@link decode} — the same stream, fed once — signals `Incomplete`
125
+ * and `Invalid` by throwing a {@link SofabError} whose `code` is
126
+ * {@link SofabErrorCode.Incomplete} or {@link SofabErrorCode.InvalidMsg}, and
127
+ * `Complete` by returning normally.
97
128
  */
98
129
  declare const DecodeStatus: {
99
130
  /** The bytes ended exactly at a field boundary — a valid message. */
@@ -105,6 +136,19 @@ declare const DecodeStatus: {
105
136
  };
106
137
  /** A decode's terminal outcome: one of the {@link DecodeStatus} values. */
107
138
  type DecodeStatus = (typeof DecodeStatus)[keyof typeof DecodeStatus];
139
+ /**
140
+ * The two outcomes {@link IStream.feed} can *return* — `Complete` and
141
+ * `Incomplete`, the ones that leave a stream usable.
142
+ *
143
+ * `Invalid` is missing on purpose, and the type is the statement: this port puts
144
+ * a refusal on the error channel (CORELIB_PLAN §6.3's second option), so a
145
+ * malformed message throws rather than returning, and a caller switching on the
146
+ * return value has no unreachable arm to write. The refusal is not lost by being
147
+ * absent here — it arrives as a {@link SofabError} whose `code` says which
148
+ * refusal it was, which is more than the outcome triple can express: a
149
+ * `LimitExceeded` rejection has no value among the three at all.
150
+ */
151
+ type FeedStatus = typeof DecodeStatus.Complete | typeof DecodeStatus.Incomplete;
108
152
 
109
153
  /**
110
154
  * Error handling for SofaBuffers.
@@ -145,7 +189,8 @@ declare const SofabErrorCode: {
145
189
  /**
146
190
  * A receiver-configured decode limit was exceeded — a dynamic array, string or
147
191
  * blob on the wire claims more elements / bytes than the caller's
148
- * {@link DecodeLimits} (`maxArrayCount` / `maxStringLen` / `maxBlobLen`)
192
+ * receiver cap (`max_dyn_array_count` / `max_dyn_string_len` /
193
+ * `max_dyn_blob_len`, §6.2.1 — stated by generated code, never by this codec)
149
194
  * allows. Deliberately distinct from {@link SofabErrorCode.InvalidMsg}:
150
195
  * exceeding a limit is *policy*, not wire malformation — the identical bytes
151
196
  * decode fine under a looser limit — so differential fuzzing must not read it
@@ -176,6 +221,12 @@ declare class SofabError extends Error {
176
221
  *
177
222
  * Values are stored as raw two's-complement bits; the same `Long` serves an
178
223
  * unsigned or signed field — {@link Long.toBigInt} takes the signedness.
224
+ *
225
+ * The two `bigint` conversions below go through the shared bit-punning scratch
226
+ * ({@link "./varint/bits64"}) that the codec's own hot paths use, rather than
227
+ * through mask-and-shift `bigint` arithmetic: the split allocated four
228
+ * intermediate `bigint`s per call and the join two, on the very boundary this
229
+ * class exists to make cheap.
179
230
  */
180
231
  declare class Long {
181
232
  /** Low 32 bits (unsigned). */
@@ -193,7 +244,11 @@ declare class Long {
193
244
  static readonly ZERO: Long;
194
245
  /** Construct from raw 32-bit halves. */
195
246
  static fromBits(low: number, high: number): Long;
196
- /** Split a `bigint` into its low/high 32-bit halves (two's complement). */
247
+ /**
248
+ * Split a `bigint` into its low/high 32-bit halves (two's complement). The
249
+ * `BigInt64Array` store *is* `ToBigInt64` — reduction modulo 2^64 — so an
250
+ * out-of-range value keeps exactly the halves the masks kept (bits64).
251
+ */
197
252
  static fromBigInt(value: bigint): Long;
198
253
  /** From an integer `number` (`|n| < 2^53`); sign handled via `bigint` once. */
199
254
  static fromNumber(n: number): Long;
@@ -205,27 +260,110 @@ declare class Long {
205
260
  toString(signed?: boolean): string;
206
261
  }
207
262
 
263
+ /**
264
+ * Encode-side text helper.
265
+ *
266
+ * Two paths turn strings into UTF-8:
267
+ *
268
+ * - {@link utf8Length} / {@link utf8Write} — an allocation-free, two-pass writer
269
+ * used by the encoder's contiguous-write fast path. `writeString` needs the byte
270
+ * length *before* the payload (it goes into the fixlen length word), so the
271
+ * length is scanned first and the bytes are then written straight into the
272
+ * output buffer. This avoids `TextEncoder.encode`'s per-call WHATWG setup cost
273
+ * plus the throwaway `Uint8Array` it allocates and the second copy into the
274
+ * buffer — which V8 profiling showed to be the encoder's dominant cost (a
275
+ * short string cost ~700 ns almost entirely in `TextEncoder`).
276
+ * - {@link utf8WriteSink} — the same walk, emitting one byte at a time into a
277
+ * {@link ByteSink}, for the streaming path where the output buffer is narrower
278
+ * than the payload and the encoder drains between bytes. It replaces a
279
+ * `TextEncoder.encode` into a throwaway array, which was an allocation on a
280
+ * codec path and so is not available to it (CORELIB_PLAN §6.6).
281
+ *
282
+ * **Strict UTF-8 (MESSAGE_SPEC §8, CORELIB_PLAN §6.4).** A `string` is UTF-8
283
+ * text; the encode side is always strict for this Unicode-string target. The
284
+ * platform encoders are lossy — `TextEncoder`, and the hand-rolled fast path it
285
+ * once mirrored, both replace an **unpaired surrogate** with `U+FFFD` — which is
286
+ * the silent data mutation §8 forbids in every mode and direction. Both paths
287
+ * here instead **reject** an unpaired surrogate with an `InvalidArgument`
288
+ * {@link SofabError} (the encode-side image of the decode `INVALID` outcome), so
289
+ * a producer can never emit bytes a strict decoder would refuse. Every *valid*
290
+ * string — ASCII, multibyte BMP, correctly paired astral code points, embedded
291
+ * `U+0000` — still encodes byte-for-byte as before.
292
+ */
293
+ /** Anything that takes UTF-8 one byte at a time — in practice, the encoder itself. */
294
+ interface ByteSink {
295
+ /** @internal Append one byte, draining to the flush sink when the buffer is full. */
296
+ putByte(b: number): void;
297
+ }
298
+
208
299
  /**
209
300
  * The output drain used for streaming encodes.
210
301
  *
211
302
  * When an {@link OStream} is given a `FlushSink`, it writes into a small caller
212
- * buffer and hands each filled region to the sink, so a message can be far
213
- * larger than the buffer — larger than RAM, even. The chunk passed to the sink
214
- * is only valid for the duration of the call; copy it if you need to retain it.
303
+ * buffer and hands each filled region to the sink, so a message can be far larger
304
+ * than the buffer — larger than RAM, even.
305
+ *
306
+ * **The sink is only ever handed the installed output buffer** — `buffer` is the
307
+ * very array the caller installed, and the bytes are `buffer[start..end)`. There
308
+ * is no second case: CORELIB_PLAN §5.1.6 forbids an encoder from passing the sink
309
+ * any other memory, so a sink never has to ask whether what it received is its own
310
+ * buffer or a payload the encoder handed through from somewhere else. (An earlier
311
+ * revision of the spec permitted that pass-through, off by default; the permission
312
+ * is withdrawn.)
313
+ *
314
+ * The region is valid for the duration of the call. A sink that **copies** it
315
+ * simply returns, and the encoder keeps writing into the same buffer from offset
316
+ * `0`. A sink that **takes** the buffer — queues it, hands it to a transport —
317
+ * must install a replacement with {@link OStream.setBuffer} before returning
318
+ * (§5.1.5); returning without one means "I copied".
319
+ *
320
+ * Passing the buffer and the coordinates rather than a `subarray` of it is not
321
+ * cosmetic: a view would be an allocation per flush, and the encoder allocates
322
+ * nothing after construction (§6.6).
323
+ *
324
+ * **`needed`** is how many *contiguous* bytes the encoder wants at the cursor
325
+ * once the handover is done, or `0` when this flush is not a request for room —
326
+ * an explicit {@link OStream.flush}, or a drain that only needs *some* space. It
327
+ * is advisory in both directions: a sink is free to ignore it, and the encoder
328
+ * has a route that works without it (an atomic unit is split, a bulk write falls
329
+ * back to writing element by element, producing the identical bytes). What it
330
+ * buys is that a sink which *does* size its replacement — `growingOStream`'s —
331
+ * can open the bulk path instead of guessing. Without it, a caller-supplied
332
+ * growing sink can only double blindly and a large array silently loses its
333
+ * kernel route.
334
+ *
335
+ * A sink written before this argument existed keeps working unchanged: it is the
336
+ * last parameter, optional, and JavaScript simply drops it.
215
337
  */
216
- type FlushSink = (chunk: Uint8Array) => void;
338
+ type FlushSink = (buffer: Uint8Array, start: number, end: number, needed?: number) => void;
217
339
 
218
340
  /**
219
341
  * The SofaBuffers encoder.
220
342
  *
221
- * `OStream` writes fields into a byte buffer. Two modes:
343
+ * `OStream` writes fields into a **caller-supplied** byte buffer — the corelib
344
+ * allocates no output buffer and never grows or reallocates one it was handed
345
+ * (CORELIB_PLAN §5.1). Two ways to drive it:
222
346
  *
223
- * - **In-memory** (`new OStream()`): an auto-growing buffer; call
224
- * {@link OStream.bytes} for the finished message.
225
- * - **Streaming** (`new OStream(buffer, offset?, flush?)`): writes into a
226
- * caller-provided buffer and, when it fills, hands the produced bytes to the
227
- * `flush` sink and continues — so the buffer can be much smaller than the
228
- * message. `offset` reserves room at the front for a lower-layer header.
347
+ * - **One-shot** (`new OStream(buffer, offset?)`): the buffer must hold the
348
+ * whole message — the size a caller derives from the generated `MAX_SIZE` —
349
+ * and a buffer that fills reports `BUFFER_FULL` rather than growing.
350
+ * - **Streaming** (`new OStream(buffer, offset?, flush?)`): when the buffer
351
+ * fills, the produced bytes go to the `flush` sink and encoding continues —
352
+ * so the buffer can be arbitrarily smaller than the message, **down to a
353
+ * single byte** (§5.1): no single write requires contiguous room, a value
354
+ * larger than the buffer is split across flushes, and the bytes produced are
355
+ * identical either way — which is why this port declares
356
+ * {@link MIN_OUTPUT_BUFFER} = 1, the floor a buffer installed *with* a sink
357
+ * must clear. `offset` reserves room at the front for a lower-layer header,
358
+ * and it is `length - offset` that must clear the floor. Without a `flush`
359
+ * sink there is nowhere to drain to, so no minimum applies.
360
+ *
361
+ * Where the message has no schema-derived bound, the allocating half is the
362
+ * caller's, and CORELIB_PLAN §5.1.2 names the shape it takes: "install a scratch
363
+ * buffer **with a sink** that appends into the growing result". `growingOStream`
364
+ * (`./accumulate.ts`) is that caller ready-made — a **helper** (§6.6.1) built out
365
+ * of the streaming mode below, owning storage the encoder never sees the whole
366
+ * of. `OStream` itself has no growth mechanism at all.
229
367
  *
230
368
  * Generated code typically writes one field per message field; the methods map
231
369
  * one-to-one onto the wire types. Problems throw {@link SofabError}.
@@ -239,18 +377,39 @@ type FlushSink = (chunk: Uint8Array) => void;
239
377
 
240
378
  /**
241
379
  * Encoder for the SofaBuffers wire format. Each `write*` method appends one
242
- * field and maps one-to-one onto a wire type. Construct it in-memory (an
243
- * auto-growing buffer, read back with {@link OStream.bytes}) or in streaming
244
- * mode over a caller-provided buffer that drains to a {@link FlushSink} as it
245
- * fills, so the message can outgrow the buffer. Invalid arguments and a full
246
- * buffer with no sink throw {@link SofabError}.
380
+ * field and maps one-to-one onto a wire type. It writes into the buffer the
381
+ * caller supplies and into no other: it allocates none of its own, grows
382
+ * none it was given, and has no hook through which anything else could grow one
383
+ * for it (CORELIB_PLAN §5.1.2, §6.6). Hand it a buffer that holds the whole
384
+ * message, or one that drains to a {@link FlushSink} as it fills so the message
385
+ * can outgrow it. Invalid arguments and a full buffer with no sink throw
386
+ * {@link SofabError}. To let the buffer follow the message instead, encode into
387
+ * the accumulator `growingOStream` builds — a helper over the streaming mode,
388
+ * not a mode of this class.
247
389
  */
248
- declare class OStream {
390
+ declare class OStream implements ByteSink {
249
391
  private buf;
250
392
  private pos;
393
+ /**
394
+ * Where the **current flush unit** begins in {@link buf}: the first byte the
395
+ * next {@link flush} hands to the sink.
396
+ */
251
397
  private start;
398
+ /**
399
+ * Where the **message so far** begins in {@link buf} — what {@link bytes} and
400
+ * {@link bytesUsed} report from.
401
+ *
402
+ * Equal to {@link start} for every ordinary stream, and the two move together:
403
+ * what has not been flushed is the whole of what is still in the buffer. They
404
+ * part only when a caller installs a replacement that **already holds** the
405
+ * earlier bytes of this message (`setBuffer`'s `carried` argument) — the shape
406
+ * an accumulating caller uses, where each new window opens after the bytes the
407
+ * previous one left behind in the same storage. Keeping the two apart is what
408
+ * lets that caller be an ordinary sink (§5.1.5) instead of a growth hook
409
+ * inside the encoder (§6.6).
410
+ */
411
+ private origin;
252
412
  private readonly flushSink;
253
- private readonly canGrow;
254
413
  private depth;
255
414
  /**
256
415
  * Ids of the innermost open sequences whose header has not been written yet
@@ -261,32 +420,85 @@ declare class OStream {
261
420
  * never buffer content, so a flush can never split a run.
262
421
  *
263
422
  * Storage plus an explicit count rather than `push`/`pop`, so the slots are
264
- * reused across messages (no allocation on a pooled encoder) and so
265
- * {@link OStream.commitPending} can zero the count *before* it writes.
423
+ * reused across messages and so {@link OStream.commitPending} can zero the
424
+ * count *before* it writes.
425
+ *
426
+ * **Fixed size, sized at construction** from `MAX_DEPTH` — the "fixed-size
427
+ * state whose size this document fixes" of CORELIB_PLAN §6.6.2, and the shape
428
+ * §6.0.1 asks for: the run covers the full depth, so it is always a contiguous
429
+ * suffix of the open sequences, every sequence stays canonical however deep it
430
+ * nests, and there is no eager-framing fallback. Nothing is allocated per
431
+ * message, per field or per sequence.
266
432
  *
267
- * The array grows on demand and is bounded only by `MAX_DEPTH` — there is no
268
- * fixed hold-back window and hence no eager-framing fallback, which is what
269
- * CORELIB_PLAN §6 ("How deep the hold-back reaches") demands of an
270
- * implementation that can allocate: canonical output at *every* depth.
433
+ * A plain array rather than an `Int32Array`: a 255-slot typed array is an
434
+ * external backing store on V8 (~3.1 µs to allocate on Node 24, against ~5 ns
435
+ * for this), and an encoder is constructed per message on the accumulator path.
436
+ * Slots are written before they are read, so it is never read holey.
271
437
  */
272
438
  private readonly pending;
273
439
  /** Valid entries in {@link OStream.pending}. */
274
440
  private nPending;
275
- private kernel;
276
- /** In-memory encoder backed by an auto-growing buffer. */
277
- constructor();
278
- /** Streaming encoder over a caller buffer, optionally draining to `flush`. */
441
+ /**
442
+ * How many buffer installations {@link OStream.setBuffer} has made. Only ever
443
+ * compared for equality across the flush callback, which is how
444
+ * {@link OStream.flush} tells "the sink copied and returned" from "the sink
445
+ * took the buffer and installed a replacement" — the one distinction §5.1
446
+ * rests the handover contract on, and the one that decides whether the start
447
+ * offset was consumed or re-armed.
448
+ */
449
+ private installs;
450
+ private readonly kernel;
451
+ /**
452
+ * Encoder over a caller buffer, optionally draining to `flush` as it fills.
453
+ *
454
+ * Those are the only two buffer models there are (CORELIB_PLAN §5.1.1): the
455
+ * buffer the caller hands over, and whatever a sink installs in its place
456
+ * (§5.1.5). There is no third parameter through which the encoder could be
457
+ * given something to enlarge — growing a destination from a write path is what
458
+ * §6.6's second violation row names, whoever owns the allocator.
459
+ *
460
+ * Constructing one is the only allocating step (§6.6): it sizes the hold-back
461
+ * run from `MAX_DEPTH` and reads the active kernel once. No `write*` call after
462
+ * that allocates anything.
463
+ */
279
464
  constructor(buffer: Uint8Array, offset?: number, flush?: FlushSink);
280
- /** Bytes currently held in the buffer (since construction or the last flush). */
465
+ /** Bytes of the message currently held in the buffer (see {@link bytes}). */
281
466
  get bytesUsed(): number;
282
467
  /**
283
- * The encoded message so far, as a view into the working buffer.
284
- * Meaningful for the in-memory mode; in streaming mode it is only the
285
- * not-yet-flushed tail. The view is valid until the next write.
468
+ * The encoded message so far, as a view into the working buffer: everything
469
+ * written since construction, the last {@link reset}, or the last flush the
470
+ * sink returned from without installing a buffer. With a flush sink that is
471
+ * normally only the not-yet-flushed tail; on a stream whose sink hands back a
472
+ * replacement carrying the earlier bytes — the accumulator `growingOStream`
473
+ * builds — it is the whole message. The view is valid until the next write.
286
474
  */
287
475
  bytes(): Uint8Array;
288
- /** Drain buffered bytes to the flush sink (no-op without one). */
476
+ /**
477
+ * Drain buffered bytes to the flush sink (no-op without one).
478
+ *
479
+ * A sink that returns **without** installing a buffer has copied what it was
480
+ * handed, so the encoder keeps writing into the same buffer — resuming at
481
+ * offset **0**. The start offset belongs to the *installation*, not to the
482
+ * buffer (CORELIB_PLAN §5.1): the buffer-set that armed it — the constructor
483
+ * or {@link OStream.setBuffer} — reserved room in the unit it began, and
484
+ * handing that unit over consumes the reservation. A sink that wants header
485
+ * room in *every* unit re-arms it by calling `setBuffer(buf, offset)` from
486
+ * inside the callback, a new installation like any other; a bare return must
487
+ * not do it implicitly, or the leading bytes would be capacity the rest of the
488
+ * stream could never use and the two shapes would be indistinguishable.
489
+ */
289
490
  flush(): void;
491
+ /**
492
+ * {@link flush}, plus the number of contiguous bytes the caller wants at the
493
+ * cursor afterwards — `0` when it wants none in particular.
494
+ *
495
+ * Split out from the public `flush` so the figure cannot be invented by a
496
+ * caller: it is the encoder's own reserve request, and a sink that sizes a
497
+ * replacement from it (`growingOStream`) has to be able to trust it. Everything
498
+ * else is identical, including that a sink which returns without installing has
499
+ * *copied* and the cursor resumes at `0`.
500
+ */
501
+ private drain;
290
502
  /**
291
503
  * Install a fresh output buffer to write into, mid-stream. Intended for the
292
504
  * streaming (flush-sink) mode: call it from inside your flush callback to hand
@@ -294,8 +506,36 @@ declare class OStream {
294
506
  * without interruption. `offset` reserves space at the front of the new
295
507
  * buffer. Any not-yet-flushed bytes in the old buffer are dropped, so
296
508
  * {@link flush} first (the flush callback fires before you swap).
509
+ *
510
+ * Every call is a **new installation**, and its `offset` applies to the unit
511
+ * it begins and is consumed when that unit is flushed (CORELIB_PLAN §5.1).
512
+ * Passing the buffer the encoder already has is an installation like any
513
+ * other: that is how a sink gets header room in *every* flushed unit — one
514
+ * framing header per packet — where returning bare would resume at `0`.
515
+ *
516
+ * On a stream that has a flush sink the new buffer must leave at least
517
+ * {@link MIN_OUTPUT_BUFFER} usable bytes (`buffer.length - offset`); a smaller
518
+ * one is rejected here, with {@link SofabErrorCode.Argument}, leaving the
519
+ * encoder on the buffer it already had. A sink-less stream has no minimum.
520
+ *
521
+ * `carried` says how many bytes **immediately before `offset`** the
522
+ * replacement already holds of *this* message — normally `0`, because a fresh
523
+ * buffer holds none. A caller that keeps the whole message in one growing
524
+ * store passes the length it copied across, so {@link bytes} keeps reporting
525
+ * the message rather than only the piece written after the swap. It changes
526
+ * nothing on the wire and nothing about flushing: the next flush still begins
527
+ * at `offset`. `growingOStream` is the caller this exists for; it is the
528
+ * §5.1.5 handover, not a growth hook inside the encoder (§6.6).
529
+ *
530
+ * The accumulating stream `growingOStream` builds is an ordinary streaming
531
+ * stream, so this works on it too, and means exactly what it means anywhere
532
+ * else: the not-yet-flushed bytes in the old buffer are dropped, encoding
533
+ * continues into yours, and its sink takes over growing *that* one from the
534
+ * next flush — reserve offset included, since what it copies out is the
535
+ * message rather than the buffer. Encode into a plain
536
+ * `new OStream(buffer, offset, flush?)` to keep the buffer yours instead.
297
537
  */
298
- setBuffer(buffer: Uint8Array, offset?: number): void;
538
+ setBuffer(buffer: Uint8Array, offset?: number, carried?: number): void;
299
539
  /**
300
540
  * Rewind the encoder to empty, reusing the existing buffer. Lets a caller pool
301
541
  * one OStream across many messages instead of allocating a fresh buffer per
@@ -306,21 +546,101 @@ declare class OStream {
306
546
  writeUnsigned(id: number, value: number | bigint): void;
307
547
  /** Write a signed integer field (zig-zag encoded). */
308
548
  writeSigned(id: number, value: number | bigint): void;
549
+ /**
550
+ * Write an unsigned 64-bit scalar from a {@link Long} — the `bigint`-free twin
551
+ * of {@link writeUnsigned}, and the scalar counterpart of
552
+ * {@link writeUnsignedArrayLong}. Produces the identical wire.
553
+ *
554
+ * There is no range check and no scratch round-trip: a `Long` *is* two 32-bit
555
+ * halves, so it is in the `uint64` domain by construction — which is the whole
556
+ * of what `splitU64` decides for a `number | bigint`. The halves go straight
557
+ * into the varint writer, so nothing is allocated per value. Nothing needs
558
+ * copying out ahead of `header` either, for the same reason the array writers
559
+ * do not: the halves come off a caller-owned immutable `Long`, not the shared
560
+ * scratch a re-entrant flush sink could overwrite.
561
+ */
562
+ writeUnsignedLong(id: number, value: Long): void;
563
+ /**
564
+ * Write a signed 64-bit scalar (zig-zag) from a {@link Long} — the
565
+ * `bigint`-free twin of {@link writeSigned}, and the scalar counterpart of
566
+ * {@link writeSignedArrayLong}. Zig-zag `(n << 1) ^ (n >> 63)` is computed on
567
+ * the lo/hi pair, so the varint goes out at its exact size (a fixed caller
568
+ * buffer must not see a 10-byte demand for a 2-byte field) and no `bigint` is
569
+ * created. A `Long` carries exactly 64 bits, so as in {@link writeUnsignedLong}
570
+ * there is nothing left to range-check.
571
+ */
572
+ writeSignedLong(id: number, value: Long): void;
309
573
  /** Write a boolean field (encoded as the unsigned value 0 or 1). */
310
574
  writeBoolean(id: number, value: boolean): void;
311
575
  /** Write an IEEE-754 32-bit float field. */
312
576
  writeFp32(id: number, value: number): void;
577
+ /**
578
+ * Write an fp32 field from its raw wire bits — the 4 little-endian payload
579
+ * bytes as one 32-bit word, which is exactly what {@link Visitor.fp32} delivers
580
+ * as `bits`.
581
+ *
582
+ * This is the re-encode half of the bit-exactness rule (CORELIB_PLAN §6.5). A JS
583
+ * `number` is a 64-bit double, and widening an fp32 **signaling** NaN into one
584
+ * quiets it, so re-encoding through {@link writeFp32} cannot reproduce such a
585
+ * payload; the bits go out verbatim here, so decode → re-encode is byte-for-byte
586
+ * for every fp32 value, sNaN included. §6.5 requires this path of every
587
+ * double-only target, and names it: "a 32-bit bits accessor".
588
+ */
589
+ writeFp32Bits(id: number, bits: number): void;
313
590
  /** Write an IEEE-754 64-bit double field. */
314
591
  writeFp64(id: number, value: number): void;
315
592
  /** Write a UTF-8 string field. */
316
593
  writeString(id: number, text: string): void;
317
594
  /** Write a blob (arbitrary bytes) field. */
318
595
  writeBlob(id: number, data: Uint8Array): void;
319
- /** Write a fixed-length field of the given subtype from raw bytes. */
596
+ /**
597
+ * Write a fixed-length field of the given subtype from raw bytes.
598
+ *
599
+ * This is the byte-level entry point — the one writer that takes the subtype
600
+ * from the caller rather than picking it — so the payload is checked
601
+ * **against that subtype** before a byte is written, and it cannot emit a
602
+ * `fixlen_word` a conformant decoder must reject (`ARGUMENT`, §6.3):
603
+ *
604
+ * * subtypes `0x4`–`0x7` are **reserved** — a decoder must treat a field
605
+ * carrying one as malformed (`INVALID`, §4.6/§5.2);
606
+ * * `Fp32` / `Fp64` payloads are **exactly** 4 / 8 bytes — any other declared
607
+ * length for those subtypes is malformed, rejected the moment the word is
608
+ * read (§4.6);
609
+ * * `String` / `Blob` take any length up to `FIXLEN_MAX`.
610
+ *
611
+ * The typed writers ({@link writeFp32}, {@link writeFp64},
612
+ * {@link writeString}) are correct by construction and go straight to the
613
+ * header; only {@link writeBlob}, whose subtype is unconstrained anyway,
614
+ * shares this path.
615
+ */
320
616
  writeFixlen(id: number, data: Uint8Array, subtype: FixlenSubtype): void;
321
- /** Write an array of unsigned integers (each a varint). */
617
+ /**
618
+ * Write an array of unsigned integers (each a varint).
619
+ *
620
+ * **The bulk kernel writes a whole array in one pass and cannot flush**, so it
621
+ * runs only where everything is known to fit. Three cases, and none of them
622
+ * asks the source how wide its elements are:
623
+ *
624
+ * * **block mode** (no sink) — the buffer is meant to hold the whole message,
625
+ * so the kernel always runs and the buffer's length is the bound, checked
626
+ * after the fact. A message that does not fit is `BUFFER_FULL`, which is
627
+ * precisely this mode's answer.
628
+ * * **a stream with room** — `VARINT_MAX_BYTES` per element is the true
629
+ * worst case for any 64-bit value, and a *growing* stream always satisfies
630
+ * it because it grows to whatever is asked.
631
+ * * **a chunk too small for that** — fill it to the last byte, hand it over,
632
+ * carry on, splitting an element where it falls. That is not a fallback but
633
+ * the mode's contract: `MIN_OUTPUT_BUFFER` is 1.
634
+ *
635
+ * What is gone is the fourth case, which asked `values.constructor` for a
636
+ * narrower bound so the kernel would run on a tightly-sized chunk. `constructor`
637
+ * is an ordinary property, an `ArrayLike` can claim any width, and a wrong
638
+ * answer silently truncated the message — §5.1's "partial output handed back as
639
+ * complete". The block mode now reaches the kernel without needing the number
640
+ * at all, which is where a tightly-sized buffer actually lives.
641
+ */
322
642
  writeUnsignedArray(id: number, values: ArrayLike<number | bigint>): void;
323
- /** Write an array of signed integers (each zig-zag + varint). */
643
+ /** Write an array of signed integers (each zig-zag + varint). See {@link writeUnsignedArray}. */
324
644
  writeSignedArray(id: number, values: ArrayLike<number | bigint>): void;
325
645
  /**
326
646
  * Write an unsigned 64-bit array from {@link Long}[] — the `bigint`-free path.
@@ -399,10 +719,93 @@ declare class OStream {
399
719
  * the reverse silently changes an array's length.
400
720
  */
401
721
  writeSequenceEndKeep(): void;
402
- /** Ensure exactly `value`'s varint size, then write it (bigint path). */
403
- private putVarint;
404
722
  /** Ensure exactly `value`'s varint size, then write it (number fast path). */
405
723
  private putVarintNum;
724
+ /**
725
+ * The multi-byte / needs-room tail of {@link putVarintNum}, kept in its own
726
+ * method so the single-byte test above stays small enough for the JIT to
727
+ * inline into every `header` / `fixlenHead` / `arrayHead` call site.
728
+ */
729
+ private putVarintNumSlow;
730
+ /**
731
+ * Write a 64-bit value, held as two 32-bit halves, as a varint.
732
+ *
733
+ * Deliberately just a bounds check and two calls: this is what the array
734
+ * loops call per element, and it only pays its way while it is small enough
735
+ * for the JIT to inline. Spelling the drain case out here instead — three
736
+ * more lines — cost 16-19% on an array streamed through a 32/64-byte buffer,
737
+ * where most elements take the fast path and want it inlined.
738
+ */
739
+ private putVarintLoHi;
740
+ /**
741
+ * A full buffer that is nonetheless wide enough to hold any varint: drain it
742
+ * and retry the worst case, which is exactly what `ensure(VARINT_MAX_BYTES)`
743
+ * did before §5.1 and is still the common streaming case. Sizing the value
744
+ * first instead — the narrow-buffer path in {@link putVarintLoHiTight} — cost
745
+ * +125 instructions on every element of an array streamed through a
746
+ * one-element-wide buffer.
747
+ */
748
+ private putVarintLoHiSlow;
749
+ /**
750
+ * Zig-zag {@link putVarintLoHi}: `(n << 1) ^ (n >> 63)` on the halves. Keeps
751
+ * its own worst-case fast path so the signed array loop reaches the combined
752
+ * zig-zag-and-encode writer directly, exactly as it did before; the drain and
753
+ * narrow-buffer cases are the same for both signs, so they are shared.
754
+ */
755
+ private putZigzagVarintLoHi;
756
+ /**
757
+ * The narrow-buffer tail of {@link putVarintLoHi}: the buffer could not hold a
758
+ * worst-case varint even empty. Sizing the value exactly keeps such a buffer
759
+ * from flushing for room it does not need; only when it cannot hold the varint
760
+ * *at all* does the value get split across flushes, seven bits at a time.
761
+ */
762
+ private putVarintLoHiTight;
763
+ /**
764
+ * Write the 4 little-endian bytes of an fp32 (§4.6).
765
+ *
766
+ * Through the shared scratch, not a `DataView` over the buffer: building that
767
+ * handle costs ~129 ns against the ~2 ns it saves on one value (§6.6.2 allows the
768
+ * handle, arithmetic forbids it here). The bulk array path amortizes one over the
769
+ * whole run instead — see the kernel.
770
+ */
771
+ private putFp32;
772
+ private putFp32Slow;
773
+ /**
774
+ * Write the 4 little-endian bytes of an fp32 held as a 32-bit word (§4.6) — the
775
+ * path {@link OStream.writeFp32Bits} takes, and the tail of
776
+ * {@link OStream.putFp32Slow}.
777
+ */
778
+ private putFp32Bits;
779
+ /**
780
+ * {@link writeFp32Array} for a `Float32Array` source: its own 32-bit words go
781
+ * out, never a `number`, which is what keeps a signaling NaN intact (§6.5) —
782
+ * on the bulk path (the kernel copies words too) and on the streamed one alike,
783
+ * so a small buffer produces the one-shot bytes (§5.1.4).
784
+ *
785
+ * Kept to the header and the bulk call, the streaming loop in its own method:
786
+ * with the loop in this body an 8-element one-shot array cost +416 Ir/op
787
+ * (+8.8%) over the pre-#185 encoder, split like this +1.1% (Callgrind).
788
+ */
789
+ private writeFp32Words;
790
+ /**
791
+ * The streamed half of {@link writeFp32Words}: one `Uint32Array` over the
792
+ * source per call, the same handle the bulk kernel takes (§6.6.2); reading it gives each word's value whatever the
793
+ * host byte order, and the shifts below store it little-endian. The words go
794
+ * out one run per stretch of free buffer, with `buf`/`pos` in locals, and the
795
+ * split points are {@link putFp32}'s: drain when fewer than 4 bytes are free,
796
+ * split an element byte by byte only when the buffer itself is narrower than 4.
797
+ */
798
+ private putFp32Words;
799
+ /** Write the 8 little-endian bytes of an fp64 (§4.6) — see {@link putFp32}. */
800
+ private putFp64;
801
+ private putFp64Slow;
802
+ /**
803
+ * Append one byte, draining to the sink first when the buffer is full.
804
+ *
805
+ * @internal Public only because {@link utf8WriteSink} writes through it — the
806
+ * narrow-buffer string path (§5.1.3). Not part of the field-writing API.
807
+ */
808
+ putByte(b: number): void;
406
809
  /**
407
810
  * Write a field header, the `(id << 3) | wireType` tag, as a varint.
408
811
  *
@@ -431,468 +834,1274 @@ declare class OStream {
431
834
  private commitPending;
432
835
  private fixlenHead;
433
836
  private arrayHead;
434
- /** Copy `data` out, flushing/growing as needed (large payloads stay chunked). */
837
+ /**
838
+ * Copy `data` out, flushing/growing as needed (large payloads stay chunked).
839
+ *
840
+ * The whole-payload case — the common one, and the only one on a buffer sized
841
+ * from `MAX_SIZE` or on the accumulator — is a single `set` of the caller's
842
+ * array: a `memcpy`, and no view at all.
843
+ *
844
+ * **A payload split across flushes takes a per-piece view: a language-forced
845
+ * handle under CORELIB_PLAN §6.6.2.** `TypedArray.set` is the only `memcpy`
846
+ * this language exposes and it takes a *typed array* as its source, so copying a
847
+ * *range* of one needs a `subarray` — §6.6.2's "the only way to name a region of
848
+ * the caller's buffer is a wrapper over it". It has the two properties that
849
+ * clause requires: it carries no message bytes (the storage is the caller's, on
850
+ * both ends) and no wire number sizes it (a handle over a thousand bytes costs
851
+ * what a handle over ten costs). The allocation-free alternative is a byte loop,
852
+ * measured at 358 MB/s against 10,963 MB/s for `set`.
853
+ *
854
+ * It never leaves this method: `set` consumes it and no caller can reach it, so
855
+ * §6.7's ban on exposing a value that outlives its callback is untouched — and so
856
+ * is §5.1.6, which is why the copy happens at all rather than the payload being
857
+ * handed to the sink. §6.6.2 asks the port to make such handles visible rather
858
+ * than invisible: the README itemises it (§9.6) and `heap-free-codec.test.ts`
859
+ * pins its count and kind.
860
+ */
435
861
  private writeRaw;
436
- /** Ensure `n` contiguous bytes are free at `pos`; returns `pos` for chaining. */
862
+ /**
863
+ * Make room for `n` contiguous bytes at `pos` if the buffer can hold them at
864
+ * all: `true` when it can (flushing or growing as needed), `false` when a
865
+ * fixed caller buffer is simply smaller than `n` and the value must be split
866
+ * across flushes instead — CORELIB_PLAN §5.1 puts the floor on the output
867
+ * buffer at a single byte, so no write may demand a contiguous run.
868
+ *
869
+ * The one case that still fails is a buffer with no sink to drain to: there is
870
+ * nowhere for a split to put the earlier bytes, so it reports BufferFull here,
871
+ * before anything is written, exactly as {@link ensure} did.
872
+ */
873
+ private tryEnsure;
874
+ /**
875
+ * Reserve `n` contiguous bytes for a bulk write — the whole payload of an
876
+ * array or a string, written in one pass into a buffer that cannot move under
877
+ * it. Every caller has an element-at-a-time route to fall back on when this
878
+ * says no, producing the identical bytes, so a `false` here is never an error.
879
+ *
880
+ * The room already at the cursor counts on **any** buffer, which is what makes
881
+ * the one-shot `new OStream(buf)` case — a caller buffer sized from the
882
+ * schema's `MAX_SIZE`, the shape CORELIB_PLAN §5.1 puts first — the fast one:
883
+ * without it a message encoded into a caller's own buffer took `TextEncoder`
884
+ * for every string and the element loop for every array, measured at 1.9 µs
885
+ * against 0.16 µs for the same five-field message.
886
+ *
887
+ * Beyond that room only a **sink** may be asked, and only by draining. Emptying
888
+ * the buffer is the one thing that can produce room without an allocation
889
+ * anywhere (§6.6), and it is legal wherever this is called — every caller has
890
+ * just written a complete header, so the cursor is on a boundary between atomic
891
+ * units (§5.1.3). A sink that installs a larger replacement (§5.1.5) therefore
892
+ * re-opens the bulk route by itself, which is how the accumulating helper keeps
893
+ * it. Nothing is *demanded*: a `false` sends the caller down its
894
+ * element-at-a-time route, which produces the identical bytes, so a fixed
895
+ * buffer too narrow for the worst case never turns into a spurious
896
+ * `BUFFER_FULL`.
897
+ */
898
+ private reserveBulk;
899
+ /**
900
+ * Commit the position a bulk kernel returned, in the **block** mode.
901
+ *
902
+ * Nothing reserves room in front of that kernel, and nothing could: the bytes an
903
+ * array takes are only known once it is encoded, and the estimate that used to
904
+ * stand in — the source's own `constructor`, which any object can set — silently
905
+ * truncated the message when it was wrong (§5.1). Here the bound is the buffer's
906
+ * own length, applied afterwards. The varint kernels stop writing at
907
+ * `out.length` and keep counting (see {@link Kernel}), so a `pos` past the end
908
+ * is the exact shortfall and nothing was written outside the buffer.
909
+ *
910
+ * This is the block mode's error and only its: the buffer is meant to hold the
911
+ * whole message, so one that does not fit is exactly what `BUFFER_FULL` means.
912
+ * The streaming mode never gets here, and `BUFFER_FULL` is unreachable there by
913
+ * contract.
914
+ */
915
+ private bulkEnd;
916
+ /**
917
+ * Ensure `n` contiguous bytes are free at `pos`; returns `pos` for chaining.
918
+ *
919
+ * The only remaining caller is the one-byte sequence-end marker, which is
920
+ * indivisible: there is no smaller piece to split it into, so a buffer that
921
+ * cannot take it has nothing left to report but `BUFFER_FULL`.
922
+ */
437
923
  private ensure;
438
924
  /** Ensure *some* room (up to `want`); returns how many bytes are available. */
439
925
  private ensureSome;
440
- private growTo;
441
926
  }
442
927
 
443
928
  /**
444
- * Opt-in decode resource limits (corelib-ts#38).
929
+ * `growingOStream` — the caller that lets the buffer follow the message.
930
+ *
931
+ * **This is the static helper layer, not the codec** (CORELIB_PLAN §6.6.1). The
932
+ * codec is {@link OStream}: it writes into the buffer it was handed and has no
933
+ * way to enlarge one. Where the schema bounds the message, generated code sizes
934
+ * one buffer from `MAX_SIZE` and a plain `new OStream(buf)` is the whole story.
935
+ * Where it does not, §5.1.2 names exactly one shape for the unbounded case:
936
+ *
937
+ * > Install a scratch buffer **with a sink** that appends into the growing
938
+ * > result; the scratch is subject to `MIN_OUTPUT_BUFFER` like any
939
+ * > sink-installed buffer.
940
+ *
941
+ * That is what this file builds, so neither generated code nor a hand-written
942
+ * one-shot encode has to write it again. The scratch and the result are the same
943
+ * storage: the encoder is installed over the free tail of a buffer this module
944
+ * owns, and when it fills, the flush callback fires, this module enlarges its
945
+ * storage and installs the next tail with {@link OStream.setBuffer} — §5.1.5's
946
+ * taking sink, and §6.6.1's second row, a helper reached from inside a callback
947
+ * the codec made. Because the encoder was writing *into the result all along*,
948
+ * absorbing a flush is one assignment: there is no per-flush copy.
949
+ *
950
+ * The predecessor was a `BufferOwner` hook: the encoder called it from
951
+ * `ensureSome`, i.e. from every `write*`, and kept the buffer it returned. That
952
+ * is §6.6's second violation row — "the codec allocates nothing itself but
953
+ * requires a growable destination and grows it … it moved the allocator call one
954
+ * type away, where a source-level audit no longer sees it" — and the hook is gone
955
+ * with it (A2-0159).
956
+ *
957
+ * **No subclass, deliberately.** An `OStream` subclass overriding `bytes()` was
958
+ * the obvious shape and cost 2.3x on `encode: composite`: a program that uses
959
+ * both `growingOStream()` and `new OStream(buf)` would then have two receiver
960
+ * maps at every `os.write*` call site, and V8 stops inlining a polymorphic one.
961
+ * `setBuffer`'s `carried` argument buys the same behaviour with one map and one
962
+ * extra number in the encoder, and a stateless module-level sink keeps the
963
+ * per-stream cost at exactly what it was: one buffer.
964
+ */
965
+
966
+ /**
967
+ * An {@link OStream} whose **buffer follows the message** — the ready-made form
968
+ * of the caller CORELIB_PLAN §5.1.2 puts the allocation in.
969
+ *
970
+ * It is the one-liner for the 90% case, where the message comfortably fits in
971
+ * memory:
972
+ *
973
+ * ```ts
974
+ * const os = growingOStream();
975
+ * os.writeUnsigned(1, 42);
976
+ * const wire = os.bytes(); // the whole message, as a view valid until the next write
977
+ * ```
978
+ *
979
+ * {@link OStream.bytes} is therefore the **whole** message here rather than a
980
+ * not-yet-flushed tail, and no write reports `BUFFER_FULL`. It is an ordinary
981
+ * streaming stream in every other respect: {@link OStream.setBuffer} works and
982
+ * means what it always means — the not-yet-flushed bytes in the old buffer are
983
+ * dropped and encoding continues into yours, which the accumulator then grows in
984
+ * turn.
985
+ *
986
+ * @param initialCapacity bytes to start from; the accumulator enlarges itself
987
+ * whenever the message outgrows it, so this only trades an initial allocation
988
+ * against the number of enlargements and never limits the message. A caller with
989
+ * a rough size in hand should pass it: 100 KB of *small fields* grows by doubling
990
+ * and costs nine enlargements from the 256-byte default. A single large field
991
+ * does not — a bulk write hands the sink its `needed`, so the buffer reaches that
992
+ * size in one step and the write keeps its bulk route.
993
+ */
994
+ declare function growingOStream(initialCapacity?: number): OStream;
995
+
996
+ /**
997
+ * The SofaBuffers decoder: `IStream`, and the visitor it drives.
998
+ *
999
+ * The **visitor is the only decode surface** (CORELIB_PLAN §5.3.1). There is no
1000
+ * pull parser, no iterator, no cursor and no convenience wrapper that decodes by
1001
+ * another route: a second surface is a second implementation of every rule in the
1002
+ * spec, and the divergences that produces are invisible to the shared vectors,
1003
+ * which exercise whichever surface the driver happened to pick.
1004
+ *
1005
+ * `IStream` is a push parser: bind a {@link Visitor} at construction, feed bytes
1006
+ * with {@link IStream.feed}, and it calls one method per decoded field. It is a
1007
+ * resumable state machine, so the chunks can be any size — a whole message, a
1008
+ * network packet, or a single byte — and a field that straddles a chunk boundary
1009
+ * is picked up seamlessly on the next call.
1010
+ *
1011
+ * The visitor is **flat**: one object receives the whole message, and nesting is
1012
+ * reported as {@link Visitor.sequenceBegin} / {@link Visitor.sequenceEnd} events
1013
+ * carrying the sequence's id and depth. A visitor per nested scope would make
1014
+ * every dispatch site here megamorphic — one hidden class per generated message
1015
+ * class in the tree — and would put a per-scope object on the decode path; one
1016
+ * flat visitor keeps the call sites (bi)morphic and the decoder allocation-free
1017
+ * (§6.6). Descending and skipping are unchanged by that choice: `sequenceBegin`
1018
+ * answers `false` to decline a subtree whole, and a field whose callback the
1019
+ * visitor does not implement is skipped.
1020
+ *
1021
+ * There is no finish / finalize step (§5.2.4) and no status accessor beside the
1022
+ * call: {@link IStream.feed} *returns* the outcome for the bytes consumed so far,
1023
+ * and that return value is the whole answer. A message that merely ends inside a
1024
+ * field is reported — never thrown — as {@link DecodeStatus.Incomplete}; only a
1025
+ * *malformed* message throws ({@link SofabErrorCode.InvalidMsg}), which is this
1026
+ * port's channel for {@link DecodeStatus.Invalid}. The caller owns end-of-input
1027
+ * and decides whether a trailing `Incomplete` is a truncation error.
1028
+ *
1029
+ * **One fact, one channel.** Each outcome leaves by exactly one route — two of
1030
+ * the three by the return value, the refusals by the throw — because a second way
1031
+ * to ask the same question is a second thing to keep in step, and this family
1032
+ * shipped the drift: a `status()` accessor answered `COMPLETE` for a message
1033
+ * `feed` had already refused. §5.3.1 makes the general form of the argument for
1034
+ * decode surfaces ("every additional surface is a second implementation of every
1035
+ * rule in this document"); the accessor was the same mistake one size down.
1036
+ */
1037
+
1038
+ /**
1039
+ * Where an array's elements are written when the visitor takes the **bulk
1040
+ * hand-off** ({@link Visitor.arrayBulk}): the destination it already owns, handed
1041
+ * over once, instead of one callback per element.
1042
+ *
1043
+ * **Why this replaced the per-element callbacks.** There used to be an
1044
+ * `arrayUnsigned` / `arraySigned` / `arrayFp32` / `arrayFp64` beside this, one call
1045
+ * per element. Measured with `bench/run_callgrind.sh`'s method over 1000-element
1046
+ * arrays (Ir/op for a message that is one array), against this hand-off filling
1047
+ * the same destination:
1048
+ *
1049
+ * | array | per element (removed) | hand-off | |
1050
+ * |---|---:|---:|---:|
1051
+ * | `array<u16>` into a `number[]` | 235 433 | 184 025 | −21.8% |
1052
+ * | `array<u64>` into a `Long[]` | 576 206 | 429 113 | −25.5% |
1053
+ * | `array<u64>` into {@link IntegerArrayTarget.lo}/`hi` | 498 618 | 299 180 | −40.0% |
1054
+ * | `array<fp64>` into a `Float64Array` | 93 520 | 36 890 | −60.6% |
1055
+ * | `array<fp32>` into a `Float32Array` | 94 497 | 35 259 | −62.7% |
1056
+ *
1057
+ * Floats gain most because their *reading* was already bulk (the §6.6.2 handle in
1058
+ * the element drain), so the callback was very nearly all that was left.
1059
+ *
1060
+ * **A short array is not the exception.** The fixed cost — the offer, the
1061
+ * target's resolution, the bound's validation — is about 600 Ir, but the hand-off
1062
+ * takes a whole array in one call, tail elements included, so it is paid once:
1063
+ * four elements at the end of a 37-byte message cost 578 Ir against the 563 the
1064
+ * removed callbacks cost, and everything longer is the table above.
1065
+ *
1066
+ * **What did move is the consumer's side, for a consumer that folds.** An element
1067
+ * callback let a reader sum, hash or convert *inside* the delivery; reading a
1068
+ * filled destination is a second pass. A reader that wants the values where they
1069
+ * are — which is what generated code wants — has no second pass and gets the
1070
+ * table. A reader that folds pays one: the same four-element array costs 1 063 Ir
1071
+ * if it sums the destination at `arrayEnd`, against 563 folding per element.
1072
+ * It is a cheaper pass than the calls it replaced on any array long enough to
1073
+ * matter, and on a four-element one it is not.
1074
+ *
1075
+ * **Declining costs nothing at all.** An array no visitor takes is walked over
1076
+ * rather than decoded: `array<fp64>` 36 890 → 9 383 Ir/op, `array<u16>` 184 025 →
1077
+ * 152 082.
1078
+ *
1079
+ * **This is how array elements are delivered — the only way.** There is no
1080
+ * callback per element beside it: §5.3.1 allows a rule one implementation, and
1081
+ * two delivery routes for the same elements were two places for the element bound
1082
+ * to be compared, two resume paths to keep in step, and a standing invitation for
1083
+ * generated code to take the slower one. `arrayBegin` and `arrayEnd` still fire
1084
+ * for every array; returning `null` declines delivery, and the elements are then
1085
+ * walked over without being decoded into existence — the `skip` half of §6.7.2's
1086
+ * two intents, which is what a visitor that declares no `arrayBulk` gets for
1087
+ * every array.
1088
+ *
1089
+ * **The destination is filled ascending from index 0**, one write per element,
1090
+ * and it must stay valid — same object, same length — until `arrayEnd`, which for
1091
+ * a chunked decode is several {@link IStream.feed} calls later. A plain-array
1092
+ * destination (`values` / `longs`) is **cut to the elements written when the array
1093
+ * ends** — including to zero for an array that is empty on the wire, and to the
1094
+ * prefix when an element is refused — so reusing one across arrays or messages can
1095
+ * never leave the previous array's tail behind, and after `arrayEnd` its `length`
1096
+ * is exactly this array's element count. (Cutting at the end rather than emptying
1097
+ * up front is measured: emptying first makes every element write a grow, which
1098
+ * cost `array<u16>` 181 583 → 231 815 Ir/op.) A typed destination is neither cut
1099
+ * nor emptied — it cannot be — and must already hold `count` elements.
1100
+ *
1101
+ * A decode that fails *inside* an array for any other reason — malformed bytes, or
1102
+ * input that simply ends — leaves the destination holding what had been written
1103
+ * when it stopped. `length` is a statement about the array only once `arrayEnd`
1104
+ * has been raised or an element has been refused. This is the one
1105
+ * place the codec holds a reference to the caller's storage between calls (§6.6
1106
+ * otherwise holds nothing past a callback), and the reference is dropped at
1107
+ * `arrayEnd` and whenever a pooled machine is released.
1108
+ *
1109
+ * **A refused element leaves the destination holding the prefix**: everything
1110
+ * before it is written, the offending element and everything after it is not, and
1111
+ * a plain-array destination is cut to exactly that length. Like every `INVALID`
1112
+ * verdict it is terminal (§5.2.1).
1113
+ */
1114
+ type ArrayTarget = IntegerArrayTarget | FloatArrayTarget | BoolArrayTarget;
1115
+ /**
1116
+ * A `boolean` array's destination: one byte per element, `0` or `1`.
1117
+ *
1118
+ * It carries **no bound**, and that is the whole reason it is a shape of its own
1119
+ * rather than a {@link IntegerArrayTarget.typed} `Uint8Array`. §4.4 gives a boolean
1120
+ * no width bound — every non-zero wire value is `true` — so there is no interval to
1121
+ * state and nothing for the width check to compare. A raw store would be wrong
1122
+ * twice over: 256 would mask to `0` and turn `true` into `false`. The decoder
1123
+ * NORMALIZES instead, writing `1` for any non-zero, which is also the only value
1124
+ * §4.4 lets an encoder emit back — so a filled destination re-encodes canonically
1125
+ * with no conversion step in between.
1126
+ */
1127
+ interface BoolArrayTarget {
1128
+ bool: Uint8Array;
1129
+ }
1130
+ /**
1131
+ * An integer array's destination and the **element bound** to enforce while
1132
+ * filling it — for an `ArrayKind.Unsigned` or `ArrayKind.Signed` array.
1133
+ *
1134
+ * Exactly one destination must be set (`values`, `longs`, or `lo` *and* `hi`);
1135
+ * any other combination is a caller mistake and is refused with
1136
+ * {@link SofabErrorCode.Argument} before a single element is written.
1137
+ *
1138
+ * **The bound is four halves, and it is never optional.** The interval is the
1139
+ * schema's — `0..65535` for a `u16`, `-2^63..2^63-1` for an `i64` — so its
1140
+ * violation is `INVALID`, a statement about the message (§6.2.1 keeps a
1141
+ * *receiver* cap off a field the schema already bounds; a receiver cap on the
1142
+ * element *count* is compared by the visitor in `arrayBegin`, as before, and this
1143
+ * hand-off happens after it).
445
1144
  *
446
- * SofaBuffers' `count` (arrays) and `maxlen` (strings / blobs) schema bounds are
447
- * optional; when a schema omits them the decoder accepts whatever count / length
448
- * the received message claims, with no upper bound. That leaves a receiver no way
449
- * to cap memory against a hostile, oversized field. {@link DecodeLimits} is that
450
- * cap: an optional options object accepted by every decode entry point
451
- * ({@link decode}, the {@link IStream} constructor, and the {@link Cursor}
452
- * constructor).
1145
+ * Halves rather than a `number` pair because a `number` cannot express the
1146
+ * 64-bit domain: `u64`'s bound is `2^64-1` and `Number.MAX_SAFE_INTEGER` is
1147
+ * `2^53-1`, so a `number` bound both *rejects valid messages* at the top of the
1148
+ * range and makes the comparison a mixed `bigint`/`number` one, which measured
1149
+ * +35% on `array<u64>` — worse than the per-element path it replaces. Two
1150
+ * unsigned 32-bit comparisons are exact and cost nothing.
453
1151
  *
454
- * The limits are a **receiver-side policy**, not part of the wire format or the
455
- * message schema. The normative source of the values is the sofabgen config
456
- * (see sofa-buffers/generator#102): the generator bakes them into generated code
457
- * as constants and passes them here at decoder construction. This corelib only
458
- * provides the mechanism — **an omitted limit means no cap (today's behavior);
459
- * there is no corelib-side default.**
1152
+ * For a **signed** array the halves are the two's-complement ones (an `i16`
1153
+ * minimum of `-32768` is `minLo = 0xffff8000`, `minHi = 0xffffffff`).
1154
+ * {@link Long.fromBigInt} is the ready-made way to compute them from the schema's
1155
+ * bound once.
460
1156
  *
461
- * Enforcement happens at header time — where the count / length is first decoded,
462
- * before any array is sized or any payload is accepted or streamed — so a claimed
463
- * oversize is rejected even if the payload never arrives. Exceeding a limit is
464
- * never clamped or truncated: it throws a {@link SofabError} with code
465
- * {@link SofabErrorCode.LimitExceeded}, which is deliberately distinct from
466
- * `InvalidMsg` (policy, not malformation).
1157
+ * The codec **compares** the bound; it never owns one (§6.2.1). There is no
1158
+ * default and no "unbounded" spelling, because an integer element always has a
1159
+ * declared width: the widest `u64` interval is `0 .. 0xffffffff_ffffffff` and is
1160
+ * stated as such.
467
1161
  */
468
- interface DecodeLimits {
1162
+ interface IntegerArrayTarget {
1163
+ /**
1164
+ * Number-first destination: each element as the `value` its per-element
1165
+ * callback would have received — a `number` when it fits exactly (`≤ 2^53-1`,
1166
+ * every `u8`..`u32` and small 64-bit values) and a `bigint` beyond that. The
1167
+ * right destination for `u8`..`u32` / `i8`..`i32` arrays, where no `bigint` is
1168
+ * ever built.
1169
+ *
1170
+ * A real `Array` — grown as it fills and cut to length when the array ends, so
1171
+ * it need not be pre-sized and never carries a previous array's tail. A typed
1172
+ * array here is refused with {@link SofabErrorCode.Argument}: it would take the
1173
+ * writes and silently drop everything past its own length.
1174
+ */
1175
+ values?: (number | bigint)[];
469
1176
  /**
470
- * Reject a dynamic (`u*` / `i*`, `fp32` / `fp64`) array whose element `count`
471
- * exceeds this, before the array is materialized. Omit for no cap.
1177
+ * Destination taking each element as a {@link Long} — no `bigint` is
1178
+ * materialised at all, which is most of why this is a quarter cheaper than the
1179
+ * per-element path for a 64-bit array (see {@link ArrayTarget}). A real
1180
+ * `Array`, cut to length at the array's end, exactly like {@link values}.
472
1181
  */
473
- maxArrayCount?: number;
1182
+ longs?: Long[];
474
1183
  /**
475
- * Reject a UTF-8 string whose declared byte length exceeds this, before the
476
- * payload is decoded or streamed. Omit for no cap.
1184
+ * Allocation-free destination: the element's low half at `lo[index]` and its
1185
+ * high half at `hi[index]` (for a signed array, the two's-complement halves).
1186
+ * Both must be set and both must hold at least `count` elements. The fastest
1187
+ * shape there is — nothing is allocated per element, not even a `Long`.
477
1188
  */
478
- maxStringLen?: number;
479
1189
  /**
480
- * Reject a blob whose declared byte length exceeds this, before the payload is
481
- * accepted or streamed. Omit for no cap.
1190
+ * Exact-width destination: one typed array whose element width IS the schema's
1191
+ * declared width — a `Uint16Array` for a `u16` array, an `Int8Array` for an
1192
+ * `i8` one. Must hold at least `count` elements.
1193
+ *
1194
+ * **The bound is still compared, and that is not negotiable.** A typed array
1195
+ * *masks* on store (`a[0] = 70000` in a `Uint16Array` is 4464), and
1196
+ * MESSAGE_SPEC §7.1 makes an element outside the declared width INVALID —
1197
+ * neither masked to the width nor kept. So this destination buys the storage,
1198
+ * never the verdict: the fill loop compares exactly as {@link values} does and
1199
+ * refuses the same elements.
1200
+ *
1201
+ * What it does buy is the store (unboxed, and no element-kind transition the
1202
+ * way a `number[]` takes when a value leaves the small-integer range), the
1203
+ * memory (2 bytes for a `u16`, against a tagged slot), and the *encoder's*
1204
+ * side, where the width is then statically known.
1205
+ *
1206
+ * The bound must fit the array's own width: a destination narrower than the
1207
+ * interval could not represent every legal element, and is refused with
1208
+ * {@link SofabErrorCode.Argument} rather than silently masking.
482
1209
  */
483
- maxBlobLen?: number;
1210
+ typed?: Uint8Array | Uint16Array | Uint32Array | Int8Array | Int16Array | Int32Array | BigUint64Array | BigInt64Array;
1211
+ lo?: Uint32Array;
1212
+ /** The high halves; see {@link lo}. */
1213
+ hi?: Uint32Array;
1214
+ /** Low half of the smallest element the schema allows. */
1215
+ minLo: number;
1216
+ /** High half of the smallest element the schema allows. */
1217
+ minHi: number;
1218
+ /** Low half of the largest element the schema allows. */
1219
+ maxLo: number;
1220
+ /** High half of the largest element the schema allows. */
1221
+ maxHi: number;
484
1222
  }
485
-
486
1223
  /**
487
- * The SofaBuffers decoder.
1224
+ * A float array's destination — for an `ArrayKind.Fp32` or `ArrayKind.Fp64`
1225
+ * array. Exactly one of the two must be set, it must match the element width the
1226
+ * array's own `fixlen_word` declared (§4.8), and it must hold at least `count`
1227
+ * elements; anything else is refused with {@link SofabErrorCode.Argument} before
1228
+ * a single element is written.
488
1229
  *
489
- * `IStream` is a push parser: feed it bytes with {@link IStream.feed} and it
490
- * drives a {@link Visitor}, calling one method per decoded field. It is a
491
- * resumable state machine, so the chunks you feed can be any size — a whole
492
- * message, a network packet, or a single byte — and a field that straddles a
493
- * chunk boundary is picked up seamlessly on the next call.
1230
+ * There is no bound here: §4.6 gives a float no schema interval to violate, so
1231
+ * there is nothing to compare and nothing for the caller to state.
494
1232
  *
495
- * Nesting is hierarchical: {@link Visitor.sequenceBegin} may return a child
496
- * visitor, and the decoder routes the nested fields to it until the matching
497
- * end. Generated message classes use this directly — a class implements
498
- * `Visitor`, and a nested-message field returns the child instance.
499
- *
500
- * There is no finish / finalize step (MESSAGE_SPEC §7): {@link IStream.feed}
501
- * throws only for a *malformed* message ({@link SofabErrorCode.InvalidMsg}); a
502
- * message that merely ends inside a field is reported — never thrown — by
503
- * {@link IStream.end}, which returns {@link DecodeStatus.Incomplete} rather than
504
- * {@link DecodeStatus.Complete}. The caller owns end-of-input and decides
505
- * whether a trailing `Incomplete` is a truncation error.
1233
+ * An `fp32` array chooses between the two: {@link f32} takes values, {@link bits}
1234
+ * takes the wire words. The choice is real, not stylistic — reading an `fp32`
1235
+ * through a double quiets a *signaling* NaN (`0x7fa00001` comes back
1236
+ * `0x7fe00001`), so a reader that must reproduce `fp32` payloads bit-for-bit
1237
+ * (§4.6/§6.5) takes {@link bits}. `fp64` needs no such choice: a `Float64Array`
1238
+ * carries all 64 bits, payload NaNs included.
506
1239
  */
507
-
1240
+ interface FloatArrayTarget {
1241
+ /** Value destination for an `ArrayKind.Fp32` array. */
1242
+ f32?: Float32Array;
1243
+ /**
1244
+ * Bit destination for an `ArrayKind.Fp32` array: each element as its 4 wire
1245
+ * bytes in one little-endian 32-bit word — the same number `OStream.writeFp32Bits`
1246
+ * takes, so a payload read through this and written back reproduces the wire
1247
+ * exactly, signaling NaNs included.
1248
+ */
1249
+ bits?: Uint32Array;
1250
+ /** Destination for an `ArrayKind.Fp64` array. */
1251
+ f64?: Float64Array;
1252
+ }
508
1253
  /**
509
- * Receives decoded fields from an {@link IStream}. Every method is optional and
510
- * defaults to a no-op, so a visitor implements only the fields it cares about
511
- * and silently skips the rest.
1254
+ * Receives decoded fields from an {@link IStream} — the one decode surface
1255
+ * (§5.3.1). Every method is optional and defaults to a no-op, so a visitor
1256
+ * implements only the fields it cares about and silently skips the rest, which is
1257
+ * the `skip` half of the two per-field intents §6.7.2 allows (the other being
1258
+ * `read`: take the value, in the call).
512
1259
  *
513
- * String and blob payloads arrive as one or more `chunk`s, each tagged with the
514
- * field's `total` length and the `offset` of the chunk within the field, so a
515
- * large payload never has to be held in one piece. Array elements arrive one at
516
- * a time between {@link Visitor.arrayBegin} and {@link Visitor.arrayEnd}.
1260
+ * **One visitor per message, not per scope.** Nested sequences arrive as
1261
+ * {@link sequenceBegin} / {@link sequenceEnd} events on this same object, each
1262
+ * carrying the sequence's `id` and its `depth` (1 for a sequence opened at the
1263
+ * root). Generated code routes on those two numbers, which it knows statically
1264
+ * from the schema.
1265
+ *
1266
+ * **Nothing handed to a visitor outlives the call.** A `string` / `blob` payload
1267
+ * is reported in pieces as a range of the caller's *own* fed chunk (§6.6.3): the
1268
+ * decoder creates no view over it and holds no storage of its own (§6.6, §6.7),
1269
+ * so a consumer that wants the value copies it out — during the call — into
1270
+ * storage it owns. {@link PayloadAcc} and {@link decodeUtf8} are the ready-made
1271
+ * way to do that.
517
1272
  */
518
1273
  interface Visitor {
519
1274
  /**
520
- * An unsigned integer field. Number-first: `value` is a `number` when it fits
521
- * exactly (`≤ 2^53-1`, covering ids, u8..u32 and small u64s) and a `bigint`
522
- * only beyond that, so the common case avoids a per-value bigint allocation.
523
- */
524
- unsigned?(id: number, value: number | bigint): void;
525
- /** A signed integer field. Number-first like {@link unsigned} (`|value| ≤ 2^53-1` ⇒ `number`). */
526
- signed?(id: number, value: number | bigint): void;
527
- /**
528
- * Opt in to the raw-bytes channel on {@link fp32} / {@link arrayFp32}. Off by
529
- * default so a value-only consumer pays nothing: when this is not `true` the
530
- * decoder never allocates the per-value little-endian view (which, per fp32
531
- * element, roughly quartered array-decode throughput in a microbenchmark). Set
532
- * it `true` only in a bit-exact consumer (transcode / raw-bits oracle) that
533
- * needs `raw` to preserve a signaling NaN.
534
- */
535
- readonly fp32Raw?: boolean;
536
- /**
537
- * An IEEE-754 32-bit float field. When you set {@link fp32Raw} to `true`,
538
- * `raw` is a zero-copy little-endian view of the exact 4 wire bytes; use it —
539
- * not `value` — when the bytes must round-trip bit-for-bit (§4.6). `value` is
540
- * a JS `number` (a 64-bit double), and widening a *signaling* NaN into a
541
- * double quiets it (sets the is-quiet bit), so `value` cannot represent an
542
- * fp32 sNaN faithfully. The view aliases the decoder's working buffer and is
543
- * valid only for the duration of the call — copy it if you retain it, exactly
544
- * as with a string/blob `chunk`. Without {@link fp32Raw}, `raw` is `undefined`
545
- * (no allocation). fp64 needs no such channel: a double holds all 64 bits
546
- * verbatim (see {@link fp64}).
547
- */
548
- fp32?(id: number, value: number, raw?: Uint8Array): void;
1275
+ * A field **header**: its `id` and `wire` type, announced the moment the header
1276
+ * varint is complete — before the value, and before the value's own header word
1277
+ * (a fixlen length word, an array count word, the fields of a nested sequence).
1278
+ *
1279
+ * An observation point for a reader that wants the field stream as it arrives —
1280
+ * which id, in which scope, in which order — without implementing the value
1281
+ * callbacks it would otherwise take to see the same thing.
1282
+ *
1283
+ * **A schema bound does not belong here.** The header settles `id` and `wire`,
1284
+ * and nothing else. An element id past the schema `count` (MESSAGE_SPEC
1285
+ * §7.1/§5.1) looks decidable from the id alone, and it is not: §7.3 applies that
1286
+ * bound only to a field whose *subtype* has confirmed it is the declared one,
1287
+ * and a contradicting subtype is skipped rather than rejected. The subtype
1288
+ * arrives in the fixlen word, so the verdict is due at {@link fixlenBegin}.
1289
+ * CORELIB_PLAN §4.1.1 makes the timing normative: a message ending inside that
1290
+ * word is `INCOMPLETE` even when the id would violate a schema bound, because
1291
+ * the low 3 bits of an unfinished varint must not influence an outcome even
1292
+ * though they are already arithmetically fixed.
1293
+ *
1294
+ * Called exactly once per field, in every scope, for every wire type — the
1295
+ * sequence *end* marker excepted: it closes a scope rather than opening a field
1296
+ * and its id is discarded (§4.9). For a nested sequence it fires before
1297
+ * {@link sequenceBegin}.
1298
+ *
1299
+ * Throwing from it rejects the field — for a verdict the header really does
1300
+ * settle on its own, such as an id this reader will not accept in any shape.
1301
+ */
1302
+ fieldBegin?(id: number, wire: WireType): void;
1303
+ /**
1304
+ * An unsigned integer field.
1305
+ *
1306
+ * `value` is number-first: a `number` when the value fits exactly
1307
+ * (`≤ 2^53-1`, covering ids, u8..u32 and small u64s) and a `bigint` only beyond
1308
+ * that. `lo` / `hi` are the exact 64 bits as two unsigned 32-bit halves — the
1309
+ * ones the varint reader already holds — for a consumer that wants the value
1310
+ * bit-exactly without going through `bigint` arithmetic ({@link Long.fromBits}
1311
+ * builds a `Long` from them). Both describe the same value; use whichever fits.
1312
+ */
1313
+ unsigned?(id: number, value: number | bigint, lo: number, hi: number): void;
1314
+ /**
1315
+ * A signed integer field. `value` is number-first like {@link unsigned}
1316
+ * (`|value| ≤ 2^53-1` ⇒ `number`); `lo` / `hi` are the **decoded**
1317
+ * (zig-zag-undone) two's-complement halves.
1318
+ */
1319
+ signed?(id: number, value: number | bigint, lo: number, hi: number): void;
1320
+ /**
1321
+ * An IEEE-754 32-bit float field.
1322
+ *
1323
+ * `value` is a JS `number` — a 64-bit double — and widening a *signaling* NaN
1324
+ * into a double quiets it (sets the is-quiet bit), so `value` cannot represent
1325
+ * an fp32 sNaN faithfully. `bits` is the exact 4 wire bytes as one little-endian
1326
+ * 32-bit word, which can: re-encode from it with
1327
+ * {@link OStream.writeFp32Bits} and the payload round-trips bit-for-bit
1328
+ * (§4.6/§6.5). It is the "32-bit bits accessor" §6.5 names, and it is always
1329
+ * present — a number costs nothing to pass and needs no opt-in flag, where the
1330
+ * byte view it replaces was an allocation per value and a borrowed slice §6.7
1331
+ * forbids.
1332
+ */
1333
+ fp32?(id: number, value: number, bits: number): void;
549
1334
  /** An IEEE-754 64-bit double field. `value` is exact — a double is 64 bits wide. */
550
1335
  fp64?(id: number, value: number): void;
551
- /** A chunk of a UTF-8 string field. */
552
- string?(id: number, total: number, offset: number, chunk: Uint8Array): void;
553
- /** A chunk of a blob field. */
554
- blob?(id: number, total: number, offset: number, chunk: Uint8Array): void;
1336
+ /**
1337
+ * Start of a `string`/`blob` field: `total` payload bytes follow, in one or more
1338
+ * {@link string}/{@link blob} calls.
1339
+ *
1340
+ * The counterpart of {@link arrayBegin}, and it exists for the same reason: a
1341
+ * receiver-side bound on the *declared length* is decided by this word, not by
1342
+ * the payload. Without it a visitor could only see `total` once payload bytes
1343
+ * arrive, so a message that ends right after an over-bound length word would
1344
+ * escape the check and degrade to `INCOMPLETE`, where §5.2.3 requires `INVALID`.
1345
+ *
1346
+ * Called exactly once per field, before any payload call — including for a
1347
+ * zero-length payload, which is still announced here and then delivered as one
1348
+ * empty range.
1349
+ */
1350
+ fixlenBegin?(id: number, subtype: FixlenSubtype, total: number): void;
1351
+ /**
1352
+ * A piece of a UTF-8 string field: the bytes `src[start..end)`, at `offset` of
1353
+ * a `total`-byte payload.
1354
+ *
1355
+ * `src` is the **caller's own chunk** — the exact array passed to
1356
+ * {@link IStream.feed} (or to {@link decode}) — handed back with the piece's
1357
+ * coordinates (§6.6.3). The decoder builds no view over it, keeps no storage,
1358
+ * and hands out no borrowed slice of its own (§6.6, §6.7). Once `feed` returns,
1359
+ * the caller may reuse that memory, so a consumer that wants the value copies it
1360
+ * out **during the call**: {@link PayloadAcc} joins pieces into a buffer it
1361
+ * owns, and {@link decodeUtf8} turns a range straight into a string.
1362
+ *
1363
+ * The bytes are **not validated**. §6.4.5 puts the UTF-8 check where a string is
1364
+ * *materialized* — a piece may end mid-code-point, and a skipped field is never
1365
+ * validated at all — so on this surface the caller who materializes owns the
1366
+ * check. {@link decodeUtf8} is it, and a hand-rolled one must be built **fatal**
1367
+ * (`new TextDecoder("utf-8", { fatal: true })`): JavaScript's default
1368
+ * `TextDecoder` substitutes `U+FFFD`, which §6.4 forbids in either direction.
1369
+ */
1370
+ string?(id: number, total: number, offset: number, src: Uint8Array, start: number, end: number): void;
1371
+ /** A piece of a blob field — see {@link string} for the `src`/`start`/`end` contract. */
1372
+ blob?(id: number, total: number, offset: number, src: Uint8Array, start: number, end: number): void;
555
1373
  /** Start of an array; `count` elements of `kind` follow. */
556
1374
  arrayBegin?(id: number, kind: ArrayKind, count: number): void;
557
- /** One unsigned array element. Number-first like {@link unsigned}. */
558
- arrayUnsigned?(id: number, index: number, value: number | bigint): void;
559
- /** One signed array element. Number-first like {@link signed}. */
560
- arraySigned?(id: number, index: number, value: number | bigint): void;
561
- /** One fp32 array element. `raw` (the element's 4 wire bytes) is present only under {@link fp32Raw} — see {@link fp32}. */
562
- arrayFp32?(id: number, index: number, value: number, raw?: Uint8Array): void;
563
- /** One fp64 array element. `value` is exact — see {@link fp64}. */
564
- arrayFp64?(id: number, index: number, value: number): void;
1375
+ /**
1376
+ * Offer of the **bulk hand-off**: return the destination this visitor has
1377
+ * already allocated for array `id` and the decoder fills it directly, one write
1378
+ * per element and no callback at all; return `null` (or leave this method
1379
+ * unimplemented) to be served element by element as before.
1380
+ *
1381
+ * Called once per array, after {@link arrayBegin} and before the first element —
1382
+ * so a receiver cap on `count` is still compared where it always was, in
1383
+ * `arrayBegin`, and a rejected array is never offered. **An array that is empty
1384
+ * on the wire is offered too**, with `count` of 0: there is nothing to write,
1385
+ * but a destination held across fields would otherwise still hold the previous
1386
+ * array's elements, and its length is the only place this array's emptiness
1387
+ * could show.
1388
+ *
1389
+ * `kind` is the element kind and it decides which destination is legal
1390
+ * ({@link IntegerArrayTarget} for `Unsigned` / `Signed`,
1391
+ * {@link FloatArrayTarget} for `Fp32` / `Fp64`); a destination that contradicts
1392
+ * it, or that is shorter than `count`, is a caller mistake and is refused with
1393
+ * {@link SofabErrorCode.Argument} before any element is written. Declining on
1394
+ * a kind this visitor did not expect is always available — and is the right
1395
+ * answer, since `null` costs nothing but the call.
1396
+ *
1397
+ * See {@link ArrayTarget} for what the decoder then guarantees: ascending
1398
+ * writes, the element bound enforced here and only here, the destination held
1399
+ * until `arrayEnd` across as many `feed` calls as the chunking takes, and a
1400
+ * partially filled destination if an element is refused.
1401
+ */
1402
+ arrayBulk?(id: number, kind: ArrayKind, count: number): ArrayTarget | null;
565
1403
  /** End of an array. */
566
1404
  arrayEnd?(id: number): void;
567
1405
  /**
568
- * Start of a nested sequence. Return a {@link Visitor} to route the nested
569
- * fields to it (its {@link Visitor.sequenceEnd} fires at the matching end);
570
- * return nothing to keep using the current visitor.
1406
+ * Start of a nested sequence — a fresh id scope (§4.9) — opened by field `id`
1407
+ * at `depth` (1 at the root).
1408
+ *
1409
+ * Return **`false`** to decline the whole subtree: no callback of any kind fires
1410
+ * inside it, nesting included, its own {@link sequenceEnd} included, and a scope
1411
+ * opened within it is never offered either. Return anything else (or nothing) to
1412
+ * descend, and the nested fields arrive on this same visitor with their own ids
1413
+ * and `depth + 1`.
1414
+ *
1415
+ * A declined subtree is still *parsed* — a sequence is framed by markers, not by
1416
+ * a length, so its end has to be found — but nothing in it is decoded into
1417
+ * existence: no piece is reported and no value is built. No receiver cap fires
1418
+ * inside one either (§6.2.1's "a skipped field is never capped"), and that falls
1419
+ * out of the structure rather than needing a rule: a cap is compared by the
1420
+ * handler this stream would have called, and a declined scope calls none. Format
1421
+ * ceilings (`ARRAY_MAX`, `FIXLEN_MAX`, `MAX_DEPTH`, the varint bound) still apply
1422
+ * everywhere: they bound what the wire may express.
571
1423
  */
572
- sequenceBegin?(id: number): Visitor | void;
573
- /** End of the nested sequence this visitor was handling. */
574
- sequenceEnd?(): void;
1424
+ sequenceBegin?(id: number, depth: number): boolean | void;
1425
+ /** End of the nested sequence opened by field `id` at `depth`. */
1426
+ sequenceEnd?(id: number, depth: number): void;
575
1427
  }
576
1428
  /**
577
- * Push parser for the SofaBuffers wire format. Feed it bytes in chunks of any
578
- * size with {@link IStream.feed} and it drives a {@link Visitor}, one call per
579
- * decoded field, resuming cleanly across chunk boundaries. Call
580
- * {@link IStream.end} after the final chunk to read whether the message finished
581
- * on a field boundary. When the whole message is already in one buffer, prefer
582
- * the faster {@link decode}.
1429
+ * Push parser for the SofaBuffers wire format, and the library's only decode
1430
+ * surface (§5.3.1).
1431
+ *
1432
+ * Bind a {@link Visitor} at construction, then feed bytes in chunks of any size
1433
+ * with {@link feed}: it calls one visitor method per decoded field and resumes
1434
+ * cleanly across chunk boundaries. Every `feed` returns the decode outcome for the
1435
+ * bytes so far, so no end / finalize call is needed — and there is nothing else to
1436
+ * ask: `feed` is the only way to learn where a stream stands, by what it returns
1437
+ * or by what it throws.
1438
+ *
1439
+ * **No receiver limit is configured here, because this codec holds none**
1440
+ * (§6.2.1). A `max_dyn_*` cap is the receiving *application's* number, stated by
1441
+ * generated code, which knows the schema and the target; it is compared inside the
1442
+ * visitor's own `arrayBegin` / `fixlenBegin` — raised by this stream at the count
1443
+ * or length header, before any payload is delivered and only for a field the
1444
+ * visitor reads — and, for a wrapper array's `string` / `blob` elements, inside
1445
+ * the `StringSeq` / `BlobSeq` collector those bounds were passed to.
1446
+ * This class used to take a `DecodeLimits` and default every absent cap to the
1447
+ * format ceiling, which §6.2.1 forbids twice over: a codec must not supply a
1448
+ * default for a limit it was not given, and a format ceiling reached because no
1449
+ * cap was stated is the format's bound and must not be presented as a receiver
1450
+ * cap.
1451
+ *
1452
+ * Constructing one is the only allocating step (§6.6): `feed` itself allocates
1453
+ * nothing at all. The one-shot {@link decode} is exactly this class fed once.
583
1454
  */
584
1455
  declare class IStream {
585
1456
  private readonly state;
586
1457
  /**
587
- * @param limits Optional opt-in decode caps ({@link DecodeLimits}). An
588
- * over-limit array count or string / blob length throws {@link SofabError}
589
- * (`LIMIT_EXCEEDED`) from {@link feed}, at the offending field's header and
590
- * before any of its payload is streamed to the visitor. Omit for no caps.
591
- */
592
- constructor(limits?: DecodeLimits);
593
- /**
594
- * Feed a chunk of bytes, dispatching decoded fields to `visitor`. Throws
595
- * {@link SofabError} (`INVALID_MSG`) only if the bytes are *malformed*;
596
- * running out of bytes mid-field is not an error — it simply suspends until
597
- * the next chunk (see {@link end}).
1458
+ * @param visitor The field handler this stream drives, for its whole life — and
1459
+ * the layer that holds the receiver caps, if any (§6.2.1; see the class doc).
598
1460
  */
599
- feed(chunk: Uint8Array, visitor: Visitor): void;
1461
+ constructor(visitor: Visitor);
600
1462
  /**
601
- * Report whether the stream ended exactly at a field boundary. Call after the
602
- * final {@link feed}: returns {@link DecodeStatus.Complete} at a clean field
603
- * boundary, or {@link DecodeStatus.Incomplete} if the last chunk ended inside
604
- * a field (a partial varint, an unfinished payload / array, or a still-open
605
- * nested sequence).
1463
+ * Feed a chunk of bytes, dispatching decoded fields to the bound visitor, and
1464
+ * **return** where the decode stands after them (§5.2.1):
1465
+ * {@link DecodeStatus.Complete} when they end exactly at a field boundary,
1466
+ * {@link DecodeStatus.Incomplete} when they end *inside* a field (a partial
1467
+ * varint, an unfinished payload / array, or a still-open nested sequence).
1468
+ * Running out of bytes mid-field is not an error — the decode merely suspends
1469
+ * until the next chunk, and the caller owns end-of-input.
1470
+ *
1471
+ * **This call is the only place the answer is.** There is no finish / finalize
1472
+ * step (§5.2.4) and no status accessor: what this returns, or throws, is the
1473
+ * whole of what the stream has to say, so a caller is never one question short
1474
+ * after it and never has two answers to reconcile. Feeding an empty chunk
1475
+ * re-reads the same value without consuming anything, for a caller that wants
1476
+ * the outcome again without holding on to it.
1477
+ *
1478
+ * The chunk is borrowed **only for the duration of this call** (§6.0): once it
1479
+ * returns, the caller may reuse, overwrite or free that memory, and the decoded
1480
+ * message is unaffected — the decoder retains nothing that points into it.
606
1481
  *
607
- * Per the finish-less spec (MESSAGE_SPEC §7) this is a pure accessor: it never
608
- * throws and never promotes an incomplete decode to an error — the caller owns
609
- * end-of-input and decides whether a trailing `Incomplete` is a truncation
610
- * error. (A *malformed* message has already thrown from {@link feed}.)
1482
+ * `INVALID` travels on the error channel — this port's idiomatic surfacing of
1483
+ * it: *malformed* bytes throw {@link SofabError} (`INVALID_MSG`) instead of
1484
+ * returning a status, which is why the return type names only the other two.
1485
+ * That verdict is **terminal** (§5.2.1): the stream latches it, so a caller that
1486
+ * catches the throw and feeds on gets the same error again from every later
1487
+ * call — no further byte is consumed and no visitor method is invoked. A caller
1488
+ * that caught it already holds the verdict, in the code on the error it caught.
1489
+ *
1490
+ * A receiver-limit rejection (`LIMIT_EXCEEDED`, §6.2.1) travels the same
1491
+ * channel — thrown out of the visitor callback that compared the cap — but it
1492
+ * is **not** the `INVALID` outcome and never becomes one: the bytes are
1493
+ * well-formed and the same message decodes under a looser cap, so it is a
1494
+ * policy rejection (§6.2.1, §6.3). The two stay distinguishable by their code,
1495
+ * which is what §6.3 requires; §6.3 leaves the surfacing open between "a fourth
1496
+ * decode outcome" and "a terminal failure carrying the `LimitExceeded` code on
1497
+ * the error channel", and this port takes the second. **Terminal** is the other
1498
+ * half of that sentence and holds exactly as it does for `INVALID`: the stream
1499
+ * latches the rejection, so every later call re-throws it under the same code,
1500
+ * consumes no byte and drives no visitor method. It is *only* on the error
1501
+ * channel — the three-valued outcome has no value for "valid, but more than I am
1502
+ * configured to accept", so there is nothing about it to read back as a status,
1503
+ * and nothing that has to be kept in step with the throw.
611
1504
  */
612
- end(): DecodeStatus;
1505
+ feed(chunk: Uint8Array): FeedStatus;
613
1506
  }
614
1507
  /**
615
1508
  * Decode a complete message held in one contiguous buffer, in a single call.
616
1509
  *
617
- * This is the non-streaming convenience — and the fast path: with the whole
618
- * message in hand it advances one cursor over the buffer instead of running the
619
- * resumable per-byte state machine, so it is markedly faster than feeding the
620
- * same bytes through
621
- * {@link IStream}. Use {@link IStream} when the message arrives in chunks; use
622
- * this when you already have it whole.
1510
+ * The non-streaming convenience, and **not** a second decoder: it is one
1511
+ * {@link IStream.feed} of the whole buffer, so it runs the same code, applies the
1512
+ * same rules and has the same memory behaviour as a chunked decode — §6.7.1
1513
+ * forbids the one-shot path from differing, right down to holding no view into
1514
+ * the buffer it was handed. Feeding a whole message is also the case the decoder's
1515
+ * fast lane is built for, so nothing is given up by having one implementation.
623
1516
  *
624
- * The whole buffer *is* the end of input, so the two failure outcomes both
625
- * throw a {@link SofabError} the caller tells apart by `code` (MESSAGE_SPEC §7):
626
- * malformed input throws `INVALID_MSG`, while input that ends inside a field —
627
- * truncation or an unclosed sequence — throws `INCOMPLETE`. A complete message
628
- * returns normally.
1517
+ * The whole buffer *is* the end of input, so the two failure outcomes both throw a
1518
+ * {@link SofabError} the caller tells apart by `code` (MESSAGE_SPEC §7): malformed
1519
+ * input throws `INVALID_MSG`, while input that ends inside a field — truncation or
1520
+ * an unclosed sequence — throws `INCOMPLETE`. A complete message returns normally.
629
1521
  *
630
- * Pass `limits` ({@link DecodeLimits}) to cap array counts and string / blob
631
- * lengths; an over-limit field throws `LIMIT_EXCEEDED` at its header, before it
632
- * is materialized. Omit for no caps (the default).
1522
+ * The receiver caps of §6.2.1 are the `visitor`'s, not this function's: it takes
1523
+ * no limits argument because this codec holds none. See {@link IStream}.
633
1524
  */
634
- declare function decode(bytes: Uint8Array, visitor: Visitor, limits?: DecodeLimits): void;
1525
+ declare function decode(bytes: Uint8Array, visitor: Visitor): void;
635
1526
 
636
1527
  /**
637
- * The pull / cursor decoder: a monomorphic companion to {@link "./fast"}.
638
- *
639
- * {@link "./fast"}'s {@link decodeContiguous} is a *push* decoder — it drives the
640
- * buffer and calls a {@link Visitor} method per field. That is the right shape
641
- * for streaming and skip-subtree callers, but the visitor call sites go
642
- * megamorphic once a single decode routes through several differently-shaped
643
- * visitor objects (one per nested message type), which a JIT cannot inline.
644
- *
645
- * {@link Cursor} inverts control: it keeps one read cursor over the contiguous
646
- * {@link Uint8Array} and exposes *pull* primitives — {@link Cursor.readHeader}
647
- * plus a typed `read*` per wire type — so **generated code drives the loop** with
648
- * a single `switch (cursor.id)` that reads straight into its own fields. Every
649
- * call site is then monomorphic (the generated per-type decoder is the only
650
- * caller), which is what lets V8 inline the whole decode into a flat loop — the
651
- * same technique protobuf's generated `decode(reader)` uses.
652
- *
653
- * It shares {@link "./fast"}'s number-first varint core verbatim: each varint is
654
- * accumulated into two 32-bit JS *numbers* (`lo`/`hi`) and a `bigint` is
655
- * materialised only for a 64-bit *value* that does not fit in `2^53-1` (never for
656
- * ids, lengths or counts). String / blob payloads are returned as a single
657
- * zero-copy `subarray` view. It reports the same three-valued outcome as the
658
- * push path (MESSAGE_SPEC §7): malformed input throws a {@link SofabError} with
659
- * code `INVALID_MSG`, and a read that runs off the end of the buffer mid-field
660
- * throws `INCOMPLETE`.
1528
+ * Decode-side text helper — the read-path twin of {@link "../encode/fixlen"}.
1529
+ *
1530
+ * Two things make a short string expensive to decode, and this avoids both.
1531
+ *
1532
+ * `TextDecoder.decode` needs a `Uint8Array` covering exactly the payload, so the
1533
+ * decoder has to build a `subarray` per string; on Node 24 that view alone costs
1534
+ * about a third of the whole read (~500 Ir/op for a 13-byte field). And the
1535
+ * decode itself is a WHATWG entry point whose per-call setup dwarfs the payload
1536
+ * at these sizes.
1537
+ *
1538
+ * The obvious fix — walk the bytes in JS and append with `+=`, the way
1539
+ * protobufjs does — trades one problem for another: the repeated concatenation
1540
+ * builds a **rope**, and the rope is flattened later, by the first consumer that
1541
+ * walks it. In a decode-then-encode round trip that consumer is the encoder's own
1542
+ * `utf8Length` / `utf8Write` pass, so the cost does not disappear, it moves out
1543
+ * of the decode where it is easy to miss. Measured on a 13-byte ASCII field
1544
+ * (decode plus one charCodeAt walk): TextDecoder 2006 Ir/op, rope 2095 — the
1545
+ * rope form looks 40 percent cheaper if only the decode is measured, and is
1546
+ * *worse* once the string is used.
1547
+ *
1548
+ * So an all-ASCII payload short enough to name its length is built with a
1549
+ * SINGLE `String.fromCharCode` call, which returns a flat string with no rope to
1550
+ * pay for afterwards: 858 Ir/op on the same measurement, less than half of
1551
+ * either alternative. Everything else — non-ASCII, or longer than
1552
+ * {@link FLAT_MAX} — goes to the fatal {@link TextDecoder} exactly as before.
1553
+ *
1554
+ * This stays **strict** (MESSAGE_SPEC section 8): the fast path is gated on every
1555
+ * byte being below 0x80, and each such byte is a well-formed single-byte UTF-8
1556
+ * sequence, so an all-ASCII run has nothing to reject. Any payload with a high
1557
+ * bit set is handed to the platform decoder whole and gets exactly the
1558
+ * validation it always got — the fast path can never accept bytes the platform
1559
+ * would refuse.
1560
+ *
1561
+ * {@link decodeUtf8} is **public** because the code that needs it is not in this
1562
+ * library. The decoder reports raw, unvalidated payload pieces (§6.4.5 puts the
1563
+ * UTF-8 check where a string is *materialized*, and a piece may end mid-code-point),
1564
+ * so materialization — and therefore the check — belongs to whoever wants the
1565
+ * value. Generated code is that whoever, and without an exported entry point every
1566
+ * generated package builds a plain fatal `TextDecoder` of its own and pays the full
1567
+ * 2006 Ir/op per short string instead of 858.
1568
+ *
1569
+ * It is **static helper layer**, not codec (§6.6.1): it allocates the string it
1570
+ * returns, and no codec path calls it.
1571
+ */
1572
+ /**
1573
+ * Decode `buf[start..end)` as strict UTF-8 — the whole of `buf` when the range
1574
+ * is omitted, which is the shape a reassembled payload (see {@link PayloadAcc})
1575
+ * arrives in.
1576
+ *
1577
+ * Malformed bytes throw a {@link SofabError} (`INVALID_MSG`), never the fatal
1578
+ * {@link TextDecoder}'s bare `TypeError`. The mapping belongs here rather than at
1579
+ * each call site: invalid UTF-8 is an invalid *message* (MESSAGE_SPEC §8,
1580
+ * CORELIB_PLAN §6.4), so it has to reach a caller as the same `INVALID` verdict
1581
+ * as every other malformation — through the one `catch (e) { if (e instanceof
1582
+ * SofabError) … }` a consumer writes — and a platform exception escaping that
1583
+ * clause is precisely the bug this closes.
1584
+ *
1585
+ * A zero-length payload is the empty string, decoded without touching the
1586
+ * platform decoder.
661
1587
  */
1588
+ declare function decodeUtf8(buf: Uint8Array, start?: number, end?: number): string;
662
1589
 
663
1590
  /**
664
- * A pull decoder over a complete message held in one contiguous buffer.
665
- *
666
- * Usage from generated code: loop on {@link readHeader}; for each field switch on
667
- * {@link id} and call the matching `read*` (which consumes that field's value and
668
- * advances the cursor); recurse into a child type's decoder on a nested sequence;
669
- * fall through to {@link skip} for an unknown id. See {@link readHeader}.
670
- *
671
- * Pass {@link DecodeLimits} to cap array counts and string / blob lengths; an
672
- * over-limit field throws {@link SofabError} (`LIMIT_EXCEEDED`) at its header,
673
- * before it is materialized. Omit for no caps (the default).
674
- */
675
- declare class Cursor {
676
- /** Field id of the header last accepted by {@link readHeader}. */
677
- id: number;
678
- /** Wire type of the header last accepted by {@link readHeader}. */
679
- wire: number;
680
- /**
681
- * Fixlen subtype of the header last accepted by {@link readHeader} — one of
682
- * {@link FixlenSubtype} — when its {@link wire} is {@link WireType.Fixlen} or
683
- * {@link WireType.ArrayFixlen}; `-1` otherwise (a non-fixlen field, or a
684
- * fixlen field whose subtype word is truncated away).
685
- *
686
- * The four fixlen subtypes (`fp32`, `fp64`, `string`, `blob`) all share one
687
- * {@link wire} type, so {@link wire} alone cannot separate them. This is the
688
- * companion accessor that can: a generated guard reads it right after
689
- * {@link readHeader} to skip a field whose delivered subtype contradicts the
690
- * schema (MESSAGE_SPEC §7.3), exactly as it already does on {@link wire} for
691
- * the other kinds:
692
- *
693
- * ```ts
694
- * case 9: if (c.wire !== WireType.Fixlen || c.fixSub !== FixlenSubtype.Fp64) {
695
- * c.skip(c.wire); break;
696
- * } o.somefp64 = c.readFp64(); break;
697
- * ```
698
- *
699
- * It is *peeked* — the subtype word is not consumed — so the matching typed
700
- * reader (or {@link skip}) still reads and validates it, and a malformed or
701
- * truncated word surfaces `INVALID` / `INCOMPLETE` there as before.
702
- */
703
- fixSub: number;
704
- private readonly buf;
705
- private readonly view;
706
- private readonly n;
707
- private p;
708
- private readonly maxArrayCount;
709
- private readonly maxStringLen;
710
- private readonly maxBlobLen;
711
- private lo;
712
- private hi;
713
- private depth;
714
- constructor(buf: Uint8Array, limits?: DecodeLimits);
715
- /**
716
- * Advance to the next field header. Returns `true` and sets {@link id} /
717
- * {@link wire} when a field follows; returns `false` — consuming the marker —
718
- * at the end of the buffer *or* at the sequence-end that closes the sequence
719
- * this decoder is reading. So a generated per-type decoder loops uniformly:
720
- *
721
- * ```ts
722
- * while (c.readHeader()) {
723
- * switch (c.id) {
724
- * case 4: this.u32 = Number(c.readUnsigned()); break;
725
- * case 10: this.child = Child.decodeFrom(c); break; // nested sequence
726
- * default: c.skip(c.wire); break; // unknown field
727
- * }
728
- * }
729
- * ```
730
- *
731
- * At the root the loop ends at end-of-buffer; inside a nested sequence it ends
732
- * at the matching {@link WireType.SequenceEnd} (which is consumed). A field
733
- * whose id is out of range throws {@link SofabError} (`INVALID_MSG`).
734
- */
735
- readHeader(): boolean;
736
- /** Read an unsigned scalar (wire {@link WireType.Unsigned}), number-first. */
737
- readUnsigned(): number | bigint;
738
- /** Read a signed scalar (wire {@link WireType.Signed}), zig-zag, number-first. */
739
- readSigned(): number | bigint;
740
- /** Read a 32-bit float scalar (wire {@link WireType.Fixlen}, subtype fp32). */
741
- readFp32(): number;
742
- /**
743
- * Read a 32-bit float scalar as its raw 4 wire bytes (little-endian), zero-copy
744
- * — the bit-preserving companion to {@link readFp32}.
745
- *
746
- * {@link readFp32} returns a JS `number` (a 64-bit double), and widening an
747
- * fp32 *signaling* NaN into a double quiets it (0x7F800001 → 0x7FC00001), so a
748
- * value consumer can never round-trip one bit-for-bit (§4.6). Generated
749
- * bit-exact decode reads the bytes here instead and re-emits them verbatim with
750
- * {@link OStream.writeFixlen} (subtype fp32) — mirroring the visitor `raw`
751
- * channel on the push paths (fast.ts / state.ts), which the pull path was
752
- * missing (corelib-ts#66).
753
- *
754
- * The header (subtype fp32, length 4) is validated exactly as in
755
- * {@link readFp32}; the returned view aliases the source buffer, valid only
756
- * until it is reused, like {@link readBlob}.
757
- */
758
- readFp32Raw(): Uint8Array;
759
- /** Read a 64-bit float scalar (wire {@link WireType.Fixlen}, subtype fp64). */
760
- readFp64(): number;
761
- /**
762
- * Read a UTF-8 string scalar (wire {@link WireType.Fixlen}, subtype string).
763
- * Pass the schema `maxlen` (byte length) for a bounded string so an
764
- * over-length is rejected as `INVALID` at the header, before the payload is
765
- * taken (see {@link fixlenLen}); the wire length is exactly the UTF-8 byte
766
- * length, so the check is exact. Omit for an unbounded string.
767
- */
768
- readString(schemaMaxlen?: number): string;
769
- /**
770
- * Read a blob scalar (wire {@link WireType.Fixlen}, subtype blob) as a
771
- * zero-copy {@link Uint8Array} view into the source buffer.
772
- */
773
- readBlob(schemaMaxlen?: number): Uint8Array;
774
- /**
775
- * Read an unsigned array (wire {@link WireType.ArrayUnsigned}), number-first
776
- * per element. Pass the schema `count` for a bounded array so an over-count is
777
- * rejected as `INVALID` at the header (see {@link arrayCount}); omit it for an
778
- * unbounded array (today's behavior).
779
- */
780
- readUnsignedArray(schemaCount?: number): (number | bigint)[];
781
- /** Read a signed array (wire {@link WireType.ArraySigned}), zig-zag, number-first per element. */
782
- readSignedArray(schemaCount?: number): (number | bigint)[];
783
- /**
784
- * Read an unsigned 64-bit array into {@link Long}[] — the `bigint`-free path.
785
- * Each element keeps the raw lo/hi halves; call {@link Long.toBigInt} to
786
- * materialise only the values the caller actually needs.
787
- */
788
- readUnsignedArrayLong(schemaCount?: number): Long[];
789
- /** Read a signed 64-bit array (zig-zag) into {@link Long}[] — the `bigint`-free path. */
790
- readSignedArrayLong(schemaCount?: number): Long[];
791
- /** Read an fp32 array (wire {@link WireType.ArrayFixlen}, element subtype fp32). */
792
- readFp32Array(schemaCount?: number): number[];
793
- /**
794
- * Read an fp32 array as its raw little-endian element payload (`count * 4`
795
- * bytes), zero-copy — the bit-preserving companion to {@link readFp32Array}.
796
- * Widening each element to a JS `number` quiets an fp32 *signaling* NaN just as
797
- * on the scalar path (§4.6; see {@link readFp32Raw}), so bit-exact decode reads
798
- * the whole payload here and re-emits it with {@link OStream.writeFp32ArrayRaw}
799
- * (corelib-ts#66). The header (element subtype fp32, size 4) is validated
800
- * exactly as in {@link readFp32Array}; the returned view aliases the source
801
- * buffer, like {@link readBlob}.
802
- */
803
- readFp32ArrayRaw(schemaCount?: number): Uint8Array;
804
- /** Read an fp64 array (wire {@link WireType.ArrayFixlen}, element subtype fp64). */
805
- readFp64Array(schemaCount?: number): number[];
806
- /**
807
- * Consume the value of the field whose header {@link readHeader} just accepted,
808
- * discarding it — for a `default:` branch that keeps the cursor in sync on an
809
- * unknown id. Pass {@link wire}. A {@link WireType.SequenceStart} skips the
810
- * whole nested sequence.
811
- */
812
- skip(wire: number): void;
813
- private skipValue;
814
- private skipSequence;
815
- /**
816
- * Peek the delivered fixlen subtype of the field {@link readHeader} just
817
- * accepted, **without advancing the cursor** — the readers / {@link skip}
818
- * still re-read and validate the word. Returns one of {@link FixlenSubtype}
819
- * (0..3), a reserved value (4..7), or `-1` when the wire is not a fixlen kind
820
- * or the subtype word is truncated away.
821
- *
822
- * The subtype is the low 3 bits of the fixlen sub-header word, and the low
823
- * bits of a LEB128 word live entirely in its **first** byte — so this only
824
- * reads one byte, it never decodes a varint.
825
- */
826
- private peekFixSub;
827
- /**
828
- * Read and validate an array count word (0..ARRAY_MAX; §4.7/§4.8). When a
829
- * `schemaCount` is given, a count above it is a schema-bound violation and is
830
- * rejected as `INVALID` — see the check below.
831
- */
832
- private arrayCount;
833
- /** Read a scalar fixlen sub-header, asserting subtype and exact byte length (floats). */
834
- private fixlenHeader;
835
- /**
836
- * Read a scalar fixlen sub-header for a string/blob, asserting subtype;
837
- * returns the byte length. When a `schemaMaxlen` is given, a length above it
838
- * is a schema-bound violation and is rejected as `INVALID` — see below.
839
- */
840
- private fixlenLen;
841
- /**
842
- * Read an array fixlen element header (count + element type); returns the
843
- * count. When a `schemaCount` is given, a count above it is a schema-bound
844
- * violation and is rejected as `INVALID` — see below.
845
- */
846
- private arrayFixlenHeader;
847
- /** Hand back a zero-copy view of the next `len` bytes, advancing the cursor. */
848
- private take;
849
- private rawFp32;
850
- private rawFp64;
851
- /**
852
- * The last varint's full value as a `bigint` (64-bit fidelity). Only ever
853
- * called from {@link unsignedValue} / {@link signedValue} on the `hi` overflow
854
- * path (`this.hi >>> 0 > 0x1fffff`), so `hi` is always non-zero here.
855
- */
856
- private big;
857
- /**
858
- * The last varint as an unsigned value, number-first: a `number` when it fits
859
- * exactly (`≤ 2^53-1`), a `bigint` only beyond that.
860
- */
861
- private unsignedValue;
862
- /** The last zig-zag varint as a signed value, number-first. */
863
- private signedValue;
864
- /** The last varint's value as a JS number — exact for ids/lengths/counts. */
865
- private num;
866
- /** The last varint with its low 3 tag bits stripped (`value >> 3`). */
867
- private upper;
868
- /**
869
- * Decode one LEB128 varint at the cursor into {@link lo} / {@link hi} (each an
870
- * unsigned 32-bit half), advancing {@link p}. Throws on truncation or a value
871
- * spilling past 64 bits (>10 bytes). Unrolled, number-only — no `bigint`.
872
- */
873
- private readVarint;
874
- private set;
1591
+ * Generated-layer support: reassembling a `string` / `blob` payload that the
1592
+ * decoder reported in pieces.
1593
+ *
1594
+ * This is the **static helper layer** of CORELIB_PLAN §6.6.1, not part of the
1595
+ * codec: it allocates, on the generated layer's behalf, and the codec never calls
1596
+ * into it — the call graph runs generated code → helper → codec, never the other
1597
+ * way round. §6.6 binds the codec; this side of the boundary is where a value gets
1598
+ * built.
1599
+ *
1600
+ * {@link IStream} reports a payload to {@link Visitor.string} /
1601
+ * {@link Visitor.blob} as one or more pieces, each a range of the caller's own fed
1602
+ * chunk tagged with the field's `total` byte length and the `offset` of the piece
1603
+ * within it. That memory is borrowed only for the duration of the call (§6.0), so
1604
+ * a consumer that wants the *value* has to copy out — and generated code always
1605
+ * wants the value: a schema field is a `string`, not a sequence of pieces.
1606
+ *
1607
+ * That join has the same shape for every schema — it is decided entirely by
1608
+ * `total` and `offset`, both runtime values — so it lives here rather than in
1609
+ * every generated package (ARCHITECTURE §8). It is also the piece that is easiest
1610
+ * to get subtly wrong and hardest to catch: two implementations can disagree about
1611
+ * where a payload was split and still produce byte-identical output on every
1612
+ * shared vector, because the split is a property of how the caller fed the bytes,
1613
+ * not of the bytes. {@link PayloadAcc} is pinned instead by a unit test that
1614
+ * splits one payload at *every* offset and requires one answer.
1615
+ */
1616
+ /**
1617
+ * Joins a `string` / `blob` payload that arrived across several fed pieces.
1618
+ *
1619
+ * **One per decoder, shared by every field.** Exactly one payload is in flight at
1620
+ * a time across a whole decode, however deep the nesting — a fixlen payload is
1621
+ * atomic on the wire, so nothing can begin inside it — so one accumulator is
1622
+ * enough.
1623
+ *
1624
+ * **What it returns is owned by the caller** and aliases nothing: the bytes are
1625
+ * copied into storage this accumulator allocated, on the whole-payload path
1626
+ * exactly as on the split one (§6.7 — "there is no mode in which the destination
1627
+ * aliases the input"). A consumer may keep the result, and a payload that arrived
1628
+ * whole is not a special case with a different lifetime.
1629
+ *
1630
+ * Sizing follows the declared `total`, so a hostile length word is bounded by
1631
+ * whatever bound the caller already applied to it — the schema `maxlen` its
1632
+ * generated guard checks, or the receiver caps of §6.2.1, both of which
1633
+ * are enforced at the length word before a piece is ever delivered. This class
1634
+ * enforces neither: it is handed a payload the caller has already accepted.
1635
+ */
1636
+ declare class PayloadAcc {
1637
+ private buf;
1638
+ private len;
1639
+ /**
1640
+ * Contribute one piece — the bytes `src[start..end)` at `offset` of a
1641
+ * `total`-byte payload — and return the **whole** payload once it is complete,
1642
+ * or `null` while bytes are still outstanding.
1643
+ *
1644
+ * `offset === 0` starts a payload — and *resets* the accumulator, so a decode
1645
+ * that was abandoned mid-payload (an `INVALID` field, a declined subtree) leaves
1646
+ * nothing behind to corrupt the next one.
1647
+ *
1648
+ * A late piece with no payload in progress returns `null` rather than writing
1649
+ * anywhere: the accumulator has no buffer to append to, and inventing one would
1650
+ * fabricate a payload out of a fragment.
1651
+ */
1652
+ take(total: number, offset: number, src: Uint8Array, start: number, end: number): Uint8Array | null;
875
1653
  }
876
1654
 
1655
+ /**
1656
+ * Generated-layer support: the element collectors for a **wrapper-sequence array**
1657
+ * (MESSAGE_SPEC §5.1) — {@link ElementSeq} and {@link FramedSeq} for the index
1658
+ * rules of any element kind, {@link StringSeq} / {@link BlobSeq} for the two leaf
1659
+ * kinds whose payload arrives here as well.
1660
+ *
1661
+ * Static helper layer, not codec (CORELIB_PLAN §6.6.1): these allocate — that is
1662
+ * their job — and no codec path reaches them. Generated code drives them from
1663
+ * inside its own flat {@link Visitor}, which is what knows, from the schema, that
1664
+ * the scope it is currently in *is* the wrapper array.
1665
+ *
1666
+ * A wrapper array is an ordinary sequence whose child fields *are* the elements,
1667
+ * and whose field id *is* the array index. So a collector is fed the two events a
1668
+ * `string` / `blob` element produces — {@link StringSeq.begin} at the length word
1669
+ * and {@link StringSeq.element} per payload piece — and places each value at
1670
+ * `out[id]`.
1671
+ *
1672
+ * Every part of that is schema-independent. Both bounds on the index (the schema
1673
+ * `count`, the receiver cap) and both on the element's byte length (the element
1674
+ * `maxlen`, the receiver cap) arrive as constructor arguments — runtime values —
1675
+ * and the field name is carried along only so a rejection can say which field it
1676
+ * was about. So the collectors belong here rather than being re-emitted, textually
1677
+ * identical, into every generated package (ARCHITECTURE §8).
1678
+ *
1679
+ * Four rules are implemented here and are worth stating once, since nothing in the
1680
+ * shared vectors can distinguish an implementation that gets them wrong from one
1681
+ * that does not — they are properties of the *decoded value*, not of the bytes
1682
+ * (which is exactly why CORELIB_PLAN §7.2 item 8 asks for them separately):
1683
+ *
1684
+ * - **Gaps are legal.** An interior element equal to its default is not written at
1685
+ * all, so ids `0, 2, 3` are well-formed. The missing index is filled with the
1686
+ * element default, never skipped over: everything after a gap keeps its index.
1687
+ * - **Length is highest present id + 1.** The wrapper carries no length, and the
1688
+ * last element is never elided, so growing to `id + 1` on each element is
1689
+ * exactly right and no trailing fill is ever needed.
1690
+ * - **A repeated element id replaces.** Last occurrence wins per id (§7.4);
1691
+ * placing at `out[id]` does that by construction, with no bookkeeping.
1692
+ * - **A rejected id extends nothing.** Every bound is checked before the
1693
+ * destination grows, so a rejection leaves the array exactly as it was and a
1694
+ * lower id delivered afterwards still lands correctly.
1695
+ *
1696
+ * **Two bounds, never both** (§6.2.1). Index and element length each carry a
1697
+ * schema bound and a receiver cap, and exactly one of the pair applies: where the
1698
+ * schema declared a bound its violation is `INVALID` — a statement about validity
1699
+ * — and where it declared none the receiver's cap governs and its violation is
1700
+ * `LIMIT_EXCEEDED`, a policy rejection on well-formed bytes. "They **MUST NOT** be
1701
+ * applied to a field the schema already bounds", so the caps are exclusive with
1702
+ * the bounds and never additive.
1703
+ *
1704
+ * **Every bound is a required argument, and none of them has a default** (§6.2.1).
1705
+ * The comparison runs here; the *number* never originates here. A corelib "**MUST
1706
+ * NOT** hold a limit of its own, **MUST NOT** supply a default for one it was not
1707
+ * given, **MUST NOT** read an omitted argument as *unlimited*, and **MUST NOT**
1708
+ * clamp to one", and a format ceiling (§6.2) reached because no cap was stated "is
1709
+ * the **format's** bound, not a receiver cap, and a port **MUST NOT** present it as
1710
+ * one". These constructors used to default `receiverCap` to `ARRAY_MAX` and
1711
+ * `receiverElemMax` to `FIXLEN_MAX`, which did exactly that: they reported
1712
+ * `LIMIT_EXCEEDED` against a ceiling nobody configured. The defaults are gone and
1713
+ * the arguments are required — §6.2.1's "strictest form, and the recommended one".
1714
+ * The *schema* bounds beside them (`cap`, `elemMax`) are required too, with
1715
+ * {@link UNBOUNDED} as the explicit "the schema declared none" spelling: an absent
1716
+ * schema bound is a fact about the schema, which only the caller knows.
1717
+ *
1718
+ * **And an argument that states no cap is a caller mistake, not a policy
1719
+ * rejection.** A required argument can still arrive as `-1`, `undefined` or
1720
+ * `Infinity` from an untyped caller, and the same reasoning applies one level in:
1721
+ * no receiver policy was set, so there is no `LIMIT_EXCEEDED` to report and no
1722
+ * unlimited mode to fall back to. Such a bound is refused at construction with
1723
+ * `Argument` (§6.3's third row: "every remaining caller mistake is
1724
+ * `InvalidArgument`") — see {@link requireReceiverBound}.
1725
+ */
1726
+
1727
+ /**
1728
+ * No *schema* bound — what `cap` / `elemMax` take for an array or element the
1729
+ * schema left open (§7.2), so the receiver cap beside them governs instead.
1730
+ *
1731
+ * Exported because the schema bounds are required arguments with no default: an
1732
+ * absent one is a fact about the **schema**, which only generated code knows, and
1733
+ * §6.2.1's ban on a codec defaulting a bound is easiest to keep honest when every
1734
+ * bound has to be spelled. This is the spelling for "there is none".
1735
+ */
1736
+ declare const UNBOUNDED = -1;
1737
+ /**
1738
+ * The slots of a wrapper-sequence array: the index rules of MESSAGE_SPEC §5.1 and
1739
+ * the two bounds of CORELIB_PLAN §6.2.1, once, for any element type.
1740
+ *
1741
+ * {@link StringSeq} and {@link BlobSeq} place leaf elements through it, and
1742
+ * generated code places a **framed** element — a `struct` / `union` / nested row —
1743
+ * through it directly: {@link reserve} at the element's `sequenceBegin`, then build
1744
+ * the child into `out[id]`. The element kind changes which path arrives here and
1745
+ * nothing else, which is the point — the bound is the index (§7.2 item 8).
1746
+ *
1747
+ * @param out The destination array; grown to `id + 1` as elements arrive.
1748
+ * @param def The element default, written into a gap and into a reserved slot.
1749
+ * @param cap The schema `count` as an index capacity: `id >= cap` is `INVALID`
1750
+ * (§7.1) — a statement about validity. Pass {@link UNBOUNDED} (`-1`) for an array
1751
+ * the schema left open, where `receiverCap` governs instead.
1752
+ * @param name The schema field name, used only in a rejection message.
1753
+ * @param receiverCap The receiver-side index cap for a schema-unbounded array
1754
+ * (§6.2.1): `id >= receiverCap` is `LIMIT_EXCEEDED`, a policy rejection. There is
1755
+ * no unlimited setting — a wrapper array announces no count, so the index is the
1756
+ * only place a receiver can bound it. **Required, with no default**: the number is
1757
+ * generated code's, and falling back to `ARRAY_MAX` would report a policy
1758
+ * rejection against a format ceiling nobody configured (§6.2.1). A value that
1759
+ * states no cap at all — negative, `NaN`, `Infinity` — is `Argument` at
1760
+ * construction, never `LIMIT_EXCEEDED` ({@link requireReceiverBound}).
1761
+ */
1762
+ declare class ElementSeq<T> {
1763
+ readonly out: T[];
1764
+ readonly def: T;
1765
+ readonly cap: number;
1766
+ readonly name: string;
1767
+ readonly receiverCap: number;
1768
+ /**
1769
+ * The one index bound in force, picked once by `indexBound`: the schema
1770
+ * `count` where the schema stated one, the receiver cap where it did not. Which
1771
+ * of the two it is decides the verdict, which is `rejectIndex`'s job.
1772
+ */
1773
+ private readonly bound;
1774
+ constructor(out: T[], def: T, cap: number, name: string, receiverCap: number);
1775
+ /**
1776
+ * Bound-check `id` and grow `out` to `id + 1`, filling any gap — and the slot
1777
+ * itself — with the element default.
1778
+ *
1779
+ * The check runs **before** the growth, which is the whole of §7.2 item 8's
1780
+ * "after a rejected id the container is not left partially extended": a
1781
+ * rejection leaves `out` exactly as it was, so a lower id delivered afterwards
1782
+ * still lands at its own index.
1783
+ */
1784
+ reserve(id: number): void;
1785
+ /**
1786
+ * The two index bounds, without growing: the schema `count` as validity
1787
+ * (`INVALID`) or, where the schema left the array open, the receiver cap as
1788
+ * capacity (`LIMIT_EXCEEDED`). Never both — §6.2.1 keeps a cap off a field the
1789
+ * schema already bounds, which is why one `bound` can stand for both.
1790
+ *
1791
+ * Split out from {@link reserve} because a leaf element is bound-checked at its
1792
+ * length word, before its payload has arrived and so before there is anything to
1793
+ * place ({@link StringSeq.begin}).
1794
+ */
1795
+ checkIndex(id: number): void;
1796
+ /**
1797
+ * What {@link reserve} does, then `value` written into the slot. A repeat
1798
+ * replaces (§7.4).
1799
+ *
1800
+ * Written out rather than delegating to {@link reserve}: on the baseline tier a
1801
+ * call is not free, and this is the per-element path. The check still precedes
1802
+ * the growth, which is the property §7.2 item 8 asks for.
1803
+ */
1804
+ place(id: number, value: T): void;
1805
+ }
1806
+ /**
1807
+ * The slots of a wrapper-sequence array whose element default is a **fresh
1808
+ * object**: a framed element (`struct` / `union`) or a nested row.
1809
+ *
1810
+ * {@link ElementSeq}'s twin, and it differs in exactly one thing — the gap value
1811
+ * comes from a **factory** instead of being shared. That single axis is why it is
1812
+ * a second class rather than a second argument: a framed element's default is
1813
+ * `new Elem()` and a row's is `[]`, both mutable and both reachable by the
1814
+ * caller, so one shared instance would alias every gap of the array onto it and —
1815
+ * since an arriving element decodes into the slot the reservation placed — would
1816
+ * alias every *element* onto it too, which is not a near miss but a flatly wrong
1817
+ * decode. {@link ElementSeq}'s `""` and zero-length `Uint8Array` have no state to
1818
+ * share, which is what makes sharing right there and wrong here.
1819
+ *
1820
+ * Everything else is {@link ElementSeq}, deliberately: the same argument order
1821
+ * with `make` in `def`'s slot, the same two exclusive index bounds through the
1822
+ * same `indexBound` / `rejectIndex` pair, the same growth geometry, the same
1823
+ * construction-time refusal of a receiver cap that states no cap.
1824
+ *
1825
+ * Generated code calls {@link reserve} at the element's own `sequenceBegin` and
1826
+ * then keeps routing the child's fields into `out[id]` itself. This class owns
1827
+ * the bound and the growth; it never owns the routing, which is the part that has
1828
+ * a different shape for every schema (ARCHITECTURE §8).
1829
+ *
1830
+ * @param out The destination array; grown to `id + 1` as elements arrive.
1831
+ * @param make Builds one element default. Called once per gap slot, and once for
1832
+ * the reserved slot itself in {@link reserve} — never for a slot already present,
1833
+ * so a re-opened element keeps the object earlier fields decoded into.
1834
+ * @param cap The schema `count` as an index capacity: `id >= cap` is `INVALID`
1835
+ * (§7.1) — a statement about validity. Pass {@link UNBOUNDED} (`-1`) for an array
1836
+ * the schema left open, where `receiverCap` governs instead.
1837
+ * @param name The schema field name, used only in a rejection message.
1838
+ * @param receiverCap The receiver-side index cap for a schema-unbounded array
1839
+ * (§6.2.1): `id >= receiverCap` is `LIMIT_EXCEEDED`, a policy rejection, never
1840
+ * `INVALID`, and never applied beside a `cap` the schema stated. **Required, with
1841
+ * no default**; a value that states no cap at all is `Argument` at construction
1842
+ * ({@link requireReceiverBound}).
1843
+ */
1844
+ declare class FramedSeq<T> {
1845
+ readonly out: T[];
1846
+ readonly make: () => T;
1847
+ readonly cap: number;
1848
+ readonly name: string;
1849
+ readonly receiverCap: number;
1850
+ /** The one index bound in force, picked once by `indexBound` — see {@link ElementSeq}. */
1851
+ private readonly bound;
1852
+ constructor(out: T[], make: () => T, cap: number, name: string, receiverCap: number);
1853
+ /**
1854
+ * The two index bounds, without growing — see {@link ElementSeq.checkIndex}.
1855
+ *
1856
+ * Split out for the same reason it is there: a caller may have a second bound
1857
+ * to take before anything is allocated. A native matrix row is that case in this
1858
+ * port — its element `count` is rejected at the array header, and §7.2 item 8
1859
+ * wants that rejection to leave the row container exactly as it was.
1860
+ */
1861
+ checkIndex(id: number): void;
1862
+ /**
1863
+ * Bound-check `id`, **then** grow `out` to `id + 1`, each new slot its own
1864
+ * `make()`.
1865
+ *
1866
+ * The order is §7.2 item 8's "after a rejected id the container is not left
1867
+ * partially extended": a rejection leaves `out` exactly as it was, so a lower id
1868
+ * delivered afterwards still lands at its own index. A slot already present is
1869
+ * left alone — a re-opened `struct` / `union` element merges into the object it
1870
+ * already built, which is what §7.4's last-occurrence-wins means for a scope
1871
+ * whose value *is* the scope.
1872
+ */
1873
+ reserve(id: number): void;
1874
+ /**
1875
+ * Bound-check `id`, fill the gap below it, then write `value` into the slot —
1876
+ * what a nested **row** needs, an array wrapper *replacing* whatever an earlier
1877
+ * opening built at that index (§7.4) rather than merging into it.
1878
+ *
1879
+ * The gap fill stops one short of `id` on purpose: the slot is about to be
1880
+ * overwritten, so calling `make()` for it would allocate an element default
1881
+ * nobody ever reads. Assigning at `out.length` extends the array by exactly one,
1882
+ * which is the same array a {@link reserve} would have left.
1883
+ */
1884
+ place(id: number, value: T): void;
1885
+ }
1886
+ /**
1887
+ * Collects the elements of a `string` wrapper-sequence array into `out`.
1888
+ *
1889
+ * @param out The destination, placed at `out[id]`; grown as elements arrive, with
1890
+ * gaps filled by the element default `""`.
1891
+ * @param acc The decoder's shared {@link PayloadAcc} — one per decode, since only
1892
+ * one payload is ever in flight.
1893
+ * @param cap The schema `count`, an index **capacity**: an element id at or above
1894
+ * it is `INVALID` (§7.1/§5.1) — a statement about validity. Pass
1895
+ * {@link UNBOUNDED} (`-1`) for an array the schema left unbounded, where
1896
+ * `receiverCap` governs instead.
1897
+ * @param elemMax The element `maxlen` in bytes, or {@link UNBOUNDED} (`-1`) for an
1898
+ * element the schema left open, where `receiverElemMax` governs instead.
1899
+ * @param name The schema field name, used only in the rejection message.
1900
+ * @param receiverCap The receiver-side index cap that applies **only** when the
1901
+ * schema left the array unbounded (§6.2.1): exceeding it is `LIMIT_EXCEEDED`, a
1902
+ * policy rejection, not `INVALID`. There is no unlimited setting — a wrapper array
1903
+ * announces no count, so the index is the only place a receiver can bound it.
1904
+ * **Required, with no default** (§6.2.1).
1905
+ * @param receiverElemMax The receiver-side `max_dyn_string_len` for an element the
1906
+ * schema left unbounded (§6.2.1), checked at the length word and answered with
1907
+ * `LIMIT_EXCEEDED`. A wrapper array's `string` elements never reach the generated
1908
+ * visitor — their length words come here — so this is where that cap belongs, and
1909
+ * it is why the decoder needs no module-wide limit object at all. **Required, with
1910
+ * no default**: `FIXLEN_MAX` is the format's bound, not a receiver cap, and §6.2.1
1911
+ * forbids presenting it as one.
1912
+ */
1913
+ declare class StringSeq {
1914
+ readonly out: string[];
1915
+ readonly acc: PayloadAcc;
1916
+ readonly cap: number;
1917
+ readonly elemMax: number;
1918
+ readonly name: string;
1919
+ readonly receiverCap: number;
1920
+ readonly receiverElemMax: number;
1921
+ /** The index rules and both index bounds, shared with every other element kind. */
1922
+ private readonly slots;
1923
+ constructor(out: string[], acc: PayloadAcc, cap: number, elemMax: number, name: string, receiverCap: number, receiverElemMax: number);
1924
+ /**
1925
+ * The element's fixlen length word ({@link Visitor.fixlenBegin}).
1926
+ *
1927
+ * The bounds are decided by this word, so they are checked here — before any
1928
+ * payload byte — and again in {@link element} below.
1929
+ *
1930
+ * That is not redundancy for its own sake: a message that ends *inside* an
1931
+ * over-long element must still be `INVALID`, and only this event runs early
1932
+ * enough to say so. Without it the verdict would degrade to `INCOMPLETE`, which
1933
+ * §5.2.3 forbids for input already known to be malformed and which §6.4 forbids
1934
+ * a chunk boundary from changing.
1935
+ *
1936
+ * An element of the wrong fixlen subtype is left alone: §7.3 requires it to be
1937
+ * *skipped*, not rejected, and it is skipped by this class simply ignoring it.
1938
+ */
1939
+ begin(id: number, subtype: FixlenSubtype, total: number): void;
1940
+ /** One payload piece of element `id` ({@link Visitor.string}). */
1941
+ element(id: number, total: number, offset: number, src: Uint8Array, start: number, end: number): void;
1942
+ /** Every bound for one element. Rejects **before** the destination grows. */
1943
+ private check;
1944
+ }
1945
+ /**
1946
+ * Collects the elements of a `blob` wrapper-sequence array into `out`. The
1947
+ * `string` twin above, with two differences: the payload is stored as bytes rather
1948
+ * than decoded, and gaps are filled with an empty {@link Uint8Array}.
1949
+ *
1950
+ * Each element is storage of its own — what {@link PayloadAcc} returns is a buffer
1951
+ * it allocated and handed over, never a view into the fed chunk (§6.7) — so a
1952
+ * stored element cannot rot when the caller reuses its input.
1953
+ *
1954
+ * @param out The destination, placed at `out[id]`.
1955
+ * @param acc The decoder's shared {@link PayloadAcc}.
1956
+ * @param cap The schema `count` as an index capacity, or {@link UNBOUNDED} (`-1`)
1957
+ * for unbounded.
1958
+ * @param elemMax The element `maxlen` in bytes, or {@link UNBOUNDED} (`-1`) for an
1959
+ * element the schema left open, where `receiverElemMax` governs instead.
1960
+ * @param name The schema field name, used only in the rejection message.
1961
+ * @param receiverCap The receiver-side index cap for a schema-unbounded array
1962
+ * (§6.2.1) — see {@link StringSeq}. Required, with no default.
1963
+ * @param receiverElemMax The receiver-side `max_dyn_blob_len` for a schema-unbounded
1964
+ * element (§6.2.1) — see {@link StringSeq}. Required, with no default.
1965
+ */
1966
+ declare class BlobSeq {
1967
+ readonly out: Uint8Array[];
1968
+ readonly acc: PayloadAcc;
1969
+ readonly cap: number;
1970
+ readonly elemMax: number;
1971
+ readonly name: string;
1972
+ readonly receiverCap: number;
1973
+ readonly receiverElemMax: number;
1974
+ /** The index rules and both index bounds, shared with every other element kind. */
1975
+ private readonly slots;
1976
+ constructor(out: Uint8Array[], acc: PayloadAcc, cap: number, elemMax: number, name: string, receiverCap: number, receiverElemMax: number);
1977
+ /** See {@link StringSeq.begin} — the early bound check, for subtype `blob`. */
1978
+ begin(id: number, subtype: FixlenSubtype, total: number): void;
1979
+ /** One payload piece of element `id` ({@link Visitor.blob}). */
1980
+ element(id: number, total: number, offset: number, src: Uint8Array, start: number, end: number): void;
1981
+ /** Every bound for one element. Rejects **before** the destination grows. */
1982
+ private check;
1983
+ }
1984
+
1985
+ /**
1986
+ * Generated-layer support: the array half of the ≠-default test that decides
1987
+ * whether a field is written at all (MESSAGE_SPEC §2).
1988
+ *
1989
+ * SofaBuffers omits a field whose value equals its schema default, so every
1990
+ * generated `serialize` compares before it writes. For a scalar that is `!==`;
1991
+ * for an array it is element-wise, because two distinct `Uint8Array` /
1992
+ * `number[]` instances holding the same values are the same *value* and must
1993
+ * encode the same way — `a !== b` would emit the field whenever the destination
1994
+ * happened to be a fresh object, and the wire is supposed to be canonical.
1995
+ *
1996
+ * The comparison has no schema in it: the declared type only decides which
1997
+ * arrays are handed over. So it is written once here instead of being emitted
1998
+ * into every generated package (ARCHITECTURE §8).
1999
+ */
2000
+
2001
+ /**
2002
+ * Element-wise equality for two array-likes: same length, and `===` at every
2003
+ * index. Covers what a leaf array field can hold — `Uint8Array` (blob), and
2004
+ * `number[]` / `bigint[]` / `boolean[]` / `string[]` — including a mixed pair
2005
+ * such as a `Uint8Array` value against a plain-array default from the schema.
2006
+ *
2007
+ * Deliberately **not** deep: a nested (wrapper) array of messages is compared by
2008
+ * the generated `isDefault()` of its element type, which is the only code that
2009
+ * knows what a default element is. Equally deliberately `===`, which makes `NaN`
2010
+ * unequal to itself — matching the scalar `!==` test, the IEEE-754 rule the rest
2011
+ * of the encoder follows, and the only reading under which an `fp32`/`fp64`
2012
+ * `NaN` element survives the round trip rather than being omitted as "default".
2013
+ * There is no identity short-circuit for the same reason: `elementsEqual(x, x)`
2014
+ * has to give the same answer as `elementsEqual(x, x.slice())`.
2015
+ */
2016
+ declare function elementsEqual(a: ArrayLike<unknown>, b: ArrayLike<unknown>): boolean;
2017
+ /**
2018
+ * The {@link Long} flavour of {@link elementsEqual}: same length, and the same
2019
+ * 64-bit value at every index.
2020
+ *
2021
+ * A separate function rather than an argument to the one above, because the
2022
+ * comparison itself is different. A `Long` is an *object identity* — two
2023
+ * instances holding the same 64 bits are `!==` — so `elementsEqual`'s `===` would
2024
+ * report every `Long`-backed array as unequal to its schema default and the
2025
+ * ≠-default test (MESSAGE_SPEC §2) would emit a field that should have been
2026
+ * omitted. Comparing the `(low, high)` halves is what "same value" means for this
2027
+ * representation, and it is the only 64-bit representation that needs it:
2028
+ * `bigint` and `number` elements are values already and go through
2029
+ * {@link elementsEqual}.
2030
+ *
2031
+ * Which arrays are handed over is the declared type's business, so there is no
2032
+ * schema in here either (ARCHITECTURE §8).
2033
+ */
2034
+ declare function longElementsEqual(a: ArrayLike<Long>, b: ArrayLike<Long>): boolean;
2035
+
2036
+ /**
2037
+ * A fresh 4-byte companion holding one fp32 word's wire image: the four
2038
+ * little-endian bytes of its 32-bit word, byte `k` in bits `8*k`.
2039
+ *
2040
+ * Generated-layer support, not a codec path: an `fp32` field whose value is a
2041
+ * `NaN` cannot be re-encoded from the `number` a JS host stored it in — the host
2042
+ * normalizes the payload bits — so the generated message keeps the four raw wire
2043
+ * bytes beside the value and re-emits those (MESSAGE_SPEC §6.5). What the decoder
2044
+ * hands over is the 32-bit *word*, because a number costs nothing to pass where
2045
+ * the byte view it replaced was an allocation per value and a borrowed slice
2046
+ * §6.7 forbids; turning that word back into bytes is this function, and it is the
2047
+ * same four shifts for every schema (ARCHITECTURE §8).
2048
+ *
2049
+ * One function and not two. An `into(out, off, bits)` flavour beside it would be
2050
+ * the natural companion — it is what the fp32 *array* path would want — but that
2051
+ * path was retired before this one moved here (§6.7 killed the view), so the
2052
+ * whole family builds this companion per `NaN` scalar and nothing else calls it.
2053
+ * Public API with no consumer drifts, so there is exactly one entry point and it
2054
+ * allocates: the allocation is per `NaN`, not per field.
2055
+ */
2056
+ declare function fp32RawBytes(bits: number): Uint8Array;
2057
+
877
2058
  /**
878
2059
  * The acceleration seam.
879
2060
  *
880
2061
  * The encoder's bulk array paths run through a {@link Kernel} — a small set of
881
2062
  * self-contained, buffer-oriented transforms over a region the caller has
882
2063
  * already sized. The default {@link "./js"} kernel is pure TypeScript and works
883
- * everywhere; a C++ (N-API) or WebAssembly build can implement the same
884
- * interface and be swapped in with {@link setKernel} for a speed-up, with **no
885
- * change to the public API**. The boundary is deliberately *bulk* (a whole
2064
+ * everywhere, and is the active kernel unless the caller replaces it: no
2065
+ * accelerated build is published, and this interface is the entire seam for one
2066
+ * (#115). A C++ (N-API) or WebAssembly build implements the same interface and
2067
+ * is swapped in with {@link setKernel} — the caller loads its own module and
2068
+ * hands over the kernel it builds — for a speed-up with **no change to the
2069
+ * public API**. The boundary is deliberately *bulk* (a whole
886
2070
  * array per call, into guaranteed capacity) so the cost of crossing into native
887
2071
  * code is amortised — never one call per element.
888
2072
  */
889
2073
  /**
890
- * Bulk, capacity-guaranteed transforms used on the encoder's fast path.
2074
+ * Bulk transforms used on the encoder's fast path.
891
2075
  *
892
- * Every method writes into `out` starting at `pos`, assuming the caller has
893
- * already ensured enough room, and returns the position just past the last byte
894
- * written. Headers, counts, flushing and validation stay in the stream classes;
895
- * a kernel only moves bytes.
2076
+ * Every method writes into `out` starting at `pos` and returns the position just
2077
+ * past the last byte written.
2078
+ *
2079
+ * **`out.length` is the bound, and the only one.** The two packers are handed a
2080
+ * region the caller has already sized exactly (4 or 8 bytes an element, a number
2081
+ * the caller can compute), so for them "enough room" is a guarantee. The two
2082
+ * *varint* kernels are not: a varint's length depends on the value, so the caller
2083
+ * cannot know the size without encoding it, and asking the source how wide its
2084
+ * elements are is exactly the guess that silently truncated messages (§5.1). So a
2085
+ * varint kernel must
2086
+ *
2087
+ * * **never write at or past `out.length`**, and
2088
+ * * **keep counting anyway**, returning the position it *would* have reached.
2089
+ *
2090
+ * The caller compares that against `out.length`: past the end means the message
2091
+ * does not fit, which in the block mode is `BUFFER_FULL` — the answer that mode
2092
+ * exists to give. The JS kernel satisfies this for free (a typed-array store past
2093
+ * the end is dropped while `pos` keeps advancing); a native or WebAssembly kernel
2094
+ * must implement it deliberately, counting the remaining elements without storing. Headers, counts and flushing stay in the stream classes; a kernel
2095
+ * only moves bytes — with one obligation it cannot delegate, because it is the
2096
+ * only code that ever looks at the elements: **the integer kernels must reject
2097
+ * an element outside the 64-bit value domain** (CORELIB_PLAN §6.2 — `0 .. 2^64
2098
+ * - 1` unsigned, `-2^63 .. 2^63 - 1` signed) by throwing `argumentError`,
2099
+ * rather than reducing it modulo 2^64. The stream classes range-check the same
2100
+ * values on their element-at-a-time streaming path, so a kernel that skipped
2101
+ * the check would make the wire depend on which constructor the caller used
2102
+ * (#106). The check is one store, one load and one compare on the `bigint`
2103
+ * branch (`splitU64` / `splitI64`); the `number` fast paths are already gated
2104
+ * on the domain and pay nothing.
896
2105
  */
897
2106
  interface Kernel {
898
2107
  /** A short identifier, surfaced in diagnostics and the parity tests. */
@@ -927,36 +2136,6 @@ declare function getKernel(): Kernel;
927
2136
  */
928
2137
  declare const jsKernel: Kernel;
929
2138
 
930
- /**
931
- * Attempt to load and install the native kernel.
932
- *
933
- * @returns `true` if a valid native kernel was installed, `false` if it could
934
- * not be loaded (not on Node, addon not installed, or wrong shape). Never
935
- * throws for a missing addon — the JS kernel remains the fallback.
936
- */
937
- declare function loadNativeKernel(): Promise<boolean>;
938
-
939
- /**
940
- * Optional WebAssembly acceleration loader.
941
- *
942
- * Unlike the native addon, a WASM kernel runs in the browser too. This module
943
- * does nothing at import time; call {@link loadWasmKernel} with the compiled
944
- * module's bytes (or a streaming source) to instantiate it and install it as
945
- * the active {@link Kernel}. A real WASM build is shipped separately; this
946
- * loader is the stable entry point the rest of the library is wired through.
947
- */
948
-
949
- /** A factory the WASM glue exposes: given the instance exports, build a Kernel. */
950
- type WasmKernelFactory = (exports: WebAssembly.Exports) => Kernel;
951
- /**
952
- * Instantiate a WASM module and install the kernel it produces.
953
- *
954
- * @param source compiled-module bytes, a `Response`/stream, or a ready module.
955
- * @param factory wraps the instance exports into a {@link Kernel}.
956
- * @returns `true` once installed; throws only if instantiation itself fails.
957
- */
958
- declare function loadWasmKernel(source: BufferSource | Response | PromiseLike<Response> | WebAssembly.Module, factory: WasmKernelFactory, imports?: WebAssembly.Imports): Promise<boolean>;
959
-
960
2139
  /**
961
2140
  * The complete public surface of the library, re-exported by {@link "./index"}
962
2141
  * both as flat named exports and, aggregated, under the `sofab` namespace.
@@ -965,39 +2144,55 @@ declare function loadWasmKernel(source: BufferSource | Response | PromiseLike<Re
965
2144
  declare const _public_API_VERSION: typeof API_VERSION;
966
2145
  declare const _public_ARRAY_MAX: typeof ARRAY_MAX;
967
2146
  type _public_ArrayKind = ArrayKind;
968
- type _public_Cursor = Cursor;
969
- declare const _public_Cursor: typeof Cursor;
970
- type _public_DecodeLimits = DecodeLimits;
2147
+ type _public_ArrayTarget = ArrayTarget;
2148
+ type _public_BlobSeq = BlobSeq;
2149
+ declare const _public_BlobSeq: typeof BlobSeq;
2150
+ type _public_BoolArrayTarget = BoolArrayTarget;
971
2151
  type _public_DecodeStatus = DecodeStatus;
2152
+ type _public_ElementSeq<T> = ElementSeq<T>;
2153
+ declare const _public_ElementSeq: typeof ElementSeq;
972
2154
  declare const _public_FIXLEN_MAX: typeof FIXLEN_MAX;
2155
+ type _public_FeedStatus = FeedStatus;
973
2156
  type _public_FixlenSubtype = FixlenSubtype;
2157
+ type _public_FloatArrayTarget = FloatArrayTarget;
974
2158
  type _public_FlushSink = FlushSink;
2159
+ type _public_FramedSeq<T> = FramedSeq<T>;
2160
+ declare const _public_FramedSeq: typeof FramedSeq;
975
2161
  declare const _public_I64_MAX: typeof I64_MAX;
976
2162
  declare const _public_I64_MIN: typeof I64_MIN;
977
2163
  declare const _public_ID_MAX: typeof ID_MAX;
978
2164
  type _public_IStream = IStream;
979
2165
  declare const _public_IStream: typeof IStream;
2166
+ type _public_IntegerArrayTarget = IntegerArrayTarget;
980
2167
  type _public_Kernel = Kernel;
981
2168
  type _public_Long = Long;
982
2169
  declare const _public_Long: typeof Long;
983
2170
  declare const _public_MAX_DEPTH: typeof MAX_DEPTH;
2171
+ declare const _public_MIN_OUTPUT_BUFFER: typeof MIN_OUTPUT_BUFFER;
984
2172
  type _public_OStream = OStream;
985
2173
  declare const _public_OStream: typeof OStream;
2174
+ type _public_PayloadAcc = PayloadAcc;
2175
+ declare const _public_PayloadAcc: typeof PayloadAcc;
986
2176
  type _public_SofabError = SofabError;
987
2177
  declare const _public_SofabError: typeof SofabError;
988
2178
  type _public_SofabErrorCode = SofabErrorCode;
2179
+ type _public_StringSeq = StringSeq;
2180
+ declare const _public_StringSeq: typeof StringSeq;
989
2181
  declare const _public_U64_MAX: typeof U64_MAX;
2182
+ declare const _public_UNBOUNDED: typeof UNBOUNDED;
990
2183
  type _public_Visitor = Visitor;
991
- type _public_WasmKernelFactory = WasmKernelFactory;
992
2184
  type _public_WireType = WireType;
993
2185
  declare const _public_decode: typeof decode;
2186
+ declare const _public_decodeUtf8: typeof decodeUtf8;
2187
+ declare const _public_elementsEqual: typeof elementsEqual;
2188
+ declare const _public_fp32RawBytes: typeof fp32RawBytes;
994
2189
  declare const _public_getKernel: typeof getKernel;
2190
+ declare const _public_growingOStream: typeof growingOStream;
995
2191
  declare const _public_jsKernel: typeof jsKernel;
996
- declare const _public_loadNativeKernel: typeof loadNativeKernel;
997
- declare const _public_loadWasmKernel: typeof loadWasmKernel;
2192
+ declare const _public_longElementsEqual: typeof longElementsEqual;
998
2193
  declare const _public_setKernel: typeof setKernel;
999
2194
  declare namespace _public {
1000
- export { _public_API_VERSION as API_VERSION, _public_ARRAY_MAX as ARRAY_MAX, type _public_ArrayKind as ArrayKind, _public_Cursor as Cursor, type _public_DecodeLimits as DecodeLimits, type _public_DecodeStatus as DecodeStatus, _public_FIXLEN_MAX as FIXLEN_MAX, type _public_FixlenSubtype as FixlenSubtype, type _public_FlushSink as FlushSink, _public_I64_MAX as I64_MAX, _public_I64_MIN as I64_MIN, _public_ID_MAX as ID_MAX, _public_IStream as IStream, type _public_Kernel as Kernel, _public_Long as Long, _public_MAX_DEPTH as MAX_DEPTH, _public_OStream as OStream, _public_SofabError as SofabError, type _public_SofabErrorCode as SofabErrorCode, _public_U64_MAX as U64_MAX, type _public_Visitor as Visitor, type _public_WasmKernelFactory as WasmKernelFactory, type _public_WireType as WireType, _public_decode as decode, _public_getKernel as getKernel, _public_jsKernel as jsKernel, _public_loadNativeKernel as loadNativeKernel, _public_loadWasmKernel as loadWasmKernel, _public_setKernel as setKernel };
2195
+ export { _public_API_VERSION as API_VERSION, _public_ARRAY_MAX as ARRAY_MAX, type _public_ArrayKind as ArrayKind, type _public_ArrayTarget as ArrayTarget, _public_BlobSeq as BlobSeq, type _public_BoolArrayTarget as BoolArrayTarget, type _public_DecodeStatus as DecodeStatus, _public_ElementSeq as ElementSeq, _public_FIXLEN_MAX as FIXLEN_MAX, type _public_FeedStatus as FeedStatus, type _public_FixlenSubtype as FixlenSubtype, type _public_FloatArrayTarget as FloatArrayTarget, type _public_FlushSink as FlushSink, _public_FramedSeq as FramedSeq, _public_I64_MAX as I64_MAX, _public_I64_MIN as I64_MIN, _public_ID_MAX as ID_MAX, _public_IStream as IStream, type _public_IntegerArrayTarget as IntegerArrayTarget, type _public_Kernel as Kernel, _public_Long as Long, _public_MAX_DEPTH as MAX_DEPTH, _public_MIN_OUTPUT_BUFFER as MIN_OUTPUT_BUFFER, _public_OStream as OStream, _public_PayloadAcc as PayloadAcc, _public_SofabError as SofabError, type _public_SofabErrorCode as SofabErrorCode, _public_StringSeq as StringSeq, _public_U64_MAX as U64_MAX, _public_UNBOUNDED as UNBOUNDED, type _public_Visitor as Visitor, type _public_WireType as WireType, _public_decode as decode, _public_decodeUtf8 as decodeUtf8, _public_elementsEqual as elementsEqual, _public_fp32RawBytes as fp32RawBytes, _public_getKernel as getKernel, _public_growingOStream as growingOStream, _public_jsKernel as jsKernel, _public_longElementsEqual as longElementsEqual, _public_setKernel as setKernel };
1001
2196
  }
1002
2197
 
1003
- export { API_VERSION, ARRAY_MAX, ArrayKind, Cursor, type DecodeLimits, DecodeStatus, FIXLEN_MAX, FixlenSubtype, type FlushSink, I64_MAX, I64_MIN, ID_MAX, IStream, type Kernel, Long, MAX_DEPTH, OStream, SofabError, SofabErrorCode, U64_MAX, type Visitor, type WasmKernelFactory, WireType, decode, getKernel, jsKernel, loadNativeKernel, loadWasmKernel, setKernel, _public as sofab };
2198
+ export { API_VERSION, ARRAY_MAX, ArrayKind, type ArrayTarget, BlobSeq, type BoolArrayTarget, DecodeStatus, ElementSeq, FIXLEN_MAX, type FeedStatus, FixlenSubtype, type FloatArrayTarget, type FlushSink, FramedSeq, I64_MAX, I64_MIN, ID_MAX, IStream, type IntegerArrayTarget, type Kernel, Long, MAX_DEPTH, MIN_OUTPUT_BUFFER, OStream, PayloadAcc, SofabError, SofabErrorCode, StringSeq, U64_MAX, UNBOUNDED, type Visitor, WireType, decode, decodeUtf8, elementsEqual, fp32RawBytes, getKernel, growingOStream, jsKernel, longElementsEqual, setKernel, _public as sofab };