functionalscript 0.46.1 → 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.
- package/fjs/basen/cbase32/module.f.mjs +18 -10
- package/fjs/basen/cbase32/proof.f.d.mts +3 -0
- package/fjs/basen/cbase32/proof.f.mjs +24 -2
- package/fjs/basen/proof.f.mjs +8 -16
- package/fjs/bnf/ll1/proof.f.d.mts +2 -0
- package/fjs/bnf/ll1/proof.f.mjs +33 -18
- package/fjs/bnf/module.f.d.mts +2 -2
- package/fjs/bnf/module.f.mjs +2 -2
- package/fjs/cas/proof.f.mjs +4 -26
- package/fjs/ci/common/module.f.mjs +6 -0
- package/fjs/ci/config/module.f.d.mts +7 -7
- package/fjs/ci/config/module.f.mjs +7 -7
- package/fjs/cli/module.f.mjs +14 -10
- package/fjs/crypto/vdf/module.f.d.mts +0 -1
- package/fjs/crypto/vdf/module.f.mjs +3 -14
- package/fjs/dev/module.f.mjs +10 -20
- package/fjs/djs/parser/module.f.d.mts +101 -53
- package/fjs/djs/parser/module.f.mjs +732 -525
- package/fjs/djs/parser/proof.f.d.mts +6 -0
- package/fjs/djs/parser/proof.f.mjs +315 -11
- package/fjs/djs/parser/types.d.ts +29 -2
- package/fjs/djs/tokenizer/module.f.mjs +122 -105
- package/fjs/djs/tokenizer/proof.f.mjs +17 -0
- package/fjs/djs/transpiler/module.f.mjs +4 -5
- package/fjs/djs/transpiler/proof.f.d.mts +2 -0
- package/fjs/djs/transpiler/proof.f.mjs +25 -0
- package/fjs/edag/amnesia/module.f.d.mts +20 -0
- package/fjs/edag/amnesia/module.f.mjs +303 -0
- package/fjs/edag/amnesia/proof.f.d.mts +79 -0
- package/fjs/edag/amnesia/proof.f.mjs +573 -0
- package/fjs/edag/amnesia/types.d.ts +15 -0
- package/fjs/edag/module.f.d.mts +410 -0
- package/fjs/edag/module.f.mjs +559 -0
- package/fjs/edag/proof.f.d.mts +116 -0
- package/fjs/edag/proof.f.mjs +686 -0
- package/fjs/edag/types.d.ts +55 -0
- package/fjs/effects/node/memory/module.d.mts +37 -3
- package/fjs/effects/node/memory/module.mjs +38 -4
- package/fjs/effects/node/memory/proof.d.mts +5 -2
- package/fjs/effects/node/memory/proof.mjs +36 -6
- package/fjs/effects/node/module.d.mts +9 -1
- package/fjs/effects/node/module.f.d.mts +39 -0
- package/fjs/effects/node/module.f.mjs +46 -0
- package/fjs/effects/node/module.mjs +234 -32
- package/fjs/effects/node/types.d.ts +28 -2
- package/fjs/effects/node/virtual/module.f.d.mts +19 -10
- package/fjs/effects/node/virtual/module.f.mjs +246 -32
- package/fjs/effects/node/virtual/proof.f.d.mts +17 -2
- package/fjs/effects/node/virtual/proof.f.mjs +232 -8
- package/fjs/effects/node/virtual/types.d.ts +57 -1
- package/fjs/emergent_testing/browser/proof.d.mts +55 -0
- package/fjs/emergent_testing/browser/proof.mjs +352 -0
- package/fjs/emergent_testing/browser/species.proof.d.mts +4 -0
- package/fjs/emergent_testing/browser/species.proof.mjs +45 -0
- package/fjs/emergent_testing/browser.d.mts +79 -0
- package/fjs/emergent_testing/browser.mjs +432 -0
- package/fjs/emergent_testing/proof.f.d.mts +1 -1
- package/fjs/emergent_testing/proof.f.mjs +2 -2
- package/fjs/js/string_escape/module.f.d.mts +47 -0
- package/fjs/js/string_escape/module.f.mjs +95 -0
- package/fjs/js/string_escape/proof.f.d.mts +8 -0
- package/fjs/js/string_escape/proof.f.mjs +68 -0
- package/fjs/js/tokenizer/module.f.mjs +14 -12
- package/fjs/mcp/cas/module.f.d.mts +1 -7
- package/fjs/mcp/cas/module.f.mjs +24 -15
- package/fjs/mcp/evo/module.f.mjs +9 -24
- package/fjs/mcp/module.f.d.mts +3 -1
- package/fjs/mcp/module.f.mjs +4 -2
- package/fjs/media/json/rtti/module.f.d.mts +2 -2
- package/fjs/media/json/rtti/module.f.mjs +1 -1
- package/fjs/media/json/schema/module.f.d.mts +15 -5
- package/fjs/media/json/schema/module.f.mjs +42 -15
- package/fjs/media/json/schema/proof.f.d.mts +10 -1
- package/fjs/media/json/schema/proof.f.mjs +88 -16
- package/fjs/media/json/serializer/module.f.mjs +14 -26
- package/fjs/media/json/types.d.ts +1 -1
- package/fjs/media/lock/module.f.d.mts +8 -3
- package/fjs/media/lock/module.f.mjs +9 -3
- package/fjs/media/lock/proof.f.d.mts +1 -1
- package/fjs/media/lock/proof.f.mjs +2 -2
- package/fjs/media/lock/types.d.ts +1 -3
- package/fjs/media/module.f.d.mts +20 -13
- package/fjs/media/module.f.mjs +29 -16
- package/fjs/media/note/module.f.d.mts +14 -7
- package/fjs/media/note/module.f.mjs +14 -7
- package/fjs/media/note/proof.f.mjs +1 -1
- package/fjs/media/proof.f.mjs +5 -5
- package/fjs/media/revision/module.f.d.mts +8 -2
- package/fjs/media/revision/module.f.mjs +9 -3
- package/fjs/media/revision/proof.f.mjs +1 -1
- package/fjs/media/revision/types.d.ts +1 -1
- package/fjs/media/type/module.f.d.mts +20 -0
- package/fjs/media/type/module.f.mjs +74 -0
- package/fjs/media/type/proof.f.d.mts +5 -0
- package/fjs/media/type/proof.f.mjs +24 -1
- package/fjs/module.f.mjs +6 -0
- package/fjs/path/module.f.d.mts +60 -3
- package/fjs/path/module.f.mjs +138 -14
- package/fjs/path/proof.f.d.mts +3 -0
- package/fjs/path/proof.f.mjs +298 -2
- package/fjs/protocol/json_rpc/module.f.d.mts +65 -28
- package/fjs/protocol/json_rpc/module.f.mjs +49 -13
- package/fjs/protocol/json_rpc/proof.f.d.mts +4 -0
- package/fjs/protocol/json_rpc/proof.f.mjs +29 -0
- package/fjs/protocol/json_rpc/types.d.ts +12 -2
- package/fjs/protocol/mcp/module.f.d.mts +89 -54
- package/fjs/protocol/mcp/module.f.mjs +110 -56
- package/fjs/protocol/mcp/proof.f.d.mts +13 -0
- package/fjs/protocol/mcp/proof.f.mjs +107 -7
- package/fjs/protocol/mcp/stdio/module.f.mjs +3 -4
- package/fjs/protocol/mcp/stdio/proof.f.mjs +2 -2
- package/fjs/protocol/mcp/types.d.ts +14 -2
- package/fjs/text/utf8/module.f.mjs +27 -1
- package/fjs/text/utf8/proof.f.mjs +37 -0
- package/fjs/types/array/module.f.mjs +18 -11
- package/fjs/types/array/proof.f.mjs +27 -1
- package/fjs/types/bigfloat/module.f.d.mts +48 -9
- package/fjs/types/bigfloat/module.f.mjs +147 -59
- package/fjs/types/bigfloat/proof.f.d.mts +2 -0
- package/fjs/types/bigfloat/proof.f.mjs +136 -4
- package/fjs/types/bigfloat/types.d.ts +17 -0
- package/fjs/types/function/module.f.d.mts +11 -2
- package/fjs/types/function/module.f.mjs +20 -2
- package/fjs/types/function/proof.f.mjs +5 -1
- package/fjs/types/phantom/types.d.ts +21 -0
- package/fjs/types/result/module.f.d.mts +4 -4
- package/fjs/types/result/module.f.mjs +2 -2
- package/fjs/types/result/proof.f.d.mts +6 -2
- package/fjs/types/result/proof.f.mjs +17 -1
- package/fjs/types/rtti/common/module.f.d.mts +78 -5
- package/fjs/types/rtti/common/module.f.mjs +168 -3
- package/fjs/types/rtti/common/proof.f.d.mts +14 -0
- package/fjs/types/rtti/common/proof.f.mjs +61 -2
- package/fjs/types/rtti/common/types.d.ts +23 -1
- package/fjs/types/rtti/data/module.f.d.mts +57 -1
- package/fjs/types/rtti/data/module.f.mjs +241 -47
- package/fjs/types/rtti/data/proof.f.d.mts +10 -0
- package/fjs/types/rtti/data/proof.f.mjs +272 -39
- package/fjs/types/rtti/data/types.d.ts +14 -7
- package/fjs/types/rtti/host.proof.d.mts +33 -0
- package/fjs/types/rtti/host.proof.mjs +170 -0
- package/fjs/types/rtti/module.f.d.mts +51 -6
- package/fjs/types/rtti/module.f.mjs +50 -3
- package/fjs/types/rtti/parse/module.f.d.mts +31 -24
- package/fjs/types/rtti/parse/module.f.mjs +197 -59
- package/fjs/types/rtti/parse/proof.f.d.mts +37 -2
- package/fjs/types/rtti/parse/proof.f.mjs +171 -47
- package/fjs/types/rtti/proof.f.d.mts +6 -0
- package/fjs/types/rtti/proof.f.mjs +42 -1
- package/fjs/types/rtti/ts/module.f.d.mts +9 -3
- package/fjs/types/rtti/ts/module.f.mjs +68 -15
- package/fjs/types/rtti/ts/proof.f.d.mts +42 -2
- package/fjs/types/rtti/ts/proof.f.mjs +139 -6
- package/fjs/types/rtti/ts/types.d.ts +164 -16
- package/fjs/types/rtti/types.d.ts +31 -3
- package/fjs/types/rtti/validate/module.f.d.mts +42 -25
- package/fjs/types/rtti/validate/module.f.mjs +187 -53
- package/fjs/types/rtti/validate/proof.f.d.mts +28 -2
- package/fjs/types/rtti/validate/proof.f.mjs +430 -75
- package/fjs/types/ts/types.d.ts +4 -0
- package/fjs/web/module.f.d.mts +115 -0
- package/fjs/web/module.f.mjs +581 -0
- package/fjs/web/proof.f.d.mts +39 -0
- package/fjs/web/proof.f.mjs +413 -0
- package/fjs/web/types.d.ts +39 -0
- package/fjs/website/browser-prepare.d.mts +5 -0
- package/fjs/website/browser-prepare.mjs +115 -0
- package/fjs/website/browser-source.d.mts +37 -0
- package/fjs/website/browser-source.mjs +202 -0
- package/fjs/website/browser-source.proof.d.mts +31 -0
- package/fjs/website/browser-source.proof.mjs +162 -0
- package/fjs/website/browser.d.mts +5 -0
- package/fjs/website/browser.mjs +8 -0
- package/fjs/website/module.f.d.mts +3 -3
- package/fjs/website/module.f.mjs +62 -9
- package/fjs/website/proof.f.d.mts +4 -0
- package/fjs/website/proof.f.mjs +18 -3
- package/package.json +2 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import { ValidationError, ValidateE } from '../common/types.ts'
|
|
2
|
+
* @import { ValidationError, ValidateE, Validate } from '../common/types.ts'
|
|
3
3
|
* @import { Type } from '../types.ts'
|
|
4
4
|
* @import { Equal } from '../../ts/types.ts'
|
|
5
5
|
* @import { Ts, Unknown } from '../ts/types.ts'
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
import { validate } from './module.f.mjs'
|
|
11
11
|
import { parse } from '../parse/module.f.mjs'
|
|
12
|
-
import {
|
|
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'
|
|
13
14
|
import { unwrap } from '../../result/module.f.mjs'
|
|
14
15
|
import { assert, assertEq, assertStructurallySame } from '../../../asserts/module.f.mjs'
|
|
15
16
|
|
|
@@ -35,6 +36,170 @@ const v = t => /** @type {any} */ (validate(t))
|
|
|
35
36
|
/** @type {(t: Type) => ValidateE} */
|
|
36
37
|
const p = t => /** @type {any} */ (parse(t))
|
|
37
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
|
+
|
|
38
203
|
export const proof = {
|
|
39
204
|
// ── the three properties this module exists for ──────────────────────────
|
|
40
205
|
//
|
|
@@ -55,16 +220,17 @@ export const proof = {
|
|
|
55
220
|
// The contrast that motivates the module.
|
|
56
221
|
assert('b' in unwrap(parse(schema)(input)), 'parse materializes it')
|
|
57
222
|
},
|
|
58
|
-
// An undeclared member survives
|
|
59
|
-
//
|
|
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.
|
|
60
226
|
undeclaredMemberSurvives: () => {
|
|
61
|
-
const schema = { a: number }
|
|
227
|
+
const schema = open({ a: number })
|
|
62
228
|
const struct = { a: 1, b: 'extra' }
|
|
63
229
|
assertStructurallySame(unwrap(validate(schema)(struct)), { a: 1, b: 'extra' })
|
|
64
230
|
assert(!('b' in unwrap(parse(schema)(struct))), 'parse drops it')
|
|
65
231
|
// The same on the other kind: a longer array keeps its tail.
|
|
66
232
|
const tuple = [1, 'extra']
|
|
67
|
-
assertStructurallySame(unwrap(validate([number])(tuple)), [1, 'extra'])
|
|
233
|
+
assertStructurallySame(unwrap(validate(open([number]))(tuple)), [1, 'extra'])
|
|
68
234
|
},
|
|
69
235
|
// On success the result *is* the argument. This is the property the
|
|
70
236
|
// other two follow from, and the mirror of `../parse/proof.f.mjs`'s
|
|
@@ -82,7 +248,7 @@ export const proof = {
|
|
|
82
248
|
same(unknown, arr)
|
|
83
249
|
const obj = { a: 1, b: 2 }
|
|
84
250
|
same(record(number), obj)
|
|
85
|
-
same({ a: number }, obj)
|
|
251
|
+
same({ a: number, b: number }, obj)
|
|
86
252
|
same(or(string, record(number)), obj)
|
|
87
253
|
const nested = { xs: [{ a: 1 }] }
|
|
88
254
|
same({ xs: array({ a: number }) }, nested)
|
|
@@ -91,38 +257,8 @@ export const proof = {
|
|
|
91
257
|
},
|
|
92
258
|
},
|
|
93
259
|
// Acceptance is `parse`'s, exactly: the two readers differ in what a
|
|
94
|
-
// success carries and in nothing else.
|
|
95
|
-
// openness on both, the short-array rule, primitives, `or`, and misses.
|
|
260
|
+
// success carries and in nothing else.
|
|
96
261
|
sameAcceptanceAsParse: () => {
|
|
97
|
-
/** @type {readonly (readonly [Type, Unknown])[]} */
|
|
98
|
-
const rows = [
|
|
99
|
-
[number, 42],
|
|
100
|
-
[number, '42'],
|
|
101
|
-
[string, 42],
|
|
102
|
-
[boolean, false],
|
|
103
|
-
[bigint, 7n],
|
|
104
|
-
[unknown, { a: [1, 'x'] }],
|
|
105
|
-
[/** @type {const} */ (42), 42],
|
|
106
|
-
[/** @type {const} */ (42), 43],
|
|
107
|
-
[array(number), [1, 2, 3]],
|
|
108
|
-
[array(number), [1, 'two']],
|
|
109
|
-
[array(number), {}],
|
|
110
|
-
[record(number), { a: 1 }],
|
|
111
|
-
[record(number), { a: 'one' }],
|
|
112
|
-
[record(number), []],
|
|
113
|
-
// the four openness rows
|
|
114
|
-
[[/** @type {const} */ (42)], [42, 'extra']],
|
|
115
|
-
[{ a: /** @type {const} */ (42) }, { a: 42, b: 'x' }],
|
|
116
|
-
[[number, option(string)], [42]],
|
|
117
|
-
[[/** @type {const} */ (42)], []],
|
|
118
|
-
[{ a: number, b: option(string) }, { a: 1 }],
|
|
119
|
-
[{ a: number }, { a: 'one' }],
|
|
120
|
-
[or(number, string), true],
|
|
121
|
-
[or(number, string), 'hello'],
|
|
122
|
-
[option(number), undefined],
|
|
123
|
-
[option(number), null],
|
|
124
|
-
[{ user: { name: string, age: number } }, { user: { name: 'A', age: 'old' } }],
|
|
125
|
-
]
|
|
126
262
|
for (const [t, value] of rows) {
|
|
127
263
|
const rv = v(t)(value)
|
|
128
264
|
const rp = p(t)(value)
|
|
@@ -133,6 +269,130 @@ export const proof = {
|
|
|
133
269
|
}
|
|
134
270
|
}
|
|
135
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
|
+
},
|
|
136
396
|
boolean: {
|
|
137
397
|
ok: () => {
|
|
138
398
|
/** @typedef {Assert<Equal<Ts<typeof boolean>, boolean>>} _RoundTrip */
|
|
@@ -199,15 +459,15 @@ export const proof = {
|
|
|
199
459
|
error: () => assertError(validate(undefined)(null)),
|
|
200
460
|
},
|
|
201
461
|
number: {
|
|
202
|
-
ok: () => assertOk(validate(
|
|
203
|
-
error: () => assertError(validate(
|
|
462
|
+
ok: () => assertOk(validate(42)(42)),
|
|
463
|
+
error: () => assertError(validate(42)(43)),
|
|
204
464
|
},
|
|
205
465
|
nan: {
|
|
206
466
|
ok: () => assertOk(validate(NaN)(NaN)),
|
|
207
467
|
error: () => {
|
|
208
468
|
assertError(validate(NaN)(0))
|
|
209
|
-
assertError(validate(
|
|
210
|
-
assertError(validate(
|
|
469
|
+
assertError(validate(0)(NaN))
|
|
470
|
+
assertError(validate(42)(NaN))
|
|
211
471
|
},
|
|
212
472
|
},
|
|
213
473
|
infinity: {
|
|
@@ -223,40 +483,36 @@ export const proof = {
|
|
|
223
483
|
signedZero: {
|
|
224
484
|
// `Object.is` distinguishes +0 and -0; `===` treats them equal.
|
|
225
485
|
distinct: () => {
|
|
226
|
-
assertError(validate(
|
|
486
|
+
assertError(validate(0)(-0))
|
|
227
487
|
assertError(validate(-0)(0))
|
|
228
488
|
},
|
|
229
489
|
self: () => {
|
|
230
|
-
assertOk(validate(
|
|
490
|
+
assertOk(validate(0)(0))
|
|
231
491
|
assertOk(validate(-0)(-0))
|
|
232
492
|
},
|
|
233
493
|
},
|
|
234
494
|
string: {
|
|
235
|
-
ok: () => assertOk(validate(
|
|
236
|
-
error: () => assertError(validate(
|
|
495
|
+
ok: () => assertOk(validate('hello')('hello')),
|
|
496
|
+
error: () => assertError(validate('hello')('world')),
|
|
237
497
|
},
|
|
238
498
|
bigint: {
|
|
239
|
-
ok: () => assertOk(validate(
|
|
240
|
-
error: () => assertError(validate(
|
|
499
|
+
ok: () => assertOk(validate(7n)(7n)),
|
|
500
|
+
error: () => assertError(validate(7n)(8n)),
|
|
241
501
|
},
|
|
242
502
|
boolean: {
|
|
243
|
-
ok: () => assertOk(validate(
|
|
244
|
-
error: () => assertError(validate(
|
|
503
|
+
ok: () => assertOk(validate(true)(true)),
|
|
504
|
+
error: () => assertError(validate(true)(false)),
|
|
245
505
|
},
|
|
246
506
|
tuple: {
|
|
247
|
-
ok: () =>
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
// ../README.md. Do not restore #1622's length check on the
|
|
254
|
-
// strength of `Ts<readonly [42]>` being an exact tuple; that
|
|
255
|
-
// mapping is exact only because TypeScript could not express the
|
|
256
|
-
// open one (see ../ts/types.ts `TupleTs`).
|
|
257
|
-
extraItemsAcceptedAndKept: () => {
|
|
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: () => {
|
|
258
513
|
const long = [42, 1, 2, 3]
|
|
259
|
-
|
|
514
|
+
assertError(validate([42])(long))
|
|
515
|
+
assert(Object.is(unwrap(validate(open([42]))(long)), long),
|
|
260
516
|
'the longer array comes back as it went in')
|
|
261
517
|
},
|
|
262
518
|
// An absent member reads as `undefined`, so a position is required
|
|
@@ -268,22 +524,28 @@ export const proof = {
|
|
|
268
524
|
assert(Object.is(out, short), 'expected the original array')
|
|
269
525
|
assertEq(short.length, 1, 'no gap is filled')
|
|
270
526
|
},
|
|
271
|
-
empty: () => assertOk(validate(
|
|
527
|
+
empty: () => assertOk(validate([])([])),
|
|
272
528
|
error: () => {
|
|
273
|
-
assertError(validate(
|
|
274
|
-
assertError(validate(
|
|
529
|
+
assertError(validate([42])([99]))
|
|
530
|
+
assertError(validate([42])({}))
|
|
275
531
|
// `42` excludes `undefined`, so position 0 is required.
|
|
276
|
-
assertError(validate(
|
|
532
|
+
assertError(validate([42])([]))
|
|
277
533
|
},
|
|
278
534
|
},
|
|
279
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.
|
|
280
541
|
ok: () => {
|
|
281
|
-
const
|
|
282
|
-
|
|
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' })
|
|
283
545
|
},
|
|
284
546
|
error: () => {
|
|
285
|
-
assertError(validate(
|
|
286
|
-
assertError(validate(
|
|
547
|
+
assertError(validate({ a: 42 })({ a: 99 }))
|
|
548
|
+
assertError(validate({ a: 42 })([]))
|
|
287
549
|
},
|
|
288
550
|
},
|
|
289
551
|
},
|
|
@@ -328,13 +590,13 @@ export const proof = {
|
|
|
328
590
|
or: {
|
|
329
591
|
consts: {
|
|
330
592
|
ok: () => {
|
|
331
|
-
const t = or(
|
|
593
|
+
const t = or(false, 42, 'hello')
|
|
332
594
|
assertOk(validate(t)(false))
|
|
333
595
|
assertOk(validate(t)(42))
|
|
334
596
|
assertOk(validate(t)('hello'))
|
|
335
597
|
},
|
|
336
598
|
error: () => {
|
|
337
|
-
const t = or(
|
|
599
|
+
const t = or(false, 42, 'hello')
|
|
338
600
|
assertError(validate(t)(true))
|
|
339
601
|
assertError(validate(t)(43))
|
|
340
602
|
assertError(validate(t)('world'))
|
|
@@ -358,7 +620,7 @@ export const proof = {
|
|
|
358
620
|
// result. `parse` here returns a length-1 array; `validate` returns
|
|
359
621
|
// the length-3 one it was given.
|
|
360
622
|
firstMatchWins: () => {
|
|
361
|
-
const t = or(
|
|
623
|
+
const t = or(open([number]), array(number))
|
|
362
624
|
const input = [1, 2, 3]
|
|
363
625
|
assert(Object.is(unwrap(validate(t)(input)), input), 'expected the original array')
|
|
364
626
|
assertStructurallySame(unwrap(parse(t)(input)), [1])
|
|
@@ -384,10 +646,10 @@ export const proof = {
|
|
|
384
646
|
validate(array(array(number)))([[1, 'x'], [2, 3]])
|
|
385
647
|
),
|
|
386
648
|
tupleIndex: () => assertErrorPath(['1'])(
|
|
387
|
-
validate(
|
|
649
|
+
validate([number, number])([1, 'two'])
|
|
388
650
|
),
|
|
389
651
|
structKey: () => assertErrorPath(['b'])(
|
|
390
|
-
validate(
|
|
652
|
+
validate({ a: number, b: number })({ a: 1, b: 'two' })
|
|
391
653
|
),
|
|
392
654
|
deepStruct: () => {
|
|
393
655
|
const schema = /** @type {const} */ ({ user: { name: string, age: number } })
|
|
@@ -423,4 +685,97 @@ export const proof = {
|
|
|
423
685
|
assertError(x({ a: 42 }))
|
|
424
686
|
},
|
|
425
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
|
+
}
|
|
426
781
|
}
|
package/fjs/types/ts/types.d.ts
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* @module
|
|
6
6
|
*/
|
|
7
7
|
export type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
|
|
8
|
+
export type And<A extends boolean, B extends boolean> = [
|
|
9
|
+
A,
|
|
10
|
+
B
|
|
11
|
+
] extends [true, true] ? true : false;
|
|
8
12
|
/**
|
|
9
13
|
* A `struct` field: the key, its type expression, and — when the third
|
|
10
14
|
* element is `true` — an optional-key marker (`"key"?: type`).
|