functionalscript 0.46.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/fjs/basen/cbase32/module.f.mjs +18 -10
  2. package/fjs/basen/cbase32/proof.f.d.mts +3 -0
  3. package/fjs/basen/cbase32/proof.f.mjs +24 -2
  4. package/fjs/basen/proof.f.mjs +8 -16
  5. package/fjs/bnf/ll1/proof.f.d.mts +2 -0
  6. package/fjs/bnf/ll1/proof.f.mjs +33 -18
  7. package/fjs/bnf/module.f.d.mts +2 -2
  8. package/fjs/bnf/module.f.mjs +2 -2
  9. package/fjs/cas/proof.f.mjs +4 -26
  10. package/fjs/ci/common/module.f.mjs +6 -0
  11. package/fjs/ci/config/module.f.d.mts +7 -7
  12. package/fjs/ci/config/module.f.mjs +7 -7
  13. package/fjs/cli/module.f.mjs +14 -10
  14. package/fjs/crypto/vdf/module.f.d.mts +0 -1
  15. package/fjs/crypto/vdf/module.f.mjs +3 -14
  16. package/fjs/dev/module.f.mjs +10 -20
  17. package/fjs/djs/parser/module.f.d.mts +101 -53
  18. package/fjs/djs/parser/module.f.mjs +732 -525
  19. package/fjs/djs/parser/proof.f.d.mts +6 -0
  20. package/fjs/djs/parser/proof.f.mjs +315 -11
  21. package/fjs/djs/parser/types.d.ts +29 -2
  22. package/fjs/djs/tokenizer/module.f.mjs +122 -105
  23. package/fjs/djs/tokenizer/proof.f.mjs +17 -0
  24. package/fjs/djs/transpiler/module.f.mjs +4 -5
  25. package/fjs/djs/transpiler/proof.f.d.mts +2 -0
  26. package/fjs/djs/transpiler/proof.f.mjs +25 -0
  27. package/fjs/edag/amnesia/module.f.d.mts +20 -0
  28. package/fjs/edag/amnesia/module.f.mjs +303 -0
  29. package/fjs/edag/amnesia/proof.f.d.mts +79 -0
  30. package/fjs/edag/amnesia/proof.f.mjs +573 -0
  31. package/fjs/edag/amnesia/types.d.ts +15 -0
  32. package/fjs/edag/module.f.d.mts +410 -0
  33. package/fjs/edag/module.f.mjs +559 -0
  34. package/fjs/edag/proof.f.d.mts +116 -0
  35. package/fjs/edag/proof.f.mjs +686 -0
  36. package/fjs/edag/types.d.ts +55 -0
  37. package/fjs/effects/node/memory/module.d.mts +37 -3
  38. package/fjs/effects/node/memory/module.mjs +38 -4
  39. package/fjs/effects/node/memory/proof.d.mts +5 -2
  40. package/fjs/effects/node/memory/proof.mjs +36 -6
  41. package/fjs/effects/node/module.d.mts +9 -1
  42. package/fjs/effects/node/module.f.d.mts +39 -0
  43. package/fjs/effects/node/module.f.mjs +46 -0
  44. package/fjs/effects/node/module.mjs +234 -32
  45. package/fjs/effects/node/types.d.ts +28 -2
  46. package/fjs/effects/node/virtual/module.f.d.mts +19 -10
  47. package/fjs/effects/node/virtual/module.f.mjs +246 -32
  48. package/fjs/effects/node/virtual/proof.f.d.mts +17 -2
  49. package/fjs/effects/node/virtual/proof.f.mjs +232 -8
  50. package/fjs/effects/node/virtual/types.d.ts +57 -1
  51. package/fjs/emergent_testing/browser/proof.d.mts +55 -0
  52. package/fjs/emergent_testing/browser/proof.mjs +352 -0
  53. package/fjs/emergent_testing/browser/species.proof.d.mts +4 -0
  54. package/fjs/emergent_testing/browser/species.proof.mjs +45 -0
  55. package/fjs/emergent_testing/browser.d.mts +79 -0
  56. package/fjs/emergent_testing/browser.mjs +432 -0
  57. package/fjs/emergent_testing/proof.f.d.mts +1 -1
  58. package/fjs/emergent_testing/proof.f.mjs +2 -2
  59. package/fjs/js/string_escape/module.f.d.mts +47 -0
  60. package/fjs/js/string_escape/module.f.mjs +95 -0
  61. package/fjs/js/string_escape/proof.f.d.mts +8 -0
  62. package/fjs/js/string_escape/proof.f.mjs +68 -0
  63. package/fjs/js/tokenizer/module.f.mjs +14 -12
  64. package/fjs/mcp/cas/module.f.d.mts +1 -7
  65. package/fjs/mcp/cas/module.f.mjs +24 -15
  66. package/fjs/mcp/evo/module.f.mjs +9 -24
  67. package/fjs/mcp/module.f.d.mts +3 -1
  68. package/fjs/mcp/module.f.mjs +4 -2
  69. package/fjs/media/json/rtti/module.f.d.mts +2 -2
  70. package/fjs/media/json/rtti/module.f.mjs +1 -1
  71. package/fjs/media/json/schema/module.f.d.mts +15 -5
  72. package/fjs/media/json/schema/module.f.mjs +42 -15
  73. package/fjs/media/json/schema/proof.f.d.mts +10 -1
  74. package/fjs/media/json/schema/proof.f.mjs +88 -16
  75. package/fjs/media/json/serializer/module.f.mjs +14 -26
  76. package/fjs/media/json/types.d.ts +1 -1
  77. package/fjs/media/lock/module.f.d.mts +8 -3
  78. package/fjs/media/lock/module.f.mjs +9 -3
  79. package/fjs/media/lock/proof.f.d.mts +1 -1
  80. package/fjs/media/lock/proof.f.mjs +2 -2
  81. package/fjs/media/lock/types.d.ts +1 -3
  82. package/fjs/media/module.f.d.mts +20 -13
  83. package/fjs/media/module.f.mjs +29 -16
  84. package/fjs/media/note/module.f.d.mts +14 -7
  85. package/fjs/media/note/module.f.mjs +14 -7
  86. package/fjs/media/note/proof.f.mjs +1 -1
  87. package/fjs/media/proof.f.mjs +5 -5
  88. package/fjs/media/revision/module.f.d.mts +8 -2
  89. package/fjs/media/revision/module.f.mjs +9 -3
  90. package/fjs/media/revision/proof.f.mjs +1 -1
  91. package/fjs/media/revision/types.d.ts +1 -1
  92. package/fjs/media/type/module.f.d.mts +20 -0
  93. package/fjs/media/type/module.f.mjs +74 -0
  94. package/fjs/media/type/proof.f.d.mts +5 -0
  95. package/fjs/media/type/proof.f.mjs +24 -1
  96. package/fjs/module.f.mjs +6 -0
  97. package/fjs/path/module.f.d.mts +60 -3
  98. package/fjs/path/module.f.mjs +138 -14
  99. package/fjs/path/proof.f.d.mts +3 -0
  100. package/fjs/path/proof.f.mjs +298 -2
  101. package/fjs/protocol/json_rpc/module.f.d.mts +65 -28
  102. package/fjs/protocol/json_rpc/module.f.mjs +49 -13
  103. package/fjs/protocol/json_rpc/proof.f.d.mts +4 -0
  104. package/fjs/protocol/json_rpc/proof.f.mjs +29 -0
  105. package/fjs/protocol/json_rpc/types.d.ts +12 -2
  106. package/fjs/protocol/mcp/module.f.d.mts +89 -54
  107. package/fjs/protocol/mcp/module.f.mjs +110 -56
  108. package/fjs/protocol/mcp/proof.f.d.mts +13 -0
  109. package/fjs/protocol/mcp/proof.f.mjs +107 -7
  110. package/fjs/protocol/mcp/stdio/module.f.mjs +3 -4
  111. package/fjs/protocol/mcp/stdio/proof.f.mjs +2 -2
  112. package/fjs/protocol/mcp/types.d.ts +14 -2
  113. package/fjs/text/utf8/module.f.mjs +27 -1
  114. package/fjs/text/utf8/proof.f.mjs +37 -0
  115. package/fjs/types/array/module.f.mjs +18 -11
  116. package/fjs/types/array/proof.f.mjs +27 -1
  117. package/fjs/types/bigfloat/module.f.d.mts +48 -9
  118. package/fjs/types/bigfloat/module.f.mjs +147 -59
  119. package/fjs/types/bigfloat/proof.f.d.mts +2 -0
  120. package/fjs/types/bigfloat/proof.f.mjs +136 -4
  121. package/fjs/types/bigfloat/types.d.ts +17 -0
  122. package/fjs/types/function/module.f.d.mts +11 -2
  123. package/fjs/types/function/module.f.mjs +20 -2
  124. package/fjs/types/function/proof.f.mjs +5 -1
  125. package/fjs/types/phantom/types.d.ts +21 -0
  126. package/fjs/types/result/module.f.d.mts +4 -4
  127. package/fjs/types/result/module.f.mjs +2 -2
  128. package/fjs/types/result/proof.f.d.mts +6 -2
  129. package/fjs/types/result/proof.f.mjs +17 -1
  130. package/fjs/types/rtti/common/module.f.d.mts +78 -5
  131. package/fjs/types/rtti/common/module.f.mjs +168 -3
  132. package/fjs/types/rtti/common/proof.f.d.mts +14 -0
  133. package/fjs/types/rtti/common/proof.f.mjs +61 -2
  134. package/fjs/types/rtti/common/types.d.ts +23 -1
  135. package/fjs/types/rtti/data/module.f.d.mts +57 -1
  136. package/fjs/types/rtti/data/module.f.mjs +241 -47
  137. package/fjs/types/rtti/data/proof.f.d.mts +10 -0
  138. package/fjs/types/rtti/data/proof.f.mjs +272 -39
  139. package/fjs/types/rtti/data/types.d.ts +14 -7
  140. package/fjs/types/rtti/host.proof.d.mts +33 -0
  141. package/fjs/types/rtti/host.proof.mjs +170 -0
  142. package/fjs/types/rtti/module.f.d.mts +51 -6
  143. package/fjs/types/rtti/module.f.mjs +50 -3
  144. package/fjs/types/rtti/parse/module.f.d.mts +31 -24
  145. package/fjs/types/rtti/parse/module.f.mjs +197 -59
  146. package/fjs/types/rtti/parse/proof.f.d.mts +37 -2
  147. package/fjs/types/rtti/parse/proof.f.mjs +171 -47
  148. package/fjs/types/rtti/proof.f.d.mts +6 -0
  149. package/fjs/types/rtti/proof.f.mjs +42 -1
  150. package/fjs/types/rtti/ts/module.f.d.mts +9 -3
  151. package/fjs/types/rtti/ts/module.f.mjs +68 -15
  152. package/fjs/types/rtti/ts/proof.f.d.mts +42 -2
  153. package/fjs/types/rtti/ts/proof.f.mjs +139 -6
  154. package/fjs/types/rtti/ts/types.d.ts +164 -16
  155. package/fjs/types/rtti/types.d.ts +31 -3
  156. package/fjs/types/rtti/validate/module.f.d.mts +120 -0
  157. package/fjs/types/rtti/validate/module.f.mjs +351 -0
  158. package/fjs/types/rtti/validate/proof.f.d.mts +154 -0
  159. package/fjs/types/rtti/validate/proof.f.mjs +781 -0
  160. package/fjs/types/ts/types.d.ts +4 -0
  161. package/fjs/web/module.f.d.mts +115 -0
  162. package/fjs/web/module.f.mjs +581 -0
  163. package/fjs/web/proof.f.d.mts +39 -0
  164. package/fjs/web/proof.f.mjs +413 -0
  165. package/fjs/web/types.d.ts +39 -0
  166. package/fjs/website/browser-prepare.d.mts +5 -0
  167. package/fjs/website/browser-prepare.mjs +115 -0
  168. package/fjs/website/browser-source.d.mts +37 -0
  169. package/fjs/website/browser-source.mjs +202 -0
  170. package/fjs/website/browser-source.proof.d.mts +31 -0
  171. package/fjs/website/browser-source.proof.mjs +162 -0
  172. package/fjs/website/browser.d.mts +5 -0
  173. package/fjs/website/browser.mjs +8 -0
  174. package/fjs/website/module.f.d.mts +3 -3
  175. package/fjs/website/module.f.mjs +62 -9
  176. package/fjs/website/proof.f.d.mts +4 -0
  177. package/fjs/website/proof.f.mjs +18 -3
  178. package/package.json +2 -3
@@ -0,0 +1,781 @@
1
+ /**
2
+ * @import { ValidationError, ValidateE, Validate } from '../common/types.ts'
3
+ * @import { Type } from '../types.ts'
4
+ * @import { Equal } from '../../ts/types.ts'
5
+ * @import { Ts, Unknown } from '../ts/types.ts'
6
+ * @import { Unknown as DjsUnknown } from '../../../djs/types.ts'
7
+ * @import { Assert } from '../../../asserts/types.ts'
8
+ */
9
+
10
+ import { validate } from './module.f.mjs'
11
+ import { parse } from '../parse/module.f.mjs'
12
+ import { toData, validate as dataValidate } from '../data/module.f.mjs'
13
+ import { boolean, number, string, bigint, unknown, array, never, open, record, rest, or, option } from '../module.f.mjs'
14
+ import { unwrap } from '../../result/module.f.mjs'
15
+ import { assert, assertEq, assertStructurallySame } from '../../../asserts/module.f.mjs'
16
+
17
+ /** @type {(r: readonly [string, unknown]) => void} */
18
+ const assertOk = ([k]) => { assertEq(k, 'ok', 'expected ok') }
19
+
20
+ /** @type {(r: readonly [string, unknown]) => void} */
21
+ const assertError = ([k]) => { assertEq(k, 'error', 'expected error') }
22
+
23
+ /** @type {(expected: readonly string[]) => (r: readonly [string, unknown]) => void} */
24
+ const assertErrorPath = expected =>
25
+ r => {
26
+ assert(r[0] === 'error', 'expected error')
27
+ const e = /** @type {ValidationError} */ (r[1])
28
+ assertStructurallySame(e.path, expected, 'unexpected error path')
29
+ }
30
+
31
+ /** Both readers with their payload type erased, so a table can hold rows of mixed schemas. */
32
+
33
+ /** @type {(t: Type) => ValidateE} */
34
+ const v = t => /** @type {any} */ (validate(t))
35
+
36
+ /** @type {(t: Type) => ValidateE} */
37
+ const p = t => /** @type {any} */ (parse(t))
38
+
39
+ /** The data form's reader, over the same erased signature. */
40
+ /** @type {(t: Type) => ValidateE} */
41
+ const d = t => dataValidate(toData(t))
42
+
43
+ /**
44
+ * A rest that is its own container, so nothing about it is inline: the
45
+ * conversion keeps `rest: "recursiveRest"` rather than recognizing that no
46
+ * finite array inhabits it, and every reader accepts a hole past the prefix
47
+ * accordingly. It is one of the two rests {@link emptyRests} must *not*
48
+ * recognize.
49
+ *
50
+ * @typedef {() => readonly ['rest', readonly [_RecursiveRest], typeof never]} _RecursiveRest
51
+ */
52
+
53
+ /** @type {_RecursiveRest} */
54
+ const recursiveRest = () => ['rest', [recursiveRest], never]
55
+
56
+ /**
57
+ * The other one: a pure `or` cycle. `toData(orCycleA)` **is** `never`, yet as a
58
+ * rest it converts to a reference and stays, so a test on the rest's own
59
+ * canonical data would answer the opposite of the criterion.
60
+ *
61
+ * @typedef {() => readonly ['or', _OrCycleB]} _OrCycleA
62
+ * @typedef {() => readonly ['or', _OrCycleA]} _OrCycleB
63
+ */
64
+
65
+ /** @type {_OrCycleA} */
66
+ const orCycleA = () => ['or', orCycleB]
67
+
68
+ /** @type {_OrCycleB} */
69
+ const orCycleB = () => ['or', orCycleA]
70
+
71
+ /**
72
+ * Two separately constructed copies of one recursive rule. Converting a rest
73
+ * reserves its rule name first, so the container's copy is named `r0` where
74
+ * converting the container alone names it `r` — which is what rules `equal`
75
+ * out as the comparison behind {@link emptyRests}.
76
+ *
77
+ * @typedef {() => readonly ['or', undefined, () => readonly ['array', _SelfList]]} _SelfList
78
+ */
79
+
80
+ /** @type {_SelfList} */
81
+ const selfList0 = () => ['or', undefined, array(selfList0)]
82
+
83
+ /** @type {_SelfList} */
84
+ const selfList1 = () => ['or', undefined, array(selfList1)]
85
+
86
+ /**
87
+ * The acceptance table. Rows cover both container kinds, the closed default
88
+ * and a stated rest on both, the short-array rule, primitives, `or`, and
89
+ * misses — every reader of a schema has to answer them the same way.
90
+ *
91
+ * @type {readonly (readonly [Type, Unknown])[]}
92
+ */
93
+ const rows = [
94
+ [number, 42],
95
+ [number, '42'],
96
+ [string, 42],
97
+ [boolean, false],
98
+ [bigint, 7n],
99
+ [unknown, { a: [1, 'x'] }],
100
+ [/** @type {const} */ (42), 42],
101
+ [/** @type {const} */ (42), 43],
102
+ [array(number), [1, 2, 3]],
103
+ [array(number), [1, 'two']],
104
+ [array(number), {}],
105
+ // an enumerable non-index key is an entry every reader walks, so it is
106
+ // held to the element type like any other — and a key is an index only in
107
+ // the canonical spelling, whatever `Number` makes of it
108
+ [array(number), Object.assign([1], { foo: 2 })],
109
+ [array(number), Object.assign([1], { foo: 'x' })],
110
+ [array(number), Object.assign([1], { '-1': 'x' })],
111
+ [array(number), Object.assign([1], { '01': 'x' })],
112
+ // an empty element set is the empty array, not "any number of holes": the
113
+ // data form normalizes such a rest away, which leaves the exact-length
114
+ // pattern, and the thunk readers bound the length to match
115
+ [array(or()), []],
116
+ [array(or()), new Array(1)],
117
+ [array(number), [, ,]],
118
+ [record(number), { a: 1 }],
119
+ [record(number), { a: 'one' }],
120
+ [record(number), []],
121
+ // the closed default, on both kinds
122
+ [[/** @type {const} */ (42)], [42, 'extra']],
123
+ [[/** @type {const} */ (42)], [42]],
124
+ [[/** @type {const} */ (42)], [42, undefined]],
125
+ [[/** @type {const} */ (42)], [42, ,]],
126
+ [[/** @type {const} */ (42)], Object.assign([42], { foo: 1 })],
127
+ [[/** @type {const} */ (42)], []],
128
+ [{ a: /** @type {const} */ (42) }, { a: 42, b: 'x' }],
129
+ [{ a: /** @type {const} */ (42) }, { a: 42 }],
130
+ // a key declared `unknown` is a member the schema has, so the canonical
131
+ // form must not drop it the way an `open` struct's is dropped
132
+ [{ a: unknown }, { a: 1 }],
133
+ [{ a: unknown }, { a: 1, b: 2 }],
134
+ // and the same rows under `open`, which is the form that admits them
135
+ [open([/** @type {const} */ (42)]), [42, 'extra']],
136
+ [open({ a: /** @type {const} */ (42) }), { a: 42, b: 'x' }],
137
+ [open([]), [1]],
138
+ [open({}), { a: 1 }],
139
+ // closedness is about *undeclared* members and leaves the short-array rule
140
+ // alone
141
+ [[number, option(string)], [42]],
142
+ // the rule is per position, not "the last one": every trailing position
143
+ // whose set admits `undefined` may be absent, so an array may stop at the
144
+ // last required one
145
+ [[number, bigint, option(string), option(null)], [2, 4n]],
146
+ [[number, bigint, option(string), option(null)], [2, 4n, 'x']],
147
+ [[number, bigint, option(string), option(null)], [2, 4n, 'x', null]],
148
+ [[number, bigint, option(string), option(null)], [2]],
149
+ [[number, bigint, option(string), option(null)], [2, 4n, 5]],
150
+ [{ a: number, b: option(string) }, { a: 1 }],
151
+ [{ a: number }, { a: 'one' }],
152
+ // a hole in a tuple schema is a declared position whose schema is
153
+ // `undefined`, so the schema's length is what it declares — the reading
154
+ // the data form has always had, and the one `Object.entries` lost
155
+ [new Array(1), [1, 2, 3]],
156
+ [new Array(1), new Array(1)],
157
+ [new Array(1), [undefined]],
158
+ [new Array(1), [1]],
159
+ [new Array(1), []],
160
+ [[, number], [9, 5]],
161
+ [[, number], [undefined, 5]],
162
+ // and a non-index enumerable own property is no position at all: a tuple
163
+ // schema is read by index, so `foo` declares nothing — which leaves a
164
+ // value's own `foo` an undeclared member like any other
165
+ [Object.assign([number], { foo: string }), [1]],
166
+ [Object.assign([number], { foo: string }), Object.assign([1], { foo: 'x' })],
167
+ [open(Object.assign([number], { foo: string })), Object.assign([1], { foo: 'x' })],
168
+ // a stated rest: what an undeclared member must be
169
+ [rest([number], string), [1, 'x', 'y']],
170
+ [rest([number], string), [1, 2]],
171
+ // a hole past the prefix is no member, so it meets no rest — which is what
172
+ // the `| undefined` in the rendered tail says
173
+ [rest([number], string), [1, ,]],
174
+ // An index the prototype supplies, and a key past the index range, are
175
+ // members too — both need in-place mutation to build, so their rows run
176
+ // through the same three readers in `../host.proof.mjs`.
177
+ [rest({ a: number }, string), { a: 1, b: 'x' }],
178
+ [rest({ a: number }, string), { a: 1, b: 2 }],
179
+ // a stated rest with nothing to answer for: the struct kind has no length,
180
+ // so it fits whatever the rest is
181
+ [rest({ a: number }, string), { a: 1 }],
182
+ // an unconstrained rest is `open`
183
+ [rest([number], unknown), [1, 'x']],
184
+ [rest({ a: number }, unknown), { a: 1, b: 'x' }],
185
+ // an empty one is the bare form, so the length is bounded again
186
+ [rest([number], never), [1, ,]],
187
+ [rest([number], or()), [1, ,]],
188
+ [rest([number], [or()]), [1, ,]],
189
+ [rest([number], [or()]), [1, 2]],
190
+ // …and a rest the conversion keeps is not empty, however few values it
191
+ // has: these two are the pair that tells the criterion from an emptiness
192
+ // analysis
193
+ [rest([number], recursiveRest), [1, ,]],
194
+ [rest([number], orCycleA), [1, ,]],
195
+ [rest([selfList0], [selfList1, never]), [undefined, ,]],
196
+ [or(number, string), true],
197
+ [or(number, string), 'hello'],
198
+ [option(number), undefined],
199
+ [option(number), null],
200
+ [{ user: { name: string, age: number } }, { user: { name: 'A', age: 'old' } }],
201
+ ]
202
+
203
+ export const proof = {
204
+ // ── the three properties this module exists for ──────────────────────────
205
+ //
206
+ // `parse` rebuilds: it materializes a declared-but-absent member as
207
+ // `undefined`, drops an undeclared one, and returns a new object. For a
208
+ // caller whose value is a document whose bytes are its identity — a
209
+ // content-addressed store, a signed payload — each of those is a
210
+ // different document. `validate` answers the same question about the
211
+ // value it was handed and hands it back.
212
+ verbatim: {
213
+ // An absent optional member stays absent. `'b' in out` is the
214
+ // assertion, not `out.b === undefined`: `parse` satisfies the latter.
215
+ absentOptionalStaysAbsent: () => {
216
+ const schema = { a: number, b: option(string) }
217
+ const input = { a: 1 }
218
+ const out = unwrap(validate(schema)(input))
219
+ assert(!('b' in out), 'an absent optional member must stay absent')
220
+ // The contrast that motivates the module.
221
+ assert('b' in unwrap(parse(schema)(input)), 'parse materializes it')
222
+ },
223
+ // An undeclared member survives — where the schema admits one at all.
224
+ // `parse` accepts the same values and does not carry the member into
225
+ // what it builds.
226
+ undeclaredMemberSurvives: () => {
227
+ const schema = open({ a: number })
228
+ const struct = { a: 1, b: 'extra' }
229
+ assertStructurallySame(unwrap(validate(schema)(struct)), { a: 1, b: 'extra' })
230
+ assert(!('b' in unwrap(parse(schema)(struct))), 'parse drops it')
231
+ // The same on the other kind: a longer array keeps its tail.
232
+ const tuple = [1, 'extra']
233
+ assertStructurallySame(unwrap(validate(open([number]))(tuple)), [1, 'extra'])
234
+ },
235
+ // On success the result *is* the argument. This is the property the
236
+ // other two follow from, and the mirror of `../parse/proof.f.mjs`'s
237
+ // `freshArray` / `freshRecord`.
238
+ referenceIdentity: () => {
239
+ /** @type {(t: Type, value: Unknown) => void} */
240
+ const same = (t, value) => {
241
+ const r = v(t)(value)
242
+ assert(r[0] === 'ok', 'expected ok')
243
+ assert(Object.is(r[1], value), 'expected the original value')
244
+ }
245
+ const arr = [1, 2, 3]
246
+ same(array(number), arr)
247
+ same([number, number, number], arr)
248
+ same(unknown, arr)
249
+ const obj = { a: 1, b: 2 }
250
+ same(record(number), obj)
251
+ same({ a: number, b: number }, obj)
252
+ same(or(string, record(number)), obj)
253
+ const nested = { xs: [{ a: 1 }] }
254
+ same({ xs: array({ a: number }) }, nested)
255
+ assert(Object.is(unwrap(validate({ xs: array({ a: number }) })(nested)).xs, nested.xs),
256
+ 'nested containers are not rebuilt either')
257
+ },
258
+ },
259
+ // Acceptance is `parse`'s, exactly: the two readers differ in what a
260
+ // success carries and in nothing else.
261
+ sameAcceptanceAsParse: () => {
262
+ for (const [t, value] of rows) {
263
+ const rv = v(t)(value)
264
+ const rp = p(t)(value)
265
+ assertEq(rv[0], rp[0], 'validate and parse must agree on acceptance')
266
+ if (rv[0] === 'error') {
267
+ assert(rp[0] === 'error', 'expected error')
268
+ assertStructurallySame(rv[1], rp[1], 'the two readers report the same error')
269
+ }
270
+ }
271
+ },
272
+ // The same table against the third reader, the data form's — the one that
273
+ // consumes `toData` output rather than the thunk graph. A schema denotes
274
+ // one set of values, so a conversion that changed which values a schema
275
+ // admits would be a bug in `toData`, and this is where it shows up: an
276
+ // open tuple whose data form was exact-length passed every row above and
277
+ // still disagreed here. Acceptance only: the data form reaches a value
278
+ // through the canonical union rather than through the schema's syntax, so
279
+ // it reports a miss as its own kind-wise failure rather than repeating
280
+ // `or`'s `no match`.
281
+ sameAcceptanceInTheDataForm: () => {
282
+ for (const [t, value] of rows) {
283
+ assertEq(d(t)(value)[0], p(t)(value)[0], 'the data form must accept what `parse` accepts')
284
+ }
285
+ },
286
+ // The two proofs above compare the readers with one another, so a
287
+ // regression all three shared would pass them both. The table's rows for
288
+ // a schema with several optional positions therefore carry their expected
289
+ // answer here as well.
290
+ //
291
+ // The rule is *per position*, not "the last one": a position is required
292
+ // exactly when its set excludes `undefined`, independently of the others.
293
+ // A dense prefix alone would only show that an optional *suffix* may be
294
+ // truncated, so the cases below also omit position 2 while position 3 is
295
+ // present, and omit a required position with everything after it present.
296
+ //
297
+ // Every case runs against the `open` form too. A stated rest is a separate
298
+ // reader on all three — `restContainerValidate`/`restContainerParse`, and
299
+ // its own conversion in the data form — and it widens *which values are
300
+ // members*, not which positions are required, so it must answer these
301
+ // identically. The one case where opening does change the answer is at the
302
+ // end.
303
+ optionalPositions: () => {
304
+ const t = /** @type {const} */ ([number, bigint, option(string), option(null)])
305
+ /** @type {(rtti: Type) => (check: (r: readonly [string, unknown]) => void) => (value: Unknown) => void} */
306
+ const every = rtti =>
307
+ check =>
308
+ value => {
309
+ for (const read of [v, p, d]) { check(read(rtti)(value)) }
310
+ }
311
+ for (const rtti of [t, open(t)]) {
312
+ const accepted = every(rtti)(assertOk)
313
+ const rejected = every(rtti)(assertError)
314
+ accepted([2, 4n]) // stops at the last required position
315
+ accepted([2, 4n, 'x']) // the first optional present
316
+ accepted([2, 4n, 'x', null]) // both present
317
+ // Omission is independent, not just truncation: an absent member
318
+ // reads as `undefined` wherever it sits, so position 2 may be
319
+ // missing while position 3 is present. A hole and an explicit
320
+ // `undefined` are the same value, so both spellings are accepted.
321
+ accepted([2, 4n, , null]) //< a hole at position 2
322
+ accepted([2, 4n, undefined, null]) //< the same value, spelled densely
323
+ rejected([2]) // `bigint` excludes `undefined`
324
+ rejected([2, 4n, 5]) // an optional that is present is still checked
325
+ // The mirror of the two rows above: `bigint` excludes `undefined`,
326
+ // so omitting position 1 fails however much of the rest is present.
327
+ rejected([2, , 'x', null]) //< a hole at position 1
328
+ }
329
+ // What opening does change: an element past the declared positions is
330
+ // a member of the open set and not of the bare, closed one.
331
+ const extra = /** @type {const} */ ([2, 4n, 'x', null, 'extra'])
332
+ every(t)(assertError)(extra)
333
+ every(open(t))(assertOk)(extra)
334
+ // How stopping short composes with running long: the two rules are
335
+ // independent, so the open form's accepted lengths run from the last
336
+ // required position upwards without a gap or a cap — 2, 3, 4, 5, and
337
+ // on. The bare form caps the top at the declared count and leaves the
338
+ // bottom where it is.
339
+ every(open(t))(assertOk)([2, 4n, 'x', null, 'a', 'b'])
340
+ },
341
+ // An interior omittable position may be absent with a *required* position
342
+ // after it, which is the sharpest witness that absence is per position
343
+ // rather than truncation: `optionalPositions`'s hole is followed by
344
+ // another omittable position, so it cannot say this. Absence is also
345
+ // positional, not a shift — `[, 5]` holds `5` at position 1 and is
346
+ // accepted, while `[5]` holds it at position 0 and is not.
347
+ interiorOptionBeforeRequired: () => {
348
+ const t = /** @type {const} */ ([option(string), number])
349
+ /** @type {(rtti: Type) => (check: (r: readonly [string, unknown]) => void) => (value: Unknown) => void} */
350
+ const every = rtti =>
351
+ check =>
352
+ value => {
353
+ for (const read of [v, p, d]) { check(read(rtti)(value)) }
354
+ }
355
+ // Open too, for the reason `optionalPositions` runs both: a stated
356
+ // rest is its own reader on all three, and this schema is not one of
357
+ // the shapes the trailing-option cases there already put through it.
358
+ for (const rtti of [t, open(t)]) {
359
+ every(rtti)(assertOk)([, 5]) //< a hole at position 0
360
+ every(rtti)(assertOk)([undefined, 5]) //< the same value, spelled densely
361
+ every(rtti)(assertOk)(['x', 5])
362
+ every(rtti)(assertError)([5]) //< `number` at position 1 is required
363
+ }
364
+ // And the one answer opening changes here as well.
365
+ const extra = /** @type {const} */ (['x', 5, 'extra'])
366
+ every(t)(assertError)(extra)
367
+ every(open(t))(assertOk)(extra)
368
+ },
369
+ // The two tables above pin that the three readers *agree*; these pin what
370
+ // they agree on, which is what the changelog entry claims.
371
+ sparseTuple: {
372
+ holeIsDeclaredUndefined: () => {
373
+ assertError(validate([, number])([9, 5]))
374
+ assertOk(validate([, number])([undefined, 5]))
375
+ assertError(validate(new Array(1))([1, 2, 3]))
376
+ assertOk(validate(new Array(1))([undefined]))
377
+ },
378
+ // A hole is a position, so a sparse schema is as long as it looks:
379
+ // `declared.length` is the schema's length, not its key count.
380
+ arityIsTheSchemaLength: () => {
381
+ assertOk(validate(new Array(1))([undefined]))
382
+ assertError(validate(new Array(1))([1]))
383
+ },
384
+ // A tuple schema is read by index, so `foo` declares no position — it
385
+ // is not matched against anything, and on the value side it is an
386
+ // undeclared member like any other, which the closed form rejects and
387
+ // `open` admits.
388
+ nonIndexPropertyIsNotDeclared: () => {
389
+ const schema = Object.assign([number], { foo: string })
390
+ assertOk(validate(schema)([1]))
391
+ assertError(validate(schema)(Object.assign([1], { foo: 'x' })))
392
+ assertOk(validate(open(schema))(Object.assign([1], { foo: 'x' })))
393
+ assertError(validate(schema)(['x']))
394
+ },
395
+ },
396
+ boolean: {
397
+ ok: () => {
398
+ /** @typedef {Assert<Equal<Ts<typeof boolean>, boolean>>} _RoundTrip */
399
+ assertOk(validate(boolean)(true))
400
+ assertOk(validate(boolean)(false))
401
+ },
402
+ error: () => {
403
+ assertError(validate(boolean)(0))
404
+ assertError(validate(boolean)('true'))
405
+ assertError(validate(boolean)(null))
406
+ },
407
+ },
408
+ number: {
409
+ ok: () => {
410
+ /** @typedef {Assert<Equal<Ts<typeof number>, number>>} _RoundTrip */
411
+ assertOk(validate(number)(42))
412
+ },
413
+ error: () => {
414
+ assertError(validate(number)('42'))
415
+ assertError(validate(number)(42n))
416
+ },
417
+ },
418
+ string: {
419
+ ok: () => {
420
+ /** @typedef {Assert<Equal<Ts<typeof string>, string>>} _RoundTrip */
421
+ assertOk(validate(string)('hello'))
422
+ },
423
+ error: () => {
424
+ assertError(validate(string)(42))
425
+ assertError(validate(string)(null))
426
+ },
427
+ },
428
+ bigint: {
429
+ ok: () => {
430
+ /** @typedef {Assert<Equal<Ts<typeof bigint>, bigint>>} _RoundTrip */
431
+ assertOk(validate(bigint)(4n))
432
+ },
433
+ error: () => {
434
+ assertError(validate(bigint)(4))
435
+ assertError(validate(bigint)('4'))
436
+ },
437
+ },
438
+ unknown: {
439
+ ok: () => {
440
+ /** @typedef {Assert<Equal<Ts<typeof unknown>, DjsUnknown>>} _RoundTrip */
441
+ assertOk(validate(unknown)(null))
442
+ assertOk(validate(unknown)(42))
443
+ assertOk(validate(unknown)('hello'))
444
+ assertOk(validate(unknown)(true))
445
+ assertOk(validate(unknown)({}))
446
+ assertOk(validate(unknown)([]))
447
+ },
448
+ },
449
+ const: {
450
+ null: {
451
+ ok: () => assertOk(validate(null)(null)),
452
+ error: () => {
453
+ assertError(validate(null)(undefined))
454
+ assertError(validate(null)(0))
455
+ },
456
+ },
457
+ undefined: {
458
+ ok: () => assertOk(validate(undefined)(undefined)),
459
+ error: () => assertError(validate(undefined)(null)),
460
+ },
461
+ number: {
462
+ ok: () => assertOk(validate(42)(42)),
463
+ error: () => assertError(validate(42)(43)),
464
+ },
465
+ nan: {
466
+ ok: () => assertOk(validate(NaN)(NaN)),
467
+ error: () => {
468
+ assertError(validate(NaN)(0))
469
+ assertError(validate(0)(NaN))
470
+ assertError(validate(42)(NaN))
471
+ },
472
+ },
473
+ infinity: {
474
+ ok: () => {
475
+ assertOk(validate(Infinity)(Infinity))
476
+ assertOk(validate(-Infinity)(-Infinity))
477
+ },
478
+ error: () => {
479
+ assertError(validate(Infinity)(-Infinity))
480
+ assertError(validate(Infinity)(0))
481
+ },
482
+ },
483
+ signedZero: {
484
+ // `Object.is` distinguishes +0 and -0; `===` treats them equal.
485
+ distinct: () => {
486
+ assertError(validate(0)(-0))
487
+ assertError(validate(-0)(0))
488
+ },
489
+ self: () => {
490
+ assertOk(validate(0)(0))
491
+ assertOk(validate(-0)(-0))
492
+ },
493
+ },
494
+ string: {
495
+ ok: () => assertOk(validate('hello')('hello')),
496
+ error: () => assertError(validate('hello')('world')),
497
+ },
498
+ bigint: {
499
+ ok: () => assertOk(validate(7n)(7n)),
500
+ error: () => assertError(validate(7n)(8n)),
501
+ },
502
+ boolean: {
503
+ ok: () => assertOk(validate(true)(true)),
504
+ error: () => assertError(validate(true)(false)),
505
+ },
506
+ tuple: {
507
+ ok: () => assertStructurallySame(
508
+ unwrap(validate([42, 'hello'])([42, 'hello'])), [42, 'hello']),
509
+ // A tuple is CLOSED, so a longer array is not one of its values —
510
+ // see "Structs and tuples are closed" in ../README.md. Under
511
+ // `open` it is, and it comes back as it went in.
512
+ extraItemsRejected: () => {
513
+ const long = [42, 1, 2, 3]
514
+ assertError(validate([42])(long))
515
+ assert(Object.is(unwrap(validate(open([42]))(long)), long),
516
+ 'the longer array comes back as it went in')
517
+ },
518
+ // An absent member reads as `undefined`, so a position is required
519
+ // exactly when its set excludes `undefined` — and nothing is
520
+ // filled in, so the array keeps its length.
521
+ shortArrayKeepsItsLength: () => {
522
+ const short = [42]
523
+ const out = unwrap(validate([number, option(string)])(short))
524
+ assert(Object.is(out, short), 'expected the original array')
525
+ assertEq(short.length, 1, 'no gap is filled')
526
+ },
527
+ empty: () => assertOk(validate([])([])),
528
+ error: () => {
529
+ assertError(validate([42])([99]))
530
+ assertError(validate([42])({}))
531
+ // `42` excludes `undefined`, so position 0 is required.
532
+ assertError(validate([42])([]))
533
+ },
534
+ },
535
+ struct: {
536
+ // `validate` takes a `const` type parameter, so a struct literal
537
+ // keeps its literal members without an `@type {const}` cast at the
538
+ // call site: this is a reader for `{ a: 42, b: 'hello' }`, not for
539
+ // `{ a: number, b: string }`. Dropping the modifier is what makes
540
+ // the assertion fail.
541
+ ok: () => {
542
+ const v = validate({ a: 42, b: 'hello' })
543
+ /** @typedef {Assert<Equal<typeof v, Validate<{ readonly a: 42, readonly b: 'hello' }>>>} _ConstParameter */
544
+ assertStructurallySame(unwrap(v({ a: 42, b: 'hello' })), { a: 42, b: 'hello' })
545
+ },
546
+ error: () => {
547
+ assertError(validate({ a: 42 })({ a: 99 }))
548
+ assertError(validate({ a: 42 })([]))
549
+ },
550
+ },
551
+ },
552
+ array: {
553
+ empty: () => {
554
+ const input = /** @type {readonly number[]} */ ([])
555
+ assert(Object.is(unwrap(validate(array(number))(input)), input), 'expected the original array')
556
+ },
557
+ ok: () => assertStructurallySame(unwrap(validate(array(number))([1, 2, 3])), [1, 2, 3]),
558
+ error: () => {
559
+ assertError(validate(array(number))([1, 'two', 3]))
560
+ assertError(validate(array(number))({}))
561
+ assertError(validate(array(number))(null))
562
+ },
563
+ nested: () => {
564
+ assertOk(validate(array(array(boolean)))([[true, false], [false]]))
565
+ assertError(validate(array(array(boolean)))([[true, 42]]))
566
+ },
567
+ },
568
+ record: {
569
+ empty: () => {
570
+ const input = /** @type {{ readonly[K in string]?: number }} */ ({})
571
+ assert(Object.is(unwrap(validate(record(number))(input)), input), 'expected the original record')
572
+ },
573
+ ok: () => assertStructurallySame(
574
+ unwrap(validate(record(string))({ a: 'hello', b: 'world' })),
575
+ { a: 'hello', b: 'world' },
576
+ ),
577
+ error: () => {
578
+ assertError(validate(record(number))({ a: 1, b: 'two' }))
579
+ assertError(validate(record(number))(null))
580
+ assertError(validate(record(number))([]))
581
+ },
582
+ },
583
+ constThunk: {
584
+ primitive: () => {
585
+ const t = () => /** @type {const} */ (['const', 7n])
586
+ assertOk(validate(t)(7n))
587
+ assertError(validate(t)(8n))
588
+ },
589
+ },
590
+ or: {
591
+ consts: {
592
+ ok: () => {
593
+ const t = or(false, 42, 'hello')
594
+ assertOk(validate(t)(false))
595
+ assertOk(validate(t)(42))
596
+ assertOk(validate(t)('hello'))
597
+ },
598
+ error: () => {
599
+ const t = or(false, 42, 'hello')
600
+ assertError(validate(t)(true))
601
+ assertError(validate(t)(43))
602
+ assertError(validate(t)('world'))
603
+ assertError(validate(t)(null))
604
+ },
605
+ },
606
+ thunks: {
607
+ ok: () => {
608
+ const t = or(number, string)
609
+ assertOk(validate(t)(42))
610
+ assertOk(validate(t)('hello'))
611
+ },
612
+ error: () => {
613
+ const t = or(number, string)
614
+ assertError(validate(t)(true))
615
+ assertError(validate(t)(null))
616
+ },
617
+ },
618
+ // The first matching variant wins, and it returns the value itself —
619
+ // so, unlike `parse`, which variant matched is not observable in the
620
+ // result. `parse` here returns a length-1 array; `validate` returns
621
+ // the length-3 one it was given.
622
+ firstMatchWins: () => {
623
+ const t = or(open([number]), array(number))
624
+ const input = [1, 2, 3]
625
+ assert(Object.is(unwrap(validate(t)(input)), input), 'expected the original array')
626
+ assertStructurallySame(unwrap(parse(t)(input)), [1])
627
+ },
628
+ },
629
+ option: {
630
+ ok: () => {
631
+ const t = option(number)
632
+ assertOk(validate(t)(42))
633
+ assertOk(validate(t)(undefined))
634
+ },
635
+ error: () => {
636
+ const t = option(number)
637
+ assertError(validate(t)(null))
638
+ assertError(validate(t)('42'))
639
+ },
640
+ },
641
+ path: {
642
+ rootMismatch: () => assertErrorPath([])(validate(number)('not a number')),
643
+ arrayIndex: () => assertErrorPath(['1'])(validate(array(number))([1, 'two', 3])),
644
+ recordKey: () => assertErrorPath(['b'])(validate(record(number))({ a: 1, b: 'two', c: 3 })),
645
+ nestedArray: () => assertErrorPath(['0', '1'])(
646
+ validate(array(array(number)))([[1, 'x'], [2, 3]])
647
+ ),
648
+ tupleIndex: () => assertErrorPath(['1'])(
649
+ validate([number, number])([1, 'two'])
650
+ ),
651
+ structKey: () => assertErrorPath(['b'])(
652
+ validate({ a: number, b: number })({ a: 1, b: 'two' })
653
+ ),
654
+ deepStruct: () => {
655
+ const schema = /** @type {const} */ ({ user: { name: string, age: number } })
656
+ assertErrorPath(['user', 'age'])(validate(schema)({ user: { name: 'A', age: 'old' } }))
657
+ },
658
+ recursiveSchema: () => {
659
+ /** @typedef {readonly _A[]} _A */
660
+ const list = () => /** @type {const} */ (['array', list])
661
+ const r = validate(list)([/** @type {_A} */ (/** @type {unknown} */ ([[42]]))])
662
+ assertErrorPath(['0', '0', '0'])(r)
663
+ },
664
+ orRoot: () => assertErrorPath([])(validate(or(number, string))(true)),
665
+ },
666
+ recursive: {
667
+ arrayOfArrays: () => {
668
+ /** @typedef {readonly _A[]} _A */
669
+ const list = () => /** @type {const} */ (['array', list])
670
+ /** @typedef {Assert<Equal<_A, Ts<typeof list>>>} _ListRoundTrip */
671
+ const x = validate(list)
672
+ assertOk(x([]))
673
+ assertOk(x([[], []]))
674
+ assertOk(x([[[], []], []]))
675
+ assertError(x([42]))
676
+ assertError(x(null))
677
+ },
678
+ recordOfRecords: () => {
679
+ const tree = () => /** @type {const} */ (['record', tree])
680
+ /** @typedef {{ readonly[K in string]?: _A }} _A */
681
+ /** @typedef {Assert<Equal<_A, Ts<typeof tree>>>} _TreeRoundTrip */
682
+ const x = validate(tree)
683
+ assertOk(x({}))
684
+ assertOk(x({ a: {}, b: { c: {} } }))
685
+ assertError(x({ a: 42 }))
686
+ },
687
+ },
688
+ // A stated rest widens acceptance and nothing else: a success still carries
689
+ // the very value it was given. The acceptance half is in the table above,
690
+ // run through all three readers; what is left to pin here is that
691
+ // `validate` stays verbatim on that form too.
692
+ rest: {
693
+ verbatim: () => {
694
+ const value = [1, 'x', 'y']
695
+ assert(Object.is(unwrap(validate(rest([number], string))(value)), value),
696
+ 'the value comes back as it went in')
697
+ const struct = { a: 1, b: 'x' }
698
+ assert(Object.is(unwrap(validate(rest({ a: number }, string))(struct)), struct),
699
+ 'and so does an object with rest-matching keys')
700
+ },
701
+ // An absent optional member still stays absent — a container's rest
702
+ // says nothing about a member it declares.
703
+ absentOptionalStaysAbsent: () => {
704
+ const out = unwrap(validate({ a: number, b: option(string) })({ a: 1 }))
705
+ assert(!('b' in out), 'an absent optional member must stay absent')
706
+ },
707
+ path: () => {
708
+ assertErrorPath(['1'])(validate([number, number])([1, 'two']))
709
+ assertErrorPath(['b'])(validate(rest({ a: number }, string))({ a: 1, b: 2 }))
710
+ // The rejection of an undeclared member is about the container, so
711
+ // it is reported at the container.
712
+ assertErrorPath([])(validate({ a: number })({ a: 1, b: 2 }))
713
+ },
714
+ notAContainer: () => {
715
+ assertError(validate(rest([number], string))({}))
716
+ assertError(validate(rest({ a: number }, string))([]))
717
+ },
718
+ },
719
+ // The three-reader table pins that the readers *agree* on the empty-rest
720
+ // criterion; these assert the verdict outright, since a row alone passes
721
+ // whenever all three move together. Each spelling is named rather than
722
+ // covered by "an independently constructed empty rest": an implementation
723
+ // recognizing empty unions but not `[or()]` passes the `or()` row while
724
+ // keeping that spelling's disagreement, and one keyed on the exported
725
+ // `never`'s identity passes the converse.
726
+ emptyRests: {
727
+ dropped: () => {
728
+ for (const r of [never, or(), [or()]]) {
729
+ assertError(validate(rest([number], r))([42, ,]))
730
+ }
731
+ // The name collision: converting the rest reserves `r`, so the
732
+ // container's own rule is named `r0` — which `equal` reads as a
733
+ // different schema and `subset` both ways does not.
734
+ assertError(v(rest([selfList0], [selfList1, never]))([undefined, ,]))
735
+ },
736
+ kept: () => {
737
+ // A rest the conversion keeps is not empty however few values it
738
+ // has: `recursiveRest` catches an emptiness analysis that reaches
739
+ // container cycles, `orCycleA` one that tests the rest's own
740
+ // canonical data, and `unknown` one that reads the absence of a
741
+ // `rest` key as elimination.
742
+ // `v` rather than `validate`: `Ts<>` walks a recursive schema
743
+ // structurally, and these two exist to be recursive.
744
+ assertOk(v(rest([number], recursiveRest))([42, ,]))
745
+ assertOk(v(rest([number], orCycleA))([42, ,]))
746
+ assertOk(validate(open([]))([1]))
747
+ },
748
+ },
749
+ // The two ways an array can reach past a closed prefix, told apart. The
750
+ // acceptance table pins that the three readers agree on both; these assert
751
+ // the verdict, since a row alone passes whenever all three move together.
752
+ // The hole is rejected by *length* — it is no member, so the member check
753
+ // alone would let it through — and the explicit `undefined` by the member
754
+ // check, since it is a member and the schema declares no position for it.
755
+ // A closed tuple therefore has exactly one spelling per value.
756
+ beyondAClosedPrefix: () => {
757
+ for (const read of [v, p, d]) {
758
+ assertError(read([/** @type {const} */ (42)])([42, ,]))
759
+ assertError(read([/** @type {const} */ (42)])([42, undefined]))
760
+ assertOk(read([/** @type {const} */ (42)])([42]))
761
+ }
762
+ },
763
+ // The walk is bounded by what the value and its prototypes carry rather
764
+ // than by `length`: a sparse array as long as the index space allows
765
+ // answers at once, where materializing the range exhausted memory first.
766
+ // The verdicts are the ordinary ones — a bare tuple is too short for it,
767
+ // a rest with nothing present past the prefix admits it.
768
+ lengthDoesNotBoundTheWalk: () => {
769
+ const big = new Array(2 ** 32 - 1)
770
+ assertError(v([option(string)])(big))
771
+ assertOk(v(rest([], string))(big))
772
+ },
773
+ arrayOptional: () => {
774
+ const a = /** @type {const} */([number, option(string)])
775
+ const v = validate(a)
776
+ assertOk(v([5]))
777
+ assertError(v(["n"]))
778
+ assertOk(v([6, "3"]))
779
+ assertError(v([6, 9]))
780
+ }
781
+ }