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
|
@@ -8,257 +8,349 @@
|
|
|
8
8
|
* @import { Fold } from '../../types/function/operator/types.ts'
|
|
9
9
|
* @import { DjsToken, DjsTokenWithMetadata } from '../tokenizer/types.ts'
|
|
10
10
|
* @import { OrderedMap } from '../../types/ordered_map/types.ts'
|
|
11
|
-
* @import { AstArray, AstConst, AstModule, AstModuleRef } from '../ast/types.ts'
|
|
11
|
+
* @import { AstArray, AstConst, AstModule, AstModuleRef, AstObject } from '../ast/types.ts'
|
|
12
12
|
* @import { TokenMetadata } from '../../js/tokenizer/types.ts'
|
|
13
|
-
* @import { ParseError, _ValueToken } from './types.ts'
|
|
13
|
+
* @import { ParseError, _FramingKeyword, _OrdinaryTokenName, _ValueToken } from './types.ts'
|
|
14
|
+
* @import { Assert } from '../../asserts/types.ts'
|
|
15
|
+
* @import { Equal } from '../../types/ts/types.ts'
|
|
16
|
+
* @import { CodePointMeta } from '../../bnf/descent/types.ts'
|
|
17
|
+
* @import { Ast, AstSequence } from '../../bnf/matcher/types.ts'
|
|
18
|
+
* @import { Rule, TerminalRange } from '../../bnf/types.ts'
|
|
19
|
+
* @import { DescentMatch } from '../../bnf/descent/types.ts'
|
|
14
20
|
*/
|
|
15
21
|
|
|
16
22
|
import { error, ok } from '../../types/result/module.f.mjs'
|
|
17
23
|
import { fold, next, toArray, length, concat } from '../../types/list/module.f.mjs'
|
|
18
24
|
import { setReplace, at } from '../../types/ordered_map/module.f.mjs'
|
|
19
25
|
import { fromMap } from '../../types/object/module.f.mjs'
|
|
20
|
-
import { assertEq } from '../../asserts/module.f.mjs'
|
|
26
|
+
import { assert, assertEq, assertNotNullish } from '../../asserts/module.f.mjs'
|
|
27
|
+
import { eof, oneEncode, option, rangeDecode, repeat0Plus, unicodeRange } from '../../bnf/module.f.mjs'
|
|
28
|
+
import { encoding } from '../../bnf/token_symbol/module.f.mjs'
|
|
29
|
+
import { toData } from '../../bnf/data/module.f.mjs'
|
|
30
|
+
import { descentParserRuleSet } from '../../bnf/descent/module.f.mjs'
|
|
21
31
|
|
|
22
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* The ordinary token stream a BNF parser layer consumes, with the tokenizer's
|
|
34
|
+
* one physical end-of-input token split off.
|
|
35
|
+
*
|
|
36
|
+
* @typedef {{
|
|
37
|
+
* readonly tokens: readonly DjsTokenWithMetadata[]
|
|
38
|
+
* readonly eofMetadata: TokenMetadata
|
|
39
|
+
* }} _TokenStream
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Splits the tokenizer's single final physical `eof` token off a token list.
|
|
44
|
+
*
|
|
45
|
+
* A BNF parser backend synthesizes its own logical end-of-input, so passing the
|
|
46
|
+
* tokenizer's physical `eof` through as an ordinary symbol would create a second
|
|
47
|
+
* end marker. Dropping it outright would instead lose the source position that a
|
|
48
|
+
* failure *at* physical end has to be reported from, so its metadata is kept
|
|
49
|
+
* aside as `eofMetadata` rather than discarded or refabricated.
|
|
50
|
+
*
|
|
51
|
+
* The tokenizer's contract is exactly one `eof`, in final position; a stream
|
|
52
|
+
* carrying one anywhere else is rejected here rather than parsed.
|
|
53
|
+
*
|
|
54
|
+
* A stream with no `eof` at all has two causes, and they are not reported the
|
|
55
|
+
* same way. A lexical failure — an unterminated string or comment — ends the
|
|
56
|
+
* stream at an `error` token and emits no `eof`, which is the tokenizer working
|
|
57
|
+
* correctly on bad input; that error is passed through with its own position.
|
|
58
|
+
* Anything else missing an `eof` is a genuine contract violation and has no
|
|
59
|
+
* position to report.
|
|
60
|
+
*
|
|
61
|
+
* @type {(tokenList: List<DjsTokenWithMetadata>) => Result<_TokenStream, ParseError>}
|
|
62
|
+
*/
|
|
63
|
+
const splitEof = tokenList => {
|
|
64
|
+
const a = toArray(tokenList)
|
|
65
|
+
const eofIdx = a.findIndex(({ token }) => token.kind === 'eof')
|
|
66
|
+
if (eofIdx === -1) {
|
|
67
|
+
// A lexical failure ends the stream at its `error` token and emits no
|
|
68
|
+
// `eof`, so the absence of one is not always a broken contract. Rejecting
|
|
69
|
+
// it as one would answer "unterminated string at 1:11" with "missing
|
|
70
|
+
// end-of-input token" and no position at all, so the error is reported
|
|
71
|
+
// where it happened — the same place the hand-written parser reports it.
|
|
72
|
+
const lastToken = a[a.length - 1]
|
|
73
|
+
return lastToken !== undefined && lastToken.token.kind === 'error'
|
|
74
|
+
? error({ message: 'unexpected token', metadata: lastToken.metadata })
|
|
75
|
+
: error({ message: 'missing end-of-input token', metadata: null })
|
|
76
|
+
}
|
|
77
|
+
const last = a.length - 1
|
|
78
|
+
if (eofIdx !== last) {
|
|
79
|
+
return error({ message: 'end-of-input token is not final', metadata: a[eofIdx].metadata })
|
|
80
|
+
}
|
|
81
|
+
return ok({ tokens: a.slice(0, last), eofMetadata: a[last].metadata })
|
|
82
|
+
}
|
|
23
83
|
|
|
24
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* The parser layer's complete finite alphabet: every token name its grammar may
|
|
86
|
+
* name as a terminal, and the exact set a token-name-to-symbol mapping has to be
|
|
87
|
+
* validated over before parsing.
|
|
88
|
+
*
|
|
89
|
+
* `eof` is not a member — {@link splitEof} removes the tokenizer's physical
|
|
90
|
+
* end-of-input token before any name is mapped, and the backend synthesizes its
|
|
91
|
+
* own logical one.
|
|
92
|
+
*
|
|
93
|
+
* The names are the *token* vocabulary, not the tokenizer grammar's tag
|
|
94
|
+
* vocabulary: only eight punctuators survive into `DjsToken`, so the JS operator
|
|
95
|
+
* set the tokenizer recognizes is far larger than what reaches this layer.
|
|
96
|
+
*
|
|
97
|
+
* A name is not always a kind. The framing keywords arrive as `id` tokens and
|
|
98
|
+
* need terminals of their own, or the grammar could not tell `export default`
|
|
99
|
+
* from two arbitrary identifiers — see {@link framingKeywords}.
|
|
100
|
+
*
|
|
101
|
+
* The `_…AreComplete` assertions below check both halves against `DjsToken` and
|
|
102
|
+
* `_FramingKeyword` at compile time, so a kind or keyword added there breaks the
|
|
103
|
+
* build rather than going unrepresented.
|
|
104
|
+
*/
|
|
105
|
+
const tokenKindNames = /** @type {const} */ ([
|
|
106
|
+
'true', 'false', 'null', 'undefined',
|
|
107
|
+
'{', '}', ':', ',', '[', ']', '.', '=',
|
|
108
|
+
'string', 'number', 'error', 'id', 'bigint',
|
|
109
|
+
'ws', 'nl', '//', '/*',
|
|
110
|
+
])
|
|
25
111
|
|
|
26
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* The framing keywords, which the tokenizer emits as `id` tokens carrying the
|
|
114
|
+
* word in `value`. Kept as its own list because the mapping has to recognize
|
|
115
|
+
* exactly these values, not merely encode them.
|
|
116
|
+
*
|
|
117
|
+
* **A grammar over this alphabet owes them an identifier rule.** None of the
|
|
118
|
+
* five is reserved: outside the framing positions the parser accepts them as
|
|
119
|
+
* ordinary identifiers, so `const export = 1`, `export default export`, and
|
|
120
|
+
* `{ from: 2, default: 3 }` all parse today. Once each carries its own symbol, a
|
|
121
|
+
* rule whose identifier terminal is the bare `id` symbol rejects every one of
|
|
122
|
+
* them. Wherever an identifier is accepted — binding names, references, object
|
|
123
|
+
* keys, import names — the terminal has to be the union of `id` and these five.
|
|
124
|
+
*
|
|
125
|
+
* Giving a word its own symbol narrows where it is *required*, never where it is
|
|
126
|
+
* *allowed*.
|
|
127
|
+
*/
|
|
128
|
+
const framingKeywords = /** @type {const} */ (['import', 'const', 'export', 'default', 'from'])
|
|
27
129
|
|
|
28
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* The complete alphabet: one name per `DjsToken` kind except `eof`, plus one per
|
|
132
|
+
* framing keyword. No keyword collides with a kind, so the two lists concatenate
|
|
133
|
+
* without a name being registered twice — which `encoding` would reject anyway.
|
|
134
|
+
*/
|
|
135
|
+
const ordinaryTokenNames = [...tokenKindNames, ...framingKeywords]
|
|
29
136
|
|
|
30
|
-
/** @typedef {
|
|
137
|
+
/** @typedef {Assert<Equal<(typeof tokenKindNames)[number], Exclude<DjsToken['kind'], 'eof'>>>} _KindsAreComplete */
|
|
31
138
|
|
|
32
|
-
/** @typedef {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
* readonly consts: List<AstConst>
|
|
36
|
-
* }} _ModuleState */
|
|
37
|
-
|
|
38
|
-
/** @typedef {{
|
|
39
|
-
* readonly state: ''
|
|
40
|
-
* readonly module: _ModuleState
|
|
41
|
-
* }} _InitialState */
|
|
42
|
-
|
|
43
|
-
/** @typedef {{
|
|
44
|
-
* readonly state: 'nl'
|
|
45
|
-
* readonly module: _ModuleState
|
|
46
|
-
* }} _NewLineRequiredState */
|
|
47
|
-
|
|
48
|
-
/** @typedef {{
|
|
49
|
-
* readonly state: 'import' | 'import+name' | 'import+from'
|
|
50
|
-
* readonly module: _ModuleState
|
|
51
|
-
* }} _ImportState */
|
|
52
|
-
|
|
53
|
-
/** @typedef {{
|
|
54
|
-
* readonly state: 'const' | 'const+name'
|
|
55
|
-
* readonly module: _ModuleState
|
|
56
|
-
* }} _ConstState */
|
|
57
|
-
|
|
58
|
-
/** @typedef {{
|
|
59
|
-
* readonly state: 'export'
|
|
60
|
-
* readonly module: _ModuleState
|
|
61
|
-
* }} _ExportState */
|
|
139
|
+
/** @typedef {Assert<Equal<(typeof framingKeywords)[number], _FramingKeyword>>} _KeywordsAreComplete */
|
|
140
|
+
|
|
141
|
+
/** @typedef {Assert<Equal<(typeof ordinaryTokenNames)[number], _OrdinaryTokenName>>} _AlphabetIsComplete */
|
|
62
142
|
|
|
63
143
|
/**
|
|
64
|
-
*
|
|
144
|
+
* `eof` is not a member of the alphabet, so a second end marker cannot be
|
|
145
|
+
* encoded rather than merely going unused — and `encode` would reject the name
|
|
146
|
+
* outright. Checked at the type level because that is where it is decidable:
|
|
147
|
+
* `includes('eof')` does not even compile against this element type.
|
|
65
148
|
*
|
|
66
|
-
*
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
*
|
|
149
|
+
* @typedef {Assert<Equal<Extract<_OrdinaryTokenName, 'eof'>, never>>} _EofIsNotAName
|
|
150
|
+
*/
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The alphabet's encoding, built once for the module rather than per parse.
|
|
71
154
|
*
|
|
72
|
-
*
|
|
155
|
+
* `encoding` asserts what the mapping needs — capacity, and no repeated name —
|
|
156
|
+
* so an alphabet that could not produce distinct symbols fails here at load
|
|
157
|
+
* rather than midway through a parse. Symbols start at `0x110000`, one past the
|
|
158
|
+
* last Unicode scalar value, so a token symbol can never be mistaken for a code
|
|
159
|
+
* point of the layer below.
|
|
73
160
|
*/
|
|
161
|
+
const tokenEncoding = encoding(ordinaryTokenNames)
|
|
74
162
|
|
|
75
|
-
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
163
|
+
/**
|
|
164
|
+
* One ordinary token as a descent input leaf: the symbol standing for its kind,
|
|
165
|
+
* paired with the whole token as metadata.
|
|
166
|
+
*
|
|
167
|
+
* The grammar above sees only the symbol — one per token, which is what makes a
|
|
168
|
+
* token stream an alphabet — while the token's value and source position ride
|
|
169
|
+
* along untouched, so nothing a diagnostic or an AST fold needs is lost.
|
|
170
|
+
*
|
|
171
|
+
* `eof` is not in the alphabet and `encode` would reject it. Reaching it here
|
|
172
|
+
* means {@link splitEof} was skipped, which is a caller bug rather than bad
|
|
173
|
+
* input, so this asserts instead of widening the result to a `Result`.
|
|
174
|
+
*
|
|
175
|
+
* @type {(t: DjsTokenWithMetadata) => CodePointMeta<DjsTokenWithMetadata>}
|
|
176
|
+
*/
|
|
177
|
+
const tokenToSymbol = t => {
|
|
178
|
+
const { token } = t
|
|
179
|
+
// A framing keyword arrives as an `id` carrying the word, so the name comes
|
|
180
|
+
// from the value there and from the kind everywhere else. `find` rather than
|
|
181
|
+
// a set membership test because it also narrows the result to the keyword
|
|
182
|
+
// union, which is what lets `encode` be called without a cast.
|
|
183
|
+
const keyword = token.kind === 'id'
|
|
184
|
+
? framingKeywords.find(k => k === token.value)
|
|
185
|
+
: undefined
|
|
186
|
+
const name = keyword ?? token.kind
|
|
187
|
+
assert(name !== 'eof', ['eof token reached the parser alphabet', t])
|
|
188
|
+
return [tokenEncoding.encode(name), t]
|
|
189
|
+
}
|
|
82
190
|
|
|
83
|
-
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
191
|
+
/**
|
|
192
|
+
* One token name as a grammar terminal.
|
|
193
|
+
*
|
|
194
|
+
* A symbol and a `TerminalRange` are both plain numbers, so `oneEncode` is what
|
|
195
|
+
* says which one is meant — `encode` returns the bare symbol a stream carries,
|
|
196
|
+
* and a rule needs the singleton range containing it.
|
|
197
|
+
*
|
|
198
|
+
* @type {(name: _OrdinaryTokenName) => TerminalRange}
|
|
199
|
+
*/
|
|
200
|
+
const sym = name => oneEncode(tokenEncoding.encode(name))
|
|
87
201
|
|
|
88
|
-
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Trivia is skipped between every pair of tokens, so almost every rule below is
|
|
204
|
+
* interleaved with it.
|
|
205
|
+
*/
|
|
206
|
+
const trivia = repeat0Plus({
|
|
207
|
+
ws: sym('ws'),
|
|
208
|
+
nl: sym('nl'),
|
|
209
|
+
lineComment: sym('//'),
|
|
210
|
+
blockComment: sym('/*'),
|
|
211
|
+
})
|
|
92
212
|
|
|
93
213
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* as a module would give it a value no JavaScript engine gives it: as
|
|
99
|
-
* JavaScript `{"a":1}` does not parse at all, and `[1,2]` is an expression
|
|
100
|
-
* statement exporting nothing.
|
|
101
|
-
*
|
|
102
|
-
* The statements are also ordered: every `import` precedes every `const`, and
|
|
103
|
-
* `export default` ends the module. `eof` here is a module with no
|
|
104
|
-
* `export default`, which is what "unexpected end" reports.
|
|
105
|
-
*
|
|
106
|
-
* @type {(token: DjsTokenWithMetadata) => (state: _InitialState) => _ParserState}
|
|
214
|
+
* Trivia that stops at a newline, for the one place a newline is not trivia:
|
|
215
|
+
* the statement separator. `import`/`const` statements must be newline-separated
|
|
216
|
+
* — the `'nl'` state in the hand-written parser — so a rule that swallowed
|
|
217
|
+
* newlines as trivia everywhere could not express it.
|
|
107
218
|
*/
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
case '//':
|
|
114
|
-
case '/*': return state
|
|
115
|
-
case 'id': {
|
|
116
|
-
switch (token.value) {
|
|
117
|
-
case 'import': return length(state.module.consts) === 0
|
|
118
|
-
? { ...state, state: 'import' }
|
|
119
|
-
: { state: 'error', error: { message: 'import must come before const', metadata } }
|
|
120
|
-
case 'const': return { ...state, state: 'const' }
|
|
121
|
-
case 'export': return { ...state, state: 'export' }
|
|
122
|
-
}
|
|
123
|
-
break
|
|
124
|
-
}
|
|
125
|
-
case 'eof': return { state: 'error', error: { message: 'unexpected end', metadata } }
|
|
126
|
-
}
|
|
127
|
-
return { state: 'error', error: { message: 'unexpected token', metadata } }
|
|
128
|
-
}
|
|
219
|
+
const statementEnd = () => [
|
|
220
|
+
repeat0Plus({ ws: sym('ws'), lineComment: sym('//'), blockComment: sym('/*') }),
|
|
221
|
+
sym('nl'),
|
|
222
|
+
trivia,
|
|
223
|
+
]
|
|
129
224
|
|
|
130
|
-
/**
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
225
|
+
/**
|
|
226
|
+
* Every word that may stand where an identifier is expected: a plain `id` and
|
|
227
|
+
* each framing keyword, since none of them is reserved.
|
|
228
|
+
*
|
|
229
|
+
* This is the union {@link framingKeywords} obliges the grammar to provide.
|
|
230
|
+
*/
|
|
231
|
+
const identifier = {
|
|
232
|
+
id: sym('id'),
|
|
233
|
+
import: sym('import'),
|
|
234
|
+
const: sym('const'),
|
|
235
|
+
export: sym('export'),
|
|
236
|
+
default: sym('default'),
|
|
237
|
+
from: sym('from'),
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** A value that is one token. */
|
|
241
|
+
const primitive = {
|
|
242
|
+
null: sym('null'),
|
|
243
|
+
true: sym('true'),
|
|
244
|
+
false: sym('false'),
|
|
245
|
+
undefined: sym('undefined'),
|
|
246
|
+
number: sym('number'),
|
|
247
|
+
string: sym('string'),
|
|
248
|
+
bigint: sym('bigint'),
|
|
140
249
|
}
|
|
141
250
|
|
|
142
|
-
/**
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
251
|
+
/**
|
|
252
|
+
* `open item, item, ... ,? close` with trivia everywhere and an optional
|
|
253
|
+
* trailing comma, which both arrays and objects allow.
|
|
254
|
+
*
|
|
255
|
+
* The trailing comma works because a failed repetition round rewinds rather than
|
|
256
|
+
* failing the match ([`bnf/descent`](../../bnf/descent/README.md)): on the final
|
|
257
|
+
* `,` the round consumes the comma, finds `]` where an item belongs, and ends the
|
|
258
|
+
* repetition back at the comma for the optional tail to take.
|
|
259
|
+
*
|
|
260
|
+
* @type {(open: TerminalRange, close: TerminalRange, item: Rule) => Rule}
|
|
261
|
+
*/
|
|
262
|
+
const delimited = (open, close, item) => () => {
|
|
263
|
+
// Each element is wrapped in a one-branch variant so it carries the tag
|
|
264
|
+
// `item`. The branch is a *sequence* rather than the rule itself, because a
|
|
265
|
+
// variant used directly as another variant's branch loses its tag to
|
|
266
|
+
// whichever inner branch matches — and every element here is a variant.
|
|
267
|
+
// The tag is what lets the fold find elements by name instead of by
|
|
268
|
+
// position in the delimiter scaffolding.
|
|
269
|
+
const element = { item: [item] }
|
|
270
|
+
return [
|
|
271
|
+
open,
|
|
272
|
+
trivia,
|
|
273
|
+
option([
|
|
274
|
+
element,
|
|
275
|
+
trivia,
|
|
276
|
+
repeat0Plus([sym(','), trivia, element, trivia]),
|
|
277
|
+
option([sym(','), trivia]),
|
|
278
|
+
]),
|
|
279
|
+
close,
|
|
280
|
+
]
|
|
155
281
|
}
|
|
156
282
|
|
|
157
|
-
/** @type {
|
|
158
|
-
const
|
|
159
|
-
switch (token.kind) {
|
|
160
|
-
case 'ws':
|
|
161
|
-
case 'nl':
|
|
162
|
-
case '//':
|
|
163
|
-
case '/*':
|
|
164
|
-
case 'eof': return state
|
|
165
|
-
default: return { state: 'error', error: { message: 'unexpected token', metadata } }
|
|
166
|
-
}
|
|
167
|
-
}
|
|
283
|
+
/** @type {Rule} */
|
|
284
|
+
const value = () => ({ primitive, ref: identifier, array, object })
|
|
168
285
|
|
|
169
|
-
|
|
170
|
-
const parseConstOp = ({ token, metadata }) => state => {
|
|
171
|
-
switch (token.kind) {
|
|
172
|
-
case 'ws':
|
|
173
|
-
case 'nl':
|
|
174
|
-
case '//':
|
|
175
|
-
case '/*': return state
|
|
176
|
-
case 'id': {
|
|
177
|
-
if (at(token.value)(state.module.refs) !== null)
|
|
178
|
-
return { state: 'error', error: { message: 'duplicate id', metadata } }
|
|
179
|
-
/** @type {AstModuleRef} */
|
|
180
|
-
const cref = ['cref', length(state.module.consts)]
|
|
181
|
-
const refs = setReplace(token.value)(cref)(state.module.refs)
|
|
182
|
-
return { ...state, state: 'const+name', module: { ...state.module, refs: refs } }
|
|
183
|
-
}
|
|
184
|
-
case 'eof': return { state: 'error', error: { message: 'unexpected end', metadata } }
|
|
185
|
-
default: return { state: 'error', error: { message: 'unexpected token', metadata } }
|
|
186
|
-
}
|
|
187
|
-
}
|
|
286
|
+
const array = delimited(sym('['), sym(']'), value)
|
|
188
287
|
|
|
189
|
-
/**
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
case '//':
|
|
195
|
-
case '/*': return state
|
|
196
|
-
case '=': return { ...state, state: 'constValue', valueState: '', top: null, stack: null }
|
|
197
|
-
case 'eof': return { state: 'error', error: { message: 'unexpected end', metadata } }
|
|
198
|
-
default: return { state: 'error', error: { message: 'unexpected token', metadata } }
|
|
199
|
-
}
|
|
288
|
+
/** A property name: bare identifier, string literal, or a computed `["a"]`. */
|
|
289
|
+
const key = {
|
|
290
|
+
plain: identifier,
|
|
291
|
+
string: sym('string'),
|
|
292
|
+
computed: () => [sym('['), trivia, { name: [sym('string')] }, trivia, sym(']')],
|
|
200
293
|
}
|
|
201
294
|
|
|
202
|
-
/** @type {
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const refs = setReplace(token.value)(aref)(state.module.refs)
|
|
216
|
-
return { ...state, state: 'import+name', module: { ...state.module, refs: refs } }
|
|
217
|
-
}
|
|
218
|
-
case 'eof': return { state: 'error', error: { message: 'unexpected end', metadata } }
|
|
219
|
-
default: return { state: 'error', error: { message: 'unexpected token', metadata } }
|
|
220
|
-
}
|
|
295
|
+
/** @type {Rule} */
|
|
296
|
+
const member = { member: () => [{ key: [key] }, trivia, sym(':'), trivia, { value: [value] }] }
|
|
297
|
+
|
|
298
|
+
const object = delimited(sym('{'), sym('}'), member)
|
|
299
|
+
|
|
300
|
+
// Each statement is tagged for the same reason an element is: the fold reads
|
|
301
|
+
// the module by finding `import`/`const`/`export` nodes, not by counting past
|
|
302
|
+
// the trivia and separators between them.
|
|
303
|
+
const importStatement = {
|
|
304
|
+
import: () => [
|
|
305
|
+
sym('import'), trivia, { name: [identifier] },
|
|
306
|
+
trivia, sym('from'), trivia, { module: [sym('string')] },
|
|
307
|
+
],
|
|
221
308
|
}
|
|
222
309
|
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
case '//':
|
|
229
|
-
case '/*': return state
|
|
230
|
-
case 'eof': return { state: 'error', error: { message: 'unexpected end', metadata } }
|
|
231
|
-
case 'id': {
|
|
232
|
-
if (token.value === 'from') return { ...state, state: 'import+from' }
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
return { state: 'error', error: { message: 'unexpected token', metadata } }
|
|
310
|
+
const constStatement = {
|
|
311
|
+
const: () => [
|
|
312
|
+
sym('const'), trivia, { name: [identifier] },
|
|
313
|
+
trivia, sym('='), trivia, { value: [value] },
|
|
314
|
+
],
|
|
236
315
|
}
|
|
237
316
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
switch (token.kind) {
|
|
241
|
-
case 'ws':
|
|
242
|
-
case 'nl':
|
|
243
|
-
case '//':
|
|
244
|
-
case '/*': return state
|
|
245
|
-
case 'string': {
|
|
246
|
-
const modules = concat(state.module.modules)([token.value])
|
|
247
|
-
return { ...state, state: 'nl', module: { ...state.module, modules: modules } }
|
|
248
|
-
}
|
|
249
|
-
case 'eof': return { state: 'error', error: { message: 'unexpected end', metadata } }
|
|
250
|
-
default: return { state: 'error', error: { message: 'unexpected token', metadata } }
|
|
251
|
-
}
|
|
317
|
+
const exportStatement = {
|
|
318
|
+
export: () => [sym('export'), trivia, sym('default'), trivia, { value: [value] }],
|
|
252
319
|
}
|
|
253
320
|
|
|
254
|
-
/**
|
|
255
|
-
|
|
321
|
+
/**
|
|
322
|
+
* The whole module: every `import` before every `const`, one `export default`
|
|
323
|
+
* last, and nothing but trivia after it.
|
|
324
|
+
*
|
|
325
|
+
* The ordering the hand-written parser enforces with a `consts.length === 0`
|
|
326
|
+
* check is just the shape of this rule, which is the point of writing the
|
|
327
|
+
* grammar down: `import* const* export`.
|
|
328
|
+
*
|
|
329
|
+
* Ending on `eof` is what makes a trailing stray token a failure rather than a
|
|
330
|
+
* short match — the backend synthesizes that symbol after the physical input.
|
|
331
|
+
*
|
|
332
|
+
* @type {Rule}
|
|
333
|
+
*/
|
|
334
|
+
const djsModule = () => [
|
|
335
|
+
trivia,
|
|
336
|
+
repeat0Plus([importStatement, statementEnd]),
|
|
337
|
+
repeat0Plus([constStatement, statementEnd]),
|
|
338
|
+
exportStatement,
|
|
339
|
+
trivia,
|
|
340
|
+
eof,
|
|
341
|
+
]
|
|
256
342
|
|
|
257
|
-
/**
|
|
258
|
-
|
|
343
|
+
/**
|
|
344
|
+
* The module matcher and the name of the rule to start it at.
|
|
345
|
+
*
|
|
346
|
+
* `toData` generates rule names, so the entry name belongs to the conversion and
|
|
347
|
+
* is read back from it rather than spelled here. Built once: converting the
|
|
348
|
+
* grammar and computing its nullability is per-grammar work, not per-parse.
|
|
349
|
+
*/
|
|
350
|
+
const [moduleRuleSet, moduleEntry] = toData(djsModule)
|
|
259
351
|
|
|
260
|
-
/** @type {
|
|
261
|
-
const
|
|
352
|
+
/** @type {DescentMatch<DjsTokenWithMetadata>} */
|
|
353
|
+
const moduleMatcher = descentParserRuleSet(moduleRuleSet)
|
|
262
354
|
|
|
263
355
|
/**
|
|
264
356
|
* The key of `{ __proto__: v }` and `{ "__proto__": v }`. JavaScript reads
|
|
@@ -269,91 +361,6 @@ const addToArray = array => value => (['array', concat(array[1])([value])])
|
|
|
269
361
|
*/
|
|
270
362
|
const protoKey = '__proto__'
|
|
271
363
|
|
|
272
|
-
/** @type {(valueState: _ValueState) => (state: _ParseValueState) => (key: string) => (metadata: TokenMetadata) => _ParserState} */
|
|
273
|
-
const pushKey = valueState => state => key => metadata => {
|
|
274
|
-
if (state.top?.[0] === 'object') { return { ...state, valueState, top: addKeyToObject(state.top)(key), stack: state.stack } }
|
|
275
|
-
return { state: 'error', error: { message: 'error', metadata } }
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* A key written as an identifier or a string literal, which is every key but
|
|
280
|
-
* the computed one — so this is where `__proto__` is refused. A JSON document
|
|
281
|
-
* spells that key the same way and means an ordinary property by it, but a
|
|
282
|
-
* JSON document is not a module and this parser does not read one
|
|
283
|
-
* ([spec: the `__proto__` key](../../../spec/README.md#the-__proto__-key)).
|
|
284
|
-
*
|
|
285
|
-
* @type {(state: _ParseValueState) => (key: string) => (metadata: TokenMetadata) => _ParserState}
|
|
286
|
-
*/
|
|
287
|
-
const pushPlainKey = state => key => metadata => key === protoKey
|
|
288
|
-
? { state: 'error', error: { message: '__proto__ requires the computed key form', metadata } }
|
|
289
|
-
: pushKey('{k')(state)(key)(metadata)
|
|
290
|
-
|
|
291
|
-
/** @type {(state: _ParseValueState) => (value: AstConst) => _ParserState} */
|
|
292
|
-
const pushValue = state => value => {
|
|
293
|
-
if (state.top === null) {
|
|
294
|
-
const consts = concat(state.module.consts)([value])
|
|
295
|
-
switch (state.state)
|
|
296
|
-
{
|
|
297
|
-
case 'exportValue': return { ...state, state: 'result', module: { ...state.module, consts: consts } }
|
|
298
|
-
case 'constValue': return { ...state, state: 'nl', module: { ...state.module, consts: consts } }
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
if (state.top?.[0] === 'array') { return { ...state, valueState: '[v', top: addToArray(state.top)(value), stack: state.stack } }
|
|
302
|
-
return { ...state, valueState: '{v', top: addValueToObject(state.top)(value), stack: state.stack }
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/** @type {(state: _ParseValueState) => (name: string) => (metadata: TokenMetadata) => _ParserState} */
|
|
306
|
-
const pushRef = state => name => metadata => {
|
|
307
|
-
const ref = at(name)(state.module.refs)
|
|
308
|
-
if (ref === null)
|
|
309
|
-
return { state: 'error', error: { message: 'const not found', metadata } }
|
|
310
|
-
return pushValue(state)(ref)
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/** @type {(state: _ParseValueState) => _ParserState} */
|
|
314
|
-
const startArray = state => {
|
|
315
|
-
const newStack = state.top === null ? null : { first: state.top, tail: state.stack }
|
|
316
|
-
return { ...state, valueState: '[', top: ['array', null], stack: newStack }
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Pops the enclosing container off `stack`. `next` is forced here rather than
|
|
320
|
-
// left as a `drop(1)` thunk: the stack is written only by startArray/startObject,
|
|
321
|
-
// always as a literal cons, and a lazy pop leaves one unforced thunk per closed
|
|
322
|
-
// container — a chain that overflows the call stack when it is finally forced.
|
|
323
|
-
/** @type {(state: _ParseValueState) => _ParseValueState} */
|
|
324
|
-
const popStack = state => {
|
|
325
|
-
const ne = next(state.stack)
|
|
326
|
-
return ne === null
|
|
327
|
-
? { ...state, valueState: '', top: null, stack: null }
|
|
328
|
-
: { ...state, valueState: '', top: ne.first, stack: ne.tail }
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/** @type {(state: _ParseValueState) => _ParserState} */
|
|
332
|
-
const endArray = state => {
|
|
333
|
-
const top = state.top
|
|
334
|
-
const newState = popStack(state)
|
|
335
|
-
if (top !== null && top[0] === 'array')
|
|
336
|
-
{
|
|
337
|
-
/** @type {AstArray} */
|
|
338
|
-
const array = ['array', toArray(top[1])]
|
|
339
|
-
return pushValue(newState)(array)
|
|
340
|
-
}
|
|
341
|
-
return pushValue(newState)(null)
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/** @type {(state: _ParseValueState) => _ParserState} */
|
|
345
|
-
const startObject = state => {
|
|
346
|
-
const newStack = state.top === null ? null : { first: state.top, tail: state.stack }
|
|
347
|
-
return { ...state, valueState: '{', top: ['object', null, ''], stack: newStack }
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/** @type {(state: _ParseValueState) => _ParserState} */
|
|
351
|
-
const endObject = state => {
|
|
352
|
-
const obj = state?.top !== null && state?.top[0] === 'object' ? fromMap(state.top[1]) : null
|
|
353
|
-
const newState = popStack(state)
|
|
354
|
-
return pushValue(newState)(obj)
|
|
355
|
-
}
|
|
356
|
-
|
|
357
364
|
/**
|
|
358
365
|
* Only ever called on a token `isValueToken` has already confirmed carries a
|
|
359
366
|
* value, so the switch covers every `_ValueToken` case with no fallback arm.
|
|
@@ -371,7 +378,6 @@ const tokenToValue = token => {
|
|
|
371
378
|
case 'undefined': return undefined
|
|
372
379
|
}
|
|
373
380
|
}
|
|
374
|
-
|
|
375
381
|
/**
|
|
376
382
|
* @param {DjsToken} token
|
|
377
383
|
* @returns {token is _ValueToken}
|
|
@@ -388,205 +394,338 @@ const isValueToken = token => {
|
|
|
388
394
|
default: return false
|
|
389
395
|
}
|
|
390
396
|
}
|
|
397
|
+
// -- folding the match into an `AstModule` ----------------------------------
|
|
391
398
|
|
|
392
|
-
/** @
|
|
393
|
-
const parseValueOp = ({ token, metadata }) => state => {
|
|
394
|
-
switch (token.kind)
|
|
395
|
-
{
|
|
396
|
-
case ']':
|
|
397
|
-
if (state.valueState === '[,') { return endArray(state) }
|
|
398
|
-
return { state: 'error', error: { message: 'unexpected token', metadata } }
|
|
399
|
-
case 'id': return pushRef(state)(token.value)(metadata)
|
|
400
|
-
case '[': return startArray(state)
|
|
401
|
-
case '{': return startObject(state)
|
|
402
|
-
case 'ws':
|
|
403
|
-
case 'nl':
|
|
404
|
-
case '//':
|
|
405
|
-
case '/*': return state
|
|
406
|
-
case 'eof': return { state: 'error', error: { message: 'unexpected end', metadata } }
|
|
407
|
-
default:
|
|
408
|
-
if (isValueToken(token)) { return pushValue(state)(tokenToValue(token)) }
|
|
409
|
-
return { state: 'error', error: { message: 'unexpected token', metadata } }
|
|
410
|
-
}
|
|
411
|
-
}
|
|
399
|
+
/** @typedef {Ast<CodePointMeta<DjsTokenWithMetadata>>} _Node */
|
|
412
400
|
|
|
413
|
-
/**
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
case 'eof': return { state: 'error', error: { message: 'unexpected end', metadata } }
|
|
427
|
-
default: return { state: 'error', error: { message: 'unexpected token', metadata } }
|
|
428
|
-
}
|
|
401
|
+
/**
|
|
402
|
+
* The token a slot holds.
|
|
403
|
+
*
|
|
404
|
+
* Every slot the fold reads holds exactly one token, and it is always the
|
|
405
|
+
* leftmost leaf — a name, a module specifier, a primitive. Walking first
|
|
406
|
+
* children rather than searching keeps this total: there is no "not found" case
|
|
407
|
+
* to branch on.
|
|
408
|
+
*
|
|
409
|
+
* @type {(node: _Node) => DjsTokenWithMetadata}
|
|
410
|
+
*/
|
|
411
|
+
const tokenOf = node => {
|
|
412
|
+
const first = node.sequence[0]
|
|
413
|
+
return first instanceof Array ? first[1] : tokenOf(first)
|
|
429
414
|
}
|
|
430
415
|
|
|
431
|
-
/**
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
416
|
+
/**
|
|
417
|
+
* A node's direct child carrying `tag`.
|
|
418
|
+
*
|
|
419
|
+
* Direct rather than recursive on purpose: a statement's `name` slot holds an
|
|
420
|
+
* identifier whose own tag may be `const` or `import`, so a search through the
|
|
421
|
+
* subtree would confuse a *word* with the statement spelling it.
|
|
422
|
+
*
|
|
423
|
+
* @type {(tag: string) => (node: _Node) => _Node}
|
|
424
|
+
*/
|
|
425
|
+
const slot = tag => node => {
|
|
426
|
+
const found = node.sequence.find(c => !(c instanceof Array) && c.tag === tag)
|
|
427
|
+
assert(found !== undefined && !(found instanceof Array), ['grammar slot missing', tag])
|
|
428
|
+
return found
|
|
444
429
|
}
|
|
445
430
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
431
|
+
/**
|
|
432
|
+
* Every node tagged `tag` under `node`, in document order.
|
|
433
|
+
*
|
|
434
|
+
* An array's elements are not its direct children — they sit inside the option
|
|
435
|
+
* and repetition scaffolding `delimited` builds — so finding them takes a
|
|
436
|
+
* search rather than a lookup.
|
|
437
|
+
*
|
|
438
|
+
* The search cannot stray into a nested value, and needs no guard saying so:
|
|
439
|
+
* every element, member and statement is wrapped in a node carrying its own
|
|
440
|
+
* tag, so the wrapper matches and the search stops there, before it could
|
|
441
|
+
* descend into the array or object inside it. That is what the wrappers are
|
|
442
|
+
* for.
|
|
443
|
+
*
|
|
444
|
+
* Iterative, over an explicit stack, for the same reason {@link foldValue} is.
|
|
445
|
+
* A repetition is only *flat* in the AST when `toData` recognizes the
|
|
446
|
+
* right-recursive shape and emits a `Repeat`; nested inside this grammar's
|
|
447
|
+
* option scaffolding it does not, so a thousand siblings are a thousand levels
|
|
448
|
+
* of tree, and recursing over them overflows exactly as deep nesting would.
|
|
449
|
+
*
|
|
450
|
+
* @type {(tag: string) => (node: _Node) => readonly _Node[]}
|
|
451
|
+
*/
|
|
452
|
+
const descendantsTagged = tag => root => {
|
|
453
|
+
/** Pushes a node's children so the leftmost is visited first. */
|
|
454
|
+
/** @type {(rest: List<_Node>, sequence: AstSequence<CodePointMeta<DjsTokenWithMetadata>>) => List<_Node>} */
|
|
455
|
+
const pushChildren = (rest, sequence) => {
|
|
456
|
+
let stack = rest
|
|
457
|
+
let i = sequence.length
|
|
458
|
+
while (i !== 0) {
|
|
459
|
+
i = i - 1
|
|
460
|
+
const child = sequence[i]
|
|
461
|
+
if (!(child instanceof Array)) { stack = { first: child, tail: stack } }
|
|
462
|
+
}
|
|
463
|
+
return stack
|
|
462
464
|
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
default: return { state: 'error', error: { message: 'unexpected token', metadata } }
|
|
465
|
+
/** @type {List<_Node>} */
|
|
466
|
+
let found = null
|
|
467
|
+
let stack = pushChildren(null, root.sequence)
|
|
468
|
+
for (;;) {
|
|
469
|
+
const top = next(stack)
|
|
470
|
+
if (top === null) { return toArray(found) }
|
|
471
|
+
const node = top.first
|
|
472
|
+
if (node.tag === tag) {
|
|
473
|
+
found = concat(found)([node])
|
|
474
|
+
stack = top.tail
|
|
475
|
+
} else {
|
|
476
|
+
stack = pushChildren(top.tail, node.sequence)
|
|
477
|
+
}
|
|
477
478
|
}
|
|
478
479
|
}
|
|
479
480
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
481
|
+
const valueSlot = slot('value')
|
|
482
|
+
|
|
483
|
+
const nameSlot = slot('name')
|
|
484
|
+
|
|
485
|
+
const moduleSlot = slot('module')
|
|
486
|
+
|
|
487
|
+
const keySlot = slot('key')
|
|
488
|
+
|
|
489
|
+
const itemsOf = descendantsTagged('item')
|
|
490
|
+
|
|
491
|
+
const membersOf = descendantsTagged('member')
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* The property name a key spells, and whether it was the computed spelling.
|
|
495
|
+
*
|
|
496
|
+
* The distinction exists for `__proto__` alone: JavaScript reads a bare or
|
|
497
|
+
* string `__proto__` as an instruction to replace the prototype, while
|
|
498
|
+
* `{ ["__proto__"]: v }` denotes an ordinary property — so only the spelling
|
|
499
|
+
* separates a rejected key from an accepted one.
|
|
500
|
+
*
|
|
501
|
+
* @type {(node: _Node) => readonly[string, boolean]}
|
|
502
|
+
*/
|
|
503
|
+
const keyOf = node => {
|
|
504
|
+
const spelling = node.sequence[0]
|
|
505
|
+
assert(!(spelling instanceof Array), 'a key held no spelling')
|
|
506
|
+
const computed = spelling.tag === 'computed'
|
|
507
|
+
const { token } = tokenOf(computed ? nameSlot(spelling) : spelling)
|
|
508
|
+
assert('value' in token && typeof token.value === 'string', 'a key token carried no name')
|
|
509
|
+
return [token.value, computed]
|
|
492
510
|
}
|
|
493
511
|
|
|
494
|
-
/**
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
512
|
+
/**
|
|
513
|
+
* A fold in progress: the names bound so far, the module specifiers and the
|
|
514
|
+
* body collected so far, and the first error if one has been met.
|
|
515
|
+
*
|
|
516
|
+
* The error rides in the state rather than wrapping every step in a `Result`,
|
|
517
|
+
* so a step reads as one expression instead of a nested match. Once set it is
|
|
518
|
+
* never replaced, which is what makes the reported error the *first* one.
|
|
519
|
+
*
|
|
520
|
+
* @typedef {{
|
|
521
|
+
* readonly refs: OrderedMap<AstModuleRef>
|
|
522
|
+
* readonly modules: readonly string[]
|
|
523
|
+
* readonly consts: readonly AstConst[]
|
|
524
|
+
* readonly error: ParseError | null
|
|
525
|
+
* }} _FoldState
|
|
526
|
+
*/
|
|
527
|
+
|
|
528
|
+
/** @type {(message: string) => (token: DjsTokenWithMetadata) => ParseError} */
|
|
529
|
+
const foldError = message => ({ metadata }) => ({ message, metadata })
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Binds a name to a reference, rejecting one already bound.
|
|
533
|
+
*
|
|
534
|
+
* `import` and `const` share one map, so a name taken by either is taken for
|
|
535
|
+
* both — the same rule the state machine gets from consulting one `refs`.
|
|
536
|
+
*
|
|
537
|
+
* @type {(state: _FoldState) => (node: _Node) => (ref: AstModuleRef) => _FoldState}
|
|
538
|
+
*/
|
|
539
|
+
const bind = state => node => ref => {
|
|
540
|
+
const withMetadata = tokenOf(nameSlot(node))
|
|
541
|
+
const { token } = withMetadata
|
|
542
|
+
assert('value' in token && typeof token.value === 'string', 'a name token carried no name')
|
|
543
|
+
return at(token.value)(state.refs) !== null
|
|
544
|
+
? { ...state, error: foldError('duplicate id')(withMetadata) }
|
|
545
|
+
: { ...state, refs: setReplace(token.value)(ref)(state.refs) }
|
|
506
546
|
}
|
|
507
547
|
|
|
508
|
-
/**
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
548
|
+
/**
|
|
549
|
+
* A frame of {@link foldValue}'s explicit stack: the container being built, the
|
|
550
|
+
* element nodes still to read, and what has been built so far.
|
|
551
|
+
*
|
|
552
|
+
* `done` is a `List` rather than an array because a frame gains one element at a
|
|
553
|
+
* time: appending to an array per element would copy the whole prefix each time,
|
|
554
|
+
* which is what makes the obvious spelling quadratic in an array's length.
|
|
555
|
+
*
|
|
556
|
+
* @typedef {{
|
|
557
|
+
* readonly items: readonly _Node[]
|
|
558
|
+
* readonly index: number
|
|
559
|
+
* readonly array: List<AstConst>
|
|
560
|
+
* readonly object: OrderedMap<AstConst>
|
|
561
|
+
* readonly keys: readonly(readonly[string, boolean])[]
|
|
562
|
+
* readonly isArray: boolean
|
|
563
|
+
* }} _FoldFrame
|
|
564
|
+
*/
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* The error a frame's current key earns, or `null`.
|
|
568
|
+
*
|
|
569
|
+
* Checked as each member is reached rather than by scanning every key first, so
|
|
570
|
+
* that an earlier member's failure is reported before a later key's. Scanning
|
|
571
|
+
* ahead reported `__proto__` in `{a: missing, __proto__: 1}`, where the parser
|
|
572
|
+
* this replaces reports the unresolved `missing` — errors are first-to-last, and
|
|
573
|
+
* a key is not special enough to jump the queue.
|
|
574
|
+
*
|
|
575
|
+
* @type {(frame: _FoldFrame) => ParseError | null}
|
|
576
|
+
*/
|
|
577
|
+
const badKey = frame => {
|
|
578
|
+
if (frame.isArray) { return null }
|
|
579
|
+
const [name, computed] = frame.keys[frame.index]
|
|
580
|
+
return name === protoKey && !computed
|
|
581
|
+
// at the key itself, not at the object's `{`
|
|
582
|
+
? foldError('__proto__ requires the computed key form')(tokenOf(keySlot(frame.items[frame.index])))
|
|
583
|
+
: null
|
|
523
584
|
}
|
|
524
585
|
|
|
525
|
-
/**
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
586
|
+
/**
|
|
587
|
+
* A value, resolved against the names bound so far.
|
|
588
|
+
*
|
|
589
|
+
* Iterative, over an explicit stack, because a value nests arbitrarily and the
|
|
590
|
+
* call stack does not: recursion here overflows at a few thousand containers,
|
|
591
|
+
* which is the defect `containerStackCost` was written to catch when the parser
|
|
592
|
+
* this replaced had its own version of it.
|
|
593
|
+
*
|
|
594
|
+
* Returns the error channel alongside the value because a reference can fail to
|
|
595
|
+
* resolve at any depth, and a container has to stop building when one does. A
|
|
596
|
+
* failed fold yields `null` for the value, which is never mistaken for a
|
|
597
|
+
* successful `null` — the caller reads the error, not the value.
|
|
598
|
+
*
|
|
599
|
+
* @type {(state: _FoldState) => (node: _Node) => readonly[AstConst, ParseError | null]}
|
|
600
|
+
*/
|
|
601
|
+
const foldValue = state => root => {
|
|
602
|
+
/** @type {List<_FoldFrame>} */
|
|
603
|
+
let stack = null
|
|
604
|
+
let node = root
|
|
605
|
+
/** @type {AstConst} */
|
|
606
|
+
let value = null
|
|
607
|
+
// `true` while descending into `node`; `false` while handing `value` back
|
|
608
|
+
// to the frame that asked for it.
|
|
609
|
+
let descending = true
|
|
610
|
+
for (;;) {
|
|
611
|
+
if (descending) {
|
|
612
|
+
const child = node.sequence[0]
|
|
613
|
+
assert(!(child instanceof Array), 'a value slot held no value')
|
|
614
|
+
if (child.tag === 'array' || child.tag === 'object') {
|
|
615
|
+
const isArray = child.tag === 'array'
|
|
616
|
+
const items = isArray ? itemsOf(child) : membersOf(child)
|
|
617
|
+
const keys = isArray ? [] : items.map(member => keyOf(keySlot(member)))
|
|
618
|
+
/** @type {_FoldFrame} */
|
|
619
|
+
const frame = { items, index: 0, array: null, object: null, keys, isArray }
|
|
620
|
+
stack = { first: frame, tail: stack }
|
|
621
|
+
if (items.length === 0) {
|
|
622
|
+
value = isArray ? ['array', []] : fromMap(null)
|
|
623
|
+
stack = assertNotNullish(next(stack)).tail
|
|
624
|
+
descending = false
|
|
625
|
+
} else {
|
|
626
|
+
const rejected = badKey(frame)
|
|
627
|
+
if (rejected !== null) { return [null, rejected] }
|
|
628
|
+
node = isArray ? items[0] : valueSlot(items[0])
|
|
629
|
+
}
|
|
630
|
+
} else {
|
|
631
|
+
const withMetadata = tokenOf(child)
|
|
632
|
+
const { token } = withMetadata
|
|
633
|
+
if (isValueToken(token)) {
|
|
634
|
+
value = tokenToValue(token)
|
|
635
|
+
} else {
|
|
636
|
+
// anything else the value rule admits is an identifier, so
|
|
637
|
+
// it names a `const` or an `import` — or nothing, which is
|
|
638
|
+
// the error.
|
|
639
|
+
assert('value' in token && typeof token.value === 'string', 'a reference carried no name')
|
|
640
|
+
const ref = at(token.value)(state.refs)
|
|
641
|
+
if (ref === null) { return [null, foldError('const not found')(withMetadata)] }
|
|
642
|
+
value = ref
|
|
643
|
+
}
|
|
644
|
+
descending = false
|
|
645
|
+
}
|
|
646
|
+
} else {
|
|
647
|
+
/** @type {{ readonly first: _FoldFrame, readonly tail: List<_FoldFrame> } | null} */
|
|
648
|
+
const top = next(stack)
|
|
649
|
+
if (top === null) { return [value, null] }
|
|
650
|
+
/** @type {_FoldFrame} */
|
|
651
|
+
const frame = top.first
|
|
652
|
+
const index = frame.index + 1
|
|
653
|
+
const array = frame.isArray ? concat(frame.array)([value]) : frame.array
|
|
654
|
+
const object = frame.isArray
|
|
655
|
+
? frame.object
|
|
656
|
+
: setReplace(frame.keys[frame.index][0])(value)(frame.object)
|
|
657
|
+
if (index === frame.items.length) {
|
|
658
|
+
/** @type {AstArray} */
|
|
659
|
+
const asArray = ['array', toArray(array)]
|
|
660
|
+
/** @type {AstObject} */
|
|
661
|
+
const asObject = fromMap(object)
|
|
662
|
+
value = frame.isArray ? asArray : asObject
|
|
663
|
+
stack = top.tail
|
|
664
|
+
} else {
|
|
665
|
+
const moved = { ...frame, index, array, object }
|
|
666
|
+
const rejected = badKey(moved)
|
|
667
|
+
if (rejected !== null) { return [null, rejected] }
|
|
668
|
+
stack = { first: moved, tail: top.tail }
|
|
669
|
+
node = frame.isArray ? frame.items[index] : valueSlot(frame.items[index])
|
|
670
|
+
descending = true
|
|
671
|
+
}
|
|
672
|
+
}
|
|
537
673
|
}
|
|
538
674
|
}
|
|
539
675
|
|
|
540
|
-
/**
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
case '//':
|
|
552
|
-
case '/*': return state
|
|
553
|
-
case 'eof': return { state: 'error', error: { message: 'unexpected end', metadata } }
|
|
554
|
-
default: return { state: 'error', error: { message: 'unexpected token', metadata } }
|
|
555
|
-
}
|
|
676
|
+
/**
|
|
677
|
+
* Adds one statement's value to the body, if nothing has failed yet.
|
|
678
|
+
*
|
|
679
|
+
* @type {(state: _FoldState) => (node: _Node) => _FoldState}
|
|
680
|
+
*/
|
|
681
|
+
const addValue = state => node => {
|
|
682
|
+
if (state.error !== null) { return state }
|
|
683
|
+
const [value, valueError] = foldValue(state)(valueSlot(node))
|
|
684
|
+
return valueError !== null
|
|
685
|
+
? { ...state, error: valueError }
|
|
686
|
+
: { ...state, consts: [...state.consts, value] }
|
|
556
687
|
}
|
|
557
688
|
|
|
558
|
-
/**
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
689
|
+
/**
|
|
690
|
+
* Folds a matched module into an `AstModule`.
|
|
691
|
+
*
|
|
692
|
+
* The statements are read positionally from the root — trivia, imports,
|
|
693
|
+
* consts, the export — because the module rule is one sequence and its parts
|
|
694
|
+
* cannot move. Everything below that is read by slot name instead.
|
|
695
|
+
*
|
|
696
|
+
* Each name is bound *before* the value that follows it is folded, which is
|
|
697
|
+
* what makes `const a = a` resolve to the constant being defined rather than
|
|
698
|
+
* fail. That is the state machine's order too, and the reason it is not one of
|
|
699
|
+
* the divergences.
|
|
700
|
+
*
|
|
701
|
+
* @type {(root: _Node) => Result<AstModule, ParseError>}
|
|
702
|
+
*/
|
|
703
|
+
const foldModule = root => {
|
|
704
|
+
const [, imports, consts, exported] = root.sequence
|
|
705
|
+
assert(!(imports instanceof Array) && !(consts instanceof Array) && !(exported instanceof Array),
|
|
706
|
+
'the module rule did not produce its statement groups')
|
|
707
|
+
/** @type {_FoldState} */
|
|
708
|
+
let state = { refs: null, modules: [], consts: [], error: null }
|
|
709
|
+
for (const statement of descendantsTagged('import')(imports)) {
|
|
710
|
+
state = bind(state)(statement)(['aref', state.modules.length])
|
|
711
|
+
if (state.error !== null) { break }
|
|
712
|
+
const specifier = tokenOf(moduleSlot(statement))
|
|
713
|
+
assert('value' in specifier.token && typeof specifier.token.value === 'string',
|
|
714
|
+
'an import specifier carried no text')
|
|
715
|
+
state = { ...state, modules: [...state.modules, specifier.token.value] }
|
|
716
|
+
}
|
|
717
|
+
for (const statement of descendantsTagged('const')(consts)) {
|
|
718
|
+
if (state.error !== null) { break }
|
|
719
|
+
state = bind(state)(statement)(['cref', state.consts.length])
|
|
720
|
+
state = addValue(state)(statement)
|
|
589
721
|
}
|
|
722
|
+
state = addValue(state)(exported)
|
|
723
|
+
if (state.error !== null) { return error(state.error) }
|
|
724
|
+
// annotated rather than inferred: a bare `[modules, consts]` widens to an
|
|
725
|
+
// array, because `readonly string[]` is itself assignable to `AstBody`.
|
|
726
|
+
/** @type {AstModule} */
|
|
727
|
+
const astModule = [state.modules, state.consts]
|
|
728
|
+
return ok(astModule)
|
|
590
729
|
}
|
|
591
730
|
|
|
592
731
|
/**
|
|
@@ -597,70 +736,138 @@ const foldOp = token => state => {
|
|
|
597
736
|
* module, and `fjs/media/json` is its reader
|
|
598
737
|
* ([spec: JSON input](../../../spec/README.md#json-input)).
|
|
599
738
|
*
|
|
739
|
+
* The grammar it accepts is written down, in the rules above, rather than
|
|
740
|
+
* implied by the control flow of a state machine — which is what this replaced.
|
|
741
|
+
*
|
|
600
742
|
* @type {(tokenList: List<DjsTokenWithMetadata>) => Result<AstModule, ParseError>}
|
|
601
743
|
*/
|
|
602
744
|
export const parseFromTokens = tokenList => {
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
745
|
+
const [tag, stream] = splitEof(toArray(tokenList))
|
|
746
|
+
if (tag === 'error') { return error(stream) }
|
|
747
|
+
const { tokens, eofMetadata } = stream
|
|
748
|
+
const { ast, success, failure } = moduleMatcher(moduleEntry, tokens.map(tokenToSymbol))
|
|
749
|
+
if (!success) {
|
|
750
|
+
const { idx } = assertNotNullish(failure)
|
|
751
|
+
// A failure past the last token is the end of input rather than a token
|
|
752
|
+
// the reader can point at, and the hand-written parser words it that
|
|
753
|
+
// way; matching it costs one comparison already being made.
|
|
754
|
+
const atEnd = idx >= tokens.length
|
|
755
|
+
return error({
|
|
756
|
+
message: atEnd ? 'unexpected end' : 'unexpected token',
|
|
757
|
+
metadata: atEnd ? eofMetadata : tokens[idx].metadata,
|
|
758
|
+
})
|
|
608
759
|
}
|
|
760
|
+
return foldModule(ast)
|
|
609
761
|
}
|
|
610
762
|
|
|
763
|
+
/** @type {(kind: 'eof' | ',') => (line: number) => DjsTokenWithMetadata} */
|
|
764
|
+
const proofToken = kind => line => ({ token: { kind }, metadata: { path: 'a.js', line, column: 1 } })
|
|
765
|
+
|
|
766
|
+
const proofEof = proofToken('eof')
|
|
767
|
+
|
|
768
|
+
const proofComma = proofToken(',')
|
|
769
|
+
|
|
611
770
|
export const proof = {
|
|
612
|
-
|
|
613
|
-
// `
|
|
614
|
-
//
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
const state = {
|
|
620
|
-
state: 'exportValue',
|
|
621
|
-
module: { refs: null, modules: null, consts: null },
|
|
622
|
-
valueState: '[',
|
|
623
|
-
top: null,
|
|
624
|
-
stack: null,
|
|
625
|
-
}
|
|
626
|
-
const result = pushKey('{k')(state)('key')({ path: 'test', line: 0, column: 0 })
|
|
627
|
-
assertEq(result.state, 'error')
|
|
771
|
+
ordinaryTokenNames: {
|
|
772
|
+
// `_AlphabetIsComplete` pins membership at compile time, but a repeated
|
|
773
|
+
// name widens to the same union and so is invisible to it. The check
|
|
774
|
+
// matters because the token-symbol mapping this alphabet feeds has to be
|
|
775
|
+
// injective over it — two entries for one name would break that.
|
|
776
|
+
noDuplicates: () => {
|
|
777
|
+
assertEq(new Set(ordinaryTokenNames).size, ordinaryTokenNames.length)
|
|
628
778
|
},
|
|
629
779
|
},
|
|
630
|
-
|
|
631
|
-
//
|
|
632
|
-
//
|
|
633
|
-
//
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
780
|
+
tokenToSymbol: {
|
|
781
|
+
// One symbol per token, distinct across the alphabet, and above every
|
|
782
|
+
// code point — the three properties that let a token stream be the
|
|
783
|
+
// alphabet of the layer above.
|
|
784
|
+
distinctAndAboveUnicode: () => {
|
|
785
|
+
const symbols = ordinaryTokenNames.map(n => tokenEncoding.encode(n))
|
|
786
|
+
assertEq(new Set(symbols).size, ordinaryTokenNames.length)
|
|
787
|
+
const [, unicodeLast] = rangeDecode(unicodeRange)
|
|
788
|
+
assert(symbols.every(s => s > unicodeLast), JSON.stringify(symbols))
|
|
789
|
+
},
|
|
790
|
+
// The distinction the hand-written parser makes by comparing
|
|
791
|
+
// `token.value`: a framing keyword and an ordinary identifier arrive as
|
|
792
|
+
// the same `id` kind, and the grammar can only tell them apart if they
|
|
793
|
+
// get different symbols here.
|
|
794
|
+
framingKeywordsAreNotIdentifiers: () => {
|
|
795
|
+
/** @type {(value: string) => number} */
|
|
796
|
+
const symbolOf = value =>
|
|
797
|
+
tokenToSymbol({ token: { kind: 'id', value }, metadata: { path: 'a.js', line: 1, column: 1 } })[0]
|
|
798
|
+
const id = symbolOf('foo')
|
|
799
|
+
const keywords = framingKeywords.map(symbolOf)
|
|
800
|
+
assert(keywords.every(s => s !== id), JSON.stringify([id, keywords]))
|
|
801
|
+
assertEq(new Set(keywords).size, framingKeywords.length)
|
|
802
|
+
assertEq(tokenEncoding.decode(symbolOf('export')), 'export')
|
|
803
|
+
assertEq(tokenEncoding.decode(id), 'id')
|
|
804
|
+
},
|
|
805
|
+
// The token rides along untouched, so a fold or a diagnostic above still
|
|
806
|
+
// has its value and its position.
|
|
807
|
+
carriesTheToken: () => {
|
|
808
|
+
/** @type {DjsTokenWithMetadata} */
|
|
809
|
+
const t = { token: { kind: 'string', value: 'v' }, metadata: { path: 'a.js', line: 7, column: 1 } }
|
|
810
|
+
const [symbol, meta] = tokenToSymbol(t)
|
|
811
|
+
assertEq(meta, t)
|
|
812
|
+
assertEq(meta.metadata.line, 7)
|
|
813
|
+
assertEq(tokenEncoding.decode(symbol), 'string')
|
|
814
|
+
},
|
|
815
|
+
throw: {
|
|
816
|
+
eofRejected: () => tokenToSymbol(proofEof(1)),
|
|
646
817
|
},
|
|
647
818
|
},
|
|
648
|
-
|
|
649
|
-
//
|
|
650
|
-
//
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
819
|
+
splitEof: {
|
|
820
|
+
// The tokenizer always ends its stream with one `eof`, so every branch
|
|
821
|
+
// but this one is reachable only from a hand-built token list.
|
|
822
|
+
final: () => {
|
|
823
|
+
const [tag, value] = splitEof([proofComma(1), proofEof(2)])
|
|
824
|
+
assert(tag === 'ok', tag)
|
|
825
|
+
assertEq(value.tokens.length, 1)
|
|
826
|
+
assertEq(value.eofMetadata.line, 2)
|
|
827
|
+
},
|
|
828
|
+
onlyEof: () => {
|
|
829
|
+
const [tag, value] = splitEof([proofEof(1)])
|
|
830
|
+
assert(tag === 'ok', tag)
|
|
831
|
+
assertEq(value.tokens.length, 0)
|
|
832
|
+
assertEq(value.eofMetadata.line, 1)
|
|
833
|
+
},
|
|
834
|
+
missing: () => {
|
|
835
|
+
const [tag, value] = splitEof([proofComma(1)])
|
|
836
|
+
assert(tag === 'error', tag)
|
|
837
|
+
assertEq(value.metadata, null)
|
|
838
|
+
},
|
|
839
|
+
// An empty stream is also missing its `eof`, and has not even a last
|
|
840
|
+
// token to blame it on. The tokenizer never produces one — an empty
|
|
841
|
+
// source still yields `eof` — so only a hand-built list reaches here.
|
|
842
|
+
empty: () => {
|
|
843
|
+
const [tag, value] = splitEof(null)
|
|
844
|
+
assert(tag === 'error', tag)
|
|
845
|
+
assertEq(value.metadata, null)
|
|
846
|
+
},
|
|
847
|
+
// The one stream with no `eof` that is not a broken contract: a lexical
|
|
848
|
+
// failure stops the tokenizer at an `error` token. The position has to
|
|
849
|
+
// survive, or "unterminated string at 1:11" would be reported as
|
|
850
|
+
// "missing end-of-input token" with nowhere to point.
|
|
851
|
+
lexicalError: () => {
|
|
852
|
+
/** @type {DjsTokenWithMetadata} */
|
|
853
|
+
const errorToken = {
|
|
854
|
+
token: { kind: 'error', message: 'unterminated string literal' },
|
|
855
|
+
metadata: { path: 'a.js', line: 3, column: 7 },
|
|
661
856
|
}
|
|
662
|
-
const
|
|
663
|
-
|
|
857
|
+
const [tag, value] = splitEof([errorToken])
|
|
858
|
+
assert(tag === 'error', tag)
|
|
859
|
+
assertEq(value.metadata?.line, 3)
|
|
860
|
+
assertEq(value.metadata?.column, 7)
|
|
861
|
+
},
|
|
862
|
+
notFinal: () => {
|
|
863
|
+
const [tag, value] = splitEof([proofEof(1), proofComma(2)])
|
|
864
|
+
assert(tag === 'error', tag)
|
|
865
|
+
assertEq(value.metadata?.line, 1)
|
|
866
|
+
},
|
|
867
|
+
duplicate: () => {
|
|
868
|
+
const [tag, value] = splitEof([proofEof(1), proofEof(2)])
|
|
869
|
+
assert(tag === 'error', tag)
|
|
870
|
+
assertEq(value.metadata?.line, 1)
|
|
664
871
|
},
|
|
665
872
|
},
|
|
666
873
|
}
|