roll-parser 3.0.0-beta.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/CHANGELOG.md +107 -9
  2. package/MIGRATION.md +147 -0
  3. package/README.md +960 -154
  4. package/dist/cli/args.d.ts +1 -0
  5. package/dist/cli/args.d.ts.map +1 -1
  6. package/dist/cli/args.js +81 -0
  7. package/dist/cli/args.js.map +1 -0
  8. package/dist/cli/format.d.ts +15 -3
  9. package/dist/cli/format.d.ts.map +1 -1
  10. package/dist/cli/format.js +17 -0
  11. package/dist/cli/format.js.map +1 -0
  12. package/dist/cli/index.d.ts +3 -0
  13. package/dist/cli/index.d.ts.map +1 -1
  14. package/dist/cli/index.js +14 -0
  15. package/dist/cli/index.js.map +1 -0
  16. package/dist/cli/main.d.ts +36 -0
  17. package/dist/cli/main.d.ts.map +1 -0
  18. package/dist/cli/main.js +83 -0
  19. package/dist/cli/main.js.map +1 -0
  20. package/dist/errors.d.ts +332 -17
  21. package/dist/errors.d.ts.map +1 -1
  22. package/dist/errors.js +141 -0
  23. package/dist/errors.js.map +1 -0
  24. package/dist/evaluator/die.d.ts +26 -0
  25. package/dist/evaluator/die.d.ts.map +1 -0
  26. package/dist/evaluator/die.js +19 -0
  27. package/dist/evaluator/die.js.map +1 -0
  28. package/dist/evaluator/env.d.ts +58 -0
  29. package/dist/evaluator/env.d.ts.map +1 -0
  30. package/dist/evaluator/env.js +11 -0
  31. package/dist/evaluator/env.js.map +1 -0
  32. package/dist/evaluator/evaluator.d.ts +41 -75
  33. package/dist/evaluator/evaluator.d.ts.map +1 -1
  34. package/dist/evaluator/evaluator.js +906 -0
  35. package/dist/evaluator/evaluator.js.map +1 -0
  36. package/dist/evaluator/modifiers/compare.js +15 -0
  37. package/dist/evaluator/modifiers/compare.js.map +1 -0
  38. package/dist/evaluator/modifiers/crit-threshold.d.ts +0 -1
  39. package/dist/evaluator/modifiers/crit-threshold.d.ts.map +1 -1
  40. package/dist/evaluator/modifiers/crit-threshold.js +23 -0
  41. package/dist/evaluator/modifiers/crit-threshold.js.map +1 -0
  42. package/dist/evaluator/modifiers/die-bound.d.ts +26 -0
  43. package/dist/evaluator/modifiers/die-bound.d.ts.map +1 -0
  44. package/dist/evaluator/modifiers/die-bound.js +14 -0
  45. package/dist/evaluator/modifiers/die-bound.js.map +1 -0
  46. package/dist/evaluator/modifiers/explode.d.ts +13 -5
  47. package/dist/evaluator/modifiers/explode.d.ts.map +1 -1
  48. package/dist/evaluator/modifiers/explode.js +103 -0
  49. package/dist/evaluator/modifiers/explode.js.map +1 -0
  50. package/dist/evaluator/modifiers/flags.d.ts +37 -0
  51. package/dist/evaluator/modifiers/flags.d.ts.map +1 -0
  52. package/dist/evaluator/modifiers/flags.js +18 -0
  53. package/dist/evaluator/modifiers/flags.js.map +1 -0
  54. package/dist/evaluator/modifiers/keep-drop.d.ts +12 -28
  55. package/dist/evaluator/modifiers/keep-drop.d.ts.map +1 -1
  56. package/dist/evaluator/modifiers/keep-drop.js +82 -0
  57. package/dist/evaluator/modifiers/keep-drop.js.map +1 -0
  58. package/dist/evaluator/modifiers/reroll.d.ts +12 -4
  59. package/dist/evaluator/modifiers/reroll.d.ts.map +1 -1
  60. package/dist/evaluator/modifiers/reroll.js +62 -0
  61. package/dist/evaluator/modifiers/reroll.js.map +1 -0
  62. package/dist/evaluator/modifiers/sort.d.ts +4 -0
  63. package/dist/evaluator/modifiers/sort.d.ts.map +1 -1
  64. package/dist/evaluator/modifiers/sort.js +13 -0
  65. package/dist/evaluator/modifiers/sort.js.map +1 -0
  66. package/dist/evaluator/modifiers/success-count.d.ts +2 -6
  67. package/dist/evaluator/modifiers/success-count.d.ts.map +1 -1
  68. package/dist/evaluator/modifiers/success-count.js +24 -0
  69. package/dist/evaluator/modifiers/success-count.js.map +1 -0
  70. package/dist/index.d.ts +33 -11
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +12 -2560
  73. package/dist/index.js.map +1 -26
  74. package/dist/lexer/lexer.d.ts +50 -5
  75. package/dist/lexer/lexer.d.ts.map +1 -1
  76. package/dist/lexer/lexer.js +260 -0
  77. package/dist/lexer/lexer.js.map +1 -0
  78. package/dist/lexer/tokens.d.ts +31 -6
  79. package/dist/lexer/tokens.d.ts.map +1 -1
  80. package/dist/lexer/tokens.js +42 -0
  81. package/dist/lexer/tokens.js.map +1 -0
  82. package/dist/parser/ast.d.ts +314 -160
  83. package/dist/parser/ast.d.ts.map +1 -1
  84. package/dist/parser/ast.js +52 -0
  85. package/dist/parser/ast.js.map +1 -0
  86. package/dist/parser/guards.d.ts +106 -0
  87. package/dist/parser/guards.d.ts.map +1 -0
  88. package/dist/parser/guards.js +121 -0
  89. package/dist/parser/guards.js.map +1 -0
  90. package/dist/parser/parser.d.ts +118 -14
  91. package/dist/parser/parser.d.ts.map +1 -1
  92. package/dist/parser/parser.js +751 -0
  93. package/dist/parser/parser.js.map +1 -0
  94. package/dist/rng/mock.d.ts +73 -12
  95. package/dist/rng/mock.d.ts.map +1 -1
  96. package/dist/rng/mock.js +30 -0
  97. package/dist/rng/mock.js.map +1 -0
  98. package/dist/rng/seeded.d.ts +141 -9
  99. package/dist/rng/seeded.d.ts.map +1 -1
  100. package/dist/rng/seeded.js +138 -0
  101. package/dist/rng/seeded.js.map +1 -0
  102. package/dist/rng/types.d.ts +57 -0
  103. package/dist/rng/types.d.ts.map +1 -1
  104. package/dist/rng/types.js +2 -0
  105. package/dist/rng/types.js.map +1 -0
  106. package/dist/roll.d.ts +58 -28
  107. package/dist/roll.d.ts.map +1 -1
  108. package/dist/roll.js +8 -0
  109. package/dist/roll.js.map +1 -0
  110. package/dist/testing.d.ts +5 -4
  111. package/dist/testing.d.ts.map +1 -1
  112. package/dist/testing.js +2 -41
  113. package/dist/testing.js.map +1 -11
  114. package/dist/types.d.ts +306 -44
  115. package/dist/types.d.ts.map +1 -1
  116. package/dist/types.js +8 -0
  117. package/dist/types.js.map +1 -0
  118. package/dist/version.d.ts +2 -0
  119. package/dist/version.d.ts.map +1 -0
  120. package/dist/version.js +2 -0
  121. package/dist/version.js.map +1 -0
  122. package/package.json +83 -34
  123. package/src/cli/args.ts +66 -10
  124. package/src/cli/format.ts +22 -3
  125. package/src/cli/index.ts +27 -84
  126. package/src/cli/main.ts +129 -0
  127. package/src/errors.ts +480 -27
  128. package/src/evaluator/die.ts +50 -0
  129. package/src/evaluator/env.ts +73 -0
  130. package/src/evaluator/evaluator.ts +653 -421
  131. package/src/evaluator/modifiers/crit-threshold.ts +3 -6
  132. package/src/evaluator/modifiers/die-bound.ts +39 -0
  133. package/src/evaluator/modifiers/explode.ts +60 -58
  134. package/src/evaluator/modifiers/flags.ts +61 -0
  135. package/src/evaluator/modifiers/keep-drop.ts +124 -126
  136. package/src/evaluator/modifiers/reroll.ts +28 -49
  137. package/src/evaluator/modifiers/sort.ts +14 -1
  138. package/src/evaluator/modifiers/success-count.ts +5 -8
  139. package/src/index.ts +56 -35
  140. package/src/lexer/lexer.ts +107 -34
  141. package/src/lexer/tokens.ts +31 -6
  142. package/src/parser/ast.ts +323 -341
  143. package/src/parser/guards.ts +248 -0
  144. package/src/parser/parser.ts +419 -242
  145. package/src/rng/mock.ts +74 -13
  146. package/src/rng/seeded.ts +299 -64
  147. package/src/rng/types.ts +57 -0
  148. package/src/roll.ts +64 -47
  149. package/src/testing.ts +5 -9
  150. package/src/types.ts +310 -43
  151. package/src/version.ts +2 -0
  152. package/dist/cli.js +0 -2608
  153. package/dist/cli.js.map +0 -28
  154. package/dist/evaluator/index.d.ts +0 -8
  155. package/dist/evaluator/index.d.ts.map +0 -1
  156. package/dist/rng/index.d.ts +0 -8
  157. package/dist/rng/index.d.ts.map +0 -1
  158. package/src/evaluator/index.ts +0 -14
  159. package/src/rng/index.ts +0 -8
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Parser-internal AST guards.
3
+ *
4
+ * These helpers answer structural questions the parser's reject rules ask
5
+ * ("does this operand resolve to a dice pool?", "is a Versus buried in
6
+ * here?"). They are implementation detail of `parser.ts` — the public
7
+ * `isXxx` type guards stay in `ast.ts`.
8
+ *
9
+ * @module parser/guards
10
+ */
11
+
12
+ import type { ASTNode } from './ast.js';
13
+
14
+ /**
15
+ * Peels nested `Grouped` wrappers, returning the first descendant that is not
16
+ * one. The narrow unwrap for reject helpers whose forbidden node cannot live
17
+ * inside `KeepDrop`/`Sort`/`CritThreshold` — those parsers already reject it
18
+ * upstream, so peeling parentheses is all that is left to see through.
19
+ */
20
+ export function unwrapGrouped(node: ASTNode): ASTNode {
21
+ let current = node;
22
+ while (current.type === 'Grouped') {
23
+ current = current.expression;
24
+ }
25
+ return current;
26
+ }
27
+
28
+ /**
29
+ * Peels every transparent wrapper — `Grouped`, `KeepDrop`, `Sort`,
30
+ * `CritThreshold`, `DieBound` — returning the first descendant that is none of
31
+ * them.
32
+ *
33
+ * "Transparent" is relative to the question being asked. These wrappers
34
+ * preserve `containsDicePool`'s answer for whatever they wrap, so they are
35
+ * transparent for "what is the underlying operand?" when deciding whether to
36
+ * reject a `Group` target (e.g., `Group` cannot be the target of `cs`/`cf`,
37
+ * even when wrapped in `KeepDrop` like `{1d6}kh1cs>5`). They are NOT
38
+ * transparent for "is this a `SuccessCount`?" or "is this a `Versus`?" —
39
+ * those questions use `unwrapGrouped`.
40
+ */
41
+ export function unwrapAllTransparent(node: ASTNode): ASTNode {
42
+ let current = node;
43
+ while (true) {
44
+ switch (current.type) {
45
+ case 'Grouped':
46
+ current = current.expression;
47
+ break;
48
+ case 'KeepDrop':
49
+ case 'Sort':
50
+ case 'CritThreshold':
51
+ case 'DieBound':
52
+ current = current.target;
53
+ break;
54
+ default:
55
+ return current;
56
+ }
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Returns `true` when `node` or any descendant satisfies `isHit`. Recurses
62
+ * directly through the entire node vocabulary: arithmetic operands,
63
+ * modifier-chain targets, `Versus` sides, function arguments, and group
64
+ * sub-expressions. This is the shared driver behind the four deep walkers
65
+ * below. The shallow walkers (`containsDicePool`, `containsFatePool`) stay
66
+ * hand-written because their rejection semantics deliberately stop at
67
+ * arithmetic boundaries.
68
+ */
69
+ function someDescendant(node: ASTNode, isHit: (node: ASTNode) => boolean): boolean {
70
+ if (isHit(node)) return true;
71
+
72
+ switch (node.type) {
73
+ case 'BinaryOp':
74
+ return someDescendant(node.left, isHit) || someDescendant(node.right, isHit);
75
+ case 'UnaryOp':
76
+ return someDescendant(node.operand, isHit);
77
+ case 'KeepDrop':
78
+ case 'Explode':
79
+ case 'Reroll':
80
+ case 'DieBound':
81
+ case 'SuccessCount':
82
+ case 'Sort':
83
+ case 'CritThreshold':
84
+ return someDescendant(node.target, isHit);
85
+ case 'Versus':
86
+ return someDescendant(node.roll, isHit) || someDescendant(node.dc, isHit);
87
+ case 'FunctionCall': {
88
+ for (const arg of node.args) {
89
+ if (someDescendant(arg, isHit)) return true;
90
+ }
91
+ return false;
92
+ }
93
+ case 'Grouped':
94
+ return someDescendant(node.expression, isHit);
95
+ case 'Group': {
96
+ for (const expression of node.expressions) {
97
+ if (someDescendant(expression, isHit)) return true;
98
+ }
99
+ return false;
100
+ }
101
+ default:
102
+ return false;
103
+ }
104
+ }
105
+
106
+ // Hoisted `someDescendant` predicates — the deep walkers run on nearly every
107
+ // LED parse, so rebuilding these closures per call is measurable churn.
108
+ const isDicePoolHit = (current: ASTNode): boolean =>
109
+ current.type === 'Dice' ||
110
+ current.type === 'FateDice' ||
111
+ (current.type === 'Group' && current.expressions.length >= 2);
112
+
113
+ const isFateDiceHit = (current: ASTNode): boolean => current.type === 'FateDice';
114
+
115
+ const isMultiSubGroupHit = (current: ASTNode): boolean =>
116
+ current.type === 'Group' && current.expressions.length >= 2;
117
+
118
+ const isVersusHit = (current: ASTNode): boolean => current.type === 'Versus';
119
+
120
+ /**
121
+ * Returns `true` only when `node`'s direct result is a dice pool —
122
+ * `Dice`, `FateDice`, or a chained pool modifier (`KeepDrop` / `Explode` /
123
+ * `Reroll`). Does NOT recurse through arithmetic wrappers (`BinaryOp`,
124
+ * `UnaryOp`, `FunctionCall`), so `(1d6+5)` and `floor(1d6/2)` are rejected.
125
+ *
126
+ * Used by the parser to reject postfix pool-modifier targets (kh/kl/dh/dl,
127
+ * !/!!/!p, r/ro) that wrap a non-pool expression. Operating on the inner
128
+ * dice pool would silently drop the surrounding arithmetic.
129
+ */
130
+ export function containsDicePool(node: ASTNode): boolean {
131
+ switch (node.type) {
132
+ case 'Dice':
133
+ case 'FateDice':
134
+ case 'KeepDrop':
135
+ case 'Explode':
136
+ case 'Reroll':
137
+ case 'DieBound':
138
+ return true;
139
+ case 'Sort':
140
+ case 'CritThreshold':
141
+ return containsDicePool(node.target);
142
+ case 'Grouped':
143
+ return containsDicePool(node.expression);
144
+ case 'Group':
145
+ // Multi-sub-roll groups (`{a, b, c}kh1`) always accept: keep/drop operates
146
+ // on subtotals, which are compound dice by definition — even a
147
+ // literal-only `{3, 5, 7}kh1` is valid. A single-sub-roll group is the
148
+ // user's explicit opt-in to flat-pool semantics, so it deep-walks
149
+ // arithmetic that a raw `(1d6+5)kh1` rejects — the Stage 3 `{}` escape
150
+ // hatch.
151
+ return node.expressions.length >= 2 || node.expressions.some(deepContainsDicePool);
152
+ default:
153
+ return false;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Deeper variant of `containsDicePool` that recurses through arithmetic and
159
+ * function wrappers. Used from the `Group` case above (ordinary parenthesized
160
+ * arithmetic `(1d6+5)kh1` must still reject, so the shallow `containsDicePool`
161
+ * handles those directly) and from the Sort parser guard (sort accepts
162
+ * `(1d6+2d8)s` per Stage 3 spec).
163
+ *
164
+ * A multi-sub-roll `Group` counts as a pool in its own right (compound dice),
165
+ * matching `containsDicePool`'s Group rule.
166
+ */
167
+ export function deepContainsDicePool(node: ASTNode): boolean {
168
+ return someDescendant(node, isDicePoolHit);
169
+ }
170
+
171
+ /**
172
+ * Returns `true` if the pool this node resolves to is (or wraps) a `FateDice`
173
+ * pool. Walks through chained pool modifiers (`KeepDrop` / `Explode` /
174
+ * `Reroll`) but not arithmetic wrappers — callers should run
175
+ * `containsDicePool` first to reject those.
176
+ *
177
+ * Used by the parser to reject `!`, `!!`, `!p` applied to Fate pools
178
+ * (`4dF!`, `(4dF)kh2!`, etc.). Fate explosion semantics are undefined, so
179
+ * parse-time rejection is preferred over a silent evaluator no-op.
180
+ *
181
+ * Inside a `Group`, recursion uses `deepContainsFatePool` to mirror
182
+ * `containsDicePool`'s deep walk through the same case — otherwise
183
+ * `{4dF+1d6}cf` slips past the bare-Fate guard and the default fumble
184
+ * check (`result === 1`) flips `+1` faces into fumbles.
185
+ */
186
+ export function containsFatePool(node: ASTNode): boolean {
187
+ switch (node.type) {
188
+ case 'FateDice':
189
+ return true;
190
+ case 'KeepDrop':
191
+ case 'Explode':
192
+ case 'Reroll':
193
+ case 'DieBound':
194
+ case 'Sort':
195
+ case 'CritThreshold':
196
+ return containsFatePool(node.target);
197
+ case 'Grouped':
198
+ return containsFatePool(node.expression);
199
+ case 'Group':
200
+ return node.expressions.some(deepContainsFatePool);
201
+ default:
202
+ return false;
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Deeper variant of `containsFatePool` that recurses through arithmetic and
208
+ * function wrappers. Mirrors `deepContainsDicePool`. Used from
209
+ * `containsFatePool`'s `Group` case so single-sub-roll groups containing
210
+ * arithmetic-wrapped Fate (`{4dF+1d6}cf`) still trip the bare-Fate guard.
211
+ *
212
+ * Outside a `Group`, ordinary parenthesized arithmetic (`(4dF+1d6)cf`) is
213
+ * already rejected upstream by shallow `containsDicePool`, so this helper
214
+ * intentionally stays Group-internal.
215
+ */
216
+ export function deepContainsFatePool(node: ASTNode): boolean {
217
+ return someDescendant(node, isFateDiceHit);
218
+ }
219
+
220
+ /**
221
+ * Deep-walks a node to find any descendant `Group` with two or more
222
+ * sub-expressions. Used by `rejectGroupTarget`'s single-sub-roll
223
+ * passthrough so a multi-sub Group buried under arithmetic
224
+ * (`{{1d6,2d8}+0}cs>5`), function calls (`{abs({1d6,2d8})}cs>5`), or any
225
+ * other non-transparent wrapper still rejects with the same error code.
226
+ *
227
+ * Without this walk, the unwrap inside `rejectGroupTarget` only peels
228
+ * `Grouped`/`KeepDrop`/`Sort`/`CritThreshold` — a multi-sub Group cloaked
229
+ * in a `BinaryOp`/`UnaryOp`/`FunctionCall` reaches the evaluator, where it
230
+ * flags crits on dice belonging to dropped sub-rolls.
231
+ */
232
+ export function containsMultiSubGroup(node: ASTNode): boolean {
233
+ return someDescendant(node, isMultiSubGroupHit);
234
+ }
235
+
236
+ /**
237
+ * Deep-walks a node to find any descendant `Versus`. A buried Versus
238
+ * (`floor(1d20 vs 15)`, `(1d20 vs 15)+0`, `{1+(1d20 vs 15)}`) must reject with
239
+ * `NESTED_VERSUS` rather than silently drop `versusMetadata` at the consumer.
240
+ *
241
+ * Two callers, both on that hole: `rejectVersusMetaOperand` guards every meta
242
+ * operand — dice count and sides, keep/drop count, thresholds, die bounds — and
243
+ * `rejectVersusTarget` uses it for its single-sub-roll Group passthrough.
244
+ * `evalMetaOperand` runs the same walk as the hand-built-AST backstop.
245
+ */
246
+ export function containsVersus(node: ASTNode): boolean {
247
+ return someDescendant(node, isVersusHit);
248
+ }