@foxystar/molang 0.1.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.
- package/README.md +82 -0
- package/esm/LRUCache.d.ts +6 -0
- package/esm/LRUCache.js +30 -0
- package/esm/diagnostics/error.d.ts +12 -0
- package/esm/diagnostics/error.js +37 -0
- package/esm/diagnostics/suggestions.d.ts +2 -0
- package/esm/diagnostics/suggestions.js +35 -0
- package/esm/index.d.ts +8 -0
- package/esm/index.js +6 -0
- package/esm/molang.d.ts +2 -0
- package/esm/molang.js +22 -0
- package/esm/package.json +3 -0
- package/esm/parser/expression.d.ts +87 -0
- package/esm/parser/expression.js +19 -0
- package/esm/parser/parser.d.ts +20 -0
- package/esm/parser/parser.js +490 -0
- package/esm/runtime/context.d.ts +19 -0
- package/esm/runtime/context.js +51 -0
- package/esm/runtime/math.d.ts +32 -0
- package/esm/runtime/math.js +89 -0
- package/esm/runtime/runtime.d.ts +34 -0
- package/esm/runtime/runtime.js +537 -0
- package/package.json +39 -0
- package/script/LRUCache.d.ts +6 -0
- package/script/LRUCache.js +33 -0
- package/script/diagnostics/error.d.ts +12 -0
- package/script/diagnostics/error.js +42 -0
- package/script/diagnostics/suggestions.d.ts +2 -0
- package/script/diagnostics/suggestions.js +40 -0
- package/script/index.d.ts +8 -0
- package/script/index.js +27 -0
- package/script/molang.d.ts +2 -0
- package/script/molang.js +29 -0
- package/script/package.json +3 -0
- package/script/parser/expression.d.ts +87 -0
- package/script/parser/expression.js +22 -0
- package/script/parser/parser.d.ts +20 -0
- package/script/parser/parser.js +494 -0
- package/script/runtime/context.d.ts +19 -0
- package/script/runtime/context.js +57 -0
- package/script/runtime/math.d.ts +32 -0
- package/script/runtime/math.js +92 -0
- package/script/runtime/runtime.d.ts +34 -0
- package/script/runtime/runtime.js +567 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import { MolangParseError } from "../diagnostics/error.js";
|
|
2
|
+
import { MolangRuntime } from "../runtime/runtime.js";
|
|
3
|
+
import { TOKEN_REGEX, } from "./expression.js";
|
|
4
|
+
export class MolangParser {
|
|
5
|
+
constructor(input) {
|
|
6
|
+
Object.defineProperty(this, "input", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
configurable: true,
|
|
9
|
+
writable: true,
|
|
10
|
+
value: input
|
|
11
|
+
});
|
|
12
|
+
Object.defineProperty(this, "tokens", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
configurable: true,
|
|
15
|
+
writable: true,
|
|
16
|
+
value: []
|
|
17
|
+
});
|
|
18
|
+
Object.defineProperty(this, "position", {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
configurable: true,
|
|
21
|
+
writable: true,
|
|
22
|
+
value: 0
|
|
23
|
+
});
|
|
24
|
+
this.tokenize();
|
|
25
|
+
}
|
|
26
|
+
tokenize() {
|
|
27
|
+
let str = this.input;
|
|
28
|
+
let offset = 0;
|
|
29
|
+
while (str.length > 0) {
|
|
30
|
+
const match = TOKEN_REGEX.exec(str);
|
|
31
|
+
if (match === null || match.index !== 0) {
|
|
32
|
+
throw new MolangParseError(`Unexpected token '${str[0]}'`, offset, this.input);
|
|
33
|
+
}
|
|
34
|
+
const raw = match[0];
|
|
35
|
+
for (const type of Object.keys(match.groups)) {
|
|
36
|
+
if (match.groups[type]) {
|
|
37
|
+
if (type !== "whitespace") {
|
|
38
|
+
this.tokens.push({
|
|
39
|
+
type: type, value: raw,
|
|
40
|
+
start: offset,
|
|
41
|
+
end: offset + raw.length
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
offset += raw.length;
|
|
45
|
+
str = str.slice(raw.length);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
this.tokens.push({
|
|
51
|
+
type: "eof", value: "",
|
|
52
|
+
start: offset, end: offset
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
peek() {
|
|
56
|
+
return this.tokens[this.position];
|
|
57
|
+
}
|
|
58
|
+
consume(expectedType) {
|
|
59
|
+
const token = this.tokens[this.position++];
|
|
60
|
+
if (expectedType && token.type !== expectedType) {
|
|
61
|
+
throw new Error(`Expected token type ${expectedType}, but got ${token.type}`);
|
|
62
|
+
}
|
|
63
|
+
return token;
|
|
64
|
+
}
|
|
65
|
+
parseProgram() {
|
|
66
|
+
const body = [];
|
|
67
|
+
const start = this.peek().start;
|
|
68
|
+
while (this.peek().type !== "eof") {
|
|
69
|
+
body.push(this.parseStatement());
|
|
70
|
+
if (this.peek().type === "semicolon") {
|
|
71
|
+
this.consume("semicolon");
|
|
72
|
+
}
|
|
73
|
+
else if (this.peek().type !== "eof") {
|
|
74
|
+
throw new MolangParseError("Expected ';' between statements", this.peek().start, this.input);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const end = this.peek().end;
|
|
78
|
+
return { type: "Program", body, start, end };
|
|
79
|
+
}
|
|
80
|
+
parseStatement() {
|
|
81
|
+
const token = this.peek();
|
|
82
|
+
// loop(...)
|
|
83
|
+
if (token.type === "identifier" && token.value === "loop") {
|
|
84
|
+
const start = token.start;
|
|
85
|
+
this.consume("identifier"); // loop
|
|
86
|
+
this.consume("parenthesis"); // (
|
|
87
|
+
const count = this.parseExpression();
|
|
88
|
+
if (this.peek().type !== "comma") {
|
|
89
|
+
throw new MolangParseError("Expected ',' after loop count", this.peek().start, this.input);
|
|
90
|
+
}
|
|
91
|
+
this.consume("comma");
|
|
92
|
+
// body is a STATEMENT, not an expression
|
|
93
|
+
const body = this.parseStatement();
|
|
94
|
+
this.consume("parenthesis"); // )
|
|
95
|
+
return {
|
|
96
|
+
type: "LoopStatement",
|
|
97
|
+
count,
|
|
98
|
+
body,
|
|
99
|
+
start,
|
|
100
|
+
end: body.end
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// for_each(...)
|
|
104
|
+
if (token.type === "identifier" && token.value === "for_each") {
|
|
105
|
+
const start = token.start;
|
|
106
|
+
this.consume("identifier"); // for_each
|
|
107
|
+
this.consume("parenthesis"); // (
|
|
108
|
+
// iterator (must be assignable)
|
|
109
|
+
const iterator = this.parseExpression();
|
|
110
|
+
if (iterator.type !== "Identifier" &&
|
|
111
|
+
iterator.type !== "IndexExpression") {
|
|
112
|
+
throw new MolangParseError("'for_each' iterator must be an assignable variable", iterator.start, this.input);
|
|
113
|
+
}
|
|
114
|
+
if (this.peek().type !== "comma") {
|
|
115
|
+
throw new MolangParseError("Expected ',' after for_each iterator", this.peek().start, this.input);
|
|
116
|
+
}
|
|
117
|
+
this.consume("comma");
|
|
118
|
+
const iterable = this.parseExpression();
|
|
119
|
+
if (this.peek().type !== "comma") {
|
|
120
|
+
throw new MolangParseError("Expected ',' after for_each iterable", this.peek().start, this.input);
|
|
121
|
+
}
|
|
122
|
+
this.consume("comma");
|
|
123
|
+
// body is a STATEMENT
|
|
124
|
+
const body = this.parseStatement();
|
|
125
|
+
this.consume("parenthesis"); // )
|
|
126
|
+
return {
|
|
127
|
+
type: "ForEachStatement",
|
|
128
|
+
iterator,
|
|
129
|
+
iterable,
|
|
130
|
+
body,
|
|
131
|
+
start,
|
|
132
|
+
end: body.end
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (token.type === "brace" && token.value === "{") {
|
|
136
|
+
const start = token.start;
|
|
137
|
+
this.consume("brace"); // {
|
|
138
|
+
const body = [];
|
|
139
|
+
while (!(this.peek().type === "brace" && this.peek().value === "}")) {
|
|
140
|
+
body.push(this.parseStatement());
|
|
141
|
+
if (this.peek().type === "semicolon") {
|
|
142
|
+
this.consume("semicolon");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const end = this.consume("brace").end; // }
|
|
146
|
+
return { type: "BlockStatement", body, start, end };
|
|
147
|
+
}
|
|
148
|
+
if (token.type === "identifier" && token.value === "return") {
|
|
149
|
+
const start = token.start;
|
|
150
|
+
this.consume("identifier");
|
|
151
|
+
// return;
|
|
152
|
+
if (this.peek().type === "semicolon" || this.peek().type === "eof") {
|
|
153
|
+
return {
|
|
154
|
+
type: "ReturnStatement",
|
|
155
|
+
start,
|
|
156
|
+
end: token.end
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
// return <expr>
|
|
160
|
+
const value = this.parseExpression();
|
|
161
|
+
return {
|
|
162
|
+
type: "ReturnStatement",
|
|
163
|
+
value,
|
|
164
|
+
start,
|
|
165
|
+
end: value.end
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// break
|
|
169
|
+
if (token.type === "identifier" && token.value === "break") {
|
|
170
|
+
const t = this.consume("identifier");
|
|
171
|
+
return { type: "BreakStatement", start: t.start, end: t.end };
|
|
172
|
+
}
|
|
173
|
+
// continue
|
|
174
|
+
if (token.type === "identifier" && token.value === "continue") {
|
|
175
|
+
const t = this.consume("identifier");
|
|
176
|
+
return { type: "ContinueStatement", start: t.start, end: t.end };
|
|
177
|
+
}
|
|
178
|
+
const expr = this.parseBinaryExpression();
|
|
179
|
+
if (this.peek().type === "operator" && this.peek().value === "?") {
|
|
180
|
+
this.consume("operator");
|
|
181
|
+
// Only allow a STATEMENT here
|
|
182
|
+
const thenStmt = this.parseStatement();
|
|
183
|
+
let elseStmt;
|
|
184
|
+
if (this.peek().type === "operator" && this.peek().value === ":") {
|
|
185
|
+
this.consume("operator"); // :
|
|
186
|
+
elseStmt = this.parseStatement();
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
type: "ConditionalStatement",
|
|
190
|
+
condition: expr,
|
|
191
|
+
then: thenStmt,
|
|
192
|
+
else: elseStmt,
|
|
193
|
+
start: expr.start,
|
|
194
|
+
end: (elseStmt ?? thenStmt).end
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
// Assignment
|
|
198
|
+
if (this.peek().type === "assignment") {
|
|
199
|
+
this.consume("assignment");
|
|
200
|
+
const value = this.parseExpression();
|
|
201
|
+
return {
|
|
202
|
+
type: "AssignStatement",
|
|
203
|
+
target: expr,
|
|
204
|
+
value,
|
|
205
|
+
start: expr.start,
|
|
206
|
+
end: value.end
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
// Expression statement
|
|
210
|
+
return {
|
|
211
|
+
type: "ExprStatement",
|
|
212
|
+
expr,
|
|
213
|
+
start: expr.start,
|
|
214
|
+
end: expr.end
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
parseExpression() {
|
|
218
|
+
return this.parseConditionalExpression();
|
|
219
|
+
}
|
|
220
|
+
parseBinaryExpression(precedence = 0) {
|
|
221
|
+
let left = this.parsePrimaryExpression();
|
|
222
|
+
while (true) {
|
|
223
|
+
const token = this.peek();
|
|
224
|
+
if (token.type === "comma" || token.type === "parenthesis" || token.type === "eof") {
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
const tokenPrecedence = this.getOperatorPrecedence(token.value);
|
|
228
|
+
if (tokenPrecedence < precedence) {
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
this.consume("operator");
|
|
232
|
+
const right = this.parseBinaryExpression(tokenPrecedence + 1);
|
|
233
|
+
if (token.value === "->") {
|
|
234
|
+
left = {
|
|
235
|
+
type: "ArrowExpression",
|
|
236
|
+
left, right,
|
|
237
|
+
start: left.start,
|
|
238
|
+
end: right.end
|
|
239
|
+
};
|
|
240
|
+
continue; // Skip
|
|
241
|
+
}
|
|
242
|
+
if (left.type === "Literal" && right.type === "Literal") {
|
|
243
|
+
const folded = this.tryFold(token.value, left.value, right.value);
|
|
244
|
+
if (folded !== undefined) {
|
|
245
|
+
left = {
|
|
246
|
+
type: "Literal",
|
|
247
|
+
value: folded,
|
|
248
|
+
start: left.start,
|
|
249
|
+
end: right.end
|
|
250
|
+
};
|
|
251
|
+
continue; // keep parsing higher-precedence ops
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
left = {
|
|
255
|
+
type: "BinaryExpression",
|
|
256
|
+
operator: token.value,
|
|
257
|
+
left, right,
|
|
258
|
+
start: left.start,
|
|
259
|
+
end: right.end
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
return left;
|
|
263
|
+
}
|
|
264
|
+
tryFold(op, left, right) {
|
|
265
|
+
if (typeof left !== "number" || typeof right !== "number") {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
switch (op) {
|
|
269
|
+
case "+": return left + right;
|
|
270
|
+
case "-": return left - right;
|
|
271
|
+
case "*": return left * right;
|
|
272
|
+
case "/": return left / right;
|
|
273
|
+
case "<": return left < right;
|
|
274
|
+
case ">": return left > right;
|
|
275
|
+
case "<=": return left <= right;
|
|
276
|
+
case ">=": return left >= right;
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
getOperatorPrecedence(operator) {
|
|
281
|
+
switch (operator) {
|
|
282
|
+
case "?": return -1; // handled outside binary parsing
|
|
283
|
+
case ":": return -1;
|
|
284
|
+
case "||": return 1;
|
|
285
|
+
case "&&": return 2;
|
|
286
|
+
case "??": return 0;
|
|
287
|
+
case "|": return 2;
|
|
288
|
+
case "^": return 3;
|
|
289
|
+
case "&": return 4;
|
|
290
|
+
case "==":
|
|
291
|
+
case "!=":
|
|
292
|
+
case "<":
|
|
293
|
+
case ">":
|
|
294
|
+
case "<=":
|
|
295
|
+
case ">=":
|
|
296
|
+
return 5;
|
|
297
|
+
case "<<":
|
|
298
|
+
case ">>":
|
|
299
|
+
case ">>>":
|
|
300
|
+
return 6;
|
|
301
|
+
case "+":
|
|
302
|
+
case "-":
|
|
303
|
+
return 7;
|
|
304
|
+
case "*":
|
|
305
|
+
case "/":
|
|
306
|
+
return 8;
|
|
307
|
+
case "->":
|
|
308
|
+
return 9;
|
|
309
|
+
default:
|
|
310
|
+
return -1;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
parseConditionalExpression() {
|
|
314
|
+
const condition = this.parseBinaryExpression();
|
|
315
|
+
if (this.peek().type === "operator" &&
|
|
316
|
+
this.peek().value === "?") {
|
|
317
|
+
this.consume("operator"); // ?
|
|
318
|
+
const then = this.parseExpression();
|
|
319
|
+
// A ? B : C
|
|
320
|
+
if (this.peek().type === "operator" &&
|
|
321
|
+
this.peek().value === ":") {
|
|
322
|
+
this.consume("operator"); // :
|
|
323
|
+
const elseStmt = this.parseExpression();
|
|
324
|
+
return {
|
|
325
|
+
type: "ConditionalExpression",
|
|
326
|
+
condition,
|
|
327
|
+
then,
|
|
328
|
+
else: elseStmt,
|
|
329
|
+
start: condition.start,
|
|
330
|
+
end: elseStmt.end
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
// Binary condition (A ? B)
|
|
334
|
+
return {
|
|
335
|
+
type: "ConditionalExpression",
|
|
336
|
+
condition,
|
|
337
|
+
then,
|
|
338
|
+
start: condition.start,
|
|
339
|
+
end: then.end
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
return condition;
|
|
343
|
+
}
|
|
344
|
+
parsePrimaryExpression() {
|
|
345
|
+
return this.parseUnary();
|
|
346
|
+
}
|
|
347
|
+
parseUnary() {
|
|
348
|
+
const token = this.peek();
|
|
349
|
+
if (token.type === "operator" && (token.value === "-" || token.value === "!")) {
|
|
350
|
+
this.consume("operator");
|
|
351
|
+
const expr = this.parseUnary();
|
|
352
|
+
return {
|
|
353
|
+
type: "UnaryExpression",
|
|
354
|
+
operator: token.value,
|
|
355
|
+
expression: expr,
|
|
356
|
+
start: token.start,
|
|
357
|
+
end: expr.end
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
return this.parseAtom();
|
|
361
|
+
}
|
|
362
|
+
parseAtom() {
|
|
363
|
+
const token = this.peek();
|
|
364
|
+
if (token.type === "number") {
|
|
365
|
+
this.consume("number");
|
|
366
|
+
return {
|
|
367
|
+
type: "Literal",
|
|
368
|
+
value: Number(token.value),
|
|
369
|
+
start: token.start,
|
|
370
|
+
end: token.end
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
if (token.type === "string") {
|
|
374
|
+
this.consume("string");
|
|
375
|
+
return {
|
|
376
|
+
type: "Literal",
|
|
377
|
+
value: token.value.slice(1, -1),
|
|
378
|
+
start: token.start,
|
|
379
|
+
end: token.end
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
if (token.type === "bracket" && token.value === "[") {
|
|
383
|
+
const start = token.start;
|
|
384
|
+
this.consume("bracket");
|
|
385
|
+
const elements = [];
|
|
386
|
+
while (!(this.peek().type === "bracket" && this.peek().value === "]")) {
|
|
387
|
+
elements.push(this.parseExpression());
|
|
388
|
+
if (this.peek().type !== "comma") {
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
this.consume("comma");
|
|
392
|
+
}
|
|
393
|
+
const close = this.consume("bracket");
|
|
394
|
+
return {
|
|
395
|
+
type: "ArrayLiteral",
|
|
396
|
+
elements,
|
|
397
|
+
start, end: close.end
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
if (token.type === "identifier" && (token.value === "true" || token.value === "false")) {
|
|
401
|
+
this.consume("identifier");
|
|
402
|
+
return {
|
|
403
|
+
type: "Literal",
|
|
404
|
+
value: token.value === "true",
|
|
405
|
+
start: token.start,
|
|
406
|
+
end: token.end
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
if (token.type === "identifier") {
|
|
410
|
+
this.consume("identifier");
|
|
411
|
+
const path = MolangRuntime.normalizePath(token.value);
|
|
412
|
+
let expr = {
|
|
413
|
+
type: "Identifier",
|
|
414
|
+
name: token.value,
|
|
415
|
+
path,
|
|
416
|
+
start: token.start,
|
|
417
|
+
end: token.end
|
|
418
|
+
};
|
|
419
|
+
// Check if this is a function call
|
|
420
|
+
if (this.peek().type === "parenthesis" && this.peek().value === "(") {
|
|
421
|
+
this.consume("parenthesis");
|
|
422
|
+
const args = [];
|
|
423
|
+
while (true) {
|
|
424
|
+
const next = this.peek();
|
|
425
|
+
if (next.type === "eof") {
|
|
426
|
+
throw new MolangParseError("Unclosed function call", token.start, this.input);
|
|
427
|
+
}
|
|
428
|
+
if (next.type === "parenthesis" && next.value === ")") {
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
if (next.type === "comma") {
|
|
432
|
+
throw new MolangParseError(`Unexpected token ','`, next.start, this.input);
|
|
433
|
+
}
|
|
434
|
+
args.push(this.parseExpression());
|
|
435
|
+
const afterArg = this.peek();
|
|
436
|
+
if (afterArg.type === "comma") {
|
|
437
|
+
this.consume("comma");
|
|
438
|
+
const lookahead = this.peek();
|
|
439
|
+
if (lookahead.type === "parenthesis" && lookahead.value === ")") {
|
|
440
|
+
throw new MolangParseError(`Unexpected token ')'`, lookahead.start, this.input);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
else if (!(afterArg.type === "parenthesis" && afterArg.value === ")")) {
|
|
444
|
+
throw new MolangParseError(`Unexpected token '${afterArg.value}'`, afterArg.start, this.input);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
this.consume("parenthesis");
|
|
448
|
+
expr = {
|
|
449
|
+
type: "CallExpression",
|
|
450
|
+
callee: expr,
|
|
451
|
+
arguments: args,
|
|
452
|
+
start: expr.start,
|
|
453
|
+
end: this.tokens[this.position - 1].end
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
while (this.peek().type === "bracket" && this.peek().value === "[") {
|
|
457
|
+
this.consume("bracket");
|
|
458
|
+
const index = this.parseExpression();
|
|
459
|
+
const close = this.consume("bracket");
|
|
460
|
+
expr = {
|
|
461
|
+
type: "IndexExpression",
|
|
462
|
+
array: expr,
|
|
463
|
+
index,
|
|
464
|
+
start: expr.start,
|
|
465
|
+
end: close.end
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
return expr;
|
|
469
|
+
}
|
|
470
|
+
if (token.type === "parenthesis" && token.value === "(") {
|
|
471
|
+
this.consume("parenthesis");
|
|
472
|
+
let expr = this.parseExpression();
|
|
473
|
+
this.consume("parenthesis");
|
|
474
|
+
while (this.peek().type === "bracket" && this.peek().value === "[") {
|
|
475
|
+
this.consume("bracket");
|
|
476
|
+
const index = this.parseExpression();
|
|
477
|
+
const close = this.consume("bracket");
|
|
478
|
+
expr = {
|
|
479
|
+
type: "IndexExpression",
|
|
480
|
+
array: expr,
|
|
481
|
+
index,
|
|
482
|
+
start: expr.start,
|
|
483
|
+
end: close.end
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
return expr;
|
|
487
|
+
}
|
|
488
|
+
throw new Error(`Unexpected token: ${token.value}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type MolangPrimitive = number | string | boolean;
|
|
2
|
+
export type MolangArray = MolangPrimitive[];
|
|
3
|
+
export type MolangValue = MolangPrimitive | MolangArray;
|
|
4
|
+
export type MolangFunction = (...args: never[]) => unknown;
|
|
5
|
+
export type MolangBinding = MolangValue | MolangFunction | MolangNamespace | undefined;
|
|
6
|
+
export type MolangNamespace = {
|
|
7
|
+
[key: string]: MolangBinding;
|
|
8
|
+
};
|
|
9
|
+
export interface MolangContext extends MolangNamespace {
|
|
10
|
+
math?: MolangNamespace;
|
|
11
|
+
variable?: MolangNamespace;
|
|
12
|
+
temp?: MolangNamespace;
|
|
13
|
+
query?: MolangNamespace;
|
|
14
|
+
context?: MolangNamespace;
|
|
15
|
+
}
|
|
16
|
+
export declare const DEFAULT_CONTEXT: MolangContext;
|
|
17
|
+
export declare function createMolangContext(initial?: MolangContext): MolangContext;
|
|
18
|
+
export declare function isNamespace(value: unknown): value is MolangNamespace;
|
|
19
|
+
export declare function mergeContext(base: MolangNamespace, user: MolangNamespace): MolangNamespace;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { MolangMath } from "./math.js";
|
|
2
|
+
import { PerlinNoise } from "@foxystar/math";
|
|
3
|
+
;
|
|
4
|
+
const PERLIN = new PerlinNoise();
|
|
5
|
+
export const DEFAULT_CONTEXT = {
|
|
6
|
+
math: MolangMath,
|
|
7
|
+
variable: {},
|
|
8
|
+
temp: {},
|
|
9
|
+
context: {},
|
|
10
|
+
query: {
|
|
11
|
+
block_state: (_name) => {
|
|
12
|
+
throw new Error("This function call can only be used in block context");
|
|
13
|
+
},
|
|
14
|
+
noise: (x, y) => {
|
|
15
|
+
return PERLIN.noise({ x, y });
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
export function createMolangContext(initial) {
|
|
20
|
+
const ctx = mergeContext(DEFAULT_CONTEXT, initial ?? {});
|
|
21
|
+
ctx.variable = {
|
|
22
|
+
...(initial?.variable ?? {}),
|
|
23
|
+
};
|
|
24
|
+
ctx.temp = {
|
|
25
|
+
...(initial?.temp ?? {}),
|
|
26
|
+
};
|
|
27
|
+
return ctx;
|
|
28
|
+
}
|
|
29
|
+
const LOCKED_KEYS = new Set(["math"]);
|
|
30
|
+
export function isNamespace(value) {
|
|
31
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
32
|
+
}
|
|
33
|
+
export function mergeContext(base, user) {
|
|
34
|
+
const out = Object.create(base);
|
|
35
|
+
for (const key in user) {
|
|
36
|
+
if (base === DEFAULT_CONTEXT && LOCKED_KEYS.has(key)) {
|
|
37
|
+
throw new Error(`Cannot override core Molang namespace '${key}'`);
|
|
38
|
+
}
|
|
39
|
+
const userValue = user[key];
|
|
40
|
+
const baseValue = base[key];
|
|
41
|
+
const baseIsNamespace = isNamespace(baseValue);
|
|
42
|
+
const userIsNamespace = isNamespace(userValue);
|
|
43
|
+
if (baseIsNamespace && userIsNamespace) {
|
|
44
|
+
out[key] = mergeContext(baseValue, userValue);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
out[key] = userValue;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const MolangMath: {
|
|
2
|
+
pi: number;
|
|
3
|
+
abs: (x: number) => number;
|
|
4
|
+
ceil: (x: number) => number;
|
|
5
|
+
exp: (x: number) => number;
|
|
6
|
+
ln: (x: number) => number;
|
|
7
|
+
floor: (x: number) => number;
|
|
8
|
+
pow: (x: number, y: number) => number;
|
|
9
|
+
round: (x: number) => number;
|
|
10
|
+
sign: (x: number) => number;
|
|
11
|
+
sqrt: (x: number) => number;
|
|
12
|
+
trunc: (x: number) => number;
|
|
13
|
+
clamp: (value: number, min: number, max: number) => number;
|
|
14
|
+
max: (...values: number[]) => number;
|
|
15
|
+
min: (...values: number[]) => number;
|
|
16
|
+
cos: (v: number) => number;
|
|
17
|
+
acos: (v: number) => number;
|
|
18
|
+
sin: (v: number) => number;
|
|
19
|
+
asin: (v: number) => number;
|
|
20
|
+
atan: (v: number) => number;
|
|
21
|
+
atan2: (y: number, x: number) => number;
|
|
22
|
+
copy_sign: (x: number, y: number) => number;
|
|
23
|
+
random: (low: number, high: number) => number;
|
|
24
|
+
random_integer: (low: number, high: number) => number;
|
|
25
|
+
die_roll: (num: number, low: number, high: number) => number;
|
|
26
|
+
die_roll_integer: (num: number, low: number, high: number) => number;
|
|
27
|
+
hermite_blend: (t: number) => number;
|
|
28
|
+
lerp: (start: number, end: number, t: number) => number;
|
|
29
|
+
lerprotate: (start: number, end: number, t: number) => number;
|
|
30
|
+
min_angle: (value: number) => number;
|
|
31
|
+
mod: (value: number, denom: number) => number;
|
|
32
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { XoroshiroRandom } from "@foxystar/math";
|
|
2
|
+
const rng = new XoroshiroRandom();
|
|
3
|
+
const DEG_TO_RAD = Math.PI / 180;
|
|
4
|
+
const RAD_TO_DEG = 180 / Math.PI;
|
|
5
|
+
function randomRange(low, high) {
|
|
6
|
+
return low + rng.nextDouble() * (high - low);
|
|
7
|
+
}
|
|
8
|
+
function randomIntRange(low, high) {
|
|
9
|
+
if (high < low) {
|
|
10
|
+
return low;
|
|
11
|
+
}
|
|
12
|
+
return low + rng.nextInt(high - low + 1);
|
|
13
|
+
}
|
|
14
|
+
export const MolangMath = {
|
|
15
|
+
pi: Math.PI,
|
|
16
|
+
abs: Math.abs,
|
|
17
|
+
ceil: Math.ceil,
|
|
18
|
+
exp: Math.exp,
|
|
19
|
+
ln: Math.log,
|
|
20
|
+
floor: Math.floor,
|
|
21
|
+
pow: Math.pow,
|
|
22
|
+
round: Math.round,
|
|
23
|
+
sign: Math.sign,
|
|
24
|
+
sqrt: Math.sqrt,
|
|
25
|
+
trunc: Math.trunc,
|
|
26
|
+
clamp: (value, min, max) => {
|
|
27
|
+
return Math.min(Math.max(value, min), max);
|
|
28
|
+
},
|
|
29
|
+
max: Math.max,
|
|
30
|
+
min: Math.min,
|
|
31
|
+
cos: (v) => {
|
|
32
|
+
return Math.cos(v * DEG_TO_RAD);
|
|
33
|
+
},
|
|
34
|
+
acos: (v) => {
|
|
35
|
+
return Math.acos(v) * RAD_TO_DEG;
|
|
36
|
+
},
|
|
37
|
+
sin: (v) => {
|
|
38
|
+
return Math.sin(v * DEG_TO_RAD);
|
|
39
|
+
},
|
|
40
|
+
asin: (v) => {
|
|
41
|
+
return Math.asin(v) * RAD_TO_DEG;
|
|
42
|
+
},
|
|
43
|
+
atan: (v) => {
|
|
44
|
+
return Math.atan(v) * RAD_TO_DEG;
|
|
45
|
+
},
|
|
46
|
+
atan2: (y, x) => {
|
|
47
|
+
return Math.atan2(y, x) * RAD_TO_DEG;
|
|
48
|
+
},
|
|
49
|
+
copy_sign: (x, y) => {
|
|
50
|
+
return Math.abs(x) * Math.sign(y);
|
|
51
|
+
},
|
|
52
|
+
random: (low, high) => {
|
|
53
|
+
return randomRange(low, high);
|
|
54
|
+
},
|
|
55
|
+
random_integer: (low, high) => {
|
|
56
|
+
return randomIntRange(low, high);
|
|
57
|
+
},
|
|
58
|
+
die_roll: (num, low, high) => {
|
|
59
|
+
let sum = 0;
|
|
60
|
+
for (let i = 0; i < num; i++) {
|
|
61
|
+
sum += randomRange(low, high);
|
|
62
|
+
}
|
|
63
|
+
return sum;
|
|
64
|
+
},
|
|
65
|
+
die_roll_integer: (num, low, high) => {
|
|
66
|
+
let sum = 0;
|
|
67
|
+
for (let i = 0; i < num; i++) {
|
|
68
|
+
sum += randomIntRange(low, high);
|
|
69
|
+
}
|
|
70
|
+
return sum;
|
|
71
|
+
},
|
|
72
|
+
hermite_blend: (t) => {
|
|
73
|
+
return 3 * t * t - 2 * t * t * t;
|
|
74
|
+
},
|
|
75
|
+
lerp: (start, end, t) => {
|
|
76
|
+
return start + (end - start) * t;
|
|
77
|
+
},
|
|
78
|
+
lerprotate: (start, end, t) => {
|
|
79
|
+
const diff = ((end - start + 540) % 360) - 180;
|
|
80
|
+
return start + diff * t;
|
|
81
|
+
},
|
|
82
|
+
min_angle: (value) => {
|
|
83
|
+
const angle = ((value + 180) % 360 + 360) % 360 - 180;
|
|
84
|
+
return angle;
|
|
85
|
+
},
|
|
86
|
+
mod: (value, denom) => {
|
|
87
|
+
return ((value % denom) + denom) % denom;
|
|
88
|
+
},
|
|
89
|
+
};
|