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
@@ -11,84 +11,170 @@ import type { ComparePoint } from '../types.js';
11
11
  * original notation string. The parser sets both on every node it produces;
12
12
  * they are typed optional so hand-constructed ASTs (tests, programmatic
13
13
  * consumers) remain valid without positions.
14
+ *
15
+ * `notation.slice(start, end)` recovers the source text of any parsed node,
16
+ * which is how the CLI underlines the failing sub-expression.
17
+ *
18
+ * @category AST
14
19
  */
15
20
  export type NodeSpan = {
16
- start?: number;
17
- end?: number;
21
+ readonly start?: number;
22
+ readonly end?: number;
18
23
  };
19
24
  /**
20
- * Numeric literal node.
25
+ * Numeric literal node — an integer or decimal written directly in the
26
+ * notation (`3`, `2.5`). Leaf node.
27
+ *
28
+ * Decimals are accepted and carried through arithmetic exactly as written;
29
+ * only dice counts and sides are required to resolve to integers.
30
+ *
31
+ * @category AST
21
32
  */
22
33
  export type LiteralNode = NodeSpan & {
23
- type: 'Literal';
24
- value: number;
34
+ readonly type: 'Literal';
35
+ readonly value: number;
25
36
  };
26
37
  /**
27
- * Dice roll node.
28
- * Count and sides can be expressions to support computed dice like (1+1)d(3*2).
38
+ * Dice roll node (`NdX`, `dX`, `d%`).
39
+ *
40
+ * `count` and `sides` are full sub-expressions, not numbers, which is what
41
+ * makes computed dice like `(1+1)d(3*2)` and `(1d4)d6` expressible. Both are
42
+ * evaluated first and must resolve to integers; `d%` desugars to a `sides`
43
+ * literal of `100`, and an omitted count to a `count` literal of `1`.
44
+ *
45
+ * Dice rolled by the `count`/`sides` sub-expressions are meta-expression dice
46
+ * — they land in `RollResult.rolls` tagged `'meta'` and belong to no pool.
47
+ *
48
+ * @category AST
29
49
  */
30
50
  export type DiceNode = NodeSpan & {
31
- type: 'Dice';
32
- count: ASTNode;
33
- sides: ASTNode;
51
+ readonly type: 'Dice';
52
+ readonly count: ASTNode;
53
+ readonly sides: ASTNode;
34
54
  };
35
55
  /**
36
56
  * Fate/Fudge dice node (`dF`).
37
57
  * Each die produces a result in {-1, 0, +1}. No configurable sides.
58
+ *
59
+ * Fate dice carry `sides: 0` as a sentinel in their `DieResult`, and are
60
+ * never `critical` or `fumble` — there is no maximum face to hit.
61
+ *
62
+ * @category AST
38
63
  */
39
64
  export type FateDiceNode = NodeSpan & {
40
- type: 'FateDice';
41
- count: ASTNode;
65
+ readonly type: 'FateDice';
66
+ readonly count: ASTNode;
42
67
  };
43
68
  /**
44
- * Binary operation node.
69
+ * Binary operation node — the arithmetic backbone of an expression.
70
+ *
71
+ * | Operator | Meaning | Precedence | Associativity |
72
+ * |----------|---------|-----------:|---------------|
73
+ * | `+` `-` | add, subtract | 10 | left |
74
+ * | `*` `/` `%` | multiply, divide, modulo | 20 | left |
75
+ * | `**` | power (also spelled `^`) | 30 | right |
76
+ *
77
+ * Dice bind tighter than all of them (40), and postfix modifiers sit at 35,
78
+ * so `2d6+3` is `(2d6)+3` and `4d6kh3*2` is `(4d6kh3)*2`. The `vs` operator
79
+ * binds loosest of all (2) and has its own {@link VersusNode}.
80
+ *
81
+ * `/` and `%` throw `DIVISION_BY_ZERO` / `MODULO_BY_ZERO` on a zero right
82
+ * side; any operator producing a non-finite total throws `NON_FINITE_RESULT`.
83
+ * Division is not rounded — wrap it in `floor()` if you need an integer.
84
+ *
85
+ * @category AST
45
86
  */
46
87
  export type BinaryOpNode = NodeSpan & {
47
- type: 'BinaryOp';
48
- operator: '+' | '-' | '*' | '/' | '%' | '**';
49
- left: ASTNode;
50
- right: ASTNode;
88
+ readonly type: 'BinaryOp';
89
+ readonly operator: '+' | '-' | '*' | '/' | '%' | '**';
90
+ readonly left: ASTNode;
91
+ readonly right: ASTNode;
51
92
  };
52
93
  /**
53
- * Unary operation node.
94
+ * Unary operation node — prefix negation, the only unary operator.
95
+ *
96
+ * Binding power 25 sits between multiplication and power, so `-1d4` is
97
+ * `-(1d4)` (negate the roll) rather than `(-1)d4`, while `-2**2` is `-(2**2)`.
98
+ *
99
+ * @category AST
54
100
  */
55
101
  export type UnaryOpNode = NodeSpan & {
56
- type: 'UnaryOp';
57
- operator: '-';
58
- operand: ASTNode;
102
+ readonly type: 'UnaryOp';
103
+ readonly operator: '-';
104
+ readonly operand: ASTNode;
59
105
  };
60
106
  /**
61
- * Keep/drop modifier node.
107
+ * Keep/drop modifier node (`khN`, `klN`, `dhN`, `dlN`, and the `kN`
108
+ * shorthand for `khN`).
109
+ *
62
110
  * Wraps a dice expression with keep highest/lowest or drop highest/lowest.
63
- */
64
- export type ModifierNode = NodeSpan & {
65
- type: 'Modifier';
66
- modifier: 'keep' | 'drop';
67
- selector: 'highest' | 'lowest';
68
- count: ASTNode;
69
- target: ASTNode;
111
+ * An omitted count defaults to 1, so `4d6kh` is `4d6kh1`. `count` is a full
112
+ * sub-expression, so `4d6kh(1d2)` is legal — and its dice are drawn *before*
113
+ * the pool.
114
+ *
115
+ * Chained modifiers (`4d6kh3dl1`) do not nest: the evaluator flattens the
116
+ * chain and applies each spec independently to the same pool, unioning the
117
+ * dropped sets, which is the Roll20 rule.
118
+ *
119
+ * @category AST
120
+ */
121
+ export type KeepDropNode = NodeSpan & {
122
+ readonly type: 'KeepDrop';
123
+ readonly kind: 'keep' | 'drop';
124
+ readonly selector: 'highest' | 'lowest';
125
+ readonly count: ASTNode;
126
+ readonly target: ASTNode;
70
127
  };
71
128
  /**
72
129
  * Exploding dice node (`!`, `!!`, `!p`, `!>Y`).
73
130
  * Wraps a dice expression with a standard, compounding, or penetrating
74
131
  * explosion. An absent `threshold` means "explode on the die's maximum face".
132
+ *
133
+ * Thresholds accept any comparator. In notation like `1d10!=10` (the
134
+ * Storyteller "10-again" rule) the `!` is the explode marker and `=10` the
135
+ * equality threshold — there is no `!=` comparator. Per-die explosion count
136
+ * is capped by `EvaluationOptions.maxExplodeIterations`.
137
+ *
138
+ * @category AST
75
139
  */
76
140
  export type ExplodeNode = NodeSpan & {
77
- type: 'Explode';
78
- variant: 'standard' | 'compound' | 'penetrating';
79
- threshold?: ComparePoint;
80
- target: ASTNode;
141
+ readonly type: 'Explode';
142
+ readonly variant: 'standard' | 'compound' | 'penetrating';
143
+ readonly threshold?: ComparePoint;
144
+ readonly target: ASTNode;
81
145
  };
82
146
  /**
83
147
  * Reroll node (`r<COND>`, `ro<COND>`).
84
148
  * Re-rolls dice that match a comparison condition. `once: true` for `ro`
85
149
  * keeps the second result regardless of match; `once: false` for `r`
86
- * re-rolls recursively until the condition no longer matches.
150
+ * re-rolls recursively until the condition no longer matches, bounded by
151
+ * `EvaluationOptions.maxRerollIterations`.
152
+ *
153
+ * @category AST
87
154
  */
88
155
  export type RerollNode = NodeSpan & {
89
- type: 'Reroll';
90
- once: boolean;
91
- condition: ComparePoint;
156
+ readonly type: 'Reroll';
157
+ readonly once: boolean;
158
+ readonly condition: ComparePoint;
159
+ readonly target: ASTNode;
160
+ };
161
+ /**
162
+ * Per-die clamp node (`minN`, `maxN`).
163
+ * `4d6min2` raises every die below 2 to 2; `4d6max5` lowers every die above
164
+ * 5 to 5. The clamp rewrites `DieResult.result` (preserving the raw face in
165
+ * `initialResult`) and re-sums the pool; `critical`/`fumble` keep reflecting
166
+ * the natural face. The bound is a full sub-expression (`4d6min(1d2)`),
167
+ * drawn *after* the pool like other threshold arguments.
168
+ *
169
+ * Chained bounds nest and apply left to right: `4d6min2max5` clamps into
170
+ * [2, 5].
171
+ *
172
+ * @category AST
173
+ */
174
+ export type DieBoundNode = NodeSpan & {
175
+ type: 'DieBound';
176
+ bound: 'min' | 'max';
177
+ value: ASTNode;
92
178
  target: ASTNode;
93
179
  };
94
180
  /**
@@ -99,12 +185,14 @@ export type RerollNode = NodeSpan & {
99
185
  * `SuccessCountNode` may not be wrapped by any postfix modifier, binary
100
186
  * operator, unary operator, versus operand, or function argument. The
101
187
  * `failThreshold` accepts any `CompareOp`; bare `fN` defaults to `operator: '='`.
188
+ *
189
+ * @category AST
102
190
  */
103
191
  export type SuccessCountNode = NodeSpan & {
104
- type: 'SuccessCount';
105
- target: ASTNode;
106
- threshold: ComparePoint;
107
- failThreshold?: ComparePoint;
192
+ readonly type: 'SuccessCount';
193
+ readonly target: ASTNode;
194
+ readonly threshold: ComparePoint;
195
+ readonly failThreshold?: ComparePoint;
108
196
  };
109
197
  /**
110
198
  * Versus node (`<roll> vs <dc>`) — PF2e Degrees of Success.
@@ -115,11 +203,13 @@ export type SuccessCountNode = NodeSpan & {
115
203
  * roll side. Lowest-precedence operator — chaining (`a vs b vs c`) is
116
204
  * rejected at parse time; nesting via parens (`a vs (b vs c)`) is rejected
117
205
  * at evaluation time.
206
+ *
207
+ * @category AST
118
208
  */
119
209
  export type VersusNode = NodeSpan & {
120
- type: 'Versus';
121
- roll: ASTNode;
122
- dc: ASTNode;
210
+ readonly type: 'Versus';
211
+ readonly roll: ASTNode;
212
+ readonly dc: ASTNode;
123
213
  };
124
214
  /**
125
215
  * Math function call node (`floor(expr)`, `max(a, b, ...)`, etc.).
@@ -128,11 +218,16 @@ export type VersusNode = NodeSpan & {
128
218
  * variadic functions `max`, `min` (minimum 2 args). Arity is validated at
129
219
  * parse time against a static table; by the time the evaluator sees a
130
220
  * `FunctionCallNode`, `args.length` is guaranteed to match the function.
221
+ *
222
+ * `max`/`min` are variadic with no upper bound, so `max(1d20, 1d20, 1d20)`
223
+ * is a valid three-way advantage roll.
224
+ *
225
+ * @category AST
131
226
  */
132
227
  export type FunctionCallNode = NodeSpan & {
133
- type: 'FunctionCall';
134
- name: string;
135
- args: ASTNode[];
228
+ readonly type: 'FunctionCall';
229
+ readonly name: string;
230
+ readonly args: readonly ASTNode[];
136
231
  };
137
232
  /**
138
233
  * Parenthesized group node (`(<expr>)`).
@@ -141,10 +236,12 @@ export type FunctionCallNode = NodeSpan & {
141
236
  * `RollResult.expression` and `RollResult.rendered` round-trip through
142
237
  * `parse` without losing precedence information. Semantically transparent:
143
238
  * evaluation returns the inner expression's value unchanged.
239
+ *
240
+ * @category AST
144
241
  */
145
242
  export type GroupedNode = NodeSpan & {
146
- type: 'Grouped';
147
- expression: ASTNode;
243
+ readonly type: 'Grouped';
244
+ readonly expression: ASTNode;
148
245
  };
149
246
  /**
150
247
  * Variable reference node (`@name` or `@{name with spaces}`).
@@ -154,10 +251,15 @@ export type GroupedNode = NodeSpan & {
154
251
  * lexer preserves case in the `AT` token's `value`, distinct from other
155
252
  * identifier tokens which lowercase. Leaf node — no LED, never wraps a
156
253
  * sub-expression.
254
+ *
255
+ * A name missing from `context` throws `UNDEFINED_VARIABLE` unless
256
+ * `onMissingVariable: 'zero'` is set.
257
+ *
258
+ * @category AST
157
259
  */
158
260
  export type VariableNode = NodeSpan & {
159
- type: 'Variable';
160
- name: string;
261
+ readonly type: 'Variable';
262
+ readonly name: string;
161
263
  };
162
264
  /**
163
265
  * Grouped-roll node (`{expr}`, `{expr1, expr2, ...}`).
@@ -167,10 +269,12 @@ export type VariableNode = NodeSpan & {
167
269
  * with the sub-roll count. `expressions.length === 1` is a passthrough
168
270
  * (flat-pool when wrapped by keep/drop); `expressions.length >= 2` treats
169
271
  * each sub-roll's subtotal as a compound die for keep/drop selection.
272
+ *
273
+ * @category AST
170
274
  */
171
275
  export type GroupNode = NodeSpan & {
172
- type: 'Group';
173
- expressions: ASTNode[];
276
+ readonly type: 'Group';
277
+ readonly expressions: readonly ASTNode[];
174
278
  };
175
279
  /**
176
280
  * Sort modifier node (`s`, `sa`, `sd`).
@@ -180,16 +284,22 @@ export type GroupNode = NodeSpan & {
180
284
  * `successes`/`failures`, or any die-level flag (`kept`/`dropped`/
181
285
  * `critical`/`fumble`). Dropped dice retain their `dropped` flag and
182
286
  * appear in sorted position alongside kept dice.
287
+ *
288
+ * `s` and `sa` both mean ascending; `sd` is descending.
289
+ *
290
+ * @category AST
183
291
  */
184
292
  export type SortNode = NodeSpan & {
185
- type: 'Sort';
186
- order: 'ascending' | 'descending';
187
- target: ASTNode;
293
+ readonly type: 'Sort';
294
+ readonly order: 'ascending' | 'descending';
295
+ readonly target: ASTNode;
188
296
  };
189
297
  /**
190
298
  * Sentinel for bare `cs` / `cf` without a ComparePoint. Resolved to
191
299
  * `result === sides` (for critical) or `result === 1` (for fumble) at
192
300
  * evaluation time, using each die's own `sides`.
301
+ *
302
+ * @category AST
193
303
  */
194
304
  export type CritThreshold = ComparePoint | 'default';
195
305
  /**
@@ -203,166 +313,210 @@ export type CritThreshold = ComparePoint | 'default';
203
313
  * explosion triggers, or success counting. `cs` and `cf` are independent
204
314
  * overrides — a side with no explicit thresholds keeps the default rule
205
315
  * (the evaluator substitutes the `'default'` sentinel at apply time).
316
+ *
317
+ * @category AST
206
318
  */
207
319
  export type CritThresholdNode = NodeSpan & {
208
- type: 'CritThreshold';
209
- successThresholds: CritThreshold[];
210
- failThresholds: CritThreshold[];
211
- target: ASTNode;
320
+ readonly type: 'CritThreshold';
321
+ readonly successThresholds: readonly CritThreshold[];
322
+ readonly failThresholds: readonly CritThreshold[];
323
+ readonly target: ASTNode;
212
324
  };
213
325
  /**
214
- * Union type of all AST nodes.
326
+ * Discriminated union of all 17 AST node types — what {@link parse} returns
327
+ * and what {@link evaluate} consumes.
328
+ *
329
+ * Narrow it either by switching on `node.type` (PascalCase discriminants, as
330
+ * opposed to the camelCase ones on {@link RollPart}) or with the exported
331
+ * type guards: {@link isLiteral}, {@link isDice}, {@link isFateDice},
332
+ * {@link isBinaryOp}, {@link isUnaryOp}, {@link isKeepDrop},
333
+ * {@link isExplode}, {@link isReroll}, {@link isDieBound},
334
+ * {@link isSuccessCount}, {@link isVersus}, {@link isFunctionCall},
335
+ * {@link isGrouped}, {@link isVariable}, {@link isGroup}, {@link isSort},
336
+ * {@link isCritThreshold}.
337
+ *
338
+ * Nodes are plain data with no methods, so they are structurally clonable and
339
+ * safe to cache. Every parser-produced node carries a {@link NodeSpan}.
340
+ *
341
+ * @example A guard-based walker — count the dice an expression can roll
342
+ * ```typescript
343
+ * import {
344
+ * type ASTNode, isBinaryOp, isDice, isFateDice, isGroup, isGrouped,
345
+ * isFunctionCall, isLiteral, isUnaryOp, isVersus, parse,
346
+ * } from 'roll-parser';
347
+ *
348
+ * function countPools(node: ASTNode): number {
349
+ * if (isDice(node) || isFateDice(node)) return 1;
350
+ * if (isLiteral(node)) return 0;
351
+ * if (isBinaryOp(node)) return countPools(node.left) + countPools(node.right);
352
+ * if (isUnaryOp(node)) return countPools(node.operand);
353
+ * if (isGrouped(node)) return countPools(node.expression);
354
+ * if (isVersus(node)) return countPools(node.roll) + countPools(node.dc);
355
+ * if (isGroup(node)) return node.expressions.reduce((n, e) => n + countPools(e), 0);
356
+ * if (isFunctionCall(node)) return node.args.reduce((n, a) => n + countPools(a), 0);
357
+ * // Every remaining variant is a postfix modifier wrapping `target`.
358
+ * return 'target' in node ? countPools(node.target) : 0;
359
+ * }
360
+ *
361
+ * countPools(parse('2d6+3')); // 1
362
+ * countPools(parse('{2d20kh1+5, 3d8!}kh1')); // 2
363
+ * countPools(parse('1+2')); // 0
364
+ * ```
365
+ *
366
+ * @category AST
215
367
  */
216
- export type ASTNode = LiteralNode | DiceNode | FateDiceNode | BinaryOpNode | UnaryOpNode | ModifierNode | ExplodeNode | RerollNode | SuccessCountNode | VersusNode | FunctionCallNode | GroupedNode | VariableNode | GroupNode | SortNode | CritThresholdNode;
368
+ export type ASTNode = LiteralNode | DiceNode | FateDiceNode | BinaryOpNode | UnaryOpNode | KeepDropNode | ExplodeNode | RerollNode | DieBoundNode | SuccessCountNode | VersusNode | FunctionCallNode | GroupedNode | VariableNode | GroupNode | SortNode | CritThresholdNode;
217
369
  /**
218
- * Type guard for LiteralNode.
370
+ * Narrows an {@link ASTNode} to a numeric literal (`3`, `2.5`).
371
+ *
372
+ * @param node - Any AST node
373
+ * @returns `true` when `node` is a {@link LiteralNode}
374
+ *
375
+ * @category AST
219
376
  */
220
377
  export declare function isLiteral(node: ASTNode): node is LiteralNode;
221
378
  /**
222
- * Type guard for DiceNode.
379
+ * Narrows an {@link ASTNode} to a dice pool (`2d6`, `d%`, `(1d4)d6`).
380
+ *
381
+ * @param node - Any AST node
382
+ * @returns `true` when `node` is a {@link DiceNode}
383
+ *
384
+ * @category AST
223
385
  */
224
386
  export declare function isDice(node: ASTNode): node is DiceNode;
225
387
  /**
226
- * Type guard for FateDiceNode.
388
+ * Narrows an {@link ASTNode} to a Fate/Fudge pool (`4dF`).
389
+ *
390
+ * @param node - Any AST node
391
+ * @returns `true` when `node` is a {@link FateDiceNode}
392
+ *
393
+ * @category AST
227
394
  */
228
395
  export declare function isFateDice(node: ASTNode): node is FateDiceNode;
229
396
  /**
230
- * Type guard for BinaryOpNode.
397
+ * Narrows an {@link ASTNode} to an arithmetic operation (`+ - * / % **`).
398
+ *
399
+ * @param node - Any AST node
400
+ * @returns `true` when `node` is a {@link BinaryOpNode}
401
+ *
402
+ * @category AST
231
403
  */
232
404
  export declare function isBinaryOp(node: ASTNode): node is BinaryOpNode;
233
405
  /**
234
- * Type guard for UnaryOpNode.
406
+ * Narrows an {@link ASTNode} to a prefix negation (`-1d4`).
407
+ *
408
+ * @param node - Any AST node
409
+ * @returns `true` when `node` is a {@link UnaryOpNode}
410
+ *
411
+ * @category AST
235
412
  */
236
413
  export declare function isUnaryOp(node: ASTNode): node is UnaryOpNode;
237
414
  /**
238
- * Type guard for ModifierNode.
415
+ * Narrows an {@link ASTNode} to a keep/drop modifier (`kh`, `kl`, `dh`, `dl`).
416
+ *
417
+ * @param node - Any AST node
418
+ * @returns `true` when `node` is a {@link KeepDropNode}
419
+ *
420
+ * @category AST
239
421
  */
240
- export declare function isModifier(node: ASTNode): node is ModifierNode;
422
+ export declare function isKeepDrop(node: ASTNode): node is KeepDropNode;
241
423
  /**
242
- * Type guard for ExplodeNode.
424
+ * Narrows an {@link ASTNode} to an explosion (`!`, `!!`, `!p`).
425
+ *
426
+ * @param node - Any AST node
427
+ * @returns `true` when `node` is a {@link ExplodeNode}
428
+ *
429
+ * @category AST
243
430
  */
244
431
  export declare function isExplode(node: ASTNode): node is ExplodeNode;
245
432
  /**
246
- * Type guard for RerollNode.
433
+ * Narrows an {@link ASTNode} to a reroll (`r`, `ro`).
434
+ *
435
+ * @param node - Any AST node
436
+ * @returns `true` when `node` is a {@link RerollNode}
437
+ *
438
+ * @category AST
247
439
  */
248
440
  export declare function isReroll(node: ASTNode): node is RerollNode;
249
441
  /**
250
- * Type guard for SuccessCountNode.
442
+ * Narrows an {@link ASTNode} to a success count (`>=6`, with optional `f1`).
443
+ *
444
+ * @param node - Any AST node
445
+ * @returns `true` when `node` is a {@link SuccessCountNode}
446
+ *
447
+ * @category AST
251
448
  */
252
449
  export declare function isSuccessCount(node: ASTNode): node is SuccessCountNode;
253
450
  /**
254
- * Type guard for VersusNode.
451
+ * Narrows an {@link ASTNode} to a PF2e degree-of-success check (`vs`).
452
+ *
453
+ * @param node - Any AST node
454
+ * @returns `true` when `node` is a {@link VersusNode}
455
+ *
456
+ * @category AST
255
457
  */
256
458
  export declare function isVersus(node: ASTNode): node is VersusNode;
257
459
  /**
258
- * Type guard for FunctionCallNode.
460
+ * Narrows an {@link ASTNode} to a math function call (`floor`, `max`, …).
461
+ *
462
+ * @param node - Any AST node
463
+ * @returns `true` when `node` is a {@link FunctionCallNode}
464
+ *
465
+ * @category AST
259
466
  */
260
467
  export declare function isFunctionCall(node: ASTNode): node is FunctionCallNode;
261
468
  /**
262
- * Type guard for GroupedNode.
469
+ * Narrows an {@link ASTNode} to a parenthesized group (`(1d6+2)`).
470
+ *
471
+ * @param node - Any AST node
472
+ * @returns `true` when `node` is a {@link GroupedNode}
473
+ *
474
+ * @category AST
263
475
  */
264
476
  export declare function isGrouped(node: ASTNode): node is GroupedNode;
265
477
  /**
266
- * Type guard for VariableNode.
478
+ * Narrows an {@link ASTNode} to a variable reference (`@str`).
479
+ *
480
+ * @param node - Any AST node
481
+ * @returns `true` when `node` is a {@link VariableNode}
482
+ *
483
+ * @category AST
267
484
  */
268
485
  export declare function isVariable(node: ASTNode): node is VariableNode;
269
486
  /**
270
- * Type guard for GroupNode.
271
- */
272
- export declare function isGroup(node: ASTNode): node is GroupNode;
273
- /**
274
- * Type guard for SortNode.
275
- */
276
- export declare function isSort(node: ASTNode): node is SortNode;
277
- /**
278
- * Type guard for CritThresholdNode.
279
- */
280
- export declare function isCritThreshold(node: ASTNode): node is CritThresholdNode;
281
- /**
282
- * Wrapper kinds that `unwrapTransparent` can peel.
487
+ * Narrows an {@link ASTNode} to a braced grouped roll (`{1d8, 1d10}`).
283
488
  *
284
- * "Transparent" is relative to the question being asked. `Modifier`/`Sort`/
285
- * `CritThreshold` are transparent for "what is the underlying operand?" when
286
- * deciding whether to reject a `Group` target — they preserve `containsDicePool`'s
287
- * answer for whatever they wrap. They are NOT transparent for "is this a
288
- * `SuccessCount`?" or "is this a `Versus`?", because the parsers that build
289
- * those wrappers already reject `SuccessCount`/`Versus` operands upstream.
290
- */
291
- export type TransparentWrapperKind = 'Grouped' | 'Modifier' | 'Sort' | 'CritThreshold';
292
- /**
293
- * Walks `node` while its `.type` is in `kinds`, returning the first descendant
294
- * that is not one of the listed wrappers. Each caller picks the subset that
295
- * matches its rejection semantics — see `TransparentWrapperKind` for guidance.
489
+ * @param node - Any AST node
490
+ * @returns `true` when `node` is a {@link GroupNode}
296
491
  *
297
- * Used by parser reject helpers to look past wrappers when deciding whether
298
- * an operand violates a rule (e.g., `Group` cannot be the target of `cs`/`cf`,
299
- * even when wrapped in `Modifier` like `{1d6}kh1cs>5`).
492
+ * @category AST
300
493
  */
301
- export declare function unwrapTransparent(node: ASTNode, kinds: readonly TransparentWrapperKind[]): ASTNode;
494
+ export declare function isGroup(node: ASTNode): node is GroupNode;
302
495
  /**
303
- * Returns `true` only when `node`'s direct result is a dice pool —
304
- * `Dice`, `FateDice`, or a chained pool modifier (`Modifier` / `Explode` /
305
- * `Reroll`). Does NOT recurse through arithmetic wrappers (`BinaryOp`,
306
- * `UnaryOp`, `FunctionCall`), so `(1d6+5)` and `floor(1d6/2)` are rejected.
496
+ * Narrows an {@link ASTNode} to a sort modifier (`s`, `sa`, `sd`).
307
497
  *
308
- * Used by the parser to reject postfix pool-modifier targets (kh/kl/dh/dl,
309
- * !/!!/!p, r/ro) that wrap a non-pool expression. Operating on the inner
310
- * dice pool would silently drop the surrounding arithmetic.
311
- */
312
- export declare function containsDicePool(node: ASTNode): boolean;
313
- /**
314
- * Deeper variant of `containsDicePool` that recurses through arithmetic and
315
- * function wrappers. Used from the `Group` case above (ordinary parenthesized
316
- * arithmetic `(1d6+5)kh1` must still reject, so the shallow `containsDicePool`
317
- * handles those directly) and from the Sort parser guard (sort accepts
318
- * `(1d6+2d8)s` per Stage 3 spec).
498
+ * @param node - Any AST node
499
+ * @returns `true` when `node` is a {@link SortNode}
500
+ *
501
+ * @category AST
319
502
  */
320
- export declare function deepContainsDicePool(node: ASTNode): boolean;
503
+ export declare function isSort(node: ASTNode): node is SortNode;
321
504
  /**
322
- * Returns `true` if the pool this node resolves to is (or wraps) a `FateDice`
323
- * pool. Walks through chained pool modifiers (`Modifier` / `Explode` /
324
- * `Reroll`) but not arithmetic wrappers — callers should run
325
- * `containsDicePool` first to reject those.
505
+ * Narrows an {@link ASTNode} to a crit-threshold override (`cs`, `cf`).
326
506
  *
327
- * Used by the parser to reject `!`, `!!`, `!p` applied to Fate pools
328
- * (`4dF!`, `(4dF)kh2!`, etc.). Fate explosion semantics are undefined, so
329
- * parse-time rejection is preferred over a silent evaluator no-op.
507
+ * @param node - Any AST node
508
+ * @returns `true` when `node` is a {@link CritThresholdNode}
330
509
  *
331
- * Inside a `Group`, recursion uses `deepContainsFatePool` to mirror
332
- * `containsDicePool`'s deep walk through the same case — otherwise
333
- * `{4dF+1d6}cf` slips past the bare-Fate guard and the default fumble
334
- * check (`result === 1`) flips `+1` faces into fumbles.
510
+ * @category AST
335
511
  */
336
- export declare function containsFatePool(node: ASTNode): boolean;
512
+ export declare function isCritThreshold(node: ASTNode): node is CritThresholdNode;
337
513
  /**
338
- * Deeper variant of `containsFatePool` that recurses through arithmetic and
339
- * function wrappers. Mirrors `deepContainsDicePool`. Used from
340
- * `containsFatePool`'s `Group` case so single-sub-roll groups containing
341
- * arithmetic-wrapped Fate (`{4dF+1d6}cf`) still trip the bare-Fate guard.
514
+ * Narrows an {@link ASTNode} to a per-die clamp (`min2`, `max5`).
342
515
  *
343
- * Outside a `Group`, ordinary parenthesized arithmetic (`(4dF+1d6)cf`) is
344
- * already rejected upstream by shallow `containsDicePool`, so this helper
345
- * intentionally stays Group-internal.
346
- */
347
- export declare function deepContainsFatePool(node: ASTNode): boolean;
348
- /**
349
- * Deep-walks a node to find any descendant `Group` with two or more
350
- * sub-expressions. Used by `rejectGroupTarget`'s single-sub-roll
351
- * passthrough so a multi-sub Group buried under arithmetic
352
- * (`{{1d6,2d8}+0}cs>5`), function calls (`{abs({1d6,2d8})}cs>5`), or any
353
- * other non-transparent wrapper still rejects with the same error code.
516
+ * @param node - Any AST node
517
+ * @returns `true` when `node` is a {@link DieBoundNode}
354
518
  *
355
- * Without this walk, the unwrap inside `rejectGroupTarget` only peels
356
- * `Grouped`/`Modifier`/`Sort`/`CritThreshold` — a multi-sub Group cloaked
357
- * in a `BinaryOp`/`UnaryOp`/`FunctionCall` revives issue #97.
358
- */
359
- export declare function containsMultiSubGroup(node: ASTNode): boolean;
360
- /**
361
- * Deep-walks a node to find any descendant `Versus`. Used by
362
- * `rejectVersusTarget`'s single-sub-roll Group passthrough so a buried
363
- * Versus (`{1+(1d20 vs 15)}cs>18`, `{abs(1d20 vs 15)}cs>18`,
364
- * `{-(1d20 vs 15)}kh1`) still rejects with `NESTED_VERSUS` instead of
365
- * silently dropping `versusMetadata` at the modifier consumer site.
519
+ * @category AST
366
520
  */
367
- export declare function containsVersus(node: ASTNode): boolean;
521
+ export declare function isDieBound(node: ASTNode): node is DieBoundNode;
368
522
  //# sourceMappingURL=ast.d.ts.map