@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
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
## `@foxystar/molang`
|
|
2
|
+
A fast, extensible, and safe implementation of the [Molang](https://learn.microsoft.com/en-us/minecraft/creator/documents/molang/introduction) expression language.
|
|
3
|
+
|
|
4
|
+
## Installation
|
|
5
|
+
```bash
|
|
6
|
+
npm install @foxystar/molang
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
`@foxystar/molang` is a full Molang interpreter written in TypeScript, designed for:
|
|
11
|
+
- Minecraft: Bedrock Edition's Scripting API
|
|
12
|
+
- Expression evaluation for custom components, systems, and tooling
|
|
13
|
+
|
|
14
|
+
It includes a parser, runtime, context system, and error handling, all built with strong typing and extensibility in mind.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
- Full Molang parser (AST-based)
|
|
18
|
+
- Runtime interpreter with control flow support
|
|
19
|
+
- Context system with inheritance and scoping
|
|
20
|
+
- Safe evaluation (restricted mutation rules)
|
|
21
|
+
- Helpful error messages with suggestions
|
|
22
|
+
- Easily extensible (custom queries, variables, functions)
|
|
23
|
+
- Built-in math + random utilities
|
|
24
|
+
|
|
25
|
+
## Quick Example
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import * as Molang from "@foxystar/molang";
|
|
29
|
+
|
|
30
|
+
const result = Molang.evaluate("1 + 2 * 3");
|
|
31
|
+
console.log(result); // 7
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Using Context
|
|
35
|
+
```ts
|
|
36
|
+
import * as Molang from "@foxystar/molang";
|
|
37
|
+
|
|
38
|
+
const context = Molang.createMolangContext({
|
|
39
|
+
variable: {
|
|
40
|
+
health: 10,
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
Molang.evaluate("v.health + 5", context); // 15
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Custom functions
|
|
48
|
+
```ts
|
|
49
|
+
import * as Molang from "@foxystar/molang";
|
|
50
|
+
|
|
51
|
+
const context = Molang.createMolangContext({
|
|
52
|
+
query: {
|
|
53
|
+
double: (x: number) => x * 2,
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
Molang.evaluate("q.double(5)", context); // 10
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
> [!NOTE]
|
|
63
|
+
> - Only `variable` and `temp` namespaces can be mutated
|
|
64
|
+
> - Prevents accidental writes to unsafe scopes
|
|
65
|
+
> - Strict mode available for additional validation
|
|
66
|
+
|
|
67
|
+
## Error Handling
|
|
68
|
+
Errors are descriptive and include helpful context:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
Molang.evaluate("q.doubl(5)");
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
Cannot call 'q.doubl' because it is undefined. Did you mean 'query.double'?
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Design Goals
|
|
79
|
+
- Predictable and safe runtime behavior
|
|
80
|
+
- Clear and actionable error messages
|
|
81
|
+
- Minimal overhead with good performance
|
|
82
|
+
- Easy integration into existing systems
|
package/esm/LRUCache.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export default class LRUCache extends Map {
|
|
2
|
+
constructor(maxSize) {
|
|
3
|
+
super();
|
|
4
|
+
Object.defineProperty(this, "maxSize", {
|
|
5
|
+
enumerable: true,
|
|
6
|
+
configurable: true,
|
|
7
|
+
writable: true,
|
|
8
|
+
value: maxSize
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
get(key) {
|
|
12
|
+
const value = super.get(key);
|
|
13
|
+
if (value !== undefined) {
|
|
14
|
+
super.delete(key);
|
|
15
|
+
super.set(key, value);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
set(key, value) {
|
|
20
|
+
if (this.has(key)) {
|
|
21
|
+
super.delete(key);
|
|
22
|
+
}
|
|
23
|
+
super.set(key, value);
|
|
24
|
+
if (this.size > this.maxSize) {
|
|
25
|
+
const firstKey = this.keys().next().value;
|
|
26
|
+
super.delete(firstKey);
|
|
27
|
+
}
|
|
28
|
+
return this;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ExprBase } from "../parser/expression.js";
|
|
2
|
+
declare class MolangError extends Error {
|
|
3
|
+
constructor(message: string, start: number, end: number, source: string);
|
|
4
|
+
private static formatError;
|
|
5
|
+
}
|
|
6
|
+
export declare class MolangParseError extends MolangError {
|
|
7
|
+
constructor(message: string, position: number, source: string);
|
|
8
|
+
}
|
|
9
|
+
export declare class MolangRuntimeError extends MolangError {
|
|
10
|
+
constructor(message: string, expr: ExprBase, source: string);
|
|
11
|
+
}
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
class MolangError extends Error {
|
|
2
|
+
constructor(message, start, end, source) {
|
|
3
|
+
super(MolangError.formatError(message, start, end, source));
|
|
4
|
+
this.name = this.constructor.name;
|
|
5
|
+
}
|
|
6
|
+
static formatError(message, pos, end, src) {
|
|
7
|
+
let line = 1;
|
|
8
|
+
let lastLineStart = 0;
|
|
9
|
+
for (let i = 0; i < pos; i++) {
|
|
10
|
+
if (src[i] === "\n") {
|
|
11
|
+
line++;
|
|
12
|
+
lastLineStart = i + 1;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const column = pos - lastLineStart + 1;
|
|
16
|
+
const lines = src.split("\n");
|
|
17
|
+
const lineText = lines[line - 1] ?? "";
|
|
18
|
+
//const pointer = " ".repeat(column - 1) + "^";
|
|
19
|
+
const span = Math.max(1, end - pos);
|
|
20
|
+
const underlineLength = Math.min(span, Math.max(1, lineText.length - column + 1));
|
|
21
|
+
const pointer = " ".repeat(column - 1) +
|
|
22
|
+
"^".repeat(underlineLength);
|
|
23
|
+
return (`${message} at line ${line - 1}, column ${column}\n` +
|
|
24
|
+
`${lineText}\n` +
|
|
25
|
+
`${pointer}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class MolangParseError extends MolangError {
|
|
29
|
+
constructor(message, position, source) {
|
|
30
|
+
super(message, position, position + 1, source);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class MolangRuntimeError extends MolangError {
|
|
34
|
+
constructor(message, expr, source) {
|
|
35
|
+
super(message, expr.start, expr.end, source);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function levenshtein(a, b) {
|
|
2
|
+
const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
3
|
+
for (let i = 0; i <= a.length; i++) {
|
|
4
|
+
dp[i][0] = i;
|
|
5
|
+
}
|
|
6
|
+
for (let j = 0; j <= b.length; j++) {
|
|
7
|
+
dp[0][j] = j;
|
|
8
|
+
}
|
|
9
|
+
for (let i = 1; i <= a.length; i++) {
|
|
10
|
+
for (let j = 1; j <= b.length; j++) {
|
|
11
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
12
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, // deletion
|
|
13
|
+
dp[i][j - 1] + 1, // insertion
|
|
14
|
+
dp[i - 1][j - 1] + cost // substitution
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return dp[a.length][b.length];
|
|
19
|
+
}
|
|
20
|
+
export function findBestMatch(name, candidates) {
|
|
21
|
+
let best = null;
|
|
22
|
+
let bestScore = Infinity;
|
|
23
|
+
for (const c of candidates) {
|
|
24
|
+
const d = levenshtein(name.toLowerCase(), c.toLowerCase());
|
|
25
|
+
if (d < bestScore) {
|
|
26
|
+
bestScore = d;
|
|
27
|
+
best = c;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Heuristic: only suggest if it's "close enough"
|
|
31
|
+
if (best !== null && bestScore <= 2) {
|
|
32
|
+
return best;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
package/esm/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { evaluate } from "./molang.js";
|
|
2
|
+
export { MolangParser } from "./parser/parser.js";
|
|
3
|
+
export type { Expr, ExprBase, Program, Statement, Token, TokenType } from "./parser/expression.js";
|
|
4
|
+
export * from "./runtime/context.js";
|
|
5
|
+
export { MolangMath } from "./runtime/math.js";
|
|
6
|
+
export { MolangRuntime } from "./runtime/runtime.js";
|
|
7
|
+
export type { MolangOptions } from "./runtime/runtime.js";
|
|
8
|
+
export * from "./diagnostics/error.js";
|
package/esm/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { evaluate } from "./molang.js";
|
|
2
|
+
export { MolangParser } from "./parser/parser.js";
|
|
3
|
+
export * from "./runtime/context.js";
|
|
4
|
+
export { MolangMath } from "./runtime/math.js";
|
|
5
|
+
export { MolangRuntime } from "./runtime/runtime.js";
|
|
6
|
+
export * from "./diagnostics/error.js";
|
package/esm/molang.d.ts
ADDED
package/esm/molang.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { MolangParser } from "./parser/parser.js";
|
|
2
|
+
import { MolangRuntime } from "./runtime/runtime.js";
|
|
3
|
+
import { DEFAULT_CONTEXT, mergeContext } from "./runtime/context.js";
|
|
4
|
+
import LRUCache from "./LRUCache.js";
|
|
5
|
+
const MAX_CACHE_SIZE = 2048;
|
|
6
|
+
const cache = new LRUCache(MAX_CACHE_SIZE);
|
|
7
|
+
export function evaluate(input, context = {}) {
|
|
8
|
+
if (context === DEFAULT_CONTEXT) {
|
|
9
|
+
context = mergeContext(DEFAULT_CONTEXT, {});
|
|
10
|
+
}
|
|
11
|
+
else if (!Object.prototype.isPrototypeOf.call(DEFAULT_CONTEXT, context)) {
|
|
12
|
+
Object.setPrototypeOf(context, DEFAULT_CONTEXT);
|
|
13
|
+
}
|
|
14
|
+
let program = cache.get(input);
|
|
15
|
+
if (!program) {
|
|
16
|
+
const parser = new MolangParser(input);
|
|
17
|
+
program = parser.parseProgram();
|
|
18
|
+
cache.set(input, program);
|
|
19
|
+
}
|
|
20
|
+
const runtime = new MolangRuntime(context, input, { strict: false });
|
|
21
|
+
return runtime.evaluateProgram(program);
|
|
22
|
+
}
|
package/esm/package.json
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export declare const TOKEN_REGEX: RegExp;
|
|
2
|
+
export type ExprBase = {
|
|
3
|
+
start: number;
|
|
4
|
+
end: number;
|
|
5
|
+
};
|
|
6
|
+
export type Expr = ({
|
|
7
|
+
type: "Literal";
|
|
8
|
+
value: unknown;
|
|
9
|
+
} & ExprBase) | ({
|
|
10
|
+
type: "Identifier";
|
|
11
|
+
name: string;
|
|
12
|
+
path: string[];
|
|
13
|
+
} & ExprBase) | ({
|
|
14
|
+
type: "BinaryExpression";
|
|
15
|
+
operator: string;
|
|
16
|
+
left: Expr;
|
|
17
|
+
right: Expr;
|
|
18
|
+
} & ExprBase) | ({
|
|
19
|
+
type: "CallExpression";
|
|
20
|
+
callee: Expr;
|
|
21
|
+
arguments: Expr[];
|
|
22
|
+
} & ExprBase) | ({
|
|
23
|
+
type: "UnaryExpression";
|
|
24
|
+
operator: string;
|
|
25
|
+
expression: Expr;
|
|
26
|
+
} & ExprBase) | ({
|
|
27
|
+
type: "ArrayLiteral";
|
|
28
|
+
elements: Expr[];
|
|
29
|
+
} & ExprBase) | ({
|
|
30
|
+
type: "IndexExpression";
|
|
31
|
+
array: Expr;
|
|
32
|
+
index: Expr;
|
|
33
|
+
} & ExprBase) | ({
|
|
34
|
+
type: "ArrowExpression";
|
|
35
|
+
left: Expr;
|
|
36
|
+
right: Expr;
|
|
37
|
+
} & ExprBase) | ({
|
|
38
|
+
type: "ConditionalExpression";
|
|
39
|
+
condition: Expr;
|
|
40
|
+
then: Expr;
|
|
41
|
+
else?: Expr;
|
|
42
|
+
} & ExprBase);
|
|
43
|
+
export type Statement = ({
|
|
44
|
+
type: "ExprStatement";
|
|
45
|
+
expr: Expr;
|
|
46
|
+
} & ExprBase) | ({
|
|
47
|
+
type: "AssignStatement";
|
|
48
|
+
target: Expr;
|
|
49
|
+
value: Expr;
|
|
50
|
+
} & ExprBase) | ({
|
|
51
|
+
type: "ReturnStatement";
|
|
52
|
+
value?: Expr;
|
|
53
|
+
} & ExprBase) | ({
|
|
54
|
+
type: "BreakStatement";
|
|
55
|
+
} & ExprBase) | ({
|
|
56
|
+
type: "ContinueStatement";
|
|
57
|
+
} & ExprBase) | ({
|
|
58
|
+
type: "LoopStatement";
|
|
59
|
+
count: Expr;
|
|
60
|
+
body: Statement;
|
|
61
|
+
} & ExprBase) | ({
|
|
62
|
+
type: "ForEachStatement";
|
|
63
|
+
iterator: Expr;
|
|
64
|
+
iterable: Expr;
|
|
65
|
+
body: Statement;
|
|
66
|
+
} & ExprBase) | ({
|
|
67
|
+
type: "ConditionalStatement";
|
|
68
|
+
condition: Expr;
|
|
69
|
+
then: Statement;
|
|
70
|
+
else?: Statement;
|
|
71
|
+
} & ExprBase) | ({
|
|
72
|
+
type: "BlockStatement";
|
|
73
|
+
body: Statement[];
|
|
74
|
+
} & ExprBase);
|
|
75
|
+
export type Program = {
|
|
76
|
+
type: "Program";
|
|
77
|
+
body: Statement[];
|
|
78
|
+
start: number;
|
|
79
|
+
end: number;
|
|
80
|
+
};
|
|
81
|
+
export type TokenType = "number" | "string" | "identifier" | "assignment" | "operator" | "parenthesis" | "brace" | "comma" | "bracket" | "semicolon" | "eof";
|
|
82
|
+
export type Token = {
|
|
83
|
+
type: TokenType;
|
|
84
|
+
value: string;
|
|
85
|
+
start: number;
|
|
86
|
+
end: number;
|
|
87
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const TOKEN_REGEX = new RegExp([
|
|
2
|
+
// number
|
|
3
|
+
"^(?<number>-?\\d+(?:\\.\\d+)?)",
|
|
4
|
+
// string (double OR single quotes)
|
|
5
|
+
"(?<string>\"[^\"]*\"|'[^']*')",
|
|
6
|
+
// identifier + dotted path
|
|
7
|
+
"(?<identifier>[a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)",
|
|
8
|
+
"(?<assignment>=(?!=))",
|
|
9
|
+
"(?<operator>\\?\\?|->|>>>|>>|<<|\\|\\||&&|==|!=|<=|>=|\\^|\\||&|\\?|:|[+\\-*/<>!])",
|
|
10
|
+
// grouping
|
|
11
|
+
"(?<parenthesis>[()])",
|
|
12
|
+
"(?<bracket>[\\[\\]])",
|
|
13
|
+
"(?<brace>[{}])",
|
|
14
|
+
// punctuation
|
|
15
|
+
"(?<comma>,)",
|
|
16
|
+
"(?<semicolon>;)",
|
|
17
|
+
// whitespace
|
|
18
|
+
"(?<whitespace>\\s+)"
|
|
19
|
+
].join("|^"));
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Program } from "./expression.js";
|
|
2
|
+
export declare class MolangParser {
|
|
3
|
+
private input;
|
|
4
|
+
private tokens;
|
|
5
|
+
private position;
|
|
6
|
+
constructor(input: string);
|
|
7
|
+
private tokenize;
|
|
8
|
+
private peek;
|
|
9
|
+
private consume;
|
|
10
|
+
parseProgram(): Program;
|
|
11
|
+
private parseStatement;
|
|
12
|
+
private parseExpression;
|
|
13
|
+
private parseBinaryExpression;
|
|
14
|
+
private tryFold;
|
|
15
|
+
private getOperatorPrecedence;
|
|
16
|
+
private parseConditionalExpression;
|
|
17
|
+
private parsePrimaryExpression;
|
|
18
|
+
private parseUnary;
|
|
19
|
+
private parseAtom;
|
|
20
|
+
}
|