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.
- 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 +120 -0
- package/fjs/types/rtti/validate/module.f.mjs +351 -0
- package/fjs/types/rtti/validate/proof.f.d.mts +154 -0
- package/fjs/types/rtti/validate/proof.f.mjs +781 -0
- 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,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-native proof execution and report rendering.
|
|
3
|
+
*
|
|
4
|
+
* The module deliberately has no Node dependencies: generated applications
|
|
5
|
+
* import it directly as an ES module in the browser.
|
|
6
|
+
* Proof failures resolve the published report with `status: 'failed'`; an
|
|
7
|
+
* automated outer controller is responsible for consuming that status and
|
|
8
|
+
* choosing a nonzero process exit code.
|
|
9
|
+
*
|
|
10
|
+
* Every DOM entry point reaches the page through the `root` element it is
|
|
11
|
+
* given — `root.ownerDocument` and its `defaultView` — never through the
|
|
12
|
+
* runner realm's own `window`/`document`. A page embedding the suite in an
|
|
13
|
+
* iframe therefore renders into that frame, and a proof can drive the module
|
|
14
|
+
* with a stand-in root.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*
|
|
18
|
+
* @import { _TestAndPath } from './types.ts'
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { collectTests, fmtPath } from './module.f.mjs'
|
|
22
|
+
|
|
23
|
+
/** @type {(value: unknown) => string} */
|
|
24
|
+
const text = value => {
|
|
25
|
+
try {
|
|
26
|
+
return String(value)
|
|
27
|
+
} catch {
|
|
28
|
+
return 'Unknown thrown value'
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The message and stack to report a thrown value by.
|
|
34
|
+
*
|
|
35
|
+
* An Error thrown from another realm — an iframe, a worker — is not
|
|
36
|
+
* `instanceof Error` here, and its stack is the very thing the report exists to
|
|
37
|
+
* carry. What the fields say is therefore the test, not where the value was
|
|
38
|
+
* made: anything carrying `message` or `stack` is read as the failure it
|
|
39
|
+
* describes, and everything else by its own text.
|
|
40
|
+
*
|
|
41
|
+
* @type {(error: unknown) => readonly [string, string]}
|
|
42
|
+
*/
|
|
43
|
+
const errorDetails = error => {
|
|
44
|
+
try {
|
|
45
|
+
if (error !== null && (typeof error === 'object' || typeof error === 'function')
|
|
46
|
+
&& ('message' in error || 'stack' in error)) {
|
|
47
|
+
const { message, stack } = /** @type {{ readonly message?: unknown, readonly stack?: unknown }} */ (error)
|
|
48
|
+
const described = text(message)
|
|
49
|
+
return [described, stack === undefined ? described : text(stack)]
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// Reading the fields, and asking whether they are there at all, are
|
|
53
|
+
// user-observable operations: revoked proxies and accessors can throw
|
|
54
|
+
// while the failure is inspected.
|
|
55
|
+
}
|
|
56
|
+
const fallback = text(error)
|
|
57
|
+
return [fallback, fallback]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @typedef {{ readonly module: string, readonly path: string, readonly status: string, readonly duration: number, readonly message?: string, readonly stack?: string }} _BrowserTestResult */
|
|
61
|
+
/** @typedef {{ readonly status: string, readonly browser: string, readonly totals: { readonly tests: number, readonly passed: number, readonly failed: number }, readonly duration: number, readonly results: readonly _BrowserTestResult[] }} BrowserTestReport */
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Attaches the handlers with the intrinsic `then`, but answers with a promise
|
|
65
|
+
* of this realm instead of the one `then` returns. That result is built by
|
|
66
|
+
* `constructor[Symbol.species]`, which a promise can make an ordinary object:
|
|
67
|
+
* awaiting it would end the test before the promise it came from ever settled
|
|
68
|
+
* and put the species object itself in the report.
|
|
69
|
+
*
|
|
70
|
+
* The `then` call still throws — before either handler is attached — for a
|
|
71
|
+
* value that is not a promise or whose species construction fails, which is
|
|
72
|
+
* what `runPromise` reads.
|
|
73
|
+
*
|
|
74
|
+
* @type {(value: unknown, fulfilled: (value: unknown) => Promise<readonly _BrowserTestResult[]> | readonly _BrowserTestResult[], rejected: (error: unknown) => readonly _BrowserTestResult[]) => Promise<readonly _BrowserTestResult[]>}
|
|
75
|
+
*/
|
|
76
|
+
const subscribe = (value, fulfilled, rejected) => {
|
|
77
|
+
/** @type {(results: Promise<readonly _BrowserTestResult[]> | readonly _BrowserTestResult[]) => void} */
|
|
78
|
+
let settle = () => undefined
|
|
79
|
+
/** @type {Promise<readonly _BrowserTestResult[]>} */
|
|
80
|
+
const settled = new Promise(resolve => { settle = resolve })
|
|
81
|
+
Reflect.apply(Promise.prototype.then, value, [
|
|
82
|
+
/** @type {(value: unknown) => void} */ (resolved => settle(fulfilled(resolved))),
|
|
83
|
+
/** @type {(error: unknown) => void} */ (error => settle(rejected(error))),
|
|
84
|
+
])
|
|
85
|
+
return settled
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Reproduces the lookup `then` performs before it builds its result promise:
|
|
90
|
+
* `constructor`, then its `Symbol.species`. A genuine promise with a hostile
|
|
91
|
+
* species throws here too; an object that only claims to be a promise failed
|
|
92
|
+
* the brand check first and reads its `constructor` cleanly. That is what
|
|
93
|
+
* separates a promise nothing can subscribe to from an ordinary proof tree,
|
|
94
|
+
* once shadowing `constructor` has turned out to be impossible.
|
|
95
|
+
*
|
|
96
|
+
* @type {(value: unknown) => boolean}
|
|
97
|
+
*/
|
|
98
|
+
const speciesFails = value => {
|
|
99
|
+
try {
|
|
100
|
+
if (value === null || value === undefined) { return false }
|
|
101
|
+
const { constructor } = /** @type {{ readonly constructor?: unknown }} */ (value)
|
|
102
|
+
if (constructor === null || constructor === undefined) { return false }
|
|
103
|
+
// The species itself never matters, only whether reading it completes:
|
|
104
|
+
// that is the step `then` takes before it builds its result.
|
|
105
|
+
void /** @type {{ readonly [Symbol.species]?: unknown }} */ (constructor)[Symbol.species]
|
|
106
|
+
return false
|
|
107
|
+
} catch {
|
|
108
|
+
return true
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Runs the intrinsic Promise `then` only for genuine promises. The first call
|
|
114
|
+
* is both the native brand check and the normal await path, so arbitrary proof
|
|
115
|
+
* objects with a `then` key are never assimilated.
|
|
116
|
+
*
|
|
117
|
+
* A genuine Promise can still throw after passing the brand check if species
|
|
118
|
+
* construction fails. In that case, temporarily shadow `constructor` with the
|
|
119
|
+
* current realm's Promise and retry the same intrinsic call; the shadow is
|
|
120
|
+
* removed immediately after the handlers are attached.
|
|
121
|
+
*
|
|
122
|
+
* A promise that pins its own `constructor`, or is frozen, leaves nothing to
|
|
123
|
+
* shadow, so no subscription is possible at all. The species failure is then
|
|
124
|
+
* reported against the test that produced the promise — the same outcome
|
|
125
|
+
* `await` gives it in the Node runner — because a result nobody can observe is
|
|
126
|
+
* not a pass. A non-extensible object that merely claims to be a promise
|
|
127
|
+
* reaches the same dead end and is still walked as the proof tree it is.
|
|
128
|
+
*
|
|
129
|
+
* @type {(value: unknown, fulfilled: (value: unknown) => Promise<readonly _BrowserTestResult[]> | readonly _BrowserTestResult[], rejected: (error: unknown) => readonly _BrowserTestResult[]) => Promise<readonly _BrowserTestResult[]> | null}
|
|
130
|
+
*/
|
|
131
|
+
const runPromise = (value, fulfilled, rejected) => {
|
|
132
|
+
const call = () => subscribe(value, fulfilled, rejected)
|
|
133
|
+
try {
|
|
134
|
+
return call()
|
|
135
|
+
} catch (error) {
|
|
136
|
+
// Either `value` is not a promise and the brand check rejected it
|
|
137
|
+
// before any handler was attached, or it is a genuine promise that
|
|
138
|
+
// failed while constructing the result through Symbol.species. Only
|
|
139
|
+
// the second case is worth a retry, and `then` attaches nothing before
|
|
140
|
+
// it throws, so the retry cannot run the handlers twice.
|
|
141
|
+
try {
|
|
142
|
+
if (Object.prototype.toString.call(value) !== '[object Promise]') { return null }
|
|
143
|
+
} catch {
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
if (value === null || (typeof value !== 'object' && typeof value !== 'function')) { return null }
|
|
147
|
+
/** @type {PropertyDescriptor | undefined} */
|
|
148
|
+
let descriptor
|
|
149
|
+
try {
|
|
150
|
+
descriptor = Object.getOwnPropertyDescriptor(value, 'constructor')
|
|
151
|
+
Object.defineProperty(value, 'constructor', { value: Promise, configurable: true })
|
|
152
|
+
} catch {
|
|
153
|
+
// Nothing to shadow, so the value is whatever its own lookup says:
|
|
154
|
+
// a promise that cannot be subscribed to fails on the species error
|
|
155
|
+
// rather than passing on a result that was never awaited, and a
|
|
156
|
+
// frozen spoof is an ordinary proof tree.
|
|
157
|
+
return speciesFails(value) ? Promise.resolve(rejected(error)) : null
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
return call()
|
|
161
|
+
} catch {
|
|
162
|
+
// The intrinsic `constructor` cannot fail the retry, so the brand
|
|
163
|
+
// check did: `value` only claims to be a promise and is walked as
|
|
164
|
+
// an ordinary proof result.
|
|
165
|
+
return null
|
|
166
|
+
} finally {
|
|
167
|
+
try {
|
|
168
|
+
if (descriptor === undefined) {
|
|
169
|
+
Reflect.deleteProperty(value, 'constructor')
|
|
170
|
+
} else {
|
|
171
|
+
Object.defineProperty(value, 'constructor', descriptor)
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
// The temporary property is configurable, so ordinary objects
|
|
175
|
+
// restore cleanly. A hostile Proxy can make restoration itself
|
|
176
|
+
// observable.
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** @type {(module: string, path: readonly (string | null)[], throws: boolean, fn: () => unknown, result: (result: _BrowserTestResult) => void) => Promise<readonly _BrowserTestResult[]>} */
|
|
183
|
+
const runOne = (module, path, throws, fn, result) => {
|
|
184
|
+
const start = performance.now()
|
|
185
|
+
/** @type {(value: unknown) => Promise<readonly _BrowserTestResult[]> | readonly _BrowserTestResult[]} */
|
|
186
|
+
const passed = value => {
|
|
187
|
+
const duration = performance.now() - start
|
|
188
|
+
if (throws) {
|
|
189
|
+
const failure = { module, path: fmtPath(path), status: 'failed', duration,
|
|
190
|
+
message: 'Expected the proof to throw', stack: '' }
|
|
191
|
+
result(failure)
|
|
192
|
+
return [failure]
|
|
193
|
+
}
|
|
194
|
+
// Reading the returned tree runs user code: an enumerable getter
|
|
195
|
+
// or a proxy trap can throw. That is a failure of the test that
|
|
196
|
+
// produced the value, never of the run — a rejected run leaves the
|
|
197
|
+
// page in `running` with no report and no completion event.
|
|
198
|
+
/** @type {readonly _TestAndPath[]} */
|
|
199
|
+
let children
|
|
200
|
+
try {
|
|
201
|
+
children = collectTests([...path, null], false, value)
|
|
202
|
+
} catch (error) {
|
|
203
|
+
return failed(error)
|
|
204
|
+
}
|
|
205
|
+
return Promise.all(children.map(([childPath, child]) =>
|
|
206
|
+
runOne(module, childPath, child.throws, child.fn, result)
|
|
207
|
+
)).then(results => {
|
|
208
|
+
const success = { module, path: fmtPath(path), status: 'passed', duration }
|
|
209
|
+
result(success)
|
|
210
|
+
return [success, ...results.flat()]
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
/** @type {(error: unknown) => readonly _BrowserTestResult[]} */
|
|
214
|
+
const failed = error => {
|
|
215
|
+
const duration = performance.now() - start
|
|
216
|
+
if (throws) {
|
|
217
|
+
const success = { module, path: fmtPath(path), status: 'passed', duration }
|
|
218
|
+
result(success)
|
|
219
|
+
return [success]
|
|
220
|
+
}
|
|
221
|
+
const [message, stack] = errorDetails(error)
|
|
222
|
+
const failure = { module, path: fmtPath(path), status: 'failed', duration, message, stack }
|
|
223
|
+
result(failure)
|
|
224
|
+
return [failure]
|
|
225
|
+
}
|
|
226
|
+
// Wrap the raw return so Promise resolution does not assimilate arbitrary
|
|
227
|
+
// objects with a `then` proof property. The Node runner awaits only actual
|
|
228
|
+
// promises, and browser execution must preserve that same test-tree rule.
|
|
229
|
+
return Promise.resolve().then(() => [fn()]).then(
|
|
230
|
+
([value]) => runPromise(value, passed, failed) ?? passed(value),
|
|
231
|
+
failed
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** @type {(status: string, duration: number, results: readonly _BrowserTestResult[]) => BrowserTestReport} */
|
|
236
|
+
const reportOf = (status, duration, results) => {
|
|
237
|
+
const failed = results.filter(result => result.status === 'failed').length
|
|
238
|
+
return {
|
|
239
|
+
status,
|
|
240
|
+
browser: navigator.userAgent,
|
|
241
|
+
totals: { tests: results.length, passed: results.length - failed, failed },
|
|
242
|
+
duration,
|
|
243
|
+
results,
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Runs named proof exports and returns the serializable browser report.
|
|
249
|
+
*
|
|
250
|
+
* @type {(modules: readonly (readonly [string, unknown])[], result?: (result: _BrowserTestResult) => void) => Promise<BrowserTestReport>}
|
|
251
|
+
*/
|
|
252
|
+
export const runBrowserProofs = (modules, result = () => undefined) => {
|
|
253
|
+
const start = performance.now()
|
|
254
|
+
// Reporting each result as it lands is the page's own code. A renderer that
|
|
255
|
+
// throws must not take the run down with it: the report it fails to show is
|
|
256
|
+
// the one thing the page is still waiting for.
|
|
257
|
+
/** @type {(result: _BrowserTestResult) => void} */
|
|
258
|
+
const announce = value => {
|
|
259
|
+
try {
|
|
260
|
+
result(value)
|
|
261
|
+
} catch {
|
|
262
|
+
// The result stays in the report the run resolves with.
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/** @type {(module: string, error: unknown) => () => Promise<readonly _BrowserTestResult[]>} */
|
|
266
|
+
const unreadable = (module, error) => () => {
|
|
267
|
+
const [message, stack] = errorDetails(error)
|
|
268
|
+
const failure = { module, path: '', status: 'failed', duration: 0, message, stack }
|
|
269
|
+
announce(failure)
|
|
270
|
+
return Promise.resolve([failure])
|
|
271
|
+
}
|
|
272
|
+
const tests = modules.flatMap(([module, proof]) => {
|
|
273
|
+
// Reading an exported tree runs user code just as reading a returned
|
|
274
|
+
// one does. A module that cannot be enumerated is one failed module,
|
|
275
|
+
// never a run that ends without a report.
|
|
276
|
+
try {
|
|
277
|
+
return collectTests([], false, proof).map(([path, entry]) =>
|
|
278
|
+
() => runOne(module, path, entry.throws, entry.fn, announce)
|
|
279
|
+
)
|
|
280
|
+
} catch (error) {
|
|
281
|
+
return [unreadable(module, error)]
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
const batchSize = 25
|
|
285
|
+
/** @type {(index: number, results: readonly _BrowserTestResult[]) => Promise<readonly _BrowserTestResult[]>} */
|
|
286
|
+
const runBatch = (index, results) => {
|
|
287
|
+
const batch = tests.slice(index, index + batchSize)
|
|
288
|
+
if (batch.length === 0) { return Promise.resolve(results) }
|
|
289
|
+
return Promise.all(batch.map(test => test())).then(next =>
|
|
290
|
+
new Promise(resolve => setTimeout(resolve, 0, [...results, ...next.flat()]))
|
|
291
|
+
).then(next => runBatch(index + batchSize, next))
|
|
292
|
+
}
|
|
293
|
+
const completed = runBatch(0, [])
|
|
294
|
+
return completed.then(results => reportOf(
|
|
295
|
+
results.some(result => result.status === 'failed') ? 'failed' : 'passed',
|
|
296
|
+
performance.now() - start,
|
|
297
|
+
results,
|
|
298
|
+
))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** @typedef {(source: string) => Promise<{ readonly proof?: unknown }>} _BrowserImporter */
|
|
302
|
+
/** @typedef {{ readonly status: 'loaded', readonly source: string, readonly proof: unknown } | { readonly status: 'error', readonly source: string, readonly error: unknown }} _LoadedModule */
|
|
303
|
+
/** @typedef {Window & { fjsBrowserTestReport?: Promise<BrowserTestReport> }} _TestWindow */
|
|
304
|
+
|
|
305
|
+
/** @type {(root: Element) => _TestWindow | null} */
|
|
306
|
+
const viewOf = root => root.ownerDocument.defaultView
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Renders the settled report into the page, publishes the run as
|
|
310
|
+
* `fjsBrowserTestReport` on the root's window, and announces it with
|
|
311
|
+
* `fjs-browser-test-complete`.
|
|
312
|
+
*
|
|
313
|
+
* @type {(root: Element, report: Promise<BrowserTestReport>) => Promise<BrowserTestReport>}
|
|
314
|
+
*/
|
|
315
|
+
const publish = (root, report) => {
|
|
316
|
+
const view = viewOf(root)
|
|
317
|
+
const done = report.then(value => {
|
|
318
|
+
renderBrowserReport(root, value)
|
|
319
|
+
view?.dispatchEvent(new CustomEvent('fjs-browser-test-complete', { detail: value }))
|
|
320
|
+
return value
|
|
321
|
+
})
|
|
322
|
+
if (view !== null) { view.fjsBrowserTestReport = done }
|
|
323
|
+
return done
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Loads proof modules after the page has rendered, reporting module-loading
|
|
328
|
+
* progress before proof execution begins.
|
|
329
|
+
*
|
|
330
|
+
* @type {(root: Element, sources: readonly string[], importer: _BrowserImporter) => Promise<BrowserTestReport>}
|
|
331
|
+
*/
|
|
332
|
+
export const startBrowserTestSources = (root, sources, importer) => {
|
|
333
|
+
const start = performance.now()
|
|
334
|
+
setState(root, 'loading')
|
|
335
|
+
let loaded = 0
|
|
336
|
+
const summary = root.querySelector('[data-test-summary]')
|
|
337
|
+
// The importer is supplied by the page, so obtaining the promise is itself
|
|
338
|
+
// a failure point: a synchronous throw becomes a rejection here and is
|
|
339
|
+
// reported as a loader failure, rather than escaping past a `loading` state
|
|
340
|
+
// that no report or completion event ever replaces.
|
|
341
|
+
/** @type {(source: string) => Promise<{ readonly proof?: unknown }>} */
|
|
342
|
+
const load = source => {
|
|
343
|
+
try {
|
|
344
|
+
return importer(source)
|
|
345
|
+
} catch (error) {
|
|
346
|
+
return Promise.reject(error)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/** @type {Promise<readonly _LoadedModule[]>} */
|
|
350
|
+
const modules = Promise.all(sources.map(source => load(source).then(
|
|
351
|
+
module => {
|
|
352
|
+
loaded += 1
|
|
353
|
+
if (summary !== null) { summary.textContent = `Loading ${loaded}/${sources.length}: ${source}` }
|
|
354
|
+
return /** @type {const} */ ({ status: 'loaded', source, proof: module.proof })
|
|
355
|
+
},
|
|
356
|
+
error => /** @type {const} */ ({ status: 'error', source, error })
|
|
357
|
+
)))
|
|
358
|
+
const report = modules.then(loadedModules => {
|
|
359
|
+
const rejected = loadedModules.flatMap(module =>
|
|
360
|
+
module.status === 'error' ? [module] : [])
|
|
361
|
+
if (rejected.length !== 0) {
|
|
362
|
+
// A module that never linked has no tests to run, so the run stops
|
|
363
|
+
// here. Each rejection is still counted as a failed result: totals
|
|
364
|
+
// that disagreed with `results` would tell an automated consumer
|
|
365
|
+
// the suite was empty rather than broken.
|
|
366
|
+
const duration = performance.now() - start
|
|
367
|
+
return publish(root, Promise.resolve(reportOf('infrastructure-error', duration,
|
|
368
|
+
rejected.map(({ source, error }) => {
|
|
369
|
+
const [message, stack] = errorDetails(error)
|
|
370
|
+
return { module: source, path: '', status: 'failed', duration, message, stack }
|
|
371
|
+
}))))
|
|
372
|
+
}
|
|
373
|
+
return startBrowserTests(root, loadedModules.flatMap(module =>
|
|
374
|
+
module.status === 'loaded'
|
|
375
|
+
? [/** @type {const} */ ([module.source, module.proof])]
|
|
376
|
+
: []))
|
|
377
|
+
})
|
|
378
|
+
const view = viewOf(root)
|
|
379
|
+
if (view !== null) { view.fjsBrowserTestReport = report }
|
|
380
|
+
return report
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** @type {(root: Element, state: string) => void} */
|
|
384
|
+
const setState = (root, state) => root.setAttribute('data-state', state)
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Renders a completed report in the browser test page.
|
|
388
|
+
*
|
|
389
|
+
* @type {(root: Element, report: BrowserTestReport) => void}
|
|
390
|
+
*/
|
|
391
|
+
export const renderBrowserReport = (root, report) => {
|
|
392
|
+
setState(root, report.status)
|
|
393
|
+
const summary = root.querySelector('[data-test-summary]')
|
|
394
|
+
if (summary !== null) {
|
|
395
|
+
summary.textContent = report.status === 'infrastructure-error'
|
|
396
|
+
? `Infrastructure error: ${report.totals.failed} failed to load (${report.duration.toFixed(1)} ms)`
|
|
397
|
+
: `${report.totals.passed} passed, ${report.totals.failed} failed (${report.duration.toFixed(1)} ms)`
|
|
398
|
+
}
|
|
399
|
+
const output = root.querySelector('[data-test-results]')
|
|
400
|
+
if (output !== null) {
|
|
401
|
+
output.replaceChildren(...report.results.map(result =>
|
|
402
|
+
renderResult(root.ownerDocument, result)))
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** @type {(document: Document, result: _BrowserTestResult) => HTMLLIElement} */
|
|
407
|
+
const renderResult = (document, result) => {
|
|
408
|
+
const item = document.createElement('li')
|
|
409
|
+
item.setAttribute('data-status', result.status)
|
|
410
|
+
const detail = result.status === 'failed' ? `: ${result.message}\n${result.stack}` : ''
|
|
411
|
+
item.textContent = `${result.status === 'passed' ? 'PASS' : 'FAIL'} ${result.module} ${result.path} (${result.duration.toFixed(1)} ms)${detail}`
|
|
412
|
+
return item
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Runs the application, publishes its promise as `window.fjsBrowserTestReport`,
|
|
417
|
+
* and dispatches `fjs-browser-test-complete` with the report in `detail`.
|
|
418
|
+
*
|
|
419
|
+
* @type {(root: Element, modules: readonly (readonly [string, unknown])[]) => Promise<BrowserTestReport>}
|
|
420
|
+
*/
|
|
421
|
+
export const startBrowserTests = (root, modules) => {
|
|
422
|
+
setState(root, 'running')
|
|
423
|
+
const output = root.querySelector('[data-test-results]')
|
|
424
|
+
if (output !== null) { output.replaceChildren() }
|
|
425
|
+
let completed = 0
|
|
426
|
+
return publish(root, runBrowserProofs(modules, result => {
|
|
427
|
+
completed += 1
|
|
428
|
+
const summary = root.querySelector('[data-test-summary]')
|
|
429
|
+
if (summary !== null) { summary.textContent = `${completed} tests completed…` }
|
|
430
|
+
if (output !== null) { output.append(renderResult(root.ownerDocument, result)) }
|
|
431
|
+
}))
|
|
432
|
+
}
|
|
@@ -23,7 +23,7 @@ import type { Ts } from '../types/rtti/ts/types.ts';
|
|
|
23
23
|
* written: no assertion here reads it, and its `undefined` payloads have no
|
|
24
24
|
* JSON representation to round-trip through anyway.
|
|
25
25
|
*/
|
|
26
|
-
declare const event: import("../types/rtti/types.ts").Or<[readonly ["result", import("../types/rtti/types.ts")._Type0<"string">, import("../types/rtti/types.ts").Type1<"array", import("../types/rtti/types.ts").Or<[import("../types/rtti/types.ts")._Type0<"string">, null]>>], readonly ["summary", import("../types/rtti/types.ts")._Type0<"number">, import("../types/rtti/types.ts")._Type0<"number">, import("../types/rtti/types.ts")._Type0<"number">]]>;
|
|
26
|
+
declare const event: import("../types/rtti/types.ts").Or<readonly [readonly ["result", import("../types/rtti/types.ts")._Type0<"string">, import("../types/rtti/types.ts").Type1<"array", import("../types/rtti/types.ts").Or<readonly [import("../types/rtti/types.ts")._Type0<"string">, null]>>], readonly ["summary", import("../types/rtti/types.ts")._Type0<"number">, import("../types/rtti/types.ts")._Type0<"number">, import("../types/rtti/types.ts")._Type0<"number">]]>;
|
|
27
27
|
export type _Event = Ts<typeof event>;
|
|
28
28
|
export type _TestReporter = Reporter<Sandbox | Write>;
|
|
29
29
|
export declare const flat: () => void;
|
|
@@ -328,7 +328,7 @@ export const reporterWriteFailure = () => {
|
|
|
328
328
|
},
|
|
329
329
|
sandbox: (/** @type {() => unknown} */ f) => (/** @type {undefined} */ s) =>
|
|
330
330
|
[s, ok({ result: ok(f()), duration: 0 })],
|
|
331
|
-
write: (_stream, _data) => s => [s, error(
|
|
331
|
+
write: (_stream, _data) => s => [s, error(['notImplemented', 'write'])],
|
|
332
332
|
}))
|
|
333
333
|
const [, code] = runner(undefined)(
|
|
334
334
|
/** @type {Effect<_FailOps, 0, number>} */(main(options('.'))))
|
|
@@ -425,7 +425,7 @@ const registerBodyPanicsOnUndispatchableEffect = () => {
|
|
|
425
425
|
return [st, ok(rs)]
|
|
426
426
|
},
|
|
427
427
|
// The runner has no `await`, which is what the body's channel carries.
|
|
428
|
-
await: _p => s => [s, error(
|
|
428
|
+
await: _p => s => [s, error(['notImplemented', 'await'])],
|
|
429
429
|
}))
|
|
430
430
|
const proof = /** @type {const} */ ({ a: () => Promise.resolve(undefined) })
|
|
431
431
|
runner([])(registerModule(registerNoopCtx, './a.f.ts', proof, ''))
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The simple string escapes — one source of truth.
|
|
3
|
+
*
|
|
4
|
+
* `\"`, `\\`, `\/`, `\b`, `\f`, `\n`, `\r`, `\t`: every escape sequence whose
|
|
5
|
+
* meaning is one fixed code point. `\uXXXX` is not one of them — its meaning
|
|
6
|
+
* is computed from the digits that follow rather than looked up — and neither
|
|
7
|
+
* are JavaScript's `\0`, `\v`, `\xXX`, or a line continuation, which
|
|
8
|
+
* FunctionalScript does not accept.
|
|
9
|
+
*
|
|
10
|
+
* The set is the same for a JavaScript string literal and for JSON's `char`
|
|
11
|
+
* production, so the JSON serializer and both tokenizers derive their view
|
|
12
|
+
* from this one list rather than keeping a copy each: an encode side and two
|
|
13
|
+
* decode sides that cannot drift apart.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The escape letter paired with the code point it denotes.
|
|
19
|
+
*
|
|
20
|
+
* The first three denote themselves — `\"` is `"` — and are escapable because
|
|
21
|
+
* an unescaped `"` would end the literal and an unescaped `\` would start
|
|
22
|
+
* another escape. The rest name C0 controls, which have no printable form of
|
|
23
|
+
* their own.
|
|
24
|
+
*
|
|
25
|
+
* @type {readonly (readonly [letter: number, codePoint: number])[]}
|
|
26
|
+
*/
|
|
27
|
+
export declare const simpleEscapes: readonly (readonly [letter: number, codePoint: number])[];
|
|
28
|
+
/**
|
|
29
|
+
* The one escape that is optional. `/` is a legal string character on its own,
|
|
30
|
+
* so `\/` has to be *accepted* while nothing has to *produce* it — which is
|
|
31
|
+
* why the decode view below covers one more entry than the encode view.
|
|
32
|
+
*/
|
|
33
|
+
export declare const optionalEscape: number;
|
|
34
|
+
/**
|
|
35
|
+
* The decode view: the code point an escape letter denotes, or `null` when the
|
|
36
|
+
* letter is not a simple escape — `u`, or a character the grammar rejects.
|
|
37
|
+
*
|
|
38
|
+
* @type {(letter: number) => number | null}
|
|
39
|
+
*/
|
|
40
|
+
export declare const escapeToCodePoint: (letter: number) => number | null;
|
|
41
|
+
/**
|
|
42
|
+
* The encode view: the escape letter denoting a code point, or `null` when the
|
|
43
|
+
* code point has no simple escape or needs none (see {@link optionalEscape}).
|
|
44
|
+
*
|
|
45
|
+
* @type {(codePoint: number) => number | null}
|
|
46
|
+
*/
|
|
47
|
+
export declare const codePointToEscape: (codePoint: number) => number | null;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The simple string escapes — one source of truth.
|
|
3
|
+
*
|
|
4
|
+
* `\"`, `\\`, `\/`, `\b`, `\f`, `\n`, `\r`, `\t`: every escape sequence whose
|
|
5
|
+
* meaning is one fixed code point. `\uXXXX` is not one of them — its meaning
|
|
6
|
+
* is computed from the digits that follow rather than looked up — and neither
|
|
7
|
+
* are JavaScript's `\0`, `\v`, `\xXX`, or a line continuation, which
|
|
8
|
+
* FunctionalScript does not accept.
|
|
9
|
+
*
|
|
10
|
+
* The set is the same for a JavaScript string literal and for JSON's `char`
|
|
11
|
+
* production, so the JSON serializer and both tokenizers derive their view
|
|
12
|
+
* from this one list rather than keeping a copy each: an encode side and two
|
|
13
|
+
* decode sides that cannot drift apart.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
backspace,
|
|
20
|
+
cr,
|
|
21
|
+
ff,
|
|
22
|
+
ht,
|
|
23
|
+
latinSmallLetterB,
|
|
24
|
+
latinSmallLetterF,
|
|
25
|
+
latinSmallLetterN,
|
|
26
|
+
latinSmallLetterR,
|
|
27
|
+
latinSmallLetterT,
|
|
28
|
+
lf,
|
|
29
|
+
quotationMark,
|
|
30
|
+
reverseSolidus,
|
|
31
|
+
solidus,
|
|
32
|
+
} from '../../text/ascii/module.f.mjs'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The escape letter paired with the code point it denotes.
|
|
36
|
+
*
|
|
37
|
+
* The first three denote themselves — `\"` is `"` — and are escapable because
|
|
38
|
+
* an unescaped `"` would end the literal and an unescaped `\` would start
|
|
39
|
+
* another escape. The rest name C0 controls, which have no printable form of
|
|
40
|
+
* their own.
|
|
41
|
+
*
|
|
42
|
+
* @type {readonly (readonly [letter: number, codePoint: number])[]}
|
|
43
|
+
*/
|
|
44
|
+
export const simpleEscapes = [
|
|
45
|
+
[quotationMark, quotationMark],
|
|
46
|
+
[reverseSolidus, reverseSolidus],
|
|
47
|
+
[solidus, solidus],
|
|
48
|
+
[latinSmallLetterB, backspace],
|
|
49
|
+
[latinSmallLetterF, ff],
|
|
50
|
+
[latinSmallLetterN, lf],
|
|
51
|
+
[latinSmallLetterR, cr],
|
|
52
|
+
[latinSmallLetterT, ht],
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The one escape that is optional. `/` is a legal string character on its own,
|
|
57
|
+
* so `\/` has to be *accepted* while nothing has to *produce* it — which is
|
|
58
|
+
* why the decode view below covers one more entry than the encode view.
|
|
59
|
+
*/
|
|
60
|
+
export const optionalEscape = solidus
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A code-point-keyed view over the pairs.
|
|
64
|
+
*
|
|
65
|
+
* A `Map` rather than a plain object, for two reasons. The JSON serializer
|
|
66
|
+
* takes this path once per character it writes, and a `Map` keyed by number
|
|
67
|
+
* never converts the key to a string. And an object's keys are not only its
|
|
68
|
+
* own: a numeric property on `Object.prototype` — which this repository would
|
|
69
|
+
* never write, but a host page can — would read back as a table entry and
|
|
70
|
+
* silently mis-escape a character. A `Map` has no prototype chain to inherit
|
|
71
|
+
* one from, so the lookup answers for the table and nothing else.
|
|
72
|
+
*
|
|
73
|
+
* @type {(pairs: readonly (readonly [number, number])[]) => (key: number) => number | null}
|
|
74
|
+
*/
|
|
75
|
+
const lookup = pairs => {
|
|
76
|
+
const map = new Map(pairs)
|
|
77
|
+
return key => map.get(key) ?? null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The decode view: the code point an escape letter denotes, or `null` when the
|
|
82
|
+
* letter is not a simple escape — `u`, or a character the grammar rejects.
|
|
83
|
+
*
|
|
84
|
+
* @type {(letter: number) => number | null}
|
|
85
|
+
*/
|
|
86
|
+
export const escapeToCodePoint = lookup(simpleEscapes)
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The encode view: the escape letter denoting a code point, or `null` when the
|
|
90
|
+
* code point has no simple escape or needs none (see {@link optionalEscape}).
|
|
91
|
+
*
|
|
92
|
+
* @type {(codePoint: number) => number | null}
|
|
93
|
+
*/
|
|
94
|
+
export const codePointToEscape = lookup(simpleEscapes.flatMap(([letter, codePoint]) =>
|
|
95
|
+
codePoint === optionalEscape ? [] : [/** @type {const} */([codePoint, letter])]))
|