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
@@ -4,52 +4,49 @@
4
4
  * Built from the serializable IR in `fjs/bnf/data`, this is a sibling of the
5
5
  * LL(1) dispatch builder (`fjs/bnf/ll1`). It walks the grammar by recursive
6
6
  * descent and preserves per-code-point metadata, producing a metadata-aware
7
- * AST ({@link AstRuleMeta}). Nullability (which rule can match empty input) is
8
- * computed once by {@link emptyTagMap} in `fjs/bnf/data`.
7
+ * AST over leaves that carry it `Ast<CodePointMeta<T>>`, built with the
8
+ * shared layer in `fjs/bnf/matcher`. Nullability (which rule can match empty
9
+ * input) is computed once by {@link emptyTagMap} in `fjs/bnf/data`.
9
10
  *
10
11
  * The caller passes physical symbols only; the matcher synthesizes the one
11
- * logical EOF ({@link eofSymbol}) after them, so a grammar can require the end
12
- * of input with the `eof` terminal.
12
+ * logical EOF after them, so a grammar can require the end of input with the
13
+ * `eof` terminal.
13
14
  *
14
15
  * A failed result also carries a {@link DescentFailure}: the furthest position a
15
16
  * terminal was rejected at, which — unlike the result's own index — never
16
17
  * rewinds and is what diagnostics should be built from.
17
18
  *
19
+ * A `Repeat` rule is matched iteratively rather than by descending into itself
20
+ * once per item, so a repetition produces one AST node holding a flat sequence
21
+ * of the items it matched instead of the right-recursive chain its functional
22
+ * spelling would otherwise build.
23
+ *
18
24
  * See `./types.ts` for the type-level API.
19
25
  *
20
26
  * @module
21
27
  *
22
28
  * @import { TerminalRange } from '../types.ts'
23
- * @import { Rule as DataRule, Sequence } from '../data/types.ts'
29
+ * @import { Rule as DataRule, RuleSet, Sequence } from '../data/types.ts'
24
30
  * @import { Rule as FRule } from '../types.ts'
25
- * @import { AstRuleMeta, AstTag, AstSequenceMeta, CodePointMeta, DescentFailure, DescentMatch, DescentMatchResult, DescentMatchRule } from './types.ts'
31
+ * @import { List } from '../../types/list/types.ts'
32
+ * @import { Ast, AstResult, AstSequence, AstTag, Cursor } from '../matcher/types.ts'
33
+ * @import { CodePointMeta, DescentFailure, DescentMatch, DescentMatchResult, DescentMatchRule } from './types.ts'
26
34
  */
27
35
 
28
- import { eofSymbol, rangeDecode } from '../module.f.mjs'
36
+ import { rangeDecode } from '../module.f.mjs'
29
37
  import { contains as rangeContains } from '../../types/range/module.f.mjs'
38
+ import { concat, toArray } from '../../types/list/module.f.mjs'
30
39
  import { definedEntries } from '../../types/object/module.f.mjs'
31
- import { emptyTagMap, toData } from '../data/module.f.mjs'
32
-
33
- /**
34
- * A match position that includes EOF consumption: `0 .. cp.length` are the
35
- * physical positions, and `cp.length + 1` is where the one synthesized EOF has
36
- * been consumed.
37
- *
38
- * This is the complete cursor the design calls `(idx, eofConsumed)`, written as
39
- * one number: `eofConsumed` can only be true at the physical end, so the pair
40
- * and the extended position hold the same information, and comparing cursors
41
- * compares progress — consuming EOF *is* progress even though the public index
42
- * does not move.
43
- *
44
- * @typedef {number} _Cursor
45
- */
40
+ import { emptyTagMap, isRepeat, toData } from '../data/module.f.mjs'
41
+ import { leafAt, mrFail, mrSuccess, physicalIdx, symbolAt } from '../matcher/module.f.mjs'
46
42
 
47
43
  /**
48
44
  * The furthest-failure record while matching, positioned by the complete
49
- * cursor. {@link DescentFailure} is its public, physically-positioned form.
45
+ * {@link Cursor}. {@link DescentFailure} is its public, physically-positioned
46
+ * form.
50
47
  *
51
48
  * @typedef {{
52
- * readonly pos: _Cursor
49
+ * readonly pos: Cursor
53
50
  * readonly expected: readonly TerminalRange[]
54
51
  * }} _Failure
55
52
  */
@@ -57,41 +54,22 @@ import { emptyTagMap, toData } from '../data/module.f.mjs'
57
54
  /**
58
55
  * The machine's own result: a {@link DescentMatchResult} positioned by the
59
56
  * complete cursor, and with no failure record — that one is tracked per match
60
- * rather than per frame.
57
+ * rather than per frame. This backend always has a position, so it needs no
58
+ * `null` case.
61
59
  *
62
60
  * @template T
63
- * @typedef {{
64
- * readonly ast: AstRuleMeta<T>
65
- * readonly success: boolean
66
- * readonly pos: _Cursor
67
- * }} _Result
68
- */
69
-
70
- /**
71
- * The public, physical index of a cursor. Consuming EOF moves the cursor past
72
- * the physical end, and both cursors report `input.length`.
73
- *
74
- * @type {(length: number) => (pos: _Cursor) => number}
61
+ * @typedef {AstResult<CodePointMeta<T>, Cursor>} _Result
75
62
  */
76
- const physicalIdx = length => pos => Math.min(pos, length)
77
63
 
78
64
  /**
79
- * The semantic symbol a cursor points at: a code point inside the physical
80
- * input, and the synthesized {@link eofSymbol} at its end. Only meaningful
81
- * where the cursor still has a symbol, `pos <= cp.length`.
65
+ * A leaf here is a code point with its metadata, so its symbol is the first
66
+ * half. This is the only thing {@link symbolAt} needs to know about a leaf.
82
67
  *
83
- * @type {<T>(cp: readonly CodePointMeta<T>[], pos: _Cursor) => number}
68
+ * @type {<T>(leaf: CodePointMeta<T>) => number}
84
69
  */
85
- const symbolAt = (cp, pos) => pos < cp.length ? cp[pos][0] : eofSymbol
70
+ const symbolOf = ([symbol]) => symbol
86
71
 
87
- /**
88
- * What consuming the symbol at a cursor contributes to the AST: the code point
89
- * with its metadata, and nothing for the synthesized EOF — it has no physical
90
- * source element.
91
- *
92
- * @type {<T>(cp: readonly CodePointMeta<T>[], pos: _Cursor) => AstSequenceMeta<T>}
93
- */
94
- const leafAt = (cp, pos) => pos < cp.length ? [cp[pos]] : []
72
+ const symbolAtCp = symbolAt(symbolOf)
95
73
 
96
74
  /**
97
75
  * Folds one rejected terminal into the furthest-failure record: further along
@@ -99,7 +77,7 @@ const leafAt = (cp, pos) => pos < cp.length ? [cp[pos]] : []
99
77
  * discarded. The comparison is on the complete cursor, so a failure after EOF
100
78
  * was consumed is further than one at the same physical index before it.
101
79
  *
102
- * @type {(failure: _Failure, pos: _Cursor, terminal: TerminalRange) => _Failure}
80
+ * @type {(failure: _Failure, pos: Cursor, terminal: TerminalRange) => _Failure}
103
81
  */
104
82
  const recordFailure = (failure, pos, terminal) => {
105
83
  if (pos > failure.pos) { return { pos, expected: [terminal] } }
@@ -108,21 +86,15 @@ const recordFailure = (failure, pos, terminal) => {
108
86
  }
109
87
 
110
88
  /**
111
- * Creates a recursive descent parser that preserves metadata for each consumed
112
- * code point.
89
+ * Creates a recursive descent parser from an already materialized
90
+ * {@link RuleSet}, preserving metadata for each consumed code point.
113
91
  *
114
92
  * @template T
115
- * @param {FRule} fr
93
+ * @param {RuleSet} ruleSet
116
94
  * @returns {DescentMatch<T>}
117
95
  */
118
- export const descentParser = fr => {
119
- const data = toData(fr)
120
- const emptyTags = emptyTagMap(data[0])
121
-
122
- /** @type {(tag: AstTag, sequence: AstSequenceMeta<T>, pos: _Cursor) => _Result<T>} */
123
- const mrSuccess = (tag, sequence, pos) => ({ ast: {tag, sequence}, success: true, pos })
124
- /** @type {(tag: AstTag, sequence: AstSequenceMeta<T>, pos: _Cursor) => _Result<T>} */
125
- const mrFail = (tag, sequence, pos) => ({ ast: {tag, sequence}, success: false, pos })
96
+ export const descentParserRuleSet = ruleSet => {
97
+ const emptyTags = emptyTagMap(ruleSet)
126
98
 
127
99
  // A suspended sequence match: items[itemIndex] is being matched by the current
128
100
  // task; `seq` holds the ASTs of the items already matched.
@@ -132,8 +104,8 @@ export const descentParser = fr => {
132
104
  * readonly tag: AstTag
133
105
  * readonly items: Sequence
134
106
  * readonly itemIndex: number
135
- * readonly startPos: _Cursor
136
- * readonly seq: AstSequenceMeta<T>
107
+ * readonly startPos: Cursor
108
+ * readonly seq: AstSequence<CodePointMeta<T>>
137
109
  * }} _SeqFrame
138
110
  */
139
111
 
@@ -145,12 +117,30 @@ export const descentParser = fr => {
145
117
  * readonly kind: 'variant'
146
118
  * readonly entries: readonly (readonly [string, string])[]
147
119
  * readonly entryIndex: number
148
- * readonly pos: _Cursor
120
+ * readonly pos: Cursor
149
121
  * readonly emptyResult: _Result<T>
150
122
  * }} _VariantFrame
151
123
  */
152
124
 
153
- /** @typedef {_SeqFrame | _VariantFrame} _Frame */
125
+ // A suspended repetition: `item` is being matched by the current task for the
126
+ // round that began at `roundStart`, and `items` holds the ASTs of the rounds
127
+ // that already completed. They accumulate as a list rather than an array
128
+ // because a repetition is as long as its input: appending to an array per
129
+ // round would copy the whole prefix each time and make one repetition
130
+ // quadratic in the number of items it matched.
131
+ /**
132
+ * @typedef {{
133
+ * readonly kind: 'repeat'
134
+ * readonly tag: AstTag
135
+ * readonly item: string
136
+ * readonly items: _Items
137
+ * readonly roundStart: Cursor
138
+ * }} _RepeatFrame
139
+ */
140
+
141
+ /** @typedef {List<Ast<CodePointMeta<T>>>} _Items */
142
+
143
+ /** @typedef {_SeqFrame | _VariantFrame | _RepeatFrame} _Frame */
154
144
 
155
145
  // Immutable cons-cell stack: O(1) push/pop, no array copying per step.
156
146
  /**
@@ -164,12 +154,28 @@ export const descentParser = fr => {
164
154
  // tuple), or null when a result is ready to resume the innermost frame.
165
155
  /**
166
156
  * @typedef {{
157
+ * readonly kind: 'rule'
167
158
  * readonly name: string
168
159
  * readonly tag: AstTag
169
- * readonly pos: _Cursor
170
- * }} _Task
160
+ * readonly pos: Cursor
161
+ * }} _RuleTask
171
162
  */
172
163
 
164
+ // The next round of a repetition, about to be started. Both the rule that
165
+ // introduces a repetition and the frame that finishes one of its rounds go
166
+ // through this, so a round is set up in exactly one place.
167
+ /**
168
+ * @typedef {{
169
+ * readonly kind: 'repeat'
170
+ * readonly tag: AstTag
171
+ * readonly item: string
172
+ * readonly items: _Items
173
+ * readonly pos: Cursor
174
+ * }} _RepeatTask
175
+ */
176
+
177
+ /** @typedef {_RuleTask | _RepeatTask} _Task */
178
+
173
179
  // The recursive-descent matcher as an explicit-stack machine: each iteration either
174
180
  // starts the current task (pushing a frame for a sequence/variant and descending into
175
181
  // its first child) or feeds the pending result into the innermost frame. Semantics are
@@ -183,7 +189,7 @@ export const descentParser = fr => {
183
189
  /** @type {_Stack} */
184
190
  let stack = null
185
191
  /** @type {_Task | null} */
186
- let task = { name, tag, pos: startPos }
192
+ let task = { kind: 'rule', name, tag, pos: startPos }
187
193
  /** @type {_Result<T>} */
188
194
  let result = mrFail(undefined, [], startPos)
189
195
  // High-water mark across the whole match, so it survives the rewinds a
@@ -196,16 +202,25 @@ export const descentParser = fr => {
196
202
  // The explicit annotation cuts a control-flow inference cycle (TS7022):
197
203
  // `name`'s narrowed type feeds `rule`, whose type would otherwise feed the
198
204
  // later `task` assignments that `name`'s narrowing depends on.
199
- const { name, tag, pos } = /** @type {_Task} */ (task)
205
+ const current = /** @type {_Task} */ (task)
200
206
  task = null
207
+ if (current.kind === 'repeat') {
208
+ // One round of a repetition is one match of its item, so the
209
+ // frame below carries the rounds already collected and the
210
+ // position this round may have to rewind to.
211
+ stack = { top: { kind: 'repeat', tag: current.tag, item: current.item, items: current.items, roundStart: current.pos }, rest: stack }
212
+ task = { kind: 'rule', name: current.item, tag: undefined, pos: current.pos }
213
+ continue
214
+ }
215
+ const { name, tag, pos } = current
201
216
  /** @type {DataRule} */
202
- const rule = data[0][name]
217
+ const rule = ruleSet[name]
203
218
  if (typeof rule === 'number') {
204
219
  // No nullable case: `emptyTagOf` in `bnf/data` returns `undefined`
205
220
  // for every terminal, so `emptyTags[name]` here is always
206
221
  // `undefined` and a terminal either consumes one symbol or fails.
207
222
  // Past the synthesized EOF there is no symbol left to consume.
208
- if (pos <= cp.length && rangeContains(...rangeDecode(rule))(symbolAt(cp, pos))) {
223
+ if (pos <= cp.length && rangeContains(...rangeDecode(rule))(symbolAtCp(cp, pos))) {
209
224
  result = mrSuccess(tag, leafAt(cp, pos), pos + 1)
210
225
  } else {
211
226
  // The only place a terminal is rejected, so the only place
@@ -218,8 +233,10 @@ export const descentParser = fr => {
218
233
  result = mrSuccess(tag, [], pos)
219
234
  } else {
220
235
  stack = { top: { kind: 'seq', tag, items: rule, itemIndex: 0, startPos: pos, seq: [] }, rest: stack }
221
- task = { name: rule[0], tag: undefined, pos }
236
+ task = { kind: 'rule', name: rule[0], tag: undefined, pos }
222
237
  }
238
+ } else if (isRepeat(rule)) {
239
+ task = { kind: 'repeat', tag, item: rule, items: null, pos }
223
240
  } else {
224
241
  const entries = definedEntries(rule)
225
242
  const emptyTag = emptyTags[name]
@@ -229,7 +246,7 @@ export const descentParser = fr => {
229
246
  } else {
230
247
  stack = { top: { kind: 'variant', entries, entryIndex: 0, pos, emptyResult }, rest: stack }
231
248
  const [entryTag, entryName] = entries[0]
232
- task = { name: entryName, tag: entryTag, pos }
249
+ task = { kind: 'rule', name: entryName, tag: entryTag, pos }
233
250
  }
234
251
  }
235
252
  continue
@@ -256,11 +273,29 @@ export const descentParser = fr => {
256
273
  const itemIndex = frame.itemIndex + 1
257
274
  if (itemIndex < frame.items.length) {
258
275
  stack = { top: { ...frame, itemIndex, seq }, rest: stack }
259
- task = { name: frame.items[itemIndex], tag: undefined, pos }
276
+ task = { kind: 'rule', name: frame.items[itemIndex], tag: undefined, pos }
260
277
  } else {
261
278
  result = mrSuccess(frame.tag, seq, pos)
262
279
  }
263
280
  }
281
+ } else if (frame.kind === 'repeat') {
282
+ if (result.success === false) {
283
+ // A round that fails ends the repetition instead of failing
284
+ // it: the rounds before it stand, and the whole match rewinds
285
+ // to where the failed round began.
286
+ result = mrSuccess(frame.tag, toArray(frame.items), frame.roundStart)
287
+ } else {
288
+ const items = concat(frame.items)([result.ast])
289
+ if (result.pos === frame.roundStart) {
290
+ // A round that consumed nothing would repeat forever. Only
291
+ // a hand-written `repeat` over a nullable rule reaches
292
+ // this — `toData` never derives one — so keep the one
293
+ // empty round and stop.
294
+ result = mrSuccess(frame.tag, toArray(items), result.pos)
295
+ } else {
296
+ task = { kind: 'repeat', tag: frame.tag, item: frame.item, items, pos: result.pos }
297
+ }
298
+ }
264
299
  } else {
265
300
  // success that consumed input wins immediately: the frame stays popped and
266
301
  // `result` propagates to the frame below, matching the recursive `return m`.
@@ -271,7 +306,7 @@ export const descentParser = fr => {
271
306
  if (entryIndex < frame.entries.length) {
272
307
  stack = { top: { ...frame, entryIndex, emptyResult }, rest: stack }
273
308
  const [entryTag, entryName] = frame.entries[entryIndex]
274
- task = { name: entryName, tag: entryTag, pos: frame.pos }
309
+ task = { kind: 'rule', name: entryName, tag: entryTag, pos: frame.pos }
275
310
  } else {
276
311
  result = emptyResult
277
312
  }
@@ -287,3 +322,13 @@ export const descentParser = fr => {
287
322
 
288
323
  return match
289
324
  }
325
+
326
+ /**
327
+ * Creates a recursive descent parser that preserves metadata for each consumed
328
+ * code point.
329
+ *
330
+ * @template T
331
+ * @param {FRule} fr
332
+ * @returns {DescentMatch<T>}
333
+ */
334
+ export const descentParser = fr => descentParserRuleSet(toData(fr)[0])
@@ -2,6 +2,7 @@
2
2
  * @module
3
3
  *
4
4
  * @import { CodePoint } from '../../text/utf16/types.ts'
5
+ * @import { RuleSet } from '../data/types.ts'
5
6
  * @import { DescentMatch, CodePointMeta, DescentMatchResult } from './types.ts'
6
7
  */
7
8
  export declare const proof: {
@@ -10,5 +11,6 @@ export declare const proof: {
10
11
  longInput: (() => void)[];
11
12
  descentParserWithMeta: (() => void)[];
12
13
  logicalEof: (() => void)[];
14
+ repeat: (() => void)[];
13
15
  furthestFailure: (() => void)[];
14
16
  };
@@ -2,6 +2,7 @@
2
2
  * @module
3
3
  *
4
4
  * @import { CodePoint } from '../../text/utf16/types.ts'
5
+ * @import { RuleSet } from '../data/types.ts'
5
6
  * @import { DescentMatch, CodePointMeta, DescentMatchResult } from './types.ts'
6
7
  */
7
8
 
@@ -9,9 +10,9 @@ import { stringToCodePointList } from '../../text/utf16/module.f.mjs'
9
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 { emptyTagMap, toData } from '../data/module.f.mjs'
12
- import { descentParser } from './module.f.mjs'
13
+ import { descentParser, descentParserRuleSet } from './module.f.mjs'
13
14
  import { assertEq, assertNotNullish } from '../../asserts/module.f.mjs'
14
- import { deterministic } from '../testlib.f.mjs'
15
+ import { deterministic, showAst } from '../testlib.f.mjs'
15
16
 
16
17
  /** @type {(cp: CodePoint) => CodePointMeta<unknown>} */
17
18
  const mapCodePoint = cp => [cp, undefined]
@@ -77,11 +78,14 @@ export const proof = {
77
78
  if (result !== '{"0":"none","3":true,"":true,"r":"none"}') { throw result }
78
79
  },
79
80
  () => {
81
+ // The whole repetition is one `repeat` rule, and a repetition always
82
+ // matches empty — with no tag, because it is a list of items rather
83
+ // than a choice between branches.
80
84
  const repeatRule = repeat0Plus(set(' \n\r\t'))
81
85
  const data = toData(repeatRule)
82
86
  const emptyTags = emptyTagMap(data[0])
83
87
  const result = JSON.stringify(emptyTags)
84
- if (result !== '{"5":true,"r":"none"}') { throw result }
88
+ assertEq(result, '{"r":true}')
85
89
  }
86
90
  ],
87
91
  descentParser: [
@@ -254,33 +258,62 @@ export const proof = {
254
258
  () => {
255
259
  const m = descentParser(deterministic())
256
260
 
257
- const expect = (/** @type {string} */s, /** @type {boolean} */expected) => {
261
+ // A match is pinned by the AST it built, not just by the fact that it
262
+ // matched: the shape is what a repetition changes, and asserting it
263
+ // subsumes success — only a full match produces one. `showAst` writes
264
+ // a node as `tag(children)` with the code points it consumed as one
265
+ // quoted string, `*` for the tagless empty match.
266
+ //
267
+ // The leading three spaces are the `repeat` to read first: they are
268
+ // three siblings of one node here, and three levels of nesting in the
269
+ // LL(1) backend's copy of these expectations.
270
+ /** @type {(s: string, expected: string) => void} */
271
+ const expectAst = (s, expected) => {
258
272
  const cp = toArray(stringToCodePointList(s))
259
273
  const mr = descentParserCpOnly(m, '', cp)
260
- const success = mr.success && mr.idx === cp.length
261
- assertEq(success, expected, mr)
274
+ assertEq(mr.success, true, s)
275
+ assertEq(mr.idx, cp.length, s)
276
+ assertEq(showAst(mr.ast), expected, s)
277
+ }
278
+
279
+ /** @type {(s: string) => void} */
280
+ const expectNoMatch = s => {
281
+ const cp = toArray(stringToCodePointList(s))
282
+ const mr = descentParserCpOnly(m, '', cp)
283
+ assertEq(mr.success && mr.idx === cp.length, false, s)
262
284
  }
263
285
 
264
- expect(' true ', true)
265
- expect(' tr2ue ', false)
266
- expect(' true" ', false)
267
- expect(' "Hello" ', true)
268
- expect(' "Hello ', false)
269
- expect(' "Hello\\n\\r\\"" ', true)
270
- expect(' -56.7e+5 ', true)
271
- expect(' h-56.7e+5 ', false)
272
- expect(' -56.7e+5 3', false)
273
- expect(' [] ', true)
274
- expect(' {} ', true)
275
- expect(' [[[]]] ', true)
276
- expect(' [1] ', true)
277
- expect(' [ 12, false, "a"] ', true)
278
- expect(' [ 12, false2, "a"] ', false)
279
- expect(' { "q": [ 12, false, [{"b" : "c"}], "a"] } ', true)
280
- expect(' { "q": [ 12, false, [{}], "a"] } ', true)
281
- expect(' { "q": [ 12, false, [}], "a"] } ', false)
282
- expect(' [{ "q": [ 12, false, [{}], "a"] }] ', true)
283
- expect(' [{ "q": [ 12, false, [}], "a"] }] ', false)
286
+ expectAst(' true ', '((" "(" ") " "(" ") " "(" ")) "true"(("t") ("r") ("u") ("e")) (" "(" ") " "(" ") " "(" ")))')
287
+ expectAst(' "Hello" ', '((" "(" ") " "(" ") " "(" ")) "string"((("\\"")) ("0x2300005b"("H") "0x5d10ffff"("e") "0x5d10ffff"("l") "0x5d10ffff"("l") "0x5d10ffff"("o")) (("\\""))) (" "(" ") " "(" ") " "(" ")))')
288
+ expectAst(' "Hello\\n\\r\\"" ', '((" "(" ") " "(" ") " "(" ")) "string"((("\\"")) ("0x2300005b"("H") "0x5d10ffff"("e") "0x5d10ffff"("l") "0x5d10ffff"("l") "0x5d10ffff"("o") "escape"((("\\\\")) "n"("n")) "escape"((("\\\\")) "r"("r")) "escape"((("\\\\")) "\\""("\\""))) (("\\""))) (" "(" ") " "(" ") " "(" ")))')
289
+ expectAst(' -56.7e+5 ', '((" "(" ") " "(" ") " "(" ")) "number"("some"(("-")) "onenine"(("5") (("6"))) "some"(((".")) (("7") ())) "some"("e"("e") "+"("+") (("5") ()))) (" "(" ") " "(" ")))')
290
+ expectAst(' [] ', '((" "(" ") " "(" ") " "(" ")) "array"((("[")) () "none"() (("]"))) (" "(" ")))')
291
+ expectAst(' {} ', '((" "(" ") " "(" ") " "(" ")) "object"((("{")) () "none"() (("}"))) (" "(" ")))')
292
+ expectAst(' [[[]]] ', '((" "(" ") " "(" ") " "(" ")) "array"((("[")) () "some"(("array"((("[")) () "some"(("array"((("[")) () "none"() (("]"))) ()) "none"()) (("]"))) ()) "none"()) (("]"))) (" "(" ")))')
293
+ expectAst(' [1] ', '((" "(" ") " "(" ") " "(" ")) "array"((("[")) () "some"(("number"("none"() "onenine"(("1") ()) "none"() "none"()) ()) "none"()) (("]"))) (" "(" ")))')
294
+ expectAst(' [ 12, false, "a"] ', '((" "(" ") " "(" ") " "(" ")) "array"((("[")) (" "(" ")) "some"(("number"("none"() "onenine"(("1") (("2"))) "none"() "none"()) ()) "some"(((((",")) (" "(" "))) ("false"(("f") ("a") ("l") ("s") ("e")) ())) "some"(((((",")) (" "(" "))) ("string"((("\\"")) ("0x5d10ffff"("a")) (("\\""))) ())) "none"()))) (("]"))) (" "(" ") " "(" ")))')
295
+ expectAst(' { "q": [ 12, false, [{"b" : "c"}], "a"] } ', '((" "(" ") " "(" ") " "(" ")) "object"((("{")) (" "(" ")) "some"(((((("\\"")) ("0x5d10ffff"("q")) (("\\""))) () ((":")) (" "(" ")) "array"((("[")) (" "(" ")) "some"(("number"("none"() "onenine"(("1") (("2"))) "none"() "none"()) ()) "some"(((((",")) (" "(" "))) ("false"(("f") ("a") ("l") ("s") ("e")) ())) "some"(((((",")) (" "(" "))) ("array"((("[")) () "some"(("object"((("{")) () "some"(((((("\\"")) ("0x5d10ffff"("b")) (("\\""))) (" "(" ")) ((":")) (" "(" ")) "string"((("\\"")) ("0x5d10ffff"("c")) (("\\"")))) ()) "none"()) (("}"))) ()) "none"()) (("]"))) ())) "some"(((((",")) (" "(" "))) ("string"((("\\"")) ("0x5d10ffff"("a")) (("\\""))) ())) "none"())))) (("]")))) (" "(" "))) "none"()) (("}"))) (" "(" ") " "(" ")))')
296
+ expectAst(' { "q": [ 12, false, [{}], "a"] } ', '((" "(" ") " "(" ") " "(" ")) "object"((("{")) (" "(" ")) "some"(((((("\\"")) ("0x5d10ffff"("q")) (("\\""))) () ((":")) (" "(" ")) "array"((("[")) (" "(" ")) "some"(("number"("none"() "onenine"(("1") (("2"))) "none"() "none"()) ()) "some"(((((",")) (" "(" "))) ("false"(("f") ("a") ("l") ("s") ("e")) ())) "some"(((((",")) (" "(" "))) ("array"((("[")) () "some"(("object"((("{")) () "none"() (("}"))) ()) "none"()) (("]"))) ())) "some"(((((",")) (" "(" "))) ("string"((("\\"")) ("0x5d10ffff"("a")) (("\\""))) ())) "none"())))) (("]")))) (" "(" "))) "none"()) (("}"))) (" "(" ") " "(" ")))')
297
+ expectAst(' [{ "q": [ 12, false, [{}], "a"] }] ', '((" "(" ") " "(" ") " "(" ")) "array"((("[")) () "some"(("object"((("{")) (" "(" ")) "some"(((((("\\"")) ("0x5d10ffff"("q")) (("\\""))) () ((":")) (" "(" ")) "array"((("[")) (" "(" ")) "some"(("number"("none"() "onenine"(("1") (("2"))) "none"() "none"()) ()) "some"(((((",")) (" "(" "))) ("false"(("f") ("a") ("l") ("s") ("e")) ())) "some"(((((",")) (" "(" "))) ("array"((("[")) () "some"(("object"((("{")) () "none"() (("}"))) ()) "none"()) (("]"))) ())) "some"(((((",")) (" "(" "))) ("string"((("\\"")) ("0x5d10ffff"("a")) (("\\""))) ())) "none"())))) (("]")))) (" "(" "))) "none"()) (("}"))) ()) "none"()) (("]"))) (" "(" ") " "(" ")))')
298
+
299
+ expectNoMatch(' tr2ue ')
300
+ expectNoMatch(' true" ')
301
+ expectNoMatch(' "Hello ')
302
+ expectNoMatch(' h-56.7e+5 ')
303
+ expectNoMatch(' -56.7e+5 3')
304
+ expectNoMatch(' [ 12, false2, "a"] ')
305
+ expectNoMatch(' { "q": [ 12, false, [}], "a"] } ')
306
+
307
+ // The invalid input in detail. A failed match's own index rewound to
308
+ // the start and locates nothing; the furthest failure is the
309
+ // high-water mark, and it lands on the `}` that closes nothing.
310
+ const bad = ' [{ "q": [ 12, false, [}], "a"] }] '
311
+ const badMr = descentParserCpOnly(m, '', toArray(stringToCodePointList(bad)))
312
+ assertEq(badMr.success, false, bad)
313
+ assertEq(badMr.idx, 0, bad)
314
+ const failure = assertNotNullish(badMr.failure)
315
+ assertEq(failure.idx, 25, bad)
316
+ assertEq(bad[failure.idx], '}', bad)
284
317
  }
285
318
  ],
286
319
  // Regression for the stack-recursive matcher bug: the matcher used to recurse
@@ -374,11 +407,12 @@ export const proof = {
374
407
  },
375
408
  () => {
376
409
  // Repetition terminates on EOF: consuming it moves the complete
377
- // cursor, so the repeat makes exactly one round and then takes its
378
- // empty branch — with `idx` alone this would never stop.
379
- const m = descentParser(repeat0Plus(eof))
380
- const mr = m('', [])
381
- assertEq(JSON.stringify(mr), '{"ast":{"sequence":[{"sequence":[]},{"tag":"none","sequence":[]}]},"success":true,"idx":0}')
410
+ // cursor, so the repeat makes exactly one round and then stops
411
+ // with `idx` alone this would never stop.
412
+ const rule = repeat0Plus(eof)
413
+ const m = descentParser(rule)
414
+ const mr = m(toData(rule)[1], [])
415
+ assertEq(JSON.stringify(mr), '{"ast":{"sequence":[{"sequence":[]}]},"success":true,"idx":0}')
382
416
  },
383
417
  () => {
384
418
  // Backtracking restores the complete cursor: `x` consumes EOF and
@@ -399,6 +433,58 @@ export const proof = {
399
433
  assertEq(f.expected[0], range('BB'))
400
434
  },
401
435
  ],
436
+ repeat: [
437
+ () => {
438
+ // The whole repetition is one node holding a flat sequence of the
439
+ // items it matched — not the right-recursive chain of `some`/`none`
440
+ // nodes its functional spelling builds.
441
+ const rule = repeat0Plus(set(' \n\r\t'))
442
+ const m = descentParser(rule)
443
+ const name = toData(rule)[1]
444
+ const mr = descentParserCpOnly(m, name, toArray(stringToCodePointList(' ')))
445
+ assertEq(
446
+ JSON.stringify(mr),
447
+ `{"ast":{"sequence":[{"tag":" ","sequence":[[${cp1(' ')},null]]},{"tag":" ","sequence":[[${cp1(' ')},null]]}]},"success":true,"idx":2}`)
448
+ },
449
+ () => {
450
+ // Zero items is a match, and the node is empty rather than tagged.
451
+ const rule = repeat0Plus(set(' \n\r\t'))
452
+ const m = descentParser(rule)
453
+ const mr = m(toData(rule)[1], [])
454
+ assertEq(JSON.stringify(mr), '{"ast":{"sequence":[]},"success":true,"idx":0}')
455
+ },
456
+ () => {
457
+ // A round that fails ends the repetition rather than failing it: the
458
+ // rounds before it stand and the match stops where the failed one
459
+ // began.
460
+ const rule = repeat0Plus(set(' \n\r\t'))
461
+ const m = descentParser(rule)
462
+ const name = toData(rule)[1]
463
+ const mr = descentParserCpOnly(m, name, toArray(stringToCodePointList(' x')))
464
+ assertEq(
465
+ JSON.stringify(mr),
466
+ `{"ast":{"sequence":[{"tag":" ","sequence":[[${cp1(' ')},null]]}]},"success":true,"idx":1}`)
467
+ },
468
+ () => {
469
+ // `toData` never folds a nullable item into a `repeat`, but a
470
+ // hand-written rule set can hold one. A round that consumes nothing
471
+ // would repeat forever, so it is kept once and ends the repetition.
472
+ /** @type {RuleSet} */
473
+ const ruleSet = {
474
+ repeated: 'optionalA',
475
+ optionalA: { some: 'a', none: 'e' },
476
+ a: range('aa'),
477
+ e: [],
478
+ }
479
+ const m = descentParserRuleSet(ruleSet)
480
+ assertEq(
481
+ JSON.stringify(m('repeated', [])),
482
+ '{"ast":{"sequence":[{"tag":"none","sequence":[]}]},"success":true,"idx":0}')
483
+ assertEq(
484
+ JSON.stringify(m('repeated', [[cp1('a'), null]])),
485
+ `{"ast":{"sequence":[{"tag":"some","sequence":[[${cp1('a')},null]]},{"tag":"none","sequence":[]}]},"success":true,"idx":1}`)
486
+ },
487
+ ],
402
488
  furthestFailure: [
403
489
  () => {
404
490
  // A branch rejected *before* the high-water mark must not pull it back:
@@ -1,11 +1,16 @@
1
1
  /**
2
2
  * Types for the recursive descent matcher backend.
3
3
  *
4
+ * The AST it builds is the shared one, over leaves that carry metadata:
5
+ * `Ast<CodePointMeta<T>>` from [`../matcher`](../matcher). What is declared
6
+ * here is what belongs to *this* backend — the metadata-carrying leaf, the
7
+ * diagnostics a backtracking matcher can report, and its public result.
8
+ *
4
9
  * @module
5
10
  */
6
11
  import type { CodePoint } from '../../text/utf16/types.ts';
12
+ import type { Ast, AstTag } from '../matcher/types.ts';
7
13
  import type { TerminalRange } from '../types.ts';
8
- export type AstTag = string | true | undefined;
9
14
  /**
10
15
  * Recursive descent matcher for a single named rule, starting from `startPos` —
11
16
  * a position that counts the synthesized end-of-input symbol, unlike the public
@@ -53,7 +58,7 @@ export type DescentFailure = {
53
58
  * stopped just before it.
54
59
  */
55
60
  export type DescentMatchResult<T> = {
56
- readonly ast: AstRuleMeta<T>;
61
+ readonly ast: Ast<CodePointMeta<T>>;
57
62
  readonly success: boolean;
58
63
  readonly idx: number;
59
64
  readonly failure?: DescentFailure;
@@ -63,17 +68,10 @@ export type DescentMatchResult<T> = {
63
68
  */
64
69
  export type DescentMatch<T> = (name: string, s: readonly CodePointMeta<T>[]) => DescentMatchResult<T>;
65
70
  /**
66
- * Code point value paired with metadata.
71
+ * Code point value paired with metadata: this backend's AST leaf.
72
+ *
73
+ * Preserving metadata per consumed code point is what distinguishes this
74
+ * backend, so the leaf type stays here rather than moving to the shared layer,
75
+ * which is parameterized by it.
67
76
  */
68
77
  export type CodePointMeta<T> = readonly [CodePoint, T];
69
- /**
70
- * AST sequence for the metadata-aware parser.
71
- */
72
- export type AstSequenceMeta<T> = readonly (AstRuleMeta<T> | CodePointMeta<T>)[];
73
- /**
74
- * Metadata-aware AST node.
75
- */
76
- export type AstRuleMeta<T> = {
77
- readonly tag: AstTag;
78
- readonly sequence: AstSequenceMeta<T>;
79
- };