@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.
Files changed (44) hide show
  1. package/README.md +82 -0
  2. package/esm/LRUCache.d.ts +6 -0
  3. package/esm/LRUCache.js +30 -0
  4. package/esm/diagnostics/error.d.ts +12 -0
  5. package/esm/diagnostics/error.js +37 -0
  6. package/esm/diagnostics/suggestions.d.ts +2 -0
  7. package/esm/diagnostics/suggestions.js +35 -0
  8. package/esm/index.d.ts +8 -0
  9. package/esm/index.js +6 -0
  10. package/esm/molang.d.ts +2 -0
  11. package/esm/molang.js +22 -0
  12. package/esm/package.json +3 -0
  13. package/esm/parser/expression.d.ts +87 -0
  14. package/esm/parser/expression.js +19 -0
  15. package/esm/parser/parser.d.ts +20 -0
  16. package/esm/parser/parser.js +490 -0
  17. package/esm/runtime/context.d.ts +19 -0
  18. package/esm/runtime/context.js +51 -0
  19. package/esm/runtime/math.d.ts +32 -0
  20. package/esm/runtime/math.js +89 -0
  21. package/esm/runtime/runtime.d.ts +34 -0
  22. package/esm/runtime/runtime.js +537 -0
  23. package/package.json +39 -0
  24. package/script/LRUCache.d.ts +6 -0
  25. package/script/LRUCache.js +33 -0
  26. package/script/diagnostics/error.d.ts +12 -0
  27. package/script/diagnostics/error.js +42 -0
  28. package/script/diagnostics/suggestions.d.ts +2 -0
  29. package/script/diagnostics/suggestions.js +40 -0
  30. package/script/index.d.ts +8 -0
  31. package/script/index.js +27 -0
  32. package/script/molang.d.ts +2 -0
  33. package/script/molang.js +29 -0
  34. package/script/package.json +3 -0
  35. package/script/parser/expression.d.ts +87 -0
  36. package/script/parser/expression.js +22 -0
  37. package/script/parser/parser.d.ts +20 -0
  38. package/script/parser/parser.js +494 -0
  39. package/script/runtime/context.d.ts +19 -0
  40. package/script/runtime/context.js +57 -0
  41. package/script/runtime/math.d.ts +32 -0
  42. package/script/runtime/math.js +92 -0
  43. package/script/runtime/runtime.d.ts +34 -0
  44. 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
@@ -0,0 +1,6 @@
1
+ export default class LRUCache<K, V> extends Map<K, V> {
2
+ private maxSize;
3
+ constructor(maxSize: number);
4
+ get(key: K): V | undefined;
5
+ set(key: K, value: V): this;
6
+ }
@@ -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,2 @@
1
+ export declare function levenshtein(a: string, b: string): number;
2
+ export declare function findBestMatch(name: string, candidates: string[]): string | null;
@@ -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";
@@ -0,0 +1,2 @@
1
+ import { MolangContext } from "./runtime/context.js";
2
+ export declare function evaluate(input: string, context?: MolangContext): unknown;
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
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -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
+ }