functionalscript 0.46.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/fjs/basen/cbase32/module.f.mjs +18 -10
  2. package/fjs/basen/cbase32/proof.f.d.mts +3 -0
  3. package/fjs/basen/cbase32/proof.f.mjs +24 -2
  4. package/fjs/basen/proof.f.mjs +8 -16
  5. package/fjs/bnf/ll1/proof.f.d.mts +2 -0
  6. package/fjs/bnf/ll1/proof.f.mjs +33 -18
  7. package/fjs/bnf/module.f.d.mts +2 -2
  8. package/fjs/bnf/module.f.mjs +2 -2
  9. package/fjs/cas/proof.f.mjs +4 -26
  10. package/fjs/ci/common/module.f.mjs +6 -0
  11. package/fjs/ci/config/module.f.d.mts +7 -7
  12. package/fjs/ci/config/module.f.mjs +7 -7
  13. package/fjs/cli/module.f.mjs +14 -10
  14. package/fjs/crypto/vdf/module.f.d.mts +0 -1
  15. package/fjs/crypto/vdf/module.f.mjs +3 -14
  16. package/fjs/dev/module.f.mjs +10 -20
  17. package/fjs/djs/parser/module.f.d.mts +101 -53
  18. package/fjs/djs/parser/module.f.mjs +732 -525
  19. package/fjs/djs/parser/proof.f.d.mts +6 -0
  20. package/fjs/djs/parser/proof.f.mjs +315 -11
  21. package/fjs/djs/parser/types.d.ts +29 -2
  22. package/fjs/djs/tokenizer/module.f.mjs +122 -105
  23. package/fjs/djs/tokenizer/proof.f.mjs +17 -0
  24. package/fjs/djs/transpiler/module.f.mjs +4 -5
  25. package/fjs/djs/transpiler/proof.f.d.mts +2 -0
  26. package/fjs/djs/transpiler/proof.f.mjs +25 -0
  27. package/fjs/edag/amnesia/module.f.d.mts +20 -0
  28. package/fjs/edag/amnesia/module.f.mjs +303 -0
  29. package/fjs/edag/amnesia/proof.f.d.mts +79 -0
  30. package/fjs/edag/amnesia/proof.f.mjs +573 -0
  31. package/fjs/edag/amnesia/types.d.ts +15 -0
  32. package/fjs/edag/module.f.d.mts +410 -0
  33. package/fjs/edag/module.f.mjs +559 -0
  34. package/fjs/edag/proof.f.d.mts +116 -0
  35. package/fjs/edag/proof.f.mjs +686 -0
  36. package/fjs/edag/types.d.ts +55 -0
  37. package/fjs/effects/node/memory/module.d.mts +37 -3
  38. package/fjs/effects/node/memory/module.mjs +38 -4
  39. package/fjs/effects/node/memory/proof.d.mts +5 -2
  40. package/fjs/effects/node/memory/proof.mjs +36 -6
  41. package/fjs/effects/node/module.d.mts +9 -1
  42. package/fjs/effects/node/module.f.d.mts +39 -0
  43. package/fjs/effects/node/module.f.mjs +46 -0
  44. package/fjs/effects/node/module.mjs +234 -32
  45. package/fjs/effects/node/types.d.ts +28 -2
  46. package/fjs/effects/node/virtual/module.f.d.mts +19 -10
  47. package/fjs/effects/node/virtual/module.f.mjs +246 -32
  48. package/fjs/effects/node/virtual/proof.f.d.mts +17 -2
  49. package/fjs/effects/node/virtual/proof.f.mjs +232 -8
  50. package/fjs/effects/node/virtual/types.d.ts +57 -1
  51. package/fjs/emergent_testing/browser/proof.d.mts +55 -0
  52. package/fjs/emergent_testing/browser/proof.mjs +352 -0
  53. package/fjs/emergent_testing/browser/species.proof.d.mts +4 -0
  54. package/fjs/emergent_testing/browser/species.proof.mjs +45 -0
  55. package/fjs/emergent_testing/browser.d.mts +79 -0
  56. package/fjs/emergent_testing/browser.mjs +432 -0
  57. package/fjs/emergent_testing/proof.f.d.mts +1 -1
  58. package/fjs/emergent_testing/proof.f.mjs +2 -2
  59. package/fjs/js/string_escape/module.f.d.mts +47 -0
  60. package/fjs/js/string_escape/module.f.mjs +95 -0
  61. package/fjs/js/string_escape/proof.f.d.mts +8 -0
  62. package/fjs/js/string_escape/proof.f.mjs +68 -0
  63. package/fjs/js/tokenizer/module.f.mjs +14 -12
  64. package/fjs/mcp/cas/module.f.d.mts +1 -7
  65. package/fjs/mcp/cas/module.f.mjs +24 -15
  66. package/fjs/mcp/evo/module.f.mjs +9 -24
  67. package/fjs/mcp/module.f.d.mts +3 -1
  68. package/fjs/mcp/module.f.mjs +4 -2
  69. package/fjs/media/json/rtti/module.f.d.mts +2 -2
  70. package/fjs/media/json/rtti/module.f.mjs +1 -1
  71. package/fjs/media/json/schema/module.f.d.mts +15 -5
  72. package/fjs/media/json/schema/module.f.mjs +42 -15
  73. package/fjs/media/json/schema/proof.f.d.mts +10 -1
  74. package/fjs/media/json/schema/proof.f.mjs +88 -16
  75. package/fjs/media/json/serializer/module.f.mjs +14 -26
  76. package/fjs/media/json/types.d.ts +1 -1
  77. package/fjs/media/lock/module.f.d.mts +8 -3
  78. package/fjs/media/lock/module.f.mjs +9 -3
  79. package/fjs/media/lock/proof.f.d.mts +1 -1
  80. package/fjs/media/lock/proof.f.mjs +2 -2
  81. package/fjs/media/lock/types.d.ts +1 -3
  82. package/fjs/media/module.f.d.mts +20 -13
  83. package/fjs/media/module.f.mjs +29 -16
  84. package/fjs/media/note/module.f.d.mts +14 -7
  85. package/fjs/media/note/module.f.mjs +14 -7
  86. package/fjs/media/note/proof.f.mjs +1 -1
  87. package/fjs/media/proof.f.mjs +5 -5
  88. package/fjs/media/revision/module.f.d.mts +8 -2
  89. package/fjs/media/revision/module.f.mjs +9 -3
  90. package/fjs/media/revision/proof.f.mjs +1 -1
  91. package/fjs/media/revision/types.d.ts +1 -1
  92. package/fjs/media/type/module.f.d.mts +20 -0
  93. package/fjs/media/type/module.f.mjs +74 -0
  94. package/fjs/media/type/proof.f.d.mts +5 -0
  95. package/fjs/media/type/proof.f.mjs +24 -1
  96. package/fjs/module.f.mjs +6 -0
  97. package/fjs/path/module.f.d.mts +60 -3
  98. package/fjs/path/module.f.mjs +138 -14
  99. package/fjs/path/proof.f.d.mts +3 -0
  100. package/fjs/path/proof.f.mjs +298 -2
  101. package/fjs/protocol/json_rpc/module.f.d.mts +65 -28
  102. package/fjs/protocol/json_rpc/module.f.mjs +49 -13
  103. package/fjs/protocol/json_rpc/proof.f.d.mts +4 -0
  104. package/fjs/protocol/json_rpc/proof.f.mjs +29 -0
  105. package/fjs/protocol/json_rpc/types.d.ts +12 -2
  106. package/fjs/protocol/mcp/module.f.d.mts +89 -54
  107. package/fjs/protocol/mcp/module.f.mjs +110 -56
  108. package/fjs/protocol/mcp/proof.f.d.mts +13 -0
  109. package/fjs/protocol/mcp/proof.f.mjs +107 -7
  110. package/fjs/protocol/mcp/stdio/module.f.mjs +3 -4
  111. package/fjs/protocol/mcp/stdio/proof.f.mjs +2 -2
  112. package/fjs/protocol/mcp/types.d.ts +14 -2
  113. package/fjs/text/utf8/module.f.mjs +27 -1
  114. package/fjs/text/utf8/proof.f.mjs +37 -0
  115. package/fjs/types/array/module.f.mjs +18 -11
  116. package/fjs/types/array/proof.f.mjs +27 -1
  117. package/fjs/types/bigfloat/module.f.d.mts +48 -9
  118. package/fjs/types/bigfloat/module.f.mjs +147 -59
  119. package/fjs/types/bigfloat/proof.f.d.mts +2 -0
  120. package/fjs/types/bigfloat/proof.f.mjs +136 -4
  121. package/fjs/types/bigfloat/types.d.ts +17 -0
  122. package/fjs/types/function/module.f.d.mts +11 -2
  123. package/fjs/types/function/module.f.mjs +20 -2
  124. package/fjs/types/function/proof.f.mjs +5 -1
  125. package/fjs/types/phantom/types.d.ts +21 -0
  126. package/fjs/types/result/module.f.d.mts +4 -4
  127. package/fjs/types/result/module.f.mjs +2 -2
  128. package/fjs/types/result/proof.f.d.mts +6 -2
  129. package/fjs/types/result/proof.f.mjs +17 -1
  130. package/fjs/types/rtti/common/module.f.d.mts +78 -5
  131. package/fjs/types/rtti/common/module.f.mjs +168 -3
  132. package/fjs/types/rtti/common/proof.f.d.mts +14 -0
  133. package/fjs/types/rtti/common/proof.f.mjs +61 -2
  134. package/fjs/types/rtti/common/types.d.ts +23 -1
  135. package/fjs/types/rtti/data/module.f.d.mts +57 -1
  136. package/fjs/types/rtti/data/module.f.mjs +241 -47
  137. package/fjs/types/rtti/data/proof.f.d.mts +10 -0
  138. package/fjs/types/rtti/data/proof.f.mjs +272 -39
  139. package/fjs/types/rtti/data/types.d.ts +14 -7
  140. package/fjs/types/rtti/host.proof.d.mts +33 -0
  141. package/fjs/types/rtti/host.proof.mjs +170 -0
  142. package/fjs/types/rtti/module.f.d.mts +51 -6
  143. package/fjs/types/rtti/module.f.mjs +50 -3
  144. package/fjs/types/rtti/parse/module.f.d.mts +31 -24
  145. package/fjs/types/rtti/parse/module.f.mjs +197 -59
  146. package/fjs/types/rtti/parse/proof.f.d.mts +37 -2
  147. package/fjs/types/rtti/parse/proof.f.mjs +171 -47
  148. package/fjs/types/rtti/proof.f.d.mts +6 -0
  149. package/fjs/types/rtti/proof.f.mjs +42 -1
  150. package/fjs/types/rtti/ts/module.f.d.mts +9 -3
  151. package/fjs/types/rtti/ts/module.f.mjs +68 -15
  152. package/fjs/types/rtti/ts/proof.f.d.mts +42 -2
  153. package/fjs/types/rtti/ts/proof.f.mjs +139 -6
  154. package/fjs/types/rtti/ts/types.d.ts +164 -16
  155. package/fjs/types/rtti/types.d.ts +31 -3
  156. package/fjs/types/rtti/validate/module.f.d.mts +120 -0
  157. package/fjs/types/rtti/validate/module.f.mjs +351 -0
  158. package/fjs/types/rtti/validate/proof.f.d.mts +154 -0
  159. package/fjs/types/rtti/validate/proof.f.mjs +781 -0
  160. package/fjs/types/ts/types.d.ts +4 -0
  161. package/fjs/web/module.f.d.mts +115 -0
  162. package/fjs/web/module.f.mjs +581 -0
  163. package/fjs/web/proof.f.d.mts +39 -0
  164. package/fjs/web/proof.f.mjs +413 -0
  165. package/fjs/web/types.d.ts +39 -0
  166. package/fjs/website/browser-prepare.d.mts +5 -0
  167. package/fjs/website/browser-prepare.mjs +115 -0
  168. package/fjs/website/browser-source.d.mts +37 -0
  169. package/fjs/website/browser-source.mjs +202 -0
  170. package/fjs/website/browser-source.proof.d.mts +31 -0
  171. package/fjs/website/browser-source.proof.mjs +162 -0
  172. package/fjs/website/browser.d.mts +5 -0
  173. package/fjs/website/browser.mjs +8 -0
  174. package/fjs/website/module.f.d.mts +3 -3
  175. package/fjs/website/module.f.mjs +62 -9
  176. package/fjs/website/proof.f.d.mts +4 -0
  177. package/fjs/website/proof.f.mjs +18 -3
  178. package/package.json +2 -3
@@ -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
- /** @typedef {['array', List<AstConst>]} _DjsStackArray */
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
- /** @typedef {['object', OrderedMap<AstConst>, string]} _DjsStackObject */
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
- /** @typedef {_DjsStackArray | _DjsStackObject} _DjsStackElement */
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
- /** @typedef {List<_DjsStackElement>} _DjsStack */
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 {_InitialState | _NewLineRequiredState | _ImportState | _ConstState | _ExportState | _ParseValueState | _ResultState | _ErrorState} _ParserState */
137
+ /** @typedef {Assert<Equal<(typeof tokenKindNames)[number], Exclude<DjsToken['kind'], 'eof'>>>} _KindsAreComplete */
31
138
 
32
- /** @typedef {{
33
- * readonly refs: OrderedMap<AstModuleRef>
34
- * readonly modules: List<string>
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
- * Where the value parser stands inside the value it is reading.
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
- * The object states spell a property out left to right: `'{'` expects a key,
67
- * `'{k'` the `:` after one, `'{:'` the value, `'{v'` the `,` or `}` after it,
68
- * and `'{,'` the next key. A computed key `["a"]` takes the two extra steps
69
- * `'{['` (the string inside the brackets) and `'{[k'` (the closing `]`), then
70
- * rejoins the plain path at `'{k'`.
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
- * @typedef {'' | '[' | '[v' | '[,' | '{' | '{[' | '{[k' | '{k' | '{:' | '{v' | '{,'} _ValueState
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
- /** @typedef {{
76
- * readonly state: 'constValue' | 'exportValue'
77
- * readonly module: _ModuleState
78
- * readonly valueState: _ValueState
79
- * readonly top: _DjsStackElement | null
80
- * readonly stack: _DjsStack
81
- * }} _ParseValueState */
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
- /** @typedef {{
84
- * readonly state: 'result'
85
- * readonly module: _ModuleState
86
- * }} _ResultState */
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
- /** @typedef {{
89
- * readonly state: 'error'
90
- * readonly error: ParseError
91
- * }} _ErrorState */
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
- * A statement begins with `import`, `const`, or `export` and with nothing
95
- * else. The three are a whitelist that grows as the language does, not a
96
- * closed set; what a statement may never begin with is a **value**. A text
97
- * that does `42`, `[1,2]`, `{"a":1}` is a JSON document, and reading it
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 parseInitialOp = ({ token, metadata }) => state => {
109
- switch (token.kind)
110
- {
111
- case 'ws':
112
- case 'nl':
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
- /** @type {(token: DjsTokenWithMetadata) => (state: _NewLineRequiredState) => _ParserState} */
131
- const parseNewLineRequiredOp = ({ token, metadata }) => state => {
132
- switch (token.kind) {
133
- case 'ws':
134
- case '//':
135
- case '/*': return state
136
- case 'nl': return { ...state, state: '' }
137
- case 'eof': return { state: 'error', error: { message: 'unexpected end', metadata } }
138
- default: return { state: 'error', error: { message: 'unexpected token', metadata } }
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
- /** @type {(token: DjsTokenWithMetadata) => (state: _ExportState) => _ParserState} */
143
- const parseExportOp = ({ token, metadata }) => state => {
144
- switch (token.kind) {
145
- case 'ws':
146
- case 'nl':
147
- case '//':
148
- case '/*': return state
149
- case 'eof': return { state: 'error', error: { message: 'unexpected end', metadata } }
150
- case 'id': {
151
- if (token.value === 'default') return { ...state, state: 'exportValue', valueState: '', top: null, stack: null }
152
- }
153
- }
154
- return { state: 'error', error: { message: 'unexpected token', metadata } }
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 {(token: DjsTokenWithMetadata) => (state: _ResultState) => _ParserState} */
158
- const parseResultOp = ({ token, metadata }) => state => {
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
- /** @type {(token: DjsTokenWithMetadata) => (state: _ConstState) => _ParserState} */
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
- /** @type {(token: DjsTokenWithMetadata) => (state: _ConstState) => _ParserState} */
190
- const parseConstNameOp = ({ token, metadata }) => state => {
191
- switch (token.kind) {
192
- case 'ws':
193
- case 'nl':
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 {(token: DjsTokenWithMetadata) => (state: _ImportState) => _ParserState} */
203
- const parseImportOp = ({ token, metadata }) => state => {
204
- switch (token.kind) {
205
- case 'ws':
206
- case 'nl':
207
- case '//':
208
- case '/*': return state
209
- case 'id': {
210
- if (at(token.value)(state.module.refs) !== null) {
211
- return { state: 'error', error: { message: 'duplicate id', metadata } }
212
- }
213
- /** @type {AstModuleRef} */
214
- const aref = ['aref', length(state.module.modules)]
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
- /** @type {(token: DjsTokenWithMetadata) => (state: _ImportState) => _ParserState} */
224
- const parseImportNameOp = ({ token, metadata }) => state => {
225
- switch (token.kind) {
226
- case 'ws':
227
- case 'nl':
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
- /** @type {(token: DjsTokenWithMetadata) => (state: _ImportState) => _ParserState} */
239
- const parseImportFromOp = ({ token, metadata }) => state => {
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
- /** @type {(obj: _DjsStackObject) => (key: string) => _DjsStackObject} */
255
- const addKeyToObject = obj => key => (['object', obj[1], key])
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
- /** @type {(obj: _DjsStackObject) => (value: AstConst) => _DjsStackObject} */
258
- const addValueToObject = obj => value => (['object', setReplace(obj[2])(value)(obj[1]), ''])
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 {(array: _DjsStackArray) => (value: AstConst) => _DjsStackArray} */
261
- const addToArray = array => value => (['array', concat(array[1])([value])])
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
- /** @type {(token: DjsTokenWithMetadata) => (state: _ParseValueState) => _ParserState} */
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
- /** @type {(token: DjsTokenWithMetadata) => (state: _ParseValueState) => _ParserState} */
414
- const parseArrayStartOp = ({ token, metadata }) => state => {
415
- if (isValueToken(token)) { return pushValue(state)(tokenToValue(token)) }
416
- switch (token.kind)
417
- {
418
- case 'id': return pushRef(state)(token.value)(metadata)
419
- case '[': return startArray(state)
420
- case ']': return endArray(state)
421
- case '{': return startObject(state)
422
- case 'ws':
423
- case 'nl':
424
- case '//':
425
- case '/*': return state
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
- /** @type {(token: DjsTokenWithMetadata) => (state: _ParseValueState) => _ParserState} */
432
- const parseArrayValueOp = ({ token, metadata }) => state => {
433
- switch (token.kind)
434
- {
435
- case ']': return endArray(state)
436
- case ',': return { ...state, valueState: '[,', top: state.top, stack: state.stack }
437
- case 'ws':
438
- case 'nl':
439
- case '//':
440
- case '/*': return state
441
- case 'eof': return { state: 'error', error: { message: 'unexpected end', metadata } }
442
- default: return { state: 'error', error: { message: 'unexpected token', metadata } }
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
- // allow identifier property names (#2410)
447
- /** @type {(token: DjsTokenWithMetadata) => (state: _ParseValueState) => _ParserState} */
448
- const parseObjectStartOp = ({ token, metadata }) => state => {
449
- switch (token.kind)
450
- {
451
- case 'string':
452
- case 'id':
453
- return pushPlainKey(state)(token.value)(metadata)
454
- case '[': return { ...state, valueState: '{[' }
455
- case '}': return endObject(state)
456
- case 'ws':
457
- case 'nl':
458
- case '//':
459
- case '/*': return state
460
- case 'eof': return { state: 'error', error: { message: 'unexpected end', metadata } }
461
- default: return { state: 'error', error: { message: 'unexpected token', metadata } }
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
- // computed property keys with a constant string key (#2470)
466
- /** @type {(token: DjsTokenWithMetadata) => (state: _ParseValueState) => _ParserState} */
467
- const parseObjectComputedKeyOp = ({ token, metadata }) => state => {
468
- switch (token.kind)
469
- {
470
- case 'string': return pushKey('{[k')(state)(token.value)(metadata)
471
- case 'ws':
472
- case 'nl':
473
- case '//':
474
- case '/*': return state
475
- case 'eof': return { state: 'error', error: { message: 'unexpected end', metadata } }
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
- /** @type {(token: DjsTokenWithMetadata) => (state: _ParseValueState) => _ParserState} */
481
- const parseObjectComputedKeyEndOp = ({ token, metadata }) => state => {
482
- switch (token.kind)
483
- {
484
- case ']': return { ...state, valueState: '{k' }
485
- case 'ws':
486
- case 'nl':
487
- case '//':
488
- case '/*': return state
489
- case 'eof': return { state: 'error', error: { message: 'unexpected end', metadata } }
490
- default: return { state: 'error', error: { message: 'unexpected token', metadata } }
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
- /** @type {(token: DjsTokenWithMetadata) => (state: _ParseValueState) => _ParserState} */
495
- const parseObjectKeyOp = ({ token, metadata }) => state => {
496
- switch (token.kind)
497
- {
498
- case ':': return { ...state, valueState: '{:', top: state.top, stack: state.stack }
499
- case 'ws':
500
- case 'nl':
501
- case '//':
502
- case '/*': return state
503
- case 'eof': return { state: 'error', error: { message: 'unexpected end', metadata } }
504
- default: return { state: 'error', error: { message: 'unexpected token', metadata } }
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
- /** @type {(token: DjsTokenWithMetadata) => (state: _ParseValueState) => _ParserState} */
509
- const parseObjectColonOp = ({ token, metadata }) => state => {
510
- if (isValueToken(token)) { return pushValue(state)(tokenToValue(token)) }
511
- switch (token.kind)
512
- {
513
- case 'id': return pushRef(state)(token.value)(metadata)
514
- case '[': return startArray(state)
515
- case '{': return startObject(state)
516
- case 'ws':
517
- case 'nl':
518
- case '//':
519
- case '/*': return state
520
- case 'eof': return { state: 'error', error: { message: 'unexpected end', metadata } }
521
- default: return { state: 'error', error: { message: 'unexpected token', metadata } }
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
- /** @type {(token: DjsTokenWithMetadata) => (state: _ParseValueState) => _ParserState} */
526
- const parseObjectNextOp = ({ token, metadata }) => state => {
527
- switch (token.kind)
528
- {
529
- case '}': return endObject(state)
530
- case ',': return { ...state, valueState: '{,', top: state.top, stack: state.stack }
531
- case 'ws':
532
- case 'nl':
533
- case '//':
534
- case '/*': return state
535
- case 'eof': return { state: 'error', error: { message: 'unexpected end', metadata } }
536
- default: return { state: 'error', error: { message: 'unexpected token', metadata } }
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
- /** @type {(token: DjsTokenWithMetadata) => (state: _ParseValueState) => _ParserState} */
541
- const parseObjectCommaOp = ({ token, metadata }) => state => {
542
- switch (token.kind)
543
- {
544
- case '}': return endObject(state)
545
- case 'string':
546
- case 'id':
547
- return pushPlainKey(state)(token.value)(metadata)
548
- case '[': return { ...state, valueState: '{[' }
549
- case 'ws':
550
- case 'nl':
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
- /** @type {Fold<DjsTokenWithMetadata, _ParserState>} */
559
- const foldOp = token => state => {
560
- switch (state.state) {
561
- case '': return parseInitialOp(token)(state)
562
- case 'nl': return parseNewLineRequiredOp(token)(state)
563
- case 'import': return parseImportOp(token)(state)
564
- case 'import+name': return parseImportNameOp(token)(state)
565
- case 'import+from': return parseImportFromOp(token)(state)
566
- case 'const': return parseConstOp(token)(state)
567
- case 'const+name': return parseConstNameOp(token)(state)
568
- case 'export': return parseExportOp(token)(state)
569
- case 'result': return parseResultOp(token)(state)
570
- case 'error': return { state: 'error', error: state.error }
571
- case 'constValue':
572
- case 'exportValue':
573
- {
574
- switch (state.valueState)
575
- {
576
- case '': return parseValueOp(token)(state)
577
- case '[': return parseArrayStartOp(token)(state)
578
- case '[v': return parseArrayValueOp(token)(state)
579
- case '[,': return parseValueOp(token)(state)
580
- case '{': return parseObjectStartOp(token)(state)
581
- case '{[': return parseObjectComputedKeyOp(token)(state)
582
- case '{[k': return parseObjectComputedKeyEndOp(token)(state)
583
- case '{k': return parseObjectKeyOp(token)(state)
584
- case '{:': return parseObjectColonOp(token)(state)
585
- case '{v': return parseObjectNextOp(token)(state)
586
- case '{,': return parseObjectCommaOp(token)(state)
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 state = fold(foldOp)({ state: '', module: { refs: null, modules: null, consts: null } })(tokenList)
604
- switch (state.state) {
605
- case 'result': return ok(/** @satisfies {AstModule} */ ([toArray(state.module.modules), toArray(state.module.consts)]))
606
- case 'error': return error(state.error)
607
- default: return error({ message: 'unexpected end', metadata: null })
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
- pushKey: {
613
- // `pushKey` is only ever invoked while `state.top` is an object (the
614
- // `'{'`/`'{,'` value-states guarantee it), so its non-object guard is
615
- // a defensive branch unreachable through `parseFromTokens`. Call it
616
- // directly to cover that branch.
617
- nonObjectTop: () => {
618
- /** @type {_ParseValueState} */
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
- endArray: {
631
- // `endArray` is only ever invoked while `state.top` is an array (the
632
- // `'['`/`'[v'`/`'[,'` value-states guarantee it), so its non-array
633
- // guard is a defensive branch unreachable through `parseFromTokens`.
634
- // Call it directly to cover that branch.
635
- nonArrayTop: () => {
636
- /** @type {_ParseValueState} */
637
- const state = {
638
- state: 'exportValue',
639
- module: { refs: null, modules: null, consts: null },
640
- valueState: '[,',
641
- top: null,
642
- stack: null,
643
- }
644
- const result = endArray(state)
645
- assertEq(result.state, 'result')
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
- endObject: {
649
- // `endObject` is only ever invoked while `state.top` is an object
650
- // (the `'{'`/`'{k'`/`'{:'`/`'{v'`/`'{,'` value-states guarantee it),
651
- // so its non-object guard is a defensive branch unreachable through
652
- // `parseFromTokens`. Call it directly to cover that branch.
653
- nonObjectTop: () => {
654
- /** @type {_ParseValueState} */
655
- const state = {
656
- state: 'exportValue',
657
- module: { refs: null, modules: null, consts: null },
658
- valueState: '{,',
659
- top: null,
660
- stack: null,
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 result = endObject(state)
663
- assertEq(result.state, 'result')
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
  }