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,751 @@
1
+ import { RollParserError } from '../errors.js';
2
+ import { lex } from '../lexer/lexer.js';
3
+ import { TokenType } from '../lexer/tokens.js';
4
+ import { isCritThreshold, isSuccessCount } from './ast.js';
5
+ import { containsDicePool, containsFatePool, containsMultiSubGroup, containsVersus, deepContainsDicePool, unwrapAllTransparent, unwrapGrouped, } from './guards.js';
6
+ export class ParseError extends RollParserError {
7
+ position;
8
+ token;
9
+ constructor(message, code, position, token, options) {
10
+ super(message, code, options);
11
+ this.name = 'ParseError';
12
+ this.position = position;
13
+ this.token = token;
14
+ }
15
+ }
16
+ const BP = {
17
+ VS_LEFT: 2,
18
+ VS_RIGHT: 3,
19
+ COMPARE: 8,
20
+ ADD_LEFT: 10,
21
+ ADD_RIGHT: 11,
22
+ MUL_LEFT: 20,
23
+ MUL_RIGHT: 21,
24
+ UNARY: 25,
25
+ POW_LEFT: 31,
26
+ POW_RIGHT: 30,
27
+ MODIFIER: 35,
28
+ DICE_LEFT: 40,
29
+ DICE_RIGHT: 41,
30
+ };
31
+ export const MAX_PARSE_DEPTH = 128;
32
+ const TOKEN_DISPLAY = {
33
+ [TokenType.LPAREN]: `'('`,
34
+ [TokenType.RPAREN]: `')'`,
35
+ [TokenType.LBRACE]: `'{'`,
36
+ [TokenType.RBRACE]: `'}'`,
37
+ [TokenType.COMMA]: `','`,
38
+ };
39
+ const FUNCTION_ARITY = {
40
+ floor: { min: 1, max: 1 },
41
+ ceil: { min: 1, max: 1 },
42
+ round: { min: 1, max: 1 },
43
+ abs: { min: 1, max: 1 },
44
+ sqrt: { min: 1, max: 1 },
45
+ pow: { min: 2, max: 2 },
46
+ max: { min: 2, max: Number.POSITIVE_INFINITY },
47
+ min: { min: 2, max: Number.POSITIVE_INFINITY },
48
+ };
49
+ function formatArity(arity) {
50
+ if (arity.max === Number.POSITIVE_INFINITY)
51
+ return `at least ${arity.min}`;
52
+ if (arity.min === arity.max)
53
+ return `${arity.min}`;
54
+ return `${arity.min}–${arity.max}`;
55
+ }
56
+ const EXPLODE_VARIANTS = {
57
+ [TokenType.EXPLODE]: 'standard',
58
+ [TokenType.EXPLODE_COMPOUND]: 'compound',
59
+ [TokenType.EXPLODE_PENETRATING]: 'penetrating',
60
+ };
61
+ export class Parser {
62
+ tokens;
63
+ pos = 0;
64
+ depth = 0;
65
+ constructor(tokens) {
66
+ this.tokens = tokens;
67
+ }
68
+ parse() {
69
+ if (this.peek().type === TokenType.EOF) {
70
+ throw new ParseError('Unexpected end of input', 'UNEXPECTED_END', this.peek().position);
71
+ }
72
+ const ast = this.parseExpression(0);
73
+ if (this.peek().type !== TokenType.EOF) {
74
+ const token = this.peek();
75
+ throw new ParseError(`Unexpected token '${token.value}'`, 'UNEXPECTED_TOKEN', token.position, token);
76
+ }
77
+ return ast;
78
+ }
79
+ parseExpression(minBp) {
80
+ const entryDepth = this.depth;
81
+ this.depth += 1;
82
+ this.guardDepth();
83
+ try {
84
+ let left = this.parseNud();
85
+ while (this.hasTokens()) {
86
+ const token = this.peek();
87
+ const leftBp = this.getLeftBp(token);
88
+ if (leftBp < minBp)
89
+ break;
90
+ this.advance();
91
+ left = this.parseLed(left, token);
92
+ this.depth += 1;
93
+ this.guardDepth();
94
+ }
95
+ return left;
96
+ }
97
+ finally {
98
+ this.depth = entryDepth;
99
+ }
100
+ }
101
+ guardDepth() {
102
+ if (this.depth > MAX_PARSE_DEPTH) {
103
+ throw new ParseError(`Expression nesting exceeds the maximum depth of ${MAX_PARSE_DEPTH}`, 'MAX_DEPTH_EXCEEDED', this.peek().position, this.peek());
104
+ }
105
+ }
106
+ parseNud() {
107
+ const token = this.advance();
108
+ switch (token.type) {
109
+ case TokenType.NUMBER:
110
+ return this.parseLiteral(token);
111
+ case TokenType.MINUS:
112
+ return this.parseUnaryMinus(token);
113
+ case TokenType.DICE:
114
+ return this.parsePrefixDice(token);
115
+ case TokenType.DICE_PERCENT:
116
+ return this.parsePrefixDicePercent(token);
117
+ case TokenType.DICE_FATE:
118
+ return this.parsePrefixFateDice(token);
119
+ case TokenType.LPAREN:
120
+ return this.parseGrouped(token);
121
+ case TokenType.LBRACE:
122
+ return this.parseGroup(token);
123
+ case TokenType.FUNCTION:
124
+ return this.parseFunctionCall(token);
125
+ case TokenType.AT:
126
+ return this.parseVariable(token);
127
+ case TokenType.EOF:
128
+ throw new ParseError('Unexpected end of input', 'UNEXPECTED_END', token.position);
129
+ default:
130
+ throw new ParseError(`Unexpected token '${token.value}'`, 'UNEXPECTED_TOKEN', token.position, token);
131
+ }
132
+ }
133
+ parseLed(left, token) {
134
+ switch (token.type) {
135
+ case TokenType.DICE:
136
+ return this.parseInfixDice(left, token);
137
+ case TokenType.DICE_PERCENT:
138
+ return this.parseInfixDicePercent(left, token);
139
+ case TokenType.DICE_FATE:
140
+ return this.parseInfixFateDice(left, token);
141
+ case TokenType.PLUS:
142
+ case TokenType.MINUS:
143
+ case TokenType.MULTIPLY:
144
+ case TokenType.DIVIDE:
145
+ case TokenType.MODULO:
146
+ case TokenType.POWER:
147
+ return this.parseBinaryOp(left, token);
148
+ case TokenType.KEEP_HIGH:
149
+ case TokenType.KEEP_LOW:
150
+ case TokenType.DROP_HIGH:
151
+ case TokenType.DROP_LOW:
152
+ return this.parseKeepDrop(left, token);
153
+ case TokenType.EXPLODE:
154
+ case TokenType.EXPLODE_COMPOUND:
155
+ case TokenType.EXPLODE_PENETRATING:
156
+ return this.parseExplode(left, token);
157
+ case TokenType.REROLL:
158
+ case TokenType.REROLL_ONCE:
159
+ return this.parseReroll(left, token);
160
+ case TokenType.SORT_ASC:
161
+ case TokenType.SORT_DESC:
162
+ return this.parseSort(left, token);
163
+ case TokenType.CRIT_SUCCESS:
164
+ case TokenType.CRIT_FAIL:
165
+ return this.parseCritThreshold(left, token);
166
+ case TokenType.FUNCTION:
167
+ return this.parseDieBound(left, token);
168
+ case TokenType.GREATER:
169
+ case TokenType.GREATER_EQUAL:
170
+ case TokenType.LESS:
171
+ case TokenType.LESS_EQUAL:
172
+ case TokenType.EQUAL:
173
+ return this.parseSuccessCount(left, token);
174
+ case TokenType.VS:
175
+ return this.parseVersus(left, token);
176
+ default:
177
+ throw new ParseError(`Unexpected infix token '${token.value}'`, 'UNEXPECTED_TOKEN', token.position, token);
178
+ }
179
+ }
180
+ static syntheticLiteral(value, token) {
181
+ return { type: 'Literal', value, start: token.position, end: token.position };
182
+ }
183
+ parseLiteral(token) {
184
+ return {
185
+ type: 'Literal',
186
+ value: Number.parseFloat(token.value),
187
+ start: token.position,
188
+ end: token.end,
189
+ };
190
+ }
191
+ parseUnaryMinus(token) {
192
+ const operand = this.parseExpression(BP.UNARY);
193
+ this.rejectSuccessCountTarget(operand, token);
194
+ return {
195
+ type: 'UnaryOp',
196
+ operator: '-',
197
+ operand,
198
+ start: token.position,
199
+ end: operand.end ?? token.end,
200
+ };
201
+ }
202
+ parsePrefixDice(token) {
203
+ const sides = this.parseExpression(BP.DICE_RIGHT);
204
+ this.rejectSuccessCountTarget(sides, token);
205
+ this.rejectVersusMetaOperand(sides, token);
206
+ return {
207
+ type: 'Dice',
208
+ count: Parser.syntheticLiteral(1, token),
209
+ sides,
210
+ start: token.position,
211
+ end: sides.end ?? token.end,
212
+ };
213
+ }
214
+ parseInfixDice(left, token) {
215
+ this.rejectSuccessCountTarget(left, token);
216
+ this.rejectVersusMetaOperand(left, token);
217
+ this.rejectBareDiceChain(left, token);
218
+ const sides = this.parseExpression(BP.DICE_RIGHT);
219
+ this.rejectSuccessCountTarget(sides, token);
220
+ this.rejectVersusMetaOperand(sides, token);
221
+ return {
222
+ type: 'Dice',
223
+ count: left,
224
+ sides,
225
+ start: left.start ?? token.position,
226
+ end: sides.end ?? token.end,
227
+ };
228
+ }
229
+ parsePrefixDicePercent(token) {
230
+ return {
231
+ type: 'Dice',
232
+ count: Parser.syntheticLiteral(1, token),
233
+ sides: Parser.syntheticLiteral(100, token),
234
+ start: token.position,
235
+ end: token.end,
236
+ };
237
+ }
238
+ parseInfixDicePercent(left, token) {
239
+ this.rejectSuccessCountTarget(left, token);
240
+ this.rejectVersusMetaOperand(left, token);
241
+ this.rejectBareDiceChain(left, token);
242
+ return {
243
+ type: 'Dice',
244
+ count: left,
245
+ sides: Parser.syntheticLiteral(100, token),
246
+ start: left.start ?? token.position,
247
+ end: token.end,
248
+ };
249
+ }
250
+ parsePrefixFateDice(token) {
251
+ return {
252
+ type: 'FateDice',
253
+ count: Parser.syntheticLiteral(1, token),
254
+ start: token.position,
255
+ end: token.end,
256
+ };
257
+ }
258
+ parseInfixFateDice(left, token) {
259
+ this.rejectSuccessCountTarget(left, token);
260
+ this.rejectVersusMetaOperand(left, token);
261
+ this.rejectBareDiceChain(left, token);
262
+ return {
263
+ type: 'FateDice',
264
+ count: left,
265
+ start: left.start ?? token.position,
266
+ end: token.end,
267
+ };
268
+ }
269
+ parseGrouped(token) {
270
+ const expression = this.parseExpression(0);
271
+ const close = this.expect(TokenType.RPAREN);
272
+ return { type: 'Grouped', expression, start: token.position, end: close.end };
273
+ }
274
+ parseGroup(startToken) {
275
+ if (this.peek().type === TokenType.RBRACE) {
276
+ throw new ParseError('Empty group', 'UNEXPECTED_TOKEN', startToken.position, startToken);
277
+ }
278
+ const expressions = [this.parseExpression(0)];
279
+ while (this.peek().type === TokenType.COMMA) {
280
+ this.advance();
281
+ expressions.push(this.parseExpression(0));
282
+ }
283
+ if (this.peek().type !== TokenType.RBRACE) {
284
+ const unterminated = this.peek();
285
+ throw new ParseError(`Unterminated group: expected '}' or ','`, 'EXPECTED_TOKEN', unterminated.position, unterminated);
286
+ }
287
+ const close = this.advance();
288
+ return { type: 'Group', expressions, start: startToken.position, end: close.end };
289
+ }
290
+ parseVariable(token) {
291
+ return { type: 'Variable', name: token.value, start: token.position, end: token.end };
292
+ }
293
+ parseFunctionCall(token) {
294
+ this.expect(TokenType.LPAREN);
295
+ const args = [];
296
+ if (this.peek().type !== TokenType.RPAREN) {
297
+ const first = this.parseExpression(0);
298
+ this.rejectSuccessCountTarget(first, token);
299
+ args.push(first);
300
+ while (this.peek().type === TokenType.COMMA) {
301
+ this.advance();
302
+ const next = this.parseExpression(0);
303
+ this.rejectSuccessCountTarget(next, token);
304
+ args.push(next);
305
+ }
306
+ }
307
+ const close = this.expect(TokenType.RPAREN);
308
+ const arity = FUNCTION_ARITY[token.value];
309
+ if (arity == null) {
310
+ throw new ParseError(`Unknown function '${token.value}'`, 'UNKNOWN_FUNCTION', token.position, token);
311
+ }
312
+ if (args.length < arity.min || args.length > arity.max) {
313
+ const expected = formatArity(arity);
314
+ throw new ParseError(`Function '${token.value}' expects ${expected} argument${arity.min === 1 && arity.max === 1 ? '' : 's'}, got ${args.length}`, 'INVALID_FUNCTION_ARITY', token.position, token);
315
+ }
316
+ return {
317
+ type: 'FunctionCall',
318
+ name: token.value,
319
+ args,
320
+ start: token.position,
321
+ end: close.end,
322
+ };
323
+ }
324
+ parseBinaryOp(left, token) {
325
+ this.rejectSuccessCountTarget(left, token);
326
+ const operator = this.getOperatorSymbol(token);
327
+ const rightBp = this.getRightBp(token);
328
+ const right = this.parseExpression(rightBp);
329
+ this.rejectSuccessCountTarget(right, token);
330
+ return {
331
+ type: 'BinaryOp',
332
+ operator,
333
+ left,
334
+ right,
335
+ start: left.start ?? token.position,
336
+ end: right.end ?? token.end,
337
+ };
338
+ }
339
+ rejectBareDiceChain(left, token) {
340
+ switch (left.type) {
341
+ case 'Dice':
342
+ case 'FateDice':
343
+ case 'Explode':
344
+ case 'Reroll':
345
+ case 'DieBound':
346
+ case 'KeepDrop':
347
+ case 'Sort':
348
+ case 'CritThreshold':
349
+ throw new ParseError(`Ambiguous dice chain: use 'dl'/'dh' to drop dice (4d6dl1) or parentheses for nested dice ((4d6)d1)`, 'AMBIGUOUS_DICE_CHAIN', token.position, token);
350
+ default:
351
+ return;
352
+ }
353
+ }
354
+ rejectSuccessCountTarget(target, token) {
355
+ const node = unwrapGrouped(target);
356
+ if (isSuccessCount(node)) {
357
+ throw new ParseError(`Cannot apply modifier after success counting`, 'INVALID_SUCCESS_COUNT_TARGET', token.position, token);
358
+ }
359
+ }
360
+ rejectGroupTarget(target, token, action, code, singleSubRollPasses = false) {
361
+ const node = unwrapAllTransparent(target);
362
+ if (node.type !== 'Group')
363
+ return;
364
+ if (singleSubRollPasses && node.expressions.length === 1) {
365
+ const inner = node.expressions[0];
366
+ if (inner != null && containsMultiSubGroup(inner)) {
367
+ throw new ParseError(`Cannot ${action} a group`, code, token.position, token);
368
+ }
369
+ return;
370
+ }
371
+ throw new ParseError(`Cannot ${action} a group`, code, token.position, token);
372
+ }
373
+ rejectVersusMetaOperand(operand, token) {
374
+ if (containsVersus(operand))
375
+ Parser.throwVersusMetaExpression(token);
376
+ }
377
+ static throwVersusMetaExpression(token) {
378
+ throw new ParseError(`Versus cannot be used as a meta-expression`, 'NESTED_VERSUS', token.position, token);
379
+ }
380
+ rejectVersusTarget(target, token) {
381
+ const node = unwrapGrouped(target);
382
+ if (node.type === 'Versus')
383
+ Parser.throwVersusMetaExpression(token);
384
+ if (node.type === 'Group' && node.expressions.length === 1) {
385
+ const inner = node.expressions[0];
386
+ if (inner != null && containsVersus(inner))
387
+ Parser.throwVersusMetaExpression(token);
388
+ }
389
+ }
390
+ parseKeepDrop(target, token) {
391
+ this.rejectSuccessCountTarget(target, token);
392
+ this.rejectVersusTarget(target, token);
393
+ if (!containsDicePool(target)) {
394
+ throw new ParseError(`Keep/drop modifiers require a dice pool target`, 'INVALID_KEEP_DROP_TARGET', token.position, token);
395
+ }
396
+ const kind = token.type === TokenType.KEEP_HIGH || token.type === TokenType.KEEP_LOW ? 'keep' : 'drop';
397
+ const selector = token.type === TokenType.KEEP_HIGH || token.type === TokenType.DROP_HIGH
398
+ ? 'highest'
399
+ : 'lowest';
400
+ const nextToken = this.peek().type;
401
+ const hasExplicitCount = nextToken === TokenType.NUMBER ||
402
+ nextToken === TokenType.LPAREN ||
403
+ nextToken === TokenType.AT;
404
+ const count = hasExplicitCount
405
+ ? this.parseExpression(BP.DICE_LEFT)
406
+ : Parser.syntheticLiteral(1, token);
407
+ this.rejectSuccessCountTarget(count, token);
408
+ this.rejectVersusMetaOperand(count, token);
409
+ return {
410
+ type: 'KeepDrop',
411
+ kind,
412
+ selector,
413
+ count,
414
+ target,
415
+ start: target.start ?? token.position,
416
+ end: hasExplicitCount ? (count.end ?? token.end) : token.end,
417
+ };
418
+ }
419
+ parseExplode(target, token) {
420
+ this.rejectSuccessCountTarget(target, token);
421
+ this.rejectGroupTarget(target, token, 'explode', 'INVALID_EXPLODE_TARGET');
422
+ if (!containsDicePool(target)) {
423
+ throw new ParseError(`Explode modifier requires a dice pool target`, 'INVALID_EXPLODE_TARGET', token.position, token);
424
+ }
425
+ if (containsFatePool(target)) {
426
+ throw new ParseError(`Fate dice cannot explode`, 'INVALID_EXPLODE_TARGET', token.position, token);
427
+ }
428
+ if (target.type === 'Explode') {
429
+ throw new ParseError(`Cannot chain explode modifiers`, 'INVALID_EXPLODE_TARGET', token.position, token);
430
+ }
431
+ const variant = EXPLODE_VARIANTS[token.type] ?? 'penetrating';
432
+ const start = target.start ?? token.position;
433
+ if (!this.isComparePointAhead()) {
434
+ return { type: 'Explode', variant, target, start, end: token.end };
435
+ }
436
+ const threshold = this.parseComparePoint();
437
+ return {
438
+ type: 'Explode',
439
+ variant,
440
+ target,
441
+ threshold,
442
+ start,
443
+ end: threshold.value.end ?? token.end,
444
+ };
445
+ }
446
+ parseReroll(target, token) {
447
+ this.rejectSuccessCountTarget(target, token);
448
+ this.rejectGroupTarget(target, token, 'reroll', 'INVALID_REROLL_TARGET');
449
+ if (!containsDicePool(target)) {
450
+ throw new ParseError(`Reroll modifier requires a dice pool target`, 'INVALID_REROLL_TARGET', token.position, token);
451
+ }
452
+ if (!this.isComparePointAhead()) {
453
+ throw new ParseError(`Expected comparison operator after '${token.value}'`, 'EXPECTED_TOKEN', token.position, token);
454
+ }
455
+ const once = token.type === TokenType.REROLL_ONCE;
456
+ const condition = this.parseComparePoint();
457
+ return {
458
+ type: 'Reroll',
459
+ once,
460
+ condition,
461
+ target,
462
+ start: target.start ?? token.position,
463
+ end: condition.value.end ?? token.end,
464
+ };
465
+ }
466
+ parseDieBound(target, token) {
467
+ this.rejectSuccessCountTarget(target, token);
468
+ this.rejectVersusTarget(target, token);
469
+ this.rejectGroupTarget(target, token, `apply '${token.value}' to`, 'INVALID_DIE_BOUND_TARGET');
470
+ if (!containsDicePool(target)) {
471
+ throw new ParseError(`Die bound modifier requires a dice pool target`, 'INVALID_DIE_BOUND_TARGET', token.position, token);
472
+ }
473
+ if (containsFatePool(target)) {
474
+ throw new ParseError(`Fate dice cannot be clamped`, 'INVALID_DIE_BOUND_TARGET', token.position, token);
475
+ }
476
+ const nextToken = this.peek().type;
477
+ const hasExplicitValue = nextToken === TokenType.NUMBER ||
478
+ nextToken === TokenType.LPAREN ||
479
+ nextToken === TokenType.AT;
480
+ if (!hasExplicitValue) {
481
+ throw new ParseError(`Expected value after '${token.value}'`, 'EXPECTED_TOKEN', token.position, token);
482
+ }
483
+ const value = this.parseExpression(BP.DICE_LEFT);
484
+ this.rejectSuccessCountTarget(value, token);
485
+ this.rejectVersusMetaOperand(value, token);
486
+ return {
487
+ type: 'DieBound',
488
+ bound: token.value === 'min' ? 'min' : 'max',
489
+ value,
490
+ target,
491
+ start: target.start ?? token.position,
492
+ end: value.end ?? token.end,
493
+ };
494
+ }
495
+ parseSort(target, token) {
496
+ this.rejectSuccessCountTarget(target, token);
497
+ this.rejectVersusTarget(target, token);
498
+ if (!deepContainsDicePool(target)) {
499
+ throw new ParseError(`Sort modifier requires a dice pool target`, 'INVALID_SORT_TARGET', token.position, token);
500
+ }
501
+ const base = unwrapAllTransparent(target);
502
+ if (base.type === 'Group' && base.expressions.length >= 2) {
503
+ throw new ParseError(`Sort modifier does not yet support multi-sub-roll groups`, 'INVALID_SORT_TARGET', token.position, token);
504
+ }
505
+ const order = token.type === TokenType.SORT_ASC ? 'ascending' : 'descending';
506
+ return {
507
+ type: 'Sort',
508
+ order,
509
+ target,
510
+ start: target.start ?? token.position,
511
+ end: token.end,
512
+ };
513
+ }
514
+ parseCritThreshold(target, token) {
515
+ this.rejectSuccessCountTarget(target, token);
516
+ this.rejectVersusTarget(target, token);
517
+ this.rejectGroupTarget(target, token, 'apply crit threshold to', 'INVALID_CRIT_THRESHOLD_TARGET', true);
518
+ if (!containsDicePool(target)) {
519
+ throw new ParseError(`Crit threshold modifier requires a dice pool target`, 'INVALID_CRIT_THRESHOLD_TARGET', token.position, token);
520
+ }
521
+ if (!this.isComparePointAhead() && containsFatePool(target)) {
522
+ throw new ParseError(`Bare cs/cf cannot apply to Fate dice`, 'INVALID_CRIT_THRESHOLD_TARGET', token.position, token);
523
+ }
524
+ const threshold = this.isComparePointAhead()
525
+ ? this.parseComparePoint()
526
+ : 'default';
527
+ const end = threshold === 'default' ? token.end : (threshold.value.end ?? token.end);
528
+ let chainTarget = target;
529
+ while (chainTarget.type === 'Grouped') {
530
+ chainTarget = chainTarget.expression;
531
+ }
532
+ if (isCritThreshold(chainTarget)) {
533
+ const isSuccess = token.type === TokenType.CRIT_SUCCESS;
534
+ return {
535
+ ...chainTarget,
536
+ successThresholds: isSuccess
537
+ ? [...chainTarget.successThresholds, threshold]
538
+ : chainTarget.successThresholds,
539
+ failThresholds: isSuccess
540
+ ? chainTarget.failThresholds
541
+ : [...chainTarget.failThresholds, threshold],
542
+ end,
543
+ };
544
+ }
545
+ return {
546
+ type: 'CritThreshold',
547
+ successThresholds: token.type === TokenType.CRIT_SUCCESS ? [threshold] : [],
548
+ failThresholds: token.type === TokenType.CRIT_FAIL ? [threshold] : [],
549
+ target,
550
+ start: target.start ?? token.position,
551
+ end,
552
+ };
553
+ }
554
+ parseSuccessCount(target, token) {
555
+ this.rejectSuccessCountTarget(target, token);
556
+ if (!containsDicePool(target)) {
557
+ throw new ParseError(`Success counting requires a dice pool target`, 'INVALID_SUCCESS_COUNT_TARGET', token.position, token);
558
+ }
559
+ const operator = this.getCompareOp(token);
560
+ const value = this.parseExpression(BP.DICE_LEFT);
561
+ this.rejectSuccessCountTarget(value, token);
562
+ this.rejectVersusMetaOperand(value, token);
563
+ const start = target.start ?? token.position;
564
+ const end = value.end ?? token.end;
565
+ if (this.peek().type !== TokenType.FAIL) {
566
+ return { type: 'SuccessCount', target, threshold: { operator, value }, start, end };
567
+ }
568
+ this.advance();
569
+ const failThreshold = this.parseFailThreshold(token);
570
+ return {
571
+ type: 'SuccessCount',
572
+ target,
573
+ threshold: { operator, value },
574
+ failThreshold,
575
+ start,
576
+ end: failThreshold.value.end ?? end,
577
+ };
578
+ }
579
+ parseFailThreshold(token) {
580
+ if (this.isComparePointAhead())
581
+ return this.parseComparePoint();
582
+ const failValue = this.parseExpression(BP.DICE_LEFT);
583
+ this.rejectSuccessCountTarget(failValue, token);
584
+ this.rejectVersusMetaOperand(failValue, token);
585
+ return { operator: '=', value: failValue };
586
+ }
587
+ parseVersus(left, token) {
588
+ this.rejectSuccessCountTarget(left, token);
589
+ let leftChain = left;
590
+ while (leftChain.type === 'Grouped') {
591
+ leftChain = leftChain.expression;
592
+ }
593
+ if (leftChain.type === 'Versus') {
594
+ throw new ParseError('Cannot chain versus operators', 'NESTED_VERSUS', token.position, token);
595
+ }
596
+ const dc = this.parseExpression(BP.VS_RIGHT);
597
+ this.rejectSuccessCountTarget(dc, token);
598
+ return {
599
+ type: 'Versus',
600
+ roll: left,
601
+ dc,
602
+ start: left.start ?? token.position,
603
+ end: dc.end ?? token.end,
604
+ };
605
+ }
606
+ isComparePointAhead() {
607
+ const type = this.peek().type;
608
+ return (type === TokenType.GREATER ||
609
+ type === TokenType.GREATER_EQUAL ||
610
+ type === TokenType.LESS ||
611
+ type === TokenType.LESS_EQUAL ||
612
+ type === TokenType.EQUAL);
613
+ }
614
+ parseComparePoint() {
615
+ const token = this.peek();
616
+ const operator = this.getCompareOp(token);
617
+ this.advance();
618
+ const value = this.parseExpression(BP.DICE_LEFT);
619
+ this.rejectSuccessCountTarget(value, token);
620
+ this.rejectVersusMetaOperand(value, token);
621
+ return { operator, value };
622
+ }
623
+ getCompareOp(token) {
624
+ switch (token.type) {
625
+ case TokenType.GREATER:
626
+ return '>';
627
+ case TokenType.GREATER_EQUAL:
628
+ return '>=';
629
+ case TokenType.LESS:
630
+ return '<';
631
+ case TokenType.LESS_EQUAL:
632
+ return '<=';
633
+ case TokenType.EQUAL:
634
+ return '=';
635
+ default:
636
+ throw new ParseError(`Expected comparison operator but got '${token.value}'`, 'EXPECTED_TOKEN', token.position, token);
637
+ }
638
+ }
639
+ getOperatorSymbol(token) {
640
+ switch (token.type) {
641
+ case TokenType.PLUS:
642
+ return '+';
643
+ case TokenType.MINUS:
644
+ return '-';
645
+ case TokenType.MULTIPLY:
646
+ return '*';
647
+ case TokenType.DIVIDE:
648
+ return '/';
649
+ case TokenType.MODULO:
650
+ return '%';
651
+ case TokenType.POWER:
652
+ return '**';
653
+ default:
654
+ throw new ParseError(`Unknown operator '${token.value}'`, 'UNEXPECTED_TOKEN', token.position, token);
655
+ }
656
+ }
657
+ getLeftBp(token) {
658
+ switch (token.type) {
659
+ case TokenType.VS:
660
+ return BP.VS_LEFT;
661
+ case TokenType.PLUS:
662
+ case TokenType.MINUS:
663
+ return BP.ADD_LEFT;
664
+ case TokenType.MULTIPLY:
665
+ case TokenType.DIVIDE:
666
+ case TokenType.MODULO:
667
+ return BP.MUL_LEFT;
668
+ case TokenType.POWER:
669
+ return BP.POW_LEFT;
670
+ case TokenType.DICE:
671
+ case TokenType.DICE_PERCENT:
672
+ case TokenType.DICE_FATE:
673
+ return BP.DICE_LEFT;
674
+ case TokenType.KEEP_HIGH:
675
+ case TokenType.KEEP_LOW:
676
+ case TokenType.DROP_HIGH:
677
+ case TokenType.DROP_LOW:
678
+ case TokenType.EXPLODE:
679
+ case TokenType.EXPLODE_COMPOUND:
680
+ case TokenType.EXPLODE_PENETRATING:
681
+ case TokenType.REROLL:
682
+ case TokenType.REROLL_ONCE:
683
+ case TokenType.SORT_ASC:
684
+ case TokenType.SORT_DESC:
685
+ case TokenType.CRIT_SUCCESS:
686
+ case TokenType.CRIT_FAIL:
687
+ return BP.MODIFIER;
688
+ case TokenType.GREATER:
689
+ case TokenType.GREATER_EQUAL:
690
+ case TokenType.LESS:
691
+ case TokenType.LESS_EQUAL:
692
+ case TokenType.EQUAL:
693
+ return BP.COMPARE;
694
+ case TokenType.FUNCTION:
695
+ return token.value === 'min' || token.value === 'max' ? BP.MODIFIER : -1;
696
+ case TokenType.RPAREN:
697
+ case TokenType.EOF:
698
+ case TokenType.COMMA:
699
+ case TokenType.LBRACE:
700
+ case TokenType.RBRACE:
701
+ return -1;
702
+ default:
703
+ return 0;
704
+ }
705
+ }
706
+ getRightBp(token) {
707
+ switch (token.type) {
708
+ case TokenType.VS:
709
+ return BP.VS_RIGHT;
710
+ case TokenType.PLUS:
711
+ case TokenType.MINUS:
712
+ return BP.ADD_RIGHT;
713
+ case TokenType.MULTIPLY:
714
+ case TokenType.DIVIDE:
715
+ case TokenType.MODULO:
716
+ return BP.MUL_RIGHT;
717
+ case TokenType.POWER:
718
+ return BP.POW_RIGHT;
719
+ case TokenType.DICE:
720
+ return BP.DICE_RIGHT;
721
+ default:
722
+ return 0;
723
+ }
724
+ }
725
+ peek() {
726
+ return (this.tokens[this.pos] ?? { type: TokenType.EOF, value: '', position: this.pos, end: this.pos });
727
+ }
728
+ advance() {
729
+ const token = this.peek();
730
+ this.pos++;
731
+ return token;
732
+ }
733
+ expect(type) {
734
+ const token = this.peek();
735
+ if (token.type !== type) {
736
+ const expected = TOKEN_DISPLAY[type];
737
+ const got = token.type === TokenType.EOF ? 'end of input' : `'${token.value}'`;
738
+ throw new ParseError(`Expected ${expected} but got ${got}`, 'EXPECTED_TOKEN', token.position, token);
739
+ }
740
+ return this.advance();
741
+ }
742
+ hasTokens() {
743
+ return this.peek().type !== TokenType.EOF;
744
+ }
745
+ }
746
+ export function parse(notation) {
747
+ const tokens = lex(notation);
748
+ const parser = new Parser(tokens);
749
+ return parser.parse();
750
+ }
751
+ //# sourceMappingURL=parser.js.map