desmost 0.9.1 → 0.10.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.
- package/README.md +12 -3
- package/dist/compiler/compile.d.ts +1 -7
- package/dist/compiler/compile.d.ts.map +1 -1
- package/dist/compiler/compile.js +3 -94
- package/dist/compiler/compile.js.map +1 -1
- package/dist/compiler/compiler.d.ts +56 -0
- package/dist/compiler/compiler.d.ts.map +1 -0
- package/dist/compiler/compiler.js +209 -0
- package/dist/compiler/compiler.js.map +1 -0
- package/dist/compiler/format.d.ts +2 -2
- package/dist/compiler/format.d.ts.map +1 -1
- package/dist/compiler/format.js +18 -9
- package/dist/compiler/format.js.map +1 -1
- package/dist/decompiler/decompile.d.ts +1 -1
- package/dist/decompiler/decompile.d.ts.map +1 -1
- package/dist/decompiler/extract.d.ts.map +1 -1
- package/dist/decompiler/extract.js +7 -3
- package/dist/decompiler/extract.js.map +1 -1
- package/dist/errors.d.ts +35 -27
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +46 -48
- package/dist/errors.js.map +1 -1
- package/dist/index.internal.d.ts +1 -1
- package/dist/index.internal.d.ts.map +1 -1
- package/dist/magic/global/desmos.d.ts +2 -2
- package/dist/magic/global/desmos.d.ts.map +1 -1
- package/dist/magic/global/desmos.js +3 -2
- package/dist/magic/global/desmos.js.map +1 -1
- package/dist/magic/global/viewport.d.ts +2 -2
- package/dist/magic/global/viewport.d.ts.map +1 -1
- package/dist/magic/global/viewport.js +5 -5
- package/dist/magic/global/viewport.js.map +1 -1
- package/dist/magic/incantation.d.ts +4 -4
- package/dist/magic/incantation.d.ts.map +1 -1
- package/dist/magic/incantation.js +15 -8
- package/dist/magic/incantation.js.map +1 -1
- package/dist/magic/local/colour.d.ts +3 -1
- package/dist/magic/local/colour.d.ts.map +1 -1
- package/dist/magic/local/colour.js +15 -11
- package/dist/magic/local/colour.js.map +1 -1
- package/dist/magic/local/label.d.ts.map +1 -1
- package/dist/magic/local/label.js +7 -7
- package/dist/magic/local/label.js.map +1 -1
- package/dist/magic/local/line.d.ts +2 -2
- package/dist/magic/local/line.d.ts.map +1 -1
- package/dist/magic/local/line.js +0 -1
- package/dist/magic/local/line.js.map +1 -1
- package/dist/options.d.ts +57 -23
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +2 -1
- package/dist/options.js.map +1 -1
- package/dist/parser/ast.d.ts +3 -17
- package/dist/parser/ast.d.ts.map +1 -1
- package/dist/parser/ast.js +0 -2
- package/dist/parser/ast.js.map +1 -1
- package/dist/parser/desmost-parser.d.ts +33 -22
- package/dist/parser/desmost-parser.d.ts.map +1 -1
- package/dist/parser/desmost-parser.js +114 -100
- package/dist/parser/desmost-parser.js.map +1 -1
- package/dist/parser/generic-parser.d.ts +23 -14
- package/dist/parser/generic-parser.d.ts.map +1 -1
- package/dist/parser/generic-parser.js +67 -27
- package/dist/parser/generic-parser.js.map +1 -1
- package/dist/utils.d.ts +9 -2
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +13 -2
- package/dist/utils.js.map +1 -1
- package/package.json +5 -2
- package/src/compiler/compile.ts +7 -120
- package/src/compiler/compiler.ts +275 -0
- package/src/compiler/format.ts +19 -7
- package/src/decompiler/decompile.ts +1 -1
- package/src/decompiler/extract.ts +3 -1
- package/src/errors.ts +40 -24
- package/src/index.internal.ts +1 -1
- package/src/magic/.template.ts +2 -2
- package/src/magic/global/desmos.ts +7 -9
- package/src/magic/global/viewport.ts +11 -17
- package/src/magic/incantation.ts +21 -22
- package/src/magic/local/colour.ts +23 -13
- package/src/magic/local/label.ts +11 -17
- package/src/magic/local/line.ts +2 -2
- package/src/options.ts +81 -47
- package/src/parser/ast.ts +1 -21
- package/src/parser/desmost-parser.ts +139 -143
- package/src/parser/generic-parser.ts +82 -40
- package/src/utils.ts +25 -3
- package/dist/compiler/evaluate.d.ts +0 -18
- package/dist/compiler/evaluate.d.ts.map +0 -1
- package/dist/compiler/evaluate.js +0 -93
- package/dist/compiler/evaluate.js.map +0 -1
- package/dist/desmos.d.ts +0 -16
- package/dist/desmos.d.ts.map +0 -1
- package/dist/desmos.js +0 -19
- package/dist/desmos.js.map +0 -1
- package/src/compiler/evaluate.ts +0 -128
- package/src/desmos.ts +0 -22
|
@@ -2,8 +2,8 @@ import { GenericParser } from "./generic-parser";
|
|
|
2
2
|
import { Ast } from "./ast";
|
|
3
3
|
|
|
4
4
|
import type { DesmostOptions } from "../options";
|
|
5
|
-
import { NO_MATCH,
|
|
6
|
-
import type { NoMatch,
|
|
5
|
+
import { NO_MATCH, DesmostError } from "../errors";
|
|
6
|
+
import type { NoMatch, Fallible } from "../errors";
|
|
7
7
|
import * as utils from "../utils";
|
|
8
8
|
|
|
9
9
|
import {
|
|
@@ -27,56 +27,79 @@ export class DesmostParser extends GenericParser
|
|
|
27
27
|
// == TOP-LEVEL == //
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Parse the next semantic block of source code
|
|
30
|
+
* Parse the next semantic block of source code.
|
|
31
|
+
*
|
|
32
|
+
* This returns:
|
|
33
|
+
*
|
|
34
|
+
* ```ts
|
|
35
|
+
* // an AST block(s) + any errors encountered
|
|
36
|
+
* { blocks: Ast[], errors: [...] }
|
|
37
|
+
*
|
|
38
|
+
* // ignored block + any errors encountered
|
|
39
|
+
* { blocks: [], errors: [...] }
|
|
40
|
+
*
|
|
41
|
+
* // parser has reached the end of the source
|
|
42
|
+
* undefined
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* `block: null` could be ignored content like comments, or it could be a parse that couldn't recover and so failed to produce a block.
|
|
31
46
|
*/
|
|
32
|
-
public parse_next():
|
|
47
|
+
public parse_next():
|
|
48
|
+
Fallible<{ blocks: Ast[], errors: DesmostError[] } | undefined>
|
|
33
49
|
{
|
|
34
|
-
this.
|
|
50
|
+
this.errors = [];
|
|
51
|
+
|
|
52
|
+
this.eat_whitespace();
|
|
35
53
|
|
|
36
54
|
if (this.out_of_bounds()) {
|
|
37
|
-
return
|
|
55
|
+
return undefined;
|
|
38
56
|
}
|
|
39
57
|
|
|
58
|
+
let blocks = [];
|
|
59
|
+
|
|
40
60
|
if (this.current === "%") {
|
|
61
|
+
// comment
|
|
41
62
|
if (this.options?.ignore_comments) {
|
|
42
|
-
// ignore comment
|
|
43
63
|
this.parse_line();
|
|
44
|
-
this.consume_end_of_block();
|
|
45
|
-
return this.parse_next();
|
|
46
64
|
}
|
|
47
65
|
else {
|
|
48
|
-
|
|
49
|
-
var expr = this.parse_comment();
|
|
66
|
+
blocks.push(this.parse_comment());
|
|
50
67
|
}
|
|
51
68
|
}
|
|
52
69
|
else if (this.current === "/") {
|
|
53
70
|
let r = this.parse_pre_sep();
|
|
54
71
|
|
|
55
|
-
// 1 global
|
|
56
72
|
if ("global" in r) {
|
|
57
|
-
|
|
73
|
+
// 1 global
|
|
74
|
+
if (r.global !== null) {
|
|
75
|
+
blocks.push(r.global);
|
|
76
|
+
}
|
|
58
77
|
}
|
|
78
|
+
else {
|
|
79
|
+
// 1+ locals + 1 expr
|
|
80
|
+
let incantations = r.locals;
|
|
59
81
|
|
|
60
|
-
|
|
61
|
-
|
|
82
|
+
if (incantations.length > 0) {
|
|
83
|
+
this.parse_sep();
|
|
84
|
+
}
|
|
62
85
|
|
|
63
|
-
|
|
64
|
-
this.parse_sep();
|
|
65
|
-
}
|
|
86
|
+
let block = this.parse_post_sep();
|
|
66
87
|
|
|
67
|
-
|
|
88
|
+
for (let invocation of incantations) {
|
|
89
|
+
block.incantations.push(invocation);
|
|
90
|
+
}
|
|
68
91
|
|
|
69
|
-
|
|
70
|
-
expr.incantations.push(invocation);
|
|
92
|
+
blocks.push(block);
|
|
71
93
|
}
|
|
72
94
|
}
|
|
73
95
|
else {
|
|
74
96
|
// 1 expr
|
|
75
|
-
|
|
97
|
+
blocks.push(this.parse_post_sep());
|
|
76
98
|
}
|
|
77
99
|
|
|
78
|
-
this.
|
|
79
|
-
|
|
100
|
+
this.eat_end_of_block();
|
|
101
|
+
|
|
102
|
+
return { blocks, errors: this.errors };
|
|
80
103
|
}
|
|
81
104
|
|
|
82
105
|
/**
|
|
@@ -86,44 +109,82 @@ export class DesmostParser extends GenericParser
|
|
|
86
109
|
* - 1+ local incantations
|
|
87
110
|
*/
|
|
88
111
|
parse_pre_sep():
|
|
89
|
-
|
|
90
|
-
| { global: Ast.IncantationInvocation<GLOBAL> |
|
|
91
|
-
| {
|
|
112
|
+
Fallible<
|
|
113
|
+
| { global: Ast.IncantationInvocation<GLOBAL> | null }
|
|
114
|
+
| { locals: Ast.IncantationInvocation<LOCAL>[] }
|
|
92
115
|
>
|
|
93
116
|
{
|
|
94
117
|
// 1 global incantation
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (r !== NO_MATCH) {
|
|
98
|
-
this.consume_end_of_block(
|
|
99
|
-
`Received excess input after global incantation /${r.incantation.identifier}`
|
|
100
|
-
);
|
|
118
|
+
try {
|
|
119
|
+
let r = this.try_parse_incantation(GLOBAL_INCANTATIONS);
|
|
101
120
|
|
|
102
|
-
|
|
121
|
+
if (r !== NO_MATCH) {
|
|
122
|
+
this.eat_whitespace();
|
|
123
|
+
return { global: r };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
this.errors.push(e as Error);
|
|
128
|
+
return { global: null };
|
|
103
129
|
}
|
|
104
130
|
|
|
105
131
|
// 1+ local incantations
|
|
106
132
|
let incantations = [];
|
|
107
133
|
|
|
108
134
|
while (this.current === "/") {
|
|
109
|
-
|
|
135
|
+
try {
|
|
136
|
+
var invocation = this.try_parse_incantation(LOCAL_INCANTATIONS);
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
this.errors.push(e as Error);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
110
143
|
if (invocation === NO_MATCH) break;
|
|
111
144
|
incantations.push(invocation);
|
|
145
|
+
this.eat_newlines();
|
|
112
146
|
}
|
|
113
147
|
|
|
114
|
-
return {
|
|
148
|
+
return { locals: incantations };
|
|
115
149
|
}
|
|
116
150
|
|
|
117
151
|
/**
|
|
118
152
|
* Parse the Desmost `::` separator.
|
|
119
153
|
*/
|
|
120
|
-
parse_sep():
|
|
154
|
+
parse_sep(): Fallible<void>
|
|
121
155
|
{
|
|
122
|
-
this.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
156
|
+
this.eat_newlines();
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
this.eat("::", {
|
|
160
|
+
msg: `Expected \`::\` separator between local incantations and expression, but found: \`${this.preview()}\``,
|
|
161
|
+
hint: `Use \`/incantation :: ${this.preview()}\``,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
if (!this.peek_line().includes("::")) throw e;
|
|
166
|
+
|
|
167
|
+
let init = this.i;
|
|
168
|
+
|
|
169
|
+
while (this.current !== ":" || this.peek() !== ":") {
|
|
170
|
+
this.advance();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const peek = 7;
|
|
174
|
+
const start = "...".length + Math.round(1.5 * peek);
|
|
175
|
+
|
|
176
|
+
this.errors.push(new DesmostError.ExcessInput({
|
|
177
|
+
msg: `While consuming \`::\` separator`,
|
|
178
|
+
show: {
|
|
179
|
+
text: `...${this.source.slice(init - peek, this.i + peek)}...`,
|
|
180
|
+
span: new utils.Range(start, start + this.i - init)
|
|
181
|
+
}
|
|
182
|
+
}));
|
|
183
|
+
|
|
184
|
+
this.eat("::");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.eat_newlines();
|
|
127
188
|
}
|
|
128
189
|
|
|
129
190
|
/**
|
|
@@ -132,9 +193,9 @@ export class DesmostParser extends GenericParser
|
|
|
132
193
|
* - 1 line of LaTeX
|
|
133
194
|
* - 1 expression incantation
|
|
134
195
|
*/
|
|
135
|
-
parse_post_sep():
|
|
196
|
+
parse_post_sep(): Fallible<Ast.Expression>
|
|
136
197
|
{
|
|
137
|
-
this.
|
|
198
|
+
this.eat_whitespace();
|
|
138
199
|
|
|
139
200
|
switch (this.current) {
|
|
140
201
|
// empty block
|
|
@@ -161,7 +222,7 @@ export class DesmostParser extends GenericParser
|
|
|
161
222
|
}
|
|
162
223
|
}
|
|
163
224
|
|
|
164
|
-
parse_comment():
|
|
225
|
+
parse_comment(): Fallible<Ast.Expression>
|
|
165
226
|
{
|
|
166
227
|
this.advance();
|
|
167
228
|
let text = this.parse_line();
|
|
@@ -179,7 +240,7 @@ export class DesmostParser extends GenericParser
|
|
|
179
240
|
/**
|
|
180
241
|
* Parse a single line of arbitrary text.
|
|
181
242
|
*/
|
|
182
|
-
parse_line():
|
|
243
|
+
parse_line(): Fallible<string>
|
|
183
244
|
{
|
|
184
245
|
let init = this.i;
|
|
185
246
|
|
|
@@ -191,64 +252,24 @@ export class DesmostParser extends GenericParser
|
|
|
191
252
|
}
|
|
192
253
|
|
|
193
254
|
/**
|
|
194
|
-
* Attempt to parse
|
|
255
|
+
* Attempt to parse an incantation invocation allowed by `incantations`.
|
|
195
256
|
*/
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
| NoMatch
|
|
257
|
+
try_parse_incantation<Effect extends Incantation.Effect>(
|
|
258
|
+
incantations: Incantation<Effect>[],
|
|
259
|
+
): Fallible<Ast.IncantationInvocation<Effect> | NoMatch>
|
|
200
260
|
{
|
|
201
261
|
let init = this.i;
|
|
202
262
|
|
|
203
|
-
if (this.
|
|
263
|
+
if (this.try_eat("/") === NO_MATCH) return NO_MATCH;
|
|
204
264
|
|
|
205
|
-
let
|
|
206
|
-
if (
|
|
265
|
+
let ident = this.try_eat_identifier();
|
|
266
|
+
if (ident === NO_MATCH) {
|
|
207
267
|
this.i = init;
|
|
208
268
|
return NO_MATCH;
|
|
209
269
|
}
|
|
210
270
|
|
|
211
|
-
let
|
|
212
|
-
|
|
213
|
-
if (incantation instanceof ArgIncantation) {
|
|
214
|
-
if (this.current === "{") {
|
|
215
|
-
data = this.parse_incantation_arg(incantation.arg_type);
|
|
216
|
-
}
|
|
217
|
-
else if (incantation.requires_arg) {
|
|
218
|
-
return {
|
|
219
|
-
kind: Ast.Kind.INVALID_INCANTATION,
|
|
220
|
-
incantation,
|
|
221
|
-
error: new UnrecoverableError.MissingInput(
|
|
222
|
-
`No argument provided for /${incantation.identifier}, which requires an argument of type: \`${incantation.arg_type}\``
|
|
223
|
-
)
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
this.consume_spaces();
|
|
229
|
-
|
|
230
|
-
// @ts-expect-error: FIXME
|
|
231
|
-
return {
|
|
232
|
-
kind: Ast.Kind.INCANTATION_INVOCATION,
|
|
233
|
-
incantation,
|
|
234
|
-
arg_raw: data,
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Attempt to parse a local incantation invocation.
|
|
240
|
-
*/
|
|
241
|
-
try_parse_local_incantation():
|
|
242
|
-
| Ast.IncantationInvocation<LOCAL>
|
|
243
|
-
| Ast.InvalidInvocation
|
|
244
|
-
| NoMatch
|
|
245
|
-
{
|
|
246
|
-
let init = this.i;
|
|
247
|
-
|
|
248
|
-
if (this.try_consume("/") === NO_MATCH) return NO_MATCH;
|
|
249
|
-
|
|
250
|
-
let incantation = this.try_parse_identifier(LOCAL_INCANTATIONS);
|
|
251
|
-
if (incantation === NO_MATCH) {
|
|
271
|
+
let incantation = incantations.find(inc => inc.identifier === ident || inc.alias === ident);
|
|
272
|
+
if (incantation == undefined) {
|
|
252
273
|
this.i = init;
|
|
253
274
|
return NO_MATCH;
|
|
254
275
|
}
|
|
@@ -260,19 +281,13 @@ export class DesmostParser extends GenericParser
|
|
|
260
281
|
arg_raw = this.parse_incantation_arg(incantation.arg_type);
|
|
261
282
|
}
|
|
262
283
|
else if (incantation.requires_arg) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
incantation
|
|
266
|
-
|
|
267
|
-
`No argument provided for /${incantation.identifier}, which requires an argument of type: \`${incantation.arg_type}\``
|
|
268
|
-
)
|
|
269
|
-
};
|
|
284
|
+
throw new DesmostError.MissingInput({
|
|
285
|
+
msg: `No argument provided for /${incantation.identifier}`,
|
|
286
|
+
hint: `/${incantation.identifier} requires an argument of type: \`${incantation.arg_type}\``,
|
|
287
|
+
});
|
|
270
288
|
}
|
|
271
289
|
}
|
|
272
290
|
|
|
273
|
-
this.consume_whitespace();
|
|
274
|
-
|
|
275
|
-
// @ts-expect-error: FIXME
|
|
276
291
|
return {
|
|
277
292
|
kind: Ast.Kind.INCANTATION_INVOCATION,
|
|
278
293
|
incantation,
|
|
@@ -283,19 +298,25 @@ export class DesmostParser extends GenericParser
|
|
|
283
298
|
/**
|
|
284
299
|
* Attempt to parse an expression incantation invocation.
|
|
285
300
|
*/
|
|
286
|
-
try_parse_expr_incantation():
|
|
301
|
+
try_parse_expr_incantation(): Fallible<Ast.Expression | NoMatch>
|
|
287
302
|
{
|
|
288
303
|
let init = this.i;
|
|
289
304
|
|
|
290
|
-
if (this.
|
|
305
|
+
if (this.try_eat("/") === NO_MATCH) return NO_MATCH;
|
|
306
|
+
|
|
307
|
+
let ident = this.try_eat_identifier();
|
|
308
|
+
if (ident === NO_MATCH) {
|
|
309
|
+
this.i = init;
|
|
310
|
+
return NO_MATCH;
|
|
311
|
+
}
|
|
291
312
|
|
|
292
|
-
let incantation =
|
|
293
|
-
if (incantation
|
|
294
|
-
// TODO maybe flag to user
|
|
313
|
+
let incantation = EXPR_INCANTATIONS.find(inc => inc.identifier === ident || inc.alias === ident);
|
|
314
|
+
if (incantation == undefined) {
|
|
295
315
|
this.i = init;
|
|
296
316
|
return NO_MATCH;
|
|
297
317
|
}
|
|
298
318
|
|
|
319
|
+
/* NOTE: All expression incantations currently require arguments, and all for the foreseeable future will too */
|
|
299
320
|
let arg_raw = this.parse_incantation_arg((incantation as ArgIncantation<EXPR>).arg_type);
|
|
300
321
|
|
|
301
322
|
let data = {};
|
|
@@ -308,27 +329,6 @@ export class DesmostParser extends GenericParser
|
|
|
308
329
|
};
|
|
309
330
|
}
|
|
310
331
|
|
|
311
|
-
/**
|
|
312
|
-
* Attempt to parse an incantation identifier.
|
|
313
|
-
*/
|
|
314
|
-
try_parse_identifier<Effect extends Incantation.Effect>(
|
|
315
|
-
incantations: Incantation<Effect>[]
|
|
316
|
-
): Incantation<Effect> | NoMatch
|
|
317
|
-
{
|
|
318
|
-
for (let incantation of incantations) {
|
|
319
|
-
// yeah the duplication here is a little meh, unfortunately needing `return` means we can't extract it into a helper
|
|
320
|
-
let r = this.try_consume(incantation.identifier);
|
|
321
|
-
if (r !== NO_MATCH) return incantation;
|
|
322
|
-
|
|
323
|
-
if (incantation.alias != undefined) {
|
|
324
|
-
let r = this.try_consume(incantation.alias);
|
|
325
|
-
if (r !== NO_MATCH) return incantation;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
return NO_MATCH;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
332
|
/**
|
|
333
333
|
* Parse an argument to an incantation, enclosed in `{}`.
|
|
334
334
|
*
|
|
@@ -394,13 +394,13 @@ export class DesmostParser extends GenericParser
|
|
|
394
394
|
* - JavaScript object: The above, plus balanced string quotes.
|
|
395
395
|
*/
|
|
396
396
|
arg_type: Incantation.ArgType,
|
|
397
|
-
):
|
|
397
|
+
): Fallible<string>
|
|
398
398
|
{
|
|
399
399
|
let init = this.i;
|
|
400
400
|
|
|
401
|
-
this.
|
|
402
|
-
`Expected \`{\` to start incantation argument, but found: \`${this.preview()}\``
|
|
403
|
-
);
|
|
401
|
+
this.eat("{", {
|
|
402
|
+
msg: `Expected \`{\` to start incantation argument, but found: \`${this.preview()}\``
|
|
403
|
+
});
|
|
404
404
|
|
|
405
405
|
/** The context stack to keep track of when the closing `}` is encountered. */
|
|
406
406
|
let stack = new ContextStack();
|
|
@@ -412,9 +412,7 @@ export class DesmostParser extends GenericParser
|
|
|
412
412
|
// If the last character was an escape, we don't care at all what the next character is!
|
|
413
413
|
// This handles \\ double escape perfectly fine, since the first negates the second
|
|
414
414
|
if (stack.try_pop(Ctx.ESCAPE)) {
|
|
415
|
-
this.advance(
|
|
416
|
-
`Unexpected end of input while parsing incantation argument, stack: ${stack.debug()}`
|
|
417
|
-
);
|
|
415
|
+
this.advance({ msg: `While parsing incantation argument`, debug: stack.debug() });
|
|
418
416
|
}
|
|
419
417
|
|
|
420
418
|
let top = stack.top;
|
|
@@ -433,7 +431,7 @@ export class DesmostParser extends GenericParser
|
|
|
433
431
|
stack.push(Ctx.ENUM);
|
|
434
432
|
indices_to_insert_quotes.push(this.i);
|
|
435
433
|
}
|
|
436
|
-
else if (top === Ctx.ENUM && this.current?.match(/[^a-zA-Z]/)) {
|
|
434
|
+
else if (top === Ctx.ENUM && this.current?.match(/[^a-zA-Z-_]/)) {
|
|
437
435
|
stack.pop();
|
|
438
436
|
indices_to_insert_quotes.push(this.i);
|
|
439
437
|
|
|
@@ -462,9 +460,7 @@ export class DesmostParser extends GenericParser
|
|
|
462
460
|
}
|
|
463
461
|
}
|
|
464
462
|
|
|
465
|
-
this.advance(
|
|
466
|
-
`Unexpected end of input while parsing incantation argument, stack: ${JSON.stringify(stack)}`
|
|
467
|
-
);
|
|
463
|
+
this.advance({ msg: `While parsing incantation argument`, debug: stack.debug() });
|
|
468
464
|
}
|
|
469
465
|
|
|
470
466
|
/* NOTE: Cut in by 1 on both sides to exclude {} braces */
|
|
@@ -529,7 +525,7 @@ class ContextStack
|
|
|
529
525
|
}
|
|
530
526
|
}
|
|
531
527
|
|
|
532
|
-
/**
|
|
528
|
+
/** If `ctx` is in the context stack, backtrack until it is popped, returning `true` if so. */
|
|
533
529
|
force_pop(ctx: Ctx, options?: StackOperationOptions): boolean
|
|
534
530
|
{
|
|
535
531
|
if (this.#should_skip(options)) return false;
|
|
@@ -1,27 +1,30 @@
|
|
|
1
|
-
import { NO_MATCH,
|
|
2
|
-
import type { NoMatch,
|
|
1
|
+
import { NO_MATCH, DesmostError } from "../errors";
|
|
2
|
+
import type { NoMatch, Fallible } from "../errors";
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
const IGNORED_CHARACTERS = new Set([
|
|
6
|
-
|
|
6
|
+
'\r',
|
|
7
7
|
]);
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* A stateful lazy parser, providing core (non-Desmost-specific) methods for parsing.
|
|
12
12
|
*
|
|
13
|
-
* Many methods come in `<verb>` and `try_<verb>` pairs. On parsing failure, the former throws an `
|
|
13
|
+
* Many methods come in `<verb>` and `try_<verb>` pairs. On parsing failure, the former throws an `DesmostError`, but the latter instead backtracks and only returns a `NoMatch`. The former is for expected parses, while the latter is for speculative parsing.
|
|
14
14
|
*/
|
|
15
15
|
export class GenericParser
|
|
16
16
|
{
|
|
17
|
-
/** The source code
|
|
18
|
-
protected source: string
|
|
17
|
+
/** The source code to parse. */
|
|
18
|
+
protected readonly source: string
|
|
19
19
|
|
|
20
20
|
/** The current position in the source code the parser is pointing to. */
|
|
21
|
-
protected i: number = 0
|
|
21
|
+
protected i: number = 0
|
|
22
22
|
|
|
23
23
|
/** The number of characters in the source code. */
|
|
24
|
-
protected length: number
|
|
24
|
+
protected readonly length: number
|
|
25
|
+
|
|
26
|
+
/** Accumulated errors in the current block being parsed. */
|
|
27
|
+
errors: DesmostError[]
|
|
25
28
|
|
|
26
29
|
|
|
27
30
|
/** Create a parser for parsing `source`. */
|
|
@@ -29,6 +32,7 @@ export class GenericParser
|
|
|
29
32
|
{
|
|
30
33
|
this.source = source;
|
|
31
34
|
this.length = source.length;
|
|
35
|
+
this.errors = [];
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
|
|
@@ -48,8 +52,21 @@ export class GenericParser
|
|
|
48
52
|
return this.source.at(this.i);
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
protected peek(): string | undefined
|
|
56
|
+
{
|
|
57
|
+
return this.source.at(this.i + 1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Peek the rest of the content in the current line.
|
|
62
|
+
*/
|
|
63
|
+
protected peek_line(): string
|
|
64
|
+
{
|
|
65
|
+
return this.source.slice(this.i, this.source.indexOf("\n", this.i));
|
|
66
|
+
}
|
|
67
|
+
|
|
51
68
|
/**
|
|
52
|
-
* Peek a snippet of the upcoming source text.
|
|
69
|
+
* Peek a snippet of the upcoming source text for display purposes.
|
|
53
70
|
*
|
|
54
71
|
* Optionally start from the given `idx`.
|
|
55
72
|
*/
|
|
@@ -66,14 +83,13 @@ export class GenericParser
|
|
|
66
83
|
/**
|
|
67
84
|
* Advance to the next character, skipping ignored characters.
|
|
68
85
|
*
|
|
69
|
-
* Errors if the parser is
|
|
86
|
+
* Errors if the parser is already out-of-bounds *before* advancing.
|
|
70
87
|
*/
|
|
71
|
-
protected advance(
|
|
88
|
+
protected advance(error_data?: DesmostError.Data): Fallible<void>
|
|
72
89
|
{
|
|
73
90
|
if (this.out_of_bounds()) {
|
|
74
|
-
throw new
|
|
91
|
+
throw new DesmostError.UnexpectedEnd(error_data ?? {});
|
|
75
92
|
}
|
|
76
|
-
|
|
77
93
|
this.#advance();
|
|
78
94
|
}
|
|
79
95
|
|
|
@@ -84,7 +100,9 @@ export class GenericParser
|
|
|
84
100
|
*/
|
|
85
101
|
protected try_advance(): void | NoMatch
|
|
86
102
|
{
|
|
87
|
-
if (this.out_of_bounds())
|
|
103
|
+
if (this.out_of_bounds()) {
|
|
104
|
+
return NO_MATCH;
|
|
105
|
+
}
|
|
88
106
|
this.#advance();
|
|
89
107
|
}
|
|
90
108
|
|
|
@@ -93,50 +111,57 @@ export class GenericParser
|
|
|
93
111
|
this.i++;
|
|
94
112
|
|
|
95
113
|
// @ts-expect-error: `this.current == undefined` is a true negative
|
|
96
|
-
|
|
97
|
-
this.
|
|
114
|
+
while (IGNORED_CHARACTERS.has(this.current)) {
|
|
115
|
+
this.i++;
|
|
98
116
|
}
|
|
99
117
|
}
|
|
100
118
|
|
|
101
119
|
/**
|
|
102
120
|
* Consume `raw`, erroring if `raw` was not found.
|
|
103
121
|
*/
|
|
104
|
-
protected
|
|
122
|
+
protected eat(raw: string, error_data?: DesmostError.Data): Fallible<void>
|
|
105
123
|
{
|
|
106
124
|
if (this.out_of_bounds()) {
|
|
107
|
-
throw new
|
|
108
|
-
`
|
|
109
|
-
|
|
125
|
+
throw new DesmostError.UnexpectedEnd({
|
|
126
|
+
msg: `While trying to consume: \`${raw}\``,
|
|
127
|
+
show: {
|
|
128
|
+
text: this.preview(this.i - 5),
|
|
129
|
+
}
|
|
130
|
+
});
|
|
110
131
|
}
|
|
111
132
|
|
|
112
133
|
let init = this.i;
|
|
113
134
|
let ii = 0;
|
|
114
135
|
|
|
115
136
|
while (this.current === raw[ii]) {
|
|
116
|
-
this.advance(
|
|
117
|
-
`
|
|
118
|
-
|
|
137
|
+
this.advance({
|
|
138
|
+
msg: `While trying to consume: \`${raw}\``,
|
|
139
|
+
show: {
|
|
140
|
+
text: `\`${this.preview(init)}\``,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
119
143
|
|
|
120
144
|
ii++;
|
|
121
145
|
if (ii === raw.length) return;
|
|
122
146
|
}
|
|
123
147
|
|
|
124
|
-
throw new
|
|
125
|
-
|
|
126
|
-
|
|
148
|
+
throw new DesmostError.UnexpectedInput({
|
|
149
|
+
msg: `Expected: \`${raw}\`, but found: \`${this.preview(init)}\``,
|
|
150
|
+
...error_data,
|
|
151
|
+
});
|
|
127
152
|
}
|
|
128
153
|
|
|
129
154
|
/**
|
|
130
155
|
* Attempt to consume `raw`, backtracking if `raw` was not found.
|
|
131
156
|
*/
|
|
132
|
-
protected
|
|
157
|
+
protected try_eat(raw: string): void | NoMatch
|
|
133
158
|
{
|
|
134
159
|
let init = this.i;
|
|
135
160
|
let ii = 0;
|
|
136
161
|
|
|
137
162
|
while (this.current === raw[ii]) {
|
|
138
163
|
let r = this.try_advance();
|
|
139
|
-
if (r === NO_MATCH)
|
|
164
|
+
if (r === NO_MATCH) break;
|
|
140
165
|
|
|
141
166
|
ii++;
|
|
142
167
|
if (ii === raw.length) return;
|
|
@@ -147,15 +172,31 @@ export class GenericParser
|
|
|
147
172
|
return NO_MATCH;
|
|
148
173
|
}
|
|
149
174
|
|
|
175
|
+
protected try_eat_identifier(): string | NoMatch
|
|
176
|
+
{
|
|
177
|
+
let init = this.i;
|
|
178
|
+
|
|
179
|
+
while (/[a-zA-Z-]/.test(this.current ?? "")) {
|
|
180
|
+
let r = this.try_advance();
|
|
181
|
+
|
|
182
|
+
if (r === NO_MATCH) {
|
|
183
|
+
this.i = init;
|
|
184
|
+
return NO_MATCH;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return this.source.slice(init, this.i);
|
|
189
|
+
}
|
|
190
|
+
|
|
150
191
|
/**
|
|
151
|
-
* Consume 0 or more space characters.
|
|
192
|
+
* Consume 0 or more space or tab characters.
|
|
152
193
|
*/
|
|
153
|
-
protected
|
|
194
|
+
protected eat_whitespace(): void
|
|
154
195
|
{
|
|
155
196
|
/* NOTE: Callers should be able to assume this is safe to call even when at end of input, since it should just match 0 characters */
|
|
156
197
|
if (this.out_of_bounds()) return;
|
|
157
198
|
|
|
158
|
-
while (this.current ===
|
|
199
|
+
while (this.current === ' ' || this.current === '\t') {
|
|
159
200
|
this.advance();
|
|
160
201
|
}
|
|
161
202
|
}
|
|
@@ -165,28 +206,29 @@ export class GenericParser
|
|
|
165
206
|
*
|
|
166
207
|
* Same as `.consume_spaces()`, but allows tabs and newlines.
|
|
167
208
|
*/
|
|
168
|
-
protected
|
|
209
|
+
protected eat_newlines(): void
|
|
169
210
|
{
|
|
170
211
|
/* NOTE: Callers should be able to assume this is safe to call even when at end of input, since it should just match 0 characters */
|
|
171
212
|
if (this.i >= this.length) return;
|
|
172
213
|
|
|
173
214
|
while (
|
|
174
|
-
this.current ===
|
|
175
|
-
|| this.current ===
|
|
176
|
-
|| this.current ===
|
|
215
|
+
this.current === ' '
|
|
216
|
+
|| this.current === '\n'
|
|
217
|
+
|| this.current === '\t'
|
|
177
218
|
) {
|
|
178
219
|
this.advance();
|
|
179
220
|
}
|
|
180
221
|
}
|
|
181
222
|
|
|
182
|
-
protected
|
|
223
|
+
protected eat_end_of_block(error_data?: DesmostError.Data): Fallible<void>
|
|
183
224
|
{
|
|
184
225
|
if (this.i >= this.length) return;
|
|
185
226
|
|
|
186
|
-
this.
|
|
227
|
+
this.eat_whitespace();
|
|
187
228
|
|
|
188
|
-
this.
|
|
189
|
-
|
|
190
|
-
|
|
229
|
+
this.eat("\n", {
|
|
230
|
+
msg: `Expected a newline at the end of a block, but found: \`${this.preview()}\``,
|
|
231
|
+
...error_data
|
|
232
|
+
});
|
|
191
233
|
}
|
|
192
234
|
}
|