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
@@ -2,6 +2,12 @@
2
2
  * @import { DjsTokenWithMetadata } from '../tokenizer/types.ts'
3
3
  */
4
4
  export declare const proof: {
5
+ parseCorpus: (() => void)[];
6
+ stackSafety: (() => void)[];
7
+ syntaxBeforeSemantic: (() => void)[];
8
+ eofContract: (() => void)[];
9
+ lexicalErrorStreamShape: (() => void)[];
10
+ framingKeywordsAsIdentifiers: (() => void)[];
5
11
  valid: (() => void)[];
6
12
  computedKey: (() => void)[];
7
13
  invalidComputedKey: (() => void)[];
@@ -9,14 +9,316 @@ import { sort } from '../../types/object/module.f.mjs'
9
9
  import { stringToList } from '../../text/utf16/module.f.mjs'
10
10
  import { stringifyAsTree } from '../serializer/module.f.mjs'
11
11
  import { stringify } from '../../media/json/module.f.mjs'
12
- import { assert, assertEq } from '../../asserts/module.f.mjs'
12
+ import { assert, assertEq, assertStructurallySame } from '../../asserts/module.f.mjs'
13
13
 
14
14
  /** @type {(s: string) => readonly DjsTokenWithMetadata[]} */
15
15
  const tokenizeString = s => toArray(tokenize(stringToList(s))(''))
16
16
 
17
17
  const stringifyDjsModule = stringifyAsTree(sort)
18
18
 
19
+ /**
20
+ * `count` copies of `element`, comma-joined.
21
+ *
22
+ * Built by repeating the *string*, which allocates the result directly, rather
23
+ * than allocating an array and then writing over it: `Array(n).fill(x)` mutates
24
+ * what it just made, and proof code is held to the same immutability rule as
25
+ * everything else here.
26
+ *
27
+ * @type {(element: string) => (count: number) => string}
28
+ */
29
+ const repeated = element => count => `${`${element},`.repeat(count - 1)}${element}`
30
+
31
+ /**
32
+ * `count` distinct `k<i>:<i>` properties, comma-joined — the object form of
33
+ * {@link repeated}, where each entry has to differ.
34
+ *
35
+ * @type {(count: number) => string}
36
+ */
37
+ const numberedMembers = count =>
38
+ Array.from({ length: count }, (_, i) => `k${i}:${i}`).join(',')
39
+
40
+ /** @type {(kind: 'ws' | 'nl' | 'null' | 'true' | 'false' | 'undefined' | 'eof', line: number) => DjsTokenWithMetadata} */
41
+ const proofKind = (kind, line) => ({ token: { kind }, metadata: { path: 'a.js', line, column: 1 } })
42
+
43
+ /** @type {(value: string, line: number) => DjsTokenWithMetadata} */
44
+ const proofId = (value, line) => ({ token: { kind: 'id', value }, metadata: { path: 'a.js', line, column: 1 } })
45
+
19
46
  export const proof = {
47
+ // The corpus that proved parity against the hand-written state machine,
48
+ // kept as fixed expectations now that the state machine is gone.
49
+ //
50
+ // Every value below was **recorded from that parser** before it was deleted,
51
+ // not written by hand and not read off the replacement — so these still say
52
+ // "the parser behaves as it always did", which a test written against the
53
+ // new implementation could not.
54
+ parseCorpus: [
55
+ () => {
56
+ for (const [source, expected] of [
57
+ ["export default null", "[[],[null]]"],
58
+ ["export default true", "[[],[true]]"],
59
+ ["export default false", "[[],[false]]"],
60
+ ["export default undefined", "[[],[undefined]]"],
61
+ ["export default 0.1", "[[],[0.1]]"],
62
+ ["export default 1.1e+2", "[[],[110]]"],
63
+ ["export default \"abc\"", "[[],[\"abc\"]]"],
64
+ ["export default 1234567890n", "[[],[1234567890n]]"],
65
+ ["export default []", "[[],[[\"array\",[]]]]"],
66
+ ["export default [1]", "[[],[[\"array\",[1]]]]"],
67
+ ["export default [1,]", "[[],[[\"array\",[1]]]]"],
68
+ ["export default [[]]", "[[],[[\"array\",[[\"array\",[]]]]]]"],
69
+ ["export default [0,[1,[2,[]]],3]", "[[],[[\"array\",[0,[\"array\",[1,[\"array\",[2,[\"array\",[]]]]]],3]]]]"],
70
+ ["export default [1234567890n]", "[[],[[\"array\",[1234567890n]]]]"],
71
+ ["export default {}", "[[],[{}]]"],
72
+ ["export default {\"a\":1}", "[[],[{\"a\":1}]]"],
73
+ ["export default {a: 1}", "[[],[{\"a\":1}]]"],
74
+ ["export default {\"a\":1,}", "[[],[{\"a\":1}]]"],
75
+ ["export default {[\"a\"]:1}", "[[],[{\"a\":1}]]"],
76
+ ["export default {a:1,\"b\":2,[\"c\"]:3,}", "[[],[{\"a\":1,\"b\":2,\"c\":3}]]"],
77
+ ["export default {\"a\":{\"b\":{\"c\":[\"d\"]}}}", "[[],[{\"a\":{\"b\":{\"c\":[\"array\",[\"d\"]]}}}]]"],
78
+ ["export default {\"a\":true,\"b\":false,\"c\":null,\"d\":undefined}", "[[],[{\"a\":true,\"b\":false,\"c\":null,\"d\":undefined}]]"],
79
+ ["export default {a:1,a:2}", "[[],[{\"a\":2}]]"],
80
+ ["export default {[\"__proto__\"]: 1}", "[[],[{\"__proto__\":1}]]"],
81
+ ["const a = 1\nexport default a", "[[],[1,[\"cref\",0]]]"],
82
+ ["const a = 1\nconst b = 2\nexport default [a,b]", "[[],[1,2,[\"array\",[[\"cref\",0],[\"cref\",1]]]]]"],
83
+ ["const a = a\nexport default a", "[[],[[\"cref\",0],[\"cref\",0]]]"],
84
+ ["import x from \"m\"\nexport default x", "[[\"m\"],[[\"aref\",0]]]"],
85
+ ["import x from \"m\"\nconst a = 1\nexport default a", "[[\"m\"],[1,[\"cref\",0]]]"],
86
+ ["import x from \"m\"\nimport y from \"n\"\nexport default [x,y]", "[[\"m\",\"n\"],[[\"array\",[[\"aref\",0],[\"aref\",1]]]]]"],
87
+ ["// c\nexport default 1", "[[],[1]]"],
88
+ ["/* c */ export default 1", "[[],[1]]"],
89
+ ["\n\n export default 1 \n\n", "[[],[1]]"],
90
+ ["const export = 1\nexport default export", "[[],[1,[\"cref\",0]]]"],
91
+ ["export default { from: 2, default: 3 }", "[[],[{\"default\":3,\"from\":2}]]"],
92
+ ]) {
93
+ const [tag, value] = parseFromTokens(tokenizeString(source))
94
+ assert(tag === 'ok', [source, tag])
95
+ assertEq(stringifyDjsModule(value), expected, source)
96
+ }
97
+ },
98
+ () => {
99
+ /** @type {readonly(readonly[string, string, readonly[number, number] | null])[]} */
100
+ const cases = [
101
+ ["42", "unexpected token", [1, 1]],
102
+ ["", "unexpected end", [1, 1]],
103
+ ["export default", "unexpected end", [1, 15]],
104
+ ["[1,2]", "unexpected token", [1, 1]],
105
+ ["{\"a\":1}", "unexpected token", [1, 1]],
106
+ ["export default [", "unexpected end", [1, 17]],
107
+ ["export default {", "unexpected end", [1, 17]],
108
+ ["const a = 1 export default a", "unexpected token", [1, 13]],
109
+ ["export default 1 2", "unexpected token", [1, 18]],
110
+ ["export default {a}", "unexpected token", [1, 18]],
111
+ ["export default {:1}", "unexpected token", [1, 17]],
112
+ ["export default [,]", "unexpected token", [1, 17]],
113
+ ["import x from y\nexport default x", "unexpected token", [1, 15]],
114
+ ["export x from \"m\"\nexport default 1", "unexpected token", [1, 8]],
115
+ ["const = 1\nexport default 1", "unexpected token", [1, 7]],
116
+ ["export default {[1]:2}", "unexpected token", [1, 18]],
117
+ ["const a = 1;\nexport default a", "unexpected token", [1, 12]],
118
+ ["export default 1\nconst b = 2", "unexpected token", [2, 1]],
119
+ ["import x from \"m\"", "unexpected end", [1, 18]],
120
+ ["const a = 1", "unexpected end", [1, 12]],
121
+ ["const a = 1\nconst a = 2\nexport default a", "duplicate id", [2, 7]],
122
+ ["import x from \"m\"\nimport x from \"n\"\nexport default x", "duplicate id", [2, 8]],
123
+ ["import x from \"m\"\nconst x = 1\nexport default x", "duplicate id", [2, 7]],
124
+ ["export default zzz", "const not found", [1, 16]],
125
+ ["const a = zzz\nexport default a", "const not found", [1, 11]],
126
+ ["export default [zzz]", "const not found", [1, 17]],
127
+ ["export default {a: zzz}", "const not found", [1, 20]],
128
+ ["export default {__proto__: 1}", "__proto__ requires the computed key form", [1, 17]],
129
+ ["export default {\"__proto__\": 1}", "__proto__ requires the computed key form", [1, 17]],
130
+ ["import x from \"m\"\nimport x from \"n\"\nimport y from \"o\"\nexport default y", "duplicate id", [2, 8]],
131
+ ["const a = 1\nconst a = 2\nconst b = 3\nexport default b", "duplicate id", [2, 7]],
132
+ ]
133
+ for (const [source, message, position] of cases) {
134
+ const [tag, value] = parseFromTokens(tokenizeString(source))
135
+ assert(tag === 'error', [source, tag])
136
+ assertEq(value.message, message, source)
137
+ const { metadata } = value
138
+ assertStructurallySame(
139
+ metadata === null ? null : [metadata.line, metadata.column],
140
+ position,
141
+ source)
142
+ }
143
+ },
144
+ ],
145
+ // Wide and deep values, which cost the BNF path a stack frame each until
146
+ // both its traversals were made iterative.
147
+ //
148
+ // A repetition is only *flat* in the AST when `toData` recognizes the
149
+ // right-recursive shape, and nested inside `delimited`'s option scaffolding
150
+ // it does not — so 5,000 siblings are 5,000 levels of tree, and a recursive
151
+ // walk over them overflows exactly as deep nesting would. The state machine
152
+ // was fixed for its own version of this; see `containerStackCost`.
153
+ stackSafety: [
154
+ () => {
155
+ const source = `export default [${repeated('null')(5000)}]`
156
+ const tokens = tokenizeString(source)
157
+ assertEq(parseFromTokens(tokens)[0], 'ok')
158
+ },
159
+ () => {
160
+ const source = `export default [${repeated('{}')(5000)}]`
161
+ const tokens = tokenizeString(source)
162
+ assertEq(parseFromTokens(tokens)[0], 'ok')
163
+ },
164
+ () => {
165
+ const source = `export default ${'['.repeat(5000)}${']'.repeat(5000)}`
166
+ const tokens = tokenizeString(source)
167
+ assertEq(parseFromTokens(tokens)[0], 'ok')
168
+ },
169
+ () => {
170
+ // wide objects walk the member list rather than the element list
171
+ const source = `export default {${numberedMembers(5000)}}`
172
+ const tokens = tokenizeString(source)
173
+ assertEq(parseFromTokens(tokens)[0], 'ok')
174
+ },
175
+ ],
176
+ // A syntax error is reported ahead of a semantic one, wherever each sits.
177
+ //
178
+ // The grammar matches the whole module before the fold runs, so a malformed
179
+ // suffix is found before any name is resolved. The parser this replaced
180
+ // streamed, so it met an unresolved name first and said so. Both are true of
181
+ // the input; they answer different questions about it.
182
+ //
183
+ // This is the design's stated failure-parity bar rather than a departure
184
+ // from it — the error must "identify the furthest relevant token or EOF",
185
+ // and the syntax failure is the furthest relevant token. It is pinned here
186
+ // because it is a *change*, and because the reasoning is not recoverable
187
+ // from the positions alone.
188
+ syntaxBeforeSemantic: [
189
+ () => {
190
+ /** @type {readonly(readonly[string, string, readonly[number, number]])[]} */
191
+ const cases = [
192
+ // was: const not found @1:11
193
+ ['const a = missing x', 'unexpected token', [1, 19]],
194
+ // was: const not found @1:11
195
+ ['const a = missing', 'unexpected end', [1, 18]],
196
+ // was: const not found @1:16
197
+ ['export default missing 1', 'unexpected token', [1, 24]],
198
+ // was: duplicate id @2:7
199
+ ['const a = 1\nconst a = 2 x', 'unexpected token', [2, 13]],
200
+ ]
201
+ for (const [source, message, position] of cases) {
202
+ const [tag, value] = parseFromTokens(tokenizeString(source))
203
+ assert(tag === 'error', [source, tag])
204
+ assertEq(value.message, message, source)
205
+ const { metadata } = value
206
+ assertStructurallySame(
207
+ metadata === null ? null : [metadata.line, metadata.column],
208
+ position,
209
+ source)
210
+ }
211
+ },
212
+ () => {
213
+ // with nothing malformed after it, the semantic error is still the
214
+ // one reported — the change is which error wins, not whether names
215
+ // are checked
216
+ const [tag, value] = parseFromTokens(tokenizeString('export default missing'))
217
+ assert(tag === 'error', tag)
218
+ assertEq(value.message, 'const not found')
219
+ assertEq(value.metadata?.column, 16)
220
+ },
221
+ ],
222
+ // The tokenizer's EOF contract, checked through the parser rather than
223
+ // through `splitEof` alone.
224
+ //
225
+ // Two of these are a **deliberate behaviour change at cutover**: the state
226
+ // machine accepts a stream with no `eof`, and one with two, because it only
227
+ // ever asks what the next token is. The BNF path requires exactly one, in
228
+ // final position, because a backend synthesizes its own logical end and a
229
+ // second marker would be a symbol the grammar has no rule for. Neither
230
+ // stream can come from the tokenizer, so this tightens a contract only a
231
+ // hand-built list could break — but it does tighten it, and the cutover
232
+ // should not be where that is discovered.
233
+ eofContract: [
234
+ () => {
235
+ const wellFormed = [
236
+ proofId('export', 1), proofKind('ws', 1), proofId('default', 1),
237
+ proofKind('ws', 1), proofKind('null', 1), proofKind('eof', 1),
238
+ ]
239
+ assertEq(parseFromTokens(wellFormed)[0], 'ok')
240
+ },
241
+ () => {
242
+ // no `eof`: the state machine read this as a complete module
243
+ const noEof = [
244
+ proofId('export', 1), proofKind('ws', 1), proofId('default', 1),
245
+ proofKind('ws', 1), proofKind('null', 1),
246
+ ]
247
+ const [tag, value] = parseFromTokens(noEof)
248
+ assert(tag === 'error', tag)
249
+ assertEq(value.message, 'missing end-of-input token')
250
+ },
251
+ () => {
252
+ // a second `eof`: likewise invisible to the state machine
253
+ const twoEof = [
254
+ proofId('export', 1), proofKind('ws', 1), proofId('default', 1),
255
+ proofKind('ws', 1), proofKind('null', 1), proofKind('eof', 1), proofKind('eof', 2),
256
+ ]
257
+ const [tag, value] = parseFromTokens(twoEof)
258
+ assert(tag === 'error', tag)
259
+ assertEq(value.message, 'end-of-input token is not final')
260
+ },
261
+ () => {
262
+ const [tag, value] = parseFromTokens([])
263
+ assert(tag === 'error', tag)
264
+ assertEq(value.message, 'missing end-of-input token')
265
+ },
266
+ ],
267
+ // A lexical failure ends the token stream at an `error` token and emits no
268
+ // `eof`. `splitEof` reads a missing `eof` that way rather than as a broken
269
+ // tokenizer contract, and reports the error where it happened — so that
270
+ // reading is pinned here against the real tokenizer, alongside the position
271
+ // the current parser reports for the same input.
272
+ lexicalErrorStreamShape: [
273
+ () => {
274
+ const tokens = tokenizeString('const a = "abc')
275
+ assertEq(tokens.length, 1)
276
+ assertEq(tokens[0].token.kind, 'error')
277
+ assertEq(tokens[0].metadata.column, 11)
278
+ },
279
+ () => {
280
+ const [tag, value] = parseFromTokens(tokenizeString('const a = "abc'))
281
+ assert(tag === 'error', tag)
282
+ assertEq(value.metadata?.line, 1)
283
+ assertEq(value.metadata?.column, 11)
284
+ },
285
+ () => {
286
+ const [tag, value] = parseFromTokens(tokenizeString('const a = /* x'))
287
+ assert(tag === 'error', tag)
288
+ assertEq(value.metadata?.column, 15)
289
+ },
290
+ ],
291
+ // None of the framing keywords is reserved: outside the positions that frame
292
+ // a module, the parser accepts them as ordinary identifiers. Pinned here
293
+ // because the BNF replacement gives each its own token symbol, and a grammar
294
+ // whose identifier rule accepted only the `id` symbol would stop parsing
295
+ // every case below without any other test noticing.
296
+ framingKeywordsAsIdentifiers: [
297
+ () => {
298
+ const [tag, value] = parseFromTokens(tokenizeString('const export = 1\nexport default export'))
299
+ assert(tag === 'ok', tag)
300
+ // `export` bound as a const and then referenced: the exported value
301
+ // resolves back to that binding, so the word acted as an identifier
302
+ // in both positions.
303
+ assertStructurallySame(value[1], [1, ['cref', 0]])
304
+ },
305
+ () => {
306
+ const [tag] = parseFromTokens(tokenizeString('const from = 1\nexport default from'))
307
+ assert(tag === 'ok', tag)
308
+ },
309
+ () => {
310
+ const [tag] = parseFromTokens(tokenizeString('const import = 1\nexport default import'))
311
+ assert(tag === 'ok', tag)
312
+ },
313
+ () => {
314
+ const [tag] = parseFromTokens(tokenizeString('export default { from: 2, default: 3 }'))
315
+ assert(tag === 'ok', tag)
316
+ },
317
+ () => {
318
+ const [tag] = parseFromTokens(tokenizeString('export default { export: 1 }'))
319
+ assert(tag === 'ok', tag)
320
+ },
321
+ ],
20
322
  valid: [
21
323
  () => {
22
324
  const tokenList = tokenizeString('export default null')
@@ -501,15 +803,14 @@ export const proof = {
501
803
  assertEq(obj[1].message, 'unexpected end')
502
804
  },
503
805
  () => {
504
- // `parseFromTokens` itself, called with no tokens at all (the
806
+ // `parseFromTokens` itself, called with no tokens at all. The
505
807
  // tokenizer never produces this — it always emits at least an
506
- // `eof` token — but the exported function's own contract must
507
- // still handle it: the initial state is neither 'result' nor
508
- // 'error', so it falls through to the same "unexpected end".
509
- const obj = parseFromTokens(null)
808
+ // `eof` — but the exported function's contract must still answer.
809
+ // Since the BNF cutover it is named for what is missing rather
810
+ // than for running out of input.
811
+ const obj = parseFromTokens([])
510
812
  assert(obj[0] === 'error', obj)
511
- assertEq(obj[1].message, 'unexpected end')
512
- assertEq(obj[1].metadata, null)
813
+ assertEq(obj[1].message, 'missing end-of-input token')
513
814
  },
514
815
  ],
515
816
  errorMetadata: [
@@ -617,7 +918,10 @@ export const proof = {
617
918
  const tokenList = tokenizeString('const b = 1 \n import a from "a.f.js" \n export default [a,b]')
618
919
  const obj = parseFromTokens(tokenList)
619
920
  assert(obj[0] === 'error', obj)
620
- assertEq(obj[1].message, 'import must come before const')
921
+ // reworded at the BNF cutover: statement ordering is shape now, so
922
+ // a late `import` is a token the grammar cannot use rather than a
923
+ // rule about ordering it could name. The position is unchanged.
924
+ assertEq(obj[1].message, 'unexpected token')
621
925
  },
622
926
  () => {
623
927
  // nothing follows `export default` but trivia
@@ -793,13 +1097,13 @@ export const proof = {
793
1097
  containerStackCost: [
794
1098
  () => {
795
1099
  const [tag, value] = parseFromTokens(tokenizeString(
796
- `export default [${Array(20000).fill('{}').join(',')}]`))
1100
+ `export default [${repeated('{}')(20000)}]`))
797
1101
  assert(tag === 'ok', tag)
798
1102
  assertEq(value[1].length, 1)
799
1103
  },
800
1104
  () => {
801
1105
  const [tag] = parseFromTokens(tokenizeString(
802
- `export default [${Array(20000).fill('[]').join(',')}]`))
1106
+ `export default [${repeated('[]')(20000)}]`))
803
1107
  assert(tag === 'ok', tag)
804
1108
  },
805
1109
  () => {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Type-level API for `fjs/djs/parser/module.f.mjs`: the `ParseError` shape
3
- * `parseFromTokens` reports, and the `_ValueToken` subset `tokenToValue`
4
- * accepts.
3
+ * `parseFromTokens` reports, the `_ValueToken` subset `tokenToValue` accepts,
4
+ * and the parser layer's token alphabet.
5
5
  *
6
6
  * @module
7
7
  */
@@ -11,6 +11,33 @@ export type ParseError = {
11
11
  readonly message: string;
12
12
  readonly metadata: TokenMetadata | null;
13
13
  };
14
+ /**
15
+ * The words that frame a module, which the grammar has to tell apart from an
16
+ * ordinary identifier.
17
+ *
18
+ * The tokenizer emits all five as `{ kind: 'id' }` with the word in `value`, so
19
+ * a parser layer keyed on `kind` alone would give them the same symbol as any
20
+ * other identifier — and a grammar over that alphabet could not distinguish
21
+ * `export default` from two arbitrary names. They therefore get terminals of
22
+ * their own, which is what a registered alphabet allows: a name's symbol comes
23
+ * from its position in the list, so a name has no length limit.
24
+ *
25
+ * This is the same distinction the hand-written parser makes by comparing
26
+ * `token.value`, lifted from control flow into the alphabet.
27
+ */
28
+ export type _FramingKeyword = 'import' | 'const' | 'export' | 'default' | 'from';
29
+ /**
30
+ * A token name the parser layer's grammar can name as a terminal: every
31
+ * `DjsToken` kind except `eof`, plus the framing keywords.
32
+ *
33
+ * `eof` is excluded because a BNF backend synthesizes its own logical
34
+ * end-of-input; the tokenizer's physical `eof` token is split off the stream
35
+ * before any name is mapped, so it never reaches this alphabet.
36
+ *
37
+ * The kinds are derived from `DjsToken` rather than listed, so a token kind
38
+ * added there cannot silently go unrepresented at the parser layer.
39
+ */
40
+ export type _OrdinaryTokenName = Exclude<DjsToken['kind'], 'eof'> | _FramingKeyword;
14
41
  /** DJS tokens that carry a directly-usable value. */
15
42
  export type _ValueToken = Extract<DjsToken, {
16
43
  readonly kind: 'null' | 'false' | 'true' | 'undefined' | 'string' | 'number' | 'bigint';