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
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime behavior of the edag `exp` schema — one section per node kind, plus
|
|
3
|
+
* a value nested through several kinds to exercise the mutual recursion.
|
|
4
|
+
* Exception: `comma` has no section of its own — its shape (`[',', exps]`) is
|
|
5
|
+
* a known-incomplete placeholder pending a redesign that can express "at
|
|
6
|
+
* least two operands, last is the result, each pre-result operand a true
|
|
7
|
+
* root" (a single-operand `,` is the identity, a reachable operand a
|
|
8
|
+
* redundant anchor — both non-canonical), not a settled node to pin. The
|
|
9
|
+
* `exps` section does validate `,`-tagged values, but only to reach `exps`,
|
|
10
|
+
* which `comma` is now the sole route to; it pins the operand array's
|
|
11
|
+
* element schema, and claims nothing about what a `,` means.
|
|
12
|
+
*
|
|
13
|
+
* @import { ValidationError } from '../types/rtti/common/types.ts'
|
|
14
|
+
* @import { Unknown } from '../types/rtti/ts/types.ts'
|
|
15
|
+
* @import { StringMap } from '../types/object/types.ts'
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { validate } from '../types/rtti/validate/module.f.mjs'
|
|
19
|
+
import { assert, assertEq, assertStructurallySame, todo } from '../asserts/module.f.mjs'
|
|
20
|
+
import {
|
|
21
|
+
exp, op0Id, op1Id, op2Id,
|
|
22
|
+
optionLambda, optionPropertyLambda, propertyLambda,
|
|
23
|
+
} from './module.f.mjs'
|
|
24
|
+
|
|
25
|
+
/** @type {(r: readonly [string, unknown]) => void} */
|
|
26
|
+
const assertOk = ([k]) => { assertEq(k, 'ok', 'expected ok') }
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `exp` is a top-level `or` trying every node kind in turn, so when a value
|
|
30
|
+
* matches none of them the reported failure is always the root (`path: []`,
|
|
31
|
+
* `message: 'no match'`) — there is no single branch whose deeper path is
|
|
32
|
+
* "the" failure. Same rule as `../types/rtti/validate/proof.f.mjs`'s `orRoot`.
|
|
33
|
+
* The three lambda schemas are `or`s too, so their failures report the same
|
|
34
|
+
* way.
|
|
35
|
+
* @type {(r: readonly [string, unknown]) => void}
|
|
36
|
+
*/
|
|
37
|
+
const assertNoMatch = r => {
|
|
38
|
+
assert(r[0] === 'error', 'expected error')
|
|
39
|
+
const e = /** @type {ValidationError} */ (r[1])
|
|
40
|
+
assertStructurallySame(e.path, [], 'unexpected error path')
|
|
41
|
+
assertEq(e.message, 'no match')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @type {(value: Unknown) => readonly [string, unknown]} */
|
|
45
|
+
const v = value => validate(exp)(value)
|
|
46
|
+
|
|
47
|
+
/** @type {(value: Unknown) => readonly [string, unknown]} */
|
|
48
|
+
const vOp0Id = value => validate(op0Id)(value)
|
|
49
|
+
|
|
50
|
+
/** @type {(value: Unknown) => readonly [string, unknown]} */
|
|
51
|
+
const vOp1Id = value => validate(op1Id)(value)
|
|
52
|
+
|
|
53
|
+
/** @type {(value: Unknown) => readonly [string, unknown]} */
|
|
54
|
+
const vOp2Id = value => validate(op2Id)(value)
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The three chain continuations, validated directly rather than only through
|
|
58
|
+
* the node that owns one. Each is a state of the two hidden-control-flow bits
|
|
59
|
+
* — a live receiver, an open short-circuit region — so which productions each
|
|
60
|
+
* admits *is* the grammar, and reaching them only through `dot`/`?.`/`?.()`
|
|
61
|
+
* would leave that untested where the states differ.
|
|
62
|
+
* @type {(value: Unknown) => readonly [string, unknown]}
|
|
63
|
+
*/
|
|
64
|
+
const vPropertyLambda = value => validate(propertyLambda)(value)
|
|
65
|
+
|
|
66
|
+
/** @type {(value: Unknown) => readonly [string, unknown]} */
|
|
67
|
+
const vOptionLambda = value => validate(optionLambda)(value)
|
|
68
|
+
|
|
69
|
+
/** @type {(value: Unknown) => readonly [string, unknown]} */
|
|
70
|
+
const vOptionPropertyLambda = value => validate(optionPropertyLambda)(value)
|
|
71
|
+
|
|
72
|
+
/** Every id `op0` currently accepts — kept as a literal list, not derived
|
|
73
|
+
* from `op0Id`, so deleting one from the schema reddens exactly its own
|
|
74
|
+
* assertion below rather than silently shrinking this list too. */
|
|
75
|
+
const op0Ids = /** @type {const} */ (['undefined', 'args', 'frame'])
|
|
76
|
+
|
|
77
|
+
/** Same purpose as `op0Ids`, for `op1`. */
|
|
78
|
+
const op1Ids = /** @type {const} */ (['String', 'Number', 'neg', '!', '~'])
|
|
79
|
+
|
|
80
|
+
/** Same purpose as `op0Ids`, for `op2`. */
|
|
81
|
+
const op2Ids = /** @type {const} */ ([
|
|
82
|
+
'=>', 'own',
|
|
83
|
+
'===', '!==', '>', '>=', '<', '<=',
|
|
84
|
+
'+', '-', '*', '/', '%', '**',
|
|
85
|
+
'&', '|', '^', '<<', '>>', '>>>',
|
|
86
|
+
'&&', '||', '??',
|
|
87
|
+
])
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The naive desugaring of `a?.at` — the shape `?.` looks like it could lower
|
|
91
|
+
* to. It guards both nullish values, as `?.` does: `null?.at` is `undefined`,
|
|
92
|
+
* not a throw, so a guard on `undefined` alone would not be the desugaring of
|
|
93
|
+
* anything. Every branch is taken by `chainsJs.desugaredOptional`, which is
|
|
94
|
+
* also where the assertions live: an assertion inside a `throw` case would be
|
|
95
|
+
* masked, since a failing one throws and so reads as the expected failure.
|
|
96
|
+
*
|
|
97
|
+
* @type {(o: any) => any}
|
|
98
|
+
*/
|
|
99
|
+
const desugarOptionalAt = o => o !== null && o !== undefined ? o.at : undefined
|
|
100
|
+
|
|
101
|
+
export const proof = {
|
|
102
|
+
primitive: {
|
|
103
|
+
ok: () => {
|
|
104
|
+
assertOk(v(null))
|
|
105
|
+
assertOk(v(true))
|
|
106
|
+
assertOk(v(false))
|
|
107
|
+
assertOk(v(42))
|
|
108
|
+
assertOk(v('hello'))
|
|
109
|
+
assertOk(v(7n))
|
|
110
|
+
},
|
|
111
|
+
error: () => {
|
|
112
|
+
// `{}` is a legal `Unknown` value (an object with no properties)
|
|
113
|
+
// but matches none of `exp`'s alternatives — not a primitive,
|
|
114
|
+
// and not a tagged tuple with an index 0.
|
|
115
|
+
assertNoMatch(v({}))
|
|
116
|
+
// Bare `undefined` is not a primitive here — see `op0`.
|
|
117
|
+
// A bare `undefined` would be indistinguishable from a missing
|
|
118
|
+
// tuple position, so `['undefined']` is its own node instead.
|
|
119
|
+
assertNoMatch(v(undefined))
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
array: {
|
|
123
|
+
ok: () => {
|
|
124
|
+
assertOk(v(['[]', []]))
|
|
125
|
+
assertOk(v(['[]', [1, 'a', true]]))
|
|
126
|
+
assertOk(v(['[]', [1, ['[]', [2, 3]]]])) // an exp nested inside the elements
|
|
127
|
+
// `spread` (`['...', exp]`) is not a top-level `exp` alternative —
|
|
128
|
+
// it's only reachable as an `items` alternative here, or as a
|
|
129
|
+
// `properties` alternative below in `object`.
|
|
130
|
+
assertOk(v(['[]', [1, ['...', 2]]])) // a spread element among plain elements
|
|
131
|
+
},
|
|
132
|
+
error: () => {
|
|
133
|
+
assertNoMatch(v(['[]', 'not-an-array']))
|
|
134
|
+
assertNoMatch(v(['[]', [1, {}]]))
|
|
135
|
+
assertNoMatch(v(['[]', [['...']]])) // spread missing its operand
|
|
136
|
+
assertNoMatch(v(['[]', [['..', 2]]])) // wrong tag, not `...`
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
object: {
|
|
140
|
+
ok: () => {
|
|
141
|
+
assertOk(v(['{}', []]))
|
|
142
|
+
assertOk(v(['{}', [[':', 'a', 1], [':', 'b', 'x']]]))
|
|
143
|
+
assertOk(v(['{}', [[':', 'a', 1], ['...', 2]]])) // a spread entry among plain properties
|
|
144
|
+
},
|
|
145
|
+
error: () => {
|
|
146
|
+
assertNoMatch(v(['{}', [[':', 'a', {}]]])) // bad value
|
|
147
|
+
assertNoMatch(v(['{}', [['a', 1]]])) // missing the `:` tag
|
|
148
|
+
assertNoMatch(v(['{}', [[':', 'a']]])) // missing the value
|
|
149
|
+
assertNoMatch(v(['{}', [['...']]])) // spread missing its operand
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
// `comma` is `exps`'s only route now that `array` holds `rttiArray(items)`,
|
|
153
|
+
// so these pin `exps` — an array of `exp`, not a single one — through it.
|
|
154
|
+
// The `,` node's own contract is still unsettled (see the module note
|
|
155
|
+
// above); nothing here depends on it beyond the tag and the operand slot.
|
|
156
|
+
exps: {
|
|
157
|
+
ok: () => {
|
|
158
|
+
assertOk(v([',', []]))
|
|
159
|
+
assertOk(v([',', [1, 'a', true]]))
|
|
160
|
+
assertOk(v([',', [['[]', []]]])) // an exp nested inside the operands
|
|
161
|
+
},
|
|
162
|
+
// The operand is the array, not one `exp` in its place — the
|
|
163
|
+
// single-vs-array slip that `array` carried until this branch. A
|
|
164
|
+
// bare string is a valid `exp`, so were `exps` a single `exp` this
|
|
165
|
+
// would validate.
|
|
166
|
+
singleExpIsError: () => assertNoMatch(v([',', 'not-an-array'])),
|
|
167
|
+
error: () => assertNoMatch(v([',', [{}]])),
|
|
168
|
+
},
|
|
169
|
+
// Every tuple in the schema is closed — none says `open` — so a trailing
|
|
170
|
+
// element past what a node declares is rejected rather than ignored. That
|
|
171
|
+
// is what makes "exactly one spelling" literal instead of "one spelling up
|
|
172
|
+
// to trailing junk": an `open` tuple would let `['args', 'extra']` be a
|
|
173
|
+
// second graph for the same function, and a fourth element on a `.` node a
|
|
174
|
+
// third.
|
|
175
|
+
closed: {
|
|
176
|
+
extraTailIsError: () => {
|
|
177
|
+
assertNoMatch(v(['args', 'extra']))
|
|
178
|
+
assertNoMatch(v(['neg', 1, 'extra']))
|
|
179
|
+
assertNoMatch(v(['+', 1, 2, 3]))
|
|
180
|
+
assertNoMatch(v(['[]', [], 'extra']))
|
|
181
|
+
assertNoMatch(v(['{}', [], 'extra']))
|
|
182
|
+
assertNoMatch(v(['[]', [['...', 1, 'extra']]]))
|
|
183
|
+
assertNoMatch(v(['{}', [[':', 'a', 1, 'extra']]]))
|
|
184
|
+
assertNoMatch(v([',', [], 'extra']))
|
|
185
|
+
assertNoMatch(v(['()', 'f', 1, 'extra']))
|
|
186
|
+
assertNoMatch(v(['.', 'a', 'b', null, 'extra']))
|
|
187
|
+
assertNoMatch(v(['?.', 'a', 'b', null, 'extra']))
|
|
188
|
+
assertNoMatch(v(['?.()', 'f', 1, null, 'extra']))
|
|
189
|
+
assertNoMatch(v(['.', 'a', 'b', ['|()', 1, null, 'extra']]))
|
|
190
|
+
assertNoMatch(v(['.', 'a', 'b', ['|?.()', 1, null, 'extra']]))
|
|
191
|
+
assertNoMatch(v(['?.', 'a', 'b', ['|.', 'c', null, 'extra']]))
|
|
192
|
+
assertNoMatch(v(['?.', 'a', 'b', ['|!()', 1, null, 'extra']]))
|
|
193
|
+
assertNoMatch(v(['.', 'a', ['Number', 1, 'extra'], null]))
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
dot: {
|
|
197
|
+
ok: () => {
|
|
198
|
+
assertOk(v(['.', 'a', 'b', null]))
|
|
199
|
+
assertOk(v(['.', ['[]', [1, 2]], 0, null]))
|
|
200
|
+
// `index`'s three accepted shapes, pinned explicitly: string,
|
|
201
|
+
// number (above), and a `numberCast` (below) — not `boolean`
|
|
202
|
+
// (see `error`).
|
|
203
|
+
assertOk(v(['.', 'a', ['Number', 1], null]))
|
|
204
|
+
},
|
|
205
|
+
// `index` — string, number, or `numberCast` — never admitted
|
|
206
|
+
// `undefined`, so a missing index has always been a real error.
|
|
207
|
+
missingIndexIsError: () => assertNoMatch(v(['.', 'a', null, null])),
|
|
208
|
+
// The continuation is not optional. `null` says "the receiver is
|
|
209
|
+
// dropped here"; a missing position says nothing, and reads as
|
|
210
|
+
// `undefined`, which no lambda admits.
|
|
211
|
+
missingTailIsError: () => assertNoMatch(v(['.', 'a', 'b'])),
|
|
212
|
+
error: () => {
|
|
213
|
+
assertNoMatch(v(['x', 'a', 'b', null]))
|
|
214
|
+
assertNoMatch(v(['.', {}, 'b', null]))
|
|
215
|
+
// `index` excludes `boolean` on purpose — not narrowed to just
|
|
216
|
+
// `string`/`number` by accident.
|
|
217
|
+
assertNoMatch(v(['.', 'a', true, null]))
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
// The chain continuations. There are three because a chain carries two
|
|
221
|
+
// bits — a live receiver (P) and an open short-circuit region (O) — and
|
|
222
|
+
// `00` is the definition of a node boundary, so the fourth cell is an
|
|
223
|
+
// `Exp`. A lambda is never an `exp` (`notAnExp`), and each state admits
|
|
224
|
+
// exactly the steps whose nesting would be observable there.
|
|
225
|
+
lambdas: {
|
|
226
|
+
// `propertyLambda` — P live, no region. Only a call can be here,
|
|
227
|
+
// because only a call spends a receiver; `|()` is terminal and
|
|
228
|
+
// `|?.()` opens a region that owns the rest of the chain.
|
|
229
|
+
propertyLambda: () => {
|
|
230
|
+
assertOk(vPropertyLambda(null))
|
|
231
|
+
assertOk(vPropertyLambda(['|()', 1, null]))
|
|
232
|
+
assertOk(vPropertyLambda(['|?.()', 1, null]))
|
|
233
|
+
assertOk(vPropertyLambda(['|?.()', 1, ['|.', 'c', null]]))
|
|
234
|
+
// No `|.`: a property step here would waste the receiver with no
|
|
235
|
+
// region to keep it in, so `a.b.c` nests `.` nodes instead. That
|
|
236
|
+
// absence is what gives a plain property path one spelling.
|
|
237
|
+
assertNoMatch(vPropertyLambda(['|.', 'c', null]))
|
|
238
|
+
// No `|!()`: there is no open region for it to close.
|
|
239
|
+
assertNoMatch(vPropertyLambda(['|!()', 1, null]))
|
|
240
|
+
},
|
|
241
|
+
// `optionLambda` — a plain value inside a region. A call stays in the
|
|
242
|
+
// region (it must, or the region would not cover it) and a property
|
|
243
|
+
// step hands a receiver on within it.
|
|
244
|
+
optionLambda: () => {
|
|
245
|
+
assertOk(vOptionLambda(null))
|
|
246
|
+
assertOk(vOptionLambda(['|()', 1, null]))
|
|
247
|
+
assertOk(vOptionLambda(['|.', 'c', null]))
|
|
248
|
+
assertOk(vOptionLambda(['|.', 'c', ['|!()', 1, null]]))
|
|
249
|
+
// Neither `|?.()` nor `|!()` is here: with the receiver already
|
|
250
|
+
// spent, guarding or closing at this point protects nothing a
|
|
251
|
+
// nested node would not protect equally.
|
|
252
|
+
assertNoMatch(vOptionLambda(['|?.()', 1, null]))
|
|
253
|
+
assertNoMatch(vOptionLambda(['|!()', 1, null]))
|
|
254
|
+
},
|
|
255
|
+
// `optionPropertyLambda` — both bits live, so every production is
|
|
256
|
+
// here: the three ways a call can relate to its region, plus the
|
|
257
|
+
// property step the region will not let leave.
|
|
258
|
+
optionPropertyLambda: () => {
|
|
259
|
+
assertOk(vOptionPropertyLambda(null))
|
|
260
|
+
assertOk(vOptionPropertyLambda(['|()', 1, null]))
|
|
261
|
+
assertOk(vOptionPropertyLambda(['|.', 'c', null]))
|
|
262
|
+
assertOk(vOptionPropertyLambda(['|?.()', 1, null]))
|
|
263
|
+
assertOk(vOptionPropertyLambda(['|!()', 1, null]))
|
|
264
|
+
// `|.` hands the region back to this same state, so every
|
|
265
|
+
// production above is reachable one property step further in —
|
|
266
|
+
// `a?.b.c?.(...d)` is the guarded call through a `|.`.
|
|
267
|
+
assertOk(vOptionPropertyLambda(['|.', 'c', ['|?.()', 1, null]]))
|
|
268
|
+
assertOk(vOptionPropertyLambda(['|.', 'c', ['|.', 'd', null]]))
|
|
269
|
+
},
|
|
270
|
+
// `|.` takes an `index` and the call steps take an `exp`, the same
|
|
271
|
+
// operand schemas the nodes use — so a general `exp` in a naming
|
|
272
|
+
// position is rejected where it is accepted in an argument one.
|
|
273
|
+
operandSchemas: () => {
|
|
274
|
+
assertOk(vOptionLambda(['|.', 0, null]))
|
|
275
|
+
assertOk(vOptionLambda(['|.', ['Number', 1], null]))
|
|
276
|
+
assertNoMatch(vOptionLambda(['|.', ['[]', []], null]))
|
|
277
|
+
assertOk(vOptionLambda(['|()', ['[]', [1, 2]], null]))
|
|
278
|
+
},
|
|
279
|
+
// A lambda only means anything as the continuation of a chain node:
|
|
280
|
+
// it takes its input implicitly, so on its own it is not an `exp` and
|
|
281
|
+
// cannot be lifted out as a shared node. The `|` prefix is what makes
|
|
282
|
+
// that statable — see `tagsAreDisjoint`.
|
|
283
|
+
notAnExp: () => {
|
|
284
|
+
assertNoMatch(v(['|.', 'b', null]))
|
|
285
|
+
assertNoMatch(v(['|()', 1, null]))
|
|
286
|
+
assertNoMatch(v(['|?.()', 1, null]))
|
|
287
|
+
assertNoMatch(v(['|!()', 1, null]))
|
|
288
|
+
},
|
|
289
|
+
// The prefix is a correctness requirement, not a readability one.
|
|
290
|
+
// Unprefixed, `['()', f, null]` would be both a `call` — call `f`
|
|
291
|
+
// with `null` as its arguments — and an `optionLambda` — call the
|
|
292
|
+
// chain's value with `f` as its arguments, and stop. Equal length, so
|
|
293
|
+
// closedness could not have separated them — it bounds a tuple's
|
|
294
|
+
// length and says nothing about its tag; only disjoint vocabularies
|
|
295
|
+
// can, and these two assertions are that disjointness.
|
|
296
|
+
tagsAreDisjoint: () => {
|
|
297
|
+
assertOk(v(['()', 'f', null]))
|
|
298
|
+
assertNoMatch(vOptionLambda(['()', 'f', null]))
|
|
299
|
+
assertNoMatch(v(['|()', 'f', null]))
|
|
300
|
+
},
|
|
301
|
+
// Uniform arity is the other half. `propertyLambda`'s `|()` is
|
|
302
|
+
// terminal, and it says so with an explicit `null` rather than by
|
|
303
|
+
// being one element shorter: a two-element terminal handed a real
|
|
304
|
+
// continuation would validate with the rest silently dropped.
|
|
305
|
+
terminalsAreExplicit: () => {
|
|
306
|
+
assertNoMatch(vPropertyLambda(['|()', 1]))
|
|
307
|
+
assertNoMatch(vPropertyLambda(['|()', 1, ['|.', 'c', null]]))
|
|
308
|
+
assertNoMatch(vOptionPropertyLambda(['|!()', 1]))
|
|
309
|
+
assertNoMatch(vOptionPropertyLambda(['|!()', 1, ['|.', 'c', null]]))
|
|
310
|
+
},
|
|
311
|
+
missingTailIsError: () => {
|
|
312
|
+
assertNoMatch(vOptionPropertyLambda(['|.', 'c']))
|
|
313
|
+
assertNoMatch(vOptionPropertyLambda(['|()', 1]))
|
|
314
|
+
assertNoMatch(vOptionPropertyLambda(['|?.()', 1]))
|
|
315
|
+
},
|
|
316
|
+
// Each tag is a real constraint, not a stand-in for `string`: a node
|
|
317
|
+
// tag in a step position is rejected, and so is an unknown one.
|
|
318
|
+
unknownOpIsRejected: () => {
|
|
319
|
+
assertNoMatch(vOptionPropertyLambda(['.', 'b', null]))
|
|
320
|
+
assertNoMatch(vOptionPropertyLambda(['|.z', 'b', null]))
|
|
321
|
+
assertNoMatch(vOptionPropertyLambda('xyz'))
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
call: {
|
|
325
|
+
ok: () => {
|
|
326
|
+
assertOk(v(['()', 'f', ['[]', []]]))
|
|
327
|
+
assertOk(v(['()', ['.', 'o', 'k', null], 1])) // (0, o.k)(...args)
|
|
328
|
+
assertOk(v(['()', 'f', ['[]', [1, 2]]]))
|
|
329
|
+
},
|
|
330
|
+
// A missing argument operand reads as `undefined` — an error, same
|
|
331
|
+
// as `op1`/`op2`'s `missingTailIsError`.
|
|
332
|
+
missingTailIsError: () => assertNoMatch(v(['()', 'f'])),
|
|
333
|
+
// `()` no longer carries a chain operand: a call with a receiver is a
|
|
334
|
+
// `.` node owning its call, so the three-element shape is the whole
|
|
335
|
+
// node and a `lambdas`-era graph is simply not one.
|
|
336
|
+
lambdasShapeIsError: () => assertNoMatch(v(['()', 'f', [], 1])),
|
|
337
|
+
},
|
|
338
|
+
optionDot: {
|
|
339
|
+
ok: () => {
|
|
340
|
+
assertOk(v(['?.', 'a', 'b', null]))
|
|
341
|
+
assertOk(v(['?.', 'a', ['Number', 1], null]))
|
|
342
|
+
assertOk(v(['?.', 'a', 'b', ['|.', 'c', null]]))
|
|
343
|
+
},
|
|
344
|
+
// Same three `index` shapes as `.`, `boolean` excluded the same way.
|
|
345
|
+
error: () => {
|
|
346
|
+
assertNoMatch(v(['?.', 'a', true, null]))
|
|
347
|
+
assertNoMatch(v(['?.z', 'a', 'b', null]))
|
|
348
|
+
},
|
|
349
|
+
// The continuation is required — `null` says "the optional region
|
|
350
|
+
// ends here", a missing position says nothing.
|
|
351
|
+
missingTailIsError: () => assertNoMatch(v(['?.', 'a', 'b'])),
|
|
352
|
+
},
|
|
353
|
+
optionCall: {
|
|
354
|
+
ok: () => {
|
|
355
|
+
assertOk(v(['?.()', 'f', 1, null]))
|
|
356
|
+
assertOk(v(['?.()', 'f', ['[]', [1]], ['|()', 2, null]]))
|
|
357
|
+
},
|
|
358
|
+
// One continuation, not two: the callee is an ordinary expression, so
|
|
359
|
+
// there is no pre-call chain for this node to own.
|
|
360
|
+
missingTailIsError: () => {
|
|
361
|
+
assertNoMatch(v(['?.()', 'f', 1]))
|
|
362
|
+
assertNoMatch(v(['?.()', 'f']))
|
|
363
|
+
},
|
|
364
|
+
error: () => assertNoMatch(v(['?.()', 'f', [], 1, []])),
|
|
365
|
+
},
|
|
366
|
+
// One entry per JS spelling whose grouping or hidden control flow the
|
|
367
|
+
// grammar exists to distinguish — the shape only, since what each
|
|
368
|
+
// denotes is the JSDoc on the nodes in `./module.f.mjs` and the executor
|
|
369
|
+
// proofs in `./amnesia/proof.f.mjs`, and lowering these spellings is
|
|
370
|
+
// `../djs/todo/compile-modules-to-edag.md`. Read as pairs: the members
|
|
371
|
+
// of a pair differ in JS, so they must differ here too.
|
|
372
|
+
chains: {
|
|
373
|
+
// A receiver is born in a `.` (or `?.`) node and spent by the call
|
|
374
|
+
// that node owns. Reaching the call through a complete node instead
|
|
375
|
+
// is the detached spelling, and a different graph.
|
|
376
|
+
receiver: () => {
|
|
377
|
+
assertOk(v(['.', 'a', 'b', ['|()', 'args', null]])) // a.b(...args)
|
|
378
|
+
assertOk(v(['()', ['.', 'a', 'b', null], 'args'])) // (0, a.b)(...args)
|
|
379
|
+
assertOk(v(['.', 'a', 'b', ['|?.()', 'args', null]])) // a.b?.(...args)
|
|
380
|
+
assertOk(v(['?.()', 'a', 'args', null])) // a?.(...args)
|
|
381
|
+
// (a?.(...args))(...args2)
|
|
382
|
+
assertOk(v(['()', ['?.()', 'a', 'args', null], 'args2']))
|
|
383
|
+
},
|
|
384
|
+
// A plain property path nests, because `propertyLambda` has no `|.`
|
|
385
|
+
// production — so `a.b.c` has exactly one spelling and the dead-prefix
|
|
386
|
+
// rule needs no lowering pass to hold.
|
|
387
|
+
propertyPath: () => {
|
|
388
|
+
assertOk(v(['.', ['.', 'a', 'b', null], 'c', null])) // a.b.c
|
|
389
|
+
// a.b(...args).c — the inner call is the inner node's business.
|
|
390
|
+
assertOk(v(['.', ['.', 'a', 'b', ['|()', 'args', null]], 'c', null]))
|
|
391
|
+
},
|
|
392
|
+
// An optional region is one continuation chain, however long, and
|
|
393
|
+
// grouping is what ends it: `a?.b.c` skips `.c` on a nullish `a`,
|
|
394
|
+
// `(a?.b).c` throws there — one node against two.
|
|
395
|
+
optionalRegion: () => {
|
|
396
|
+
assertOk(v(['?.', 'a', 'b', null])) // a?.b
|
|
397
|
+
assertOk(v(['?.', 'a', 'b', ['|.', 'c', null]])) // a?.b.c
|
|
398
|
+
assertOk(v(['.', ['?.', 'a', 'b', null], 'c', null])) // (a?.b).c
|
|
399
|
+
// a?.b.c(...args) — the call is inside the region.
|
|
400
|
+
assertOk(v(['?.', 'a', 'b', ['|.', 'c', ['|()', 'args', null]]]))
|
|
401
|
+
// (a?.b).c(...args) — the parens ended it, so a `.` node owns the
|
|
402
|
+
// call and `a?.b` is a complete node under it.
|
|
403
|
+
assertOk(v(['.', ['?.', 'a', 'b', null], 'c', ['|()', 'args', null]]))
|
|
404
|
+
// a?.b(...args).c(...args2) — one region across two calls.
|
|
405
|
+
assertOk(v(['?.', 'a', 'b',
|
|
406
|
+
['|()', 'args', ['|.', 'c', ['|()', 'args2', null]]]]))
|
|
407
|
+
// a?.(...args).c and a?.(...args)(...args2)
|
|
408
|
+
assertOk(v(['?.()', 'a', 'args', ['|.', 'c', null]]))
|
|
409
|
+
assertOk(v(['?.()', 'a', 'args', ['|()', 'args2', null]]))
|
|
410
|
+
},
|
|
411
|
+
// The three ways a call can relate to the region around it — the
|
|
412
|
+
// complete taxonomy, and the reason `|!()` is a tag of its own.
|
|
413
|
+
callsAgainstTheRegion: () => {
|
|
414
|
+
assertOk(v(['?.', 'a', 'b', ['|()', 'args', null]])) // a?.b(...args)
|
|
415
|
+
assertOk(v(['?.', 'a', 'b', ['|?.()', 'args', null]])) // a?.b?.(...args)
|
|
416
|
+
assertOk(v(['?.', 'a', 'b', ['|!()', 'args', null]])) // (a?.b)(...args)
|
|
417
|
+
// (a?.b.c)(...args) — the region closes after a property step,
|
|
418
|
+
// which is the same `|!()` one step further in.
|
|
419
|
+
assertOk(v(['?.', 'a', 'b', ['|.', 'c', ['|!()', 'args', null]]]))
|
|
420
|
+
// a?.b.c?.(...args) — and so is the guarded call.
|
|
421
|
+
assertOk(v(['?.', 'a', 'b', ['|.', 'c', ['|?.()', 'args', null]]]))
|
|
422
|
+
},
|
|
423
|
+
// The operands an optional node skips on its nullish branch have to
|
|
424
|
+
// be operands *of* that node, which is what makes `k`/`a`
|
|
425
|
+
// observably unevaluated: `a?.[k]` and `f?.(...a)`.
|
|
426
|
+
skippedOperands: () => {
|
|
427
|
+
assertOk(v(['?.', 'a', ['Number', 'k'], null])) // a?.[k]
|
|
428
|
+
assertOk(v(['?.()', 'f', 'a', null])) // f?.(...a)
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
// The four duplicate families a flat step array admitted are not
|
|
432
|
+
// forbidden here — they cannot be written. Each is one production the
|
|
433
|
+
// grammar does not have, and each `assertNoMatch` is the family it kills.
|
|
434
|
+
unspellable: {
|
|
435
|
+
// `a?.b?.c` — no lambda has a `?.` production at all; `?.` is only
|
|
436
|
+
// ever a node tag, so a guarded property access always starts a node.
|
|
437
|
+
optionalPropertyStep: () => {
|
|
438
|
+
assertNoMatch(v(['?.', 'a', 'b', ['|?.', 'c', null]]))
|
|
439
|
+
assertOk(v(['?.', ['?.', 'a', 'b', null], 'c', null])) // the spelling
|
|
440
|
+
},
|
|
441
|
+
// `a.b(...c)?.d` — `propertyLambda`'s `|()` is terminal, so the chain
|
|
442
|
+
// exits and what follows is an ordinary node over an ordinary value.
|
|
443
|
+
callTerminatesPropertyLambda: () => {
|
|
444
|
+
assertNoMatch(v(['.', 'a', 'b', ['|()', 'c', ['|.', 'd', null]]]))
|
|
445
|
+
assertOk(v(['?.', ['.', 'a', 'b', ['|()', 'c', null]], 'd', null]))
|
|
446
|
+
},
|
|
447
|
+
// `(a?.(...b))(...c)` — `optionLambda` has no `|!()`, since with the
|
|
448
|
+
// receiver already spent there is nothing for the close to keep. The
|
|
449
|
+
// outer call is a plain `()` over a complete `?.()` node.
|
|
450
|
+
closeWithoutReceiver: () => {
|
|
451
|
+
assertNoMatch(v(['?.()', 'a', 'b', ['|!()', 'c', null]]))
|
|
452
|
+
assertOk(v(['()', ['?.()', 'a', 'b', null], 'c']))
|
|
453
|
+
},
|
|
454
|
+
// `a?.b(...c)?.d` — `optionLambda` has no guarded step either, so the
|
|
455
|
+
// guarded access after the call starts its own node.
|
|
456
|
+
guardedStepAfterCall: () => {
|
|
457
|
+
assertNoMatch(v(['?.', 'a', 'b', ['|()', 'c', ['|?.()', 'd', null]]]))
|
|
458
|
+
assertOk(v(['?.()', ['?.', 'a', 'b', ['|()', 'c', null]], 'd', null]))
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
op0: {
|
|
462
|
+
ok: () => {
|
|
463
|
+
// Every id `op0` accepts, pinned individually: deleting any one
|
|
464
|
+
// of these three from `op0Id` reddens exactly this loop, not
|
|
465
|
+
// some other assertion that happens to still pass.
|
|
466
|
+
for (const id of op0Ids) {
|
|
467
|
+
assertOk(v([id]))
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
error: () => {
|
|
471
|
+
assertNoMatch(v([]))
|
|
472
|
+
assertNoMatch(v(['argz']))
|
|
473
|
+
},
|
|
474
|
+
// `op0Id` is a real constraint, not a stand-in for `string`: an id
|
|
475
|
+
// outside its three members is rejected, both directly and as part
|
|
476
|
+
// of a full `exp` value.
|
|
477
|
+
unknownIdIsRejected: () => {
|
|
478
|
+
assertNoMatch(vOp0Id('xyz'))
|
|
479
|
+
assertNoMatch(v(['xyz']))
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
op1: {
|
|
483
|
+
ok: () => {
|
|
484
|
+
// Every id `op1` accepts, pinned individually: deleting any one
|
|
485
|
+
// of these five from `op1Id` reddens exactly this loop, not
|
|
486
|
+
// some other assertion that happens to still pass.
|
|
487
|
+
for (const id of op1Ids) {
|
|
488
|
+
assertOk(v([id, 1]))
|
|
489
|
+
}
|
|
490
|
+
assertOk(v(['neg', ['neg', 1]])) // an exp nested inside the operand
|
|
491
|
+
// Composes through `exp`'s recursion like any other node.
|
|
492
|
+
assertOk(v(['[]', [['Number', 1]]]))
|
|
493
|
+
assertOk(v(['()', ['Number', 1], 2]))
|
|
494
|
+
},
|
|
495
|
+
// A missing operand reads as `undefined`, no longer a valid bare
|
|
496
|
+
// `exp` — see `op0`.
|
|
497
|
+
missingTailIsError: () => assertNoMatch(v(['neg'])),
|
|
498
|
+
error: () => assertNoMatch(v(['negz', 1])),
|
|
499
|
+
// `op1Id` is a real constraint, not a stand-in for `string`: an id
|
|
500
|
+
// outside its five members is rejected, both directly and as part
|
|
501
|
+
// of a full `exp` value.
|
|
502
|
+
unknownIdIsRejected: () => {
|
|
503
|
+
assertNoMatch(vOp1Id('xyz'))
|
|
504
|
+
assertNoMatch(v(['xyz', 1]))
|
|
505
|
+
},
|
|
506
|
+
// `own`'s point is bypassing the prototype chain — including the
|
|
507
|
+
// `__proto__` special case a computed key already avoids in JS.
|
|
508
|
+
// Demonstrates the pattern `['own', ...]` (an `op2` id) denotes;
|
|
509
|
+
// not a schema check.
|
|
510
|
+
ownJs: () => {
|
|
511
|
+
/** @type {<T>(a: StringMap<T>, k: string) => T|undefined } */
|
|
512
|
+
const own = (a, k) => Object.getOwnPropertyDescriptor(a, k)?.value
|
|
513
|
+
const a = { ['__proto__']: 42 }
|
|
514
|
+
assertEq(own(a, '__proto__'), 42)
|
|
515
|
+
assertEq(own(a, 'x'), undefined)
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
op2: {
|
|
519
|
+
ok: () => {
|
|
520
|
+
// Every id `op2` accepts, pinned individually: deleting any one
|
|
521
|
+
// of the twenty-four from `op2Id` reddens exactly this loop, not
|
|
522
|
+
// some other assertion that happens to still pass.
|
|
523
|
+
for (const id of op2Ids) {
|
|
524
|
+
assertOk(v([id, 1, 2]))
|
|
525
|
+
}
|
|
526
|
+
assertOk(v(['+', ['+', 1, 2], 3])) // an exp nested inside an operand
|
|
527
|
+
},
|
|
528
|
+
// A missing operand reads as `undefined`, no longer a valid bare
|
|
529
|
+
// `exp` — see `op0`. True whether one or both are missing.
|
|
530
|
+
missingTailIsError: () => {
|
|
531
|
+
assertNoMatch(v(['+', 1]))
|
|
532
|
+
assertNoMatch(v(['+']))
|
|
533
|
+
},
|
|
534
|
+
error: () => assertNoMatch(v(['+z', 1, 2])),
|
|
535
|
+
// Same point as `op1`'s: `op2Id` constrains membership.
|
|
536
|
+
unknownIdIsRejected: () => {
|
|
537
|
+
assertNoMatch(vOp2Id('xyz'))
|
|
538
|
+
assertNoMatch(v(['xyz', 1, 2]))
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
// The JS these nodes have to agree with, run on the host engine — the
|
|
542
|
+
// `ownJs` pattern one level up: what the vocabulary denotes, not what the
|
|
543
|
+
// schema accepts. Every spelling here is one `chains` pins the shape of,
|
|
544
|
+
// and each is why that shape is what it is. `todo()` always throws, so it
|
|
545
|
+
// doubles as an evaluation probe: reaching it is observable without any
|
|
546
|
+
// mutation, and a case that returns instead proves the operand was
|
|
547
|
+
// skipped.
|
|
548
|
+
chainsJs: {
|
|
549
|
+
// Receiver: a property reference carries its base into the call as
|
|
550
|
+
// `this`, and parentheses around the reference do not break that —
|
|
551
|
+
// only detaching the value does (`throw.detachedReceiver`). It holds
|
|
552
|
+
// across an optional link too, which is why `(a?.b)(d)` is a `?.`
|
|
553
|
+
// node with a `|!()` continuation, `['?.', a, 'b', ['|!()', d, null]]`,
|
|
554
|
+
// rather than a `()` over a complete `['?.', a, 'b', null]`: the
|
|
555
|
+
// latter would produce an ordinary value and lose the receiver.
|
|
556
|
+
receiver: () => {
|
|
557
|
+
const a = [42]
|
|
558
|
+
assertEq(a.at(0), 42)
|
|
559
|
+
assertEq((a.at)(0), 42)
|
|
560
|
+
assertEq((a?.at)(0), 42) // ['?.', a, 'at', ['|!()', …, null]]
|
|
561
|
+
assertEq((a?.at)?.(0), 42) // ['?.', a, 'at', ['|?.()', …, null]]
|
|
562
|
+
assertEq(a.at?.(0), 42) // ['.', a, 'at', ['|?.()', …, null]]
|
|
563
|
+
assertEq(a?.at?.(0), 42) // ['?.', a, 'at', ['|?.()', …, null]]
|
|
564
|
+
},
|
|
565
|
+
// Short-circuit: a nullish link skips the rest of its chain, and
|
|
566
|
+
// grouping is what ends that chain — `u?.at.name` is `undefined`
|
|
567
|
+
// where `(u?.at).name` throws (`throw.groupedOptional`), one
|
|
568
|
+
// continuation against two nodes.
|
|
569
|
+
shortCircuit: () => {
|
|
570
|
+
/** @type {any} */
|
|
571
|
+
const u = undefined
|
|
572
|
+
assertEq(u?.at, undefined)
|
|
573
|
+
assertEq(u?.at.name, undefined) // ['?.', u, 'at', ['|.', 'name', null]]
|
|
574
|
+
assertEq(u?.at?.(0), undefined)
|
|
575
|
+
// The operands on the skipped branch are never evaluated: an
|
|
576
|
+
// optional property's index, and an optional call's arguments.
|
|
577
|
+
assertEq(u?.[todo()], undefined)
|
|
578
|
+
assertEq(u?.(todo()), undefined)
|
|
579
|
+
},
|
|
580
|
+
// The quiet half of `throw.groupedOptional`: a group ends the chain,
|
|
581
|
+
// but when what follows is itself guarded the ending is unobservable.
|
|
582
|
+
// That is the parenthesis law's own escape clause, and the reason
|
|
583
|
+
// `null` is the right spelling of a bare `(a?.b)`.
|
|
584
|
+
grouping: () => {
|
|
585
|
+
/** @type {any} */
|
|
586
|
+
const u = undefined
|
|
587
|
+
assertEq((u?.at)?.name, undefined)
|
|
588
|
+
assertEq((u?.at)?.(0), undefined)
|
|
589
|
+
},
|
|
590
|
+
// What the naive desugaring of `?.` gets right, every branch of it:
|
|
591
|
+
// `undefined` on either nullish input, and on anything else the *same
|
|
592
|
+
// function* `a.at` denotes. Nothing is wrong with any of those values
|
|
593
|
+
// — what it loses is the receiver, which `throw.desugaredOptional`
|
|
594
|
+
// pins by calling the last one. That is why `?.` cannot lower to a
|
|
595
|
+
// conditional, and why `(a?.b)(d)` keeps its receiver through a
|
|
596
|
+
// `|!()` step instead of completing a `['?.', …]` node that would
|
|
597
|
+
// hand on an ordinary value.
|
|
598
|
+
desugaredOptional: () => {
|
|
599
|
+
assertEq(desugarOptionalAt(null), undefined)
|
|
600
|
+
assertEq(desugarOptionalAt(undefined), undefined)
|
|
601
|
+
// Identity, not just equality: a receiver-bound function (e.g.
|
|
602
|
+
// `o.at.bind(o)`) is `!==` to `[42].at` even though it behaves
|
|
603
|
+
// the same on `(0)`. So this line and `throw.desugaredOptional`
|
|
604
|
+
// both redden under that mutation — the identity check here,
|
|
605
|
+
// the call there.
|
|
606
|
+
assertEq(desugarOptionalAt([42]), [42].at)
|
|
607
|
+
},
|
|
608
|
+
// The two spellings that differ *only* in whether the arguments ran —
|
|
609
|
+
// `a.b(...c)` throws at the access with `c` untouched, `(a?.b)(...c)`
|
|
610
|
+
// short-circuits, evaluates `c`, and throws at the call — cannot be
|
|
611
|
+
// pinned here, nor by the node either: both readings throw, and a
|
|
612
|
+
// `throw` case is pass/fail rather than payload-inspecting. What
|
|
613
|
+
// carries that order is the shape of `callProperty` in
|
|
614
|
+
// `./amnesia/module.f.mjs`; "Where the host engines disagree" in
|
|
615
|
+
// `./README.md` states the gap. Nor can `(u?.at)(0)` be pinned: both are
|
|
616
|
+
// `|!()` terms, JavaScriptCore (so `bun test`) carries the
|
|
617
|
+
// short-circuit through the parentheses and answers `undefined` where
|
|
618
|
+
// V8 throws, so asserting either answer would redden a runner. The
|
|
619
|
+
// node is unaffected — `['?.', u, 'at', ['|!()', …, null]]` means the
|
|
620
|
+
// throwing reading — and `optionRegion.throw.closeStepOnUndefined` in
|
|
621
|
+
// `./amnesia/proof.f.mjs` pins it by evaluating the node, which is
|
|
622
|
+
// the only oracle that works on every runner. See "Chains" in
|
|
623
|
+
// `./README.md`. `throw.groupedOptional` below pins the same boundary
|
|
624
|
+
// through a property access, where every engine agrees.
|
|
625
|
+
throw: {
|
|
626
|
+
// `const at = a.at; at(0)` — the value without its receiver,
|
|
627
|
+
// the case that makes receiver state part of what a graph means.
|
|
628
|
+
detachedReceiver: () => {
|
|
629
|
+
const at = [42].at
|
|
630
|
+
return at(0)
|
|
631
|
+
},
|
|
632
|
+
// The desugaring above, called: `42` through `(a?.at)(0)`
|
|
633
|
+
// (`receiver`), a throw once a conditional has made it a value.
|
|
634
|
+
desugaredOptional: () => desugarOptionalAt([42])(0),
|
|
635
|
+
// `(u?.at).name` — the parens ended the optional chain, so
|
|
636
|
+
// `.name` runs on `undefined` instead of being skipped.
|
|
637
|
+
groupedOptional: () => {
|
|
638
|
+
/** @type {any} */
|
|
639
|
+
const u = undefined
|
|
640
|
+
return (u?.at).name
|
|
641
|
+
},
|
|
642
|
+
// The mirror of `shortCircuit`'s last two: on a non-nullish
|
|
643
|
+
// input those same operands *are* evaluated.
|
|
644
|
+
evaluatedIndex: () => [42]?.[todo()],
|
|
645
|
+
evaluatedArgument: () => [42].at?.(todo()),
|
|
646
|
+
// `(u?.(0))(1)` — the inner call already cleared the receiver, so
|
|
647
|
+
// the group's boundary carries nothing and every runner agrees.
|
|
648
|
+
// That is also why the grammar has no `|!()` in `optionLambda`:
|
|
649
|
+
// this is a plain `()` over a complete `?.()` node.
|
|
650
|
+
groupedOptionalCallOfCall: () => {
|
|
651
|
+
/** @type {any} */
|
|
652
|
+
const u = undefined
|
|
653
|
+
return (u?.(0))(1)
|
|
654
|
+
},
|
|
655
|
+
// Two shapes bun answers differently, so neither can be
|
|
656
|
+
// asserted. Both throw everywhere else; see "Chains" in
|
|
657
|
+
// `./README.md` for which is the engine and which is the
|
|
658
|
+
// transpiler, and
|
|
659
|
+
// `../../todo/blocked/bun-optional-chain-parentheses.md` for what
|
|
660
|
+
// unblocks them. The second must stay commented rather than merely
|
|
661
|
+
// fail: bun rejects it at parse, taking the file down.
|
|
662
|
+
//
|
|
663
|
+
// groupedOptionalCall: () => {
|
|
664
|
+
// /** @type {any} */
|
|
665
|
+
// const u = undefined
|
|
666
|
+
// return (u?.at)(0)
|
|
667
|
+
// },
|
|
668
|
+
//
|
|
669
|
+
// groupedOptionalTag: () => {
|
|
670
|
+
// /** @type {any} */
|
|
671
|
+
// const u = undefined
|
|
672
|
+
// return (u?.at)`tag`
|
|
673
|
+
// },
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
// `f(...args)[k](obj.a)` in AST form — exercises the mutual recursion
|
|
677
|
+
// through `exp` rather than any one node kind in isolation.
|
|
678
|
+
nested: () => {
|
|
679
|
+
const value = /** @type {const} */ (['.',
|
|
680
|
+
['()', 'f', ['args']],
|
|
681
|
+
'k',
|
|
682
|
+
['|()', ['[]', [['.', 'obj', 'a', null]]], null],
|
|
683
|
+
])
|
|
684
|
+
assertOk(v(value))
|
|
685
|
+
},
|
|
686
|
+
}
|