1/**
2 * A compact binary codec derived from the encoded side of a Schema.
3 *
4 * The default wire format supports compatible schema evolution. An array of
5 * structs is written as a row run: rows declare their shape once and
6 * back-reference repeated strings, so both stay off the wire on later rows.
7 * Fingerprint mode uses positional layouts and rejects mismatches for smaller
8 * frames; its row shapes are presence masks instead of field id lists. Encoded
9 * results are arena-backed views; see {@link toCodec} for ownership details.
10 *
11 * @stability unstable
12 * @since 4.0.0
13 */
14import * as Arr from "../Array.ts"
15import * as BigDecimal from "../BigDecimal.ts"
16import * as Cause from "../Cause.ts"
17import * as Channel from "../Channel.ts"
18import * as Chunk from "../Chunk.ts"
19import * as DateTime from "../DateTime.ts"
20import * as Duration from "../Duration.ts"
21import * as Effect from "../Effect.ts"
22import * as Exit from "../Exit.ts"
23import { dual, memoize } from "../Function.ts"
24import * as HashMap from "../HashMap.ts"
25import * as HashSet from "../HashSet.ts"
26import { assignProperty } from "../internal/record.ts"
27import * as InternalParserProtocol from "../internal/schema/parser.ts"
28import * as Option from "../Option.ts"
29import * as Predicate from "../Predicate.ts"
30import * as Pull from "../Pull.ts"
31import * as Redacted from "../Redacted.ts"
32import * as Result from "../Result.ts"
33import * as Schema from "../Schema.ts"
34import * as SchemaAST from "../SchemaAST.ts"
35import * as SchemaIssue from "../SchemaIssue.ts"
36import * as SchemaParser from "../SchemaParser.ts"
37import * as SchemaTransformation from "../SchemaTransformation.ts"
38
39/**
40 * Selects the wire mode.
41 *
42 * **Details**
43 *
44 * The default mode supports compatible schema evolution. `fingerprint: true`
45 * uses positional layouts and an 8-byte layout hash for smaller frames, but
46 * requires peers to use the same schema definition.
47 *
48 * @stability unstable
49 * @category models
50 * @since 4.0.0
51 */
52export interface Options {
53 /**
54 * Uses a compact positional layout with a schema fingerprint.
55 *
56 * @stability unstable
57 * @since 4.0.0
58 */
59 readonly fingerprint?: boolean | undefined
60}
61
62/**
63 * The codec type returned by {@link toCodec}.
64 *
65 * @stability unstable
66 * @category models
67 * @since 4.0.0
68 */
69export interface toCodec<S extends Schema.Constraint> extends
70 Schema.Codec<
71 S["Type"],
72 Uint8Array<ArrayBuffer>,
73 S["DecodingServices"],
74 S["EncodingServices"]
75 >
76{}
77
78const codecCache = new WeakMap<Schema.Constraint, toCodec<any>>()
79const fingerprintCodecCache = new WeakMap<Schema.Constraint, toCodec<any>>()
80const directCodecCache = new WeakMap<Schema.Constraint, toCodec<any>>()
81const directFingerprintCodecCache = new WeakMap<Schema.Constraint, toCodec<any>>()
82
83/**
84 * Derives a compact binary codec from a schema.
85 *
86 * **Details**
87 *
88 * The wire layout is compiled from the encoded side of the schema. Each
89 * encode/decode handles exactly one frame; use {@link parser} for streams.
90 * Derived codecs are memoized by schema identity and wire mode.
91 *
92 * Encoded results are arena-backed views and may share a larger buffer. Use
93 * `bytes.slice()` when independent ownership is required.
94 *
95 * **Example** (Deriving a binary codec)
96 *
97 * ```ts
98 * import { Schema } from "effect"
99 * import { SchemaBinary } from "effect/encoding"
100 *
101 * const Person = Schema.Struct({ name: Schema.String, age: Schema.Number })
102 * const codec = SchemaBinary.toCodec(Person)
103 *
104 * const bytes = Schema.encodeUnknownSync(codec)({ name: "Ada", age: 36 })
105 * const person = Schema.decodeUnknownSync(codec)(bytes)
106 * ```
107 *
108 * @stability unstable
109 * @category constructors
110 * @since 4.0.0
111 */
112export function toCodec<S extends Schema.Constraint>(schema: S, options?: Options): toCodec<S> {
113 const cache = options?.fingerprint === true ? fingerprintCodecCache : codecCache
114 const cached = cache.get(schema)
115 if (cached !== undefined) return cached
116 const { exact, layout, recursive, target } = compileTarget(schema)
117 const mode = compileMode(layout, options?.fingerprint)
118 const trusted: Trusted | undefined = exact ? { value: undefined } : undefined
119 const codec = assembleCodec<S>(
120 trusted === undefined ? target : withTrustedDecode(target, trusted, !recursive),
121 layout,
122 mode,
123 trusted
124 )
125 cache.set(schema, codec)
126 return codec
127}
128
129/**
130 * Derives the {@link toCodec} codec without the schema pass around the binary
131 * layer.
132 *
133 * Only schemas the binary layer already validates on its own take the direct
134 * path; anything else falls back to {@link toCodec}. A direct codec encodes and
135 * decodes the same values as {@link toCodec} but is not a sound `Schema.is`
136 * guard, so it is only for callers that never guard on it. Derived codecs are
137 * memoized by schema identity and wire mode.
138 *
139 * @internal
140 */
141export function toCodecDirect<S extends Schema.Constraint>(schema: S, options?: Options): toCodec<S> {
142 const cache = options?.fingerprint === true ? directFingerprintCodecCache : directCodecCache
143 const cached = cache.get(schema)
144 if (cached !== undefined) return cached
145 const { exact, exitSuccess, layout, target } = compileTarget(schema)
146 if (!exact) {
147 if (!exitSuccess) return toCodec(schema, options)
148 const trusted: Trusted = { value: undefined }
149 const codec = assembleCodec<S>(
150 withExitSuccessDecode(target, trusted),
151 layout,
152 compileMode(layout, options?.fingerprint),
153 trusted,
154 true
155 )
156 cache.set(schema, codec)
157 return codec
158 }
159 const codec = assembleCodec<S>(
160 passThrough(target),
161 layout,
162 compileMode(layout, options?.fingerprint),
163 undefined
164 )
165 cache.set(schema, codec)
166 return codec
167}
168
169function assembleCodec<S extends Schema.Constraint>(
170 target: Schema.Constraint,
171 layout: Layout,
172 mode: Mode,
173 trusted: Trusted | undefined,
174 successOnly = false
175): toCodec<S> {
176 return (Schema.Uint8Array as Schema.instanceOf<Uint8Array<ArrayBuffer>>).pipe(
177 Schema.decodeTo(target, makeTransformation(layout, mode, trusted, successOnly))
178 ) as unknown as toCodec<S>
179}
180
181/**
182 * Encodes one frame without the schema pass {@link toCodec} wraps around it.
183 *
184 * Only schemas the binary layer already validates on its own take the direct
185 * path; anything else falls back to the codec, so checks and transformations
186 * still run.
187 *
188 * @internal
189 */
190export function encodeUnknownSync<S extends Schema.Constraint>(
191 schema: S,
192 options?: SchemaAST.ParseOptions & Options
193): (value: unknown) => Uint8Array<ArrayBuffer> {
194 const { exact, exitSuccess, layout } = compileTarget(schema)
195 const mode = compileMode(layout, options?.fingerprint)
196 const parseOptions: SchemaAST.ParseOptions = options ?? SchemaAST.defaultParseOptions
197 if (!exact || parseOptions.onExcessProperty === "error") {
198 const fallback = Schema.encodeUnknownSync(
199 toCodec(schema, options) as unknown as Schema.ConstraintEncoder<unknown, never>,
200 options
201 ) as (value: unknown) => Uint8Array<ArrayBuffer>
202 // The binary layer drops excess keys instead of reporting them.
203 if (!exitSuccess || parseOptions.onExcessProperty === "error") return fallback
204 return (value) => {
205 if (!isSuccessExit(value)) return fallback(value)
206 try {
207 return encodeFrame(layout, value, parseOptions, mode)
208 } catch (e) {
209 throw e instanceof IssueError ? new Schema.SchemaError(e.issue) : e
210 }
211 }
212 }
213 return (value) => {
214 try {
215 return encodeFrame(layout, value, parseOptions, mode)
216 } catch (e) {
217 throw e instanceof IssueError ? new Schema.SchemaError(e.issue) : e
218 }
219 }
220}
221
222/**
223 * Encodes concatenated frames in one writer pass, so a batch costs no more
224 * allocations than a single frame.
225 *
226 * Shares {@link encodeUnknownSync}'s direct path and its fallback.
227 *
228 * @internal
229 */
230export function encodeManyUnknownSync<S extends Schema.Constraint>(
231 schema: S,
232 options?: SchemaAST.ParseOptions & Options
233): (values: ReadonlyArray<unknown>) => Uint8Array<ArrayBuffer> {
234 const { exact, layout } = compileTarget(schema)
235 const parseOptions: SchemaAST.ParseOptions = options ?? SchemaAST.defaultParseOptions
236 if (!exact || parseOptions.onExcessProperty === "error") {
237 const encode = encodeUnknownSync(schema, options)
238 return (values) => concatFrames(values.map(encode))
239 }
240 const mode = compileMode(layout, options?.fingerprint)
241 return (values) => {
242 try {
243 return encodeFrames(layout, values, parseOptions, mode)
244 } catch (e) {
245 throw e instanceof IssueError ? new Schema.SchemaError(e.issue) : e
246 }
247 }
248}
249
250function concatFrames(frames: ReadonlyArray<Uint8Array<ArrayBuffer>>): Uint8Array<ArrayBuffer> {
251 if (frames.length === 1) return frames[0]
252 let length = 0
253 for (const frame of frames) length += frame.length
254 const out = new Uint8Array(length)
255 let offset = 0
256 for (const frame of frames) {
257 out.set(frame, offset)
258 offset += frame.length
259 }
260 return out
261}
262
263/**
264 * A stateful frame parser for concatenated {@link toCodec} outputs.
265 *
266 * @stability unstable
267 * @category models
268 * @since 4.0.0
269 */
270export interface Parser<T> {
271 /**
272 * Feeds another chunk into the parser and returns any completed values.
273 *
274 * @stability unstable
275 * @since 4.0.0
276 */
277 feed(chunk: Uint8Array): Effect.Effect<ReadonlyArray<T>, Schema.SchemaError>
278 /**
279 * Feeds another chunk into the parser synchronously.
280 *
281 * @stability unstable
282 * @since 4.0.0
283 */
284 feedSync(chunk: Uint8Array): ReadonlyArray<T>
285 /**
286 * Finishes parsing and fails if an incomplete frame remains.
287 *
288 * @stability unstable
289 * @since 4.0.0
290 */
291 end: Effect.Effect<void, Schema.SchemaError>
292 /**
293 * Finishes parsing synchronously and throws for an incomplete frame.
294 *
295 * @stability unstable
296 * @since 4.0.0
297 */
298 endSync(): void
299}
300
301/**
302 * Creates a stateful parser for a stream of concatenated frames.
303 *
304 * **Details**
305 *
306 * Values completed before a failure remain observable. After a failure, the
307 * parser rejects further calls. Use `maxFrameSize` to limit buffered frames.
308 *
309 * @stability unstable
310 * @category constructors
311 * @since 4.0.0
312 */
313export function parser<S extends Schema.Constraint>(
314 schema: S,
315 options?: SchemaAST.ParseOptions & Options & StreamOptions & { readonly maxFrameSize?: number | undefined }
316): Parser<S["Type"]> {
317 const { exact, exitSuccess, layout, target } = compileTarget(schema)
318 const parseOptions: SchemaAST.ParseOptions = options ?? SchemaAST.defaultParseOptions
319 const decodeTarget = exact
320 ? undefined
321 : Schema.decodeUnknownSync(target as Schema.ConstraintDecoder<unknown>, parseOptions)
322 const decodeEncoded = decodeTarget !== undefined && exitSuccess
323 ? (value: unknown) => isSuccessExit(value) ? value : decodeTarget(value)
324 : decodeTarget
325 return makeParser(
326 layout,
327 compileMode(layout, options?.fingerprint),
328 parseOptions,
329 options?.maxFrameSize,
330 decodeEncoded,
331 requireDictionary(exact, options)
332 )
333}
334
335/**
336 * Options for the connection-scoped pair, {@link encoder} and {@link parser}.
337 *
338 * @stability unstable
339 * @category models
340 * @since 4.0.0
341 */
342export interface StreamOptions {
343 /**
344 * Share one string dictionary across every frame on the connection, so a
345 * string that repeats costs a reference after the first frame that carries
346 * it. Frames stop standing alone: they only decode through the parser that
347 * saw the frames before them, in order.
348 *
349 * **Gotchas**
350 *
351 * Both ends have to set it. A schema the binary layer does not fully
352 * validate on its own cannot use it, and asking for it throws.
353 *
354 * @stability unstable
355 * @since 4.0.0
356 */
357 readonly dictionary?: boolean | undefined
358}
359
360function requireDictionary(exact: boolean, options: StreamOptions | undefined): boolean {
361 if (options?.dictionary !== true) return false
362 if (!exact) {
363 throw new Error("Binary layout: dictionary needs a schema the binary layer validates on its own")
364 }
365 return true
366}
367
368/**
369 * A writer for one connection. Every frame it produces shares the string
370 * dictionary that the matching {@link parser} rebuilds as it reads them, so the
371 * two have to be created from the same schema and the same options.
372 *
373 * @stability unstable
374 * @category constructors
375 * @since 4.0.0
376 */
377export function encoder<S extends Schema.Constraint>(
378 schema: S,
379 options?: SchemaAST.ParseOptions & Options & StreamOptions
380): Encoder {
381 const { exact, layout, target } = compileTarget(schema)
382 const dictionary = requireDictionary(exact, options)
383 const encode = encodeUnknownSync(schema, options)
384 const encodeMany = encodeManyUnknownSync(schema, options)
385 if (!dictionary) return { encode, encodeMany }
386 const mode = compileMode(layout, options?.fingerprint)
387 const parseOptions: SchemaAST.ParseOptions = options ?? SchemaAST.defaultParseOptions
388 const validate = parseOptions.onExcessProperty === "error"
389 ? Schema.encodeUnknownSync(
390 target as Schema.ConstraintEncoder<unknown, never>,
391 options
392 ) as (value: unknown) => unknown
393 : undefined
394 const dict = makeDictWrite()
395 const run = <A>(f: () => A): A => {
396 try {
397 return f()
398 } catch (e) {
399 throw e instanceof IssueError ? new Schema.SchemaError(e.issue) : e
400 }
401 }
402 return {
403 encode: (value) =>
404 run(() => encodeFrame(layout, validate === undefined ? value : validate(value), parseOptions, mode, dict)),
405 encodeMany: (values) =>
406 run(() =>
407 encodeFrames(
408 layout,
409 validate === undefined ? values : values.map(validate),
410 parseOptions,
411 mode,
412 dict
413 )
414 )
415 }
416}
417
418/**
419 * The writer returned by {@link encoder}.
420 *
421 * @stability unstable
422 * @category models
423 * @since 4.0.0
424 */
425export interface Encoder {
426 encode(value: unknown): Uint8Array<ArrayBuffer>
427 encodeMany(values: ReadonlyArray<unknown>): Uint8Array<ArrayBuffer>
428}
429
430// Frame parser over a compiled layout. `decodeEncoded` runs the target schema
431// pass on each framed value; `undefined` yields the raw binary-decoded values.
432function makeParser<T>(
433 layout: Layout,
434 mode: Mode,
435 parseOptions: SchemaAST.ParseOptions,
436 maxFrameSize: number | undefined,
437 decodeEncoded: ((value: unknown) => unknown) | undefined,
438 dictionary: boolean
439): Parser<T> {
440 const dict: DictRead | undefined = dictionary ? [] : undefined
441 let buffer = new Uint8Array(0)
442 let bufferStart = 0
443 let bufferEnd = 0
444 let stashed: Schema.SchemaError | undefined
445 let spent = false
446 const body = new Reader()
447 const indexSignatures = new IndexSignatureCache(parseOptions, PARSER_INDEX_SIGNATURE_CACHE_SIZE)
448
449 const release = () => {
450 body.release()
451 indexSignatures.clear()
452 buffer = EMPTY_READER_BUFFER
453 bufferStart = bufferEnd = 0
454 }
455
456 const failSync = (expected: string, input?: unknown): never => {
457 throw new Schema.SchemaError(new SchemaIssue.InvalidValue({ expected }, input, parseOptions))
458 }
459
460 const takeStashed = (): never => {
461 const error = stashed!
462 stashed = undefined
463 throw error
464 }
465
466 // Copy the unconsumed tail of a directly parsed chunk into the buffer.
467 const stashTail = (chunk: Uint8Array, start: number) => {
468 const remaining = chunk.length - start
469 if (remaining > buffer.length) {
470 buffer = new Uint8Array(Math.max(256, remaining))
471 }
472 buffer.set(start === 0 ? chunk : chunk.subarray(start), 0)
473 bufferStart = 0
474 bufferEnd = remaining
475 }
476
477 const self: Parser<T> = {
478 feedSync(chunk) {
479 if (stashed !== undefined) return takeStashed()
480 if (spent) return failSync("parser is spent")
481 // Complete frames in a fresh chunk parse in place; only partial frames
482 // are ever copied into the buffer.
483 const direct = bufferEnd === bufferStart
484 let buf: Uint8Array
485 let pos: number
486 let end: number
487 if (direct) {
488 buf = chunk
489 pos = 0
490 end = chunk.length
491 } else {
492 if (chunk.length > 0) {
493 const remaining = bufferEnd - bufferStart
494 const required = remaining + chunk.length
495 if (required > buffer.length) {
496 const next = new Uint8Array(Math.max(256, buffer.length * 2, required))
497 next.set(buffer.subarray(bufferStart, bufferEnd))
498 buffer = next
499 bufferStart = 0
500 bufferEnd = remaining
501 } else if (bufferStart > 0) {
502 buffer.copyWithin(0, bufferStart, bufferEnd)
503 bufferStart = 0
504 bufferEnd = remaining
505 }
506 buffer.set(chunk, bufferEnd)
507 bufferEnd += chunk.length
508 }
509 buf = buffer
510 pos = bufferStart
511 end = bufferEnd
512 }
513 const out: Array<T> = []
514 const done = (): Array<T> => {
515 if (direct) {
516 if (pos < end) stashTail(chunk, pos)
517 } else {
518 bufferStart = pos
519 if (bufferStart === bufferEnd) bufferStart = bufferEnd = 0
520 }
521 return out
522 }
523 const fail = (expected: string, input?: unknown): Array<T> => {
524 spent = true
525 const error = new Schema.SchemaError(new SchemaIssue.InvalidValue({ expected }, input, parseOptions))
526 release()
527 if (out.length === 0) throw error
528 stashed = error
529 return out
530 }
531 while (true) {
532 // Use bigint only for frame lengths wider than six varint groups.
533 let frameLen = 0
534 let headerLen = -1
535 const buffered = end - pos
536 let scale = 1
537 for (let i = 0; i < Math.min(6, buffered); i++) {
538 const b = buf[pos + i]
539 frameLen += (b & 0x7F) * scale
540 if ((b & 0x80) === 0) {
541 headerLen = i + 1
542 break
543 }
544 scale *= 128
545 }
546 if (headerLen === -1) {
547 if (buffered < 6) return done()
548 let n = BigInt(frameLen)
549 for (let i = 6; i < Math.min(10, buffered); i++) {
550 const b = buf[pos + i]
551 n |= BigInt(b & 0x7F) << BigInt(i * 7)
552 if ((b & 0x80) === 0) {
553 headerLen = i + 1
554 if (n > MAX_SAFE_BIGINT) return fail("safe integer length", n)
555 frameLen = Number(n)
556 break
557 }
558 }
559 if (headerLen === -1) {
560 if (buffered >= 10) return fail("uvarint", buf.subarray(pos, pos + 10))
561 return done()
562 }
563 }
564 if (frameLen === 0) return fail("nonzero frame length", frameLen)
565 if (maxFrameSize !== undefined && frameLen > maxFrameSize) {
566 return fail("frame within maxFrameSize", frameLen)
567 }
568 if (headerLen + frameLen > buffered) return done()
569 const savedPathLen = issuePathLen
570 try {
571 issuePathLen = 0
572 const bodyStart = pos + headerLen
573 indexSignatures.beginFrame()
574 body.reset(buf, bodyStart, bodyStart + frameLen, parseOptions, indexSignatures, mode.positional, dict)
575 const value = decodeFrameBody(layout, body, mode)
576 out.push((decodeEncoded === undefined ? value : decodeEncoded(value)) as T)
577 } catch (e) {
578 spent = true
579 release()
580 const error = e instanceof IssueError
581 ? new Schema.SchemaError(e.issue)
582 : Schema.isSchemaError(e)
583 ? e
584 : (() => {
585 throw e
586 })()
587 if (out.length === 0) throw error
588 stashed = error
589 return out
590 } finally {
591 issuePathLen = savedPathLen
592 }
593 pos += headerLen + frameLen
594 }
595 },
596 endSync() {
597 if (stashed !== undefined) return takeStashed()
598 if (spent) return failSync("parser is spent")
599 spent = true
600 const input = bufferStart < bufferEnd ? buffer.subarray(bufferStart, bufferEnd) : undefined
601 release()
602 if (input !== undefined) return failSync("complete value", input)
603 },
604 feed: (chunk) =>
605 Effect.suspend(() => {
606 try {
607 return Effect.succeed(self.feedSync(chunk))
608 } catch (e) {
609 if (Schema.isSchemaError(e)) return Effect.fail(e)
610 throw e
611 }
612 }),
613 end: Effect.suspend(() => {
614 try {
615 self.endSync()
616 return Effect.void
617 } catch (e) {
618 if (Schema.isSchemaError(e)) return Effect.fail(e)
619 throw e
620 }
621 })
622 }
623 return self
624}
625
626/**
627 * Creates a channel that encodes chunks of schema values into binary frames.
628 *
629 * **Details**
630 *
631 * Each input chunk is emitted as one byte element holding concatenated frames
632 * written in one writer pass, so a batch costs no more allocations than a
633 * single frame. Schemas with transformations first run one schema pass per
634 * chunk to the binary-adjusted encoded side — supporting async transformations
635 * and encoding services — before that writer pass. Failures are
636 * `Schema.SchemaError`, matching {@link toCodec}. `maxFrameSize` only applies
637 * to {@link decode} and is ignored here.
638 *
639 * @stability unstable
640 * @category channels
641 * @since 4.0.0
642 */
643export const encode = <S extends Schema.Constraint>(
644 schema: S,
645 options?: SchemaAST.ParseOptions & Options & { readonly maxFrameSize?: number | undefined }
646) =>
647<IE = never, Done = unknown>(): Channel.Channel<
648 Arr.NonEmptyReadonlyArray<Uint8Array<ArrayBuffer>>,
649 Schema.SchemaError | IE,
650 Done,
651 Arr.NonEmptyReadonlyArray<S["Type"]>,
652 IE,
653 Done,
654 S["EncodingServices"]
655> => {
656 // Everything here depends only on schema and options, so it is derived once
657 // per channel rather than once per run.
658 const { exact, layout, target } = compileTarget(schema)
659 const parseOptions: SchemaAST.ParseOptions = options ?? SchemaAST.defaultParseOptions
660 const mode = compileMode(layout, options?.fingerprint)
661 const write = (
662 values: ReadonlyArray<unknown>
663 ): Effect.Effect<Arr.NonEmptyReadonlyArray<Uint8Array<ArrayBuffer>>, Schema.SchemaError> => {
664 try {
665 return Effect.succeed(Arr.of(encodeFrames(layout, values, parseOptions, mode)))
666 } catch (e) {
667 if (e instanceof IssueError) return Effect.fail(new Schema.SchemaError(e.issue))
668 return Effect.die(e)
669 }
670 }
671 if (exact && parseOptions.onExcessProperty !== "error") {
672 return Channel.fromTransform((upstream, _scope) => Effect.sync(() => Effect.flatMap(upstream, write)))
673 }
674 // One schema pass per chunk brings the values to the binary-adjusted encoded
675 // side the writer expects, so the writer pass stays one sync batch.
676 const encodeValues = Schema.encodeUnknownEffect(
677 Schema.NonEmptyArray(target) as unknown as Schema.ConstraintEncoder<unknown, never>,
678 options
679 ) as (values: ReadonlyArray<unknown>) => Effect.Effect<ReadonlyArray<unknown>, Schema.SchemaError>
680 return Channel.fromTransform((upstream, _scope) =>
681 Effect.sync(() => Effect.flatMap(upstream, (chunk) => Effect.flatMap(encodeValues(chunk), write)))
682 )
683}
684
685/**
686 * Creates a channel that decodes chunks of binary frames into schema values.
687 *
688 * **Details**
689 *
690 * The channel keeps one frame parser for its lifetime: frames may be
691 * fragmented across chunks or concatenated within one, values completed before
692 * a failure remain observable, and a leftover incomplete frame fails when the
693 * upstream is done. Schemas with transformations run the schema pass per
694 * framed value, which supports async transformations and decoding services.
695 * Failures are `Schema.SchemaError`, matching {@link toCodec}.
696 *
697 * @stability unstable
698 * @category channels
699 * @since 4.0.0
700 */
701export const decode = <S extends Schema.Constraint>(
702 schema: S,
703 options?: SchemaAST.ParseOptions & Options & { readonly maxFrameSize?: number | undefined }
704) =>
705<IE = never, Done = unknown>(): Channel.Channel<
706 Arr.NonEmptyReadonlyArray<S["Type"]>,
707 Schema.SchemaError | IE,
708 Done,
709 Arr.NonEmptyReadonlyArray<Uint8Array<ArrayBuffer>>,
710 IE,
711 Done,
712 S["DecodingServices"]
713> => {
714 // Everything here depends only on schema and options, so it is derived once
715 // per channel; the frame parser and failure stash below stay per run.
716 const { exact, exitSuccess, layout, target } = compileTarget(schema)
717 const parseOptions: SchemaAST.ParseOptions = options ?? SchemaAST.defaultParseOptions
718 const mode = compileMode(layout, options?.fingerprint)
719 const decodeTarget = exact
720 ? undefined
721 : Schema.decodeUnknownEffect(target as Schema.ConstraintDecoder<unknown>, parseOptions)
722 const decodeEncoded = decodeTarget !== undefined && exitSuccess
723 ? (value: unknown) => isSuccessExit(value) ? Effect.succeed(value) : decodeTarget(value)
724 : decodeTarget
725 return Channel.fromTransform((upstream, _scope) =>
726 Effect.sync(() => {
727 const frames = makeParser<unknown>(layout, mode, parseOptions, options?.maxFrameSize, undefined, false)
728 let stashed: Schema.SchemaError | undefined
729
730 // Decodes framed values in order. On a failure, values decoded before it
731 // are emitted first and the error fails the next pull.
732 const decodeAll = (
733 raw: ReadonlyArray<unknown>
734 ): Pull.Pull<Arr.NonEmptyReadonlyArray<S["Type"]>, Schema.SchemaError | IE, Done> => {
735 const out: Array<S["Type"]> = []
736 const loop: Effect.Effect<Arr.NonEmptyReadonlyArray<S["Type"]>, Schema.SchemaError> = Effect.suspend(() =>
737 out.length === raw.length
738 ? Effect.succeed(out as unknown as Arr.NonEmptyReadonlyArray<S["Type"]>)
739 : Effect.flatMap(decodeEncoded!(raw[out.length]), (value) => {
740 out.push(value as S["Type"])
741 return loop
742 })
743 )
744 return Effect.catch(loop, (error) => {
745 if (out.length === 0) {
746 stashed = undefined
747 return Effect.fail(error)
748 }
749 stashed = error
750 return Effect.succeed(out as unknown as Arr.NonEmptyReadonlyArray<S["Type"]>)
751 })
752 }
753
754 const feed = (
755 chunk: Arr.NonEmptyReadonlyArray<Uint8Array<ArrayBuffer>>
756 ): Pull.Pull<Arr.NonEmptyReadonlyArray<S["Type"]>, Schema.SchemaError | IE, Done> => {
757 const raw: Array<unknown> = []
758 try {
759 for (let i = 0; i < chunk.length; i++) {
760 const values = frames.feedSync(chunk[i])
761 for (let j = 0; j < values.length; j++) raw.push(values[j])
762 }
763 } catch (e) {
764 if (!Schema.isSchemaError(e)) return Effect.die(e)
765 if (raw.length === 0) return Effect.fail(e)
766 // Emit the values framed before the failure, then fail on the next
767 // pull.
768 stashed = e
769 }
770 if (!Arr.isReadonlyArrayNonEmpty(raw)) return pull
771 return decodeEncoded === undefined
772 ? Effect.succeed(raw as Arr.NonEmptyReadonlyArray<S["Type"]>)
773 : decodeAll(raw)
774 }
775
776 const pull: Pull.Pull<Arr.NonEmptyReadonlyArray<S["Type"]>, Schema.SchemaError | IE, Done> = Effect.suspend(() =>
777 stashed !== undefined ? Effect.fail(stashed) : Pull.matchEffect(upstream, {
778 onSuccess: feed,
779 onFailure: Effect.failCause,
780 onDone: (done): Pull.Pull<never, Schema.SchemaError, Done> => {
781 try {
782 frames.endSync()
783 return Cause.done(done)
784 } catch (e) {
785 if (Schema.isSchemaError(e)) return Effect.fail(e)
786 throw e
787 }
788 }
789 })
790 )
791
792 return pull
793 })
794 )
795}
796
797/**
798 * Wraps a bidirectional byte channel with schema-driven binary encoding and
799 * decoding.
800 *
801 * **Details**
802 *
803 * Values sent to the wrapped channel are encoded with `inputSchema` as binary
804 * frames; bytes received from it are decoded with `outputSchema`.
805 *
806 * @stability unstable
807 * @category channels
808 * @since 4.0.0
809 */
810export const duplex: {
811 <In extends Schema.Constraint, Out extends Schema.Constraint>(
812 options: SchemaAST.ParseOptions & Options & {
813 readonly inputSchema: In
814 readonly outputSchema: Out
815 readonly maxFrameSize?: number | undefined
816 }
817 ): <OutErr, OutDone, InErr, InDone, R>(
818 self: Channel.Channel<
819 Arr.NonEmptyReadonlyArray<Uint8Array<ArrayBuffer>>,
820 OutErr,
821 OutDone,
822 Arr.NonEmptyReadonlyArray<Uint8Array<ArrayBuffer>>,
823 Schema.SchemaError | InErr,
824 InDone,
825 R
826 >
827 ) => Channel.Channel<
828 Arr.NonEmptyReadonlyArray<Out["Type"]>,
829 Schema.SchemaError | OutErr,
830 OutDone,
831 Arr.NonEmptyReadonlyArray<In["Type"]>,
832 InErr,
833 InDone,
834 R | In["EncodingServices"] | Out["DecodingServices"]
835 >
836 <Out extends Schema.Constraint, In extends Schema.Constraint, OutErr, OutDone, InErr, InDone, R>(
837 self: Channel.Channel<
838 Arr.NonEmptyReadonlyArray<Uint8Array<ArrayBuffer>>,
839 OutErr,
840 OutDone,
841 Arr.NonEmptyReadonlyArray<Uint8Array<ArrayBuffer>>,
842 Schema.SchemaError | InErr,
843 InDone,
844 R
845 >,
846 options: SchemaAST.ParseOptions & Options & {
847 readonly inputSchema: In
848 readonly outputSchema: Out
849 readonly maxFrameSize?: number | undefined
850 }
851 ): Channel.Channel<
852 Arr.NonEmptyReadonlyArray<Out["Type"]>,
853 Schema.SchemaError | OutErr,
854 OutDone,
855 Arr.NonEmptyReadonlyArray<In["Type"]>,
856 InErr,
857 InDone,
858 R | In["EncodingServices"] | Out["DecodingServices"]
859 >
860} = dual(2, <Out extends Schema.Constraint, In extends Schema.Constraint, OutErr, OutDone, InErr, InDone, R>(
861 self: Channel.Channel<
862 Arr.NonEmptyReadonlyArray<Uint8Array<ArrayBuffer>>,
863 OutErr,
864 OutDone,
865 Arr.NonEmptyReadonlyArray<Uint8Array<ArrayBuffer>>,
866 Schema.SchemaError | InErr,
867 InDone,
868 R
869 >,
870 options: SchemaAST.ParseOptions & Options & {
871 readonly inputSchema: In
872 readonly outputSchema: Out
873 readonly maxFrameSize?: number | undefined
874 }
875): Channel.Channel<
876 Arr.NonEmptyReadonlyArray<Out["Type"]>,
877 Schema.SchemaError | OutErr,
878 OutDone,
879 Arr.NonEmptyReadonlyArray<In["Type"]>,
880 InErr,
881 InDone,
882 R | In["EncodingServices"] | Out["DecodingServices"]
883> =>
884 encode(options.inputSchema, options)<InErr, InDone>().pipe(
885 Channel.pipeTo(self),
886 Channel.pipeTo(decode(options.outputSchema, options)())
887 ))
888
889/**
890 * Assigns an explicit wire field id to a struct property.
891 *
892 * **When to use**
893 *
894 * Use to preserve the wire id across a rename or resolve a hash collision.
895 *
896 * **Details**
897 *
898 * Valid ids are integers from 1 through 4294967295.
899 *
900 * **Example** (Assigning a wire field id)
901 *
902 * ```ts
903 * import { Schema } from "effect"
904 * import { SchemaBinary } from "effect/encoding"
905 *
906 * const Person = Schema.Struct({
907 * id: Schema.String.pipe(SchemaBinary.fieldId(1))
908 * })
909 * ```
910 *
911 * @stability unstable
912 * @category annotations
913 * @since 4.0.0
914 */
915export function fieldId(id: number) {
916 if (!Number.isInteger(id) || id <= 0 || id > 0xFFFFFFFF) {
917 throw new Error(`Binary layout field id must be an integer in [1, 4294967295], got ${id}`)
918 }
919 const annotations = { [FIELD_ID_ANNOTATION_KEY]: id }
920 const annotateLastLink = SchemaAST.applyToLastLink((ast) => SchemaAST.annotateKey(ast, annotations))
921 return <S extends Schema.Top>(self: S): S["Rebuild"] =>
922 self.rebuild(annotateLastLink(SchemaAST.annotateKey(self.ast, annotations)))
923}
924
925const FIELD_ID_ANNOTATION_KEY = "~effect/encoding/SchemaBinary/fieldId"
926
927// Bit 0 selects fingerprint mode; all other flag bits are reserved.
928const ENVELOPE = 0x20 // version nibble 2, flags 0
929const ENVELOPE_FINGERPRINT = 0x21 // version nibble 2, flag bit 0
930
931const MAX_SAFE_BIGINT = BigInt(Number.MAX_SAFE_INTEGER)
932const BIGINT_ZERO = BigInt(0)
933const BIGINT_ONE = BigInt(1)
934const BIGINT_TWO = BigInt(2)
935const BIGINT_SEVEN = BigInt(7)
936const BIGINT_VARINT_MASK = BigInt(0x7F)
937const BIGINT_NANOS_PER_MILLI = BigInt(1_000_000)
938const BIGINT_BYTE_MASK = BigInt(0xFF)
939const BIGINT_U32_MASK = BigInt(0xFFFFFFFF)
940const BIGINT_THIRTY_TWO = BigInt(32)
941
942const utf8Encode = new TextEncoder()
943const utf8DecodeFatal = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true })
944
945// General numbers use up to seven varint bytes, a varint mantissa with a
946// decimal scale byte, or an eight-byte f64.
947const NUMBER_VARINT_MAX_BYTES = 7
948
949const NUMBER_VARINT_MAX_MAGNITUDE = 281_474_976_710_655 // 2 ** 48 - 1
950
951// Above this magnitude, build the sign-magnitude code with bigint.
952const EXACT_MAGNITUDE_MAX = 4_503_599_627_370_495 // 2 ** 52 - 1
953
954const NUMBER_RUN_F64 = 0
955const NUMBER_RUN_VARINT = 1
956const NUMBER_RUN_DECIMAL = 2
957
958// Recursive layouts must fail before the JavaScript stack does. This is high
959// enough for practical payloads while keeping the decoder's stack bounded.
960const MAX_NESTING_DEPTH = 512
961
962// Variable-width elements must be backed by frame bytes apart from a bounded
963// allowance. Provably zero-width elements need their own absolute bound.
964const ARRAY_ALLOCATION_SLACK = 1_048_576
965const ZERO_WIDTH_ARRAY_MAX = 4_194_304
966
967// Struct field tags pack the field id with enough information to skip the
968// payload without consulting the schema. Decimal is a sign-magnitude mantissa
969// varint followed by an unsigned scale varint.
970const FIELD_WIRE_SIZED = 0
971const FIELD_WIRE_VARINT = 1
972const FIELD_WIRE_FIXED64 = 2
973const FIELD_WIRE_FALSE = 3
974const FIELD_WIRE_TRUE = 4
975const FIELD_WIRE_DECIMAL = 5
976const FIELD_WIRE_FIXED32 = 6
977const FIELD_WIRE_EMPTY = 7
978const FIELD_WIRE_FACTOR = 8
979
980function isVarintNumber(value: unknown): boolean {
981 return typeof value === "number" && Number.isInteger(value) &&
982 value >= -NUMBER_VARINT_MAX_MAGNITUDE && value <= NUMBER_VARINT_MAX_MAGNITUDE
983}
984
985// Short decimals ride the varint path as mantissa + one scale byte. The
986// mantissa is capped at six varint bytes so the total stays under the
987// eight-byte f64 discriminator.
988const DECIMAL_SCALE_MAX = 8
989const DECIMAL_MANTISSA_MAX = 2_199_023_255_551 // 2 ** 41 - 1
990
991const POW10 = [1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000]
992
993// The scale that makes `value` an exact short-decimal mantissa, or 0.
994function decimalScale(value: number): number {
995 for (let scale = 1; scale <= DECIMAL_SCALE_MAX; scale++) {
996 const mantissa = Math.round(value * POW10[scale])
997 // The mantissa only grows with the scale, so once it leaves the range no
998 // later scale can fit; this also rejects NaN and the infinities in one
999 // comparison.
1000 if (!(mantissa >= -DECIMAL_MANTISSA_MAX && mantissa <= DECIMAL_MANTISSA_MAX)) return 0
1001 if (mantissa / POW10[scale] === value) return scale
1002 }
1003 return 0
1004}
1005
1006// Sign-magnitude preserves `-0`, unlike zigzag.
1007function decodeSignMagnitude(code: number): number {
1008 const magnitude = Math.floor(code / 2)
1009 return code % 2 === 1 ? -magnitude : magnitude
1010}
1011
1012const K = {
1013 bool: 1,
1014 null: 2,
1015 undefined: 3,
1016 number: 4,
1017 string: 5,
1018 bytes: 6,
1019 bigint: 7,
1020 int64: 8,
1021 struct: 9,
1022 variant: 10,
1023 array: 11,
1024 option: 12,
1025 result: 13,
1026 duration: 14,
1027 bigDecimal: 15,
1028 dateTimeZoned: 16,
1029 json: 17,
1030 exit: 18,
1031 cause: 19,
1032 causeReason: 20
1033} as const
1034
1035function fnv32(bytes: ArrayLike<number>): number {
1036 let hash = 0x811C9DC5
1037 for (let i = 0; i < bytes.length; i++) {
1038 hash = Math.imul(hash ^ bytes[i], 0x01000193)
1039 }
1040 return hash >>> 0
1041}
1042
1043const FNV64_OFFSET_BASIS = BigInt("14695981039346656037")
1044const FNV64_PRIME = BigInt("1099511628211")
1045const FNV64_MASK = BigInt("18446744073709551615")
1046
1047function fnv64(bytes: ArrayLike<number>): bigint {
1048 let hash = FNV64_OFFSET_BASIS
1049 for (let i = 0; i < bytes.length; i++) {
1050 hash = ((hash ^ BigInt(bytes[i])) * FNV64_PRIME) & FNV64_MASK
1051 }
1052 return hash
1053}
1054
1055function pushBytes(out: Array<number>, bytes: ArrayLike<number>) {
1056 for (let i = 0; i < bytes.length; i++) out.push(bytes[i])
1057}
1058
1059function pushUvarint(out: Array<number>, n: number) {
1060 while (n > 0x7F) {
1061 out.push((n & 0x7F) | 0x80)
1062 n = Math.floor(n / 128)
1063 }
1064 out.push(n)
1065}
1066
1067function pushU32(out: Array<number>, n: number) {
1068 out.push(n & 0xFF, (n >>> 8) & 0xFF, (n >>> 16) & 0xFF, (n >>> 24) & 0xFF)
1069}
1070
1071function pushU64(out: Array<number>, n: bigint) {
1072 for (let i = 0; i < 8; i++) {
1073 out.push(Number((n >> BigInt(i * 8)) & BIGINT_BYTE_MASK))
1074 }
1075}
1076
1077function compareBytes(a: Uint8Array, b: Uint8Array): number {
1078 const len = Math.min(a.length, b.length)
1079 for (let i = 0; i < len; i++) {
1080 if (a[i] !== b[i]) return a[i] - b[i]
1081 }
1082 return a.length - b.length
1083}
1084
1085class IssueError extends Error {
1086 readonly issue: SchemaIssue.Issue
1087 constructor(issue: SchemaIssue.Issue) {
1088 super("SchemaBinary failure")
1089 this.issue = issue
1090 }
1091}
1092
1093function invalid(expected: string, input?: unknown, options?: SchemaAST.ParseOptions): never {
1094 throw issueError(new SchemaIssue.InvalidValue({ expected }, input, options))
1095}
1096
1097// Materialize the ambient path only when raising an issue.
1098const issuePath: Array<PropertyKey> = []
1099let issuePathLen = 0
1100
1101function issueError(issue: SchemaIssue.Issue): IssueError {
1102 return new IssueError(
1103 issuePathLen === 0 ? issue : new SchemaIssue.Pointer(issuePath.slice(0, issuePathLen), issue)
1104 )
1105}
1106
1107function uvarintBytes(n: number): Uint8Array {
1108 const out = new Uint8Array(uvarintSize(n))
1109 let p = 0
1110 while (n > 0x7F) {
1111 out[p++] = (n & 0x7F) | 0x80
1112 n = Math.floor(n / 128)
1113 }
1114 out[p] = n
1115 return out
1116}
1117
1118function uvarintSize(n: number): number {
1119 let size = 1
1120 while (n > 0x7F) {
1121 n = Math.floor(n / 128)
1122 size++
1123 }
1124 return size
1125}
1126
1127// Avoid TextDecoder's fixed cost for short strings.
1128const UTF8_INLINE_LIMIT = 32
1129
1130function decodeUtf8(
1131 buf: Uint8Array,
1132 start: number,
1133 end: number,
1134 options: SchemaAST.ParseOptions | undefined
1135): string {
1136 const len = end - start
1137 if (len === 0) return ""
1138 if (len <= UTF8_INLINE_LIMIT) {
1139 const ascii = decodeAscii(buf, start, end)
1140 if (ascii !== undefined) return ascii
1141 }
1142 try {
1143 return utf8DecodeFatal.decode(buf.subarray(start, end))
1144 } catch {
1145 invalid("utf-8", undefined, options)
1146 }
1147}
1148
1149// Sixteen and eight code unit blocks, checking their high bits together and
1150// giving up on the first non-ASCII byte, then one call for the 0-7 tail.
1151function decodeAscii(buf: Uint8Array, start: number, end: number): string | undefined {
1152 let out = ""
1153 let i = start
1154 for (; i + 16 <= end; i += 16) {
1155 const a = buf[i], b = buf[i + 1], c = buf[i + 2], d = buf[i + 3]
1156 const e = buf[i + 4], f = buf[i + 5], g = buf[i + 6], h = buf[i + 7]
1157 const a2 = buf[i + 8], b2 = buf[i + 9], c2 = buf[i + 10], d2 = buf[i + 11]
1158 const e2 = buf[i + 12], f2 = buf[i + 13], g2 = buf[i + 14], h2 = buf[i + 15]
1159 if ((a | b | c | d | e | f | g | h | a2 | b2 | c2 | d2 | e2 | f2 | g2 | h2) > 0x7F) return undefined
1160 out += String.fromCharCode(a, b, c, d, e, f, g, h, a2, b2, c2, d2, e2, f2, g2, h2)
1161 }
1162 if (i + 8 <= end) {
1163 const a = buf[i], b = buf[i + 1], c = buf[i + 2], d = buf[i + 3]
1164 const e = buf[i + 4], f = buf[i + 5], g = buf[i + 6], h = buf[i + 7]
1165 if ((a | b | c | d | e | f | g | h) > 0x7F) return undefined
1166 out += String.fromCharCode(a, b, c, d, e, f, g, h)
1167 i += 8
1168 }
1169 switch (end - i) {
1170 case 0:
1171 return out
1172 case 1: {
1173 const a = buf[i]
1174 if (a > 0x7F) return undefined
1175 return out + String.fromCharCode(a)
1176 }
1177 case 2: {
1178 const a = buf[i], b = buf[i + 1]
1179 if ((a | b) > 0x7F) return undefined
1180 return out + String.fromCharCode(a, b)
1181 }
1182 case 3: {
1183 const a = buf[i], b = buf[i + 1], c = buf[i + 2]
1184 if ((a | b | c) > 0x7F) return undefined
1185 return out + String.fromCharCode(a, b, c)
1186 }
1187 case 4: {
1188 const a = buf[i], b = buf[i + 1], c = buf[i + 2], d = buf[i + 3]
1189 if ((a | b | c | d) > 0x7F) return undefined
1190 return out + String.fromCharCode(a, b, c, d)
1191 }
1192 case 5: {
1193 const a = buf[i], b = buf[i + 1], c = buf[i + 2], d = buf[i + 3], e = buf[i + 4]
1194 if ((a | b | c | d | e) > 0x7F) return undefined
1195 return out + String.fromCharCode(a, b, c, d, e)
1196 }
1197 case 6: {
1198 const a = buf[i], b = buf[i + 1], c = buf[i + 2], d = buf[i + 3]
1199 const e = buf[i + 4], f = buf[i + 5]
1200 if ((a | b | c | d | e | f) > 0x7F) return undefined
1201 return out + String.fromCharCode(a, b, c, d, e, f)
1202 }
1203 default: {
1204 const a = buf[i], b = buf[i + 1], c = buf[i + 2], d = buf[i + 3]
1205 const e = buf[i + 4], f = buf[i + 5], g = buf[i + 6]
1206 if ((a | b | c | d | e | f | g) > 0x7F) return undefined
1207 return out + String.fromCharCode(a, b, c, d, e, f, g)
1208 }
1209 }
1210}
1211
1212const OUTPUT_ARENA_SIZE = 64 * 1024
1213
1214// Bound attacker-controlled keys retained between parser feeds.
1215const PARSER_INDEX_SIGNATURE_CACHE_SIZE = 256
1216
1217// Bound the last universal record shape retained by a compiled layout.
1218const ENCODE_INDEX_SIGNATURE_CACHE_KEYS = 512
1219const ENCODE_INDEX_SIGNATURE_CACHE_CODE_UNITS = 16 * 1024
1220
1221interface OutputArena {
1222 readonly buf: Uint8Array<ArrayBuffer>
1223 readonly view: DataView<ArrayBuffer>
1224 offset: number
1225 writing: boolean
1226}
1227
1228function makeOutputArena(size: number): OutputArena {
1229 const buf: Uint8Array<ArrayBuffer> = new Uint8Array(size)
1230 return { buf, view: new DataView(buf.buffer), offset: 0, writing: false }
1231}
1232
1233let outputArena = makeOutputArena(OUTPUT_ARENA_SIZE)
1234
1235class Writer {
1236 buf: Uint8Array<ArrayBuffer> = new Uint8Array(0)
1237 view = new DataView(this.buf.buffer)
1238 arena: OutputArena | undefined
1239 start = 0
1240 limit = 0
1241 len = 0
1242 reset() {
1243 let arena = outputArena
1244 // Nested codecs cannot share the active arena tail.
1245 if (arena.writing || arena.offset >= arena.buf.length) {
1246 arena = outputArena = makeOutputArena(OUTPUT_ARENA_SIZE)
1247 }
1248 arena.writing = true
1249 this.arena = arena
1250 this.buf = arena.buf
1251 this.view = arena.view
1252 this.start = arena.offset
1253 this.limit = arena.buf.length - arena.offset
1254 this.len = 0
1255 }
1256 ensure(n: number) {
1257 if (this.len + n > this.limit) {
1258 const previous = this.arena!
1259 const required = this.len + n
1260 let size = OUTPUT_ARENA_SIZE
1261 while (size < required) size *= 2
1262 const next = makeOutputArena(size)
1263 next.writing = true
1264 next.buf.set(this.buf.subarray(this.start, this.start + this.len))
1265 previous.writing = false
1266 this.arena = outputArena = next
1267 this.buf = next.buf
1268 this.view = next.view
1269 this.start = 0
1270 this.limit = next.buf.length
1271 }
1272 }
1273 byte(b: number) {
1274 this.ensure(1)
1275 this.buf[this.start + this.len++] = b
1276 }
1277 bytes(b: Uint8Array) {
1278 this.ensure(b.length)
1279 this.buf.set(b, this.start + this.len)
1280 this.len += b.length
1281 }
1282 uvarint(n: number) {
1283 this.ensure(10)
1284 const buf = this.buf
1285 let p = this.start + this.len
1286 if (n < 0x80) {
1287 buf[p++] = n
1288 } else {
1289 buf[p++] = (n & 0x7F) | 0x80
1290 if (n < 0x4000) {
1291 buf[p++] = n >>> 7
1292 } else {
1293 buf[p++] = ((n >>> 7) & 0x7F) | 0x80
1294 n = Math.floor(n / 0x4000)
1295 while (n > 0x7F) {
1296 buf[p++] = (n & 0x7F) | 0x80
1297 n = Math.floor(n / 128)
1298 }
1299 buf[p++] = n
1300 }
1301 }
1302 this.len = p - this.start
1303 }
1304 uvarintBig(n: bigint) {
1305 while (n > BIGINT_VARINT_MASK) {
1306 this.byte(Number(n & BIGINT_VARINT_MASK) | 0x80)
1307 n >>= BIGINT_SEVEN
1308 }
1309 this.byte(Number(n))
1310 }
1311 zigzag(n: bigint) {
1312 this.uvarintBig(n >= BIGINT_ZERO ? n << BIGINT_ONE : (-n << BIGINT_ONE) - BIGINT_ONE)
1313 }
1314 numberVarint(n: number) {
1315 const negative = n < 0 || (n === 0 && 1 / n < 0)
1316 const magnitude = negative ? -n : n
1317 if (magnitude <= EXACT_MAGNITUDE_MAX) {
1318 this.uvarint(magnitude * 2 + (negative ? 1 : 0))
1319 } else {
1320 this.uvarintBig(BigInt(magnitude) * BIGINT_TWO + (negative ? BIGINT_ONE : BIGINT_ZERO))
1321 }
1322 }
1323 f64(n: number) {
1324 this.ensure(8)
1325 this.view.setFloat64(this.start + this.len, n, true)
1326 this.len += 8
1327 }
1328 i64(n: bigint) {
1329 this.ensure(8)
1330 this.view.setBigInt64(this.start + this.len, n, true)
1331 this.len += 8
1332 }
1333 u32le(n: number) {
1334 this.ensure(4)
1335 this.view.setUint32(this.start + this.len, n >>> 0, true)
1336 this.len += 4
1337 }
1338 i32le(n: number) {
1339 this.ensure(4)
1340 this.view.setInt32(this.start + this.len, n | 0, true)
1341 this.len += 4
1342 }
1343 // Writes a field id and reserves its length byte under one bounds check.
1344 idAndMark(idBytes: Uint8Array): number {
1345 const n = idBytes.length
1346 this.ensure(n + 1)
1347 const buf = this.buf
1348 let p = this.start + this.len
1349 for (let i = 0; i < n; i++) buf[p++] = idBytes[i]
1350 const mark = p - this.start
1351 this.len = mark + 1
1352 return mark
1353 }
1354 // Reserve one byte, then expand and backfill the length prefix if needed.
1355 beginSized(): number {
1356 this.ensure(1)
1357 return this.len++
1358 }
1359 endSized(mark: number) {
1360 const code = this.len - mark - 1
1361 if (code < 0x80) this.buf[this.start + mark] = code
1362 else this.growSized(mark, code)
1363 }
1364 // Row-run regions prefix `length * 2`; odd codes are back-references.
1365 endSizedRun(mark: number) {
1366 const code = (this.len - mark - 1) * 2
1367 if (code < 0x80) this.buf[this.start + mark] = code
1368 else this.growSized(mark, code)
1369 }
1370 private growSized(mark: number, code: number) {
1371 const size = uvarintSize(code)
1372 const extra = size - 1
1373 this.ensure(extra)
1374 const absoluteMark = this.start + mark
1375 this.buf.copyWithin(absoluteMark + size, absoluteMark + 1, this.start + this.len)
1376 this.len += extra
1377 let n = code
1378 let p = absoluteMark
1379 while (n > 0x7F) {
1380 this.buf[p++] = (n & 0x7F) | 0x80
1381 n = Math.floor(n / 128)
1382 }
1383 this.buf[p] = n
1384 }
1385 string(s: string) {
1386 const n = s.length
1387 if (n === 0) return
1388 this.ensure(n * 3)
1389 const buf = this.buf
1390 let p = this.start + this.len
1391 let i = 0
1392 for (; i + 4 <= n; i += 4) {
1393 const a = s.charCodeAt(i)
1394 const b = s.charCodeAt(i + 1)
1395 const c = s.charCodeAt(i + 2)
1396 const d = s.charCodeAt(i + 3)
1397 if ((a | b | c | d) > 0x7F) {
1398 this.len += utf8Encode.encodeInto(s, buf.subarray(this.start + this.len)).written
1399 return
1400 }
1401 buf[p++] = a
1402 buf[p++] = b
1403 buf[p++] = c
1404 buf[p++] = d
1405 }
1406 for (; i < n; i++) {
1407 const c = s.charCodeAt(i)
1408 if (c > 0x7F) {
1409 this.len += utf8Encode.encodeInto(s, buf.subarray(this.start + this.len)).written
1410 return
1411 }
1412 buf[p++] = c
1413 }
1414 this.len = p - this.start
1415 }
1416 out(): Uint8Array<ArrayBuffer> {
1417 const arena = this.arena!
1418 const out = new Uint8Array(arena.buf.buffer, this.start, this.len)
1419 if (arena === outputArena) arena.offset = this.start + this.len
1420 arena.writing = false
1421 this.arena = undefined
1422 return out
1423 }
1424 abort() {
1425 const arena = this.arena
1426 if (arena === undefined) return
1427 if (arena === outputArena) arena.offset = this.start
1428 arena.writing = false
1429 this.arena = undefined
1430 }
1431}
1432
1433// Index-signature predicates are not part of the fingerprint, so unmatched
1434// keys are dropped in both modes.
1435// A plain string parameter accepts every key without running the matcher.
1436function matchesEveryKey(parameter: SchemaAST.AST): boolean {
1437 const ast = SchemaAST.toEncoded(parameter)
1438 return ast._tag === "String" && ast.checks === undefined
1439}
1440
1441function matchIndexSignature(
1442 layout: StructLayout,
1443 key: string,
1444 options: SchemaAST.ParseOptions
1445): ExtraSignature | undefined {
1446 return layout.extra.find((s) => SchemaAST.getIndexSignatureKeys({ [key]: null }, s.parameter, options).length > 0)
1447}
1448
1449class IndexSignatureCache {
1450 entries = new WeakMap<StructLayout, Map<string, ExtraSignature | undefined>>()
1451 readonly orderLayouts: Array<StructLayout | undefined> | undefined
1452 readonly orderKeys: Array<string | undefined> | undefined
1453 readonly options: SchemaAST.ParseOptions
1454 size = 0
1455 next = 0
1456 replace = false
1457 // Only the parser cache outlives one operation, and it is always bounded.
1458 constructor(options: SchemaAST.ParseOptions, capacity?: number) {
1459 this.options = options
1460 this.orderLayouts = capacity === undefined ? undefined : new Array(capacity)
1461 this.orderKeys = capacity === undefined ? undefined : new Array(capacity)
1462 }
1463 beginFrame() {
1464 this.replace = true
1465 }
1466 find(layout: StructLayout, key: string): ExtraSignature | undefined {
1467 let entries = this.entries.get(layout)
1468 if (entries === undefined) {
1469 entries = new Map()
1470 this.entries.set(layout, entries)
1471 }
1472 if (entries.has(key)) return entries.get(key)
1473 const signature = matchIndexSignature(layout, key, this.options)
1474 const orderLayouts = this.orderLayouts
1475 if (orderLayouts === undefined) {
1476 entries.set(key, signature)
1477 return signature
1478 }
1479 const orderKeys = this.orderKeys!
1480 if (this.size < orderLayouts.length) {
1481 orderLayouts[this.size] = layout
1482 orderKeys[this.size] = key
1483 this.size++
1484 entries.set(key, signature)
1485 } else if (this.replace) {
1486 this.replace = false
1487 const evictedLayout = orderLayouts[this.next]!
1488 const evictedEntries = evictedLayout === layout ? entries : this.entries.get(evictedLayout)
1489 evictedEntries?.delete(orderKeys[this.next]!)
1490 orderLayouts[this.next] = layout
1491 orderKeys[this.next] = key
1492 this.next = (this.next + 1) % orderLayouts.length
1493 entries.set(key, signature)
1494 }
1495 return signature
1496 }
1497 clear() {
1498 this.entries = new WeakMap()
1499 this.orderLayouts?.fill(undefined)
1500 this.orderKeys?.fill(undefined)
1501 this.size = this.next = 0
1502 this.replace = false
1503 }
1504}
1505
1506// A named buffer preserves the build's pure annotation for these placeholders.
1507const EMPTY_READER_ARRAY_BUFFER = new ArrayBuffer(0)
1508const EMPTY_READER_BUFFER = new Uint8Array(EMPTY_READER_ARRAY_BUFFER)
1509const EMPTY_READER_VIEW = new DataView(EMPTY_READER_ARRAY_BUFFER)
1510
1511class Reader {
1512 pos = 0
1513 nesting = 0
1514 buf: Uint8Array = EMPTY_READER_BUFFER
1515 // Frames of varints and strings never need a view, so it is built on demand
1516 // and reused while the reader stays on one buffer.
1517 view: DataView | undefined
1518 viewCache: DataView = EMPTY_READER_VIEW
1519 viewBuffer: ArrayBufferLike = EMPTY_READER_ARRAY_BUFFER
1520 viewOffset = 0
1521 viewLength = 0
1522 end = 0
1523 options: SchemaAST.ParseOptions = SchemaAST.defaultParseOptions
1524 indexSignatures: IndexSignatureCache | undefined
1525 positional = false
1526 // Handed to the row-run field's immediate consumer, then cleared.
1527 intern: Array<unknown> | undefined
1528 // String tables shared by every frame on one connection; `undefined` for a
1529 // one-shot decode, where each frame stands alone.
1530 dict: DictRead | undefined
1531 reset(
1532 buf: Uint8Array,
1533 start: number,
1534 end: number,
1535 options: SchemaAST.ParseOptions,
1536 indexSignatures: IndexSignatureCache | undefined,
1537 positional: boolean,
1538 dict: DictRead | undefined
1539 ) {
1540 this.nesting = 0
1541 this.view = undefined
1542 this.buf = buf
1543 this.pos = start
1544 this.end = end
1545 this.options = options
1546 this.indexSignatures = indexSignatures
1547 this.positional = positional
1548 this.intern = undefined
1549 this.dict = dict
1550 }
1551 release() {
1552 this.pos = this.end = this.nesting = 0
1553 this.buf = EMPTY_READER_BUFFER
1554 this.view = undefined
1555 this.viewCache = EMPTY_READER_VIEW
1556 this.viewBuffer = EMPTY_READER_ARRAY_BUFFER
1557 this.viewOffset = this.viewLength = 0
1558 this.options = SchemaAST.defaultParseOptions
1559 this.indexSignatures = undefined
1560 this.positional = false
1561 this.intern = undefined
1562 this.dict = undefined
1563 }
1564 get remaining(): number {
1565 return this.end - this.pos
1566 }
1567 dataView(): DataView {
1568 const buf = this.buf
1569 if (
1570 buf.buffer !== this.viewBuffer || buf.byteOffset !== this.viewOffset || buf.byteLength !== this.viewLength
1571 ) {
1572 this.viewBuffer = buf.buffer
1573 this.viewOffset = buf.byteOffset
1574 this.viewLength = buf.byteLength
1575 this.viewCache = new DataView(buf.buffer, buf.byteOffset, buf.byteLength)
1576 }
1577 return this.view = this.viewCache
1578 }
1579 byte(): number {
1580 if (this.pos >= this.end) invalid("complete value", undefined, this.options)
1581 return this.buf[this.pos++]
1582 }
1583 take(n: number): Uint8Array {
1584 if (this.pos + n > this.end) invalid("complete value", undefined, this.options)
1585 const out = this.buf.subarray(this.pos, this.pos + n)
1586 this.pos += n
1587 return out
1588 }
1589 // Decoded bytes outlive the parser's buffer, so they are copied, not viewed.
1590 takeCopy(n: number): Uint8Array<ArrayBuffer> {
1591 if (this.pos + n > this.end) invalid("complete value", undefined, this.options)
1592 const out = this.buf.slice(this.pos, this.pos + n) as Uint8Array<ArrayBuffer>
1593 this.pos += n
1594 return out
1595 }
1596 // Decode a bounded child value without allocating another reader.
1597 enter(len: number): number {
1598 if (this.pos + len > this.end) invalid("complete value", undefined, this.options)
1599 const saved = this.end
1600 this.end = this.pos + len
1601 return saved
1602 }
1603 exit(saved: number) {
1604 this.pos = this.end
1605 this.end = saved
1606 }
1607 readUtf8(n: number): string {
1608 if (this.pos + n > this.end) invalid("complete value", undefined, this.options)
1609 const start = this.pos
1610 this.pos += n
1611 return decodeUtf8(this.buf, start, start + n, this.options)
1612 }
1613 // Use bitwise arithmetic for the first four varint groups.
1614 uvarint(): number {
1615 const buf = this.buf
1616 const end = this.end
1617 let pos = this.pos
1618 if (pos >= end) invalid("complete value", undefined, this.options)
1619 let b = buf[pos++]
1620 if (b < 0x80) {
1621 this.pos = pos
1622 return b
1623 }
1624 let value = b & 0x7F
1625 if (pos >= end) invalid("complete value", undefined, this.options)
1626 b = buf[pos++]
1627 value |= (b & 0x7F) << 7
1628 if (b < 0x80) {
1629 this.pos = pos
1630 return value
1631 }
1632 if (pos >= end) invalid("complete value", undefined, this.options)
1633 b = buf[pos++]
1634 value |= (b & 0x7F) << 14
1635 if (b < 0x80) {
1636 this.pos = pos
1637 return value
1638 }
1639 if (pos >= end) invalid("complete value", undefined, this.options)
1640 b = buf[pos++]
1641 value |= (b & 0x7F) << 21
1642 if (b < 0x80) {
1643 this.pos = pos
1644 return value
1645 }
1646 let scale = 268435456 // 2 ** 28
1647 for (let i = 4; i < 10; i++) {
1648 if (pos >= end) {
1649 this.pos = pos
1650 invalid("complete value", undefined, this.options)
1651 }
1652 b = buf[pos++]
1653 const chunk = (b & 0x7F) * scale
1654 if (chunk > Number.MAX_SAFE_INTEGER - value) {
1655 this.pos = pos
1656 invalid("safe integer length", undefined, this.options)
1657 }
1658 value += chunk
1659 if (b < 0x80) {
1660 this.pos = pos
1661 return value
1662 }
1663 scale *= 128
1664 }
1665 this.pos = pos
1666 invalid("uvarint", undefined, this.options)
1667 }
1668 // Field tags are capped at 35 bits, so their common five-byte form can skip
1669 // the general safe-integer loop in uvarint.
1670 fieldTag(): number {
1671 const buf = this.buf
1672 const end = this.end
1673 let pos = this.pos
1674 if (pos >= end) invalid("complete value", undefined, this.options)
1675 let b = buf[pos++]
1676 if (b < 0x80) {
1677 this.pos = pos
1678 return b
1679 }
1680 let value = b & 0x7F
1681 if (pos >= end) invalid("complete value", undefined, this.options)
1682 b = buf[pos++]
1683 value |= (b & 0x7F) << 7
1684 if (b < 0x80) {
1685 this.pos = pos
1686 return value
1687 }
1688 if (pos >= end) invalid("complete value", undefined, this.options)
1689 b = buf[pos++]
1690 value |= (b & 0x7F) << 14
1691 if (b < 0x80) {
1692 this.pos = pos
1693 return value
1694 }
1695 if (pos >= end) invalid("complete value", undefined, this.options)
1696 b = buf[pos++]
1697 value |= (b & 0x7F) << 21
1698 if (b < 0x80) {
1699 this.pos = pos
1700 return value
1701 }
1702 if (pos >= end) invalid("complete value", undefined, this.options)
1703 b = buf[pos++]
1704 this.pos = pos
1705 if (b >= 0x80) invalid("uvarint", undefined, this.options)
1706 return value + b * 268435456
1707 }
1708 uvarintBig(): bigint {
1709 let value = BIGINT_ZERO
1710 let shift = BIGINT_ZERO
1711 while (true) {
1712 const b = this.byte()
1713 value |= BigInt(b & 0x7F) << shift
1714 if ((b & 0x80) === 0) return value
1715 shift += BIGINT_SEVEN
1716 }
1717 }
1718 // Wider schema-proven integers switch to bigint arithmetic.
1719 numberVarint(): number {
1720 const buf = this.buf
1721 const end = this.end
1722 let pos = this.pos
1723 let value = 0
1724 let scale = 1
1725 for (let i = 0; i < NUMBER_VARINT_MAX_BYTES; i++) {
1726 if (pos >= end) {
1727 this.pos = pos
1728 invalid("complete value", undefined, this.options)
1729 }
1730 const b = buf[pos++]
1731 value += (b & 0x7F) * scale
1732 if (b < 0x80) {
1733 this.pos = pos
1734 return decodeSignMagnitude(value)
1735 }
1736 scale *= 128
1737 }
1738 const code = this.uvarintBig()
1739 const magnitude = Number(code >> BIGINT_ONE)
1740 if (!Number.isFinite(magnitude)) invalid("safe integer length", undefined, this.options)
1741 return (code & BIGINT_ONE) === BIGINT_ONE ? -magnitude : magnitude
1742 }
1743 zigzag(): bigint {
1744 const u = this.uvarintBig()
1745 return (u & BIGINT_ONE) === BIGINT_ONE
1746 ? -((u + BIGINT_ONE) >> BIGINT_ONE)
1747 : u >> BIGINT_ONE
1748 }
1749 f64(): number {
1750 if (this.pos + 8 > this.end) invalid("complete value", undefined, this.options)
1751 const value = (this.view ?? this.dataView()).getFloat64(this.pos, true)
1752 this.pos += 8
1753 return value
1754 }
1755 i64(): bigint {
1756 if (this.pos + 8 > this.end) invalid("complete value", undefined, this.options)
1757 const value = (this.view ?? this.dataView()).getBigInt64(this.pos, true)
1758 this.pos += 8
1759 return value
1760 }
1761 u32le(): number {
1762 if (this.pos + 4 > this.end) invalid("complete value", undefined, this.options)
1763 const value = (this.view ?? this.dataView()).getUint32(this.pos, true)
1764 this.pos += 4
1765 return value
1766 }
1767 i32le(): number {
1768 if (this.pos + 4 > this.end) invalid("complete value", undefined, this.options)
1769 const value = (this.view ?? this.dataView()).getInt32(this.pos, true)
1770 this.pos += 4
1771 return value
1772 }
1773}
1774
1775type LeafKind =
1776 | "bool"
1777 | "null"
1778 | "undefined"
1779 | "number"
1780 | "int"
1781 | "string"
1782 | "symbol"
1783 | "bytes"
1784 | "bigint"
1785 | "json"
1786 | "duration"
1787 | "bigDecimal"
1788 | "dateTimeZoned"
1789
1790type Layout =
1791 | LeafLayout
1792 | LiteralLayout
1793 | { readonly _: "int64"; readonly flavor: "date" | "utc" }
1794 | { readonly _: "never"; readonly ast: SchemaAST.AST }
1795 | StructLayout
1796 | ArrayLayout
1797 | UnionLayout
1798 | { readonly _: "option"; value: Layout }
1799 | { readonly _: "result"; success: Layout; failure: Layout }
1800 | { readonly _: "exit"; value: Layout; cause: ReasonLayout }
1801 | ReasonLayout
1802
1803// Every leaf carries a `slot`, so they all share one shape and the layout
1804// switch stays monomorphic on them. Only strings use it: it indexes their table
1805// in a connection dictionary, and both ends compile the same schema in the same
1806// order, so they number the leaves the same way.
1807interface LeafLayout {
1808 readonly _: LeafKind
1809 readonly slot: number
1810}
1811
1812// Literal values are validated here rather than by a downstream schema pass.
1813interface LiteralLayout {
1814 readonly _: "literal"
1815 readonly leaf: Layout
1816 readonly values: ReadonlyArray<SchemaAST.LiteralValue>
1817 // Larger literal sets (enums) check membership in a set instead of a scan.
1818 readonly valueSet: ReadonlySet<SchemaAST.LiteralValue> | undefined
1819}
1820
1821function literalLayout(leaf: Layout, values: ReadonlyArray<SchemaAST.LiteralValue>): LiteralLayout {
1822 return { _: "literal", leaf, values, valueSet: values.length > 4 ? new Set(values) : undefined }
1823}
1824
1825function hasLiteral(layout: LiteralLayout, value: unknown): boolean {
1826 const values = layout.values
1827 return layout.valueSet !== undefined
1828 ? layout.valueSet.has(value as SchemaAST.LiteralValue)
1829 : values.length === 1
1830 ? value === values[0]
1831 : values.includes(value as SchemaAST.LiteralValue)
1832}
1833
1834function literalExpected(layout: LiteralLayout): string {
1835 return layout.values.map((value) => typeof value === "string" ? JSON.stringify(value) : String(value)).join(" | ")
1836}
1837
1838interface ReasonLayout {
1839 readonly _: "cause" | "causeReason"
1840 error: Layout
1841 defect: Layout
1842}
1843
1844interface Field {
1845 readonly name: string
1846 readonly id: number
1847 readonly tagBytes: Uint8Array
1848 index: number
1849 readonly optional: boolean
1850 readonly annotations: Schema.Annotations.Key<unknown> | undefined
1851 layout: Layout
1852 wireMask: number
1853 inline: boolean
1854}
1855
1856interface ExtraSignature {
1857 readonly parameter: SchemaAST.IndexSignature["parameter"]
1858 layout: Layout
1859 wireMask: number
1860}
1861
1862interface StructLayout {
1863 readonly _: "struct"
1864 readonly ast: SchemaAST.AST
1865 readonly fields: Array<Field>
1866 readonly byId: Map<number, Field>
1867 readonly extra: Array<ExtraSignature>
1868 // The lone signature every key matches, so lookups skip the cache.
1869 readonly extraAll: ExtraSignature | undefined
1870 readonly names: Set<string>
1871 // Reuse the last universal index-signature shape across encode calls.
1872 extraShape: ExtraShape | undefined
1873 optionalCount: number
1874 // Required field indices below 32 as one comparison; wider structs with
1875 // required fields past that keep the per-field check.
1876 requiredMask: number
1877 requiredWide: boolean
1878}
1879
1880interface Slot {
1881 readonly optional: boolean
1882 layout: Layout
1883}
1884
1885interface ArrayLayout {
1886 readonly _: "array"
1887 readonly ast: SchemaAST.AST
1888 readonly elements: Array<Slot>
1889 readonly rest: Array<Layout>
1890 readonly hasCount: boolean
1891 readonly minCount: number
1892 // Shared layout for `Schema.Array(S)`.
1893 uniform: Layout | undefined
1894 uniformInline: boolean
1895 uniformPacked: number | undefined
1896 uniformNumbers: boolean
1897 // Lazily compiled row-run plan; `null` once known to be unavailable.
1898 run: RunPlan | null | undefined
1899}
1900
1901// Struct rows repeated in one array share their field ids and repeated strings.
1902interface RunPlan {
1903 readonly struct: StructLayout
1904 // Per field index: INTERN_NONE / SELF / ELEMENTS / KEYS.
1905 readonly intern: Array<number>
1906 readonly shapes: boolean
1907 readonly byName: Map<string, number> | undefined
1908}
1909
1910const INTERN_NONE = 0
1911const INTERN_SELF = 1
1912const INTERN_ELEMENTS = 2
1913const INTERN_KEYS = 3
1914
1915// Rows carry a presence mask over fields plus one bit for the extra block.
1916const RUN_MAX_FIELDS = 30
1917const RUN_EXTRA_BIT = 1 << RUN_MAX_FIELDS
1918
1919interface VariantRow {
1920 readonly tag: number
1921 readonly sentinels: ReadonlyArray<SchemaAST.Sentinel>
1922 readonly tuple: boolean
1923 payload: Layout
1924 position: number
1925}
1926
1927interface UnionMember {
1928 readonly kind: number
1929 readonly layout: Layout
1930 position: number
1931}
1932
1933interface UnionPosition {
1934 readonly variant: VariantRow | undefined
1935 readonly layout: Layout
1936}
1937
1938// A key every variant pins to its own literal, so encoding picks the variant
1939// with one lookup instead of scanning.
1940interface Discriminator {
1941 readonly key: PropertyKey
1942 readonly rows: Map<unknown, VariantRow>
1943}
1944
1945interface UnionLayout {
1946 readonly _: "union"
1947 readonly ast: SchemaAST.AST
1948 readonly variants: Array<VariantRow>
1949 readonly byTag: Map<number, VariantRow>
1950 discriminator: Discriminator | undefined
1951 readonly others: Array<UnionMember>
1952 readonly byKind: Map<number, Layout>
1953 // Canonical order used by fingerprint mode.
1954 readonly byPos: Array<UnionPosition>
1955}
1956
1957function isSelfDelimiting(layout: Layout): boolean {
1958 return layout._ === "literal" ? isSelfDelimiting(layout.leaf) : layout._ === "int"
1959}
1960
1961function packedSize(layout: Layout): number | undefined {
1962 switch (layout._) {
1963 case "literal":
1964 return packedSize(layout.leaf)
1965 case "bool":
1966 return 1
1967 case "int64":
1968 return 8
1969 default:
1970 return undefined
1971 }
1972}
1973
1974function isInlineSlot(layout: Layout): boolean {
1975 return packedSize(layout) !== undefined || isSelfDelimiting(layout) ||
1976 layout._ === "null" || layout._ === "undefined"
1977}
1978
1979function isZeroWidthSlot(layout: Layout): boolean {
1980 return layout._ === "literal" ? isZeroWidthSlot(layout.leaf) : layout._ === "null" || layout._ === "undefined"
1981}
1982
1983// One registry row per natively supported declaration: its wire kind and
1984// whether the binary layer validates it exactly on its own (`true`) or exactly
1985// when its type parameters are ("parameters"). `compileDeclaration` holds the
1986// matching layout constructors; its fallback asserts the two stay in sync.
1987const natives: Record<string, { readonly kind: number; readonly exact: true | "parameters" }> = {
1988 "effect/schema/Date": { kind: K.int64, exact: true },
1989 "effect/schema/DateTimeUtc": { kind: K.int64, exact: true },
1990 "effect/schema/DateTimeZoned": { kind: K.dateTimeZoned, exact: true },
1991 "effect/schema/Duration": { kind: K.duration, exact: true },
1992 "effect/schema/BigDecimal": { kind: K.bigDecimal, exact: true },
1993 "effect/schema/Uint8Array": { kind: K.bytes, exact: true },
1994 "effect/schema/Option": { kind: K.option, exact: "parameters" },
1995 "effect/schema/Result": { kind: K.result, exact: "parameters" },
1996 "effect/schema/Exit": { kind: K.exit, exact: "parameters" },
1997 "effect/schema/Cause": { kind: K.cause, exact: "parameters" },
1998 "effect/schema/CauseReason": { kind: K.causeReason, exact: "parameters" }
1999}
2000
2001function representationId(ast: SchemaAST.AST): string | undefined {
2002 const representation = ast.annotations?.representation
2003 return Predicate.isObject(representation) && typeof (representation as { id?: unknown }).id === "string"
2004 ? (representation as { id: string }).id
2005 : undefined
2006}
2007
2008const toBinaryAST = SchemaAST.applyToSelfOrLastLinkEncodingIdempotent((ast) => {
2009 const out = toBinaryASTStep(ast)
2010 const context = ast.context
2011 if (out === ast || context === undefined) return out
2012 return SchemaAST.replaceContextLastLink(
2013 out,
2014 new SchemaAST.Context(context.isOptional, context.isMutable, undefined, context.annotations)
2015 )
2016})
2017
2018function toBinaryASTStep(ast: SchemaAST.AST): SchemaAST.AST {
2019 switch (ast._tag) {
2020 case "Declaration": {
2021 const id = representationId(ast)
2022 if (
2023 id !== undefined && (id in natives || id === "effect/schema/Json" || id === "effect/schema/MutableJson")
2024 ) {
2025 return ast.recur(toBinaryAST)
2026 }
2027 const getJson = ast.annotations?.toCodecJson
2028 const getCodec = ast.annotations?.toCodec
2029 if (!Predicate.isFunction(getJson) && !Predicate.isFunction(getCodec)) {
2030 return ast
2031 }
2032 const typeParameters = ast.typeParameters.map((tp) => Schema.make<Schema.Constraint>(SchemaAST.toEncoded(tp)))
2033 const link = (Predicate.isFunction(getJson) ? getJson(typeParameters) : undefined) ??
2034 (Predicate.isFunction(getCodec) ? getCodec(typeParameters) : undefined)
2035 return link === undefined ? ast : SchemaAST.replaceEncoding(ast, [SchemaAST.mapLink(link, toBinaryAST)])
2036 }
2037 case "Arrays":
2038 case "Objects":
2039 case "Union":
2040 case "Suspend":
2041 return ast.recur(toBinaryAST)
2042 default:
2043 return ast
2044 }
2045}
2046
2047function sentinelSetHash(sentinels: ReadonlyArray<SchemaAST.Sentinel>): number {
2048 const sorted = [...sentinels].sort((a, b) => {
2049 const an = typeof a.key === "number"
2050 const bn = typeof b.key === "number"
2051 if (an !== bn) return an ? -1 : 1
2052 if (an) return (a.key as number) - (b.key as number)
2053 return compareBytes(utf8Encode.encode(a.key as string), utf8Encode.encode(b.key as string))
2054 })
2055 const out: Array<number> = []
2056 for (const sentinel of sorted) {
2057 const keyBytes = utf8Encode.encode(String(sentinel.key))
2058 out.push(typeof sentinel.key === "number" ? 0 : 1)
2059 pushU32(out, keyBytes.length)
2060 pushBytes(out, keyBytes)
2061 const literal = sentinel.literal
2062 const valueBytes = utf8Encode.encode(sentinelLiteralString(literal))
2063 out.push(sentinelLiteralKind(literal))
2064 pushU32(out, valueBytes.length)
2065 pushBytes(out, valueBytes)
2066 }
2067 return fnv32(out)
2068}
2069
2070function sentinelLiteralKind(literal: SchemaAST.LiteralValue | symbol): number {
2071 switch (typeof literal) {
2072 case "string":
2073 return 1
2074 case "number":
2075 return 2
2076 case "boolean":
2077 return 3
2078 case "bigint":
2079 return 4
2080 default:
2081 return 5
2082 }
2083}
2084
2085function sentinelLiteralString(literal: SchemaAST.LiteralValue | symbol): string {
2086 if (typeof literal === "symbol") {
2087 const key = globalThis.Symbol.keyFor(literal)
2088 if (key === undefined) {
2089 throw new Error("Binary layout: unregistered unique symbol (Symbol.keyFor)")
2090 }
2091 return key
2092 }
2093 return String(literal)
2094}
2095
2096function parameterHasSymbol(parameter: SchemaAST.AST): boolean {
2097 switch (parameter._tag) {
2098 case "Symbol":
2099 return true
2100 case "Union":
2101 return parameter.types.some(parameterHasSymbol)
2102 default:
2103 return false
2104 }
2105}
2106
2107function isJsonDeclaration(ast: SchemaAST.Declaration): boolean {
2108 const id = representationId(ast)
2109 if (id === "effect/schema/Json" || id === "effect/schema/MutableJson") return true
2110 return Predicate.isFunction(ast.annotations?.toCodecJson)
2111}
2112
2113// Detect integer checks nested inside filter groups.
2114function provesInteger(ast: SchemaAST.AST): boolean {
2115 const checks = ast.checks
2116 if (checks === undefined) return false
2117 const go = (check: SchemaAST.Check<never>): boolean => {
2118 const id = check.annotations?.representation?.id
2119 if (id === "effect/schema/isInt") return true
2120 return check._tag === "FilterGroup" && check.checks.some(go)
2121 }
2122 return checks.some(go)
2123}
2124
2125function resolveSuspend(ast: SchemaAST.AST): SchemaAST.AST {
2126 while (ast._tag === "Suspend") {
2127 ast = ast.thunk()
2128 }
2129 return ast
2130}
2131
2132function enumsToLiterals(ast: SchemaAST.Enum): SchemaAST.Union<SchemaAST.Literal> {
2133 return new SchemaAST.Union(
2134 ast.enums.map((e) => new SchemaAST.Literal(e[1], { title: e[0] }))
2135 )
2136}
2137
2138function flattenMembers(union: SchemaAST.Union): Array<SchemaAST.AST> {
2139 const out: Array<SchemaAST.AST> = []
2140 const seen = new Set<SchemaAST.AST>()
2141 const go = (ast: SchemaAST.AST) => {
2142 const resolved = resolveSuspend(ast)
2143 if (seen.has(resolved)) return
2144 seen.add(resolved)
2145 switch (resolved._tag) {
2146 case "Never":
2147 return
2148 case "Union":
2149 resolved.types.forEach(go)
2150 return
2151 case "Enum":
2152 enumsToLiterals(resolved).types.forEach(go)
2153 return
2154 default:
2155 out.push(resolved)
2156 }
2157 }
2158 union.types.forEach(go)
2159 return out
2160}
2161
2162function literalKind(literal: SchemaAST.LiteralValue): LeafKind {
2163 switch (typeof literal) {
2164 case "string":
2165 return "string"
2166 case "number":
2167 return "number"
2168 case "boolean":
2169 return "bool"
2170 default:
2171 return "bigint"
2172 }
2173}
2174
2175// Classify union members without eagerly compiling recursive members.
2176function astKind(ast: SchemaAST.AST): number {
2177 switch (ast._tag) {
2178 case "String":
2179 case "TemplateLiteral":
2180 case "Symbol":
2181 return K.string
2182 case "UniqueSymbol":
2183 if (globalThis.Symbol.keyFor(ast.symbol) === undefined) {
2184 throw new Error("Binary layout: unregistered unique symbol (Symbol.keyFor)")
2185 }
2186 return K.string
2187 case "Boolean":
2188 return K.bool
2189 case "Null":
2190 return K.null
2191 case "Undefined":
2192 case "Void":
2193 return K.undefined
2194 case "Number":
2195 return K.number
2196 case "BigInt":
2197 return K.bigint
2198 case "Literal":
2199 switch (typeof ast.literal) {
2200 case "string":
2201 return K.string
2202 case "number":
2203 return K.number
2204 case "boolean":
2205 return K.bool
2206 default:
2207 return K.bigint
2208 }
2209 case "Unknown":
2210 case "Any":
2211 case "ObjectKeyword":
2212 return K.json
2213 case "Objects":
2214 return K.struct
2215 case "Arrays":
2216 return K.array
2217 case "Declaration": {
2218 const id = representationId(ast)
2219 if (id !== undefined && id in natives) return natives[id].kind
2220 if (isJsonDeclaration(ast)) return K.json
2221 throw new Error(`Binary layout: declaration ${id ?? "<anonymous>"} has no toCodecJson or toCodec`)
2222 }
2223 case "Suspend":
2224 return astKind(resolveSuspend(ast))
2225 default:
2226 throw new Error("Binary layout: union members are not uniquely identifiable")
2227 }
2228}
2229
2230function computeRequired(layout: StructLayout) {
2231 for (const field of layout.fields) {
2232 if (field.optional) continue
2233 if (field.index < 32) layout.requiredMask |= 1 << field.index
2234 else layout.requiredWide = true
2235 }
2236}
2237
2238interface CompiledLayout {
2239 readonly layout: Layout
2240 readonly recursive: boolean
2241 readonly exact: boolean
2242 readonly exitSuccess: boolean
2243}
2244
2245function compileLayout(root: SchemaAST.AST): CompiledLayout {
2246 const exact = isExact(root)
2247 const exitSuccess = !exact && isExitWithExactSuccess(root)
2248 root = SchemaAST.toEncoded(root)
2249 const memo = new Map<SchemaAST.AST, Layout>()
2250 let recursive = false
2251 let stringSlots = 0
2252
2253 const leaf = (kind: LeafKind): LeafLayout => ({ _: kind, slot: kind === "string" ? stringSlots++ : 0 })
2254
2255 function compile(ast: SchemaAST.AST): Layout {
2256 const hit = memo.get(ast)
2257 if (hit !== undefined) return hit
2258 const layout = go(ast)
2259 memo.set(ast, layout)
2260 return layout
2261 }
2262
2263 function go(ast: SchemaAST.AST): Layout {
2264 switch (ast._tag) {
2265 case "String":
2266 case "TemplateLiteral":
2267 return leaf("string")
2268 case "Symbol":
2269 case "UniqueSymbol":
2270 return leaf("symbol")
2271 case "Boolean":
2272 return leaf("bool")
2273 case "Null":
2274 return leaf("null")
2275 case "Undefined":
2276 case "Void":
2277 return leaf("undefined")
2278 case "Number":
2279 return leaf(provesInteger(ast) ? "int" : "number")
2280 case "BigInt":
2281 return leaf("bigint")
2282 case "Literal":
2283 return literalLayout(leaf(literalKind(ast.literal)), [ast.literal])
2284 case "Unknown":
2285 case "Any":
2286 case "ObjectKeyword":
2287 return leaf("json")
2288 case "Never":
2289 return { _: "never", ast }
2290 case "Enum":
2291 return compileUnion(ast, flattenMembers(enumsToLiterals(ast)))
2292 case "Suspend": {
2293 recursive = true
2294 const layout = compile(ast.thunk())
2295 memo.set(ast, layout)
2296 return layout
2297 }
2298 case "Objects":
2299 return compileStruct(ast)
2300 case "Arrays":
2301 return compileArray(ast)
2302 case "Union":
2303 return compileUnion(ast, flattenMembers(ast))
2304 case "Declaration":
2305 return compileDeclaration(ast)
2306 }
2307 }
2308
2309 function compileStruct(ast: SchemaAST.Objects): StructLayout {
2310 const fields: Array<Field> = []
2311 const types: Array<SchemaAST.AST> = []
2312 const idNames = new Map<number, Array<string>>()
2313 for (const ps of ast.propertySignatures) {
2314 if (typeof ps.name === "symbol") {
2315 throw new Error("Binary layout: symbol property names are illegal")
2316 }
2317 const name = String(ps.name)
2318 const annotations = ps.type.context?.annotations
2319 const explicit = annotations?.[FIELD_ID_ANNOTATION_KEY]
2320 const id = typeof explicit === "number" ? explicit : fnv32(utf8Encode.encode(name))
2321 if (!Number.isInteger(id) || id < 0 || id > 0xFFFFFFFF) {
2322 throw new Error(`Binary layout: illegal field id for ${name}`)
2323 }
2324 const names = idNames.get(id)
2325 if (names === undefined) idNames.set(id, [name])
2326 else names.push(name)
2327 fields.push({
2328 name,
2329 id,
2330 tagBytes: uvarintBytes(id * FIELD_WIRE_FACTOR + FIELD_WIRE_SIZED),
2331 index: 0,
2332 optional: ps.type.context?.isOptional === true,
2333 annotations,
2334 layout: undefined as unknown as Layout,
2335 wireMask: 0,
2336 inline: false
2337 })
2338 types.push(ps.type)
2339 }
2340 for (const [id, names] of idNames) {
2341 if (id === 0 || names.length > 1) {
2342 throw new Error(`Binary layout field id collision: ${id} (${names.join(", ")})`)
2343 }
2344 }
2345 const extra: Array<ExtraSignature> = []
2346 for (const is of ast.indexSignatures) {
2347 if (parameterHasSymbol(is.parameter)) {
2348 throw new Error("Binary layout: symbol property names are illegal")
2349 }
2350 extra.push({ parameter: is.parameter, layout: undefined as unknown as Layout, wireMask: 0 })
2351 }
2352 const layout: StructLayout = {
2353 _: "struct",
2354 ast,
2355 fields,
2356 byId: new Map(),
2357 extra,
2358 extraAll: extra.length === 1 && matchesEveryKey(extra[0].parameter) ? extra[0] : undefined,
2359 names: new Set(fields.map((f) => f.name)),
2360 extraShape: undefined,
2361 optionalCount: 0,
2362 requiredMask: 0,
2363 requiredWide: false
2364 }
2365 memo.set(ast, layout)
2366 for (let i = 0; i < fields.length; i++) {
2367 const field = fields[i]
2368 field.layout = compile(types[i])
2369 field.wireMask = fieldWireMask(field.layout)
2370 // Recursive placeholders already have the discriminant used here.
2371 field.inline = isInlineSlot(field.layout)
2372 if (field.optional) layout.optionalCount++
2373 layout.byId.set(field.id, field)
2374 }
2375 fields.sort((a, b) => a.id - b.id)
2376 for (let i = 0; i < fields.length; i++) fields[i].index = i
2377 computeRequired(layout)
2378 for (let i = 0; i < extra.length; i++) {
2379 extra[i].layout = compile(ast.indexSignatures[i].type)
2380 extra[i].wireMask = fieldWireMask(extra[i].layout)
2381 }
2382 return layout
2383 }
2384
2385 function compileArray(ast: SchemaAST.Arrays): ArrayLayout {
2386 const hasCount = ast.rest.length > 0 || ast.elements.some((e) => e.context?.isOptional === true)
2387 const requiredElements = ast.elements.filter((e) => e.context?.isOptional !== true).length
2388 const tailLen = Math.max(0, ast.rest.length - 1)
2389 const layout: ArrayLayout = {
2390 _: "array",
2391 ast,
2392 elements: [],
2393 rest: [],
2394 hasCount,
2395 minCount: requiredElements + tailLen,
2396 uniform: undefined,
2397 uniformInline: false,
2398 uniformPacked: undefined,
2399 uniformNumbers: false,
2400 run: undefined
2401 }
2402 memo.set(ast, layout)
2403 for (const element of ast.elements) {
2404 layout.elements.push({
2405 optional: element.context?.isOptional === true,
2406 layout: compile(element)
2407 })
2408 }
2409 for (const rest of ast.rest) {
2410 layout.rest.push(compile(rest))
2411 }
2412 if (layout.elements.length === 0 && layout.rest.length === 1) {
2413 const slot = layout.rest[0]
2414 layout.uniform = slot
2415 layout.uniformPacked = packedSize(slot)
2416 layout.uniformNumbers = slot._ === "number"
2417 layout.uniformInline = isInlineSlot(slot)
2418 }
2419 return layout
2420 }
2421
2422 function compileDeclaration(ast: SchemaAST.Declaration): Layout {
2423 const id = representationId(ast)
2424 switch (id) {
2425 case "effect/schema/Date":
2426 return { _: "int64", flavor: "date" }
2427 case "effect/schema/DateTimeUtc":
2428 return { _: "int64", flavor: "utc" }
2429 case "effect/schema/DateTimeZoned":
2430 return leaf("dateTimeZoned")
2431 case "effect/schema/Duration":
2432 return leaf("duration")
2433 case "effect/schema/BigDecimal":
2434 return leaf("bigDecimal")
2435 case "effect/schema/Uint8Array":
2436 return leaf("bytes")
2437 }
2438 const tps = ast.typeParameters
2439 switch (id) {
2440 case "effect/schema/Option": {
2441 const layout = { _: "option" as const, value: undefined as unknown as Layout }
2442 memo.set(ast, layout)
2443 layout.value = compile(tps[0])
2444 return layout
2445 }
2446 case "effect/schema/Result": {
2447 const layout = {
2448 _: "result" as const,
2449 success: undefined as unknown as Layout,
2450 failure: undefined as unknown as Layout
2451 }
2452 memo.set(ast, layout)
2453 layout.success = compile(tps[0])
2454 layout.failure = compile(tps[1])
2455 return layout
2456 }
2457 case "effect/schema/Exit": {
2458 const cause: ReasonLayout = {
2459 _: "cause",
2460 error: undefined as unknown as Layout,
2461 defect: undefined as unknown as Layout
2462 }
2463 const layout = { _: "exit" as const, value: undefined as unknown as Layout, cause }
2464 memo.set(ast, layout)
2465 layout.value = compile(tps[0])
2466 cause.error = compile(tps[1])
2467 cause.defect = compile(tps[2])
2468 return layout
2469 }
2470 case "effect/schema/Cause":
2471 case "effect/schema/CauseReason": {
2472 const layout: ReasonLayout = {
2473 _: id === "effect/schema/Cause" ? "cause" : "causeReason",
2474 error: undefined as unknown as Layout,
2475 defect: undefined as unknown as Layout
2476 }
2477 memo.set(ast, layout)
2478 layout.error = compile(tps[0])
2479 layout.defect = compile(tps[1])
2480 return layout
2481 }
2482 }
2483 if (id !== undefined && id in natives) {
2484 throw new Error(`Binary layout: native declaration ${id} has no layout constructor`)
2485 }
2486 if (isJsonDeclaration(ast)) return leaf("json")
2487 throw new Error(`Binary layout: declaration ${id ?? "<anonymous>"} has no toCodecJson or toCodec`)
2488 }
2489
2490 function compileUnion(ast: SchemaAST.AST, members: Array<SchemaAST.AST>): Layout {
2491 if (members.length === 0) return { _: "never", ast }
2492 const variantMembers: Array<{ member: SchemaAST.AST; sentinels: ReadonlyArray<SchemaAST.Sentinel> }> = []
2493 const rowMembers: Array<{ member: SchemaAST.AST; kind: number }> = []
2494 const literalRows = new Map<number, Layout>()
2495 const literalValues = new Map<number, Array<SchemaAST.LiteralValue>>()
2496 const addLiteralRow = (kind: number, row: Layout) => {
2497 const existing = literalRows.get(kind)
2498 if (existing !== undefined && existing._ !== row._) {
2499 throw new Error("Binary layout: union members are not uniquely identifiable")
2500 }
2501 literalRows.set(kind, row)
2502 }
2503 for (const member of members) {
2504 if (member._tag === "Literal") {
2505 const kind = astKind(member)
2506 const values = literalValues.get(kind)
2507 if (values === undefined) {
2508 literalValues.set(kind, [member.literal])
2509 } else if (!values.includes(member.literal)) {
2510 values.push(member.literal)
2511 }
2512 continue
2513 }
2514 if (member._tag === "UniqueSymbol") {
2515 astKind(member) // validates the symbol is registered
2516 addLiteralRow(K.string, leaf("symbol"))
2517 continue
2518 }
2519 if (member._tag === "Objects" || member._tag === "Arrays") {
2520 const sentinels = SchemaAST.collectSentinels(member)
2521 if (sentinels !== undefined && sentinels.length > 0) {
2522 for (const sentinel of sentinels) {
2523 if (typeof sentinel.key === "symbol") {
2524 throw new Error("Binary layout: symbol property names are illegal")
2525 }
2526 }
2527 variantMembers.push({ member, sentinels })
2528 continue
2529 }
2530 }
2531 rowMembers.push({ member, kind: astKind(member) })
2532 }
2533 for (const [kind, values] of literalValues) {
2534 addLiteralRow(kind, literalLayout(leaf(literalKind(values[0])), values))
2535 }
2536 if (variantMembers.length === 0 && rowMembers.length === 0 && literalRows.size === 1) {
2537 return literalRows.values().next().value!
2538 }
2539 const kinds = new Set<number>(literalRows.keys())
2540 for (const { kind } of rowMembers) {
2541 if (kinds.has(kind)) {
2542 throw new Error("Binary layout: union members are not uniquely identifiable")
2543 }
2544 kinds.add(kind)
2545 }
2546 if (variantMembers.length === 0 && literalRows.size === 0 && rowMembers.length === 1) {
2547 const layout = compile(rowMembers[0].member)
2548 memo.set(ast, layout)
2549 return layout
2550 }
2551 const tags = new Map<number, ReadonlyArray<SchemaAST.Sentinel>>()
2552 const variantTags: Array<number> = []
2553 for (const { sentinels } of variantMembers) {
2554 const tag = sentinelSetHash(sentinels)
2555 if (tags.has(tag)) {
2556 throw new Error(`Binary layout sentinel collision: ${tag}`)
2557 }
2558 tags.set(tag, sentinels)
2559 variantTags.push(tag)
2560 }
2561 const layout: UnionLayout = {
2562 _: "union",
2563 ast,
2564 variants: [],
2565 byTag: new Map(),
2566 discriminator: undefined,
2567 others: [],
2568 byKind: new Map(),
2569 byPos: []
2570 }
2571 memo.set(ast, layout)
2572 for (let v = 0; v < variantMembers.length; v++) {
2573 const { member, sentinels } = variantMembers[v]
2574 const tag = variantTags[v]
2575 const full = compile(member)
2576 let payload: Layout
2577 let tuple: boolean
2578 if (member._tag === "Objects") {
2579 const struct = full as StructLayout
2580 const sentinelNames = new Set(sentinels.map((s) => String(s.key)))
2581 const fields = struct.fields.filter((f) => !sentinelNames.has(f.name))
2582 const payloadStruct: StructLayout = {
2583 _: "struct",
2584 ast: struct.ast,
2585 fields,
2586 byId: new Map(fields.map((f) => [f.id, f])),
2587 extra: struct.extra,
2588 extraAll: struct.extraAll,
2589 names: struct.names,
2590 extraShape: undefined,
2591 optionalCount: fields.reduce((count, f) => f.optional ? count + 1 : count, 0),
2592 requiredMask: 0,
2593 requiredWide: false
2594 }
2595 computeRequired(payloadStruct)
2596 payload = payloadStruct
2597 tuple = false
2598 } else {
2599 payload = full
2600 tuple = true
2601 }
2602 const row: VariantRow = { tag, sentinels, tuple, payload, position: 0 }
2603 layout.variants.push(row)
2604 layout.byTag.set(tag, row)
2605 }
2606 for (const [kind, row] of literalRows) {
2607 layout.others.push({ kind, layout: row, position: 0 })
2608 layout.byKind.set(kind, row)
2609 }
2610 for (const { kind, member } of rowMembers) {
2611 const row = compile(member)
2612 layout.others.push({ kind, layout: row, position: 0 })
2613 layout.byKind.set(kind, row)
2614 }
2615 // Fingerprint positions are independent of declaration order.
2616 for (const row of [...layout.variants].sort((a, b) => a.tag - b.tag)) {
2617 row.position = layout.byPos.length
2618 layout.byPos.push({ variant: row, layout: row.payload })
2619 }
2620 for (const member of [...layout.others].sort((a, b) => a.kind - b.kind)) {
2621 member.position = layout.byPos.length
2622 layout.byPos.push({ variant: undefined, layout: member.layout })
2623 }
2624 // Probe specific runtime guards before `json`, which matches anything.
2625 layout.others.sort((a, b) => matchRank(a.layout) - matchRank(b.layout))
2626 layout.discriminator = findDiscriminator(layout.variants)
2627 return layout
2628 }
2629
2630 const layout = compile(root)
2631 return { layout, recursive, exact, exitSuccess }
2632}
2633
2634// The parser may return the binary decoder's value directly only when that
2635// decoder proves every predicate and no Schema parser behavior can change it.
2636// Schemas whose decoded values the binary layer already produces and validates
2637// on its own, so the schema pass around it would only repeat work. Constructor
2638// defaults are ignored because they only run during construction.
2639function isExact(root: SchemaAST.AST): boolean {
2640 // A suspend that is still being walked counts as exact: if nothing else in
2641 // the cycle disqualifies it, the whole cycle is exact.
2642 const walked = new Map<SchemaAST.AST, boolean>()
2643 const exact = (ast: SchemaAST.AST): boolean => {
2644 if (
2645 ast.encoding !== undefined || ast.checks !== undefined ||
2646 (ast as { readonly encodingChecks?: SchemaAST.Checks }).encodingChecks !== undefined
2647 ) {
2648 return false
2649 }
2650 switch (ast._tag) {
2651 case "String":
2652 case "Symbol":
2653 case "Boolean":
2654 case "Null":
2655 case "Undefined":
2656 case "Void":
2657 case "Number":
2658 case "BigInt":
2659 case "Literal":
2660 return true
2661 // The binary layer rejects every value for a never layout, in both
2662 // directions, so the schema pass has nothing left to say about it.
2663 // Inferred schemas hit this through the element type of an empty array.
2664 case "Never":
2665 return true
2666 // Every decoded JSON value is a valid `unknown`, and the encoder already
2667 // reports the values it cannot represent, so the pass adds nothing.
2668 // `ObjectKeyword` is not here: the JSON layout can hand it a string.
2669 case "Unknown":
2670 case "Any":
2671 return true
2672 case "Arrays":
2673 return ast.elements.every(exact) && ast.rest.every(exact)
2674 case "Objects":
2675 return ast.propertySignatures.every((property) => exact(property.type)) &&
2676 ast.indexSignatures.every((signature) =>
2677 signature.parameter._tag === "String" && exact(signature.parameter) && exact(signature.type)
2678 )
2679 case "Union":
2680 return (ast.options?.mode ?? "anyOf") === "anyOf" && ast.types.every(exact)
2681 // The layout compiles straight through a suspend, so the binary layer
2682 // validates whatever the thunk returns. Only decoding gets to act on
2683 // this: encoding a recursive schema still needs the cycle walk, which is
2684 // why {@link toCodec} keeps the schema pass on that side.
2685 case "Suspend": {
2686 const seen = walked.get(ast)
2687 if (seen !== undefined) return seen
2688 walked.set(ast, true)
2689 const result = exact(ast.thunk())
2690 walked.set(ast, result)
2691 return result
2692 }
2693 case "Declaration": {
2694 const id = representationId(ast)
2695 const native = id !== undefined ? natives[id] : undefined
2696 if (native === undefined) return false
2697 return native.exact === true || ast.typeParameters.every(exact)
2698 }
2699 default:
2700 return false
2701 }
2702 }
2703 return exact(root)
2704}
2705
2706// A clean Exit whose success schema is exact: the binary layer fully produces
2707// and validates success exits, so only failure exits need the schema pass for
2708// their cause's error and defect encodings.
2709function isExitWithExactSuccess(root: SchemaAST.AST): boolean {
2710 return root._tag === "Declaration" &&
2711 root.encoding === undefined && root.checks === undefined &&
2712 (root as { readonly encodingChecks?: SchemaAST.Checks }).encodingChecks === undefined &&
2713 representationId(root) === "effect/schema/Exit" &&
2714 isExact(root.typeParameters[0])
2715}
2716
2717// Layouts sharing a wire kind can still require different fingerprints.
2718const F = {
2719 backEdge: 0,
2720 bool: 1,
2721 null: 2,
2722 undefined: 3,
2723 number: 4,
2724 int: 5,
2725 string: 6,
2726 symbol: 7,
2727 bytes: 8,
2728 bigint: 9,
2729 json: 10,
2730 duration: 11,
2731 bigDecimal: 12,
2732 dateTimeZoned: 13,
2733 date: 14,
2734 dateTimeUtc: 15,
2735 never: 16,
2736 struct: 17,
2737 array: 18,
2738 union: 19,
2739 option: 20,
2740 result: 21,
2741 exit: 22,
2742 cause: 23,
2743 causeReason: 24,
2744 literal: 25
2745} as const
2746
2747// Hash the compiled layout graph, including cycle back-edge distances.
2748function layoutFingerprint(root: Layout): bigint {
2749 const cache = new Map<Layout, bigint>()
2750 const stack: Array<Layout> = []
2751 // Cache only subtrees that do not escape above their own root.
2752 let escape = Number.MAX_SAFE_INTEGER
2753
2754 function go(layout: Layout): bigint {
2755 const at = stack.lastIndexOf(layout)
2756 if (at >= 0) {
2757 if (at < escape) escape = at
2758 const out: Array<number> = [F.backEdge]
2759 pushUvarint(out, stack.length - at)
2760 return fnv64(out)
2761 }
2762 const cached = cache.get(layout)
2763 if (cached !== undefined) return cached
2764 const self = stack.length
2765 const outerEscape = escape
2766 escape = Number.MAX_SAFE_INTEGER
2767 stack.push(layout)
2768 const hash = fnv64(structure(layout))
2769 stack.pop()
2770 const closed = escape >= self
2771 if (closed) cache.set(layout, hash)
2772 escape = closed ? outerEscape : Math.min(outerEscape, escape)
2773 return hash
2774 }
2775
2776 function structure(layout: Layout): Array<number> {
2777 const out: Array<number> = []
2778 switch (layout._) {
2779 case "bool":
2780 case "null":
2781 case "undefined":
2782 case "number":
2783 case "int":
2784 case "string":
2785 case "symbol":
2786 case "bytes":
2787 case "bigint":
2788 case "json":
2789 case "duration":
2790 case "bigDecimal":
2791 case "dateTimeZoned":
2792 out.push(F[layout._])
2793 return out
2794 case "literal": {
2795 out.push(F.literal)
2796 pushBytes(out, structure(layout.leaf))
2797 pushUvarint(out, layout.values.length)
2798 // Sorted, so a union hashes the same whatever order it declares.
2799 const sorted = layout.values
2800 .map((value) => [sentinelLiteralKind(value), sentinelLiteralString(value)] as const)
2801 .sort((a, b) => a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : a[0] - b[0])
2802 for (const [kind, text] of sorted) {
2803 out.push(kind)
2804 const bytes = utf8Encode.encode(text)
2805 pushUvarint(out, bytes.length)
2806 pushBytes(out, bytes)
2807 }
2808 return out
2809 }
2810 case "int64":
2811 out.push(layout.flavor === "date" ? F.date : F.dateTimeUtc)
2812 return out
2813 case "never":
2814 out.push(F.never)
2815 return out
2816 case "struct": {
2817 out.push(F.struct)
2818 pushUvarint(out, layout.fields.length)
2819 for (const field of layout.fields) {
2820 pushU32(out, field.id)
2821 out.push(field.optional ? 1 : 0)
2822 pushU64(out, go(field.layout))
2823 }
2824 pushUvarint(out, layout.extra.length)
2825 for (const signature of layout.extra) pushU64(out, go(signature.layout))
2826 return out
2827 }
2828 case "array": {
2829 out.push(F.array)
2830 pushUvarint(out, layout.elements.length)
2831 for (const slot of layout.elements) {
2832 out.push(slot.optional ? 1 : 0)
2833 pushU64(out, go(slot.layout))
2834 }
2835 pushUvarint(out, layout.rest.length)
2836 for (const rest of layout.rest) pushU64(out, go(rest))
2837 return out
2838 }
2839 case "union": {
2840 out.push(F.union)
2841 pushUvarint(out, layout.byPos.length)
2842 for (const position of layout.byPos) {
2843 const variant = position.variant
2844 if (variant === undefined) out.push(0)
2845 else {
2846 out.push(1, variant.tuple ? 1 : 0)
2847 pushU32(out, variant.tag)
2848 }
2849 pushU64(out, go(position.layout))
2850 }
2851 return out
2852 }
2853 case "option":
2854 out.push(F.option)
2855 pushU64(out, go(layout.value))
2856 return out
2857 case "result":
2858 out.push(F.result)
2859 pushU64(out, go(layout.success))
2860 pushU64(out, go(layout.failure))
2861 return out
2862 case "exit":
2863 out.push(F.exit)
2864 pushU64(out, go(layout.value))
2865 pushU64(out, go(layout.cause.error))
2866 pushU64(out, go(layout.cause.defect))
2867 return out
2868 case "cause":
2869 case "causeReason":
2870 out.push(layout._ === "cause" ? F.cause : F.causeReason)
2871 pushU64(out, go(layout.error))
2872 pushU64(out, go(layout.defect))
2873 return out
2874 }
2875 }
2876
2877 return go(root)
2878}
2879
2880// A frame header the decoder can compare byte by byte, so frames that hold no
2881// wide numbers never build a DataView.
2882function fingerprintBytes(lo: number, hi: number): Uint8Array {
2883 const out = new Uint8Array(8)
2884 for (let i = 0; i < 4; i++) {
2885 out[i] = (lo >>> (i * 8)) & 0xFF
2886 out[i + 4] = (hi >>> (i * 8)) & 0xFF
2887 }
2888 return out
2889}
2890
2891interface Mode {
2892 readonly positional: boolean
2893 readonly envelope: number
2894 readonly expectedEnvelope: string
2895 readonly fingerprint: Uint8Array
2896}
2897
2898const defaultMode: Mode = {
2899 positional: false,
2900 envelope: ENVELOPE,
2901 expectedEnvelope: "version 2 envelope, flags 0",
2902 fingerprint: new Uint8Array(8)
2903}
2904
2905function fingerprintMode(layout: Layout): Mode {
2906 const fingerprint = layoutFingerprint(layout)
2907 return {
2908 positional: true,
2909 envelope: ENVELOPE_FINGERPRINT,
2910 expectedEnvelope: "version 2 envelope, flags 1",
2911 fingerprint: fingerprintBytes(
2912 Number(fingerprint & BIGINT_U32_MASK),
2913 Number((fingerprint >> BIGINT_THIRTY_TWO) & BIGINT_U32_MASK)
2914 )
2915 }
2916}
2917
2918function matchRank(layout: Layout): number {
2919 switch (layout._) {
2920 case "json":
2921 return 2
2922 case "struct":
2923 return 1
2924 default:
2925 return 0
2926 }
2927}
2928
2929function matchesLayout(layout: Layout, value: unknown): boolean {
2930 switch (layout._) {
2931 case "literal":
2932 return hasLiteral(layout, value)
2933 case "bool":
2934 return typeof value === "boolean"
2935 case "null":
2936 return value === null
2937 case "undefined":
2938 return value === undefined
2939 case "number":
2940 case "int":
2941 return typeof value === "number"
2942 case "string":
2943 return typeof value === "string"
2944 case "symbol":
2945 return typeof value === "symbol"
2946 case "bigint":
2947 return typeof value === "bigint"
2948 case "bytes":
2949 return value instanceof Uint8Array
2950 case "int64":
2951 return layout.flavor === "date"
2952 ? value instanceof Date
2953 : DateTime.isDateTime(value) && value._tag === "Utc"
2954 case "dateTimeZoned":
2955 return DateTime.isDateTime(value) && value._tag === "Zoned"
2956 case "duration":
2957 return Duration.isDuration(value)
2958 case "bigDecimal":
2959 return BigDecimal.isBigDecimal(value)
2960 case "option":
2961 return Option.isOption(value)
2962 case "result":
2963 return Result.isResult(value)
2964 case "exit":
2965 return Exit.isExit(value)
2966 case "cause":
2967 return Cause.isCause(value)
2968 case "causeReason":
2969 return Cause.isReason(value)
2970 case "array":
2971 return Array.isArray(value)
2972 case "struct":
2973 return Predicate.isObject(value) && !Array.isArray(value)
2974 case "json":
2975 return true
2976 case "union":
2977 case "never":
2978 return false
2979 }
2980}
2981
2982interface EncodeContext {
2983 readonly options: SchemaAST.ParseOptions
2984 readonly positional: boolean
2985 indexSignatures: IndexSignatureCache | undefined
2986 // Records of one shape repeat within a frame, so the last one is reused.
2987 extraShape: ExtraShape | undefined
2988 // Handed to the row-run field's immediate consumer, then cleared.
2989 intern: InternWrite | undefined
2990 // String tables shared by every frame on one connection; `undefined` for a
2991 // one-shot encode, where each frame stands alone.
2992 readonly dict: DictWrite | undefined
2993}
2994
2995function encodeFail(expected: string, input: unknown, options: SchemaAST.ParseOptions): never {
2996 throw issueError(new SchemaIssue.InvalidValue({ expected }, input, options))
2997}
2998
2999function isCyclic(value: unknown, stack = new Set<object>()): boolean {
3000 if (!Predicate.isObjectOrArray(value)) return false
3001 const isArray = Array.isArray(value)
3002 const prototype = Object.getPrototypeOf(value)
3003 if (isArray || prototype === Object.prototype || prototype === null) {
3004 if (stack.has(value)) return true
3005 stack.add(value)
3006 try {
3007 if (isArray) {
3008 for (let i = 0; i < value.length; i++) {
3009 if (isCyclic(value[i], stack)) return true
3010 }
3011 } else {
3012 for (const key of Object.keys(value)) {
3013 if (isCyclic((value as Record<string, unknown>)[key], stack)) return true
3014 }
3015 }
3016 } finally {
3017 stack.delete(value)
3018 }
3019 return false
3020 }
3021 if (
3022 value instanceof Date ||
3023 value instanceof Uint8Array ||
3024 DateTime.isDateTime(value) ||
3025 Duration.isDuration(value) ||
3026 BigDecimal.isBigDecimal(value)
3027 ) return false
3028 if (stack.has(value)) return true
3029 stack.add(value)
3030 try {
3031 if (Option.isOption(value)) {
3032 return Option.isSome(value) && isCyclic(value.value, stack)
3033 }
3034 if (Result.isResult(value)) {
3035 return isCyclic(Result.isSuccess(value) ? value.success : value.failure, stack)
3036 }
3037 if (Exit.isExit(value)) {
3038 return isCyclic(Exit.isSuccess(value) ? value.value : value.cause, stack)
3039 }
3040 if (Cause.isCause(value)) {
3041 return value.reasons.some((reason) => isCyclic(reason, stack))
3042 }
3043 if (Cause.isReason(value)) {
3044 switch (value._tag) {
3045 case "Fail":
3046 return isCyclic(value.error, stack)
3047 case "Die":
3048 return isCyclic(value.defect, stack)
3049 case "Interrupt":
3050 return false
3051 }
3052 }
3053 if (Chunk.isChunk(value)) {
3054 for (const item of value) {
3055 if (isCyclic(item, stack)) return true
3056 }
3057 return false
3058 }
3059 if (HashMap.isHashMap(value)) {
3060 for (const [key, item] of value) {
3061 if (isCyclic(key, stack) || isCyclic(item, stack)) return true
3062 }
3063 return false
3064 }
3065 if (HashSet.isHashSet(value)) {
3066 for (const item of value) {
3067 if (isCyclic(item, stack)) return true
3068 }
3069 return false
3070 }
3071 if (Redacted.isRedacted(value)) {
3072 return isCyclic(Redacted.value(value), stack)
3073 }
3074 for (const key of Object.keys(value)) {
3075 if (isCyclic((value as Record<string, unknown>)[key], stack)) return true
3076 }
3077 } finally {
3078 stack.delete(value)
3079 }
3080 return false
3081}
3082
3083function encodeSized(ctx: EncodeContext, layout: Layout, value: unknown, w: Writer) {
3084 const mark = w.beginSized()
3085 encodeValue(ctx, layout, value, w)
3086 w.endSized(mark)
3087}
3088
3089function encodeSymbol(ctx: EncodeContext, value: unknown, w: Writer) {
3090 const key = globalThis.Symbol.keyFor(value as symbol)
3091 if (key === undefined) encodeFail("registered symbol", value, ctx.options)
3092 w.string(key)
3093}
3094
3095function encodeReason(ctx: EncodeContext, layout: ReasonLayout, value: unknown, w: Writer) {
3096 const reason = value as Cause.Reason<unknown>
3097 switch (reason._tag) {
3098 case "Fail":
3099 w.byte(0)
3100 encodeValue(ctx, layout.error, reason.error, w)
3101 return
3102 case "Die":
3103 w.byte(1)
3104 encodeValue(ctx, layout.defect, reason.defect, w)
3105 return
3106 case "Interrupt":
3107 if (reason.fiberId === undefined) {
3108 w.byte(2)
3109 } else {
3110 w.byte(3)
3111 w.f64(reason.fiberId)
3112 }
3113 }
3114}
3115
3116// `keyBytes` stays undefined while every key is ASCII, where code unit order
3117// and length already match raw UTF-8.
3118type ExtraPair = [key: string, signature: ExtraSignature, keyBytes: Uint8Array | undefined]
3119
3120function isAscii(key: string): boolean {
3121 for (let i = 0; i < key.length; i++) {
3122 if (key.charCodeAt(i) > 0x7F) return false
3123 }
3124 return true
3125}
3126
3127interface ExtraShape {
3128 readonly layout: StructLayout
3129 readonly keys: ReadonlyArray<string>
3130 readonly pairs: Array<ExtraPair>
3131}
3132
3133function sameShape(shape: ExtraShape, layout: StructLayout, keys: ReadonlyArray<string>): boolean {
3134 if (shape.layout !== layout || shape.keys.length !== keys.length) return false
3135 for (let i = 0; i < keys.length; i++) {
3136 if (shape.keys[i] !== keys[i]) return false
3137 }
3138 return true
3139}
3140
3141function canCacheExtraShape(layout: StructLayout, keys: ReadonlyArray<string>): boolean {
3142 if (layout.extraAll === undefined || keys.length > ENCODE_INDEX_SIGNATURE_CACHE_KEYS) return false
3143 let codeUnits = 0
3144 for (let i = 0; i < keys.length; i++) {
3145 codeUnits += keys[i].length
3146 if (codeUnits > ENCODE_INDEX_SIGNATURE_CACHE_CODE_UNITS) return false
3147 }
3148 return true
3149}
3150
3151// Sort extra keys by raw UTF-8 for deterministic output.
3152function extraPairs(ctx: EncodeContext, layout: StructLayout, obj: Record<string, unknown>): Array<ExtraPair> {
3153 const keys = Object.keys(obj)
3154 const cached = layout.extraShape
3155 if (cached !== undefined && sameShape(cached, layout, keys)) return cached.pairs
3156 const persistent = canCacheExtraShape(layout, keys)
3157 const shape = persistent ? undefined : ctx.extraShape
3158 if (shape !== undefined && sameShape(shape, layout, keys)) return shape.pairs
3159 const named = layout.names
3160 const every = layout.extraAll
3161 const pairs: Array<ExtraPair> = []
3162 let ascii = true
3163 for (const key of keys) {
3164 if (named.has(key)) continue
3165 const signature = every ?? (ctx.indexSignatures ??= new IndexSignatureCache(ctx.options)).find(layout, key)
3166 if (signature === undefined) continue
3167 if (ascii && !isAscii(key)) ascii = false
3168 pairs.push([key, signature, undefined])
3169 }
3170 if (ascii) {
3171 pairs.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)
3172 } else {
3173 for (const pair of pairs) pair[2] = utf8Encode.encode(pair[0])
3174 pairs.sort((a, b) => compareBytes(a[2]!, b[2]!))
3175 }
3176 if (persistent) {
3177 for (const pair of pairs) pair[2] ??= utf8Encode.encode(pair[0])
3178 }
3179 const next = { layout, keys, pairs }
3180 if (persistent) layout.extraShape = next
3181 else ctx.extraShape = next
3182 return pairs
3183}
3184
3185function encodeExtraPairs(
3186 ctx: EncodeContext,
3187 pairs: Array<ExtraPair>,
3188 obj: Record<string, unknown>,
3189 w: Writer,
3190 keys?: InternWrite | undefined
3191) {
3192 for (const [key, signature, keyBytes] of pairs) {
3193 const keyLength = keyBytes === undefined ? key.length : keyBytes.length
3194 const value = obj[key]
3195 const wire = valueWireCode(signature.layout, value, ctx.options)
3196 const kind = wire % FIELD_WIRE_FACTOR
3197 if (keys === undefined) {
3198 w.uvarint(keyLength * FIELD_WIRE_FACTOR + kind)
3199 } else {
3200 if (!keys.disabled) {
3201 const ref = internRef(keys, key)
3202 if (ref !== undefined) {
3203 w.uvarint(ref * 16 + kind * 2 + 1)
3204 issuePath[issuePathLen++] = key
3205 encodeWirePayload(ctx, signature.layout, value, wire, w)
3206 issuePathLen--
3207 continue
3208 }
3209 internAdd(keys, key)
3210 }
3211 w.uvarint(keyLength * 16 + kind * 2)
3212 }
3213 if (keyBytes === undefined) w.string(key)
3214 else w.bytes(keyBytes)
3215 issuePath[issuePathLen++] = key
3216 encodeWirePayload(ctx, signature.layout, value, wire, w)
3217 issuePathLen--
3218 }
3219}
3220
3221// Decimal scale rides above the low three kind bits internally, avoiding a
3222// second decimalScale scan when the payload is written.
3223function valueWireCode(layout: Layout, value: unknown, options: SchemaAST.ParseOptions): number {
3224 if (layout._ === "literal") {
3225 if (!hasLiteral(layout, value)) encodeFail(literalExpected(layout), value, options)
3226 return valueWireCode(layout.leaf, value, options)
3227 }
3228 switch (layout._) {
3229 case "bool":
3230 if (typeof value !== "boolean") encodeFail("a boolean", value, options)
3231 return value ? FIELD_WIRE_TRUE : FIELD_WIRE_FALSE
3232 case "number":
3233 if (typeof value !== "number") encodeFail("a number", value, options)
3234 if (isVarintNumber(value)) return FIELD_WIRE_VARINT
3235 {
3236 const scale = decimalScale(value)
3237 return scale === 0 ? FIELD_WIRE_FIXED64 : scale * FIELD_WIRE_FACTOR + FIELD_WIRE_DECIMAL
3238 }
3239 case "int":
3240 if (!Number.isSafeInteger(value)) encodeFail("an integer", value, options)
3241 return FIELD_WIRE_VARINT
3242 default:
3243 return FIELD_WIRE_SIZED
3244 }
3245}
3246
3247function encodeWirePayload(ctx: EncodeContext, layout: Layout, value: unknown, wire: number, w: Writer) {
3248 const kind = wire % FIELD_WIRE_FACTOR
3249 switch (kind) {
3250 // EMPTY and FIXED32 are decoder-side skip support only; `valueWireCode`
3251 // never produces them.
3252 case FIELD_WIRE_FALSE:
3253 case FIELD_WIRE_TRUE:
3254 return
3255 case FIELD_WIRE_VARINT:
3256 w.numberVarint(value as number)
3257 return
3258 case FIELD_WIRE_FIXED64:
3259 w.f64(value as number)
3260 return
3261 case FIELD_WIRE_DECIMAL: {
3262 const scale = Math.floor(wire / FIELD_WIRE_FACTOR)
3263 w.numberVarint(Math.round((value as number) * POW10[scale]))
3264 w.uvarint(scale)
3265 return
3266 }
3267 default:
3268 encodeSized(ctx, layout, value, w)
3269 }
3270}
3271
3272function encodeStructField(ctx: EncodeContext, field: Field, value: unknown, w: Writer) {
3273 const wire = valueWireCode(field.layout, value, ctx.options)
3274 const kind = wire % FIELD_WIRE_FACTOR
3275 if (kind === FIELD_WIRE_SIZED) {
3276 const mark = w.idAndMark(field.tagBytes)
3277 encodeValue(ctx, field.layout, value, w)
3278 w.endSized(mark)
3279 return
3280 }
3281 w.uvarint(field.id * FIELD_WIRE_FACTOR + kind)
3282 encodeWirePayload(ctx, field.layout, value, wire, w)
3283}
3284
3285function runPresenceMask(plan: RunPlan, obj: Record<string, unknown>): number | undefined {
3286 const byName = plan.byName
3287 if (byName === undefined) return undefined
3288 const keys = Object.getOwnPropertyNames(obj)
3289 let mask = 0
3290 for (let i = 0; i < keys.length; i++) {
3291 const index = byName.get(keys[i])
3292 if (index !== undefined) mask |= 1 << index
3293 }
3294 const struct = plan.struct
3295 if ((mask & struct.requiredMask) !== struct.requiredMask) {
3296 const fields = struct.fields
3297 for (let i = 0; i < fields.length; i++) {
3298 const field = fields[i]
3299 if (!field.optional && (mask & (1 << i)) === 0) {
3300 issuePath[issuePathLen++] = field.name
3301 throw issueError(new SchemaIssue.MissingKey(field.annotations))
3302 }
3303 }
3304 }
3305 return mask
3306}
3307
3308function encodeStructFields(ctx: EncodeContext, layout: StructLayout, value: object, w: Writer) {
3309 const obj = value as Record<string, unknown>
3310 if (layout.extra.length > 0) {
3311 // Only a run field with index signatures hands over an intern table.
3312 const keys = ctx.intern
3313 ctx.intern = undefined
3314 const pairs = extraPairs(ctx, layout, obj)
3315 if (pairs.length > 0) {
3316 w.uvarint(0)
3317 const mark = w.beginSized()
3318 encodeExtraPairs(ctx, pairs, obj, w, keys)
3319 w.endSized(mark)
3320 }
3321 }
3322 const fields = layout.fields
3323 for (let i = 0; i < fields.length; i++) {
3324 const field = fields[i]
3325 const name = field.name
3326 if (!Object.hasOwn(obj, name)) {
3327 if (field.optional) continue
3328 issuePath[issuePathLen++] = name
3329 throw issueError(new SchemaIssue.MissingKey(field.annotations))
3330 }
3331 issuePath[issuePathLen++] = name
3332 encodeStructField(ctx, field, obj[name], w)
3333 issuePathLen--
3334 }
3335}
3336
3337// Fingerprint structs use a presence bitmap followed by positional fields.
3338function encodeStructPositional(ctx: EncodeContext, layout: StructLayout, value: object, w: Writer) {
3339 const obj = value as Record<string, unknown>
3340 // Only a run field with index signatures hands over an intern table. Extras
3341 // encode after the fields here, so claim it before a nested field can.
3342 const keys = ctx.intern
3343 ctx.intern = undefined
3344 const bitmapBytes = (layout.optionalCount + 7) >> 3
3345 let bitmap = 0
3346 if (bitmapBytes > 0) {
3347 bitmap = w.len
3348 for (let i = 0; i < bitmapBytes; i++) w.byte(0)
3349 }
3350 const fields = layout.fields
3351 let optionalIndex = 0
3352 for (let i = 0; i < fields.length; i++) {
3353 const field = fields[i]
3354 const name = field.name
3355 const present = Object.hasOwn(obj, name)
3356 if (field.optional) {
3357 // Resolve the bitmap address again after any arena growth.
3358 if (present) w.buf[w.start + bitmap + (optionalIndex >> 3)] |= 1 << (optionalIndex & 7)
3359 optionalIndex++
3360 if (!present) continue
3361 } else if (!present) {
3362 issuePath[issuePathLen++] = name
3363 throw issueError(new SchemaIssue.MissingKey(field.annotations))
3364 }
3365 issuePath[issuePathLen++] = name
3366 if (field.inline) encodeValue(ctx, field.layout, obj[name], w)
3367 else encodeSized(ctx, field.layout, obj[name], w)
3368 issuePathLen--
3369 }
3370 if (layout.extra.length > 0) {
3371 const pairs = extraPairs(ctx, layout, obj)
3372 w.uvarint(pairs.length)
3373 encodeExtraPairs(ctx, pairs, obj, w, keys)
3374 }
3375}
3376
3377function internKind(layout: Layout): number {
3378 switch (layout._) {
3379 case "string":
3380 return INTERN_SELF
3381 case "array":
3382 return layout.uniform?._ === "string" ? INTERN_ELEMENTS : INTERN_NONE
3383 case "struct":
3384 return layout.extra.length > 0 ? INTERN_KEYS : INTERN_NONE
3385 default:
3386 return INTERN_NONE
3387 }
3388}
3389
3390// `Schema.Array(S)` and `Schema.NonEmptyArray(S)` both give every slot one layout.
3391function runStruct(layout: ArrayLayout): StructLayout | undefined {
3392 let uniform = layout.uniform
3393 if (uniform === undefined) {
3394 if (layout.rest.length !== 1) return undefined
3395 uniform = layout.rest[0]
3396 for (const slot of layout.elements) {
3397 if (slot.optional || slot.layout !== uniform) return undefined
3398 }
3399 }
3400 return uniform._ === "struct" ? uniform : undefined
3401}
3402
3403function runPlan(layout: ArrayLayout): RunPlan | null {
3404 let plan = layout.run
3405 if (plan !== undefined) return plan
3406 const struct = runStruct(layout)
3407 if (struct === undefined) {
3408 layout.run = plan = null
3409 return plan
3410 }
3411 const intern = struct.fields.map((field) => internKind(field.layout))
3412 const shapes = struct.fields.length <= RUN_MAX_FIELDS
3413 plan = {
3414 struct,
3415 intern,
3416 shapes,
3417 byName: shapes && struct.extra.length === 0
3418 ? new Map(struct.fields.map((field, index) => [field.name, index]))
3419 : undefined
3420 }
3421 layout.run = plan
3422 return plan
3423}
3424
3425// One table per field, so a field the reader skips can never shift another
3426// field's references. Holds the values written literally so far, in reference
3427// order. Short runs scan the array and longer ones build a map, while a table
3428// whose first `INTERN_DISABLE_AT` values never repeated stops tracking
3429// entirely: references are optional for writers, and the reader keeps indexing
3430// literal values either way, so only repeats with more than that many distinct
3431// values between them lose their back-references.
3432interface InternWrite {
3433 readonly values: Array<unknown>
3434 refs: Map<unknown, number> | undefined
3435 hits: number
3436 disabled: boolean
3437}
3438
3439const INTERN_MAP_AT = 16
3440const INTERN_DISABLE_AT = 64
3441
3442function internWrite(tables: Array<InternWrite | undefined>, index: number): InternWrite {
3443 return tables[index] ??= { values: [], refs: undefined, hits: 0, disabled: false }
3444}
3445
3446function internRef(table: InternWrite, value: unknown): number | undefined {
3447 const refs = table.refs
3448 if (refs !== undefined) {
3449 const ref = refs.get(value)
3450 if (ref !== undefined) table.hits++
3451 return ref
3452 }
3453 const values = table.values
3454 for (let i = 0; i < values.length; i++) {
3455 if (values[i] === value) {
3456 table.hits++
3457 return i
3458 }
3459 }
3460 return undefined
3461}
3462
3463function internAdd(table: InternWrite, value: unknown) {
3464 const values = table.values
3465 if (table.refs !== undefined) {
3466 if (table.hits === 0 && values.length >= INTERN_DISABLE_AT) {
3467 table.disabled = true
3468 table.refs = undefined
3469 return
3470 }
3471 table.refs.set(value, values.length)
3472 } else if (values.length >= INTERN_MAP_AT) {
3473 const refs = table.refs = new Map()
3474 for (let i = 0; i < values.length; i++) refs.set(values[i], i)
3475 refs.set(value, values.length)
3476 }
3477 values.push(value)
3478}
3479
3480// A connection dictionary: string tables that outlive a single frame. The
3481// writer and the reader build the same tables from the same frames, so a
3482// reference means the same string on both ends. Fields that share a schema
3483// object share a table, which is why `Schema.String` fields all land in one:
3484// splitting them costs every layout a few percent and buys the RPC envelope,
3485// where every string repeats, nothing.
3486//
3487// A leaf starts out only watching. It writes plain strings and remembers them
3488// until one comes back, and only then starts writing references. A leaf whose
3489// strings never repeat gives up and goes back to writing plain strings for
3490// good, so it never costs a byte more than no dictionary at all.
3491const DICT_WATCHING = 0
3492const DICT_REFERENCING = 1
3493const DICT_GAVE_UP = 2
3494
3495// How many distinct strings a leaf watches before giving up on it.
3496const DICT_WATCH_LIMIT = 64
3497// How many strings one leaf keeps once it is referencing.
3498const DICT_MAX_ENTRIES = 512
3499
3500interface DictSlot {
3501 readonly values: Array<string>
3502 readonly refs: Map<string, number>
3503 state: number
3504 // Frame that last touched this slot, so a frame records one mark per slot.
3505 stamp: number
3506}
3507
3508type DictRead = Array<DictSlot | undefined>
3509
3510interface DictWrite {
3511 readonly slots: DictRead
3512 // A frame that fails is never sent, so its additions have to come back out or
3513 // the reader's tables drift from the writer's.
3514 readonly marks: Array<DictMark>
3515 frame: number
3516}
3517
3518interface DictMark {
3519 readonly slot: DictSlot
3520 readonly length: number
3521 readonly state: number
3522}
3523
3524function makeDictWrite(): DictWrite {
3525 return { slots: [], marks: [], frame: 0 }
3526}
3527
3528function dictSlot(slots: DictRead, index: number): DictSlot {
3529 return slots[index] ??= { values: [], refs: new Map(), state: DICT_WATCHING, stamp: -1 }
3530}
3531
3532function dictWatch(slot: DictSlot, value: string) {
3533 if (slot.refs.has(value)) {
3534 slot.state = DICT_REFERENCING
3535 return
3536 }
3537 slot.refs.set(value, slot.values.length)
3538 slot.values.push(value)
3539 if (slot.values.length >= DICT_WATCH_LIMIT) slot.state = DICT_GAVE_UP
3540}
3541
3542function dictAdd(slot: DictSlot, value: string) {
3543 const values = slot.values
3544 if (values.length >= DICT_MAX_ENTRIES) return
3545 slot.refs.set(value, values.length)
3546 values.push(value)
3547}
3548
3549function dictMark(dict: DictWrite, slot: DictSlot) {
3550 if (slot.stamp === dict.frame) return
3551 slot.stamp = dict.frame
3552 dict.marks.push({ slot, length: slot.values.length, state: slot.state })
3553}
3554
3555function dictCommit(dict: DictWrite) {
3556 dict.marks.length = 0
3557 dict.frame++
3558}
3559
3560function dictRollback(dict: DictWrite) {
3561 const marks = dict.marks
3562 for (let i = marks.length - 1; i >= 0; i--) {
3563 const { length, slot, state } = marks[i]
3564 const values = slot.values
3565 for (let j = length; j < values.length; j++) slot.refs.delete(values[j])
3566 values.length = length
3567 slot.state = state
3568 }
3569 marks.length = 0
3570 dict.frame++
3571}
3572
3573// A referencing leaf writes either a reference or a zero marker and the string.
3574// A watching leaf writes the string alone, so watching costs nothing on the
3575// wire.
3576function encodeString(dict: DictWrite, layout: LeafLayout, value: string, w: Writer) {
3577 const slot = dictSlot(dict.slots, layout.slot)
3578 const state = slot.state
3579 if (state === DICT_GAVE_UP) {
3580 w.string(value)
3581 return
3582 }
3583 dictMark(dict, slot)
3584 if (state === DICT_WATCHING) {
3585 w.string(value)
3586 dictWatch(slot, value)
3587 return
3588 }
3589 const ref = slot.refs.get(value)
3590 if (ref !== undefined) {
3591 w.uvarint(ref * 2 + 1)
3592 return
3593 }
3594 w.byte(0)
3595 dictAdd(slot, value)
3596 w.string(value)
3597}
3598
3599function decodeString(dict: DictRead, layout: LeafLayout, r: Reader): string {
3600 const slot = dictSlot(dict, layout.slot)
3601 const state = slot.state
3602 if (state === DICT_GAVE_UP) return r.readUtf8(r.end - r.pos)
3603 if (state === DICT_WATCHING) {
3604 const value = r.readUtf8(r.end - r.pos)
3605 dictWatch(slot, value)
3606 return value
3607 }
3608 const marker = r.uvarint()
3609 if ((marker & 1) === 1) {
3610 const ref = (marker - 1) / 2
3611 const values = slot.values
3612 if (ref >= values.length) invalid("a dictionary reference", marker, r.options)
3613 return values[ref]
3614 }
3615 if (marker !== 0) invalid("a dictionary marker", marker, r.options)
3616 const value = r.readUtf8(r.end - r.pos)
3617 dictAdd(slot, value)
3618 return value
3619}
3620
3621function encodeInterned(table: InternWrite, ctx: EncodeContext, layout: Layout, value: unknown, w: Writer) {
3622 if (!table.disabled) {
3623 const ref = internRef(table, value)
3624 if (ref !== undefined) {
3625 w.uvarint(ref * 2 + 1)
3626 return
3627 }
3628 internAdd(table, value)
3629 }
3630 const mark = w.beginSized()
3631 encodeValue(ctx, layout, value, w)
3632 w.endSizedRun(mark)
3633}
3634
3635function encodeStructRun(ctx: EncodeContext, plan: RunPlan, arr: ReadonlyArray<unknown>, count: number, w: Writer) {
3636 const shapes: Array<number> = []
3637 const tables: Array<InternWrite | undefined> = []
3638 for (let i = 0; i < count; i++) {
3639 issuePath[issuePathLen++] = i
3640 const mark = w.beginSized()
3641 encodeRunRow(ctx, plan, shapes, tables, arr[i], w)
3642 w.endSized(mark)
3643 issuePathLen--
3644 }
3645}
3646
3647function encodeRunRow(
3648 ctx: EncodeContext,
3649 plan: RunPlan,
3650 shapes: Array<number>,
3651 tables: Array<InternWrite | undefined>,
3652 value: unknown,
3653 w: Writer
3654) {
3655 const struct = plan.struct
3656 const obj = value as Record<string, unknown>
3657 const fields = struct.fields
3658 let pairs: Array<ExtraPair> | undefined
3659 if (struct.extra.length > 0) {
3660 const found = extraPairs(ctx, struct, obj)
3661 if (found.length > 0) pairs = found
3662 }
3663 const wide = !plan.shapes
3664 let mask = pairs === undefined ? 0 : RUN_EXTRA_BIT
3665 const presenceMask = runPresenceMask(plan, obj)
3666 if (presenceMask !== undefined) {
3667 mask = presenceMask
3668 } else {
3669 for (let i = 0; i < fields.length; i++) {
3670 const field = fields[i]
3671 if (Object.hasOwn(obj, field.name)) {
3672 if (!wide) mask |= 1 << i
3673 } else if (!field.optional) {
3674 issuePath[issuePathLen++] = field.name
3675 throw issueError(new SchemaIssue.MissingKey(field.annotations))
3676 }
3677 }
3678 }
3679 let shape = -1
3680 for (let i = 0; i < shapes.length; i++) {
3681 if (shapes[i] === mask) {
3682 shape = i
3683 break
3684 }
3685 }
3686 if (shape >= 0) {
3687 w.uvarint(shape + 1)
3688 } else {
3689 w.uvarint(0)
3690 if (!wide) shapes.push(mask)
3691 }
3692 const declare = shape < 0
3693 // Fingerprint mode proves both layouts match, so a declared shape is the
3694 // presence mask instead of a field id list.
3695 const positionalShape = ctx.positional && !wide
3696 if (declare && positionalShape) w.uvarint(mask)
3697 if (pairs !== undefined) {
3698 if (declare && !positionalShape) w.uvarint(0)
3699 const mark = w.beginSized()
3700 encodeExtraPairs(ctx, pairs, obj, w)
3701 w.endSizedRun(mark)
3702 }
3703 for (let i = 0; i < fields.length; i++) {
3704 const field = fields[i]
3705 if (wide ? !Object.hasOwn(obj, field.name) : (mask & (1 << i)) === 0) continue
3706 if (declare && !positionalShape) w.uvarint(field.id)
3707 issuePath[issuePathLen++] = field.name
3708 const kind = plan.intern[i]
3709 if (kind === INTERN_SELF) {
3710 encodeInterned(internWrite(tables, i), ctx, field.layout, obj[field.name], w)
3711 } else {
3712 if (kind !== INTERN_NONE) ctx.intern = internWrite(tables, i)
3713 const mark = w.beginSized()
3714 encodeValue(ctx, field.layout, obj[field.name], w)
3715 w.endSizedRun(mark)
3716 ctx.intern = undefined
3717 }
3718 issuePathLen--
3719 }
3720}
3721
3722function arraySlot(layout: ArrayLayout, index: number, count: number): Layout {
3723 const elementLen = layout.elements.length
3724 if (index < elementLen) return layout.elements[index].layout
3725 const tailLen = Math.max(0, layout.rest.length - 1)
3726 const tailThreshold = Math.max(elementLen, count - tailLen)
3727 return index >= tailThreshold ? layout.rest[index - tailThreshold + 1] : layout.rest[0]
3728}
3729
3730function encodeArray(ctx: EncodeContext, layout: ArrayLayout, arr: ReadonlyArray<unknown>, w: Writer) {
3731 const count = arr.length
3732 if (layout.rest.length === 0 && count > layout.elements.length) {
3733 issuePath[issuePathLen++] = layout.elements.length
3734 throw issueError(
3735 new SchemaIssue.UnexpectedKey(layout.ast, arr[layout.elements.length], ctx.options)
3736 )
3737 }
3738 if (count < layout.minCount) {
3739 issuePath[issuePathLen++] = count
3740 throw issueError(new SchemaIssue.MissingKey(undefined))
3741 }
3742 if (layout.hasCount) w.uvarint(count)
3743 if (count > 0) {
3744 const plan = runPlan(layout)
3745 if (plan !== null) {
3746 encodeStructRun(ctx, plan, arr, count, w)
3747 return
3748 }
3749 }
3750 const uniform = layout.uniform
3751 if (uniform !== undefined) {
3752 if (layout.uniformNumbers) {
3753 encodeNumberRun(ctx, arr, count, w)
3754 return
3755 }
3756 // Only a run field holding an array of strings hands over an intern table.
3757 const table = ctx.intern
3758 ctx.intern = undefined
3759 if (table !== undefined) {
3760 for (let i = 0; i < count; i++) {
3761 issuePath[issuePathLen++] = i
3762 encodeInterned(table, ctx, uniform, arr[i], w)
3763 issuePathLen--
3764 }
3765 return
3766 }
3767 const inline = layout.uniformInline
3768 for (let i = 0; i < count; i++) {
3769 issuePath[issuePathLen++] = i
3770 if (inline) encodeValue(ctx, uniform, arr[i], w)
3771 else encodeSized(ctx, uniform, arr[i], w)
3772 issuePathLen--
3773 }
3774 return
3775 }
3776 for (let i = 0; i < count; i++) {
3777 const slot = arraySlot(layout, i, count)
3778 issuePath[issuePathLen++] = i
3779 if (isInlineSlot(slot)) {
3780 encodeValue(ctx, slot, arr[i], w)
3781 } else {
3782 encodeSized(ctx, slot, arr[i], w)
3783 }
3784 issuePathLen--
3785 }
3786}
3787
3788// Uniform number arrays share one mode byte: varint, packed decimal, or f64.
3789// Scales found by the classification pass, reused by the decimal write pass so
3790// each non-integer scans `decimalScale` once. Only written and read within one
3791// synchronous `encodeNumberRun` call.
3792const numberRunScales: Array<number> = []
3793
3794function encodeNumberRun(ctx: EncodeContext, arr: ReadonlyArray<unknown>, count: number, w: Writer) {
3795 let varint = true
3796 let decimal = true
3797 for (let i = 0; i < count; i++) {
3798 const value = arr[i]
3799 if (typeof value !== "number") {
3800 issuePath[issuePathLen++] = i
3801 encodeFail("a number", value, ctx.options)
3802 }
3803 if (isVarintNumber(value)) {
3804 const magnitude = value < 0 ? -value : value
3805 if (magnitude > DECIMAL_MANTISSA_MAX) decimal = false
3806 } else {
3807 varint = false
3808 const scale = decimalScale(value)
3809 if (scale === 0) decimal = false
3810 else numberRunScales[i] = scale
3811 }
3812 if (!varint && !decimal) break
3813 }
3814 if (varint) {
3815 w.byte(NUMBER_RUN_VARINT)
3816 for (let i = 0; i < count; i++) w.numberVarint(arr[i] as number)
3817 } else if (decimal) {
3818 w.byte(NUMBER_RUN_DECIMAL)
3819 for (let i = 0; i < count; i++) {
3820 const value = arr[i] as number
3821 // Integers take scale 0 even when a scale would fit, matching the
3822 // classification pass writing scales only for non-integers.
3823 const scale = Number.isInteger(value) ? 0 : numberRunScales[i]
3824 const mantissa = scale === 0 ? value : Math.round(value * POW10[scale])
3825 const negative = mantissa < 0 || (mantissa === 0 && 1 / mantissa < 0)
3826 const magnitude = negative ? -mantissa : mantissa
3827 w.uvarint((magnitude * 2 + (negative ? 1 : 0)) * 16 + scale)
3828 }
3829 } else {
3830 w.byte(NUMBER_RUN_F64)
3831 for (let i = 0; i < count; i++) w.f64(arr[i] as number)
3832 }
3833}
3834
3835function findDiscriminator(variants: ReadonlyArray<VariantRow>): Discriminator | undefined {
3836 if (variants.length < 2 || variants.some((variant) => variant.tuple)) return undefined
3837 for (const candidate of variants[0].sentinels) {
3838 const rows = new Map<unknown, VariantRow>()
3839 for (const variant of variants) {
3840 const sentinel = variant.sentinels.find((s) => s.key === candidate.key)
3841 // `NaN` matches under `Map`'s key equality but not under `===`.
3842 if (sentinel === undefined || rows.has(sentinel.literal) || Number.isNaN(sentinel.literal)) break
3843 rows.set(sentinel.literal, variant)
3844 }
3845 if (rows.size === variants.length) return { key: candidate.key, rows }
3846 }
3847 return undefined
3848}
3849
3850// An inherited sentinel does not select a variant, matching the schema pass.
3851function hasSentinels(variant: VariantRow, value: Record<PropertyKey, unknown>): boolean {
3852 const sentinels = variant.sentinels
3853 for (let i = 0; i < sentinels.length; i++) {
3854 const { key, literal } = sentinels[i]
3855 if (value[key] !== literal || !Object.hasOwn(value, key)) return false
3856 }
3857 return true
3858}
3859
3860function matchesVariant(variant: VariantRow, value: unknown): boolean {
3861 return variant.tuple
3862 ? Array.isArray(value) && hasSentinels(variant, value as unknown as Record<PropertyKey, unknown>)
3863 : Predicate.isObject(value) && !Array.isArray(value) &&
3864 hasSentinels(variant, value as Record<PropertyKey, unknown>)
3865}
3866
3867function encodeVariant(ctx: EncodeContext, variant: VariantRow, value: unknown, w: Writer) {
3868 if (ctx.positional) {
3869 w.uvarint(variant.position)
3870 } else {
3871 w.byte(K.variant)
3872 w.u32le(variant.tag)
3873 }
3874 encodeValue(ctx, variant.payload, value, w)
3875}
3876
3877function encodeUnion(ctx: EncodeContext, layout: UnionLayout, value: unknown, w: Writer) {
3878 const discriminator = layout.discriminator
3879 if (discriminator !== undefined) {
3880 // A miss rules out every variant, because each one pins this key to a
3881 // literal no other variant uses.
3882 if (Predicate.isObject(value) && !Array.isArray(value)) {
3883 const record = value as Record<PropertyKey, unknown>
3884 const variant = discriminator.rows.get(record[discriminator.key])
3885 if (variant !== undefined && hasSentinels(variant, record)) {
3886 encodeVariant(ctx, variant, value, w)
3887 return
3888 }
3889 }
3890 } else {
3891 for (const variant of layout.variants) {
3892 if (matchesVariant(variant, value)) {
3893 encodeVariant(ctx, variant, value, w)
3894 return
3895 }
3896 }
3897 }
3898 for (const member of layout.others) {
3899 if (matchesLayout(member.layout, value)) {
3900 if (ctx.positional) w.uvarint(member.position)
3901 else w.byte(member.kind)
3902 encodeValue(ctx, member.layout, value, w)
3903 return
3904 }
3905 }
3906 throw issueError(new SchemaIssue.InvalidType(layout.ast, value, ctx.options))
3907}
3908
3909function encodeValue(ctx: EncodeContext, layout: Layout, value: unknown, w: Writer): void {
3910 switch (layout._) {
3911 case "literal":
3912 if (!hasLiteral(layout, value)) encodeFail(literalExpected(layout), value, ctx.options)
3913 encodeValue(ctx, layout.leaf, value, w)
3914 return
3915 case "bool":
3916 if (typeof value !== "boolean") encodeFail("a boolean", value, ctx.options)
3917 w.byte(value === true ? 1 : 0)
3918 return
3919 case "null":
3920 if (value !== null) encodeFail("null", value, ctx.options)
3921 return
3922 case "undefined":
3923 if (value !== undefined) encodeFail("undefined", value, ctx.options)
3924 return
3925 case "number": {
3926 if (typeof value !== "number") encodeFail("a number", value, ctx.options)
3927 if (isVarintNumber(value)) {
3928 w.numberVarint(value)
3929 return
3930 }
3931 const scale = decimalScale(value)
3932 if (scale > 0) {
3933 w.numberVarint(Math.round(value * POW10[scale]))
3934 w.byte(scale)
3935 } else {
3936 w.f64(value)
3937 }
3938 return
3939 }
3940 case "int": {
3941 // `disableChecks` can bypass the integer check used to select this layout.
3942 if (!Number.isSafeInteger(value)) encodeFail("an integer", value, ctx.options)
3943 w.numberVarint(value as number)
3944 return
3945 }
3946 case "string": {
3947 if (typeof value !== "string") encodeFail("a string", value, ctx.options)
3948 const dict = ctx.dict
3949 if (dict === undefined) w.string(value)
3950 else encodeString(dict, layout, value, w)
3951 return
3952 }
3953 case "symbol":
3954 if (typeof value !== "symbol") encodeFail("a symbol", value, ctx.options)
3955 encodeSymbol(ctx, value, w)
3956 return
3957 case "bytes":
3958 if (!(value instanceof Uint8Array)) encodeFail("a Uint8Array", value, ctx.options)
3959 w.bytes(value)
3960 return
3961 case "bigint":
3962 if (typeof value !== "bigint") encodeFail("a bigint", value, ctx.options)
3963 w.zigzag(value)
3964 return
3965 case "int64": {
3966 if (!matchesLayout(layout, value)) {
3967 encodeFail(layout.flavor === "date" ? "a Date" : "a DateTime.Utc", value, ctx.options)
3968 }
3969 const millis = layout.flavor === "date" ? (value as Date).getTime() : (value as DateTime.Utc).epochMilliseconds
3970 if (Number.isNaN(millis)) encodeFail("a valid Date", value, ctx.options)
3971 w.i64(BigInt(millis))
3972 return
3973 }
3974 case "dateTimeZoned": {
3975 if (!matchesLayout(layout, value)) encodeFail("a DateTime.Zoned", value, ctx.options)
3976 const zoned = value as DateTime.Zoned
3977 w.i64(BigInt(zoned.epochMilliseconds))
3978 if (zoned.zone._tag === "Offset") {
3979 w.byte(0)
3980 w.i32le(zoned.zone.offset)
3981 } else {
3982 w.byte(1)
3983 w.string(zoned.zone.id)
3984 }
3985 return
3986 }
3987 case "duration": {
3988 if (!Duration.isDuration(value)) encodeFail("a Duration", value, ctx.options)
3989 const duration = value.value
3990 switch (duration._tag) {
3991 case "Infinity":
3992 w.byte(1)
3993 return
3994 case "NegativeInfinity":
3995 w.byte(2)
3996 return
3997 case "Nanos":
3998 w.byte(0)
3999 w.zigzag(duration.nanos)
4000 return
4001 case "Millis":
4002 w.byte(0)
4003 w.zigzag(BigInt(duration.millis) * BIGINT_NANOS_PER_MILLI)
4004 return
4005 }
4006 }
4007 case "bigDecimal": {
4008 if (!BigDecimal.isBigDecimal(value)) encodeFail("a BigDecimal", value, ctx.options)
4009 const normalized = BigDecimal.normalize(value)
4010 w.zigzag(normalized.value)
4011 w.zigzag(BigInt(normalized.scale))
4012 return
4013 }
4014 case "json": {
4015 let text: string | undefined
4016 try {
4017 text = JSON.stringify(value)
4018 } catch {
4019 text = undefined
4020 }
4021 if (text === undefined) {
4022 if (isCyclic(value)) encodeFail("acyclic value", value, ctx.options)
4023 encodeFail("a JSON-serializable value", value, ctx.options)
4024 }
4025 w.string(text)
4026 return
4027 }
4028 case "option": {
4029 if (!Option.isOption(value)) encodeFail("an Option", value, ctx.options)
4030 const option = value
4031 if (option._tag === "None") {
4032 w.byte(0)
4033 } else {
4034 w.byte(1)
4035 encodeValue(ctx, layout.value, option.value, w)
4036 }
4037 return
4038 }
4039 case "result": {
4040 if (!Result.isResult(value)) encodeFail("a Result", value, ctx.options)
4041 const result = value
4042 if (result._tag === "Success") {
4043 w.byte(0)
4044 encodeValue(ctx, layout.success, result.success, w)
4045 } else {
4046 w.byte(1)
4047 encodeValue(ctx, layout.failure, result.failure, w)
4048 }
4049 return
4050 }
4051 case "exit": {
4052 if (!Exit.isExit(value)) encodeFail("an Exit", value, ctx.options)
4053 const exit = value
4054 if (exit._tag === "Success") {
4055 w.byte(0)
4056 encodeValue(ctx, layout.value, exit.value, w)
4057 } else {
4058 w.byte(1)
4059 encodeValue(ctx, layout.cause, exit.cause, w)
4060 }
4061 return
4062 }
4063 case "cause": {
4064 if (!Cause.isCause(value)) encodeFail("a Cause", value, ctx.options)
4065 const reasons = value.reasons
4066 w.uvarint(reasons.length)
4067 for (const reason of reasons) {
4068 const mark = w.beginSized()
4069 encodeReason(ctx, layout, reason, w)
4070 w.endSized(mark)
4071 }
4072 return
4073 }
4074 case "causeReason": {
4075 if (!Cause.isReason(value)) encodeFail("a Cause.Reason", value, ctx.options)
4076 encodeReason(ctx, layout, value, w)
4077 return
4078 }
4079 case "struct": {
4080 if (!Predicate.isObject(value) || Array.isArray(value)) encodeFail("an object", value, ctx.options)
4081 if (ctx.positional) encodeStructPositional(ctx, layout, value, w)
4082 else encodeStructFields(ctx, layout, value, w)
4083 return
4084 }
4085 case "array": {
4086 if (!Array.isArray(value)) encodeFail("an array", value, ctx.options)
4087 encodeArray(ctx, layout, value, w)
4088 return
4089 }
4090 case "union":
4091 encodeUnion(ctx, layout, value, w)
4092 return
4093 case "never":
4094 throw issueError(new SchemaIssue.InvalidType(layout.ast, value, ctx.options))
4095 }
4096}
4097
4098// Reuse one top-level writer while allowing nested codecs to allocate their own.
4099let pooledWriter: Writer | undefined = new Writer()
4100
4101function writeFrame(ctx: EncodeContext, layout: Layout, value: unknown, mode: Mode, w: Writer) {
4102 const mark = w.beginSized()
4103 w.byte(mode.envelope)
4104 if (mode.positional) w.bytes(mode.fingerprint)
4105 encodeValue(ctx, layout, value, w)
4106 w.endSized(mark)
4107}
4108
4109// One context and one writer across a batch, so the caches built for the
4110// first frame serve the rest and the frames need no second pass to join. The
4111// single-value and batch entry points share this wrapper: the pooled-writer
4112// protocol (checkout, reset, abort, restore) must not drift between them.
4113function runFrames(
4114 layout: Layout,
4115 value: unknown,
4116 values: ReadonlyArray<unknown> | undefined,
4117 options: SchemaAST.ParseOptions,
4118 mode: Mode,
4119 dict: DictWrite | undefined
4120): Uint8Array<ArrayBuffer> {
4121 const ctx: EncodeContext = {
4122 options,
4123 positional: mode.positional,
4124 indexSignatures: undefined,
4125 extraShape: undefined,
4126 intern: undefined,
4127 dict
4128 }
4129 const w = pooledWriter ?? new Writer()
4130 const pooled = w === pooledWriter
4131 if (pooled) pooledWriter = undefined
4132 w.reset()
4133 const savedPathLen = issuePathLen
4134 // A batch is one output: a failure anywhere discards every frame in it, so
4135 // the dictionary commits or rolls back once for the whole call.
4136 let sent = false
4137 try {
4138 if (values === undefined) {
4139 issuePathLen = 0
4140 writeFrame(ctx, layout, value, mode, w)
4141 } else {
4142 for (let i = 0; i < values.length; i++) {
4143 issuePathLen = 0
4144 writeFrame(ctx, layout, values[i], mode, w)
4145 }
4146 }
4147 const out = w.out()
4148 sent = true
4149 return out
4150 } finally {
4151 if (dict !== undefined) {
4152 if (sent) dictCommit(dict)
4153 else dictRollback(dict)
4154 }
4155 w.abort()
4156 issuePathLen = savedPathLen
4157 if (pooled) pooledWriter = w
4158 }
4159}
4160
4161function encodeFrames(
4162 layout: Layout,
4163 values: ReadonlyArray<unknown>,
4164 options: SchemaAST.ParseOptions,
4165 mode: Mode,
4166 dict?: DictWrite | undefined
4167): Uint8Array<ArrayBuffer> {
4168 return runFrames(layout, undefined, values, options, mode, dict)
4169}
4170
4171function encodeFrame(
4172 layout: Layout,
4173 value: unknown,
4174 options: SchemaAST.ParseOptions,
4175 mode: Mode,
4176 dict?: DictWrite | undefined
4177): Uint8Array<ArrayBuffer> {
4178 return runFrames(layout, value, undefined, options, mode, dict)
4179}
4180
4181// The direct paths bypass the schema pass for values the binary layer fully
4182// validates on its own; success exits are that case for `exitSuccess` targets.
4183function isSuccessExit(value: unknown): boolean {
4184 return Exit.isExit(value) && Exit.isSuccess(value)
4185}
4186
4187// Unknown union members resolve to this sentinel.
4188const ABSENT = globalThis.Symbol.for("~effect/encoding/SchemaBinary/absent")
4189
4190function decodeChecked(layout: Layout, r: Reader): unknown {
4191 const value = decodeValue(layout, r)
4192 if (value !== ABSENT && r.pos !== r.end) invalid("no leftover bytes", undefined, r.options)
4193 return value
4194}
4195
4196function decodeSized(layout: Layout, r: Reader): unknown {
4197 const saved = r.enter(r.uvarint())
4198 const value = decodeChecked(layout, r)
4199 r.exit(saved)
4200 return value
4201}
4202
4203function decodeSlot(layout: Layout, r: Reader): unknown {
4204 if (isSelfDelimiting(layout)) return decodeValue(layout, r)
4205 const size = packedSize(layout)
4206 const saved = r.enter(
4207 size !== undefined ? size : layout._ === "null" || layout._ === "undefined" ? 0 : r.uvarint()
4208 )
4209 const value = decodeChecked(layout, r)
4210 r.exit(saved)
4211 return value
4212}
4213
4214// Records are usually small, so duplicate keys scan a list and only wide
4215// records spill into a set.
4216const SEEN_LIST_MAX = 8
4217
4218interface SeenKeys {
4219 readonly list: Array<string>
4220 set: Set<string> | undefined
4221}
4222
4223function seenKey(seen: SeenKeys, key: string): boolean {
4224 const set = seen.set
4225 if (set !== undefined) {
4226 if (set.has(key)) return true
4227 set.add(key)
4228 return false
4229 }
4230 const list = seen.list
4231 if (list.includes(key)) return true
4232 if (list.length >= SEEN_LIST_MAX) {
4233 const set = seen.set = new Set<string>()
4234 for (let i = 0; i < list.length; i++) set.add(list[i])
4235 set.add(key)
4236 } else {
4237 list.push(key)
4238 }
4239 return false
4240}
4241
4242function decodeExtraPair(
4243 layout: StructLayout,
4244 r: Reader,
4245 out: Record<string, unknown>,
4246 seen: SeenKeys,
4247 keys?: Array<unknown> | undefined
4248) {
4249 const code = r.uvarint()
4250 const keyCode = keys === undefined ? Math.floor(code / FIELD_WIRE_FACTOR) : Math.floor(code / 16)
4251 const kind = keys === undefined ? code - keyCode * FIELD_WIRE_FACTOR : Math.floor((code - keyCode * 16) / 2)
4252 const key = keys === undefined
4253 ? r.readUtf8(keyCode)
4254 : decodeExtraKey(keys, code, keyCode, r)
4255 if (layout.names.has(key)) invalid("an extra key distinct from declared fields", key, r.options)
4256 if (seenKey(seen, key)) invalid("unique extra keys", undefined, r.options)
4257 const signature = layout.extraAll ??
4258 (r.indexSignatures ??= new IndexSignatureCache(r.options)).find(layout, key)
4259 if (signature === undefined) {
4260 skipFieldPayload(kind, r)
4261 return
4262 }
4263 issuePath[issuePathLen++] = key
4264 const value = decodeFieldPayload(signature.layout, signature.wireMask, kind, r)
4265 issuePathLen--
4266 if (value !== ABSENT) assignProperty(out, key, value)
4267}
4268
4269function decodeExtraKey(keys: Array<unknown>, code: number, keyCode: number, r: Reader): string {
4270 if ((code & 1) === 1) {
4271 if (keyCode >= keys.length) invalid("a known back-reference", undefined, r.options)
4272 return keys[keyCode] as string
4273 }
4274 const key = r.readUtf8(keyCode)
4275 keys.push(key)
4276 return key
4277}
4278
4279function missingKeyIssue(field: Field): SchemaIssue.Issue {
4280 return new SchemaIssue.Pointer([field.name], new SchemaIssue.MissingKey(field.annotations))
4281}
4282
4283function throwMissingKeys(layout: StructLayout, issues: Array<SchemaIssue.Issue>): never {
4284 throw issueError(
4285 new SchemaIssue.Composite(layout.ast, issues as [SchemaIssue.Issue, ...Array<SchemaIssue.Issue>])
4286 )
4287}
4288
4289function skipFieldVarint(r: Reader) {
4290 while ((r.byte() & 0x80) !== 0) {
4291 // Scan without accumulating an attacker-controlled bigint.
4292 }
4293}
4294
4295function skipFieldPayload(kind: number, r: Reader) {
4296 switch (kind) {
4297 case FIELD_WIRE_SIZED:
4298 r.take(r.uvarint())
4299 return
4300 case FIELD_WIRE_VARINT:
4301 skipFieldVarint(r)
4302 return
4303 case FIELD_WIRE_FIXED64:
4304 r.take(8)
4305 return
4306 case FIELD_WIRE_FALSE:
4307 case FIELD_WIRE_TRUE:
4308 case FIELD_WIRE_EMPTY:
4309 return
4310 case FIELD_WIRE_DECIMAL:
4311 skipFieldVarint(r)
4312 skipFieldVarint(r)
4313 return
4314 case FIELD_WIRE_FIXED32:
4315 r.take(4)
4316 return
4317 }
4318}
4319
4320function fieldWireMask(layout: Layout): number {
4321 if (layout._ === "literal") return fieldWireMask(layout.leaf)
4322 switch (layout._) {
4323 case "bool":
4324 return (1 << FIELD_WIRE_FALSE) | (1 << FIELD_WIRE_TRUE)
4325 case "number":
4326 return (1 << FIELD_WIRE_VARINT) | (1 << FIELD_WIRE_FIXED64) | (1 << FIELD_WIRE_DECIMAL)
4327 case "int":
4328 return 1 << FIELD_WIRE_VARINT
4329 default:
4330 return 1 << FIELD_WIRE_SIZED
4331 }
4332}
4333
4334function decodeScalarField(kind: number, r: Reader): unknown {
4335 switch (kind) {
4336 case FIELD_WIRE_FALSE:
4337 return false
4338 case FIELD_WIRE_TRUE:
4339 return true
4340 case FIELD_WIRE_VARINT:
4341 return r.numberVarint()
4342 case FIELD_WIRE_FIXED64:
4343 return r.f64()
4344 case FIELD_WIRE_DECIMAL: {
4345 const mantissa = r.numberVarint()
4346 const scale = r.uvarint()
4347 if (scale === 0 || scale > DECIMAL_SCALE_MAX) invalid("a decimal scale in [1, 8]", scale, r.options)
4348 return mantissa / POW10[scale]
4349 }
4350 }
4351 return invalid("matching field wire kind", undefined, r.options)
4352}
4353
4354function decodeFieldPayload(layout: Layout, wireMask: number, kind: number, r: Reader): unknown {
4355 if ((wireMask & (1 << kind)) === 0) {
4356 skipFieldPayload(kind, r)
4357 return ABSENT
4358 }
4359 if (kind !== FIELD_WIRE_SIZED) {
4360 const value = decodeScalarField(kind, r)
4361 if (layout._ === "literal" && !hasLiteral(layout, value)) {
4362 invalid(literalExpected(layout), value, r.options)
4363 }
4364 return value
4365 }
4366 const saved = r.enter(r.uvarint())
4367 const value = decodeChecked(layout, r)
4368 r.exit(saved)
4369 return value
4370}
4371
4372function decodeStruct(layout: StructLayout, r: Reader): unknown {
4373 // Only a run field with index signatures hands over an intern table.
4374 let keys: Array<unknown> | undefined
4375 if (layout.extra.length > 0) {
4376 keys = r.intern
4377 r.intern = undefined
4378 }
4379 const out: Record<string, unknown> = {}
4380 // Use bitmasks for common structs and allocate sets only when needed.
4381 let seenMask = 0
4382 let presentMask = 0
4383 let seenWide: Set<number> | undefined
4384 let presentWide: Set<number> | undefined
4385 let seenUnknown: Set<number> | undefined
4386 let seenExtra = false
4387 // Fast-path fields in encoder order and fall back to the id map.
4388 const fields = layout.fields
4389 let cursor = 0
4390 while (r.pos < r.end) {
4391 const tag = r.fieldTag()
4392 const id = Math.floor(tag / FIELD_WIRE_FACTOR)
4393 const kind = tag - id * FIELD_WIRE_FACTOR
4394 if (id === 0) {
4395 if (seenExtra) invalid("unique field ids", undefined, r.options)
4396 seenExtra = true
4397 if (kind !== FIELD_WIRE_SIZED) invalid("extra field map", undefined, r.options)
4398 const saved = r.enter(r.uvarint())
4399 decodeExtraPairs(layout, r, out, keys)
4400 r.exit(saved)
4401 continue
4402 }
4403 let field: Field | undefined
4404 if (cursor < fields.length && fields[cursor].id === id) {
4405 field = fields[cursor]
4406 cursor++
4407 } else {
4408 field = layout.byId.get(id)
4409 if (field !== undefined) cursor = field.index + 1
4410 }
4411 if (field === undefined) {
4412 if (seenUnknown === undefined) seenUnknown = new Set()
4413 else if (seenUnknown.has(id)) invalid("unique field ids", undefined, r.options)
4414 seenUnknown.add(id)
4415 skipFieldPayload(kind, r)
4416 continue
4417 }
4418 const index = field.index
4419 if (index < 32) {
4420 const bit = 1 << index
4421 if ((seenMask & bit) !== 0) invalid("unique field ids", undefined, r.options)
4422 seenMask |= bit
4423 } else if (seenWide === undefined) {
4424 seenWide = new Set([index])
4425 } else {
4426 if (seenWide.has(index)) invalid("unique field ids", undefined, r.options)
4427 seenWide.add(index)
4428 }
4429 issuePath[issuePathLen++] = field.name
4430 const value = decodeFieldPayload(field.layout, field.wireMask, kind, r)
4431 issuePathLen--
4432 if (value !== ABSENT) {
4433 assignProperty(out, field.name, value)
4434 if (index < 32) presentMask |= 1 << index
4435 else (presentWide ??= new Set()).add(index)
4436 }
4437 }
4438 checkRequiredFields(layout, presentMask, presentWide, r)
4439 return out
4440}
4441
4442// Fails when a required field is absent, honoring `errors: "all"`.
4443function checkRequiredFields(
4444 struct: StructLayout,
4445 presentMask: number,
4446 presentWide: Set<number> | undefined,
4447 r: Reader
4448): void {
4449 if ((presentMask & struct.requiredMask) === struct.requiredMask && !struct.requiredWide) return
4450 const fields = struct.fields
4451 let issues: Array<SchemaIssue.Issue> | undefined
4452 for (let i = 0; i < fields.length; i++) {
4453 const field = fields[i]
4454 const index = field.index
4455 const present = index < 32 ? (presentMask & (1 << index)) !== 0 : presentWide?.has(index) === true
4456 if (!field.optional && !present) {
4457 ;(issues ??= []).push(missingKeyIssue(field))
4458 if (r.options.errors !== "all") break
4459 }
4460 }
4461 if (issues !== undefined) throwMissingKeys(struct, issues)
4462}
4463
4464function decodeStructPositional(layout: StructLayout, r: Reader): unknown {
4465 // Only a run field with index signatures hands over an intern table. Extras
4466 // decode after the fields here, so claim it before a nested field can.
4467 let keys: Array<unknown> | undefined
4468 if (layout.extra.length > 0) {
4469 keys = r.intern
4470 r.intern = undefined
4471 }
4472 const out: Record<string, unknown> = {}
4473 const bitmapBytes = (layout.optionalCount + 7) >> 3
4474 let bitmap = 0
4475 if (bitmapBytes > 0) {
4476 if (r.pos + bitmapBytes > r.end) invalid("complete value", undefined, r.options)
4477 bitmap = r.pos
4478 r.pos += bitmapBytes
4479 }
4480 const buf = r.buf
4481 const fields = layout.fields
4482 let optionalIndex = 0
4483 let issues: Array<SchemaIssue.Issue> | undefined
4484 for (let i = 0; i < fields.length; i++) {
4485 const field = fields[i]
4486 if (field.optional) {
4487 const present = (buf[bitmap + (optionalIndex >> 3)] & (1 << (optionalIndex & 7))) !== 0
4488 optionalIndex++
4489 if (!present) continue
4490 }
4491 issuePath[issuePathLen++] = field.name
4492 const value = field.inline ? decodeSlot(field.layout, r) : decodeSized(field.layout, r)
4493 issuePathLen--
4494 if (value !== ABSENT) assignProperty(out, field.name, value)
4495 else if (!field.optional) {
4496 ;(issues ??= []).push(missingKeyIssue(field))
4497 if (r.options.errors !== "all") break
4498 }
4499 }
4500 if (issues !== undefined) throwMissingKeys(layout, issues)
4501 if (layout.extra.length > 0) {
4502 const count = r.uvarint()
4503 if (count > r.remaining) invalid("complete value", undefined, r.options)
4504 if (count > 0) {
4505 const seen: SeenKeys = { list: [], set: undefined }
4506 for (let i = 0; i < count; i++) decodeExtraPair(layout, r, out, seen, keys)
4507 }
4508 }
4509 return out
4510}
4511
4512function decodeExtraPairs(
4513 layout: StructLayout,
4514 r: Reader,
4515 out: Record<string, unknown>,
4516 keys?: Array<unknown> | undefined
4517) {
4518 const seen: SeenKeys = { list: [], set: undefined }
4519 while (r.pos < r.end) decodeExtraPair(layout, r, out, seen, keys)
4520}
4521
4522// A row shape is the writer's tag order: a known field, `null` for the extra
4523// block, or `undefined` for a field this reader does not have.
4524// A future shape version can retain scalar wire kinds here so reused rows can
4525// omit their per-value extents too.
4526type RunSlot = Field | null | undefined
4527
4528function decodeInterned(table: Array<unknown>, layout: Layout, r: Reader): unknown {
4529 const code = r.uvarint()
4530 if ((code & 1) === 1) {
4531 const ref = (code - 1) / 2
4532 if (ref >= table.length) invalid("a known back-reference", undefined, r.options)
4533 return table[ref]
4534 }
4535 // Plain strings consume their whole region, so they skip the reader window.
4536 if (layout._ === "string") {
4537 if (r.dict !== undefined) {
4538 const saved = r.enter(code / 2)
4539 const value = decodeChecked(layout, r)
4540 r.exit(saved)
4541 table.push(value)
4542 return value
4543 }
4544 const value = r.readUtf8(code / 2)
4545 table.push(value)
4546 return value
4547 }
4548 const saved = r.enter(code / 2)
4549 const value = decodeChecked(layout, r)
4550 r.exit(saved)
4551 table.push(value)
4552 return value
4553}
4554
4555function decodeStructRun(plan: RunPlan, count: number, r: Reader): Array<unknown> {
4556 const out: Array<unknown> = new Array(count)
4557 const shapes: Array<Array<RunSlot>> = []
4558 const tables: Array<Array<unknown> | undefined> = []
4559 for (let i = 0; i < count; i++) {
4560 issuePath[issuePathLen++] = i
4561 const saved = r.enter(r.uvarint())
4562 out[i] = decodeRunRow(plan, shapes, tables, r)
4563 r.exit(saved)
4564 issuePathLen--
4565 }
4566 return out
4567}
4568
4569// A positional shape is the presence mask over layout field order.
4570function positionalRunShape(struct: StructLayout, r: Reader): Array<RunSlot> {
4571 const mask = r.uvarint()
4572 const fields = struct.fields
4573 const fieldBits = mask & ~RUN_EXTRA_BIT
4574 if (mask > RUN_EXTRA_BIT * 2 - 1 || (fieldBits >>> fields.length) !== 0) {
4575 invalid("a known row shape", undefined, r.options)
4576 }
4577 const shape: Array<RunSlot> = []
4578 if ((mask & RUN_EXTRA_BIT) !== 0) shape.push(null)
4579 for (let i = 0; i < fields.length; i++) {
4580 if ((fieldBits & (1 << i)) !== 0) shape.push(fields[i])
4581 }
4582 return shape
4583}
4584
4585function decodeRunRow(
4586 plan: RunPlan,
4587 shapes: Array<Array<RunSlot>>,
4588 tables: Array<Array<unknown> | undefined>,
4589 r: Reader
4590): unknown {
4591 const struct = plan.struct
4592 const out: Record<string, unknown> = {}
4593 let presentMask = 0
4594 let presentWide: Set<number> | undefined
4595 const code = r.uvarint()
4596 let shape: Array<RunSlot> | undefined
4597 if (code === 0) {
4598 if (r.positional && plan.shapes) {
4599 shape = positionalRunShape(struct, r)
4600 shapes.push(shape)
4601 }
4602 } else {
4603 shape = shapes[code - 1]
4604 if (shape === undefined) invalid("a known row shape", undefined, r.options)
4605 }
4606 if (shape !== undefined) {
4607 for (let i = 0; i < shape.length; i++) {
4608 const filled = decodeRunSlot(plan, tables, shape[i], out, r)
4609 if (filled >= 0) {
4610 if (filled < 32) presentMask |= 1 << filled
4611 else (presentWide ??= new Set()).add(filled)
4612 }
4613 }
4614 if (r.pos !== r.end) invalid("no leftover bytes", undefined, r.options)
4615 } else {
4616 const declared: Array<RunSlot> | undefined = plan.shapes ? [] : undefined
4617 let seenMask = 0
4618 let seenWide: Set<number> | undefined
4619 let seenIds: Set<number> | undefined
4620 let seenExtra = false
4621 while (r.pos < r.end) {
4622 const id = r.uvarint()
4623 let slot: RunSlot
4624 if (id === 0) {
4625 if (seenExtra) invalid("unique field ids", undefined, r.options)
4626 seenExtra = true
4627 slot = null
4628 } else {
4629 slot = struct.byId.get(id)
4630 if (slot === undefined) {
4631 if (seenIds === undefined) seenIds = new Set([id])
4632 else if (seenIds.has(id)) invalid("unique field ids", undefined, r.options)
4633 else seenIds.add(id)
4634 } else if (slot.index < 32) {
4635 if ((seenMask & (1 << slot.index)) !== 0) invalid("unique field ids", undefined, r.options)
4636 seenMask |= 1 << slot.index
4637 } else if (seenWide === undefined) {
4638 seenWide = new Set([slot.index])
4639 } else {
4640 if (seenWide.has(slot.index)) invalid("unique field ids", undefined, r.options)
4641 seenWide.add(slot.index)
4642 }
4643 }
4644 declared?.push(slot)
4645 const filled = decodeRunSlot(plan, tables, slot, out, r)
4646 if (filled >= 0) {
4647 if (filled < 32) presentMask |= 1 << filled
4648 else (presentWide ??= new Set()).add(filled)
4649 }
4650 }
4651 if (declared !== undefined) shapes.push(declared)
4652 }
4653 checkRequiredFields(struct, presentMask, presentWide, r)
4654 return out
4655}
4656
4657// Returns the field index this slot filled, or -1.
4658function decodeRunSlot(
4659 plan: RunPlan,
4660 tables: Array<Array<unknown> | undefined>,
4661 slot: RunSlot,
4662 out: Record<string, unknown>,
4663 r: Reader
4664): number {
4665 const code = r.uvarint()
4666 if ((code & 1) === 1) {
4667 if (slot === null || slot === undefined) return -1
4668 const table = tables[slot.index]
4669 const ref = (code - 1) / 2
4670 if (table === undefined || ref >= table.length) {
4671 invalid("a known back-reference", undefined, r.options)
4672 }
4673 const value = table[ref]
4674 if (value === ABSENT) return -1
4675 assignProperty(out, slot.name, value)
4676 return slot.index
4677 }
4678 const saved = r.enter(code / 2)
4679 if (slot === null) {
4680 decodeExtraPairs(plan.struct, r, out)
4681 r.exit(saved)
4682 return -1
4683 }
4684 if (slot === undefined) {
4685 r.exit(saved)
4686 return -1
4687 }
4688 const kind = plan.intern[slot.index]
4689 if (kind === INTERN_ELEMENTS || kind === INTERN_KEYS) {
4690 r.intern = tables[slot.index] ??= []
4691 }
4692 issuePath[issuePathLen++] = slot.name
4693 const value = decodeChecked(slot.layout, r)
4694 issuePathLen--
4695 r.intern = undefined
4696 r.exit(saved)
4697 if (kind === INTERN_SELF) (tables[slot.index] ??= []).push(value)
4698 if (value === ABSENT) return -1
4699 assignProperty(out, slot.name, value)
4700 return slot.index
4701}
4702
4703function decodeArray(layout: ArrayLayout, r: Reader): unknown {
4704 const elementLen = layout.elements.length
4705 const count = layout.hasCount ? r.uvarint() : elementLen
4706 if (layout.rest.length === 0 && count > elementLen) {
4707 issuePath[issuePathLen++] = elementLen
4708 throw issueError(new SchemaIssue.UnexpectedKey(layout.ast, undefined, r.options))
4709 }
4710 if (count < layout.minCount) {
4711 issuePath[issuePathLen++] = count
4712 throw issueError(new SchemaIssue.MissingKey(undefined))
4713 }
4714 if (layout.hasCount) {
4715 const allocationLimit = layout.rest.length > 0 && isZeroWidthSlot(layout.rest[0])
4716 ? ZERO_WIDTH_ARRAY_MAX
4717 : r.remaining + ARRAY_ALLOCATION_SLACK
4718 if (count > allocationLimit) invalid("array count within allocation limit", count, r.options)
4719 }
4720 if (count > 0) {
4721 const plan = runPlan(layout)
4722 if (plan !== null) return decodeStructRun(plan, count, r)
4723 }
4724 const out: Array<unknown> = new Array(count)
4725 const uniform = layout.uniform
4726 if (uniform !== undefined) {
4727 if (layout.uniformNumbers) return decodeNumberRun(out, count, r)
4728 // Only a run field holding an array of strings hands over an intern table.
4729 const table = r.intern
4730 r.intern = undefined
4731 if (table !== undefined) {
4732 for (let i = 0; i < count; i++) {
4733 issuePath[issuePathLen++] = i
4734 const value = decodeInterned(table, uniform, r)
4735 if (value === ABSENT) throw issueError(new SchemaIssue.MissingKey(undefined))
4736 issuePathLen--
4737 out[i] = value
4738 }
4739 return out
4740 }
4741 if (isSelfDelimiting(uniform)) {
4742 for (let i = 0; i < count; i++) {
4743 issuePath[issuePathLen++] = i
4744 out[i] = decodeValue(uniform, r)
4745 issuePathLen--
4746 }
4747 return out
4748 }
4749 // Strings consume their whole region, so they skip the reader window.
4750 if (uniform._ === "string") {
4751 for (let i = 0; i < count; i++) {
4752 issuePath[issuePathLen++] = i
4753 out[i] = r.dict === undefined ? r.readUtf8(r.uvarint()) : decodeSized(uniform, r)
4754 issuePathLen--
4755 }
4756 return out
4757 }
4758 const packed = layout.uniformPacked
4759 const inline = layout.uniformInline
4760 for (let i = 0; i < count; i++) {
4761 issuePath[issuePathLen++] = i
4762 const saved = r.enter(inline ? packed ?? 0 : r.uvarint())
4763 const value = decodeChecked(uniform, r)
4764 r.exit(saved)
4765 if (value === ABSENT) throw issueError(new SchemaIssue.MissingKey(undefined))
4766 issuePathLen--
4767 out[i] = value
4768 }
4769 return out
4770 }
4771 for (let i = 0; i < count; i++) {
4772 const slot = arraySlot(layout, i, count)
4773 const optional = i < elementLen && layout.elements[i].optional
4774 if (!layout.hasCount && r.remaining === 0 && !optional && slot._ !== "null" && slot._ !== "undefined") {
4775 issuePath[issuePathLen++] = i
4776 throw issueError(new SchemaIssue.MissingKey(undefined))
4777 }
4778 issuePath[issuePathLen++] = i
4779 const value = decodeSlot(slot, r)
4780 if (value === ABSENT) {
4781 if (optional) invalid("known union member", undefined, r.options)
4782 throw issueError(new SchemaIssue.MissingKey(undefined))
4783 }
4784 issuePathLen--
4785 out[i] = value
4786 }
4787 if (!layout.hasCount && r.pos < r.end) {
4788 issuePath[issuePathLen++] = elementLen
4789 throw issueError(new SchemaIssue.UnexpectedKey(layout.ast, undefined, r.options))
4790 }
4791 return out
4792}
4793
4794function decodeNumberRun(out: Array<unknown>, count: number, r: Reader): Array<unknown> {
4795 const mode = r.byte()
4796 if (mode === NUMBER_RUN_F64) {
4797 if (r.remaining !== count * 8) invalid("f64", undefined, r.options)
4798 for (let i = 0; i < count; i++) out[i] = r.f64()
4799 return out
4800 }
4801 if (mode === NUMBER_RUN_DECIMAL) {
4802 for (let i = 0; i < count; i++) {
4803 issuePath[issuePathLen++] = i
4804 const code = r.uvarint()
4805 const scale = code % 16
4806 if (scale > DECIMAL_SCALE_MAX) invalid("decimal", undefined, r.options)
4807 const value = decodeSignMagnitude((code - scale) / 16)
4808 out[i] = scale === 0 ? value : value / POW10[scale]
4809 issuePathLen--
4810 }
4811 return out
4812 }
4813 if (mode !== NUMBER_RUN_VARINT) invalid("f64", undefined, r.options)
4814 for (let i = 0; i < count; i++) {
4815 issuePath[issuePathLen++] = i
4816 out[i] = r.numberVarint()
4817 issuePathLen--
4818 }
4819 return out
4820}
4821
4822function decodeUnion(layout: UnionLayout, r: Reader): unknown {
4823 const kind = r.byte()
4824 if (kind === K.variant) {
4825 const tag = r.u32le()
4826 const variant = layout.byTag.get(tag)
4827 if (variant === undefined) {
4828 r.take(r.remaining)
4829 return ABSENT
4830 }
4831 const payload = decodeChecked(variant.payload, r)
4832 if (payload === ABSENT) return ABSENT
4833 if (!variant.tuple) {
4834 for (const sentinel of variant.sentinels) {
4835 assignProperty(payload as object, sentinel.key, sentinel.literal)
4836 }
4837 }
4838 return payload
4839 }
4840 const member = layout.byKind.get(kind)
4841 if (member === undefined) {
4842 r.take(r.remaining)
4843 return ABSENT
4844 }
4845 return decodeChecked(member, r)
4846}
4847
4848// Fingerprint mode rejects selectors outside its canonical member table.
4849function decodeUnionPositional(layout: UnionLayout, r: Reader): unknown {
4850 const position = layout.byPos[r.uvarint()]
4851 if (position === undefined) invalid("known union member", undefined, r.options)
4852 const payload = decodeChecked(position.layout, r)
4853 const variant = position.variant
4854 if (variant !== undefined && !variant.tuple && payload !== ABSENT) {
4855 for (const sentinel of variant.sentinels) {
4856 assignProperty(payload as object, sentinel.key, sentinel.literal)
4857 }
4858 }
4859 return payload
4860}
4861
4862function decodeReason(layout: ReasonLayout, r: Reader): unknown {
4863 const tag = r.byte()
4864 switch (tag) {
4865 case 0:
4866 return Cause.makeFailReason(requirePresent(decodeChecked(layout.error, r)))
4867 case 1:
4868 return Cause.makeDieReason(requirePresent(decodeChecked(layout.defect, r)))
4869 case 2:
4870 if (r.remaining !== 0) invalid("empty", undefined, r.options)
4871 return Cause.makeInterruptReason()
4872 case 3: {
4873 if (r.remaining !== 8) invalid("f64", undefined, r.options)
4874 return Cause.makeInterruptReason(r.f64())
4875 }
4876 default:
4877 r.take(r.remaining)
4878 return ABSENT
4879 }
4880}
4881
4882function requirePresent(value: unknown): unknown {
4883 if (value === ABSENT) throw issueError(new SchemaIssue.MissingKey(undefined))
4884 return value
4885}
4886
4887function decodeValue(layout: Layout, r: Reader): unknown {
4888 if (r.nesting >= MAX_NESTING_DEPTH) {
4889 invalid(`nesting depth at most ${MAX_NESTING_DEPTH}`, r.nesting + 1, r.options)
4890 }
4891 r.nesting++
4892 try {
4893 return decodeValueUnchecked(layout, r)
4894 } finally {
4895 r.nesting--
4896 }
4897}
4898
4899function decodeValueUnchecked(layout: Layout, r: Reader): unknown {
4900 switch (layout._) {
4901 case "literal": {
4902 const value = decodeValue(layout.leaf, r)
4903 if (!hasLiteral(layout, value)) invalid(literalExpected(layout), value, r.options)
4904 return value
4905 }
4906 case "bool": {
4907 if (r.remaining !== 1) invalid("bool", undefined, r.options)
4908 const b = r.byte()
4909 if (b > 1) invalid("bool", undefined, r.options)
4910 return b === 1
4911 }
4912 case "null":
4913 if (r.remaining !== 0) invalid("empty", undefined, r.options)
4914 return null
4915 case "undefined":
4916 if (r.remaining !== 0) invalid("empty", undefined, r.options)
4917 return undefined
4918 case "number": {
4919 const len = r.remaining
4920 if (len === 8) return r.f64()
4921 if (len === 0 || len > NUMBER_VARINT_MAX_BYTES) invalid("f64", undefined, r.options)
4922 const value = r.numberVarint()
4923 if (r.remaining === 0) return value
4924 const scale = r.byte()
4925 if (r.remaining !== 0 || scale === 0 || scale > DECIMAL_SCALE_MAX) {
4926 invalid("decimal", undefined, r.options)
4927 }
4928 return value / POW10[scale]
4929 }
4930 case "int":
4931 return r.numberVarint()
4932 case "string": {
4933 const dict = r.dict
4934 return dict === undefined ? r.readUtf8(r.end - r.pos) : decodeString(dict, layout, r)
4935 }
4936 case "symbol":
4937 return globalThis.Symbol.for(r.readUtf8(r.end - r.pos))
4938 case "bytes":
4939 return r.takeCopy(r.end - r.pos)
4940 case "bigint":
4941 return r.zigzag()
4942 case "int64": {
4943 if (r.remaining !== 8) invalid("int64", undefined, r.options)
4944 const millis = Number(r.i64())
4945 const date = new Date(millis)
4946 if (Number.isNaN(date.getTime())) {
4947 invalid(layout.flavor === "date" ? "a valid Date" : "a valid DateTime", millis, r.options)
4948 }
4949 return layout.flavor === "date" ? date : DateTime.makeUnsafe(millis)
4950 }
4951 case "dateTimeZoned": {
4952 const millis = Number(r.i64())
4953 const tag = r.byte()
4954 let timeZone: number | string
4955 if (tag === 0) {
4956 if (r.remaining !== 4) invalid("time zone", undefined, r.options)
4957 timeZone = r.i32le()
4958 } else if (tag === 1) {
4959 timeZone = r.readUtf8(r.end - r.pos)
4960 } else {
4961 return invalid("time zone", undefined, r.options)
4962 }
4963 try {
4964 return DateTime.makeZonedUnsafe(millis, { timeZone })
4965 } catch {
4966 return invalid("time zone", undefined, r.options)
4967 }
4968 }
4969 case "duration": {
4970 const tag = r.byte()
4971 switch (tag) {
4972 case 0:
4973 return Duration.nanos(r.zigzag())
4974 case 1:
4975 return Duration.infinity
4976 case 2:
4977 return Duration.negativeInfinity
4978 default:
4979 return invalid("duration", undefined, r.options)
4980 }
4981 }
4982 case "bigDecimal": {
4983 const value = r.zigzag()
4984 const scale = r.zigzag()
4985 if (scale > MAX_SAFE_BIGINT || -scale > MAX_SAFE_BIGINT) invalid("safe integer length", undefined, r.options)
4986 return BigDecimal.make(value, Number(scale))
4987 }
4988 case "json": {
4989 const text = r.readUtf8(r.end - r.pos)
4990 try {
4991 return JSON.parse(text)
4992 } catch {
4993 return invalid("json", undefined, r.options)
4994 }
4995 }
4996 case "option": {
4997 const tag = r.byte()
4998 if (tag === 0) {
4999 if (r.remaining !== 0) invalid("empty", undefined, r.options)
5000 return Option.none()
5001 }
5002 if (tag !== 1) invalid("bool", undefined, r.options)
5003 return Option.some(requirePresent(decodeChecked(layout.value, r)))
5004 }
5005 case "result": {
5006 const tag = r.byte()
5007 if (tag === 0) return Result.succeed(requirePresent(decodeChecked(layout.success, r)))
5008 if (tag !== 1) invalid("bool", undefined, r.options)
5009 return Result.fail(requirePresent(decodeChecked(layout.failure, r)))
5010 }
5011 case "exit": {
5012 const tag = r.byte()
5013 if (tag === 0) return Exit.succeed(requirePresent(decodeChecked(layout.value, r)))
5014 if (tag !== 1) invalid("bool", undefined, r.options)
5015 const cause = decodeValue(layout.cause, r)
5016 return Exit.failCause(cause as Cause.Cause<unknown>)
5017 }
5018 case "cause": {
5019 const count = r.uvarint()
5020 const reasons: Array<Cause.Reason<unknown>> = []
5021 for (let i = 0; i < count; i++) {
5022 issuePath[issuePathLen++] = i
5023 const saved = r.enter(r.uvarint())
5024 const reason = decodeReason(layout, r)
5025 r.exit(saved)
5026 issuePathLen--
5027 if (reason !== ABSENT) reasons.push(reason as Cause.Reason<unknown>)
5028 }
5029 if (r.pos !== r.end) invalid("no leftover bytes", undefined, r.options)
5030 return Cause.fromReasons(reasons)
5031 }
5032 case "causeReason":
5033 return decodeReason(layout, r)
5034 case "struct":
5035 return r.positional ? decodeStructPositional(layout, r) : decodeStruct(layout, r)
5036 case "array":
5037 return decodeArray(layout, r)
5038 case "union":
5039 return r.positional ? decodeUnionPositional(layout, r) : decodeUnion(layout, r)
5040 case "never":
5041 throw issueError(new SchemaIssue.InvalidType(layout.ast, undefined, r.options))
5042 }
5043}
5044
5045function decodeFrameBody(layout: Layout, r: Reader, mode: Mode): unknown {
5046 const envelope = r.byte()
5047 if (envelope !== mode.envelope) invalid(mode.expectedEnvelope, envelope, r.options)
5048 if (mode.positional) {
5049 if (r.remaining < 8) invalid("complete value", undefined, r.options)
5050 const buf = r.buf
5051 const expected = mode.fingerprint
5052 const pos = r.pos
5053 for (let i = 0; i < 8; i++) {
5054 if (buf[pos + i] !== expected[i]) invalid("matching layout fingerprint", undefined, r.options)
5055 }
5056 r.pos = pos + 8
5057 }
5058 const value = decodeChecked(layout, r)
5059 if (value === ABSENT) throw issueError(new SchemaIssue.MissingKey(undefined))
5060 return value
5061}
5062
5063// Reuse one top-level reader while allowing nested codecs to allocate their own.
5064let pooledReader: Reader | undefined = new Reader()
5065
5066function decodeOneShot(
5067 layout: Layout,
5068 bytes: Uint8Array,
5069 options: SchemaAST.ParseOptions,
5070 mode: Mode
5071): unknown {
5072 const r = pooledReader ?? new Reader()
5073 const pooled = r === pooledReader
5074 if (pooled) pooledReader = undefined
5075 r.reset(bytes, 0, bytes.length, options, undefined, mode.positional, undefined)
5076 const savedPathLen = issuePathLen
5077 issuePathLen = 0
5078 try {
5079 const n = r.uvarint()
5080 if (n === 0) invalid("nonzero frame length", undefined, options)
5081 const saved = r.enter(n)
5082 const value = decodeFrameBody(layout, r, mode)
5083 r.exit(saved)
5084 if (r.pos !== bytes.length) invalid("no leftover bytes", undefined, options)
5085 return value
5086 } finally {
5087 issuePathLen = savedPathLen
5088 r.release()
5089 if (pooled) pooledReader = r
5090 }
5091}
5092
5093function makeTransformation(
5094 layout: Layout,
5095 mode: Mode,
5096 trusted: Trusted | undefined,
5097 successOnly = false
5098): SchemaTransformation.Transformation<unknown, Uint8Array<ArrayBuffer>> {
5099 return SchemaTransformation.transformEffect({
5100 decode: (bytes: Uint8Array<ArrayBuffer>, options) => {
5101 try {
5102 const value = decodeOneShot(layout, bytes, options, mode)
5103 if (
5104 trusted !== undefined && Predicate.isObjectOrArray(value) &&
5105 (!successOnly || isSuccessExit(value))
5106 ) trusted.value = value
5107 return Effect.succeed(value)
5108 } catch (e) {
5109 return e instanceof IssueError ? Effect.fail(e.issue) : Effect.die(e)
5110 }
5111 },
5112 encode: (value: unknown, options) => {
5113 try {
5114 return Effect.succeed(encodeFrame(layout, value, options, mode))
5115 } catch (e) {
5116 return e instanceof IssueError ? Effect.fail(e.issue) : Effect.die(e)
5117 }
5118 }
5119 })
5120}
5121
5122const fingerprintModeMemo = memoize(fingerprintMode)
5123
5124function compileMode(layout: Layout, fingerprint: boolean | undefined): Mode {
5125 return fingerprint === true ? fingerprintModeMemo(layout) : defaultMode
5126}
5127
5128interface CompiledTarget {
5129 readonly target: Schema.Constraint
5130 readonly layout: Layout
5131 readonly exact: boolean
5132 readonly exitSuccess: boolean
5133 // A recursive schema can hold a cyclic value, which only the schema pass
5134 // detects, so encoding one keeps that pass even when the layout is exact.
5135 readonly recursive: boolean
5136}
5137
5138const compileTargetFromAst = memoize((ast: SchemaAST.AST): CompiledTarget => {
5139 const raw = Schema.make<Schema.Constraint>(toBinaryAST(ast))
5140 const { exact, exitSuccess, layout, recursive } = compileLayout(raw.ast)
5141 // Only recursive schemas need the cycle walk.
5142 return { target: recursive ? withCycleGuard(raw) : raw, layout, exact, exitSuccess, recursive }
5143})
5144
5145function compileTarget(schema: Schema.Constraint): CompiledTarget {
5146 return compileTargetFromAst(schema.ast)
5147}
5148
5149// The value the binary decoder produced most recently. The schema pass around
5150// the decode runs straight after it, so one slot is all the handoff needs, and
5151// a value that misses the slot just pays the real check. The slot holds that
5152// one value until the next decode replaces it.
5153interface Trusted {
5154 value: object | undefined
5155}
5156
5157function takeTrusted(trusted: Trusted, input: unknown): boolean {
5158 if (trusted.value === undefined || input !== trusted.value) return false
5159 trusted.value = undefined
5160 return true
5161}
5162
5163// A target that bypasses the schema pass when `accept` takes the input, and
5164// otherwise runs the target's type schema. The two directions get their own
5165// predicate: `flip` swaps the declaration's encoding run in when encoding, the
5166// way it already swaps `encodingChecks`.
5167function bypassPass(
5168 target: Schema.Constraint,
5169 decodeAccept: (input: unknown, options: SchemaAST.ParseOptions) => boolean,
5170 encodeAccept: (input: unknown, options: SchemaAST.ParseOptions) => boolean
5171): Schema.Constraint {
5172 const type = Schema.make(SchemaAST.toType(target.ast))
5173 const parse = SchemaParser.decodeUnknownEffect(type as Schema.ConstraintDecoder<unknown>)
5174 // A bypassed input comes back unchanged, which the shared "same value" exit
5175 // says without allocating one per call.
5176 const run = (
5177 accept: (input: unknown, options: SchemaAST.ParseOptions) => boolean
5178 ): SchemaAST.Declaration["run"] =>
5179 () =>
5180 (input, _ast, options) => accept(input, options) ? InternalParserProtocol.sameExit : parse(input, options)
5181 return Schema.make(
5182 new SchemaAST.Declaration(
5183 [type.ast],
5184 run(decodeAccept),
5185 { identifier: "binary value" },
5186 undefined,
5187 undefined,
5188 undefined,
5189 undefined,
5190 run(encodeAccept)
5191 )
5192 )
5193}
5194
5195// The binary layer drops unknown keys instead of reporting them, so only
5196// `onExcessProperty: "error"` needs the schema pass to see the input.
5197function reportsExcess(_input: unknown, options: SchemaAST.ParseOptions): boolean {
5198 return options.onExcessProperty !== "error"
5199}
5200
5201// The binary layer already validates an exact schema in both directions, so the
5202// schema pass around it repeats that work. Encoding skips it outright. Decoding
5203// only lets values the binary layer just produced through, so every other
5204// input, `Schema.is` included, still runs the real check.
5205function withTrustedDecode(
5206 target: Schema.Constraint,
5207 trusted: Trusted,
5208 bypassEncode: boolean
5209): Schema.Constraint {
5210 return bypassPass(
5211 target,
5212 (input) => takeTrusted(trusted, input),
5213 // A recursive schema keeps the pass on encode: decoding cannot build a
5214 // cycle, but a caller can hand one in, and the cycle walk lives there.
5215 bypassEncode ? reportsExcess : constFalse
5216 )
5217}
5218
5219function constFalse(): boolean {
5220 return false
5221}
5222
5223// Success exits with an exact success schema are fully validated by the binary
5224// layer, so only failure exits pay the schema pass for their cause's error and
5225// defect encodings. The type parameter flips with the direction, so the same
5226// fallback decodes binary failure exits and encodes failure exits back to the
5227// wire representation. Under `onExcessProperty: "error"` only decoder-produced
5228// values bypass the pass: the binary layer drops unknown keys instead of
5229// reporting them, so other success exits keep the schema pass. It is not a
5230// sound `Schema.is` guard, which is why only {@link toCodecDirect} uses it.
5231function withExitSuccessDecode(target: Schema.Constraint, trusted: Trusted): Schema.Constraint {
5232 return Schema.declareConstructor<unknown>()(
5233 [target],
5234 ([codec]) => {
5235 const parse = SchemaParser.decodeUnknownEffect(codec as Schema.ConstraintDecoder<unknown>)
5236 return (input, _ast, options) =>
5237 takeTrusted(trusted, input) ||
5238 (isSuccessExit(input) && options.onExcessProperty !== "error")
5239 ? Effect.succeed(input)
5240 : parse(input, options)
5241 },
5242 { identifier: "binary exit value" }
5243 )
5244}
5245
5246// An exact schema is fully validated by the binary layer in both directions, so
5247// this target adds nothing, except under `onExcessProperty: "error"`. It is not
5248// a sound `Schema.is` guard, which is why only {@link toCodecDirect} uses it.
5249function passThrough(target: Schema.Constraint): Schema.Constraint {
5250 return bypassPass(target, reportsExcess, reportsExcess)
5251}
5252
5253// Skip the cycle walk once for structurally decoded values.
5254function withCycleGuard(target: Schema.Constraint): Schema.Constraint {
5255 const type = Schema.make(SchemaAST.toType(target.ast))
5256 const decoded = new WeakSet<object>()
5257 const guard = Schema.declareConstructor<unknown>()(
5258 [type],
5259 ([type]) => (input, ast, options) => {
5260 if (Predicate.isObjectOrArray(input) && decoded.delete(input)) {
5261 return Effect.succeed(input)
5262 }
5263 return isCyclic(input)
5264 ? Effect.fail(new SchemaIssue.InvalidType(ast, input, options))
5265 : SchemaParser.decodeUnknownEffect(type)(input, options)
5266 },
5267 { identifier: "acyclic value" }
5268 )
5269 return Schema.decodeTo(
5270 guard,
5271 SchemaTransformation.transform({
5272 decode: (value) => {
5273 if (Predicate.isObjectOrArray(value)) decoded.add(value)
5274 return value
5275 },
5276 encode: (value) => value
5277 })
5278 )(target)
5279}