├── .gitignore ├── .vscode ├── settings.json └── launch.json ├── dist ├── tokenizer │ ├── tokenTypes.d.ts │ ├── token.d.ts │ └── Tokenizer.d.ts ├── parser │ ├── parselets │ │ ├── postfix.d.ts │ │ ├── infix.d.ts │ │ ├── loop.d.ts │ │ ├── break.d.ts │ │ ├── Return.d.ts │ │ ├── number.d.ts │ │ ├── string.d.ts │ │ ├── boolean.d.ts │ │ ├── forEach.d.ts │ │ ├── continue.d.ts │ │ ├── scope.d.ts │ │ ├── groupParselet.d.ts │ │ ├── function.d.ts │ │ ├── arrayAccess.d.ts │ │ ├── name.d.ts │ │ ├── AndOperator.d.ts │ │ ├── NotEquals.d.ts │ │ ├── GreaterOperator.d.ts │ │ ├── OrOperator.d.ts │ │ ├── SmallerOperator.d.ts │ │ ├── equals.d.ts │ │ ├── ternary.d.ts │ │ ├── statement.d.ts │ │ ├── prefix.d.ts │ │ ├── QuestionOperator.d.ts │ │ └── binaryOperator.d.ts │ ├── molang.d.ts │ ├── expressions │ │ ├── void.d.ts │ │ ├── boolean.d.ts │ │ ├── break.d.ts │ │ ├── continue.d.ts │ │ ├── number.d.ts │ │ ├── string.d.ts │ │ ├── static.d.ts │ │ ├── return.d.ts │ │ ├── postfix.d.ts │ │ ├── arrayAccess.d.ts │ │ ├── loop.d.ts │ │ ├── prefix.d.ts │ │ ├── group.d.ts │ │ ├── forEach.d.ts │ │ ├── statement.d.ts │ │ ├── ContextSwitch.d.ts │ │ ├── function.d.ts │ │ ├── genericOperator.d.ts │ │ ├── name.d.ts │ │ ├── ternary.d.ts │ │ └── index.d.ts │ ├── precedence.d.ts │ ├── expression.d.ts │ └── parse.d.ts ├── custom │ ├── transformStatement.d.ts │ ├── main.d.ts │ ├── class.d.ts │ └── function.d.ts ├── env │ ├── queries.d.ts │ ├── env.d.ts │ ├── math.d.ts │ └── standardEnv.d.ts ├── main.d.ts └── MoLang.d.ts ├── lib ├── parser │ ├── parselets │ │ ├── postfix.ts │ │ ├── infix.ts │ │ ├── break.ts │ │ ├── continue.ts │ │ ├── string.ts │ │ ├── number.ts │ │ ├── boolean.ts │ │ ├── groupParselet.ts │ │ ├── prefix.ts │ │ ├── return.ts │ │ ├── scope.ts │ │ ├── Equals.ts │ │ ├── arrayAccess.ts │ │ ├── AndOperator.ts │ │ ├── OrOperator.ts │ │ ├── NotEquals.ts │ │ ├── loop.ts │ │ ├── forEach.ts │ │ ├── function.ts │ │ ├── ternary.ts │ │ ├── QuestionOperator.ts │ │ ├── GreaterOperator.ts │ │ ├── SmallerOperator.ts │ │ ├── name.ts │ │ ├── statement.ts │ │ └── binaryOperator.ts │ ├── precedence.ts │ ├── expressions │ │ ├── void.ts │ │ ├── boolean.ts │ │ ├── break.ts │ │ ├── continue.ts │ │ ├── string.ts │ │ ├── number.ts │ │ ├── static.ts │ │ ├── return.ts │ │ ├── postfix.ts │ │ ├── arrayAccess.ts │ │ ├── group.ts │ │ ├── index.ts │ │ ├── genericOperator.ts │ │ ├── name.ts │ │ ├── prefix.ts │ │ ├── ContextSwitch.ts │ │ ├── function.ts │ │ ├── loop.ts │ │ ├── forEach.ts │ │ ├── ternary.ts │ │ └── statement.ts │ ├── expression.ts │ ├── parse.ts │ └── molang.ts ├── env │ ├── standardEnv.ts │ ├── queries.ts │ ├── math.ts │ └── env.ts ├── tokenizer │ ├── token.ts │ ├── tokenTypes.ts │ └── Tokenizer.ts ├── custom │ ├── transformStatement.ts │ ├── class.ts │ ├── function.ts │ └── main.ts ├── main.ts └── Molang.ts ├── .prettierrc.json ├── jest.config.js ├── vitest.config.ts ├── vite.config.ts ├── tsconfig.json ├── __tests__ ├── minimize.ts ├── stringify.ts ├── env.ts ├── custom │ ├── class.ts │ └── function.ts ├── resolveStatic.ts └── tests.ts ├── __bench__ ├── minimize.ts └── main.ts ├── package.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | perf/* 3 | 4 | 5 | tsconfig.tsbuildinfo 6 | .DS_STORE -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | } -------------------------------------------------------------------------------- /dist/tokenizer/tokenTypes.d.ts: -------------------------------------------------------------------------------- 1 | export declare const TokenTypes: Record; 2 | export declare const KeywordTokens: Set; 3 | -------------------------------------------------------------------------------- /lib/parser/parselets/postfix.ts: -------------------------------------------------------------------------------- 1 | import { IInfixParselet } from './infix' 2 | 3 | export interface IPostfixParselet extends IInfixParselet {} 4 | -------------------------------------------------------------------------------- /dist/parser/parselets/postfix.d.ts: -------------------------------------------------------------------------------- 1 | import { IInfixParselet } from './infix'; 2 | export interface IPostfixParselet extends IInfixParselet { 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "useTabs": true, 4 | "tabWidth": 4, 5 | "semi": false, 6 | "singleQuote": true, 7 | "printWidth": 80 8 | } -------------------------------------------------------------------------------- /dist/parser/molang.d.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from './parse'; 2 | import { IParserConfig } from '../main'; 3 | export declare class MolangParser extends Parser { 4 | constructor(config: Partial); 5 | } 6 | -------------------------------------------------------------------------------- /lib/env/standardEnv.ts: -------------------------------------------------------------------------------- 1 | import { MolangMathLib } from './math' 2 | import { standardQueries } from './queries' 3 | 4 | export const standardEnv = (useRadians: boolean) => ({ 5 | ...MolangMathLib(useRadians), 6 | ...standardQueries, 7 | }) 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "roots": [ 3 | "/lib" 4 | ], 5 | "testMatch": [ 6 | "**/__tests__/**/*.+(ts|tsx|js)", 7 | "**/?(*.)+(spec|test).+(ts|tsx|js)" 8 | ], 9 | "transform": { 10 | "^.+\\.(ts|tsx)$": "ts-jest" 11 | }, 12 | } -------------------------------------------------------------------------------- /lib/parser/parselets/infix.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse' 2 | import { IExpression } from '../expression' 3 | import { Token } from '../../tokenizer/token' 4 | 5 | export interface IInfixParselet { 6 | readonly precedence: number 7 | parse: (parser: Parser, left: IExpression, token: Token) => IExpression 8 | } 9 | -------------------------------------------------------------------------------- /dist/parser/expressions/void.d.ts: -------------------------------------------------------------------------------- 1 | import { Expression } from '../expression'; 2 | export declare class VoidExpression extends Expression { 3 | type: string; 4 | get allExpressions(): never[]; 5 | setExpressionAt(): void; 6 | isStatic(): boolean; 7 | eval(): number; 8 | toString(): string; 9 | } 10 | -------------------------------------------------------------------------------- /dist/parser/parselets/infix.d.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse'; 2 | import { IExpression } from '../expression'; 3 | import { Token } from '../../tokenizer/token'; 4 | export interface IInfixParselet { 5 | readonly precedence: number; 6 | parse: (parser: Parser, left: IExpression, token: Token) => IExpression; 7 | } 8 | -------------------------------------------------------------------------------- /dist/custom/transformStatement.d.ts: -------------------------------------------------------------------------------- 1 | import { IExpression } from '../parser/expression'; 2 | import { GroupExpression } from '../parser/expressions/group'; 3 | import { StatementExpression } from '../parser/expressions/statement'; 4 | export declare function transformStatement(expression: IExpression): IExpression | GroupExpression | StatementExpression; 5 | -------------------------------------------------------------------------------- /dist/env/queries.d.ts: -------------------------------------------------------------------------------- 1 | export declare const standardQueries: { 2 | 'query.in_range': (value: number, min: number, max: number) => boolean; 3 | 'query.all': (mustMatch: unknown, ...values: unknown[]) => boolean; 4 | 'query.any': (mustMatch: unknown, ...values: unknown[]) => boolean; 5 | 'query.count': (countable: unknown) => number; 6 | }; 7 | -------------------------------------------------------------------------------- /lib/parser/precedence.ts: -------------------------------------------------------------------------------- 1 | export enum EPrecedence { 2 | SCOPE = 1, 3 | STATEMENT, 4 | 5 | ASSIGNMENT, 6 | CONDITIONAL, 7 | 8 | ARRAY_ACCESS, 9 | 10 | NULLISH_COALESCING, 11 | 12 | AND, 13 | OR, 14 | 15 | EQUALS_COMPARE, 16 | COMPARE, 17 | 18 | SUM, 19 | PRODUCT, 20 | EXPONENT, 21 | 22 | PREFIX, 23 | POSTFIX, 24 | FUNCTION, 25 | } 26 | -------------------------------------------------------------------------------- /dist/parser/expressions/boolean.d.ts: -------------------------------------------------------------------------------- 1 | import { Expression } from '../expression'; 2 | export declare class BooleanExpression extends Expression { 3 | protected value: boolean; 4 | type: string; 5 | constructor(value: boolean); 6 | get allExpressions(): never[]; 7 | setExpressionAt(): void; 8 | isStatic(): boolean; 9 | eval(): boolean; 10 | } 11 | -------------------------------------------------------------------------------- /dist/parser/expressions/break.d.ts: -------------------------------------------------------------------------------- 1 | import { Expression } from '../expression'; 2 | export declare class BreakExpression extends Expression { 3 | type: string; 4 | isBreak: boolean; 5 | constructor(); 6 | get allExpressions(): never[]; 7 | setExpressionAt(): void; 8 | isStatic(): boolean; 9 | eval(): number; 10 | isString(): string; 11 | } 12 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { mergeConfig } from 'vite' 2 | import { defineConfig } from 'vitest/config' 3 | import viteConfig from './vite.config' 4 | 5 | export default mergeConfig( 6 | viteConfig, 7 | defineConfig({ 8 | test: { 9 | include: ['**/__tests__/**/*.ts'], 10 | benchmark: { 11 | include: ['**/__bench__/**/*.ts'], 12 | }, 13 | }, 14 | }) 15 | ) 16 | -------------------------------------------------------------------------------- /dist/parser/expressions/continue.d.ts: -------------------------------------------------------------------------------- 1 | import { Expression } from '../expression'; 2 | export declare class ContinueExpression extends Expression { 3 | type: string; 4 | isContinue: boolean; 5 | constructor(); 6 | get allExpressions(): never[]; 7 | setExpressionAt(): void; 8 | isStatic(): boolean; 9 | eval(): number; 10 | toString(): string; 11 | } 12 | -------------------------------------------------------------------------------- /dist/parser/expressions/number.d.ts: -------------------------------------------------------------------------------- 1 | import { Expression } from '../expression'; 2 | export declare class NumberExpression extends Expression { 3 | protected value: number; 4 | type: string; 5 | constructor(value: number); 6 | get allExpressions(): never[]; 7 | setExpressionAt(): void; 8 | isStatic(): boolean; 9 | eval(): number; 10 | toString(): string; 11 | } 12 | -------------------------------------------------------------------------------- /dist/parser/expressions/string.d.ts: -------------------------------------------------------------------------------- 1 | import { Expression } from '../expression'; 2 | export declare class StringExpression extends Expression { 3 | protected name: string; 4 | type: string; 5 | constructor(name: string); 6 | get allExpressions(): never[]; 7 | setExpressionAt(): void; 8 | isStatic(): boolean; 9 | eval(): string; 10 | toString(): string; 11 | } 12 | -------------------------------------------------------------------------------- /lib/parser/expressions/void.ts: -------------------------------------------------------------------------------- 1 | import { Expression } from '../expression' 2 | 3 | export class VoidExpression extends Expression { 4 | type = 'VoidExpression' 5 | 6 | get allExpressions() { 7 | return [] 8 | } 9 | setExpressionAt() {} 10 | 11 | isStatic() { 12 | return true 13 | } 14 | 15 | eval() { 16 | return 0 17 | } 18 | toString() { 19 | return '' 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /dist/parser/parselets/loop.d.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse'; 2 | import { Token } from '../../tokenizer/token'; 3 | import { IPrefixParselet } from './prefix'; 4 | import { LoopExpression } from '../expressions/loop'; 5 | export declare class LoopParselet implements IPrefixParselet { 6 | precedence: number; 7 | constructor(precedence?: number); 8 | parse(parser: Parser, token: Token): LoopExpression; 9 | } 10 | -------------------------------------------------------------------------------- /lib/parser/parselets/break.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse' 2 | import { Token } from '../../tokenizer/token' 3 | import { IPrefixParselet } from './prefix' 4 | import { BreakExpression } from '../expressions/break' 5 | 6 | export class BreakParselet implements IPrefixParselet { 7 | constructor(public precedence = 0) {} 8 | 9 | parse(parser: Parser, token: Token) { 10 | return new BreakExpression() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /dist/parser/parselets/break.d.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse'; 2 | import { Token } from '../../tokenizer/token'; 3 | import { IPrefixParselet } from './prefix'; 4 | import { BreakExpression } from '../expressions/break'; 5 | export declare class BreakParselet implements IPrefixParselet { 6 | precedence: number; 7 | constructor(precedence?: number); 8 | parse(parser: Parser, token: Token): BreakExpression; 9 | } 10 | -------------------------------------------------------------------------------- /lib/parser/expressions/boolean.ts: -------------------------------------------------------------------------------- 1 | import { Expression } from '../expression' 2 | 3 | export class BooleanExpression extends Expression { 4 | type = 'BooleanExpression' 5 | constructor(protected value: boolean) { 6 | super() 7 | } 8 | 9 | get allExpressions() { 10 | return [] 11 | } 12 | setExpressionAt() {} 13 | 14 | isStatic() { 15 | return true 16 | } 17 | 18 | eval() { 19 | return this.value 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /dist/parser/parselets/Return.d.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse'; 2 | import { Token } from '../../tokenizer/token'; 3 | import { IPrefixParselet } from './prefix'; 4 | import { ReturnExpression } from '../expressions/return'; 5 | export declare class ReturnParselet implements IPrefixParselet { 6 | precedence: number; 7 | constructor(precedence?: number); 8 | parse(parser: Parser, token: Token): ReturnExpression; 9 | } 10 | -------------------------------------------------------------------------------- /dist/parser/parselets/number.d.ts: -------------------------------------------------------------------------------- 1 | import { IPrefixParselet } from './prefix'; 2 | import { Token } from '../../tokenizer/token'; 3 | import { Parser } from '../parse'; 4 | import { NumberExpression } from '../expressions/number'; 5 | export declare class NumberParselet implements IPrefixParselet { 6 | precedence: number; 7 | constructor(precedence?: number); 8 | parse(parser: Parser, token: Token): NumberExpression; 9 | } 10 | -------------------------------------------------------------------------------- /dist/parser/parselets/string.d.ts: -------------------------------------------------------------------------------- 1 | import { IPrefixParselet } from './prefix'; 2 | import { Token } from '../../tokenizer/token'; 3 | import { Parser } from '../parse'; 4 | import { StringExpression } from '../expressions/string'; 5 | export declare class StringParselet implements IPrefixParselet { 6 | precedence: number; 7 | constructor(precedence?: number); 8 | parse(parser: Parser, token: Token): StringExpression; 9 | } 10 | -------------------------------------------------------------------------------- /dist/parser/precedence.d.ts: -------------------------------------------------------------------------------- 1 | export declare enum EPrecedence { 2 | SCOPE = 1, 3 | STATEMENT = 2, 4 | ASSIGNMENT = 3, 5 | CONDITIONAL = 4, 6 | ARRAY_ACCESS = 5, 7 | NULLISH_COALESCING = 6, 8 | AND = 7, 9 | OR = 8, 10 | EQUALS_COMPARE = 9, 11 | COMPARE = 10, 12 | SUM = 11, 13 | PRODUCT = 12, 14 | EXPONENT = 13, 15 | PREFIX = 14, 16 | POSTFIX = 15, 17 | FUNCTION = 16 18 | } 19 | -------------------------------------------------------------------------------- /dist/parser/parselets/boolean.d.ts: -------------------------------------------------------------------------------- 1 | import { IPrefixParselet } from './prefix'; 2 | import { Token } from '../../tokenizer/token'; 3 | import { Parser } from '../parse'; 4 | import { BooleanExpression } from '../expressions/boolean'; 5 | export declare class BooleanParselet implements IPrefixParselet { 6 | precedence: number; 7 | constructor(precedence?: number); 8 | parse(parser: Parser, token: Token): BooleanExpression; 9 | } 10 | -------------------------------------------------------------------------------- /dist/parser/parselets/forEach.d.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse'; 2 | import { Token } from '../../tokenizer/token'; 3 | import { IPrefixParselet } from './prefix'; 4 | import { ForEachExpression } from '../expressions/forEach'; 5 | export declare class ForEachParselet implements IPrefixParselet { 6 | precedence: number; 7 | constructor(precedence?: number); 8 | parse(parser: Parser, token: Token): ForEachExpression; 9 | } 10 | -------------------------------------------------------------------------------- /lib/parser/parselets/continue.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse' 2 | import { Token } from '../../tokenizer/token' 3 | import { IPrefixParselet } from './prefix' 4 | import { ContinueExpression } from '../expressions/continue' 5 | 6 | export class ContinueParselet implements IPrefixParselet { 7 | constructor(public precedence = 0) {} 8 | 9 | parse(parser: Parser, token: Token) { 10 | return new ContinueExpression() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /dist/parser/parselets/continue.d.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse'; 2 | import { Token } from '../../tokenizer/token'; 3 | import { IPrefixParselet } from './prefix'; 4 | import { ContinueExpression } from '../expressions/continue'; 5 | export declare class ContinueParselet implements IPrefixParselet { 6 | precedence: number; 7 | constructor(precedence?: number); 8 | parse(parser: Parser, token: Token): ContinueExpression; 9 | } 10 | -------------------------------------------------------------------------------- /lib/parser/parselets/string.ts: -------------------------------------------------------------------------------- 1 | import { IPrefixParselet } from './prefix' 2 | import { Token } from '../../tokenizer/token' 3 | import { Parser } from '../parse' 4 | import { StringExpression } from '../expressions/string' 5 | 6 | export class StringParselet implements IPrefixParselet { 7 | constructor(public precedence = 0) {} 8 | 9 | parse(parser: Parser, token: Token) { 10 | return new StringExpression(token.getText()) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /dist/parser/expressions/static.d.ts: -------------------------------------------------------------------------------- 1 | import { Expression } from '../expression'; 2 | export declare class StaticExpression extends Expression { 3 | protected value: unknown; 4 | readonly isReturn: boolean; 5 | type: string; 6 | constructor(value: unknown, isReturn?: boolean); 7 | get allExpressions(): never[]; 8 | setExpressionAt(): void; 9 | isStatic(): boolean; 10 | eval(): unknown; 11 | toString(): string; 12 | } 13 | -------------------------------------------------------------------------------- /lib/parser/parselets/number.ts: -------------------------------------------------------------------------------- 1 | import { IPrefixParselet } from './prefix' 2 | import { Token } from '../../tokenizer/token' 3 | import { Parser } from '../parse' 4 | import { NumberExpression } from '../expressions/number' 5 | 6 | export class NumberParselet implements IPrefixParselet { 7 | constructor(public precedence = 0) {} 8 | 9 | parse(parser: Parser, token: Token) { 10 | return new NumberExpression(Number(token.getText())) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/parser/parselets/boolean.ts: -------------------------------------------------------------------------------- 1 | import { IPrefixParselet } from './prefix' 2 | import { Token } from '../../tokenizer/token' 3 | import { Parser } from '../parse' 4 | import { BooleanExpression } from '../expressions/boolean' 5 | 6 | export class BooleanParselet implements IPrefixParselet { 7 | constructor(public precedence = 0) {} 8 | 9 | parse(parser: Parser, token: Token) { 10 | return new BooleanExpression(token.getText() === 'true') 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /dist/parser/parselets/scope.d.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse'; 2 | import { Token } from '../../tokenizer/token'; 3 | import { IPrefixParselet } from './prefix'; 4 | import { GroupExpression } from '../expressions/group'; 5 | export declare class ScopeParselet implements IPrefixParselet { 6 | precedence: number; 7 | constructor(precedence?: number); 8 | parse(parser: Parser, token: Token): import("../expression").IExpression | GroupExpression; 9 | } 10 | -------------------------------------------------------------------------------- /lib/parser/expressions/break.ts: -------------------------------------------------------------------------------- 1 | import { Expression } from '../expression' 2 | 3 | export class BreakExpression extends Expression { 4 | type = 'BreakExpression' 5 | isBreak = true 6 | 7 | constructor() { 8 | super() 9 | } 10 | 11 | get allExpressions() { 12 | return [] 13 | } 14 | setExpressionAt() {} 15 | 16 | isStatic() { 17 | return false 18 | } 19 | 20 | eval() { 21 | return 0 22 | } 23 | isString() { 24 | return 'break' 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /dist/parser/parselets/groupParselet.d.ts: -------------------------------------------------------------------------------- 1 | import { IPrefixParselet } from './prefix'; 2 | import { Token } from '../../tokenizer/token'; 3 | import { Parser } from '../parse'; 4 | import { GroupExpression } from '../expressions/group'; 5 | export declare class GroupParselet implements IPrefixParselet { 6 | precedence: number; 7 | constructor(precedence?: number); 8 | parse(parser: Parser, token: Token): import("../expression").IExpression | GroupExpression; 9 | } 10 | -------------------------------------------------------------------------------- /lib/parser/expressions/continue.ts: -------------------------------------------------------------------------------- 1 | import { Expression } from '../expression' 2 | 3 | export class ContinueExpression extends Expression { 4 | type = 'ContinueExpression' 5 | isContinue = true 6 | 7 | constructor() { 8 | super() 9 | } 10 | 11 | get allExpressions() { 12 | return [] 13 | } 14 | setExpressionAt() {} 15 | 16 | isStatic() { 17 | return false 18 | } 19 | 20 | eval() { 21 | return 0 22 | } 23 | toString() { 24 | return 'continue' 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /dist/parser/expressions/return.d.ts: -------------------------------------------------------------------------------- 1 | import { Expression, IExpression } from '../expression'; 2 | export declare class ReturnExpression extends Expression { 3 | protected expression: IExpression; 4 | type: string; 5 | isReturn: boolean; 6 | constructor(expression: IExpression); 7 | get allExpressions(): IExpression[]; 8 | setExpressionAt(_: number, expr: IExpression): void; 9 | isStatic(): boolean; 10 | eval(): unknown; 11 | toString(): string; 12 | } 13 | -------------------------------------------------------------------------------- /dist/parser/parselets/function.d.ts: -------------------------------------------------------------------------------- 1 | import { Token } from '../../tokenizer/token'; 2 | import { Parser } from '../parse'; 3 | import { IInfixParselet } from './infix'; 4 | import { IExpression } from '../expression'; 5 | import { FunctionExpression } from '../expressions/function'; 6 | export declare class FunctionParselet implements IInfixParselet { 7 | precedence: number; 8 | constructor(precedence?: number); 9 | parse(parser: Parser, left: IExpression, token: Token): FunctionExpression; 10 | } 11 | -------------------------------------------------------------------------------- /dist/parser/parselets/arrayAccess.d.ts: -------------------------------------------------------------------------------- 1 | import { Token } from '../../tokenizer/token'; 2 | import { Parser } from '../parse'; 3 | import { IInfixParselet } from './infix'; 4 | import { IExpression } from '../expression'; 5 | import { ArrayAccessExpression } from '../expressions/arrayAccess'; 6 | export declare class ArrayAccessParselet implements IInfixParselet { 7 | precedence: number; 8 | constructor(precedence?: number); 9 | parse(parser: Parser, left: IExpression, token: Token): ArrayAccessExpression; 10 | } 11 | -------------------------------------------------------------------------------- /lib/parser/expressions/string.ts: -------------------------------------------------------------------------------- 1 | import { Expression } from '../expression' 2 | 3 | export class StringExpression extends Expression { 4 | type = 'StringExpression' 5 | 6 | constructor(protected name: string) { 7 | super() 8 | } 9 | 10 | get allExpressions() { 11 | return [] 12 | } 13 | setExpressionAt() {} 14 | 15 | isStatic() { 16 | return true 17 | } 18 | 19 | eval() { 20 | return this.name.substring(1, this.name.length - 1) 21 | } 22 | 23 | toString() { 24 | return this.name 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /dist/parser/parselets/name.d.ts: -------------------------------------------------------------------------------- 1 | import { IPrefixParselet } from './prefix'; 2 | import { Token } from '../../tokenizer/token'; 3 | import { Parser } from '../parse'; 4 | import { NameExpression } from '../expressions/name'; 5 | import { ContextSwitchExpression } from '../expressions/ContextSwitch'; 6 | export declare class NameParselet implements IPrefixParselet { 7 | precedence: number; 8 | constructor(precedence?: number); 9 | parse(parser: Parser, token: Token): NameExpression | ContextSwitchExpression; 10 | } 11 | -------------------------------------------------------------------------------- /dist/parser/parselets/AndOperator.d.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse'; 2 | import { Token } from '../../tokenizer/token'; 3 | import { IExpression } from '../expression'; 4 | import { GenericOperatorExpression } from '../expressions/genericOperator'; 5 | import { IInfixParselet } from './infix'; 6 | export declare class AndOperator implements IInfixParselet { 7 | precedence: number; 8 | constructor(precedence?: number); 9 | parse(parser: Parser, leftExpression: IExpression, token: Token): GenericOperatorExpression; 10 | } 11 | -------------------------------------------------------------------------------- /dist/parser/parselets/NotEquals.d.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse'; 2 | import { Token } from '../../tokenizer/token'; 3 | import { IExpression } from '../expression'; 4 | import { GenericOperatorExpression } from '../expressions/genericOperator'; 5 | import { IInfixParselet } from './infix'; 6 | export declare class NotEqualsOperator implements IInfixParselet { 7 | precedence: number; 8 | constructor(precedence?: number); 9 | parse(parser: Parser, leftExpression: IExpression, token: Token): GenericOperatorExpression; 10 | } 11 | -------------------------------------------------------------------------------- /dist/parser/expressions/postfix.d.ts: -------------------------------------------------------------------------------- 1 | import { TTokenType } from '../../tokenizer/token'; 2 | import { Expression, IExpression } from '../expression'; 3 | export declare class PostfixExpression extends Expression { 4 | protected expression: IExpression; 5 | protected tokenType: TTokenType; 6 | type: string; 7 | constructor(expression: IExpression, tokenType: TTokenType); 8 | get allExpressions(): IExpression[]; 9 | setExpressionAt(_: number, expr: IExpression): void; 10 | isStatic(): boolean; 11 | eval(): void; 12 | } 13 | -------------------------------------------------------------------------------- /dist/parser/parselets/GreaterOperator.d.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse'; 2 | import { Token } from '../../tokenizer/token'; 3 | import { IExpression } from '../expression'; 4 | import { GenericOperatorExpression } from '../expressions/genericOperator'; 5 | import { IInfixParselet } from './infix'; 6 | export declare class GreaterOperator implements IInfixParselet { 7 | precedence: number; 8 | constructor(precedence?: number); 9 | parse(parser: Parser, leftExpression: IExpression, token: Token): GenericOperatorExpression; 10 | } 11 | -------------------------------------------------------------------------------- /dist/parser/parselets/OrOperator.d.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../../parser/parse'; 2 | import { Token } from '../../tokenizer/token'; 3 | import { IExpression } from '../expression'; 4 | import { GenericOperatorExpression } from '../expressions/genericOperator'; 5 | import { IInfixParselet } from './infix'; 6 | export declare class OrOperator implements IInfixParselet { 7 | precedence: number; 8 | constructor(precedence?: number); 9 | parse(parser: Parser, leftExpression: IExpression, token: Token): GenericOperatorExpression; 10 | } 11 | -------------------------------------------------------------------------------- /dist/parser/parselets/SmallerOperator.d.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse'; 2 | import { Token } from '../../tokenizer/token'; 3 | import { IExpression } from '../expression'; 4 | import { GenericOperatorExpression } from '../expressions/genericOperator'; 5 | import { IInfixParselet } from './infix'; 6 | export declare class SmallerOperator implements IInfixParselet { 7 | precedence: number; 8 | constructor(precedence?: number); 9 | parse(parser: Parser, leftExpression: IExpression, token: Token): GenericOperatorExpression; 10 | } 11 | -------------------------------------------------------------------------------- /dist/parser/parselets/equals.d.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../../parser/parse'; 2 | import { Token } from '../../tokenizer/token'; 3 | import { IExpression } from '../expression'; 4 | import { GenericOperatorExpression } from '../expressions/genericOperator'; 5 | import { IInfixParselet } from './infix'; 6 | export declare class EqualsOperator implements IInfixParselet { 7 | precedence: number; 8 | constructor(precedence?: number); 9 | parse(parser: Parser, leftExpression: IExpression, token: Token): GenericOperatorExpression; 10 | } 11 | -------------------------------------------------------------------------------- /dist/parser/parselets/ternary.d.ts: -------------------------------------------------------------------------------- 1 | import { IInfixParselet } from './infix'; 2 | import { Parser } from '../parse'; 3 | import { IExpression } from '../expression'; 4 | import { Token } from '../../tokenizer/token'; 5 | import { TernaryExpression } from '../expressions/ternary'; 6 | export declare class TernaryParselet implements IInfixParselet { 7 | precedence: number; 8 | exprName: string; 9 | constructor(precedence?: number); 10 | parse(parser: Parser, leftExpression: IExpression, token: Token): IExpression | TernaryExpression; 11 | } 12 | -------------------------------------------------------------------------------- /dist/parser/expressions/arrayAccess.d.ts: -------------------------------------------------------------------------------- 1 | import { Expression, IExpression } from '../expression'; 2 | export declare class ArrayAccessExpression extends Expression { 3 | protected name: IExpression; 4 | protected lookup: IExpression; 5 | type: string; 6 | constructor(name: IExpression, lookup: IExpression); 7 | get allExpressions(): IExpression[]; 8 | setExpressionAt(index: number, expr: IExpression): void; 9 | isStatic(): boolean; 10 | setPointer(value: unknown): void; 11 | eval(): any; 12 | toString(): string; 13 | } 14 | -------------------------------------------------------------------------------- /dist/parser/parselets/statement.d.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse'; 2 | import { IExpression } from '../expression'; 3 | import { Token } from '../../tokenizer/token'; 4 | import { IInfixParselet } from './infix'; 5 | import { StatementExpression } from '../expressions/statement'; 6 | export declare class StatementParselet implements IInfixParselet { 7 | precedence: number; 8 | constructor(precedence?: number); 9 | findReEntryPoint(parser: Parser): void; 10 | parse(parser: Parser, left: IExpression, token: Token): StatementExpression; 11 | } 12 | -------------------------------------------------------------------------------- /dist/tokenizer/token.d.ts: -------------------------------------------------------------------------------- 1 | export declare type TTokenType = string; 2 | export declare class Token { 3 | protected type: string; 4 | protected text: string; 5 | protected startColumn: number; 6 | protected startLine: number; 7 | constructor(type: string, text: string, startColumn: number, startLine: number); 8 | getType(): string; 9 | getText(): string; 10 | getPosition(): { 11 | startColumn: number; 12 | startLineNumber: number; 13 | endColumn: number; 14 | endLineNumber: number; 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /dist/tokenizer/Tokenizer.d.ts: -------------------------------------------------------------------------------- 1 | import { Token } from './token'; 2 | export declare class Tokenizer { 3 | protected keywordTokens: Set; 4 | protected i: number; 5 | protected currentColumn: number; 6 | protected currentLine: number; 7 | protected lastColumns: number; 8 | protected expression: string; 9 | constructor(addKeywords?: Set); 10 | init(expression: string): void; 11 | next(): Token; 12 | hasNext(): boolean; 13 | protected isLetter(char: string): boolean; 14 | protected isNumber(char: string): boolean; 15 | } 16 | -------------------------------------------------------------------------------- /dist/parser/parselets/prefix.d.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse'; 2 | import { IExpression } from '../expression'; 3 | import { Token } from '../../tokenizer/token'; 4 | import { PrefixExpression } from '../expressions/prefix'; 5 | export interface IPrefixParselet { 6 | readonly precedence: number; 7 | parse: (parser: Parser, token: Token) => IExpression; 8 | } 9 | export declare class PrefixOperator implements IPrefixParselet { 10 | precedence: number; 11 | constructor(precedence?: number); 12 | parse(parser: Parser, token: Token): PrefixExpression; 13 | } 14 | -------------------------------------------------------------------------------- /lib/tokenizer/token.ts: -------------------------------------------------------------------------------- 1 | export type TTokenType = string 2 | 3 | export class Token { 4 | constructor( 5 | protected type: string, 6 | protected text: string, 7 | protected startColumn: number, 8 | protected startLine: number 9 | ) {} 10 | 11 | getType() { 12 | return this.type 13 | } 14 | getText() { 15 | return this.text 16 | } 17 | getPosition() { 18 | return { 19 | startColumn: this.startColumn, 20 | startLineNumber: this.startLine, 21 | endColumn: this.startColumn + this.text.length, 22 | endLineNumber: this.startLine, 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /dist/parser/expressions/loop.d.ts: -------------------------------------------------------------------------------- 1 | import { Expression, IExpression } from '../expression'; 2 | export declare class LoopExpression extends Expression { 3 | protected count: IExpression; 4 | protected expression: IExpression; 5 | type: string; 6 | constructor(count: IExpression, expression: IExpression); 7 | get allExpressions(): IExpression[]; 8 | get isNoopLoop(): boolean; 9 | setExpressionAt(index: number, expr: IExpression): void; 10 | get isReturn(): boolean | undefined; 11 | isStatic(): boolean; 12 | eval(): unknown; 13 | toString(): string; 14 | } 15 | -------------------------------------------------------------------------------- /dist/parser/expressions/prefix.d.ts: -------------------------------------------------------------------------------- 1 | import { TTokenType } from '../../tokenizer/token'; 2 | import { Expression, IExpression } from '../expression'; 3 | export declare class PrefixExpression extends Expression { 4 | protected tokenType: TTokenType; 5 | protected expression: IExpression; 6 | type: string; 7 | constructor(tokenType: TTokenType, expression: IExpression); 8 | get allExpressions(): IExpression[]; 9 | setExpressionAt(_: number, expr: IExpression): void; 10 | isStatic(): boolean; 11 | eval(): number | boolean | undefined; 12 | toString(): string; 13 | } 14 | -------------------------------------------------------------------------------- /lib/parser/expressions/number.ts: -------------------------------------------------------------------------------- 1 | import { Expression } from '../expression' 2 | 3 | export class NumberExpression extends Expression { 4 | type = 'NumberExpression' 5 | 6 | constructor(protected value: number) { 7 | super() 8 | } 9 | 10 | get allExpressions() { 11 | return [] 12 | } 13 | setExpressionAt() {} 14 | 15 | isStatic() { 16 | return true 17 | } 18 | 19 | eval() { 20 | return this.value 21 | } 22 | toString() { 23 | const n = '' + this.value 24 | 25 | // Leading zeros can be omitted 26 | if (n.startsWith('0.')) return n.slice(1) 27 | return n 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /dist/parser/parselets/QuestionOperator.d.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse'; 2 | import { Token } from '../../tokenizer/token'; 3 | import { IExpression } from '../expression'; 4 | import { GenericOperatorExpression } from '../expressions/genericOperator'; 5 | import { IInfixParselet } from './infix'; 6 | export declare class QuestionOperator implements IInfixParselet { 7 | precedence: number; 8 | constructor(precedence?: number); 9 | parse(parser: Parser, leftExpression: IExpression, token: Token): IExpression | GenericOperatorExpression | import("../expressions").TernaryExpression; 10 | } 11 | -------------------------------------------------------------------------------- /lib/parser/parselets/groupParselet.ts: -------------------------------------------------------------------------------- 1 | import { IPrefixParselet } from './prefix' 2 | import { Token } from '../../tokenizer/token' 3 | import { Parser } from '../parse' 4 | import { GroupExpression } from '../expressions/group' 5 | 6 | export class GroupParselet implements IPrefixParselet { 7 | constructor(public precedence = 0) {} 8 | 9 | parse(parser: Parser, token: Token) { 10 | const expression = parser.parseExpression(this.precedence) 11 | parser.consume('RIGHT_PARENT') 12 | 13 | if (parser.config.keepGroups) 14 | return new GroupExpression(expression, '()') 15 | 16 | return expression 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /dist/parser/expressions/group.d.ts: -------------------------------------------------------------------------------- 1 | import { Expression, IExpression } from '../expression'; 2 | export declare class GroupExpression extends Expression { 3 | protected expression: IExpression; 4 | protected brackets: string; 5 | type: string; 6 | constructor(expression: IExpression, brackets: string); 7 | get allExpressions(): IExpression[]; 8 | setExpressionAt(_: number, expr: IExpression): void; 9 | isStatic(): boolean; 10 | get isReturn(): boolean | undefined; 11 | get isBreak(): boolean | undefined; 12 | get isContinue(): boolean | undefined; 13 | eval(): unknown; 14 | toString(): string; 15 | } 16 | -------------------------------------------------------------------------------- /lib/parser/expressions/static.ts: -------------------------------------------------------------------------------- 1 | import { Expression } from '../expression' 2 | 3 | export class StaticExpression extends Expression { 4 | type = 'StaticExpression' 5 | constructor(protected value: unknown, public readonly isReturn = false) { 6 | super() 7 | } 8 | 9 | get allExpressions() { 10 | return [] 11 | } 12 | setExpressionAt() {} 13 | 14 | isStatic() { 15 | return true 16 | } 17 | 18 | eval() { 19 | return this.value 20 | } 21 | toString() { 22 | let val = this.value 23 | if (typeof val === 'string') val = `'${val}'` 24 | 25 | if (this.isReturn) return `return ${val}` 26 | return '' + val 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /dist/parser/expressions/forEach.d.ts: -------------------------------------------------------------------------------- 1 | import { Expression, IExpression } from '../expression'; 2 | export declare class ForEachExpression extends Expression { 3 | protected variable: IExpression; 4 | protected arrayExpression: IExpression; 5 | protected expression: IExpression; 6 | type: string; 7 | constructor(variable: IExpression, arrayExpression: IExpression, expression: IExpression); 8 | get isReturn(): boolean | undefined; 9 | get allExpressions(): IExpression[]; 10 | setExpressionAt(index: number, expr: IExpression): void; 11 | isStatic(): boolean; 12 | eval(): unknown; 13 | toString(): string; 14 | } 15 | -------------------------------------------------------------------------------- /lib/parser/expressions/return.ts: -------------------------------------------------------------------------------- 1 | import { Expression, IExpression } from '../expression' 2 | 3 | export class ReturnExpression extends Expression { 4 | type = 'ReturnExpression' 5 | isReturn = true 6 | 7 | constructor(protected expression: IExpression) { 8 | super() 9 | } 10 | 11 | get allExpressions() { 12 | return [this.expression] 13 | } 14 | setExpressionAt(_: number, expr: IExpression) { 15 | this.expression = expr 16 | } 17 | 18 | isStatic() { 19 | return false 20 | } 21 | 22 | eval() { 23 | return this.expression.eval() 24 | } 25 | 26 | toString() { 27 | return `return ${this.expression.toString()}` 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/parser/parselets/prefix.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse' 2 | import { IExpression } from '../expression' 3 | import { Token } from '../../tokenizer/token' 4 | import { PrefixExpression } from '../expressions/prefix' 5 | 6 | export interface IPrefixParselet { 7 | readonly precedence: number 8 | parse: (parser: Parser, token: Token) => IExpression 9 | } 10 | 11 | export class PrefixOperator implements IPrefixParselet { 12 | constructor(public precedence = 0) {} 13 | 14 | parse(parser: Parser, token: Token) { 15 | return new PrefixExpression( 16 | token.getType(), 17 | parser.parseExpression(this.precedence) 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/tokenizer/tokenTypes.ts: -------------------------------------------------------------------------------- 1 | export const TokenTypes: Record = { 2 | '!': 'BANG', 3 | '&': 'AND', 4 | '(': 'LEFT_PARENT', 5 | ')': 'RIGHT_PARENT', 6 | '*': 'ASTERISK', 7 | '+': 'PLUS', 8 | ',': 'COMMA', 9 | '-': 'MINUS', 10 | '/': 'SLASH', 11 | ':': 'COLON', 12 | ';': 'SEMICOLON', 13 | '<': 'SMALLER', 14 | '=': 'EQUALS', 15 | '>': 'GREATER', 16 | '?': 'QUESTION', 17 | '[': 'ARRAY_LEFT', 18 | ']': 'ARRAY_RIGHT', 19 | '{': 'CURLY_LEFT', 20 | '|': 'OR', 21 | '}': 'CURLY_RIGHT', 22 | } 23 | 24 | export const KeywordTokens = new Set([ 25 | 'return', 26 | 'continue', 27 | 'break', 28 | 'for_each', 29 | 'loop', 30 | 'false', 31 | 'true', 32 | ]) 33 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}\\lib\\main.ts", 15 | "outFiles": [ 16 | "${workspaceFolder}/**/*.js" 17 | ] 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /lib/parser/parselets/return.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse' 2 | import { Token } from '../../tokenizer/token' 3 | import { IPrefixParselet } from './prefix' 4 | import { NumberExpression } from '../expressions/number' 5 | import { ReturnExpression } from '../expressions/return' 6 | import { EPrecedence } from '../precedence' 7 | 8 | export class ReturnParselet implements IPrefixParselet { 9 | constructor(public precedence = 0) {} 10 | 11 | parse(parser: Parser, token: Token) { 12 | const expr = parser.parseExpression(EPrecedence.STATEMENT + 1) 13 | 14 | return new ReturnExpression( 15 | parser.match('SEMICOLON', false) ? expr : new NumberExpression(0) 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /dist/parser/expressions/statement.d.ts: -------------------------------------------------------------------------------- 1 | import { Expression, IExpression } from '../expression'; 2 | export declare class StatementExpression extends Expression { 3 | protected expressions: IExpression[]; 4 | type: string; 5 | protected didReturn?: boolean; 6 | protected wasLoopBroken: boolean; 7 | protected wasLoopContinued: boolean; 8 | constructor(expressions: IExpression[]); 9 | get allExpressions(): IExpression[]; 10 | setExpressionAt(index: number, expr: IExpression): void; 11 | get isReturn(): boolean; 12 | get isBreak(): boolean; 13 | get isContinue(): boolean; 14 | isStatic(): boolean; 15 | eval(): unknown; 16 | toString(): string; 17 | } 18 | -------------------------------------------------------------------------------- /dist/custom/main.d.ts: -------------------------------------------------------------------------------- 1 | import { IParserConfig } from '../main'; 2 | import { MolangParser } from '../parser/molang'; 3 | import { IExpression } from '../parser/expression'; 4 | export declare class CustomMolangParser extends MolangParser { 5 | readonly functions: Map; 6 | readonly classes: Map; 7 | constructor(config: Partial); 8 | reset(): void; 9 | } 10 | export declare class CustomMolang { 11 | protected parser: CustomMolangParser; 12 | constructor(env: any); 13 | get functions(): Map; 14 | parse(expression: string): IExpression; 15 | transform(source: string): string; 16 | reset(): void; 17 | } 18 | -------------------------------------------------------------------------------- /lib/parser/expressions/postfix.ts: -------------------------------------------------------------------------------- 1 | import { TTokenType } from '../../tokenizer/token' 2 | import { Expression, IExpression } from '../expression' 3 | 4 | export class PostfixExpression extends Expression { 5 | type = 'PostfixExpression' 6 | 7 | constructor( 8 | protected expression: IExpression, 9 | protected tokenType: TTokenType 10 | ) { 11 | super() 12 | } 13 | 14 | get allExpressions() { 15 | return [this.expression] 16 | } 17 | setExpressionAt(_: number, expr: IExpression) { 18 | this.expression = expr 19 | } 20 | 21 | isStatic() { 22 | return this.expression.isStatic() 23 | } 24 | 25 | eval() { 26 | switch (this.tokenType) { 27 | case 'X': { 28 | // DO SOMETHING 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | import { resolve } from 'path' 3 | 4 | export default defineConfig({ 5 | test: { 6 | includeSource: ['src/**/*.test.ts'], 7 | }, 8 | define: { 9 | 'import.meta.vitest': 'undefined', 10 | }, 11 | build: { 12 | lib: { 13 | entry: resolve(__dirname, 'lib/main.ts'), 14 | name: 'Molang', 15 | fileName: (format) => `molang.${format}.js`, 16 | }, 17 | rollupOptions: { 18 | external: [ 19 | 'json5', 20 | 'path-browserify', 21 | 'mc-project-core', 22 | 'molang', 23 | 'fs', 24 | 'bridge-common-utils', 25 | 'is-glob', 26 | 'bridge-js-runtime', 27 | '@swc/wasm-web', 28 | 'micromatch', 29 | ], 30 | }, 31 | }, 32 | }) 33 | -------------------------------------------------------------------------------- /lib/parser/parselets/scope.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse' 2 | import { Token } from '../../tokenizer/token' 3 | import { IPrefixParselet } from './prefix' 4 | import { GroupExpression } from '../expressions/group' 5 | 6 | export class ScopeParselet implements IPrefixParselet { 7 | constructor(public precedence = 0) {} 8 | 9 | parse(parser: Parser, token: Token) { 10 | let expr = parser.parseExpression(this.precedence) 11 | 12 | if ( 13 | parser.config.useOptimizer && 14 | parser.config.earlyReturnsSkipTokenization && 15 | expr.isReturn 16 | ) 17 | parser.match('CURLY_RIGHT') 18 | else parser.consume('CURLY_RIGHT') 19 | 20 | return parser.config.keepGroups ? new GroupExpression(expr, '{}') : expr 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | /* Basic Options */ 5 | "target": "ESNext", 6 | "module": "CommonJS", 7 | "moduleResolution": "node", 8 | "outDir": "./dist/", 9 | "rootDir": "./lib/", 10 | "composite": true, 11 | "removeComments": true, 12 | "sourceMap": false, 13 | "declaration": true, 14 | "emitDeclarationOnly": true, 15 | 16 | "forceConsistentCasingInFileNames": true, 17 | 18 | /* Strict Type-Checking Options */ 19 | "strict": true, 20 | "strictFunctionTypes": true, 21 | "noImplicitAny": true, 22 | 23 | /* Additional Checks */ 24 | "esModuleInterop": true, 25 | "resolveJsonModule": true 26 | }, 27 | "include": ["./lib/**/*.ts"], 28 | "types": ["node"] 29 | } 30 | -------------------------------------------------------------------------------- /dist/parser/expressions/ContextSwitch.d.ts: -------------------------------------------------------------------------------- 1 | import { Expression, IExpression } from '../expression'; 2 | import { FunctionExpression } from './function'; 3 | import { NameExpression } from './name'; 4 | export declare class ContextSwitchExpression extends Expression { 5 | protected leftExpr: NameExpression | FunctionExpression; 6 | protected rightExpr: NameExpression | FunctionExpression; 7 | type: string; 8 | constructor(leftExpr: NameExpression | FunctionExpression, rightExpr: NameExpression | FunctionExpression); 9 | get allExpressions(): (NameExpression | FunctionExpression)[]; 10 | setExpressionAt(index: number, expr: IExpression): void; 11 | isStatic(): boolean; 12 | eval(): any; 13 | toString(): string; 14 | } 15 | -------------------------------------------------------------------------------- /dist/parser/expressions/function.d.ts: -------------------------------------------------------------------------------- 1 | import { NameExpression } from './name'; 2 | import { Expression, IExpression } from '../expression'; 3 | import { ExecutionEnvironment } from '../../env/env'; 4 | export declare class FunctionExpression extends Expression { 5 | protected name: NameExpression; 6 | protected args: IExpression[]; 7 | type: string; 8 | constructor(name: NameExpression, args: IExpression[]); 9 | get allExpressions(): (IExpression | NameExpression)[]; 10 | setExpressionAt(index: number, expr: Expression): void; 11 | setExecutionEnv(executionEnv: ExecutionEnvironment): void; 12 | get executionEnv(): ExecutionEnvironment; 13 | isStatic(): boolean; 14 | eval(): unknown; 15 | toString(): string; 16 | } 17 | -------------------------------------------------------------------------------- /lib/parser/parselets/Equals.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../../parser/parse' 2 | import { Token } from '../../tokenizer/token' 3 | import { IExpression } from '../expression' 4 | import { GenericOperatorExpression } from '../expressions/genericOperator' 5 | import { IInfixParselet } from './infix' 6 | 7 | export class EqualsOperator implements IInfixParselet { 8 | constructor(public precedence = 0) {} 9 | 10 | parse(parser: Parser, leftExpression: IExpression, token: Token) { 11 | return new GenericOperatorExpression( 12 | leftExpression, 13 | parser.parseExpression(this.precedence), 14 | '==', 15 | (leftExpression: IExpression, rightExpression: IExpression) => 16 | leftExpression.eval() === rightExpression.eval() 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /dist/parser/expressions/genericOperator.d.ts: -------------------------------------------------------------------------------- 1 | import { Expression, IExpression } from '../expression'; 2 | export declare class GenericOperatorExpression extends Expression { 3 | protected left: IExpression; 4 | protected right: IExpression; 5 | readonly operator: string; 6 | protected evalHelper: (leftExpression: IExpression, rightExpression: IExpression) => unknown; 7 | type: string; 8 | constructor(left: IExpression, right: IExpression, operator: string, evalHelper: (leftExpression: IExpression, rightExpression: IExpression) => unknown); 9 | get allExpressions(): IExpression[]; 10 | setExpressionAt(index: number, expr: IExpression): void; 11 | isStatic(): boolean; 12 | eval(): unknown; 13 | toString(): string; 14 | } 15 | -------------------------------------------------------------------------------- /dist/parser/expressions/name.d.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionEnvironment } from '../../env/env'; 2 | import { Expression } from '../expression'; 3 | export declare class NameExpression extends Expression { 4 | executionEnv: ExecutionEnvironment; 5 | protected name: string; 6 | protected isFunctionCall: boolean; 7 | type: string; 8 | constructor(executionEnv: ExecutionEnvironment, name: string, isFunctionCall?: boolean); 9 | get allExpressions(): never[]; 10 | setExpressionAt(): void; 11 | isStatic(): boolean; 12 | setPointer(value: unknown): void; 13 | setFunctionCall(value?: boolean): void; 14 | setName(name: string): void; 15 | setExecutionEnv(executionEnv: ExecutionEnvironment): void; 16 | eval(): any; 17 | toString(): string; 18 | } 19 | -------------------------------------------------------------------------------- /lib/parser/parselets/arrayAccess.ts: -------------------------------------------------------------------------------- 1 | import { Token } from '../../tokenizer/token' 2 | import { Parser } from '../parse' 3 | import { IInfixParselet } from './infix' 4 | import { IExpression } from '../expression' 5 | import { ArrayAccessExpression } from '../expressions/arrayAccess' 6 | 7 | export class ArrayAccessParselet implements IInfixParselet { 8 | constructor(public precedence = 0) {} 9 | 10 | parse(parser: Parser, left: IExpression, token: Token) { 11 | const expr = parser.parseExpression(this.precedence - 1) 12 | 13 | if (!left.setPointer) throw new Error(`"${left.type}" is not an array`) 14 | 15 | if (!parser.match('ARRAY_RIGHT')) 16 | throw new Error( 17 | `No closing bracket for opening bracket "[${expr.eval()}"` 18 | ) 19 | 20 | return new ArrayAccessExpression(left, expr) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /dist/main.d.ts: -------------------------------------------------------------------------------- 1 | import { TVariableHandler } from './env/env' 2 | import { Tokenizer } from './tokenizer/Tokenizer' 3 | export interface IParserConfig { 4 | useCache: boolean 5 | maxCacheSize: number 6 | useOptimizer: boolean 7 | useAggressiveStaticOptimizer: boolean 8 | earlyReturnsSkipParsing: boolean 9 | earlyReturnsSkipTokenization: boolean 10 | tokenizer: Tokenizer 11 | keepGroups: boolean 12 | convertUndefined: boolean 13 | useRadians: boolean 14 | variableHandler: TVariableHandler 15 | assumeFlatEnvironment: boolean 16 | } 17 | export { Tokenizer } from './tokenizer/Tokenizer' 18 | export type { IExpression } from './parser/expression' 19 | export { CustomMolang } from './custom/main' 20 | export { Molang } from './Molang' 21 | export * as expressions from './parser/expressions/index' 22 | export { Context } from './env/env' 23 | -------------------------------------------------------------------------------- /dist/parser/expressions/ternary.d.ts: -------------------------------------------------------------------------------- 1 | import { Expression, IExpression } from '../expression'; 2 | export declare class TernaryExpression extends Expression { 3 | protected leftExpression: IExpression; 4 | protected thenExpression: IExpression; 5 | protected elseExpression: IExpression; 6 | type: string; 7 | protected leftResult: unknown; 8 | constructor(leftExpression: IExpression, thenExpression: IExpression, elseExpression: IExpression); 9 | get allExpressions(): IExpression[]; 10 | setExpressionAt(index: number, expr: IExpression): void; 11 | get isReturn(): boolean | undefined; 12 | get hasReturn(): boolean | undefined; 13 | get isContinue(): boolean | undefined; 14 | get isBreak(): boolean | undefined; 15 | isStatic(): boolean; 16 | eval(): unknown; 17 | toString(): string; 18 | } 19 | -------------------------------------------------------------------------------- /lib/parser/parselets/AndOperator.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse' 2 | import { Token } from '../../tokenizer/token' 3 | import { IExpression } from '../expression' 4 | import { GenericOperatorExpression } from '../expressions/genericOperator' 5 | import { IInfixParselet } from './infix' 6 | 7 | export class AndOperator implements IInfixParselet { 8 | constructor(public precedence = 0) {} 9 | 10 | parse(parser: Parser, leftExpression: IExpression, token: Token) { 11 | if (parser.match('AND')) 12 | return new GenericOperatorExpression( 13 | leftExpression, 14 | parser.parseExpression(this.precedence), 15 | '&&', 16 | (leftExpression: IExpression, rightExpression: IExpression) => 17 | leftExpression.eval() && rightExpression.eval() 18 | ) 19 | else throw new Error(`"&" not followed by another "&"`) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/parser/parselets/OrOperator.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../../parser/parse' 2 | import { Token } from '../../tokenizer/token' 3 | import { IExpression } from '../expression' 4 | import { GenericOperatorExpression } from '../expressions/genericOperator' 5 | import { IInfixParselet } from './infix' 6 | 7 | export class OrOperator implements IInfixParselet { 8 | constructor(public precedence = 0) {} 9 | 10 | parse(parser: Parser, leftExpression: IExpression, token: Token) { 11 | if (parser.match('OR')) 12 | return new GenericOperatorExpression( 13 | leftExpression, 14 | parser.parseExpression(this.precedence), 15 | '||', 16 | (leftExpression: IExpression, rightExpression: IExpression) => 17 | leftExpression.eval() || rightExpression.eval() 18 | ) 19 | else throw new Error(`"|" not followed by another "|"`) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/env/queries.ts: -------------------------------------------------------------------------------- 1 | const inRange = (value: number, min: number, max: number) => { 2 | // Check that value, min and max are numbers 3 | if ( 4 | typeof value !== 'number' || 5 | typeof min !== 'number' || 6 | typeof max !== 'number' 7 | ) { 8 | console.error('"query.in_range": value, min and max must be numbers') 9 | return false 10 | } 11 | 12 | return value >= min && value <= max 13 | } 14 | 15 | const all = (mustMatch: unknown, ...values: unknown[]) => 16 | values.every((v) => v === mustMatch) 17 | 18 | const any = (mustMatch: unknown, ...values: unknown[]) => 19 | values.some((v) => v === mustMatch) 20 | 21 | const count = (countable: unknown) => 22 | Array.isArray(countable) ? countable.length : 1 23 | 24 | export const standardQueries = { 25 | 'query.in_range': inRange, 26 | 'query.all': all, 27 | 'query.any': any, 28 | 'query.count': count, 29 | } 30 | -------------------------------------------------------------------------------- /lib/parser/parselets/NotEquals.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse' 2 | import { Token } from '../../tokenizer/token' 3 | import { IExpression } from '../expression' 4 | import { GenericOperatorExpression } from '../expressions/genericOperator' 5 | import { IInfixParselet } from './infix' 6 | 7 | export class NotEqualsOperator implements IInfixParselet { 8 | constructor(public precedence = 0) {} 9 | 10 | parse(parser: Parser, leftExpression: IExpression, token: Token) { 11 | if (parser.match('EQUALS')) { 12 | return new GenericOperatorExpression( 13 | leftExpression, 14 | parser.parseExpression(this.precedence), 15 | '!=', 16 | (leftExpression: IExpression, rightExpression: IExpression) => 17 | leftExpression.eval() !== rightExpression.eval() 18 | ) 19 | } else { 20 | throw new Error(`! was used as a binary operator`) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/parser/expressions/arrayAccess.ts: -------------------------------------------------------------------------------- 1 | import { Expression, IExpression } from '../expression' 2 | 3 | export class ArrayAccessExpression extends Expression { 4 | type = 'ArrayAccessExpression' 5 | constructor(protected name: IExpression, protected lookup: IExpression) { 6 | super() 7 | } 8 | 9 | get allExpressions() { 10 | return [this.name, this.lookup] 11 | } 12 | setExpressionAt(index: number, expr: IExpression) { 13 | if (index === 0) this.name = expr 14 | else if (index === 1) this.lookup = expr 15 | } 16 | 17 | isStatic() { 18 | return false 19 | } 20 | 21 | setPointer(value: unknown) { 22 | ;(this.name.eval())[this.lookup.eval()] = value 23 | } 24 | 25 | eval() { 26 | return (this.name.eval())[this.lookup.eval()] 27 | } 28 | 29 | toString() { 30 | return `${this.name.toString()}[${this.lookup.toString()}]` 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /__tests__/minimize.ts: -------------------------------------------------------------------------------- 1 | import { Molang } from '../lib/main' 2 | import { test, expect } from 'vitest' 3 | 4 | test('Parse & stringify statements', () => { 5 | const molang = new Molang(undefined, { 6 | useCache: false, 7 | useOptimizer: true, 8 | useAggressiveStaticOptimizer: true, 9 | keepGroups: true, 10 | earlyReturnsSkipTokenization: false, 11 | earlyReturnsSkipParsing: false, 12 | }) 13 | 14 | const minimize = (str: string) => 15 | molang.minimize(molang.parse(str)).toString() 16 | 17 | const tests = { 18 | 'query.position()': 'q.position()', 19 | 'variable.x = 1; return variable.x;': 'v.x=1;return v.x;', 20 | 'return temp.my_var;': 'return t.v0;', 21 | '20 + 50': '70', 22 | 'variable.x + 0': 'v.x', 23 | '0 + variable.x': 'v.x', 24 | } 25 | 26 | for (const [test, result] of Object.entries(tests)) { 27 | expect(minimize(test)).toBe(result) 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /lib/custom/transformStatement.ts: -------------------------------------------------------------------------------- 1 | import { IExpression } from '../parser/expression' 2 | import { GroupExpression } from '../parser/expressions/group' 3 | import { ReturnExpression } from '../parser/expressions/return' 4 | import { StatementExpression } from '../parser/expressions/statement' 5 | 6 | export function transformStatement(expression: IExpression) { 7 | if (expression instanceof ReturnExpression) 8 | return new GroupExpression(expression.allExpressions[0], '()') 9 | if (!(expression instanceof StatementExpression)) return expression 10 | if (expression.allExpressions.length > 1) return expression 11 | 12 | // Only one statement, test whether it is a return statement 13 | const expr = expression.allExpressions[0] 14 | if (expr instanceof ReturnExpression) { 15 | return new GroupExpression(expr.allExpressions[0], '()') 16 | } else { 17 | return expression 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/parser/expressions/group.ts: -------------------------------------------------------------------------------- 1 | import { Expression, IExpression } from '../expression' 2 | 3 | export class GroupExpression extends Expression { 4 | type = 'GroupExpression' 5 | 6 | constructor(protected expression: IExpression, protected brackets: string) { 7 | super() 8 | } 9 | 10 | get allExpressions() { 11 | return [this.expression] 12 | } 13 | setExpressionAt(_: number, expr: IExpression) { 14 | this.expression = expr 15 | } 16 | 17 | isStatic() { 18 | return this.expression.isStatic() 19 | } 20 | get isReturn() { 21 | return this.expression.isReturn 22 | } 23 | get isBreak() { 24 | return this.expression.isBreak 25 | } 26 | get isContinue() { 27 | return this.expression.isContinue 28 | } 29 | 30 | eval() { 31 | return this.expression.eval() 32 | } 33 | toString() { 34 | return `${this.brackets[0]}${this.expression.toString()}${ 35 | this.brackets[1] 36 | }` 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /dist/custom/class.d.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parser/parse'; 2 | import { Token } from '../tokenizer/token'; 3 | import { IPrefixParselet } from '../parser/parselets/prefix'; 4 | import { Expression, IExpression } from '../parser/expression'; 5 | export declare class CustomClassParselet implements IPrefixParselet { 6 | precedence: number; 7 | constructor(precedence?: number); 8 | parse(parser: Parser, token: Token): CustomClassExpression; 9 | } 10 | declare class CustomClassExpression extends Expression { 11 | protected functionBody: IExpression; 12 | type: string; 13 | constructor(functions: Map, functionName: string, args: string[], functionBody: IExpression); 14 | get allExpressions(): IExpression[]; 15 | setExpressionAt(_: number, expr: IExpression): void; 16 | get isReturn(): boolean; 17 | isStatic(): boolean; 18 | eval(): number; 19 | } 20 | export {}; 21 | -------------------------------------------------------------------------------- /lib/parser/parselets/loop.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse' 2 | import { Token } from '../../tokenizer/token' 3 | import { IPrefixParselet } from './prefix' 4 | import { IExpression } from '../expression' 5 | import { LoopExpression } from '../expressions/loop' 6 | 7 | export class LoopParselet implements IPrefixParselet { 8 | constructor(public precedence = 0) {} 9 | 10 | parse(parser: Parser, token: Token) { 11 | parser.consume('LEFT_PARENT') 12 | const args: IExpression[] = [] 13 | 14 | if (parser.match('RIGHT_PARENT')) 15 | throw new Error(`loop() called without arguments`) 16 | 17 | do { 18 | args.push(parser.parseExpression()) 19 | } while (parser.match('COMMA')) 20 | parser.consume('RIGHT_PARENT') 21 | 22 | if (args.length !== 2) 23 | throw new Error( 24 | `There must be exactly two loop() arguments; found ${args.length}` 25 | ) 26 | 27 | return new LoopExpression(args[0], args[1]) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /dist/custom/function.d.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parser/parse'; 2 | import { Token } from '../parser/../tokenizer/token'; 3 | import { IPrefixParselet } from '../parser/parselets/prefix'; 4 | import { Expression, IExpression } from '../parser/expression'; 5 | export declare class CustomFunctionParselet implements IPrefixParselet { 6 | precedence: number; 7 | constructor(precedence?: number); 8 | parse(parser: Parser, token: Token): CustomFunctionExpression; 9 | } 10 | declare class CustomFunctionExpression extends Expression { 11 | protected functionBody: IExpression; 12 | type: string; 13 | constructor(functions: Map, functionName: string, args: string[], functionBody: IExpression); 14 | get allExpressions(): IExpression[]; 15 | setExpressionAt(_: number, expr: IExpression): void; 16 | get isReturn(): boolean; 17 | isStatic(): boolean; 18 | eval(): number; 19 | } 20 | export {}; 21 | -------------------------------------------------------------------------------- /dist/env/env.d.ts: -------------------------------------------------------------------------------- 1 | export declare type TVariableHandler = (variableName: string, variables: Record) => unknown; 2 | export interface IEnvConfig { 3 | useRadians?: boolean; 4 | convertUndefined?: boolean; 5 | variableHandler?: TVariableHandler; 6 | isFlat?: boolean; 7 | } 8 | export declare class ExecutionEnvironment { 9 | readonly config: IEnvConfig; 10 | protected env: Record; 11 | constructor(env: Record, config: IEnvConfig); 12 | updateConfig({ variableHandler, convertUndefined, useRadians, }: IEnvConfig): void; 13 | get(): Record; 14 | protected flattenEnv(newEnv: Record, addKey?: string, current?: any): any; 15 | setAt(lookup: string, value: unknown): unknown; 16 | getFrom(lookup: string): any; 17 | } 18 | export declare class Context { 19 | readonly env: any; 20 | readonly __isContext = true; 21 | constructor(env: any); 22 | } 23 | -------------------------------------------------------------------------------- /lib/parser/expressions/index.ts: -------------------------------------------------------------------------------- 1 | export { ArrayAccessExpression } from './arrayAccess' 2 | export { BooleanExpression } from './boolean' 3 | export { BreakExpression } from './break' 4 | export { ContinueExpression } from './continue' 5 | export { ForEachExpression } from './forEach' 6 | export { FunctionExpression } from './function' 7 | export { GenericOperatorExpression } from './genericOperator' 8 | export { GroupExpression } from './group' 9 | export { LoopExpression } from './loop' 10 | export { NameExpression } from './name' 11 | export { NumberExpression } from './number' 12 | export { PostfixExpression } from './postfix' 13 | export { PrefixExpression } from './prefix' 14 | export { ReturnExpression } from './return' 15 | export { StatementExpression } from './statement' 16 | export { StaticExpression } from './static' 17 | export { StringExpression } from './string' 18 | export { TernaryExpression } from './ternary' 19 | export { VoidExpression } from './void' 20 | -------------------------------------------------------------------------------- /lib/parser/parselets/forEach.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse' 2 | import { Token } from '../../tokenizer/token' 3 | import { IPrefixParselet } from './prefix' 4 | import { IExpression } from '../expression' 5 | import { ForEachExpression } from '../expressions/forEach' 6 | 7 | export class ForEachParselet implements IPrefixParselet { 8 | constructor(public precedence = 0) {} 9 | 10 | parse(parser: Parser, token: Token) { 11 | parser.consume('LEFT_PARENT') 12 | const args: IExpression[] = [] 13 | 14 | if (parser.match('RIGHT_PARENT')) 15 | throw new Error(`for_each() called without arguments`) 16 | 17 | do { 18 | args.push(parser.parseExpression()) 19 | } while (parser.match('COMMA')) 20 | parser.consume('RIGHT_PARENT') 21 | 22 | if (args.length !== 3) 23 | throw new Error( 24 | `There must be exactly three for_each() arguments; found ${args.length}` 25 | ) 26 | 27 | return new ForEachExpression(args[0], args[1], args[2]) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /__bench__/minimize.ts: -------------------------------------------------------------------------------- 1 | import { Molang } from '../lib/main' 2 | import { describe, bench } from 'vitest' 3 | 4 | describe('Molang minification', () => { 5 | const molang = new Molang(undefined, { 6 | useCache: false, 7 | useOptimizer: true, 8 | useAggressiveStaticOptimizer: true, 9 | keepGroups: true, 10 | earlyReturnsSkipTokenization: false, 11 | earlyReturnsSkipParsing: false, 12 | }) 13 | 14 | const minimize = (str: string) => 15 | molang.minimize(molang.parse(str)).toString() 16 | 17 | const expressions = [ 18 | 'variable.hand_bob = query.life_time < 0.01 ? 0.0 : variable.hand_bob + ((query.is_on_ground && query.is_alive ? math.clamp(math.sqrt(math.pow(query.position_delta(0), 2.0) + math.pow(query.position_delta(2), 2.0)), 0.0, 0.1) : 0.0) - variable.hand_bob) * 0.02;', 19 | ] 20 | 21 | for (const expression of expressions) { 22 | bench(expression, () => { 23 | minimize(expression) 24 | }) 25 | console.log(minimize(expression)) 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /dist/parser/expressions/index.d.ts: -------------------------------------------------------------------------------- 1 | export { ArrayAccessExpression } from './arrayAccess'; 2 | export { BooleanExpression } from './boolean'; 3 | export { BreakExpression } from './break'; 4 | export { ContinueExpression } from './continue'; 5 | export { ForEachExpression } from './forEach'; 6 | export { FunctionExpression } from './function'; 7 | export { GenericOperatorExpression } from './genericOperator'; 8 | export { GroupExpression } from './group'; 9 | export { LoopExpression } from './loop'; 10 | export { NameExpression } from './name'; 11 | export { NumberExpression } from './number'; 12 | export { PostfixExpression } from './postfix'; 13 | export { PrefixExpression } from './prefix'; 14 | export { ReturnExpression } from './return'; 15 | export { StatementExpression } from './statement'; 16 | export { StaticExpression } from './static'; 17 | export { StringExpression } from './string'; 18 | export { TernaryExpression } from './ternary'; 19 | export { VoidExpression } from './void'; 20 | -------------------------------------------------------------------------------- /lib/parser/expressions/genericOperator.ts: -------------------------------------------------------------------------------- 1 | import { Expression, IExpression } from '../expression' 2 | 3 | export class GenericOperatorExpression extends Expression { 4 | type = 'GenericOperatorExpression' 5 | 6 | constructor( 7 | protected left: IExpression, 8 | protected right: IExpression, 9 | public readonly operator: string, 10 | protected evalHelper: ( 11 | leftExpression: IExpression, 12 | rightExpression: IExpression 13 | ) => unknown 14 | ) { 15 | super() 16 | } 17 | 18 | get allExpressions() { 19 | return [this.left, this.right] 20 | } 21 | setExpressionAt(index: number, expr: IExpression) { 22 | if (index === 0) this.left = expr 23 | else if (index === 1) this.right = expr 24 | } 25 | 26 | isStatic() { 27 | return this.left.isStatic() && this.right.isStatic() 28 | } 29 | 30 | eval() { 31 | return this.evalHelper(this.left, this.right) 32 | } 33 | 34 | toString() { 35 | return `${this.left.toString()}${this.operator}${this.right.toString()}` 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/parser/parselets/function.ts: -------------------------------------------------------------------------------- 1 | import { Token } from '../../tokenizer/token' 2 | import { Parser } from '../parse' 3 | import { IInfixParselet } from './infix' 4 | import { IExpression } from '../expression' 5 | import { FunctionExpression } from '../expressions/function' 6 | import { NameExpression } from '../expressions' 7 | 8 | export class FunctionParselet implements IInfixParselet { 9 | constructor(public precedence = 0) {} 10 | 11 | parse(parser: Parser, left: IExpression, token: Token) { 12 | const args: IExpression[] = [] 13 | 14 | if (!left.setFunctionCall) 15 | throw new Error(`${left.type} is not callable!`) 16 | 17 | left.setFunctionCall(true) 18 | 19 | if (!parser.match('RIGHT_PARENT')) { 20 | do { 21 | args.push(parser.parseExpression()) 22 | } while (parser.match('COMMA')) 23 | parser.consume('RIGHT_PARENT') 24 | } 25 | 26 | // Must be a NameExpression because the .setFunctionCall() method exists 27 | return new FunctionExpression(left, args) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/parser/parselets/ternary.ts: -------------------------------------------------------------------------------- 1 | import { IInfixParselet } from './infix' 2 | import { Parser } from '../parse' 3 | import { IExpression } from '../expression' 4 | import { Token } from '../../tokenizer/token' 5 | import { TernaryExpression } from '../expressions/ternary' 6 | import { VoidExpression } from '../expressions/void' 7 | 8 | export class TernaryParselet implements IInfixParselet { 9 | exprName = 'Ternary' 10 | constructor(public precedence = 0) {} 11 | 12 | parse(parser: Parser, leftExpression: IExpression, token: Token) { 13 | let thenExpr = parser.parseExpression(this.precedence - 1) 14 | let elseExpr: IExpression 15 | 16 | if (parser.match('COLON')) { 17 | elseExpr = parser.parseExpression(this.precedence - 1) 18 | } else { 19 | elseExpr = new VoidExpression() 20 | } 21 | 22 | if (parser.config.useOptimizer && leftExpression.isStatic()) { 23 | return leftExpression.eval() ? thenExpr : elseExpr 24 | } 25 | 26 | return new TernaryExpression(leftExpression, thenExpr, elseExpr) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/parser/parselets/QuestionOperator.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse' 2 | import { Token } from '../../tokenizer/token' 3 | import { IExpression } from '../expression' 4 | import { GenericOperatorExpression } from '../expressions/genericOperator' 5 | import { IInfixParselet } from './infix' 6 | import { TernaryParselet } from './ternary' 7 | import { EPrecedence } from '../precedence' 8 | 9 | const ternaryParselet = new TernaryParselet(EPrecedence.CONDITIONAL) 10 | export class QuestionOperator implements IInfixParselet { 11 | constructor(public precedence = 0) {} 12 | 13 | parse(parser: Parser, leftExpression: IExpression, token: Token) { 14 | if (parser.match('QUESTION')) { 15 | return new GenericOperatorExpression( 16 | leftExpression, 17 | parser.parseExpression(this.precedence), 18 | '??', 19 | (leftExpression: IExpression, rightExpression: IExpression) => 20 | leftExpression.eval() ?? rightExpression.eval() 21 | ) 22 | } else { 23 | return ternaryParselet.parse(parser, leftExpression, token) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /dist/MoLang.d.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionEnvironment } from './env/env'; 2 | import { IExpression, IParserConfig } from './main'; 3 | import { MolangParser } from './parser/molang'; 4 | export declare class Molang { 5 | protected config: Partial; 6 | protected expressionCache: Record; 7 | protected totalCacheEntries: number; 8 | protected executionEnvironment: ExecutionEnvironment; 9 | protected parser: MolangParser; 10 | constructor(env?: Record, config?: Partial); 11 | updateConfig(newConfig: Partial): void; 12 | updateExecutionEnv(env: Record, isFlat?: boolean): void; 13 | clearCache(): void; 14 | execute(expression: string): unknown; 15 | executeAndCatch(expression: string): unknown; 16 | parse(expression: string): IExpression; 17 | rearrangeOptimally(ast: IExpression): IExpression; 18 | resolveStatic(ast: IExpression): IExpression; 19 | minimize(ast: IExpression): IExpression; 20 | getParser(): MolangParser; 21 | } 22 | -------------------------------------------------------------------------------- /dist/parser/parselets/binaryOperator.d.ts: -------------------------------------------------------------------------------- 1 | import { IInfixParselet } from './infix'; 2 | import { Parser } from '../parse'; 3 | import { IExpression } from '../expression'; 4 | import { Token } from '../../tokenizer/token'; 5 | import { GenericOperatorExpression } from '../expressions/genericOperator'; 6 | export declare const plusHelper: (leftExpression: IExpression, rightExpression: IExpression) => any; 7 | export declare const minusHelper: (leftExpression: IExpression, rightExpression: IExpression) => number; 8 | export declare const divideHelper: (leftExpression: IExpression, rightExpression: IExpression) => number; 9 | export declare const multiplyHelper: (leftExpression: IExpression, rightExpression: IExpression) => number; 10 | export declare const assignHelper: (leftExpression: IExpression, rightExpression: IExpression) => number; 11 | export declare class BinaryOperator implements IInfixParselet { 12 | precedence: number; 13 | constructor(precedence?: number); 14 | parse(parser: Parser, leftExpression: IExpression, token: Token): GenericOperatorExpression; 15 | } 16 | -------------------------------------------------------------------------------- /lib/parser/expressions/name.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionEnvironment } from '../../env/env' 2 | import { Expression } from '../expression' 3 | 4 | export class NameExpression extends Expression { 5 | type = 'NameExpression' 6 | 7 | constructor( 8 | public executionEnv: ExecutionEnvironment, 9 | protected name: string, 10 | protected isFunctionCall = false 11 | ) { 12 | super() 13 | } 14 | 15 | get allExpressions() { 16 | return [] 17 | } 18 | setExpressionAt() {} 19 | 20 | isStatic() { 21 | return false 22 | } 23 | 24 | setPointer(value: unknown) { 25 | this.executionEnv.setAt(this.name, value) 26 | } 27 | 28 | setFunctionCall(value = true) { 29 | this.isFunctionCall = value 30 | } 31 | setName(name: string) { 32 | this.name = name 33 | } 34 | setExecutionEnv(executionEnv: ExecutionEnvironment) { 35 | this.executionEnv = executionEnv 36 | } 37 | 38 | eval() { 39 | const value = this.executionEnv.getFrom(this.name) 40 | if (!this.isFunctionCall && typeof value === 'function') return value() 41 | return value 42 | } 43 | 44 | toString() { 45 | return this.name 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/parser/parselets/GreaterOperator.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse' 2 | import { Token } from '../../tokenizer/token' 3 | import { IExpression } from '../expression' 4 | import { GenericOperatorExpression } from '../expressions/genericOperator' 5 | import { IInfixParselet } from './infix' 6 | 7 | export class GreaterOperator implements IInfixParselet { 8 | constructor(public precedence = 0) {} 9 | 10 | parse(parser: Parser, leftExpression: IExpression, token: Token) { 11 | if (parser.match('EQUALS')) 12 | return new GenericOperatorExpression( 13 | leftExpression, 14 | parser.parseExpression(this.precedence), 15 | '>=', 16 | (leftExpression: IExpression, rightExpression: IExpression) => 17 | // @ts-ignore 18 | leftExpression.eval() >= rightExpression.eval() 19 | ) 20 | else { 21 | return new GenericOperatorExpression( 22 | leftExpression, 23 | parser.parseExpression(this.precedence), 24 | '>', 25 | (leftExpression: IExpression, rightExpression: IExpression) => 26 | // @ts-ignore 27 | leftExpression.eval() > rightExpression.eval() 28 | ) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/parser/parselets/SmallerOperator.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse' 2 | import { Token } from '../../tokenizer/token' 3 | import { IExpression } from '../expression' 4 | import { GenericOperatorExpression } from '../expressions/genericOperator' 5 | import { IInfixParselet } from './infix' 6 | 7 | export class SmallerOperator implements IInfixParselet { 8 | constructor(public precedence = 0) {} 9 | 10 | parse(parser: Parser, leftExpression: IExpression, token: Token) { 11 | if (parser.match('EQUALS')) 12 | return new GenericOperatorExpression( 13 | leftExpression, 14 | parser.parseExpression(this.precedence), 15 | '<=', 16 | (leftExpression: IExpression, rightExpression: IExpression) => 17 | // @ts-ignore 18 | leftExpression.eval() <= rightExpression.eval() 19 | ) 20 | else { 21 | return new GenericOperatorExpression( 22 | leftExpression, 23 | parser.parseExpression(this.precedence), 24 | '<', 25 | (leftExpression: IExpression, rightExpression: IExpression) => 26 | // @ts-ignore 27 | leftExpression.eval() < rightExpression.eval() 28 | ) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bridge-editor/molang", 3 | "version": "2.0.2", 4 | "description": "A fast parser for Minecraft's MoLang", 5 | "main": "./dist/molang.umd.js", 6 | "module": "./dist/molang.es.js", 7 | "types": "./dist/main.d.ts", 8 | "exports": { 9 | ".": { 10 | "import": "./dist/molang.es.js", 11 | "require": "./dist/molang.umd.js" 12 | } 13 | }, 14 | "directories": { 15 | "lib": "lib" 16 | }, 17 | "scripts": { 18 | "build:types": "tsc --project tsconfig.json", 19 | "build:only": "vite build", 20 | "build": "npm run build:only && npm run build:types", 21 | "test": "vitest", 22 | "bench": "vitest bench" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/bridge-core/molang.git" 27 | }, 28 | "author": "solvedDev", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/bridge-core/molang/issues" 32 | }, 33 | "homepage": "https://github.com/bridge-core/molang#readme", 34 | "devDependencies": { 35 | "@types/node": "^13.1.2", 36 | "molangjs": "^1.5.0", 37 | "typescript": "^4.2.3", 38 | "vite": "^3.1.3", 39 | "vitest": "^0.23.4" 40 | } 41 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 solvedDev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /__tests__/stringify.ts: -------------------------------------------------------------------------------- 1 | import { Molang } from '../lib/main' 2 | import { test, expect } from 'vitest' 3 | 4 | test('Parse & stringify statements', () => { 5 | const molang = new Molang( 6 | { 'variable.is_true': 1 }, 7 | { 8 | useCache: false, 9 | useOptimizer: true, 10 | useAggressiveStaticOptimizer: true, 11 | keepGroups: true, 12 | earlyReturnsSkipTokenization: false, 13 | earlyReturnsSkipParsing: false, 14 | } 15 | ) 16 | 17 | const tests = { 18 | 'v.is_false ? v.x': 'v.is_false?v.x', 19 | 'v.is_true ? v.x : v.y': 'v.is_true?v.x:v.y', 20 | 'return v.test ? v.x : v.y;': 'return v.test?v.x:v.y;', 21 | 'loop(10, {v.x = 1 + 2 * 4;}); return v.x;': 22 | 'loop(10,{v.x=1+2*4;});return v.x;', 23 | '(v.x + v.y) * v.z': '(v.x+v.y)*v.z', 24 | "1 ? '1' : 'other'; return 1;": 'return 1;', 25 | "return 1 ? '1' : 'other';": "return '1';", 26 | "1 ? '1' : 'other'": "'1'", 27 | 'array.t[v.t]': 'array.t[v.t]', 28 | 'return -(1+1);': 'return -(1+1);', 29 | 'return .5;': 'return .5;', 30 | } 31 | 32 | for (const [test, result] of Object.entries(tests)) { 33 | expect(molang.parse(test).toString()).toBe(result) 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /__tests__/env.ts: -------------------------------------------------------------------------------- 1 | import { Molang } from '../lib/main' 2 | import { test, expect } from 'vitest' 3 | 4 | test('execute without provided env', () => { 5 | const molang = new Molang() 6 | expect(molang.execute('math.pow(2,2)')).toBe(4) 7 | }) 8 | 9 | // Test standard env 10 | const molang = new Molang({ 11 | 'query.array': [1, 2, 3], 12 | 'query.simple': 2, 13 | }) 14 | 15 | const tests: Record = { 16 | 'query.any(1,1,2,3)': 1, 17 | 'query.any(1,2,3)': 0, 18 | 'query.all(1,1,2,3)': 0, 19 | 'query.all(1,1,1,1)': 1, 20 | 'query.in_range(1,1,2)': 1, 21 | 'query.in_range(2,1,2)': 1, 22 | 'query.in_range(3,1,2)': 0, 23 | 'query.count(q.array)': 3, 24 | 'query.count(3)': 1, 25 | 'query.self->query.simple': 2, 26 | 'query.self->query.count(2)': 1, 27 | } 28 | 29 | test('Standard Environment', () => { 30 | for (const test in tests) { 31 | expect(molang.execute(test)).toBe(tests[test]) 32 | } 33 | }) 34 | 35 | test('Object within env', () => { 36 | const molang = new Molang({ 37 | 'context.position': { 38 | x: () => 1, 39 | y: () => 2, 40 | }, 41 | }) 42 | 43 | expect(molang.execute('c.position.x')).toBe(1) 44 | expect(molang.execute('c.position.y')).toBe(2) 45 | }) 46 | -------------------------------------------------------------------------------- /dist/parser/expression.d.ts: -------------------------------------------------------------------------------- 1 | export interface IExpression { 2 | readonly type: string; 3 | readonly isReturn?: boolean; 4 | readonly isBreak?: boolean; 5 | readonly isContinue?: boolean; 6 | readonly allExpressions: IExpression[]; 7 | setFunctionCall?: (value: boolean) => void; 8 | setPointer?: (value: unknown) => void; 9 | setExpressionAt(index: number, expr: IExpression): void; 10 | eval(): unknown; 11 | isStatic(): boolean; 12 | walk(cb: TIterateCallback): IExpression; 13 | iterate(cb: TIterateCallback, visited: Set): void; 14 | some(predicate: (expr: IExpression) => boolean): boolean; 15 | } 16 | export declare abstract class Expression implements IExpression { 17 | abstract readonly type: string; 18 | abstract eval(): unknown; 19 | abstract isStatic(): boolean; 20 | toString(): string; 21 | abstract allExpressions: IExpression[]; 22 | abstract setExpressionAt(index: number, expr: IExpression): void; 23 | walk(cb: TIterateCallback, visited?: Set): IExpression; 24 | iterate(cb: TIterateCallback, visited: Set): void; 25 | some(predicate: (expr: IExpression) => boolean): boolean; 26 | } 27 | export declare type TIterateCallback = (expr: IExpression) => IExpression | undefined; 28 | -------------------------------------------------------------------------------- /lib/parser/expressions/prefix.ts: -------------------------------------------------------------------------------- 1 | import { TTokenType } from '../../tokenizer/token' 2 | import { Expression, IExpression } from '../expression' 3 | 4 | export class PrefixExpression extends Expression { 5 | type = 'PrefixExpression' 6 | 7 | constructor( 8 | protected tokenType: TTokenType, 9 | protected expression: IExpression 10 | ) { 11 | super() 12 | } 13 | 14 | get allExpressions() { 15 | return [this.expression] 16 | } 17 | setExpressionAt(_: number, expr: IExpression) { 18 | this.expression = expr 19 | } 20 | 21 | isStatic() { 22 | return this.expression.isStatic() 23 | } 24 | 25 | eval() { 26 | const value = this.expression.eval() 27 | 28 | if (typeof value !== 'number') 29 | throw new Error( 30 | `Cannot use "${ 31 | this.tokenType 32 | }" operator in front of ${typeof value} "${value}"` 33 | ) 34 | 35 | switch (this.tokenType) { 36 | case 'MINUS': { 37 | return -value 38 | } 39 | case 'BANG': { 40 | return !value 41 | } 42 | } 43 | } 44 | 45 | toString() { 46 | switch (this.tokenType) { 47 | case 'MINUS': { 48 | return `-${this.expression.toString()}` 49 | } 50 | case 'BANG': { 51 | return `!${this.expression.toString()}` 52 | } 53 | } 54 | 55 | throw new Error(`Unknown prefix operator: "${this.tokenType}"`) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/parser/expressions/ContextSwitch.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionEnvironment } from '../../env/env' 2 | import { Expression, IExpression } from '../expression' 3 | import { FunctionExpression } from './function' 4 | import { NameExpression } from './name' 5 | 6 | export class ContextSwitchExpression extends Expression { 7 | type = 'NameExpression' 8 | 9 | constructor( 10 | protected leftExpr: NameExpression | FunctionExpression, 11 | protected rightExpr: NameExpression | FunctionExpression 12 | ) { 13 | super() 14 | } 15 | 16 | get allExpressions() { 17 | return [this.leftExpr, this.rightExpr] 18 | } 19 | setExpressionAt(index: number, expr: IExpression) { 20 | if (!(expr instanceof NameExpression)) 21 | throw new Error( 22 | `Cannot use context switch operator "->" on ${expr.type}` 23 | ) 24 | 25 | if (index === 0) this.leftExpr = expr 26 | else if (index === 1) this.rightExpr = expr 27 | } 28 | 29 | isStatic() { 30 | return false 31 | } 32 | 33 | eval() { 34 | const context = this.leftExpr.eval() 35 | if (typeof context !== 'object') return 0 36 | 37 | this.rightExpr.setExecutionEnv( 38 | new ExecutionEnvironment( 39 | context, 40 | this.rightExpr.executionEnv.config 41 | ) 42 | ) 43 | return this.rightExpr.eval() 44 | } 45 | 46 | toString() { 47 | return `${this.leftExpr.toString()}->${this.rightExpr.toString()}` 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/parser/parselets/name.ts: -------------------------------------------------------------------------------- 1 | import { IPrefixParselet } from './prefix' 2 | import { Token } from '../../tokenizer/token' 3 | import { Parser } from '../parse' 4 | import { NameExpression } from '../expressions/name' 5 | import { ContextSwitchExpression } from '../expressions/ContextSwitch' 6 | import { FunctionExpression } from '../expressions/function' 7 | import { EPrecedence } from '../precedence' 8 | 9 | export class NameParselet implements IPrefixParselet { 10 | constructor(public precedence = 0) {} 11 | 12 | parse(parser: Parser, token: Token) { 13 | const nameExpr = new NameExpression( 14 | parser.executionEnv, 15 | token.getText() 16 | ) 17 | const nextTokens = [parser.lookAhead(0), parser.lookAhead(1)] 18 | 19 | // Context switching operator "->" 20 | if ( 21 | nextTokens[0].getType() === 'MINUS' && 22 | nextTokens[1].getType() === 'GREATER' 23 | ) { 24 | parser.consume('MINUS') 25 | parser.consume('GREATER') 26 | 27 | const expr = parser.parseExpression(EPrecedence.FUNCTION - 1) 28 | 29 | if ( 30 | expr.type !== 'NameExpression' && 31 | expr.type !== 'FunctionExpression' 32 | ) 33 | throw new Error( 34 | `Cannot use context switch operator "->" on ${expr.type}` 35 | ) 36 | 37 | return new ContextSwitchExpression( 38 | nameExpr, 39 | expr 40 | ) 41 | } 42 | 43 | return nameExpr 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/parser/expressions/function.ts: -------------------------------------------------------------------------------- 1 | import { NameExpression } from './name' 2 | import { Expression, IExpression } from '../expression' 3 | import { ExecutionEnvironment } from '../../env/env' 4 | 5 | export class FunctionExpression extends Expression { 6 | type = 'FunctionExpression' 7 | 8 | constructor(protected name: NameExpression, protected args: IExpression[]) { 9 | super() 10 | } 11 | 12 | get allExpressions() { 13 | return [this.name, ...this.args] 14 | } 15 | setExpressionAt(index: number, expr: Expression) { 16 | if (index === 0) this.name = expr 17 | else if (index > 0) this.args[index - 1] = expr 18 | } 19 | setExecutionEnv(executionEnv: ExecutionEnvironment) { 20 | this.name.setExecutionEnv(executionEnv) 21 | } 22 | get executionEnv() { 23 | return this.name.executionEnv 24 | } 25 | 26 | isStatic() { 27 | return false 28 | } 29 | 30 | eval() { 31 | const args: unknown[] = [] 32 | let i = 0 33 | while (i < this.args.length) args.push(this.args[i++].eval()) 34 | 35 | const func = <(...args: unknown[]) => unknown>this.name.eval() 36 | if (typeof func !== 'function') 37 | throw new Error( 38 | `${(this.name).toString()} is not callable!` 39 | ) 40 | return func(...args) 41 | } 42 | 43 | toString() { 44 | let str = `${this.name.toString()}(` 45 | for (let i = 0; i < this.args.length; i++) { 46 | str += `${this.args[i].toString()}${ 47 | i + 1 < this.args.length ? ',' : '' 48 | }` 49 | } 50 | 51 | return `${str})` 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /dist/env/math.d.ts: -------------------------------------------------------------------------------- 1 | export declare const MolangMathLib: (useRadians: boolean) => { 2 | 'math.abs': (x: number) => number; 3 | 'math.acos': (x: number) => number; 4 | 'math.asin': (x: number) => number; 5 | 'math.atan': (x: number) => number; 6 | 'math.atan2': (y: number, x: number) => number; 7 | 'math.ceil': (x: number) => number; 8 | 'math.clamp': (value: number, min: number, max: number) => number; 9 | 'math.cos': (x: number) => number; 10 | 'math.die_roll': (sum: number, low: number, high: number) => number; 11 | 'math.die_roll_integer': (sum: number, low: number, high: number) => number; 12 | 'math.exp': (x: number) => number; 13 | 'math.floor': (x: number) => number; 14 | 'math.hermite_blend': (value: number) => number; 15 | 'math.lerp': (start: number, end: number, amount: number) => number; 16 | 'math.lerp_rotate': (start: number, end: number, amount: number) => number; 17 | 'math.ln': (x: number) => number; 18 | 'math.max': (...values: number[]) => number; 19 | 'math.min': (...values: number[]) => number; 20 | 'math.min_angle': (value: number) => number; 21 | 'math.mod': (value: number, denominator: number) => number; 22 | 'math.pi': number; 23 | 'math.pow': (x: number, y: number) => number; 24 | 'math.random': (low: number, high: number) => number; 25 | 'math.random_integer': (low: number, high: number) => number; 26 | 'math.round': (x: number) => number; 27 | 'math.sin': (x: number) => number; 28 | 'math.sqrt': (x: number) => number; 29 | 'math.trunc': (x: number) => number; 30 | }; 31 | -------------------------------------------------------------------------------- /dist/parser/parse.d.ts: -------------------------------------------------------------------------------- 1 | import { Tokenizer } from '../tokenizer/Tokenizer'; 2 | import { TTokenType, Token } from '../tokenizer/token'; 3 | import { IPrefixParselet } from './parselets/prefix'; 4 | import { IInfixParselet } from './parselets/infix'; 5 | import { IExpression } from './expression'; 6 | import { ExecutionEnvironment } from '../env/env'; 7 | import { IParserConfig } from '../main'; 8 | export declare class Parser { 9 | config: Partial; 10 | protected prefixParselets: Map; 11 | protected infixParselets: Map; 12 | protected readTokens: Token[]; 13 | protected tokenIterator: Tokenizer; 14 | executionEnv: ExecutionEnvironment; 15 | constructor(config: Partial); 16 | updateConfig(config: Partial): void; 17 | init(expression: string): void; 18 | setTokenizer(tokenizer: Tokenizer): void; 19 | setExecutionEnvironment(executionEnv: ExecutionEnvironment): void; 20 | parseExpression(precedence?: number): IExpression; 21 | parseInfixExpression(expressionLeft: IExpression, precedence?: number): IExpression; 22 | getPrecedence(): number; 23 | consume(expected?: TTokenType): Token; 24 | match(expected: TTokenType, consume?: boolean): boolean; 25 | lookAhead(distance: number): Token; 26 | registerInfix(tokenType: TTokenType, infixParselet: IInfixParselet): void; 27 | registerPrefix(tokenType: TTokenType, prefixParselet: IPrefixParselet): void; 28 | getInfix(tokenType: TTokenType): IInfixParselet | undefined; 29 | getPrefix(tokenType: TTokenType): IPrefixParselet | undefined; 30 | } 31 | -------------------------------------------------------------------------------- /lib/parser/expressions/loop.ts: -------------------------------------------------------------------------------- 1 | import { Expression, IExpression } from '../expression' 2 | 3 | export class LoopExpression extends Expression { 4 | type = 'LoopExpression' 5 | 6 | constructor( 7 | protected count: IExpression, 8 | protected expression: IExpression 9 | ) { 10 | super() 11 | } 12 | 13 | get allExpressions() { 14 | return [this.count, this.expression] 15 | } 16 | get isNoopLoop() { 17 | return this.count.isStatic() && this.count.eval() === 0 18 | } 19 | 20 | setExpressionAt(index: number, expr: IExpression) { 21 | if (index === 0) this.count = expr 22 | else if (index === 1) this.expression = expr 23 | } 24 | 25 | get isReturn() { 26 | return this.isNoopLoop ? false : this.expression.isReturn 27 | } 28 | 29 | isStatic() { 30 | return this.count.isStatic() && this.expression.isStatic() 31 | } 32 | 33 | eval() { 34 | const repeatCount = Number(this.count.eval()) 35 | 36 | if(repeatCount === 0) return 0 37 | if (Number.isNaN(repeatCount)) 38 | throw new Error( 39 | `First loop() argument must be of type number, received "${typeof this.count.eval()}"` 40 | ) 41 | if (repeatCount > 1024) 42 | throw new Error( 43 | `Cannot loop more than 1024x times, received "${repeatCount}"` 44 | ) 45 | 46 | let i = 0 47 | while (i < repeatCount) { 48 | i++ 49 | const res = this.expression.eval() 50 | 51 | if (this.expression.isBreak) break 52 | else if (this.expression.isContinue) continue 53 | else if (this.expression.isReturn) return res 54 | } 55 | 56 | return 0 57 | } 58 | 59 | toString() { 60 | if(this.isNoopLoop) return '' 61 | 62 | return `loop(${this.count.toString()},${this.expression.toString()})` 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/parser/expressions/forEach.ts: -------------------------------------------------------------------------------- 1 | import { Expression, IExpression } from '../expression' 2 | 3 | export class ForEachExpression extends Expression { 4 | type = 'ForEachExpression' 5 | 6 | constructor( 7 | protected variable: IExpression, 8 | protected arrayExpression: IExpression, 9 | protected expression: IExpression 10 | ) { 11 | super() 12 | if (!this.variable.setPointer) 13 | throw new Error( 14 | `First for_each() argument must be a variable, received "${typeof this.variable.eval()}"` 15 | ) 16 | } 17 | 18 | get isReturn() { 19 | return this.expression.isReturn 20 | } 21 | get allExpressions() { 22 | return [this.variable, this.arrayExpression, this.expression] 23 | } 24 | setExpressionAt(index: number, expr: IExpression) { 25 | if (index === 0) this.variable = expr 26 | else if (index === 1) this.arrayExpression = expr 27 | else if (index === 2) this.expression = expr 28 | } 29 | 30 | isStatic() { 31 | return ( 32 | this.variable.isStatic() && 33 | this.arrayExpression.isStatic() && 34 | this.expression.isStatic() 35 | ) 36 | } 37 | 38 | eval() { 39 | const array = this.arrayExpression.eval() 40 | if (!Array.isArray(array)) 41 | throw new Error( 42 | `Second for_each() argument must be an array, received "${typeof array}"` 43 | ) 44 | 45 | let i = 0 46 | while (i < array.length) { 47 | // Error detection for this.variable is part of the constructor 48 | this.variable.setPointer?.(array[i++]) 49 | 50 | const res = this.expression.eval() 51 | 52 | if (this.expression.isBreak) break 53 | else if (this.expression.isContinue) continue 54 | else if (this.expression.isReturn) return res 55 | } 56 | 57 | return 0 58 | } 59 | 60 | toString() { 61 | return `for_each(${this.variable.toString()},${this.arrayExpression.toString()},${this.expression.toString()})` 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /__bench__/main.ts: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | import MolangJS from 'molangjs' 3 | import { Molang } from '../lib/main' 4 | import { Tokenizer } from '../lib/tokenizer/Tokenizer' 5 | import { bench, describe } from 'vitest' 6 | 7 | const expression = 8 | 'variable.hand_bob = query.life_time < 0.01 ? 0.0 : variable.hand_bob + ((query.is_on_ground && query.is_alive ? math.clamp(math.sqrt(math.pow(query.position_delta(0), 2.0) + math.pow(query.position_delta(2), 2.0)), 0.0, 0.1) : 0.0) - variable.hand_bob) * 0.02;' 9 | 10 | const env = { 11 | 'variable.hand_bob': 0, 12 | 'query.life_time': () => 0.1, 13 | 'query.is_on_ground': () => 1, 14 | 'query.is_alive': () => 1, 15 | 'query.position_delta': () => 2, 16 | } 17 | 18 | const molang = new Molang(env, { 19 | useCache: false, 20 | }) 21 | 22 | const tokenizer = new Tokenizer() 23 | describe('Molang Tokenizer', () => { 24 | bench('Init', () => { 25 | tokenizer.init(expression) 26 | }) 27 | bench('Tokenize', () => { 28 | while (tokenizer.hasNext()) tokenizer.next() 29 | }) 30 | }) 31 | 32 | const molangjs = new MolangJS() 33 | molangjs.cache_enabled = false 34 | 35 | describe('Raw Parse & Execute', () => { 36 | bench('Molang', () => { 37 | molang.execute(expression) 38 | }) 39 | 40 | bench('MolangJS', () => { 41 | molangjs.parse(expression, env) 42 | }) 43 | }) 44 | 45 | // Update Molang 46 | molang.clearCache() 47 | molang.updateConfig({ useCache: true }) 48 | // Update MolangJS 49 | molangjs.cache_enabled = true 50 | describe('Cached Parse & Execute', () => { 51 | bench('Molang', () => { 52 | molang.execute(expression) 53 | }) 54 | 55 | bench('MolangJS', () => { 56 | molangjs.parse(expression, env) 57 | }) 58 | }) 59 | 60 | describe('Execute', () => { 61 | bench('Molang', () => { 62 | molang.execute(expression) 63 | }) 64 | 65 | bench('MolangJS', () => { 66 | molangjs.parse(expression, env) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /dist/env/standardEnv.d.ts: -------------------------------------------------------------------------------- 1 | export declare const standardEnv: (useRadians: boolean) => { 2 | 'query.in_range': (value: number, min: number, max: number) => boolean; 3 | 'query.all': (mustMatch: unknown, ...values: unknown[]) => boolean; 4 | 'query.any': (mustMatch: unknown, ...values: unknown[]) => boolean; 5 | 'query.count': (countable: unknown) => number; 6 | 'math.abs': (x: number) => number; 7 | 'math.acos': (x: number) => number; 8 | 'math.asin': (x: number) => number; 9 | 'math.atan': (x: number) => number; 10 | 'math.atan2': (y: number, x: number) => number; 11 | 'math.ceil': (x: number) => number; 12 | 'math.clamp': (value: number, min: number, max: number) => number; 13 | 'math.cos': (x: number) => number; 14 | 'math.die_roll': (sum: number, low: number, high: number) => number; 15 | 'math.die_roll_integer': (sum: number, low: number, high: number) => number; 16 | 'math.exp': (x: number) => number; 17 | 'math.floor': (x: number) => number; 18 | 'math.hermite_blend': (value: number) => number; 19 | 'math.lerp': (start: number, end: number, amount: number) => number; 20 | 'math.lerp_rotate': (start: number, end: number, amount: number) => number; 21 | 'math.ln': (x: number) => number; 22 | 'math.max': (...values: number[]) => number; 23 | 'math.min': (...values: number[]) => number; 24 | 'math.min_angle': (value: number) => number; 25 | 'math.mod': (value: number, denominator: number) => number; 26 | 'math.pi': number; 27 | 'math.pow': (x: number, y: number) => number; 28 | 'math.random': (low: number, high: number) => number; 29 | 'math.random_integer': (low: number, high: number) => number; 30 | 'math.round': (x: number) => number; 31 | 'math.sin': (x: number) => number; 32 | 'math.sqrt': (x: number) => number; 33 | 'math.trunc': (x: number) => number; 34 | }; 35 | -------------------------------------------------------------------------------- /lib/parser/expression.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface that describes an AST Expression 3 | */ 4 | export interface IExpression { 5 | readonly type: string 6 | readonly isReturn?: boolean 7 | readonly isBreak?: boolean 8 | readonly isContinue?: boolean 9 | readonly allExpressions: IExpression[] 10 | 11 | setFunctionCall?: (value: boolean) => void 12 | setPointer?: (value: unknown) => void 13 | setExpressionAt(index: number, expr: IExpression): void 14 | eval(): unknown 15 | isStatic(): boolean 16 | walk(cb: TIterateCallback): IExpression 17 | iterate(cb: TIterateCallback, visited: Set): void 18 | some(predicate: (expr: IExpression) => boolean): boolean 19 | } 20 | 21 | export abstract class Expression implements IExpression { 22 | public abstract readonly type: string 23 | 24 | abstract eval(): unknown 25 | abstract isStatic(): boolean 26 | 27 | toString() { 28 | return `${this.eval()}` 29 | } 30 | 31 | abstract allExpressions: IExpression[] 32 | abstract setExpressionAt(index: number, expr: IExpression): void 33 | 34 | walk(cb: TIterateCallback, visited = new Set()): IExpression { 35 | let expr = cb(this) ?? this 36 | 37 | expr.iterate(cb, visited) 38 | 39 | return expr 40 | } 41 | iterate(cb: TIterateCallback, visited: Set) { 42 | for (let i = 0; i < this.allExpressions.length; i++) { 43 | const originalExpr = this.allExpressions[i] 44 | if (visited.has(originalExpr)) continue 45 | else visited.add(originalExpr) 46 | 47 | const expr = cb(originalExpr) ?? originalExpr 48 | 49 | if (expr !== originalExpr && visited.has(expr)) continue 50 | else visited.add(expr) 51 | 52 | this.setExpressionAt(i, expr) 53 | expr.iterate(cb, visited) 54 | } 55 | } 56 | some(predicate: (expr: IExpression) => boolean) { 57 | return this.allExpressions.some( 58 | (expr) => predicate(expr) || expr.some(predicate) 59 | ) 60 | } 61 | } 62 | 63 | export type TIterateCallback = (expr: IExpression) => IExpression | undefined 64 | -------------------------------------------------------------------------------- /__tests__/custom/class.ts: -------------------------------------------------------------------------------- 1 | import { CustomMolang } from '../../lib/custom/main' 2 | import { test, expect } from 'vitest' 3 | 4 | test('Custom syntax', () => { 5 | const customMolang = new CustomMolang({}) 6 | 7 | // customMolang.parse( 8 | // ` 9 | // # A basic custom class 10 | // class('linked_list', { 11 | // # Variables are accessible from outside of the class definition 12 | // v.length = 0; 13 | // # Temporary variables can only be accessed within the class definition 14 | // t.first; 15 | 16 | // # Functions declared within a class automatically become class methods 17 | // function("add_first", "element", { 18 | // v.length = v.length + 1; 19 | 20 | // t.newfirst.element = arg.element; 21 | // t.newfirst.next = t.first; 22 | // t.first = t.newfirst; 23 | // return; 24 | // }); 25 | 26 | // function("add_last", "element", { 27 | // v.length = v.length + 1; 28 | 29 | // t.curr = t.first; 30 | // loop(v.length - 1, { 31 | // t.curr = t.curr.next; 32 | // }); 33 | // t.newele.element = arg.element; 34 | // t.curr = t.newele; 35 | // (!t.first) ? t.first = t.curr; 36 | // return; 37 | // }); 38 | 39 | // function("get_element", "index", { 40 | // t.curr = t.first; 41 | // t.ele = t.curr.element; 42 | // loop(arg.index, { 43 | // t.curr = t.curr.next; 44 | // t.ele = t.curr.element; 45 | // }); 46 | // return t.ele; 47 | // }); 48 | // }); 49 | 50 | // # Usage: 51 | // # v.list = class.linked_list(); 52 | // # q.log(v.list.length); 53 | // # v.list.add_first(1); 54 | // # q.log(v.list.length); 55 | // # q.log(v.list.get_element(0)); 56 | // ` 57 | // ) 58 | 59 | // expect( 60 | // customMolang.transform( 61 | // 'v.list = class.linked_list(); v.list.add_first(1); return v.list.get_element(0);' 62 | // ) 63 | // ).toBe(`v.list.length=0;v.list.first=0;v.list.length=v.list.length+1;`) 64 | }) 65 | -------------------------------------------------------------------------------- /lib/parser/expressions/ternary.ts: -------------------------------------------------------------------------------- 1 | import { Expression, IExpression } from '../expression' 2 | import { VoidExpression } from './void' 3 | 4 | export class TernaryExpression extends Expression { 5 | type = 'TernaryExpression' 6 | protected leftResult: unknown 7 | 8 | constructor( 9 | protected leftExpression: IExpression, 10 | protected thenExpression: IExpression, 11 | protected elseExpression: IExpression 12 | ) { 13 | super() 14 | } 15 | 16 | get allExpressions() { 17 | if (this.leftExpression.isStatic()) 18 | return [ 19 | this.leftExpression, 20 | this.leftExpression.eval() 21 | ? this.thenExpression 22 | : this.elseExpression, 23 | ] 24 | return [this.leftExpression, this.thenExpression, this.elseExpression] 25 | } 26 | setExpressionAt(index: number, expr: IExpression) { 27 | if (index === 0) this.leftExpression = expr 28 | else if (index === 1) this.thenExpression = expr 29 | else if (index === 2) this.elseExpression = expr 30 | } 31 | 32 | get isReturn() { 33 | if (this.leftResult === undefined) 34 | return this.thenExpression.isReturn && this.elseExpression.isReturn 35 | return this.leftResult 36 | ? this.thenExpression.isReturn 37 | : this.elseExpression.isReturn 38 | } 39 | get hasReturn() { 40 | return this.thenExpression.isReturn || this.elseExpression.isReturn 41 | } 42 | get isContinue() { 43 | if (this.leftResult === undefined) 44 | return ( 45 | this.thenExpression.isContinue && this.elseExpression.isContinue 46 | ) 47 | return this.leftResult 48 | ? this.thenExpression.isContinue 49 | : this.elseExpression.isContinue 50 | } 51 | get isBreak() { 52 | if (this.leftResult === undefined) 53 | return this.thenExpression.isBreak && this.elseExpression.isBreak 54 | return this.leftResult 55 | ? this.thenExpression.isBreak 56 | : this.elseExpression.isBreak 57 | } 58 | 59 | isStatic() { 60 | return ( 61 | this.leftExpression.isStatic() && 62 | this.thenExpression.isStatic() && 63 | this.elseExpression.isStatic() 64 | ) 65 | } 66 | 67 | eval() { 68 | this.leftResult = this.leftExpression.eval() 69 | return this.leftResult 70 | ? this.thenExpression.eval() 71 | : this.elseExpression.eval() 72 | } 73 | 74 | toString() { 75 | if (this.elseExpression instanceof VoidExpression) 76 | return `${this.leftExpression.toString()}?${this.thenExpression.toString()}` 77 | return `${this.leftExpression.toString()}?${this.thenExpression.toString()}:${this.elseExpression.toString()}` 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/parser/parselets/statement.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parse' 2 | import { IExpression } from '../expression' 3 | import { Token } from '../../tokenizer/token' 4 | import { IInfixParselet } from './infix' 5 | import { StatementExpression } from '../expressions/statement' 6 | import { StaticExpression } from '../expressions/static' 7 | 8 | export class StatementParselet implements IInfixParselet { 9 | constructor(public precedence = 0) {} 10 | 11 | findReEntryPoint(parser: Parser) { 12 | let bracketCount = 1 13 | let tokenType = parser.lookAhead(0).getType() 14 | while (tokenType !== 'EOF') { 15 | if (tokenType == 'CURLY_RIGHT') bracketCount-- 16 | else if (tokenType === 'CURLY_LEFT') bracketCount++ 17 | if (bracketCount === 0) break 18 | 19 | parser.consume() 20 | tokenType = parser.lookAhead(0).getType() 21 | } 22 | } 23 | 24 | parse(parser: Parser, left: IExpression, token: Token) { 25 | if (parser.config.useOptimizer) { 26 | if (left.isStatic()) 27 | left = new StaticExpression(left.eval(), left.isReturn) 28 | 29 | if (parser.config.earlyReturnsSkipParsing && left.isReturn) { 30 | if (!parser.config.earlyReturnsSkipTokenization) 31 | this.findReEntryPoint(parser) 32 | 33 | return new StatementExpression([left]) 34 | } 35 | } 36 | 37 | const expressions: IExpression[] = [left] 38 | 39 | if (!parser.match('CURLY_RIGHT', false)) { 40 | do { 41 | let expr = parser.parseExpression(this.precedence) 42 | if (parser.config.useOptimizer) { 43 | if (expr.isStatic()) { 44 | if ( 45 | parser.config.useAggressiveStaticOptimizer && 46 | !expr.isReturn 47 | ) 48 | continue 49 | expr = new StaticExpression(expr.eval(), expr.isReturn) 50 | } 51 | 52 | if ( 53 | parser.config.earlyReturnsSkipParsing && 54 | (expr.isBreak || expr.isContinue || expr.isReturn) 55 | ) { 56 | expressions.push(expr) 57 | 58 | if (!parser.config.earlyReturnsSkipTokenization) 59 | this.findReEntryPoint(parser) 60 | 61 | return new StatementExpression(expressions) 62 | } 63 | } 64 | 65 | expressions.push(expr) 66 | } while ( 67 | parser.match('SEMICOLON') && 68 | !parser.match('EOF') && 69 | !parser.match('CURLY_RIGHT', false) 70 | ) 71 | } 72 | 73 | parser.match('SEMICOLON') 74 | 75 | const statementExpr = new StatementExpression(expressions) 76 | // if (parser.config.useOptimizer && statementExpr.isStatic()) { 77 | // return new StaticExpression( 78 | // statementExpr.eval(), 79 | // statementExpr.isReturn 80 | // ) 81 | // } 82 | return statementExpr 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/parser/expressions/statement.ts: -------------------------------------------------------------------------------- 1 | import { Expression, IExpression } from '../expression' 2 | import { StaticExpression } from './static' 3 | import { VoidExpression } from './void' 4 | 5 | export class StatementExpression extends Expression { 6 | type = 'StatementExpression' 7 | protected didReturn?: boolean = undefined 8 | protected wasLoopBroken = false 9 | protected wasLoopContinued = false 10 | 11 | constructor(protected expressions: IExpression[]) { 12 | super() 13 | } 14 | 15 | get allExpressions() { 16 | return this.expressions 17 | } 18 | setExpressionAt(index: number, expr: IExpression) { 19 | this.expressions[index] = expr 20 | } 21 | 22 | get isReturn() { 23 | if (this.didReturn !== undefined) return this.didReturn 24 | 25 | // This breaks scope vs. statement parsing for some reason 26 | let i = 0 27 | while (i < this.expressions.length) { 28 | const expr = this.expressions[i] 29 | 30 | if (expr.isBreak) return false 31 | if (expr.isContinue) return false 32 | if (expr.isReturn) { 33 | this.didReturn = true 34 | return true 35 | } 36 | i++ 37 | } 38 | this.didReturn = false 39 | return false 40 | } 41 | 42 | get isBreak() { 43 | if (this.wasLoopBroken) { 44 | this.wasLoopBroken = false 45 | return true 46 | } 47 | return false 48 | } 49 | get isContinue() { 50 | if (this.wasLoopContinued) { 51 | this.wasLoopContinued = false 52 | return true 53 | } 54 | return false 55 | } 56 | 57 | isStatic() { 58 | let i = 0 59 | while (i < this.expressions.length) { 60 | if (!this.expressions[i].isStatic()) return false 61 | i++ 62 | } 63 | return true 64 | } 65 | 66 | eval() { 67 | this.didReturn = false 68 | this.wasLoopBroken = false 69 | this.wasLoopContinued = false 70 | let i = 0 71 | while (i < this.expressions.length) { 72 | let res = this.expressions[i].eval() 73 | 74 | if (this.expressions[i].isReturn) { 75 | this.didReturn = true 76 | return res 77 | } else if (this.expressions[i].isContinue) { 78 | this.wasLoopContinued = true 79 | return 80 | } else if (this.expressions[i].isBreak) { 81 | this.wasLoopBroken = true 82 | return 83 | } 84 | i++ 85 | } 86 | return 0 87 | } 88 | 89 | toString() { 90 | let str = '' 91 | for (const expr of this.expressions) { 92 | if ( 93 | expr instanceof VoidExpression || 94 | (expr instanceof StaticExpression && !expr.isReturn) 95 | ) 96 | continue 97 | 98 | const exprStr = expr.toString() 99 | if (exprStr) str += `${exprStr};` 100 | } 101 | 102 | return str 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /__tests__/resolveStatic.ts: -------------------------------------------------------------------------------- 1 | import { Molang } from '../lib/Molang' 2 | import { test, expect } from 'vitest' 3 | 4 | test('Molang.resolveStatic(expr)', () => { 5 | const molang = new Molang( 6 | { 'variable.is_true': 1 }, 7 | { 8 | useCache: false, 9 | useOptimizer: true, 10 | useAggressiveStaticOptimizer: true, 11 | keepGroups: true, 12 | earlyReturnsSkipTokenization: false, 13 | earlyReturnsSkipParsing: false, 14 | } 15 | ) 16 | 17 | const tests: Record = { 18 | 'v.test*0': '0', 19 | 20 | 'v.test+0': 'v.test', 21 | 'v.test-0': 'v.test', 22 | '0-v.test': '-v.test', 23 | '0+v.test': 'v.test', 24 | 25 | 'v.test/1': 'v.test', 26 | 'v.test*1': 'v.test', 27 | '1*v.test': 'v.test', 28 | '1/v.test': '1/v.test', 29 | 'v.test/0': 'v.test/0', //While the molang specs state that this returns 0, we do not optimize this as it is a clear error of the programmer, so it should be kept for visibility 30 | '0/v.test': '0', 31 | '0*v.test': '0', 32 | 33 | //Test rearrangement 34 | 'math.sin(query.life_time*180*.5-5)*4*0.2-0.1233': 35 | '0.8*math.sin(90*query.life_time-5)-0.1233', 36 | '3 * 10 * 0.3 * v.test * 10 * 20 * 100': '180000*v.test', 37 | '3 * 10 * 0.3 + 10 * v.test * 10 * 20 * 100': '9+200000*v.test', 38 | '1+v.test+2': '3+v.test', 39 | '3 * 10 * 0.3 * v.test * 10 * v.x * 100': '9000*v.test*v.x', 40 | '3 + 10 + 1 * 0.2 * v.test * 10 * v.x * 100 + 10': '23+200*v.test*v.x', 41 | 'math.cos(((query.life_time - 2.0) * 180 / 3)) * 0.05 + 1': 42 | 'math.cos((60*(query.life_time-2)))*0.05+1', 43 | 'math.cos(((query.life_time - 2.0) / 180 / 3)) * 0.05 + 1': 44 | 'math.cos(((query.life_time-2)/540))*0.05+1', 45 | 'math.cos(((query.life_time - 2.0) / 180 * 3)) * 0.05 + 1': 46 | 'math.cos((60*(query.life_time-2)))*0.05+1', 47 | '3 / 10 * 0.3 * v.test * 20 * v.x / 100': '0.018*v.test*v.x', 48 | '3 - 10 + 0.3 + v.test * 20 * v.x+ 20 - 100': '-86.7+v.test*20*v.x', 49 | '3 - 10 - 2 - v.test - 2 - 5 - 20': '-36-v.test', 50 | '3 - 10 - 2 - v.test - 2 - 5 + 1 - 20': '-35-v.test', 51 | '3 - 10 - 2 - v.test - 2 - 5 + 1 - 20 + (34 * 10 - 30) / 2': 52 | '120-v.test', 53 | '3 - 10 - 2 - v.test - 2 - 5 + 1 - 20 + (34 * 10 - 30 * v.x) / 2': 54 | '-35-v.test+(340-30*v.x)/2', 55 | 56 | //More Tests, making sure of the order of operations 57 | '1 + 2 * 3': '7', 58 | '(1 + 2) * 3': '9', 59 | '1 + 2 * 3 + 4': '11', 60 | '(1 + 2) * (3 + 4)': '21', 61 | '1 + 2 * 3 + 4 * 5': '27', 62 | 63 | //Test common subexpression elimination 64 | 'v.test + v.test': '2*v.test', 65 | 'v.test - v.test': '0', 66 | } 67 | 68 | for (const [test, result] of Object.entries(tests)) { 69 | const ast = molang.parse(test) 70 | 71 | expect(molang.resolveStatic(ast).toString()).toBe(result) 72 | } 73 | }) 74 | -------------------------------------------------------------------------------- /lib/custom/class.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parser/parse' 2 | import { Token } from '../tokenizer/token' 3 | import { IPrefixParselet } from '../parser/parselets/prefix' 4 | import { Expression, IExpression } from '../parser/expression' 5 | import { StringExpression } from '../parser/expressions/string' 6 | import { StatementExpression } from '../parser/expressions/statement' 7 | import { CustomMolangParser } from './main' 8 | import { GroupExpression } from '../parser/expressions/group' 9 | 10 | export class CustomClassParselet implements IPrefixParselet { 11 | constructor(public precedence = 0) {} 12 | 13 | parse(parser: Parser, token: Token) { 14 | parser.consume('LEFT_PARENT') 15 | if (parser.match('RIGHT_PARENT')) 16 | throw new Error(`function() called without arguments`) 17 | 18 | let args: string[] = [] 19 | let classBody: IExpression | undefined 20 | let className: string | undefined 21 | do { 22 | const expr = parser.parseExpression() 23 | if (expr instanceof StringExpression) { 24 | if (!className) className = expr.eval() 25 | else args.push(expr.eval()) 26 | } else if ( 27 | expr instanceof StatementExpression || 28 | expr instanceof GroupExpression 29 | ) { 30 | classBody = expr 31 | } else { 32 | throw new Error( 33 | `Unexpected expresion: found "${expr.constructor.name}"` 34 | ) 35 | } 36 | } while (parser.match('COMMA')) 37 | parser.consume('RIGHT_PARENT') 38 | 39 | if (!className) 40 | throw new Error( 41 | `Missing class() name (argument 1); found "${className}"` 42 | ) 43 | if (!classBody) 44 | throw new Error( 45 | `Missing class() body (argument ${args.length + 2})` 46 | ) 47 | // Make sure that the function declaration is terminated with a semicolon 48 | if (parser.lookAhead(0).getType() !== 'SEMICOLON') 49 | throw new Error(`Missing semicolon after class expression`) 50 | 51 | return new CustomClassExpression( 52 | (parser).functions, 53 | className, 54 | args, 55 | classBody 56 | ) 57 | } 58 | } 59 | 60 | class CustomClassExpression extends Expression { 61 | type = 'CustomClassExpression' 62 | constructor( 63 | functions: Map, 64 | functionName: string, 65 | args: string[], 66 | protected functionBody: IExpression 67 | ) { 68 | super() 69 | functions.set(functionName.toLowerCase(), [ 70 | args, 71 | functionBody instanceof GroupExpression 72 | ? functionBody.allExpressions[0].toString() 73 | : functionBody.toString(), 74 | ]) 75 | } 76 | 77 | get allExpressions() { 78 | return [this.functionBody] 79 | } 80 | setExpressionAt(_: number, expr: IExpression) { 81 | this.functionBody = expr 82 | } 83 | 84 | get isReturn() { 85 | // Scopes inside of functions may use return statements 86 | return false 87 | } 88 | 89 | isStatic() { 90 | return true 91 | } 92 | 93 | eval() { 94 | return 0 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/custom/function.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '../parser/parse' 2 | import { Token } from '../parser/../tokenizer/token' 3 | import { IPrefixParselet } from '../parser/parselets/prefix' 4 | import { Expression, IExpression } from '../parser/expression' 5 | import { StringExpression } from '../parser/expressions/string' 6 | import { StatementExpression } from '../parser/expressions/statement' 7 | import { CustomMolangParser } from './main' 8 | import { GroupExpression } from '../parser/expressions/group' 9 | 10 | export class CustomFunctionParselet implements IPrefixParselet { 11 | constructor(public precedence = 0) {} 12 | 13 | parse(parser: Parser, token: Token) { 14 | parser.consume('LEFT_PARENT') 15 | if (parser.match('RIGHT_PARENT')) 16 | throw new Error(`function() called without arguments`) 17 | 18 | let args: string[] = [] 19 | let functionBody: IExpression | undefined 20 | let functionName: string | undefined 21 | do { 22 | const expr = parser.parseExpression() 23 | if (expr instanceof StringExpression) { 24 | if (!functionName) functionName = expr.eval() 25 | else args.push(expr.eval()) 26 | } else if ( 27 | expr instanceof StatementExpression || 28 | expr instanceof GroupExpression 29 | ) { 30 | functionBody = expr 31 | } else { 32 | throw new Error( 33 | `Unexpected expresion: found "${expr.constructor.name}"` 34 | ) 35 | } 36 | } while (parser.match('COMMA')) 37 | parser.consume('RIGHT_PARENT') 38 | 39 | if (!functionName) 40 | throw new Error( 41 | `Missing function() name (argument 1); found "${functionName}"` 42 | ) 43 | if (!functionBody) 44 | throw new Error( 45 | `Missing function() body (argument ${args.length + 2})` 46 | ) 47 | // Make sure that the function declaration is terminated with a semicolon 48 | if (parser.lookAhead(0).getType() !== 'SEMICOLON') 49 | throw new Error(`Missing semicolon after function expression`) 50 | 51 | return new CustomFunctionExpression( 52 | (parser).functions, 53 | functionName, 54 | args, 55 | functionBody 56 | ) 57 | } 58 | } 59 | 60 | class CustomFunctionExpression extends Expression { 61 | type = 'CustomFunctionExpression' 62 | constructor( 63 | functions: Map, 64 | functionName: string, 65 | args: string[], 66 | protected functionBody: IExpression 67 | ) { 68 | super() 69 | functions.set(functionName.toLowerCase(), [ 70 | args, 71 | functionBody instanceof GroupExpression 72 | ? functionBody.allExpressions[0].toString() 73 | : functionBody.toString(), 74 | ]) 75 | } 76 | 77 | get allExpressions() { 78 | return [this.functionBody] 79 | } 80 | setExpressionAt(_: number, expr: IExpression) { 81 | this.functionBody = expr 82 | } 83 | 84 | get isReturn() { 85 | // Scopes inside of functions may use return statements 86 | return false 87 | } 88 | 89 | isStatic() { 90 | return true 91 | } 92 | 93 | eval() { 94 | return 0 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/env/math.ts: -------------------------------------------------------------------------------- 1 | const clamp = (value: number, min: number, max: number) => { 2 | if (typeof value !== 'number' || Number.isNaN(value)) return min 3 | else if (value > max) return max 4 | else if (value < min) return min 5 | return value 6 | } 7 | const dieRoll = (sum: number, low: number, high: number) => { 8 | let i = 0 9 | let total = 0 10 | while (i < sum) total += random(low, high) 11 | return total 12 | } 13 | const dieRollInt = (sum: number, low: number, high: number) => { 14 | let i = 0 15 | let total = 0 16 | while (i < sum) total += randomInt(low, high) 17 | return total 18 | } 19 | const hermiteBlend = (value: number) => 3 * value ** 2 - 2 * value ** 3 20 | const lerp = (start: number, end: number, amount: number) => { 21 | if (amount < 0) amount = 0 22 | else if (amount > 1) amount = 1 23 | 24 | return start + (end - start) * amount 25 | } 26 | // Written by @JannisX11 (https://github.com/JannisX11/MolangJS/blob/master/molang.js#L383); modified for usage inside of this Molang parser 27 | const lerprotate = (start: number, end: number, amount: number) => { 28 | const radify = (n: number) => (((n + 180) % 360) + 180) % 360 29 | start = radify(start) 30 | end = radify(end) 31 | if (start > end) { 32 | let tmp = start 33 | start = end 34 | end = tmp 35 | } 36 | 37 | if (end - start > 180) return radify(end + amount * (360 - (end - start))) 38 | else return start + amount * (end - start) 39 | } 40 | const mod = (value: number, denominator: number) => value % denominator 41 | const random = (low: number, high: number) => low + Math.random() * (high - low) 42 | const randomInt = (low: number, high: number) => 43 | Math.round(low + Math.random() * (high - low)) 44 | 45 | const minAngle = (value: number) => { 46 | value = value % 360 47 | value = (value + 360) % 360 48 | 49 | if (value > 179) value -= 360 50 | return value 51 | } 52 | 53 | export const MolangMathLib = (useRadians: boolean) => { 54 | const degRadFactor = useRadians ? 1 : Math.PI / 180 55 | 56 | return { 57 | 'math.abs': Math.abs, 58 | 'math.acos': (x: number) => Math.acos(x) / degRadFactor, 59 | 'math.asin': (x: number) => Math.asin(x) / degRadFactor, 60 | 'math.atan': (x: number) => Math.atan(x) / degRadFactor, 61 | 'math.atan2': (y: number, x: number) => Math.atan2(y, x) / degRadFactor, 62 | 'math.ceil': Math.ceil, 63 | 'math.clamp': clamp, 64 | 'math.cos': (x: number) => Math.cos(x * degRadFactor), 65 | 'math.die_roll': dieRoll, 66 | 'math.die_roll_integer': dieRollInt, 67 | 'math.exp': Math.exp, 68 | 'math.floor': Math.floor, 69 | 'math.hermite_blend': hermiteBlend, 70 | 'math.lerp': lerp, 71 | 'math.lerp_rotate': lerprotate, 72 | 'math.ln': Math.log, 73 | 'math.max': Math.max, 74 | 'math.min': Math.min, 75 | 'math.min_angle': minAngle, 76 | 'math.mod': mod, 77 | 'math.pi': Math.PI, 78 | 'math.pow': Math.pow, 79 | 'math.random': random, 80 | 'math.random_integer': randomInt, 81 | 'math.round': Math.round, 82 | 'math.sin': (x: number) => Math.sin(x * degRadFactor), 83 | 'math.sqrt': Math.sqrt, 84 | 'math.trunc': Math.trunc, 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/main.ts: -------------------------------------------------------------------------------- 1 | import { TVariableHandler } from './env/env' 2 | import { Tokenizer } from './tokenizer/Tokenizer' 3 | 4 | /** 5 | * How the parser and interpreter should handle your Molang expression 6 | */ 7 | 8 | export interface IParserConfig { 9 | /** 10 | * Whether a cache should be used to speed up executing Molang. 11 | * The cache saves an AST for every parsed expression. 12 | * This allows us to skip the tokenization & parsing step before executing known Molang expressions 13 | * 14 | * @default true 15 | */ 16 | useCache: boolean 17 | /** 18 | * How many expressions can be cached. After reaching `maxCacheSize`, the whole cache is cleared automatically. 19 | * Can be set to `Infinity` to remove the limit completely 20 | * 21 | * @default 256 22 | */ 23 | maxCacheSize: number 24 | /** 25 | * The optimizer can drastically speed up parsing & executing Molang. 26 | * It enables skipping of unreachable statements, pre-evaluating static expressions and skipping of statements with no effect 27 | * when used together with the `useAggressiveStaticOptimizer` option 28 | * 29 | * @default true 30 | */ 31 | useOptimizer: boolean 32 | /** 33 | * Skip execution of statements with no effect 34 | * when used together with the `useOptimizer` option 35 | * 36 | * @default true 37 | */ 38 | useAggressiveStaticOptimizer: boolean 39 | 40 | /** 41 | * This options makes early return statements skip all parsing work completely 42 | * 43 | * @default true 44 | */ 45 | earlyReturnsSkipParsing: boolean 46 | /** 47 | * This options makes early return statements skip all tokenization work completely if earlyReturnsSkipParsing is set to true 48 | * 49 | * @default true 50 | */ 51 | earlyReturnsSkipTokenization: boolean 52 | /** 53 | * Tokenizer to use for tokenizing the expression 54 | */ 55 | tokenizer: Tokenizer 56 | /** 57 | * Create expression instances for brackets ("()", "{}") 58 | * 59 | * This should only be set to true if you want to use the .toString() method of an expression 60 | * or you want to iterate over the whole AST 61 | * 62 | * @default false 63 | */ 64 | keepGroups: boolean 65 | 66 | /** 67 | * Whether to convert undefined variables to "0" 68 | * 69 | * @default false 70 | */ 71 | convertUndefined: boolean 72 | 73 | /** 74 | * Use radians instead of degrees for trigonometric functions 75 | * 76 | * @default false 77 | */ 78 | useRadians: boolean 79 | 80 | /** 81 | * Resolve undefined variables 82 | */ 83 | variableHandler: TVariableHandler 84 | 85 | /** 86 | * Don not try to flatten the passed environement 87 | * 88 | * @example { query: { test: 1 } } -> { 'query.test': 1 } 89 | * 90 | * @default false 91 | */ 92 | assumeFlatEnvironment: boolean 93 | } 94 | 95 | export { Tokenizer } from './tokenizer/Tokenizer' 96 | export type { IExpression } from './parser/expression' 97 | export { CustomMolang } from './custom/main' 98 | export { Molang } from './Molang' 99 | export * as expressions from './parser/expressions/index' 100 | export { Context } from './env/env' 101 | -------------------------------------------------------------------------------- /lib/parser/parse.ts: -------------------------------------------------------------------------------- 1 | import { Tokenizer } from '../tokenizer/Tokenizer' 2 | import { TTokenType, Token } from '../tokenizer/token' 3 | import { IPrefixParselet } from './parselets/prefix' 4 | import { IInfixParselet } from './parselets/infix' 5 | import { IExpression } from './expression' 6 | import { ExecutionEnvironment } from '../env/env' 7 | import { IParserConfig } from '../main' 8 | import { VoidExpression } from './expressions/void' 9 | 10 | export class Parser { 11 | protected prefixParselets = new Map() 12 | protected infixParselets = new Map() 13 | protected readTokens: Token[] = [] 14 | protected tokenIterator = new Tokenizer() 15 | executionEnv!: ExecutionEnvironment 16 | 17 | constructor(public config: Partial) {} 18 | 19 | updateConfig(config: Partial) { 20 | this.config = config 21 | } 22 | 23 | init(expression: string) { 24 | this.tokenIterator.init(expression) 25 | this.readTokens = [] 26 | } 27 | setTokenizer(tokenizer: Tokenizer) { 28 | this.tokenIterator = tokenizer 29 | } 30 | setExecutionEnvironment(executionEnv: ExecutionEnvironment) { 31 | this.executionEnv = executionEnv 32 | } 33 | 34 | parseExpression(precedence = 0): IExpression { 35 | let token = this.consume() 36 | if (token.getType() === 'EOF') return new VoidExpression() 37 | 38 | const prefix = this.prefixParselets.get(token.getType()) 39 | if (!prefix) { 40 | throw new Error( 41 | `Cannot parse ${token.getType()} expression "${token.getType()}"` 42 | ) 43 | } 44 | 45 | let expressionLeft = prefix.parse(this, token) 46 | return this.parseInfixExpression(expressionLeft, precedence) 47 | } 48 | 49 | parseInfixExpression(expressionLeft: IExpression, precedence = 0) { 50 | let token 51 | 52 | while (this.getPrecedence() > precedence) { 53 | token = this.consume() 54 | let tokenType = token.getType() 55 | if (tokenType === 'EQUALS' && !this.match('EQUALS')) { 56 | tokenType = 'ASSIGN' 57 | } 58 | 59 | const infix = this.infixParselets.get(tokenType) 60 | if (!infix) 61 | throw new Error(`Unknown infix parselet: "${tokenType}"`) 62 | expressionLeft = infix.parse(this, expressionLeft, token) 63 | } 64 | 65 | return expressionLeft 66 | } 67 | 68 | getPrecedence() { 69 | const parselet = this.infixParselets.get(this.lookAhead(0).getType()) 70 | return parselet?.precedence ?? 0 71 | } 72 | 73 | consume(expected?: TTokenType) { 74 | //Sets the lastLineNumber & startColumn before consuming next token 75 | //Used for getting the exact location an error occurs 76 | // this.tokenIterator.step() 77 | 78 | const token = this.lookAhead(0) 79 | 80 | if (expected && token.getType() !== expected) { 81 | throw new Error( 82 | `Expected token "${expected}" and found "${token.getType()}"` 83 | ) 84 | } 85 | 86 | this.readTokens.shift()! 87 | return token 88 | } 89 | 90 | match(expected: TTokenType, consume = true) { 91 | const token = this.lookAhead(0) 92 | if (token.getType() !== expected) return false 93 | 94 | if (consume) this.consume() 95 | return true 96 | } 97 | 98 | lookAhead(distance: number) { 99 | while (distance >= this.readTokens.length) 100 | this.readTokens.push(this.tokenIterator.next()) 101 | 102 | return this.readTokens[distance] 103 | } 104 | 105 | registerInfix(tokenType: TTokenType, infixParselet: IInfixParselet) { 106 | this.infixParselets.set(tokenType, infixParselet) 107 | } 108 | registerPrefix(tokenType: TTokenType, prefixParselet: IPrefixParselet) { 109 | this.prefixParselets.set(tokenType, prefixParselet) 110 | } 111 | 112 | getInfix(tokenType: TTokenType) { 113 | return this.infixParselets.get(tokenType) 114 | } 115 | getPrefix(tokenType: TTokenType) { 116 | return this.prefixParselets.get(tokenType) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/parser/molang.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from './parse' 2 | import { BinaryOperator } from './parselets/binaryOperator' 3 | import { EPrecedence } from './precedence' 4 | import { PrefixOperator } from './parselets/prefix' 5 | import { NumberParselet } from './parselets/number' 6 | import { NameParselet } from './parselets/name' 7 | import { GroupParselet } from './parselets/groupParselet' 8 | import { ReturnParselet } from './parselets/return' 9 | import { StatementParselet } from './parselets/statement' 10 | import { StringParselet } from './parselets/string' 11 | import { FunctionParselet } from './parselets/function' 12 | import { ArrayAccessParselet } from './parselets/arrayAccess' 13 | import { ScopeParselet } from './parselets/scope' 14 | import { LoopParselet } from './parselets/loop' 15 | import { ForEachParselet } from './parselets/forEach' 16 | import { ContinueParselet } from './parselets/continue' 17 | import { BreakParselet } from './parselets/break' 18 | import { BooleanParselet } from './parselets/boolean' 19 | import { IParserConfig } from '../main' 20 | import { EqualsOperator } from './parselets/Equals' 21 | import { NotEqualsOperator } from './parselets/NotEquals' 22 | import { AndOperator } from './parselets/AndOperator' 23 | import { OrOperator } from './parselets/OrOperator' 24 | import { SmallerOperator } from './parselets/SmallerOperator' 25 | import { GreaterOperator } from './parselets/GreaterOperator' 26 | import { QuestionOperator } from './parselets/QuestionOperator' 27 | 28 | export class MolangParser extends Parser { 29 | constructor(config: Partial) { 30 | super(config) 31 | 32 | //Special parselets 33 | this.registerPrefix('NAME', new NameParselet()) 34 | this.registerPrefix('STRING', new StringParselet()) 35 | this.registerPrefix('NUMBER', new NumberParselet()) 36 | this.registerPrefix('TRUE', new BooleanParselet(EPrecedence.PREFIX)) 37 | this.registerPrefix('FALSE', new BooleanParselet(EPrecedence.PREFIX)) 38 | this.registerPrefix('RETURN', new ReturnParselet()) 39 | this.registerPrefix('CONTINUE', new ContinueParselet()) 40 | this.registerPrefix('BREAK', new BreakParselet()) 41 | this.registerPrefix('LOOP', new LoopParselet()) 42 | this.registerPrefix('FOR_EACH', new ForEachParselet()) 43 | this.registerInfix( 44 | 'QUESTION', 45 | new QuestionOperator(EPrecedence.CONDITIONAL) 46 | ) 47 | this.registerPrefix('LEFT_PARENT', new GroupParselet()) 48 | this.registerInfix( 49 | 'LEFT_PARENT', 50 | new FunctionParselet(EPrecedence.FUNCTION) 51 | ) 52 | this.registerInfix( 53 | 'ARRAY_LEFT', 54 | new ArrayAccessParselet(EPrecedence.ARRAY_ACCESS) 55 | ) 56 | this.registerPrefix('CURLY_LEFT', new ScopeParselet(EPrecedence.SCOPE)) 57 | this.registerInfix( 58 | 'SEMICOLON', 59 | new StatementParselet(EPrecedence.STATEMENT) 60 | ) 61 | 62 | //Prefix parselets 63 | this.registerPrefix('MINUS', new PrefixOperator(EPrecedence.PREFIX)) 64 | this.registerPrefix('BANG', new PrefixOperator(EPrecedence.PREFIX)) 65 | 66 | //Postfix parselets 67 | //Nothing here yet 68 | 69 | //Infix parselets 70 | this.registerInfix('PLUS', new BinaryOperator(EPrecedence.SUM)) 71 | this.registerInfix('MINUS', new BinaryOperator(EPrecedence.SUM)) 72 | this.registerInfix('ASTERISK', new BinaryOperator(EPrecedence.PRODUCT)) 73 | this.registerInfix('SLASH', new BinaryOperator(EPrecedence.PRODUCT)) 74 | this.registerInfix( 75 | 'EQUALS', 76 | new EqualsOperator(EPrecedence.EQUALS_COMPARE) 77 | ) 78 | this.registerInfix( 79 | 'BANG', 80 | new NotEqualsOperator(EPrecedence.EQUALS_COMPARE) 81 | ) 82 | this.registerInfix('GREATER', new GreaterOperator(EPrecedence.COMPARE)) 83 | this.registerInfix('SMALLER', new SmallerOperator(EPrecedence.COMPARE)) 84 | this.registerInfix('AND', new AndOperator(EPrecedence.AND)) 85 | this.registerInfix('OR', new OrOperator(EPrecedence.OR)) 86 | this.registerInfix('ASSIGN', new BinaryOperator(EPrecedence.ASSIGNMENT)) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/env/env.ts: -------------------------------------------------------------------------------- 1 | import { standardEnv } from './standardEnv' 2 | 3 | export type TVariableHandler = ( 4 | variableName: string, 5 | variables: Record 6 | ) => unknown 7 | 8 | export interface IEnvConfig { 9 | useRadians?: boolean 10 | convertUndefined?: boolean 11 | variableHandler?: TVariableHandler 12 | isFlat?: boolean 13 | } 14 | 15 | export class ExecutionEnvironment { 16 | protected env: Record 17 | 18 | constructor(env: Record, public readonly config: IEnvConfig) { 19 | if (!env) throw new Error(`Provided environment must be an object`) 20 | 21 | this.env = { 22 | ...standardEnv(config.useRadians ?? false), 23 | 'query.self': () => this.env, 24 | ...(config.isFlat ? env : this.flattenEnv(env)), 25 | } 26 | } 27 | 28 | updateConfig({ 29 | variableHandler, 30 | convertUndefined, 31 | useRadians, 32 | }: IEnvConfig) { 33 | if (convertUndefined !== undefined) 34 | this.config.convertUndefined = convertUndefined 35 | if (typeof variableHandler === 'function') 36 | this.config.variableHandler = variableHandler 37 | 38 | if (!!this.config.useRadians !== !!useRadians) { 39 | this.env = Object.assign(this.env, standardEnv(!!useRadians)) 40 | } 41 | } 42 | 43 | get() { 44 | return this.env 45 | } 46 | 47 | protected flattenEnv( 48 | newEnv: Record, 49 | addKey = '', 50 | current: any = {} 51 | ) { 52 | for (let key in newEnv) { 53 | let newKey = key 54 | 55 | if (key[1] === '.') { 56 | switch (key[0]) { 57 | case 'q': 58 | newKey = 'query' + key.substring(1, key.length) 59 | break 60 | case 't': 61 | newKey = 'temp' + key.substring(1, key.length) 62 | break 63 | case 'v': 64 | newKey = 'variable' + key.substring(1, key.length) 65 | break 66 | case 'c': 67 | newKey = 'context' + key.substring(1, key.length) 68 | break 69 | case 'f': 70 | newKey = 'function' + key.substring(1, key.length) 71 | break 72 | } 73 | } 74 | 75 | if (newEnv[key]?.__isContext) { 76 | current[`${addKey}${newKey}`] = newEnv[key].env 77 | } else if ( 78 | typeof newEnv[key] === 'object' && 79 | !Array.isArray(newEnv[key]) 80 | ) { 81 | this.flattenEnv(newEnv[key], `${addKey}${key}.`, current) 82 | } else { 83 | current[`${addKey}${newKey}`] = newEnv[key] 84 | } 85 | } 86 | 87 | return current 88 | } 89 | 90 | setAt(lookup: string, value: unknown) { 91 | if (lookup[1] === '.') { 92 | switch (lookup[0]) { 93 | case 'q': 94 | lookup = 'query' + lookup.substring(1, lookup.length) 95 | break 96 | case 't': 97 | lookup = 'temp' + lookup.substring(1, lookup.length) 98 | break 99 | case 'v': 100 | lookup = 'variable' + lookup.substring(1, lookup.length) 101 | break 102 | case 'c': 103 | lookup = 'context' + lookup.substring(1, lookup.length) 104 | break 105 | case 'f': 106 | lookup = 'function' + lookup.substring(1, lookup.length) 107 | break 108 | } 109 | } 110 | 111 | return (this.env[lookup] = value) 112 | } 113 | 114 | getFrom(lookup: string) { 115 | if (lookup[1] === '.') { 116 | switch (lookup[0]) { 117 | case 'q': 118 | lookup = 'query' + lookup.substring(1, lookup.length) 119 | break 120 | case 't': 121 | lookup = 'temp' + lookup.substring(1, lookup.length) 122 | break 123 | case 'v': 124 | lookup = 'variable' + lookup.substring(1, lookup.length) 125 | break 126 | case 'c': 127 | lookup = 'context' + lookup.substring(1, lookup.length) 128 | break 129 | case 'f': 130 | lookup = 'function' + lookup.substring(1, lookup.length) 131 | break 132 | } 133 | } 134 | 135 | const res = 136 | this.env[lookup] ?? this.config.variableHandler?.(lookup, this.env) 137 | return res === undefined && this.config.convertUndefined ? 0 : res 138 | } 139 | } 140 | 141 | export class Context { 142 | public readonly __isContext = true 143 | constructor(public readonly env: any) {} 144 | } 145 | -------------------------------------------------------------------------------- /lib/parser/parselets/binaryOperator.ts: -------------------------------------------------------------------------------- 1 | import { IInfixParselet } from './infix' 2 | import { Parser } from '../parse' 3 | import { IExpression } from '../expression' 4 | import { Token } from '../../tokenizer/token' 5 | import { GenericOperatorExpression } from '../expressions/genericOperator' 6 | 7 | export const plusHelper = ( 8 | leftExpression: IExpression, 9 | rightExpression: IExpression 10 | ) => { 11 | const leftValue = leftExpression.eval() 12 | const rightValue = rightExpression.eval() 13 | if ( 14 | !(typeof leftValue === 'number' || typeof leftValue === 'boolean') || 15 | !(typeof rightValue === 'number' || typeof rightValue === 'boolean') 16 | ) 17 | throw new Error( 18 | `Cannot use numeric operators for expression "${leftValue} + ${rightValue}"` 19 | ) 20 | //@ts-ignore 21 | return leftValue + rightValue 22 | } 23 | export const minusHelper = ( 24 | leftExpression: IExpression, 25 | rightExpression: IExpression 26 | ) => { 27 | const leftValue = leftExpression.eval() 28 | const rightValue = rightExpression.eval() 29 | if ( 30 | !(typeof leftValue === 'number' || typeof leftValue === 'boolean') || 31 | !(typeof rightValue === 'number' || typeof rightValue === 'boolean') 32 | ) 33 | throw new Error( 34 | `Cannot use numeric operators for expression "${leftValue} - ${rightValue}"` 35 | ) 36 | //@ts-ignore 37 | return leftValue - rightValue 38 | } 39 | export const divideHelper = ( 40 | leftExpression: IExpression, 41 | rightExpression: IExpression 42 | ) => { 43 | const leftValue = leftExpression.eval() 44 | const rightValue = rightExpression.eval() 45 | if ( 46 | !(typeof leftValue === 'number' || typeof leftValue === 'boolean') || 47 | !(typeof rightValue === 'number' || typeof rightValue === 'boolean') 48 | ) 49 | throw new Error( 50 | `Cannot use numeric operators for expression "${leftValue} / ${rightValue}"` 51 | ) 52 | //@ts-ignore 53 | return leftValue / rightValue 54 | } 55 | export const multiplyHelper = ( 56 | leftExpression: IExpression, 57 | rightExpression: IExpression 58 | ) => { 59 | const leftValue = leftExpression.eval() 60 | const rightValue = rightExpression.eval() 61 | if ( 62 | !(typeof leftValue === 'number' || typeof leftValue === 'boolean') || 63 | !(typeof rightValue === 'number' || typeof rightValue === 'boolean') 64 | ) 65 | throw new Error( 66 | `Cannot use numeric operators for expression "${leftValue} * ${rightValue}"` 67 | ) 68 | //@ts-ignore 69 | return leftValue * rightValue 70 | } 71 | export const assignHelper = ( 72 | leftExpression: IExpression, 73 | rightExpression: IExpression 74 | ) => { 75 | if (leftExpression.setPointer) { 76 | leftExpression.setPointer(rightExpression.eval()) 77 | return 0 78 | } else { 79 | throw Error(`Cannot assign to ${leftExpression.type}`) 80 | } 81 | } 82 | 83 | export class BinaryOperator implements IInfixParselet { 84 | constructor(public precedence = 0) {} 85 | 86 | parse(parser: Parser, leftExpression: IExpression, token: Token) { 87 | const rightExpression = parser.parseExpression(this.precedence) 88 | // return new AdditionExpression(leftExpression, rightExpression) 89 | 90 | const tokenText = token.getText() 91 | 92 | switch (tokenText) { 93 | case '+': 94 | return new GenericOperatorExpression( 95 | leftExpression, 96 | rightExpression, 97 | tokenText, 98 | plusHelper 99 | ) 100 | case '-': 101 | return new GenericOperatorExpression( 102 | leftExpression, 103 | rightExpression, 104 | tokenText, 105 | minusHelper 106 | ) 107 | case '*': 108 | return new GenericOperatorExpression( 109 | leftExpression, 110 | rightExpression, 111 | tokenText, 112 | multiplyHelper 113 | ) 114 | case '/': 115 | return new GenericOperatorExpression( 116 | leftExpression, 117 | rightExpression, 118 | tokenText, 119 | divideHelper 120 | ) 121 | case '=': { 122 | return new GenericOperatorExpression( 123 | leftExpression, 124 | rightExpression, 125 | '=', 126 | assignHelper 127 | ) 128 | } 129 | 130 | default: 131 | throw new Error(`Operator not implemented`) 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /lib/tokenizer/Tokenizer.ts: -------------------------------------------------------------------------------- 1 | import { TokenTypes, KeywordTokens } from './tokenTypes' 2 | import { Token } from './token' 3 | 4 | export class Tokenizer { 5 | protected keywordTokens: Set 6 | protected i = 0 7 | protected currentColumn = 0 8 | protected currentLine = 0 9 | protected lastColumns = 0 10 | protected expression!: string 11 | 12 | constructor(addKeywords?: Set) { 13 | if (addKeywords) 14 | this.keywordTokens = new Set([...KeywordTokens, ...addKeywords]) 15 | else this.keywordTokens = KeywordTokens 16 | } 17 | 18 | init(expression: string) { 19 | this.currentLine = 0 20 | this.currentColumn = 0 21 | this.lastColumns = 0 22 | this.i = 0 23 | this.expression = expression 24 | } 25 | 26 | next(): Token { 27 | this.currentColumn = this.i - this.lastColumns 28 | 29 | while ( 30 | this.i < this.expression.length && 31 | (this.expression[this.i] === ' ' || 32 | this.expression[this.i] === '\t' || 33 | this.expression[this.i] === '\n') 34 | ) { 35 | if (this.expression[this.i] === '\n') { 36 | this.currentLine++ 37 | this.currentColumn = 0 38 | this.lastColumns = this.i + 1 39 | } 40 | this.i++ 41 | } 42 | 43 | // This is unnecessary for parsing simple, vanilla molang expressions 44 | // Might make sense to move it into a "TokenizerWithComments" class in the future 45 | if (this.expression[this.i] === '#') { 46 | const index = this.expression.indexOf('\n', this.i + 1) 47 | this.i = index === -1 ? this.expression.length : index 48 | this.currentLine++ 49 | this.lastColumns = this.i + 1 50 | this.currentColumn = 0 51 | return this.next() 52 | } 53 | 54 | // Check tokens with one char 55 | let token = TokenTypes[this.expression[this.i]] 56 | if (token) { 57 | return new Token( 58 | token, 59 | this.expression[this.i++], 60 | this.currentColumn, 61 | this.currentLine 62 | ) 63 | } else if ( 64 | this.isLetter(this.expression[this.i]) || 65 | this.expression[this.i] === '_' 66 | ) { 67 | let j = this.i + 1 68 | while ( 69 | j < this.expression.length && 70 | (this.isLetter(this.expression[j]) || 71 | this.isNumber(this.expression[j]) || 72 | this.expression[j] === '_' || 73 | this.expression[j] === '.') 74 | ) { 75 | j++ 76 | } 77 | 78 | const value = this.expression.substring(this.i, j).toLowerCase() 79 | 80 | this.i = j 81 | return new Token( 82 | this.keywordTokens.has(value) ? value.toUpperCase() : 'NAME', 83 | value, 84 | this.currentColumn, 85 | this.currentLine 86 | ) 87 | } else if ( 88 | this.isNumber(this.expression[this.i]) || 89 | this.expression[this.i] === '.' 90 | ) { 91 | let j = this.i + 1 92 | let hasDecimal = this.expression[this.i] === '.' 93 | while ( 94 | j < this.expression.length && 95 | (this.isNumber(this.expression[j]) || 96 | (this.expression[j] === '.' && !hasDecimal)) 97 | ) { 98 | if (this.expression[j] === '.') hasDecimal = true 99 | j++ 100 | } 101 | 102 | const token = new Token( 103 | 'NUMBER', 104 | this.expression.substring(this.i, j), 105 | this.currentColumn, 106 | this.currentLine 107 | ) 108 | // Support notations like "0.5f" 109 | const usesFloatNotation = hasDecimal && this.expression[j] === 'f' 110 | 111 | this.i = usesFloatNotation ? j + 1 : j 112 | 113 | return token 114 | } else if (this.expression[this.i] === "'") { 115 | let j = this.i + 1 116 | while (j < this.expression.length && this.expression[j] !== "'") { 117 | j++ 118 | } 119 | j++ 120 | const token = new Token( 121 | 'STRING', 122 | this.expression.substring(this.i, j), 123 | this.currentColumn, 124 | this.currentLine 125 | ) 126 | this.i = j 127 | return token 128 | } 129 | 130 | if (this.hasNext()) { 131 | this.i++ 132 | return this.next() 133 | } 134 | return new Token('EOF', '', this.currentColumn, this.currentLine) 135 | } 136 | hasNext() { 137 | return this.i < this.expression.length 138 | } 139 | 140 | protected isLetter(char: string) { 141 | return (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') 142 | } 143 | 144 | protected isNumber(char: string) { 145 | return char >= '0' && char <= '9' 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /__tests__/custom/function.ts: -------------------------------------------------------------------------------- 1 | import { CustomMolang } from '../../lib/custom/main' 2 | import { test, expect } from 'vitest' 3 | 4 | test('Custom syntax', () => { 5 | const customMolang = new CustomMolang({}) 6 | 7 | customMolang.parse( 8 | ` 9 | function('sq', 'base', { return math.pow(arg.base, 2); }); 10 | function('on_axis', 'axis', { return arg.axis == 'x' ? v.x : v.y; }); 11 | function('fibonacci', 'total', { 12 | t.current = 0; 13 | t.prev = 0; 14 | loop(a.total, t.current == 0 ? {t.current = 1;} : {t.tmp = t.current; t.current = t.current + t.prev; t.prev = t.tmp;}); 15 | return t.current; 16 | }); 17 | function('pow', 'base', 'exp', { 18 | return a.exp == 0 ? 1 : a.base * f.pow(a.base, a.exp - 1); 19 | }); 20 | 21 | # Comment test return 1; 22 | function('get_axis', 'axis', { 23 | a.axis == 'x' ? { return v.x; }; 24 | a.axis == 'y' ? { return v.y; }; 25 | return v.z; 26 | }); 27 | function('pi', { 28 | return math.pi; 29 | }); 30 | function('early_return', 'return_0', { 31 | a.return_0 ? { 32 | return 0; 33 | }; 34 | return 1; 35 | }); 36 | function('dead_end', 'return_0', { 37 | a.return_0 ? { 38 | return 0; 39 | } : { 40 | return 1; 41 | }; 42 | t.test = 0; 43 | return t.test; 44 | }); 45 | function('complex_early_return', 'return_0', { 46 | a.return_0 ? { 47 | a.return_0 > 1 ? { 48 | return a.return_0; 49 | }; 50 | return 0; 51 | }; 52 | a.return_0 == 1 ? { 53 | return 2; 54 | }; 55 | return 1; 56 | }); 57 | function('simple_early_return', 'return_0', { 58 | return 3; 59 | a.return_0 ? { 60 | a.return_0 > 1 ? { 61 | return a.return_0; 62 | }; 63 | return 0; 64 | }; 65 | a.return_0 == 1 ? { 66 | return 2; 67 | }; 68 | return 1; 69 | }); 70 | function('op_precedence', 'm', { 71 | return -(a.m + 1); 72 | }); 73 | 74 | # Make sure that .molang files support using double quotes 75 | function("test_double_quotes", { 76 | return 0; 77 | }); 78 | 79 | # Ensure that structs are properly supported 80 | function('create_list', { 81 | t.list.length = 0; 82 | return t.list; 83 | }); 84 | # Ensure that functions without return statements work correctly 85 | function('test_no_return', { 86 | t.test = 0; 87 | }); 88 | ` 89 | ) 90 | 91 | expect(customMolang.transform('f.pow(2, 2)')).toBe('4') 92 | expect(customMolang.transform('f.fibonacci(4)')).toBe( 93 | 'return ({t.__scvar0=0;t.__scvar1=0;loop(4,t.__scvar0==0?{t.__scvar0=1;}:{t.__scvar2=t.__scvar0;t.__scvar0=t.__scvar0+t.__scvar1;t.__scvar1=t.__scvar2;});t.__scvar3=t.__scvar0;}+t.__scvar3);' 94 | ) 95 | expect( 96 | customMolang.transform('f.pow(2, f.pow(2,2)) + f.fibonacci(0)') 97 | ).toBe( 98 | 'return 16+({t.__scvar0=0;t.__scvar1=0;t.__scvar3=t.__scvar0;}+t.__scvar3);' 99 | ) 100 | expect(customMolang.transform('f.sq(2)')).toBe('(math.pow(2,2))') 101 | expect(customMolang.transform("f.sq(f.on_axis('x'))")).toBe( 102 | '(math.pow((v.x),2))' 103 | ) 104 | 105 | expect(customMolang.transform('f.get_axis(t.axis)')).toBe( 106 | "return ({t.axis=='x'?{t.__scvar0=v.x;}:{t.axis=='y'?{t.__scvar0=v.y;}:{t.__scvar0=v.z;};};}+t.__scvar0);" 107 | ) 108 | expect(customMolang.transform('t.x = 1; f.sq(2);')).toBe( 109 | 't.x=1;(math.pow(2,2));' 110 | ) 111 | expect(customMolang.transform('t.x = 1; return f.sq(2);')).toBe( 112 | 't.x=1;return (math.pow(2,2));' 113 | ) 114 | expect(customMolang.transform('f.pi()')).toBe('(math.pi)') 115 | expect(customMolang.transform('f.early_return(v.is_true)')).toBe( 116 | 'return ({v.is_true?{t.__scvar0=0;}:{t.__scvar0=1;};}+t.__scvar0);' 117 | ) 118 | expect(customMolang.transform('f.early_return(v.x)')).toBe( 119 | 'return ({v.x?{t.__scvar0=0;}:{t.__scvar0=1;};}+t.__scvar0);' 120 | ) 121 | // TODO: Once temp variable elimination is in, this should just return '1' 122 | expect(customMolang.transform('f.early_return(0)')).toBe( 123 | 'return ({t.__scvar0=1;}+t.__scvar0);' 124 | ) 125 | expect(customMolang.transform('f.dead_end(v.x)')).toBe( 126 | 'return ({v.x?{t.__scvar0=0;}:{t.__scvar0=1;};}+(t.__scvar0??0));' 127 | ) 128 | expect(customMolang.transform('f.complex_early_return(v.x)')).toBe( 129 | 'return ({v.x?{v.x>1?{t.__scvar0=v.x;}:{t.__scvar0=0;};}:{v.x==1?{t.__scvar0=2;}:{t.__scvar0=1;};};}+t.__scvar0);' 130 | ) 131 | expect(customMolang.transform('f.simple_early_return(v.x)')).toBe('3') 132 | expect(customMolang.transform('f.op_precedence(1)')).toBe('-2') 133 | expect(customMolang.transform('f.create_list()')).toBe( 134 | `return ({t.__scvar0.length=0;t.__scvar1=t.__scvar0;}+t.__scvar1);` 135 | ) 136 | expect(customMolang.transform('f.test_no_return()')).toBe( 137 | `return ({t.__scvar0=0;});` 138 | ) 139 | }) 140 | -------------------------------------------------------------------------------- /__tests__/tests.ts: -------------------------------------------------------------------------------- 1 | import { Context, ExecutionEnvironment } from '../lib/env/env' 2 | import { Molang } from '../lib/main' 3 | import MolangJs from 'molangjs' 4 | import { describe, test, expect } from 'vitest' 5 | 6 | const TESTS: [string, number | string][] = [ 7 | /** 8 | * Basic tests 9 | */ 10 | ['true', 1.0], 11 | ['false', 0.0], 12 | ['(0.5f) + 1', 1.5], 13 | ['false ? 5', 0], 14 | ['true ? 5', 5], 15 | ['1 + 1', 2], 16 | ['1 + 1 * 2', 3], 17 | ['return 1', 0], //Your typical Minecraft quirk 18 | ['return 1;', 1], 19 | ['-(1 + 1)', -2], 20 | ['(1 + 1) * 2', 4], 21 | ['(1 + 1) * (1 + 1)', 4], 22 | ["'test' == 'test2'", 0], 23 | ['0 <= 0', 1.0], 24 | ['0 == 0', 1.0], 25 | ['0 != 0', 0.0], 26 | ['((7 * 0) + 1) / 2', 0.5], 27 | ['4 / 2 == 2', 1], 28 | ['1 == 1 && 0 == 0', 1], 29 | ['0 ? 1 : 2', 2], 30 | ['return 0 ? 1;', 0], 31 | ['return 1 ? 1;', 1], 32 | ['(0 ? 1 : 2) ? 3 : 4', 3], 33 | // Molang should parse A?B?C:D:E as A?(B?C:D):E 34 | ['q.variant == 11 ? 1 ? 2 : 0 : q.variant == 27', 2], 35 | // Molang should parse A?B:C?D:E as A?B:(C?D:E), not (A?B:C)?D:E 36 | ['q.variant == 11 ? 1 : q.variant == 27 ? 2 : 0', 1], 37 | ['t.v = 1 ? 2 : 1; return t.v;', 2], 38 | // Molang should parse "A && B || C" as "(A && B) || C" 39 | ['t.v = 0; return ({t.v = 1;}+1) && 0 || t.v;', 1], 40 | // Molang should parse "A == C > D" as "A == (C > D)" 41 | ['2 == 2 > 0', 0], 42 | ['0 ? 1 : 2; return 1;', 1], 43 | ["(1 && 0) + 1 ? 'true' : 'false'", 'true'], 44 | ["!(1 && 0) ? 'true' : 'false'", 'true'], 45 | ['v.x ?? 1', 1], 46 | ['(variable.non_existent ?? 3) + (variable.existent ?? 9)', 5], 47 | 48 | /** 49 | * Advanced syntax: Loops, break, continue & scope 50 | */ 51 | ['1.0 ? { return 1; };', 1], 52 | ['1.0 ? { variable.scope_test = 1; return variable.scope_test; };', 1], 53 | ['v.x = 0; loop(10, v.x = v.x + 1); return v.x;', 10], 54 | ['v.x = 0; loop(10, { v.x = v.x + 1; }); return v.x;', 10], 55 | ['v.x = 2; loop(10, { return 1; }); return v.x;', 1], 56 | ['v.x = 2; loop(0, { return 1; }); return v.x;', 2], 57 | [ 58 | 't.total = 0; for_each(t.current, texture.skin_id, { t.total = t.total + t.current; }); return t.total;', 59 | 55, 60 | ], 61 | [ 62 | 't.total = 0; for_each(t.current, texture.skin_id, { loop(10, t.total = t.total + t.current); }); return t.total;', 63 | 550, 64 | ], 65 | [ 66 | 't.total = 0; for_each(t.current, texture.skin_id, { math.mod(t.current, 2) ? continue; t.total = t.total + t.current; }); return t.total;', 67 | 30, 68 | ], 69 | ['v.x = 2; loop(10, { break; return 1; }); return v.x;', 2], 70 | ['{ variable.test = 1; variable.test_2 = 2; };', 0], 71 | [ 72 | '{ ({ variable.test = 1; variable.test_2 = 2; }); variable.test_3 = 3; };', 73 | 0, 74 | ], 75 | 76 | /** 77 | * Function calls & variable lookups 78 | */ 79 | ['(Math.Random(0,0))', 0], 80 | ['2 + Math.pow(2, 3)', 10], 81 | ['test(1+1, 3+3)', 8], 82 | ["query.get_position >= 0 ? 'hello' : 'test'", 'hello'], 83 | ["return query.get_position(0) < 0 ? 'hello';", 0.0], 84 | ["variable.temp = 'test'", 0], 85 | ['variable.temp', 'test'], 86 | ["variable.temp == 'test'", 1], 87 | ['variable.foo = 1.0 ? 0 : 1', 0], 88 | ['variable.foo', 0], 89 | ['query.get_equipped_item_name(0)', 'diamond_sword_0'], 90 | ['query.get_equipped_item_name(1)', 'diamond_sword_1'], 91 | ['math.add(1, 5)', 6], 92 | ['rider.slot', 1], 93 | ['rider.is(math.add(1, 5))', 6], 94 | ['texture.variants[0]', '1'], 95 | ['texture.mark_variants[0] = 2', 0], 96 | ['texture.mark_variants[0]', 2], 97 | ['texture.variants[texture.variants[5]]', 6], 98 | ['texture.variants[math.add(1, 3)]', 5], 99 | ['math.add(rider.get_length(texture.variants[0]) + 5, 6)', 12], 100 | ['query.get_position(0) >= 0 && query.get_position(0) <= 0', 1.0], 101 | ['!(1 + 3) && query.test_something_else', 0], 102 | [ 103 | '({ query.get_position ? {return 0;}; variable.return_value = 1; } + variable.return_value)', 104 | 1, 105 | ], 106 | ['v.foo ? 1.0 : texture.variants[0]', '1'], 107 | 108 | /** 109 | * Function calls + property access 110 | * 111 | * Disabled until we actually support these patterns 112 | */ 113 | // ['c.position().x', 1], 114 | // ['c.position.y', 2], 115 | 116 | /** 117 | * Context 118 | */ 119 | ['context.other->query.test * context.other->query.test', 1], 120 | ['context.not_a_valid.context->query.test', 0], 121 | 122 | // Test comments 123 | ['1; # return 0;\n return 2;', 2], 124 | ] 125 | 126 | const env = { 127 | test(n1: number, n2: number) { 128 | return n1 + n2 129 | }, 130 | length(arr: unknown[]) { 131 | return arr.length 132 | }, 133 | variable: { 134 | existent: 2, 135 | }, 136 | query: { 137 | get_equipped_item_name(slot: number) { 138 | return 'diamond_sword_' + slot 139 | }, 140 | get_position() { 141 | return 0 142 | }, 143 | variant: 11, 144 | }, 145 | texture: { 146 | mark_variants: [], 147 | variants: ['1', 2, 3, 4, 5, 6, 6], 148 | skin_id: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 149 | }, 150 | math: { 151 | add(a: number, b: number) { 152 | return a + b 153 | }, 154 | }, 155 | rider: { 156 | slot: 1, 157 | is(id: number) { 158 | return id 159 | }, 160 | get_length(str: string) { 161 | return str.length 162 | }, 163 | }, 164 | context: { 165 | other: new Context({ 166 | query: { 167 | test: 1, 168 | }, 169 | }), 170 | position: () => ({ 171 | x: 1, 172 | y: 2, 173 | }), 174 | }, 175 | } 176 | 177 | describe('parse(string)', () => { 178 | const molang = new Molang(env, { useOptimizer: false }) 179 | const molangJs = new MolangJs() 180 | const flatEnv = new ExecutionEnvironment(env, { isFlat: false }).get() 181 | 182 | TESTS.forEach(([t, res]) => { 183 | test(`Optimizer: "${t}" => ${res}`, () => { 184 | expect(molang.execute(t)).toBe(res) 185 | }) 186 | molang.updateConfig({ useOptimizer: true }) 187 | test(`Optimizer: "${t}" => ${res}`, () => { 188 | expect(molang.execute(t)).toBe(res) 189 | }) 190 | 191 | // test(`MolangJS: "${t}" => ${res}`, () => { 192 | // expect(molangJs.parse(t, flatEnv)).toBe(res) 193 | // }) 194 | }) 195 | }) 196 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Molang 2 | 3 | A fast Molang parser used and developed by the bridge. team. This library has full support for all of Minecraft's Molang features. 4 | 5 | ## About 6 | 7 | > Molang is a simple expression-based language designed for fast calculation of values at run-time. Its focus is solely to enable script-like capabilities in high-performance systems where JavaScript is not performant at scale. We need scripting capabilities in these low-level systems to support end-user modding capabilities, custom entities, rendering, and animations. 8 | 9 | \- From the Minecraft documentation 10 | 11 | ## Installation 12 | 13 | - `npm i molang` 14 | 15 | **or** 16 | 17 | - Download the `dist/main.web.js` file and add the script to your HTML page (library access via global `Molang` object). 18 | 19 | ## Basic Usage 20 | 21 | To execute a basic Molang statement, first construct a new instance of the `Molang` class. The first constructor argument is the environment your Molang script will have access to and the second argument configures the Molang interpreter. Take a look at the `IParserConfig` interface [for a list of all available options](https://github.com/bridge-core/Molang/blob/master/lib/main.ts). 22 | 23 | `molang.execute(...)` simply executes a Molang script and returns the value it evaluates to. 24 | 25 | ```javascript 26 | import { Molang } from 'molang' 27 | 28 | const molang = new Molang( 29 | { 30 | query: { 31 | x: 0, 32 | get(val) { 33 | return val + 4 34 | }, 35 | }, 36 | }, 37 | { useCache: true } 38 | ) 39 | molang.execute('query.x + query.get(3) == 7') 40 | ``` 41 | 42 | ### Setting up nested environments 43 | 44 | For the context switching operator "->", you can set up nested environments like this: 45 | 46 | ```javascript 47 | import { Molang, Context } from 'molang' 48 | 49 | const molang = new Molang({ 50 | query: { 51 | test: 1, 52 | }, 53 | context: { 54 | other: new Context({ 55 | query: { test: 2 }, 56 | }), 57 | }, 58 | }) 59 | 60 | molang.execute('query.test') // Returns 1 61 | molang.execute('context.other->query.test') // Returns 2 62 | ``` 63 | 64 | ## Using Custom Molang Functions 65 | 66 | Custom Molang functions were designed to support `.molang` files within bridge. 67 | 68 | ```javascript 69 | import { CustomMolang } from 'molang' 70 | 71 | const customMolang = new CustomMolang({}) 72 | 73 | const molangFunctions = ... // Somehow load Molang input that defines custom functions 74 | 75 | // Make custom functions known to Molang parser 76 | customMolang.parse(molangFunctions) 77 | 78 | const molangSource = ... // Somehow load Molang source from JSON files 79 | 80 | const transformedSource = customMolang.transform(molangSource) 81 | ... // Write the transformed source string back to the JSON file or do further processing 82 | ``` 83 | 84 | A custom Molang function is defined like this: 85 | 86 | ```javascript 87 | function('sq', 'base', { 88 | return math.pow(a.base, 2); 89 | }); 90 | 91 | function('pow', 'base', 'exp', { 92 | return a.exp == 0 ? 1 : a.base * f.pow(a.base, a.exp - 1); 93 | }); 94 | ``` 95 | 96 | - The first argument always defines the function name 97 | - All following arguments except the last one define input arguments 98 | - The last argument is the function body 99 | - Temporary variables get scoped to the current function body automatically 100 | - Basic recursion is supported as long as the interpreter can stop the recursive calls at compile-time 101 | - To call a function inside of Molang scripts, simply do `f.sq(2)` or `f.pow(3, 2)` 102 | 103 | ## Using AST Scripts 104 | 105 | You can write abitrary scripts to traverse the abstract syntax tree this library builds. 106 | 107 | ```javascript 108 | import { Molang, expressions } from 'molang' 109 | 110 | const molang = new Molang() 111 | 112 | let ast = molang.parse(`context.other->query.something + 1`) 113 | const { NumberExpression } = expressions 114 | 115 | // This increments all numbers within a Molang script 116 | ast = ast.walk((expr) => { 117 | if (expr instanceof NumberExpression) 118 | return new NumberExpression(expr.eval() + 1) 119 | }) 120 | 121 | const output = ast.toString() // 'context.other->query.something+2' 122 | ``` 123 | 124 | ## Performance 125 | 126 | **Disclaimer:** Both bridge.'s Molang library and Blockbench's library are usually fast enough. However, bridge.'s Molang interpreter shines when it comes to executing a wide variety of different scripts (ineffective cache) where it is up to 10x faster at interpreting a vanilla Molang script. 127 | 128 | ### Vanilla Script 129 | 130 | The following script gets executed 100,000 times for the first test: 131 | 132 | `variable.hand_bob = query.life_time < 0.01 ? 0.0 : variable.hand_bob + ((query.is_on_ground && query.is_alive ? math.clamp(math.sqrt(math.pow(query.position_delta(0), 2.0) + math.pow(query.position_delta(2), 2.0)), 0.0, 0.1) : 0.0) - variable.hand_bob) * 0.02;` 133 | 134 | ### Molang 135 | 136 | Used by bridge. 137 | 138 | | Test | Average Time | 139 | | -------------------------- | ------------ | 140 | | Parse & Execute (uncached) | 1253.332ms | 141 | | Parse & Execute (cached) | 90.036ms | 142 | 143 | ### MolangJS 144 | 145 | Used by Blockbench & Snowstorm 146 | | Test | Average Time | 147 | | -------------------------- | ------------ | 148 | | Parse & Execute (uncached) | 11872ms | 149 | | Parse & Execute (cached) | 185.299ms | 150 | 151 | ### Early Return 152 | 153 | The same script as above, except that we now insert a "return 1;" in front of it. bridge.'s interpreter is smart enough to figure out that the whole expression is static after it parsed `return 1;`. These kinds of optimizations can be found throughout our library. 154 | 155 | ### Molang 156 | 157 | Used by bridge. 158 | 159 | | Test | Average Time | 160 | | -------------------------- | ------------ | 161 | | Parse & Execute (uncached) | 103.61ms | 162 | | Parse & Execute (cached) | 8.835ms | 163 | 164 | ### MolangJS 165 | 166 | Used by Blockbench & Snowstorm 167 | | Test | Average Time | 168 | | -------------------------- | ------------ | 169 | | Parse & Execute (uncached) | 13230.682ms | 170 | | Parse & Execute (cached) | 147.786ms | 171 | 172 | ## Molang Playground 173 | 174 | We have built a very basic Molang playground with this interpreter. You can use it at [bridge-core.github.io/molang-playground](https://bridge-core.github.io/molang-playground). 175 | -------------------------------------------------------------------------------- /lib/custom/main.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionEnvironment } from '../env/env' 2 | import { IParserConfig } from '../main' 3 | import { MolangParser } from '../parser/molang' 4 | import { Tokenizer } from '../tokenizer/Tokenizer' 5 | import { CustomFunctionParselet } from './function' 6 | import { Molang } from '../Molang' 7 | import { StatementExpression } from '../parser/expressions/statement' 8 | import { transformStatement } from './transformStatement' 9 | import { NameExpression } from '../parser/expressions/name' 10 | import { ReturnExpression } from '../parser/expressions/return' 11 | import { GenericOperatorExpression } from '../parser/expressions/genericOperator' 12 | import { TernaryExpression } from '../parser/expressions/ternary' 13 | import { IExpression } from '../parser/expression' 14 | import { VoidExpression } from '../parser/expressions/void' 15 | import { GroupExpression } from '../parser/expressions/group' 16 | import { CustomClassParselet } from './class' 17 | 18 | export class CustomMolangParser extends MolangParser { 19 | public readonly functions = new Map() 20 | public readonly classes = new Map() // TODO: Make class data more specific than "any" 21 | 22 | constructor(config: Partial) { 23 | super(config) 24 | this.registerPrefix('FUNCTION', new CustomFunctionParselet()) 25 | // this.registerPrefix('CLASS', new CustomClassParselet()) 26 | } 27 | 28 | reset() { 29 | this.functions.clear() 30 | } 31 | } 32 | 33 | export class CustomMolang { 34 | protected parser: CustomMolangParser 35 | 36 | constructor(env: any) { 37 | this.parser = new CustomMolangParser({ 38 | useCache: false, 39 | useOptimizer: true, 40 | useAggressiveStaticOptimizer: true, 41 | keepGroups: true, 42 | earlyReturnsSkipParsing: false, 43 | earlyReturnsSkipTokenization: false, 44 | }) 45 | this.parser.setExecutionEnvironment( 46 | new ExecutionEnvironment(this.parser, env) 47 | ) 48 | this.parser.setTokenizer( 49 | new Tokenizer(new Set(['function' /*'class'*/])) 50 | ) 51 | } 52 | 53 | get functions() { 54 | return this.parser.functions 55 | } 56 | 57 | parse(expression: string) { 58 | this.parser.init(expression.replace(/\"/g, "'")) 59 | const abstractSyntaxTree = this.parser.parseExpression() 60 | 61 | return abstractSyntaxTree 62 | } 63 | 64 | transform(source: string) { 65 | const molang = new Molang( 66 | {}, 67 | { 68 | useCache: false, 69 | keepGroups: true, 70 | useOptimizer: true, 71 | useAggressiveStaticOptimizer: true, 72 | earlyReturnsSkipParsing: true, 73 | earlyReturnsSkipTokenization: false, 74 | } 75 | ) 76 | 77 | let totalScoped = 0 78 | let ast = molang.parse(source) 79 | 80 | let isComplexExpression = false 81 | if (ast instanceof StatementExpression) { 82 | isComplexExpression = true 83 | } 84 | 85 | let containsComplexExpressions = false 86 | ast = ast.walk((expr: any) => { 87 | // Only run code on function expressions which start with "f." or "function." 88 | if ( 89 | expr.type !== 'FunctionExpression' || 90 | (!expr.name.name.startsWith?.('f.') && 91 | !expr.name.name.startsWith?.('function.')) 92 | ) 93 | return 94 | 95 | const nameExpr = expr.name 96 | const functionName = nameExpr.name.replace(/(f|function)\./g, '') 97 | const argValues = expr.args 98 | 99 | let [args, functionBody] = this.functions.get(functionName) ?? [] 100 | if (!functionBody || !args) return 101 | 102 | // Insert argument values 103 | functionBody = functionBody.replace( 104 | /(a|arg)\.(\w+)/g, 105 | (match, prefix, argName) => { 106 | const val = 107 | argValues[args!.indexOf(argName)]?.toString() ?? '0' 108 | 109 | return val.replace(/(t|temp)\./, 'outer_temp.') 110 | } 111 | ) 112 | 113 | let funcAst = transformStatement(molang.parse(functionBody)) 114 | 115 | if (funcAst instanceof StatementExpression) { 116 | const hasTopLevelReturn = funcAst.allExpressions.some( 117 | (expr) => expr instanceof ReturnExpression 118 | ) 119 | const hasReturn = 120 | hasTopLevelReturn || 121 | funcAst.some((expr) => expr instanceof ReturnExpression) 122 | 123 | funcAst = molang.parse( 124 | `({${functionBody}}+${ 125 | hasReturn 126 | ? hasTopLevelReturn 127 | ? 't.return_value' 128 | : '(t.return_value??0)' // If there's no top-level return, we need to ensure that the variable access doesn't fail 129 | : '0' // Return 0 if no return statement 130 | })` 131 | ) 132 | 133 | containsComplexExpressions = true 134 | } 135 | 136 | const varNameMap = new Map() 137 | funcAst = funcAst.walk((expr) => { 138 | if (expr instanceof NameExpression) { 139 | const fullName = expr.toString() 140 | // Remove "a."/"t."/etc. from var name 141 | let tmp = fullName.split('.') 142 | const varType = tmp.shift() 143 | const [structName, ...structProperties] = tmp 144 | const structAccess = 145 | structProperties.length > 0 146 | ? '.' + structProperties.join('.') 147 | : '' 148 | 149 | if (varType === 't' || varType === 'temp') { 150 | // Scope temp./t. variables to functions 151 | let newName = varNameMap.get(structName) 152 | if (!newName) { 153 | newName = `t.__scvar${totalScoped++}` 154 | varNameMap.set(structName, newName) 155 | } 156 | 157 | expr.setName(`${newName}${structAccess}`) 158 | } else if (varType === 'outer_temp') { 159 | expr.setName(`t.${structName}${structAccess}`) 160 | } 161 | 162 | return undefined 163 | } else if (expr instanceof ReturnExpression) { 164 | const nameExpr = new NameExpression( 165 | molang.getParser().executionEnv, 166 | 't.return_value' 167 | ) 168 | const returnValExpr = expr.allExpressions[0] 169 | 170 | return new GenericOperatorExpression( 171 | nameExpr, 172 | returnValExpr, 173 | '=', 174 | () => { 175 | nameExpr.setPointer(returnValExpr.eval()) 176 | } 177 | ) 178 | } else if (expr instanceof StatementExpression) { 179 | // Make early returns work correctly by adjusting ternary statements which contain return statements 180 | const expressions: IExpression[] = [] 181 | 182 | for (let i = 0; i < expr.allExpressions.length; i++) { 183 | const currExpr = expr.allExpressions[i] 184 | 185 | if ( 186 | currExpr instanceof TernaryExpression && 187 | currExpr.hasReturn 188 | ) { 189 | handleTernary( 190 | currExpr, 191 | expr.allExpressions.slice(i + 1) 192 | ) 193 | 194 | expressions.push(currExpr) 195 | break 196 | } else if (currExpr.isReturn) { 197 | expressions.push(currExpr) 198 | break 199 | } 200 | 201 | expressions.push(currExpr) 202 | } 203 | 204 | return new StatementExpression(expressions) 205 | } 206 | }) 207 | 208 | return funcAst 209 | }) 210 | 211 | const finalAst = molang.parse(ast.toString()) 212 | molang.resolveStatic(finalAst) 213 | return !isComplexExpression && containsComplexExpressions 214 | ? `return ${finalAst.toString()};` 215 | : finalAst.toString() 216 | } 217 | 218 | reset() { 219 | this.functions.clear() 220 | } 221 | } 222 | 223 | function handleTernary( 224 | returnTernary: TernaryExpression, 225 | currentExpressions: IExpression[] 226 | ) { 227 | // If & else branch end with return statements -> we can omit everything after the ternary 228 | if (returnTernary.isReturn) return 229 | 230 | const notReturningBranchIndex = returnTernary.allExpressions[2].isReturn 231 | ? 1 232 | : 2 233 | const notReturningBranch = 234 | returnTernary.allExpressions[notReturningBranchIndex] 235 | 236 | if (!(notReturningBranch instanceof VoidExpression)) { 237 | if ( 238 | notReturningBranch instanceof GroupExpression && 239 | notReturningBranch.allExpressions[0] instanceof StatementExpression 240 | ) { 241 | currentExpressions.unshift(...notReturningBranch.allExpressions) 242 | } else { 243 | currentExpressions.unshift(notReturningBranch) 244 | } 245 | } 246 | if (currentExpressions.length > 0) 247 | returnTernary.setExpressionAt( 248 | notReturningBranchIndex, 249 | new GroupExpression( 250 | new StatementExpression(currentExpressions), 251 | '{}' 252 | ) 253 | ) 254 | } 255 | -------------------------------------------------------------------------------- /lib/Molang.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionEnvironment } from './env/env' 2 | import { IExpression, IParserConfig } from './main' 3 | import { NameExpression, PrefixExpression } from './parser/expressions' 4 | import { GenericOperatorExpression } from './parser/expressions/genericOperator' 5 | import { 6 | plusHelper, 7 | minusHelper, 8 | multiplyHelper, 9 | divideHelper, 10 | } from './parser/parselets/binaryOperator' 11 | import { StaticExpression } from './parser/expressions/static' 12 | import { StringExpression } from './parser/expressions/string' 13 | import { MolangParser } from './parser/molang' 14 | 15 | export class Molang { 16 | protected expressionCache: Record = {} 17 | protected totalCacheEntries = 0 18 | protected executionEnvironment!: ExecutionEnvironment 19 | 20 | protected parser: MolangParser 21 | 22 | constructor( 23 | env: Record = {}, 24 | protected config: Partial = {} 25 | ) { 26 | if (config.useOptimizer === undefined) this.config.useOptimizer = true 27 | if (config.useCache === undefined) this.config.useCache = true 28 | if (config.earlyReturnsSkipParsing === undefined) 29 | this.config.earlyReturnsSkipParsing = true 30 | if (config.earlyReturnsSkipTokenization === undefined) 31 | this.config.earlyReturnsSkipTokenization = true 32 | if (config.convertUndefined === undefined) 33 | this.config.convertUndefined = false 34 | 35 | this.parser = new MolangParser({ 36 | ...this.config, 37 | tokenizer: undefined, 38 | }) 39 | 40 | this.updateExecutionEnv(env, config.assumeFlatEnvironment) 41 | } 42 | 43 | updateConfig(newConfig: Partial) { 44 | newConfig = Object.assign(this.config, newConfig) 45 | 46 | if (newConfig.tokenizer) this.parser.setTokenizer(newConfig.tokenizer) 47 | this.parser.updateConfig({ ...this.config, tokenizer: undefined }) 48 | this.executionEnvironment.updateConfig(newConfig) 49 | } 50 | updateExecutionEnv(env: Record, isFlat = false) { 51 | this.executionEnvironment = new ExecutionEnvironment(env, { 52 | useRadians: this.config.useRadians, 53 | convertUndefined: this.config.convertUndefined, 54 | isFlat, 55 | variableHandler: this.config.variableHandler, 56 | }) 57 | this.parser.setExecutionEnvironment(this.executionEnvironment) 58 | } 59 | /** 60 | * Clears the Molang expression cache 61 | */ 62 | clearCache() { 63 | this.expressionCache = {} 64 | this.totalCacheEntries = 0 65 | } 66 | 67 | /** 68 | * Execute the given Molang string `expression` 69 | * @param expression The Molang string to execute 70 | * 71 | * @returns The value the Molang expression corresponds to 72 | */ 73 | execute(expression: string) { 74 | this.parser.setExecutionEnvironment(this.executionEnvironment) 75 | const abstractSyntaxTree = this.parse(expression) 76 | 77 | const result = abstractSyntaxTree.eval() 78 | if (result === undefined) return 0 79 | if (typeof result === 'boolean') return Number(result) 80 | return result 81 | } 82 | /** 83 | * Execute the given Molang string `expression` 84 | * In case of errors, return 0 85 | * @param expression The Molang string to execute 86 | * 87 | * @returns The value the Molang expression corresponds to and 0 if the statement is invalid 88 | */ 89 | executeAndCatch(expression: string) { 90 | try { 91 | return this.execute(expression) 92 | } catch { 93 | return 0 94 | } 95 | } 96 | 97 | /** 98 | * Parse the given Molang string `expression` 99 | * @param expression The Molang string to parse 100 | * 101 | * @returns An AST that corresponds to the Molang expression 102 | */ 103 | parse(expression: string): IExpression { 104 | if (this.config.useCache ?? true) { 105 | const abstractSyntaxTree = this.expressionCache[expression] 106 | if (abstractSyntaxTree) return abstractSyntaxTree 107 | } 108 | 109 | this.parser.init(expression) 110 | let abstractSyntaxTree = this.parser.parseExpression() 111 | if ((this.config.useOptimizer ?? true) && abstractSyntaxTree.isStatic()) 112 | abstractSyntaxTree = new StaticExpression(abstractSyntaxTree.eval()) 113 | // console.log(JSON.stringify(abstractSyntaxTree, null, ' ')) 114 | 115 | if (this.config.useCache ?? true) { 116 | if (this.totalCacheEntries > (this.config.maxCacheSize || 256)) 117 | this.clearCache() 118 | 119 | this.expressionCache[expression] = abstractSyntaxTree 120 | this.totalCacheEntries++ 121 | } 122 | 123 | return abstractSyntaxTree 124 | } 125 | 126 | rearrangeOptimally(ast: IExpression): IExpression { 127 | let lastAst 128 | do { 129 | lastAst = ast.toString() 130 | ast = ast.walk((expr) => { 131 | if (expr instanceof GenericOperatorExpression) { 132 | let leftExpr = expr.allExpressions[0] 133 | let rightExpr = expr.allExpressions[1] 134 | 135 | if ( 136 | leftExpr instanceof GenericOperatorExpression && 137 | rightExpr.isStatic() 138 | ) { 139 | let rightSubExpr = leftExpr.allExpressions[1] 140 | let leftSubExpr = leftExpr.allExpressions[0] 141 | 142 | //If leftmost is nonstatic and right is, swap 143 | if ( 144 | !leftSubExpr.isStatic() && 145 | !( 146 | leftSubExpr instanceof GenericOperatorExpression 147 | ) && 148 | rightSubExpr.isStatic() 149 | ) { 150 | let temp = leftSubExpr 151 | leftSubExpr = rightSubExpr 152 | rightSubExpr = temp 153 | } 154 | 155 | if (!rightSubExpr.isStatic()) { 156 | //Both are additions 157 | if ( 158 | expr.operator === '+' && 159 | leftExpr.operator === '+' 160 | ) { 161 | const newSubExpr = 162 | new GenericOperatorExpression( 163 | leftSubExpr, 164 | rightExpr, 165 | '+', 166 | plusHelper 167 | ) 168 | return new GenericOperatorExpression( 169 | newSubExpr, 170 | rightSubExpr, 171 | '+', 172 | plusHelper 173 | ) 174 | } 175 | 176 | //Both are subtractions 177 | if ( 178 | expr.operator === '-' && 179 | leftExpr.operator === '-' 180 | ) { 181 | const newSubExpr = 182 | new GenericOperatorExpression( 183 | leftSubExpr, 184 | rightExpr, 185 | '-', 186 | minusHelper 187 | ) 188 | return new GenericOperatorExpression( 189 | newSubExpr, 190 | rightSubExpr, 191 | '-', 192 | minusHelper 193 | ) 194 | } 195 | 196 | //Both are multiplications 197 | if ( 198 | expr.operator === '*' && 199 | leftExpr.operator === '*' 200 | ) { 201 | const newSubExpr = 202 | new GenericOperatorExpression( 203 | leftSubExpr, 204 | rightExpr, 205 | '*', 206 | multiplyHelper 207 | ) 208 | return new GenericOperatorExpression( 209 | newSubExpr, 210 | rightSubExpr, 211 | '*', 212 | multiplyHelper 213 | ) 214 | } 215 | 216 | //One is a division, other is a multiplication 217 | if ( 218 | (expr.operator === '/' && 219 | leftExpr.operator === '*') || 220 | (expr.operator === '*' && 221 | leftExpr.operator === '/') 222 | ) { 223 | const newSubExpr = 224 | new GenericOperatorExpression( 225 | leftSubExpr, 226 | rightExpr, 227 | '/', 228 | divideHelper 229 | ) 230 | return new GenericOperatorExpression( 231 | newSubExpr, 232 | rightSubExpr, 233 | '*', 234 | multiplyHelper 235 | ) 236 | } 237 | 238 | //Two divisions 239 | if ( 240 | expr.operator === '/' && 241 | leftExpr.operator === '/' 242 | ) { 243 | const newSubExpr = 244 | new GenericOperatorExpression( 245 | leftSubExpr, 246 | rightExpr, 247 | '*', 248 | multiplyHelper 249 | ) 250 | return new GenericOperatorExpression( 251 | rightSubExpr, 252 | newSubExpr, 253 | '/', 254 | divideHelper 255 | ) 256 | } 257 | 258 | //First is a subtraction, other is an addition 259 | if ( 260 | expr.operator === '-' && 261 | leftExpr.operator === '+' 262 | ) { 263 | const newSubExpr = 264 | new GenericOperatorExpression( 265 | leftSubExpr, 266 | rightExpr, 267 | '-', 268 | minusHelper 269 | ) 270 | return new GenericOperatorExpression( 271 | newSubExpr, 272 | rightSubExpr, 273 | '+', 274 | plusHelper 275 | ) 276 | } 277 | 278 | //First is an addition, other is an subtraction 279 | if ( 280 | expr.operator === '+' && 281 | leftExpr.operator === '-' 282 | ) { 283 | const newSubExpr = 284 | new GenericOperatorExpression( 285 | leftSubExpr, 286 | rightExpr, 287 | '+', 288 | plusHelper 289 | ) 290 | return new GenericOperatorExpression( 291 | newSubExpr, 292 | rightSubExpr, 293 | '-', 294 | minusHelper 295 | ) 296 | } 297 | } 298 | } 299 | } 300 | }) 301 | } while (ast.toString() !== lastAst) 302 | 303 | return ast 304 | } 305 | 306 | resolveStatic(ast: IExpression) { 307 | // 0. Rearrange statements so all static expressions can be resolved 308 | ast = this.rearrangeOptimally(ast) 309 | 310 | // 1. Resolve all static expressions 311 | ast = ast.walk((expr) => { 312 | if (expr instanceof StringExpression) return 313 | 314 | if (expr.isStatic()) return new StaticExpression(expr.eval()) 315 | }) 316 | 317 | // 2. Remove unnecessary operations 318 | ast = ast.walk((expr) => { 319 | if (expr instanceof GenericOperatorExpression) { 320 | switch (expr.operator) { 321 | case '+': 322 | case '-': { 323 | // If one of the two operands is 0, 324 | // we can simplify the expression to only return the other operand 325 | const zeroEquivalentOperand = expr.allExpressions.find( 326 | (expr) => expr.isStatic() && expr.eval() === 0 327 | ) 328 | //We check if the first operand is the zero equivalent operand 329 | const firstOperand = 330 | expr.allExpressions[0] === zeroEquivalentOperand 331 | if (zeroEquivalentOperand) { 332 | const otherOperand = expr.allExpressions.find( 333 | (expr) => expr !== zeroEquivalentOperand 334 | ) 335 | //If have subtraction and the first operand is the zero equivalent operand, we need to negate the other operand 336 | if ( 337 | expr.operator === '-' && 338 | firstOperand && 339 | otherOperand 340 | ) { 341 | return new PrefixExpression( 342 | 'MINUS', 343 | otherOperand 344 | ) 345 | } 346 | //Fallback to only returning the other operand 347 | return otherOperand 348 | } 349 | 350 | break 351 | } 352 | case '*': { 353 | // If one of the two operands is 0, 354 | // we can simplify the expression to 0 355 | const zeroEquivalentOperand = expr.allExpressions.find( 356 | (expr) => expr.isStatic() && expr.eval() === 0 357 | ) 358 | if (zeroEquivalentOperand) { 359 | return new StaticExpression(0) 360 | } 361 | 362 | // If one of the two operands is 1, 363 | // we can simplify the expression to only return the other operand 364 | const oneEquivalentOperand = expr.allExpressions.find( 365 | (expr) => expr.isStatic() && expr.eval() === 1 366 | ) 367 | if (oneEquivalentOperand) { 368 | const otherOperand = expr.allExpressions.find( 369 | (expr) => expr !== oneEquivalentOperand 370 | ) 371 | return otherOperand 372 | } 373 | } 374 | case '/': { 375 | const leftOperand = expr.allExpressions[0] 376 | const rightOperand = expr.allExpressions[1] 377 | // If the right operand is 1, we can simplify the expression to only return the left operand 378 | if ( 379 | rightOperand.isStatic() && 380 | rightOperand.eval() === 1 381 | ) { 382 | return leftOperand 383 | } 384 | 385 | // If the left operand is 0, we can simplify the expression to 0 386 | if ( 387 | leftOperand.isStatic() && 388 | leftOperand.eval() === 0 389 | ) { 390 | return new StaticExpression(0) 391 | } 392 | 393 | break 394 | } 395 | } 396 | 397 | //Limited common subexpression elimination 398 | switch (expr.operator) { 399 | case '+': { 400 | const leftOperand = expr.allExpressions[0] 401 | const rightOperand = expr.allExpressions[1] 402 | if ( 403 | leftOperand.toString() === rightOperand.toString() 404 | ) { 405 | return new GenericOperatorExpression( 406 | new StaticExpression(2), 407 | leftOperand, 408 | '*', 409 | multiplyHelper 410 | ) 411 | } 412 | break 413 | } 414 | case '-': { 415 | const leftOperand = expr.allExpressions[0] 416 | const rightOperand = expr.allExpressions[1] 417 | if ( 418 | leftOperand.toString() === rightOperand.toString() 419 | ) { 420 | return new StaticExpression(0) 421 | } 422 | } 423 | } 424 | } 425 | }) 426 | 427 | return ast 428 | } 429 | 430 | minimize(ast: IExpression) { 431 | // 1. Resolve all static expressions 432 | ast = this.resolveStatic(ast) 433 | 434 | // 2. Rename accessors to short hand 435 | const replaceMap = new Map([ 436 | ['query.', 'q.'], 437 | ['variable.', 'v.'], 438 | ['context.', 'c.'], 439 | ['temp.', 't.'], 440 | ]) 441 | ast = ast.walk((expr) => { 442 | if (expr instanceof NameExpression) { 443 | const name = expr.toString() 444 | 445 | for (const [key, replaceWith] of replaceMap) { 446 | if (name.startsWith(key)) { 447 | expr.setName(name.replace(key, replaceWith)) 448 | } 449 | } 450 | 451 | return expr 452 | } 453 | }) 454 | 455 | // 3. Rename variables 456 | /** 457 | * TODO: We need to store the variable map across multiple calls to minimize because 458 | * the variable transform needs to be consistent across multiple molang scripts. 459 | * Temporary variables should still be cleared after a single call to minimize though. 460 | */ 461 | const variableMap = new Map() 462 | ast = ast.walk((expr) => { 463 | if (expr instanceof NameExpression) { 464 | const name = expr.toString() 465 | 466 | if (!name.startsWith('v.') && !name.startsWith('t.')) return 467 | // Don't minify vars like "v.x" or "t.x" 468 | if (name.length === 3) return 469 | 470 | const varPrefix = name.startsWith('v.') ? 'v.' : 't.' 471 | 472 | if (variableMap.has(name)) { 473 | expr.setName(variableMap.get(name)) 474 | } else { 475 | // Get unique name 476 | const newName = `${varPrefix}v${variableMap.size}` 477 | variableMap.set(name, newName) 478 | expr.setName(newName) 479 | } 480 | 481 | return expr 482 | } 483 | }) 484 | 485 | return ast 486 | } 487 | 488 | getParser() { 489 | return this.parser 490 | } 491 | } 492 | --------------------------------------------------------------------------------