functionalscript 0.45.0 → 0.46.1

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 (296) hide show
  1. package/README.md +5 -3
  2. package/fjs/asn.1/module.f.mjs +8 -1
  3. package/fjs/asn.1/proof.f.d.mts +10 -0
  4. package/fjs/asn.1/proof.f.mjs +16 -0
  5. package/fjs/basen/base128/module.f.mjs +22 -5
  6. package/fjs/bnf/data/module.f.d.mts +32 -6
  7. package/fjs/bnf/data/module.f.mjs +112 -7
  8. package/fjs/bnf/data/proof.f.d.mts +2 -0
  9. package/fjs/bnf/data/proof.f.mjs +74 -3
  10. package/fjs/bnf/data/types.d.ts +20 -2
  11. package/fjs/bnf/descent/module.f.d.mts +28 -14
  12. package/fjs/bnf/descent/module.f.mjs +122 -77
  13. package/fjs/bnf/descent/proof.f.d.mts +2 -0
  14. package/fjs/bnf/descent/proof.f.mjs +117 -31
  15. package/fjs/bnf/descent/types.d.ts +12 -14
  16. package/fjs/bnf/ll1/module.f.d.mts +64 -22
  17. package/fjs/bnf/ll1/module.f.mjs +214 -154
  18. package/fjs/bnf/ll1/proof.f.d.mts +15 -2
  19. package/fjs/bnf/ll1/proof.f.mjs +323 -149
  20. package/fjs/bnf/ll1/types.d.ts +16 -24
  21. package/fjs/bnf/matcher/module.f.d.mts +66 -0
  22. package/fjs/bnf/matcher/module.f.mjs +81 -0
  23. package/fjs/bnf/matcher/proof.f.d.mts +10 -0
  24. package/fjs/bnf/matcher/proof.f.mjs +79 -0
  25. package/fjs/bnf/matcher/types.d.ts +54 -0
  26. package/fjs/bnf/testlib.f.d.mts +31 -0
  27. package/fjs/bnf/testlib.f.mjs +80 -0
  28. package/fjs/cas/cli/module.f.d.mts +1 -1
  29. package/fjs/cas/cli/module.f.mjs +14 -20
  30. package/fjs/cas/cli/proof.f.d.mts +1 -3
  31. package/fjs/cas/cli/proof.f.mjs +44 -33
  32. package/fjs/cas/evo/module.f.d.mts +64 -18
  33. package/fjs/cas/evo/module.f.mjs +148 -70
  34. package/fjs/cas/evo/proof.f.d.mts +10 -1
  35. package/fjs/cas/evo/proof.f.mjs +305 -223
  36. package/fjs/cas/evo/types.d.ts +45 -9
  37. package/fjs/cas/module.f.d.mts +18 -32
  38. package/fjs/cas/module.f.mjs +129 -128
  39. package/fjs/cas/proof.f.d.mts +11 -8
  40. package/fjs/cas/proof.f.mjs +259 -147
  41. package/fjs/cas/types.d.ts +24 -10
  42. package/fjs/ci/config/module.f.d.mts +2 -2
  43. package/fjs/ci/config/module.f.mjs +2 -2
  44. package/fjs/ci/module.f.d.mts +5 -5
  45. package/fjs/ci/module.f.mjs +8 -7
  46. package/fjs/ci/nix/module.f.d.mts +7 -5
  47. package/fjs/ci/nix/module.f.mjs +13 -12
  48. package/fjs/ci/nix/proof.f.mjs +2 -2
  49. package/fjs/ci/proof.f.mjs +9 -5
  50. package/fjs/cli/module.f.d.mts +4 -6
  51. package/fjs/cli/module.f.mjs +4 -8
  52. package/fjs/cli/proof.f.mjs +17 -16
  53. package/fjs/cli/types.d.ts +2 -3
  54. package/fjs/common/monoid/types.d.ts +1 -1
  55. package/fjs/crypto/hmac/module.f.mjs +2 -2
  56. package/fjs/crypto/sha2/module.f.mjs +3 -1
  57. package/fjs/crypto/sha2/proof.f.d.mts +1 -0
  58. package/fjs/crypto/sha2/proof.f.mjs +24 -0
  59. package/fjs/crypto/sha2/types.d.ts +11 -0
  60. package/fjs/crypto/sign/module.f.mjs +2 -2
  61. package/fjs/dev/module.f.d.mts +13 -4
  62. package/fjs/dev/module.f.mjs +56 -27
  63. package/fjs/dev/update/module.f.d.mts +9 -4
  64. package/fjs/dev/update/module.f.mjs +14 -10
  65. package/fjs/dev/update/proof.f.d.mts +1 -3
  66. package/fjs/dev/update/proof.f.mjs +10 -5
  67. package/fjs/djs/module.f.d.mts +13 -5
  68. package/fjs/djs/module.f.mjs +31 -16
  69. package/fjs/djs/parser/module.f.d.mts +13 -3
  70. package/fjs/djs/parser/module.f.mjs +117 -16
  71. package/fjs/djs/parser/proof.f.d.mts +5 -1
  72. package/fjs/djs/parser/proof.f.mjs +274 -12
  73. package/fjs/djs/parser/types.d.ts +7 -1
  74. package/fjs/djs/proof.f.d.mts +18 -2
  75. package/fjs/djs/proof.f.mjs +187 -12
  76. package/fjs/djs/serializer/module.f.d.mts +25 -8
  77. package/fjs/djs/serializer/module.f.mjs +61 -15
  78. package/fjs/djs/serializer/proof.f.d.mts +5 -0
  79. package/fjs/djs/serializer/proof.f.mjs +23 -1
  80. package/fjs/djs/tokenizer/module.f.d.mts +17 -5
  81. package/fjs/djs/tokenizer/module.f.mjs +99 -60
  82. package/fjs/djs/tokenizer/proof.f.d.mts +1 -1
  83. package/fjs/djs/tokenizer/proof.f.mjs +74 -67
  84. package/fjs/djs/transpiler/module.f.d.mts +12 -7
  85. package/fjs/djs/transpiler/module.f.mjs +82 -56
  86. package/fjs/djs/transpiler/types.d.ts +7 -3
  87. package/fjs/djs/types.d.ts +7 -1
  88. package/fjs/effects/list/module.f.d.mts +14 -11
  89. package/fjs/effects/list/module.f.mjs +13 -11
  90. package/fjs/effects/list/types.d.ts +27 -7
  91. package/fjs/effects/memory/module.f.d.mts +2 -1
  92. package/fjs/effects/memory/module.f.mjs +3 -4
  93. package/fjs/effects/memory/proof.f.mjs +13 -7
  94. package/fjs/effects/memory/types.d.ts +4 -3
  95. package/fjs/effects/mock/module.f.d.mts +20 -4
  96. package/fjs/effects/mock/module.f.mjs +37 -5
  97. package/fjs/effects/mock/types.d.ts +10 -1
  98. package/fjs/effects/module.d.mts +4 -2
  99. package/fjs/effects/module.f.d.mts +466 -280
  100. package/fjs/effects/module.f.mjs +537 -299
  101. package/fjs/effects/module.mjs +2 -1
  102. package/fjs/effects/node/memory/module.d.mts +4 -2
  103. package/fjs/effects/node/memory/module.mjs +6 -3
  104. package/fjs/effects/node/memory/proof.mjs +10 -4
  105. package/fjs/effects/node/module.d.mts +4 -3
  106. package/fjs/effects/node/module.f.d.mts +168 -33
  107. package/fjs/effects/node/module.f.mjs +256 -52
  108. package/fjs/effects/node/module.mjs +57 -34
  109. package/fjs/effects/node/proof.f.d.mts +28 -2
  110. package/fjs/effects/node/proof.f.mjs +161 -42
  111. package/fjs/effects/node/types.d.ts +106 -18
  112. package/fjs/effects/node/virtual/module.f.d.mts +18 -4
  113. package/fjs/effects/node/virtual/module.f.mjs +110 -68
  114. package/fjs/effects/node/virtual/proof.f.d.mts +28 -2
  115. package/fjs/effects/node/virtual/proof.f.mjs +190 -9
  116. package/fjs/effects/proof.f.d.mts +69 -37
  117. package/fjs/effects/proof.f.mjs +410 -130
  118. package/fjs/effects/types.d.ts +161 -33
  119. package/fjs/emergent_testing/module.f.d.mts +19 -12
  120. package/fjs/emergent_testing/module.f.mjs +93 -33
  121. package/fjs/emergent_testing/proof.f.d.mts +21 -7
  122. package/fjs/emergent_testing/proof.f.mjs +166 -32
  123. package/fjs/emergent_testing/types.d.ts +22 -4
  124. package/fjs/fsm/module.f.d.mts +14 -4
  125. package/fjs/fsm/module.f.mjs +54 -37
  126. package/fjs/fsm/proof.f.d.mts +2 -0
  127. package/fjs/fsm/proof.f.mjs +83 -114
  128. package/fjs/js/keywords/module.f.d.mts +52 -0
  129. package/fjs/js/keywords/module.f.mjs +72 -0
  130. package/fjs/js/keywords/proof.f.d.mts +3 -0
  131. package/fjs/js/keywords/proof.f.mjs +13 -0
  132. package/fjs/js/tokenizer/module.f.d.mts +26 -6
  133. package/fjs/js/tokenizer/module.f.mjs +145 -159
  134. package/fjs/js/tokenizer/proof.f.d.mts +1 -0
  135. package/fjs/js/tokenizer/proof.f.mjs +54 -24
  136. package/fjs/js/tokenizer/types.d.ts +33 -24
  137. package/fjs/mcp/cas/module.f.d.mts +1 -6
  138. package/fjs/mcp/cas/module.f.mjs +55 -51
  139. package/fjs/mcp/cas/proof.f.d.mts +15 -0
  140. package/fjs/mcp/cas/proof.f.mjs +174 -0
  141. package/fjs/mcp/evo/module.f.d.mts +19 -10
  142. package/fjs/mcp/evo/module.f.mjs +48 -27
  143. package/fjs/mcp/evo/proof.f.d.mts +6 -1
  144. package/fjs/mcp/evo/proof.f.mjs +115 -31
  145. package/fjs/mcp/module.f.d.mts +4 -4
  146. package/fjs/mcp/module.f.mjs +6 -6
  147. package/fjs/mcp/proof.f.d.mts +5 -3
  148. package/fjs/mcp/proof.f.mjs +112 -46
  149. package/fjs/media/html/module.f.mjs +1 -1
  150. package/fjs/media/json/extended/module.f.d.mts +82 -0
  151. package/fjs/media/json/extended/module.f.mjs +153 -0
  152. package/fjs/media/json/extended/proof.f.d.mts +42 -0
  153. package/fjs/media/json/extended/proof.f.mjs +127 -0
  154. package/fjs/media/json/extended/types.d.ts +23 -0
  155. package/fjs/media/json/module.f.d.mts +8 -2
  156. package/fjs/media/json/module.f.mjs +43 -41
  157. package/fjs/media/json/number/module.f.d.mts +59 -0
  158. package/fjs/media/json/number/module.f.mjs +136 -0
  159. package/fjs/media/json/number/proof.f.d.mts +24 -0
  160. package/fjs/media/json/number/proof.f.mjs +86 -0
  161. package/fjs/media/json/number/types.d.ts +28 -0
  162. package/fjs/media/json/parser/module.f.d.mts +25 -13
  163. package/fjs/media/json/parser/module.f.mjs +114 -70
  164. package/fjs/media/json/parser/proof.f.d.mts +5 -0
  165. package/fjs/media/json/parser/proof.f.mjs +31 -1
  166. package/fjs/media/json/parser/types.d.ts +33 -14
  167. package/fjs/media/json/rtti/module.f.d.mts +1 -1
  168. package/fjs/media/json/rtti/module.f.mjs +1 -1
  169. package/fjs/media/json/rtti/proof.f.mjs +9 -9
  170. package/fjs/media/json/schema/module.f.mjs +3 -13
  171. package/fjs/media/json/schema/proof.f.d.mts +0 -1
  172. package/fjs/media/json/schema/proof.f.mjs +1 -2
  173. package/fjs/media/json/serializer/module.f.d.mts +32 -1
  174. package/fjs/media/json/serializer/module.f.mjs +64 -2
  175. package/fjs/media/json/tokenizer/module.f.mjs +7 -3
  176. package/fjs/media/json/tokenizer/proof.f.d.mts +6 -0
  177. package/fjs/media/json/tokenizer/proof.f.mjs +62 -21
  178. package/fjs/media/json/types.d.ts +36 -10
  179. package/fjs/media/lock/module.f.d.mts +100 -0
  180. package/fjs/media/lock/module.f.mjs +125 -0
  181. package/fjs/media/lock/proof.f.d.mts +33 -0
  182. package/fjs/media/lock/proof.f.mjs +196 -0
  183. package/fjs/media/lock/types.d.ts +15 -0
  184. package/fjs/media/module.f.d.mts +6 -5
  185. package/fjs/media/module.f.mjs +8 -7
  186. package/fjs/media/note/module.f.d.mts +121 -0
  187. package/fjs/media/note/module.f.mjs +131 -0
  188. package/fjs/media/note/proof.f.d.mts +29 -0
  189. package/fjs/media/note/proof.f.mjs +150 -0
  190. package/fjs/media/note/types.d.ts +10 -0
  191. package/fjs/media/proof.f.d.mts +4 -1
  192. package/fjs/media/proof.f.mjs +40 -21
  193. package/fjs/media/revision/module.f.d.mts +78 -7
  194. package/fjs/media/revision/module.f.mjs +119 -12
  195. package/fjs/media/revision/proof.f.d.mts +10 -0
  196. package/fjs/media/revision/proof.f.mjs +88 -0
  197. package/fjs/media/revision/types.d.ts +34 -5
  198. package/fjs/media/type/module.f.d.mts +33 -15
  199. package/fjs/media/type/module.f.mjs +35 -29
  200. package/fjs/media/type/proof.f.d.mts +2 -1
  201. package/fjs/media/type/proof.f.mjs +30 -10
  202. package/fjs/module.f.mjs +29 -8
  203. package/fjs/nanvm/proof.f.mjs +3 -3
  204. package/fjs/nanvm/rust/module.f.mjs +1 -1
  205. package/fjs/nanvm/update/module.f.d.mts +4 -4
  206. package/fjs/nanvm/update/module.f.mjs +8 -9
  207. package/fjs/nanvm/update/proof.f.mjs +4 -3
  208. package/fjs/proof.f.d.mts +3 -3
  209. package/fjs/proof.f.mjs +36 -10
  210. package/fjs/protocol/json_rpc/module.f.d.mts +2 -2
  211. package/fjs/protocol/json_rpc/module.f.mjs +3 -3
  212. package/fjs/protocol/json_rpc/proof.f.mjs +4 -4
  213. package/fjs/protocol/mcp/module.f.d.mts +10 -13
  214. package/fjs/protocol/mcp/module.f.mjs +74 -58
  215. package/fjs/protocol/mcp/proof.f.d.mts +13 -4
  216. package/fjs/protocol/mcp/proof.f.mjs +193 -61
  217. package/fjs/protocol/mcp/stdio/module.f.d.mts +15 -7
  218. package/fjs/protocol/mcp/stdio/module.f.mjs +38 -25
  219. package/fjs/protocol/mcp/stdio/proof.f.d.mts +4 -2
  220. package/fjs/protocol/mcp/stdio/proof.f.mjs +45 -12
  221. package/fjs/protocol/mcp/stdio/types.d.ts +1 -1
  222. package/fjs/protocol/mcp/types.d.ts +17 -6
  223. package/fjs/sul/id/module.f.d.mts +0 -1
  224. package/fjs/sul/id/module.f.mjs +2 -3
  225. package/fjs/sul/level/hash/module.f.mjs +2 -1
  226. package/fjs/sul/level/hash/proof.f.mjs +2 -2
  227. package/fjs/sul/module.f.mjs +18 -13
  228. package/fjs/text/code_point/module.f.d.mts +8 -0
  229. package/fjs/text/code_point/module.f.mjs +8 -1
  230. package/fjs/text/code_point/proof.f.d.mts +1 -0
  231. package/fjs/text/code_point/proof.f.mjs +11 -0
  232. package/fjs/text/sgr/module.f.d.mts +4 -6
  233. package/fjs/text/sgr/module.f.mjs +6 -7
  234. package/fjs/text/utf16/module.f.mjs +7 -2
  235. package/fjs/text/utf16/proof.f.mjs +3 -3
  236. package/fjs/text/utf8/module.f.mjs +4 -2
  237. package/fjs/types/array/module.f.mjs +14 -2
  238. package/fjs/types/bit_vec/module.f.d.mts +0 -2
  239. package/fjs/types/bit_vec/module.f.mjs +46 -32
  240. package/fjs/types/bit_vec/proof.f.mjs +2 -2
  241. package/fjs/types/btree/remove/module.f.mjs +1 -1
  242. package/fjs/types/btree/set/module.f.mjs +9 -11
  243. package/fjs/types/btree/set/proof.f.mjs +12 -0
  244. package/fjs/types/byte_set/module.f.d.mts +11 -4
  245. package/fjs/types/byte_set/module.f.mjs +14 -6
  246. package/fjs/types/byte_set/proof.f.mjs +7 -7
  247. package/fjs/types/function/compare/module.f.mjs +10 -3
  248. package/fjs/types/list/module.f.d.mts +1 -1
  249. package/fjs/types/list/module.f.mjs +1 -1
  250. package/fjs/types/nullable/module.f.d.mts +18 -4
  251. package/fjs/types/nullable/module.f.mjs +21 -4
  252. package/fjs/types/nullable/proof.f.d.mts +4 -0
  253. package/fjs/types/nullable/proof.f.mjs +15 -0
  254. package/fjs/types/object/module.f.d.mts +12 -2
  255. package/fjs/types/object/module.f.mjs +11 -1
  256. package/fjs/types/patricia_trie/module.f.mjs +26 -11
  257. package/fjs/types/result/module.f.d.mts +3 -3
  258. package/fjs/types/result/module.f.mjs +3 -3
  259. package/fjs/types/rtti/common/module.f.d.mts +25 -28
  260. package/fjs/types/rtti/common/module.f.mjs +36 -32
  261. package/fjs/types/rtti/common/proof.f.mjs +5 -5
  262. package/fjs/types/rtti/data/module.f.d.mts +14 -0
  263. package/fjs/types/rtti/data/module.f.mjs +25 -2
  264. package/fjs/types/rtti/data/proof.f.d.mts +1 -0
  265. package/fjs/types/rtti/data/proof.f.mjs +25 -2
  266. package/fjs/types/rtti/parse/module.f.d.mts +24 -14
  267. package/fjs/types/rtti/parse/module.f.mjs +37 -28
  268. package/fjs/types/rtti/parse/proof.f.d.mts +3 -2
  269. package/fjs/types/rtti/parse/proof.f.mjs +33 -14
  270. package/fjs/types/rtti/proof.f.mjs +3 -1
  271. package/fjs/types/rtti/ts/module.f.mjs +13 -17
  272. package/fjs/types/rtti/ts/types.d.ts +14 -1
  273. package/fjs/types/rtti/validate/module.f.d.mts +81 -19
  274. package/fjs/types/rtti/validate/module.f.mjs +107 -61
  275. package/fjs/types/rtti/validate/proof.f.d.mts +13 -10
  276. package/fjs/types/rtti/validate/proof.f.mjs +186 -197
  277. package/fjs/types/sorted_set/module.f.d.mts +18 -0
  278. package/fjs/types/sorted_set/module.f.mjs +22 -0
  279. package/fjs/types/sorted_set/proof.f.d.mts +1 -0
  280. package/fjs/types/sorted_set/proof.f.mjs +16 -1
  281. package/fjs/types/uint8array/module.f.d.mts +1 -1
  282. package/fjs/types/uint8array/module.f.mjs +1 -1
  283. package/fjs/website/module.f.d.mts +3 -3
  284. package/fjs/website/module.f.mjs +4 -7
  285. package/fjs/website/proof.f.mjs +2 -1
  286. package/package.json +2 -2
  287. package/fjs/dev/package_json/module.f.d.mts +0 -39
  288. package/fjs/dev/package_json/module.f.mjs +0 -40
  289. package/fjs/dev/package_json/proof.f.d.mts +0 -6
  290. package/fjs/dev/package_json/proof.f.mjs +0 -32
  291. package/fjs/effects/eff/module.f.d.mts +0 -20
  292. package/fjs/effects/eff/module.f.mjs +0 -70
  293. package/fjs/effects/eff/proof.f.d.mts +0 -15
  294. package/fjs/effects/eff/proof.f.mjs +0 -69
  295. package/fjs/effects/eff/types.d.ts +0 -71
  296. package/fjs/types/rtti/validate/types.d.ts +0 -6
@@ -1,64 +1,88 @@
1
1
  /**
2
- * @module
3
- *
2
+ * @import { CodePoint } from '../../text/utf16/types.ts'
4
3
  * @import { RuleSet } from '../data/types.ts'
4
+ * @import { CodePointMeta } from '../descent/types.ts'
5
+ * @import { Rule as FRule } from '../types.ts'
5
6
  * @import { MatchResult } from './types.ts'
6
7
  */
7
8
 
8
9
  import { stringToCodePointList } from '../../text/utf16/module.f.mjs'
9
- import { toArray } from '../../types/list/module.f.mjs'
10
+ import { map, toArray } from '../../types/list/module.f.mjs'
10
11
  import { commaJoin0Plus, eof, option, range, repeat0Plus, set } from '../module.f.mjs'
11
12
  import { toData } from '../data/module.f.mjs'
13
+ import { descentParser } from '../descent/module.f.mjs'
12
14
  import { dispatchMap, parser, parserRuleSet } from './module.f.mjs'
13
- import { assertEq } from '../../asserts/module.f.mjs'
14
- import { deterministic } from '../testlib.f.mjs'
15
+ import { assertEq, assertNotNullish } from '../../asserts/module.f.mjs'
16
+ import { deterministic, showAst } from '../testlib.f.mjs'
17
+
18
+ /** @type {(cp: CodePoint) => CodePointMeta<unknown>} */
19
+ const mapCodePoint = cp => [cp, undefined]
20
+
21
+ /**
22
+ * One grammar matched by both backends, which must build the same AST for it:
23
+ * the shape is pinned once and asserted against each backend, so neither can
24
+ * drift from the shared contract without this failing.
25
+ *
26
+ * @type {(rule: FRule, s: string, expected: string) => () => void}
27
+ */
28
+ const bothBackends = (rule, s, expected) => () => {
29
+ const [, entry] = toData(rule)
30
+ const cp = toArray(stringToCodePointList(s))
31
+ const dm = descentParser(rule)(entry, toArray(map(mapCodePoint)(cp)))
32
+ assertEq(showAst(dm.ast), expected, s)
33
+ assertEq(showAst(parser(rule)(entry, cp)[0]), expected, s)
34
+ }
15
35
 
16
36
  export const proof = {
17
37
  dispatch: [
18
38
  () => {
19
39
  const terminalRangeRule = range('AF')
20
- const data = toData(terminalRangeRule)
21
- const dm = dispatchMap(data[0])
22
- const result = JSON.stringify(dm)
23
- if (result !== '{"":{"rangeMap":[[null,64],[{"rules":[]},70]]}}') { throw result }
40
+ const dm = dispatchMap(toData(terminalRangeRule)[0])
41
+ assertEq(
42
+ JSON.stringify(dm),
43
+ '{"":{"rangeMap":[[null,64],[{"name":""},70]]}}')
24
44
  },
25
45
  () => {
46
+ // The sequence's own entry holds its first set only — the first
47
+ // item's, because that item cannot match empty. Nothing is
48
+ // consumed by dispatch, so no chain of follow-up rules is stored.
26
49
  const stringRule = 'AB'
27
- const data = toData(stringRule)
28
- const dm = dispatchMap(data[0])
29
- const result = JSON.stringify(dm)
30
- if (result !== '{"0":{"rangeMap":[[null,64],[{"rules":[]},65]]},"1":{"rangeMap":[[null,65],[{"rules":[]},66]]},"":{"rangeMap":[[null,64],[{"rules":["1"]},65]]}}') { throw result }
50
+ const dm = dispatchMap(toData(stringRule)[0])
51
+ assertEq(
52
+ JSON.stringify(dm),
53
+ '{"0":{"rangeMap":[[null,64],[{"name":"0"},65]]},"1":{"rangeMap":[[null,65],[{"name":"1"},66]]},"":{"rangeMap":[[null,64],[{"name":"0"},65]]}}')
31
54
  },
32
55
  () => {
33
56
  const a = range('AA')
34
57
  const b = range('BB')
35
58
  const ab = [a, b]
36
- const data = toData(ab)
37
- const dm = dispatchMap(data[0])
38
- const result = JSON.stringify(dm)
39
- if (result !== '{"0":{"rangeMap":[[null,64],[{"rules":[]},65]]},"1":{"rangeMap":[[null,65],[{"rules":[]},66]]},"":{"rangeMap":[[null,64],[{"rules":["1"]},65]]}}') { throw result }
59
+ const dm = dispatchMap(toData(ab)[0])
60
+ assertEq(
61
+ JSON.stringify(dm),
62
+ '{"0":{"rangeMap":[[null,64],[{"name":"0"},65]]},"1":{"rangeMap":[[null,65],[{"name":"1"},66]]},"":{"rangeMap":[[null,64],[{"name":"0"},65]]}}')
40
63
  },
41
64
  () => {
42
65
  const emptyRule = ''
43
- const data = toData(emptyRule)
44
- const dm = dispatchMap(data[0])
45
- const result = JSON.stringify(dm)
46
- if (result !== '{"":{"emptyTag":true,"rangeMap":[]}}') { throw result }
66
+ const dm = dispatchMap(toData(emptyRule)[0])
67
+ assertEq(JSON.stringify(dm), '{"":{"rangeMap":[]}}')
47
68
  },
48
69
  () => {
70
+ // A variant's entry is the one whose values are read: the branch
71
+ // each lookahead selects, entered before the symbol is consumed.
49
72
  const variantRule = { 'a': range('AA'), 'b': range('BB')}
50
- const data = toData(variantRule)
51
- const dm = dispatchMap(data[0])
52
- const result = JSON.stringify(dm)
53
- if (result !== '{"0":{"rangeMap":[[null,64],[{"rules":[]},65]]},"1":{"rangeMap":[[null,65],[{"rules":[]},66]]},"":{"rangeMap":[[null,64],[{"tag":"a","rules":[]},65],[{"tag":"b","rules":[]},66]]}}') { throw result }
73
+ const dm = dispatchMap(toData(variantRule)[0])
74
+ assertEq(
75
+ JSON.stringify(dm),
76
+ '{"0":{"rangeMap":[[null,64],[{"name":"0"},65]]},"1":{"rangeMap":[[null,65],[{"name":"1"},66]]},"":{"rangeMap":[[null,64],[{"tag":"a","name":"0"},65],[{"tag":"b","name":"1"},66]]}}')
54
77
  },
55
78
  () => {
79
+ // `empty` is the nullable branch a dispatch miss selects.
56
80
  const emptyRule = ''
57
81
  const variantRule = { 'e': emptyRule, 'a': range('AA')}
58
- const data = toData(variantRule)
59
- const dm = dispatchMap(data[0])
60
- const result = JSON.stringify(dm)
61
- if (result !== '{"0":{"emptyTag":true,"rangeMap":[]},"1":{"rangeMap":[[null,64],[{"rules":[]},65]]},"":{"emptyTag":"e","rangeMap":[[null,64],[{"tag":"a","rules":[]},65]]}}') { throw result }
82
+ const dm = dispatchMap(toData(variantRule)[0])
83
+ assertEq(
84
+ JSON.stringify(dm),
85
+ '{"0":{"rangeMap":[]},"1":{"rangeMap":[[null,64],[{"name":"1"},65]]},"":{"empty":{"tag":"e","name":"0"},"rangeMap":[[null,64],[{"tag":"a","name":"1"},65]]}}')
62
86
  },
63
87
  () => {
64
88
  const emptyRule = ''
@@ -66,12 +90,14 @@ export const proof = {
66
90
  const optionalMinusRule = { 'none': emptyRule, 'minus': minursRule}
67
91
  const digitRule = range('09')
68
92
  const numberRule = [optionalMinusRule, digitRule]
69
- const data = toData(numberRule)
70
- const dm = dispatchMap(data[0])
71
- const result = JSON.stringify(dm)
72
- if (result !== '{"0":{"emptyTag":"none","rangeMap":[[null,44],[{"tag":"minus","rules":[]},45]]},"1":{"emptyTag":true,"rangeMap":[]},"2":{"rangeMap":[[null,44],[{"rules":[]},45]]},"3":{"rangeMap":[[null,47],[{"rules":[]},57]]},"":{"rangeMap":[[null,44],[{"tag":"minus","rules":["3"]},45],[null,47],[{"rules":[]},57]]}}') { throw result }
93
+ const dm = dispatchMap(toData(numberRule)[0])
94
+ assertEq(
95
+ JSON.stringify(dm),
96
+ '{"0":{"empty":{"tag":"none","name":"1"},"rangeMap":[[null,44],[{"tag":"minus","name":"2"},45]]},"1":{"rangeMap":[]},"2":{"rangeMap":[[null,44],[{"name":"2"},45]]},"3":{"rangeMap":[[null,47],[{"name":"3"},57]]},"":{"rangeMap":[[null,44],[{"tag":"minus","name":"2"},45],[null,47],[{"name":"3"},57]]}}')
73
97
  },
74
98
  () => {
99
+ // A nullable prefix merges each item's first set into the
100
+ // sequence's, up to and including the first non-nullable item.
75
101
  const emptyRule = ''
76
102
  const spaceRule = range(' ')
77
103
  const optionalSpaceRule = { 'none': emptyRule, 'space': spaceRule}
@@ -79,113 +105,126 @@ export const proof = {
79
105
  const optionalMinusRule = { 'none': emptyRule, 'minus': minusRule}
80
106
  const digitRule = range('09')
81
107
  const numberRule = [optionalSpaceRule, optionalMinusRule, digitRule]
82
- const data = toData(numberRule)
83
- const dm = dispatchMap(data[0])
84
- const result = JSON.stringify(dm)
85
- if (result !== '{"0":{"emptyTag":"none","rangeMap":[[null,31],[{"tag":"space","rules":[]},32]]},"1":{"emptyTag":true,"rangeMap":[]},"2":{"rangeMap":[[null,31],[{"rules":[]},32]]},"3":{"emptyTag":"none","rangeMap":[[null,44],[{"tag":"minus","rules":[]},45]]},"4":{"rangeMap":[[null,44],[{"rules":[]},45]]},"5":{"rangeMap":[[null,47],[{"rules":[]},57]]},"":{"rangeMap":[[null,31],[{"tag":"space","rules":["3","5"]},32],[null,44],[{"tag":"minus","rules":["5"]},45],[null,47],[{"rules":[]},57]]}}') { throw result }
108
+ const dm = dispatchMap(toData(numberRule)[0])
109
+ assertEq(
110
+ JSON.stringify(dm),
111
+ '{"0":{"empty":{"tag":"none","name":"1"},"rangeMap":[[null,31],[{"tag":"space","name":"2"},32]]},"1":{"rangeMap":[]},"2":{"rangeMap":[[null,31],[{"name":"2"},32]]},"3":{"empty":{"tag":"none","name":"1"},"rangeMap":[[null,44],[{"tag":"minus","name":"4"},45]]},"4":{"rangeMap":[[null,44],[{"name":"4"},45]]},"5":{"rangeMap":[[null,47],[{"name":"5"},57]]},"":{"rangeMap":[[null,31],[{"tag":"space","name":"2"},32],[null,44],[{"tag":"minus","name":"4"},45],[null,47],[{"name":"5"},57]]}}')
112
+ },
113
+ () => {
114
+ // A sequence of nullable items only: the first-set loop runs off
115
+ // the end instead of stopping at a non-nullable item.
116
+ /** @type {RuleSet} */
117
+ const ruleSet = {
118
+ s: ['o1', 'o2'],
119
+ o1: { some: 'm', none: 'e' },
120
+ o2: { some: 'a', none: 'e' },
121
+ m: range('--'),
122
+ a: range('AA'),
123
+ e: [],
124
+ }
125
+ assertEq(
126
+ JSON.stringify(dispatchMap(ruleSet)),
127
+ '{"m":{"rangeMap":[[null,44],[{"name":"m"},45]]},"e":{"rangeMap":[]},"o1":{"empty":{"tag":"none","name":"e"},"rangeMap":[[null,44],[{"tag":"some","name":"m"},45]]},"a":{"rangeMap":[[null,64],[{"name":"a"},65]]},"o2":{"empty":{"tag":"none","name":"e"},"rangeMap":[[null,64],[{"tag":"some","name":"a"},65]]},"s":{"rangeMap":[[null,44],[{"tag":"some","name":"m"},45],[null,64],[{"tag":"some","name":"a"},65]]}}')
128
+ assertEq(
129
+ JSON.stringify(parserRuleSet(ruleSet)('s', [45, 65])),
130
+ '[{"sequence":[{"tag":"some","sequence":[45]},{"tag":"some","sequence":[65]}]},true,[]]')
86
131
  }
87
132
  ],
88
133
  parser: [
89
134
  () => {
90
135
  const emptyRule = ''
91
136
  const m = parser(emptyRule)
92
- const mr = m("", [])
93
- const result = JSON.stringify(mr)
94
- if (result !== '[{"tag":true,"sequence":[]},true,[]]') { throw result }
137
+ assertEq(JSON.stringify(m("", [])), '[{"sequence":[]},true,[]]')
95
138
  },
96
139
  () => {
97
140
  const emptyRule = ''
98
141
  const m = parser(emptyRule)
99
- const mr = m("", [65, 70])
100
- const result = JSON.stringify(mr)
101
- if (result !== '[{"tag":true,"sequence":[]},true,[65,70]]') { throw result }
142
+ assertEq(JSON.stringify(m("", [65, 70])), '[{"sequence":[]},true,[65,70]]')
102
143
  },
103
144
  () => {
104
145
  const terminalRangeRule = range('AF')
105
146
  const m = parser(terminalRangeRule)
106
- const mr = m("", [65])
107
- const result = JSON.stringify(mr)
108
- if (result !== '[{"sequence":[65]},true,[]]') { throw result }
147
+ assertEq(JSON.stringify(m("", [65])), '[{"sequence":[65]},true,[]]')
109
148
  },
110
149
  () => {
111
150
  const terminalRangeRule = 0x000079_000087
112
151
  const m = parser(terminalRangeRule)
113
- const mr = m("", [64])
114
- const result = JSON.stringify(mr)
115
- if (result !== '[{"sequence":[]},false,[64]]') { throw result }
152
+ assertEq(JSON.stringify(m("", [64])), '[{"sequence":[]},false,[64]]')
116
153
  },
117
154
  () => {
118
155
  const terminalRangeRule = 0x000080_000087 //broken range
119
156
  const m = parser(terminalRangeRule)
120
- const mr = m("", [64])
121
- const result = JSON.stringify(mr)
122
- if (result !== '[{"sequence":[]},false,[64]]') { throw result }
157
+ assertEq(JSON.stringify(m("", [64])), '[{"sequence":[]},false,[64]]')
158
+ },
159
+ () => {
160
+ const variantRule = { 'a': range('AA'), 'b': range('BB')}
161
+ const m = parser(variantRule)
162
+ assertEq(JSON.stringify(m("", [65])), '[{"tag":"a","sequence":[65]},true,[]]')
123
163
  },
124
164
  () => {
125
165
  const variantRule = { 'a': range('AA'), 'b': range('BB')}
126
166
  const m = parser(variantRule)
127
- const mr = m("", [65])
128
- const result = JSON.stringify(mr)
129
- if (result !== '[{"tag":"a","sequence":[65]},true,[]]') { throw result }
167
+ assertEq(JSON.stringify(m("", [64])), '[{"sequence":[]},false,[64]]')
130
168
  },
131
169
  () => {
170
+ // No branch dispatches on the end of input and none is nullable:
171
+ // the match ran out of input, the `null` remainder.
132
172
  const variantRule = { 'a': range('AA'), 'b': range('BB')}
133
173
  const m = parser(variantRule)
134
- const mr = m("", [64])
135
- const result = JSON.stringify(mr)
136
- if (result !== '[{"sequence":[]},false,[64]]') { throw result }
174
+ assertEq(JSON.stringify(m("", [])), '[{"sequence":[]},true,null]')
137
175
  },
138
176
  () => {
139
177
  const emptyRule = ''
140
178
  const variantRule = { 'e': emptyRule, 'a': range('AA')}
141
- const m = parser(variantRule)
142
- const mr = m("", [])
143
- const result = JSON.stringify(mr)
144
- if (result !== '[{"tag":"e","sequence":[]},true,[]]') { throw result }
179
+ const m = parser(variantRule)
180
+ assertEq(JSON.stringify(m("", [])), '[{"tag":"e","sequence":[]},true,[]]')
145
181
  },
146
182
  () => {
147
183
  const emptyRule = ''
148
184
  const variantRule = { 'e': emptyRule, 'a': range('AA')}
149
185
  const m = parser(variantRule)
150
- const mr = m("", [64])
151
- const result = JSON.stringify(mr)
152
- if (result !== '[{"tag":"e","sequence":[]},true,[64]]') { throw result }
186
+ assertEq(JSON.stringify(m("", [64])), '[{"tag":"e","sequence":[]},true,[64]]')
153
187
  },
154
188
  () => {
189
+ // Every item of a sequence owns a node — the leading one included.
155
190
  const stringRule = 'AB'
156
191
  const m = parser(stringRule)
157
- const mr = m("", [65,66])
158
- const result = JSON.stringify(mr)
159
- if (result !== '[{"sequence":[65,{"sequence":[66]}]},true,[]]') { throw result }
192
+ assertEq(
193
+ JSON.stringify(m("", [65,66])),
194
+ '[{"sequence":[{"sequence":[65]},{"sequence":[66]}]},true,[]]')
160
195
  },
161
196
  () => {
197
+ // LL(1) never backtracks, so a failure keeps the position where
198
+ // matching stopped: on the rejected `C`.
162
199
  const stringRule = 'AB'
163
200
  const m = parser(stringRule)
164
- const mr = m("", [65,67])
165
- const result = JSON.stringify(mr)
166
- if (result !== '[{"sequence":[]},false,[67]]') { throw result }
201
+ assertEq(JSON.stringify(m("", [65,67])), '[{"sequence":[]},false,[67]]')
167
202
  },
168
203
  () => {
204
+ // An option that matched empty still owns a node, so the AST says
205
+ // the option was considered.
169
206
  const emptyRule = ''
170
207
  const minursRule = range('--')
171
208
  const optionalMinusRule = { 'none': emptyRule, 'minus': minursRule}
172
209
  const digitRule = range('09')
173
210
  const numberRule = [optionalMinusRule, digitRule]
174
211
  const m = parser(numberRule)
175
- const mr = m("", [50])
176
- const result = JSON.stringify(mr)
177
- if (result !== '[{"sequence":[50]},true,[]]') { throw result }
212
+ assertEq(
213
+ JSON.stringify(m("", [50])),
214
+ '[{"sequence":[{"tag":"none","sequence":[]},{"sequence":[50]}]},true,[]]')
178
215
  },
179
216
  () => {
217
+ // Taken, the option's tag lands on its own node, not the
218
+ // enclosing sequence's.
180
219
  const emptyRule = ''
181
220
  const minusRule = range('--')
182
221
  const optionalMinusRule = { 'none': emptyRule, 'minus': minusRule}
183
222
  const digitRule = range('09')
184
223
  const numberRule = [optionalMinusRule, digitRule]
185
224
  const m = parser(numberRule)
186
- const mr = m("", [45,50])
187
- const result = JSON.stringify(mr)
188
- if (result !== '[{"tag":"minus","sequence":[45,{"sequence":[50]}]},true,[]]') { throw result }
225
+ assertEq(
226
+ JSON.stringify(m("", [45,50])),
227
+ '[{"sequence":[{"tag":"minus","sequence":[45]},{"sequence":[50]}]},true,[]]')
189
228
  },
190
229
  () => {
191
230
  const emptyRule = ''
@@ -194,9 +233,11 @@ export const proof = {
194
233
  const digitRule = range('09')
195
234
  const numberRule = [optionalMinusRule, digitRule]
196
235
  const m = parser(numberRule)
197
- const mr = m("", [])
198
- const result = JSON.stringify(mr)
199
- if (result !== '[{"sequence":[]},true,null]') { throw result } //if remainder is null it means failed
236
+ // The `null` remainder means the match ran out of input; the AST
237
+ // keeps what had matched by then.
238
+ assertEq(
239
+ JSON.stringify(m("", [])),
240
+ '[{"sequence":[{"tag":"none","sequence":[]},{"sequence":[]}]},true,null]')
200
241
  },
201
242
  () => {
202
243
  const m = parser(option('a'))
@@ -242,51 +283,78 @@ export const proof = {
242
283
  expect('{a}', true)
243
284
  },
244
285
  () => {
245
- const m = parser(deterministic())
286
+ const rule = deterministic()
287
+ const [, entry] = toData(rule)
288
+ const md = descentParser(rule)
289
+ const ml = parser(rule)
246
290
 
247
- /** @type {(mr: MatchResult) => boolean} */
248
- const isSuccess = (mr) => mr[1] && mr[2]?.length === 0
249
- /** @type {(s: string, success: boolean) => void} */
250
- const expect = (s, success) => {
251
- const mr = m('', toArray(stringToCodePointList(s)))
252
- assertEq(isSuccess(mr), success, mr)
291
+ // The same inputs as `bnf/descent`'s copy of this group, which
292
+ // pins the AST each one builds. Both backends produce the same
293
+ // AST for the same grammar, so this side asserts equality with
294
+ // `bnf/descent` instead of restating the expectations.
295
+ /** @type {(s: string) => void} */
296
+ const expectSameAst = s => {
297
+ const cp = toArray(stringToCodePointList(s))
298
+ const dm = md(entry, toArray(map(mapCodePoint)(cp)))
299
+ const mr = ml(entry, cp)
300
+ assertEq(mr[1], true, s)
301
+ assertEq(mr[2]?.length, 0, s)
302
+ assertEq(showAst(mr[0]), showAst(dm.ast), s)
253
303
  }
254
304
 
255
- expect(' true ', true)
256
- expect(' tr2ue ', false)
257
- expect(' true" ', false)
258
- expect(' "Hello" ', true)
259
- expect(' "Hello ', false)
260
- expect(' "Hello\\n\\r\\"" ', true)
261
- expect(' -56.7e+5 ', true)
262
- expect(' h-56.7e+5 ', false)
263
- expect(' -56.7e+5 3', false)
264
- expect(' [] ', true)
265
- expect(' {} ', true)
266
- expect(' [[[]]] ', true)
267
- expect(' [1] ', true)
268
- expect(' [ 12, false, "a"] ', true)
269
- expect(' [ 12, false2, "a"] ', false)
270
- expect(' { "q": [ 12, false, [{"b" : "c"}], "a"] } ', true)
271
- expect(' { "q": [ 12, false, [{}], "a"] } ', true)
272
- expect(' { "q": [ 12, false, [}], "a"] } ', false)
273
- expect(' [{ "q": [ 12, false, [{}], "a"] }] ', true)
274
- expect(' [{ "q": [ 12, false, [}], "a"] }] ', false)
305
+ /** @type {(s: string) => void} */
306
+ const expectNoMatch = s => {
307
+ const mr = ml(entry, toArray(stringToCodePointList(s)))
308
+ assertEq(mr[1] && mr[2]?.length === 0, false, s)
309
+ }
310
+
311
+ expectSameAst(' true ')
312
+ expectSameAst(' "Hello" ')
313
+ expectSameAst(' "Hello\\n\\r\\"" ')
314
+ expectSameAst(' -56.7e+5 ')
315
+ expectSameAst(' [] ')
316
+ expectSameAst(' {} ')
317
+ expectSameAst(' [[[]]] ')
318
+ expectSameAst(' [1] ')
319
+ expectSameAst(' [ 12, false, "a"] ')
320
+ expectSameAst(' { "q": [ 12, false, [{"b" : "c"}], "a"] } ')
321
+ expectSameAst(' { "q": [ 12, false, [{}], "a"] } ')
322
+ expectSameAst(' [{ "q": [ 12, false, [{}], "a"] }] ')
323
+
324
+ expectNoMatch(' tr2ue ')
325
+ expectNoMatch(' true" ')
326
+ expectNoMatch(' "Hello ')
327
+ expectNoMatch(' h-56.7e+5 ')
328
+ expectNoMatch(' -56.7e+5 3')
329
+ expectNoMatch(' [ 12, false2, "a"] ')
330
+ expectNoMatch(' { "q": [ 12, false, [}], "a"] } ')
331
+
332
+ // The invalid input in detail. This backend never backtracks, so
333
+ // the remainder is where matching stopped: on the `}` that closes
334
+ // nothing, the same position the descent backend reports as its
335
+ // furthest failure.
336
+ const bad = ' [{ "q": [ 12, false, [}], "a"] }] '
337
+ const badMr = ml(entry, toArray(stringToCodePointList(bad)))
338
+ assertEq(badMr[1], false, bad)
339
+ const remainder = assertNotNullish(badMr[2])
340
+ assertEq(bad.length - remainder.length, 25, bad)
341
+ assertEq(bad[25], '}', bad)
275
342
  }
276
343
  ],
277
344
  longInput: [
278
345
  () => {
279
- // Long right-recursive repetition: one `repeat0Plus` chain across the
280
- // whole input. This is the shape that overflowed the JS call stack
281
- // when the matcher recursed once per consumed code point.
282
- const m = parser(repeat0Plus(set(' \n\r\t')))
283
- const [, success, remainder] = m('', toArray(stringToCodePointList(' '.repeat(10000))))
346
+ // Long repetition across the whole input: matched iteratively, so
347
+ // neither the JS call stack nor the frame stack grows with it.
348
+ const rule = repeat0Plus(set(' \n\r\t'))
349
+ const m = parser(rule)
350
+ const [, success, remainder] = m(toData(rule)[1], toArray(stringToCodePointList(' '.repeat(10000))))
284
351
  assertEq(success, true)
285
352
  assertEq(remainder?.length, 0)
286
353
  },
287
354
  () => {
288
- // Deep non-repetition nesting: 5000 bracket levels in the JSON-like
289
- // test grammar a shape a repetition-specific fix would not cover.
355
+ // Deep non-repetition nesting: 5000 bracket levels in the
356
+ // JSON-like test grammar. The frame stack grows on the heap, so
357
+ // depth is bounded by memory rather than the JS call stack.
290
358
  const m = parser(deterministic())
291
359
  const n = 5000
292
360
  const cp = toArray(stringToCodePointList('['.repeat(n) + ']'.repeat(n)))
@@ -300,7 +368,7 @@ export const proof = {
300
368
  // EOF dispatches below every ordinary symbol, so its cut point is
301
369
  // `-2` — the stored endpoint codes never reach the dispatch map.
302
370
  const dm = dispatchMap(toData(eof)[0])
303
- assertEq(JSON.stringify(dm), '{"":{"rangeMap":[[null,-2],[{"rules":[]},-1]]}}')
371
+ assertEq(JSON.stringify(dm), '{"":{"rangeMap":[[null,-2],[{"name":""},-1]]}}')
304
372
  },
305
373
  () => {
306
374
  // The matcher synthesizes one EOF after the physical input, so an
@@ -319,14 +387,20 @@ export const proof = {
319
387
  // Non-empty input: the terminal consumes the synthesized EOF after
320
388
  // the last code point.
321
389
  const m = parser([range('AA'), eof])
322
- assertEq(JSON.stringify(m('', [65])), '[{"sequence":[65,{"sequence":[]}]},true,[]]')
390
+ assertEq(JSON.stringify(m('', [65])), '[{"sequence":[{"sequence":[65]},{"sequence":[]}]},true,[]]')
323
391
  },
324
392
  () => {
325
393
  // Exactly one EOF is synthesized: the second `eof` terminal has
326
394
  // nothing to consume, so the match runs out of input — the `null`
327
395
  // remainder this backend reports for that.
328
396
  const m = parser([eof, eof])
329
- assertEq(JSON.stringify(m('', [])), '[{"sequence":[{"sequence":[]}]},true,null]')
397
+ assertEq(JSON.stringify(m('', [])), '[{"sequence":[{"sequence":[]},{"sequence":[]}]},true,null]')
398
+ },
399
+ () => {
400
+ // A variant past the consumed EOF has no symbol to dispatch on:
401
+ // the match runs out of input there too.
402
+ const m = parser([eof, { a: range('AA') }])
403
+ assertEq(JSON.stringify(m('', [])), '[{"sequence":[{"sequence":[]},{"sequence":[]}]},true,null]')
330
404
  },
331
405
  () => {
332
406
  // EOF as one alternative among ordinary terminals.
@@ -334,56 +408,156 @@ export const proof = {
334
408
  assertEq(JSON.stringify(m('', [])), '[{"tag":"e","sequence":[]},true,[]]')
335
409
  assertEq(JSON.stringify(m('', [65])), '[{"tag":"a","sequence":[65]},true,[]]')
336
410
  },
337
- ],
338
- repeat: [
339
- () => {
340
- /** @type {readonly [RuleSet, string]} */
341
- const repeatData = [{"":["ws","repa"],"ws":[],"repa":["a",""],"a":1090519105},""]
342
- const dm = dispatchMap(repeatData[0])
343
- const result = JSON.stringify(dm)
344
- if (result !== '{"ws":{"emptyTag":true,"rangeMap":[]},"a":{"rangeMap":[[null,64],[{"rules":[]},65]]},"repa":{"rangeMap":[[null,64],[{"rules":[""]},65]]},"":{"rangeMap":[[null,64],[{"rules":[""]},65]]}}') { throw result }
345
- }
346
- ],
347
- repeatParser: [
348
411
  () => {
349
- /** @type {readonly [RuleSet, string]} */
350
- const repeatData = [{"":["ws","repa"],"ws":[],"repa":["a",""],"a":1090519105},""]
351
- const m = parserRuleSet(repeatData[0])
352
- const mr = m("", [])
353
- const result = JSON.stringify(mr)
354
- if (result !== '[{"sequence":[]},true,null]') { throw result }
412
+ // Repetition terminates on EOF: consuming it moves the cursor, so
413
+ // the repeat makes exactly one round and then stops — its second
414
+ // round has no symbol left to dispatch on.
415
+ const rule = repeat0Plus(eof)
416
+ const m = parser(rule)
417
+ assertEq(JSON.stringify(m(toData(rule)[1], [])), '[{"sequence":[{"sequence":[]}]},true,[]]')
355
418
  },
356
419
  () => {
357
- /** @type {readonly [RuleSet, string]} */
358
- const repeatData = [{"":["ws","repa"],"ws":[],"repa":["a",""],"a":1090519105},""]
359
- const m = parserRuleSet(repeatData[0])
360
- const mr = m("", [65])
361
- const result = JSON.stringify(mr)
362
- if (result !== '[{"sequence":[65,{"sequence":[]}]},true,null]') { throw result }
420
+ // Running out of input inside a repetition round ends the whole
421
+ // match, keeping the rounds collected so far.
422
+ const rule = repeat0Plus([eof, eof])
423
+ const m = parser(rule)
424
+ assertEq(
425
+ JSON.stringify(m(toData(rule)[1], [])),
426
+ '[{"sequence":[{"sequence":[{"sequence":[]},{"sequence":[]}]}]},true,null]')
363
427
  },
428
+ ],
429
+ repeat: [
364
430
  () => {
431
+ // A `repeat` rule's entry is its item's first set: the matcher
432
+ // starts one more round exactly while the lookahead is in it, and
433
+ // matches the repetition iteratively into one flat node.
434
+ const rule = repeat0Plus(range('AF'))
435
+ const [ruleSet, entry] = toData(rule)
436
+ assertEq(JSON.stringify(ruleSet[entry]), '"0"')
437
+ assertEq(
438
+ JSON.stringify(dispatchMap(ruleSet)[entry]),
439
+ '{"rangeMap":[[null,64],[{"name":"0"},70]]}')
440
+ },
441
+ () => {
442
+ // A repetition of itself has no first set to dispatch on, and
443
+ // asking for one would not terminate. `toData` never folds such a
444
+ // rule; a hand-written rule set can still hold one, and it can
445
+ // only match zero rounds.
446
+ /** @type {RuleSet} */
447
+ const ruleSet = { repeated: 'repeated' }
448
+ assertEq(
449
+ JSON.stringify(dispatchMap(ruleSet)),
450
+ '{"repeated":{"rangeMap":[]}}')
451
+ const m = parserRuleSet(ruleSet)
452
+ assertEq(JSON.stringify(m('repeated', [])), '[{"sequence":[]},true,[]]')
453
+ assertEq(JSON.stringify(m('repeated', [65])), '[{"sequence":[]},true,[65]]')
454
+ },
455
+ () => {
456
+ // The right-recursive spelling `toData` folds away can still be
457
+ // hand-written; it then dispatches — and nests — as the grammar
458
+ // says, one recursion level per item.
365
459
  /** @type {readonly [RuleSet, string]} */
366
460
  const repeatData = [{"":["ws","repa"],"ws":[],"repa":["a",""],"a":1090519105},""]
367
- const m = parserRuleSet(repeatData[0])
368
- const mr = m("", [65,65,65])
369
- const result = JSON.stringify(mr)
370
- if (result !== '[{"sequence":[65,{"sequence":[65,{"sequence":[65,{"sequence":[]}]}]}]},true,null]') { throw result }
461
+ assertEq(
462
+ JSON.stringify(dispatchMap(repeatData[0])),
463
+ '{"ws":{"rangeMap":[]},"a":{"rangeMap":[[null,64],[{"name":"a"},65]]},"repa":{"rangeMap":[[null,64],[{"name":"a"},65]]},"":{"rangeMap":[[null,64],[{"name":"a"},65]]}}')
371
464
  },
465
+ ],
466
+ repeatParser: [
372
467
  () => {
468
+ // The hand-written right-recursive chain: `""` requires `repa`,
469
+ // which requires `a` and `""` again, so every input eventually
470
+ // runs out — the `null` remainder — and the AST nests one level
471
+ // per matched item because the *grammar* is right-recursive.
373
472
  /** @type {readonly [RuleSet, string]} */
374
473
  const repeatData = [{"":["ws","repa"],"ws":[],"repa":["a",""],"a":1090519105},""]
375
474
  const m = parserRuleSet(repeatData[0])
376
- const mr = m("", [66])
377
- const result = JSON.stringify(mr)
378
- if (result !== '[{"sequence":[]},false,[66]]') { throw result }
379
- }
475
+ assertEq(
476
+ JSON.stringify(m("", [])),
477
+ '[{"sequence":[{"sequence":[]},{"sequence":[{"sequence":[]}]}]},true,null]')
478
+ const mr1 = m("", [65])
479
+ assertEq(showAst(mr1[0]), '(() (("A") (() (()))))')
480
+ assertEq(mr1[1], true)
481
+ assertEq(mr1[2], null)
482
+ const mr3 = m("", [65,65,65])
483
+ assertEq(showAst(mr3[0]), '(() (("A") (() (("A") (() (("A") (() (()))))))))')
484
+ assertEq(mr3[1], true)
485
+ assertEq(mr3[2], null)
486
+ assertEq(JSON.stringify(m("", [66])), '[{"sequence":[]},false,[66]]')
487
+ },
380
488
  ],
489
+ // The two backends consume the same `RuleSet` and must agree about the AST
490
+ // it implies: `bnf/descent` builds a node per rule *invocation*, and so
491
+ // does this backend — a rule is entered before its first symbol is
492
+ // consumed, so the dispatch only selects and never absorbs a child's leaf,
493
+ // children or tag into the enclosing node.
494
+ //
495
+ // Each case below is one grammar matched by both backends against one
496
+ // pinned AST, so neither can drift from the shared contract unremarked.
497
+ // These are the cases that used to diverge (see the git history of
498
+ // `../ll1/README.md` for the old shapes); only `variant` always agreed.
499
+ descentEquivalence: {
500
+ // Every item of a sequence owns a node, the leading one included.
501
+ leadingItem: bothBackends(
502
+ [range('AA'), range('BB'), range('CC')], 'ABC',
503
+ '(("A") ("B") ("C"))'),
504
+ // A taken variant tags the branch's own node.
505
+ variant: bothBackends(
506
+ { a: range('AA'), b: range('BB') }, 'A',
507
+ '"a"("A")'),
508
+ // A nullable item that matched empty still owns a node, so the AST
509
+ // says the option was considered.
510
+ skippedOption: bothBackends(
511
+ [option(range('--')), range('09')], '5',
512
+ '("none"() ("5"))'),
513
+ // Taken, the option's tag lands on its own node, not the enclosing one.
514
+ takenOption: bothBackends(
515
+ [option(range('--')), range('09')], '-5',
516
+ '("some"("-") ("5"))'),
517
+ // A repetition is one flat node of items in both backends.
518
+ repetition: bothBackends(
519
+ repeat0Plus(range('AA')), 'AAA',
520
+ '(("A") ("A") ("A"))'),
521
+ // Grouping survives: `[[A,B],C]` and `[A,B,C]` — the `leadingItem`
522
+ // case above — are distinguishable ASTs.
523
+ nesting: bothBackends(
524
+ [[range('AA'), range('BB')], range('CC')], 'ABC',
525
+ '((("A") ("B")) ("C"))'),
526
+ // A repetition leading a sequence: its first set is inlined into the
527
+ // sequence's dispatch entry at build time, yet it still matches as an
528
+ // iterative rule invocation with a flat node of its own.
529
+ repetitionInNullablePrefix: bothBackends(
530
+ [repeat0Plus(range('AA')), range('09')], 'AA5',
531
+ '((("A") ("A")) ("5"))'),
532
+ // The same repetition matching empty leaves an empty node, not nothing.
533
+ emptyRepetitionInNullablePrefix: bothBackends(
534
+ [repeat0Plus(range('AA')), range('09')], '5',
535
+ '(() ("5"))'),
536
+ },
381
537
  throw: {
382
538
  ambiguousVariantDispatch: () => {
383
539
  // Two alternatives covering the same code point — dispatch merge throws.
384
540
  const conflictRule = { 'a': range('AA'), 'b': range('AA') }
385
541
  dispatchMap(toData(conflictRule)[0])
386
- }
542
+ },
543
+ // Left recursion is not LL(1): no lookahead can decide it, and a match
544
+ // would loop at the same position forever, so building throws instead.
545
+ leftRecursiveSequence: () => {
546
+ // `s` reaches itself through its nullable first item.
547
+ /** @type {RuleSet} */
548
+ const ruleSet = {
549
+ s: ['o', 's'],
550
+ o: { some: 'm', none: 'e' },
551
+ m: range('--'),
552
+ e: [],
553
+ }
554
+ dispatchMap(ruleSet)
555
+ },
556
+ leftRecursiveVariant: () => {
557
+ /** @type {RuleSet} */
558
+ const ruleSet = { v: { x: 'v', a: 'a' }, a: range('AA') }
559
+ dispatchMap(ruleSet)
560
+ },
387
561
  },
388
562
  // Regression: `emptyTagMap` must stay correct for cyclic rule references.
389
563
  // A naive single-pass, memoized derivation that marks a rule nullable as