diff --git a/__tests__/minimize.ts b/__tests__/minimize.ts index 8e95506..ea4324d 100644 --- a/__tests__/minimize.ts +++ b/__tests__/minimize.ts @@ -18,6 +18,8 @@ test('Parse & stringify statements', () => { 'query.position()': 'q.position()', 'variable.x = 1; return variable.x;': 'v.v0=1;return v.v0;', '20 + 50': '70', + 'variable.x + 0' : 'v.v0', + '0 + variable.x' : 'v.v0' } for (const [test, result] of Object.entries(tests)) { diff --git a/__tests__/resolveStatic.ts b/__tests__/resolveStatic.ts index b70fb33..85cc1b1 100644 --- a/__tests__/resolveStatic.ts +++ b/__tests__/resolveStatic.ts @@ -16,12 +16,47 @@ test('Molang.resolveStatic(expr)', () => { const tests: Record = { 'v.test*0': '0', - + 'v.test+0': 'v.test', 'v.test-0': 'v.test', - + '0-v.test': '-v.test', + '0+v.test': 'v.test', + 'v.test/1': 'v.test', 'v.test*1': 'v.test', + '1*v.test': 'v.test', + '1/v.test': '1/v.test', + 'v.test/0': 'v.test/0', //While the molang specs state that this returns 0, we do not optimize this as it is a clear error of the programmer, so it should be kept for visibility + '0/v.test': '0', + '0*v.test': '0', + + //Test rearrangement + 'math.sin(query.life_time*180*.5-5)*4*0.2-0.1233' : '0.8*math.sin(90*query.life_time-5)-0.1233', + '3 * 10 * 0.3 * v.test * 10 * 20 * 100' : '180000*v.test', + '3 * 10 * 0.3 + 10 * v.test * 10 * 20 * 100' : '9+200000*v.test', + '1+v.test+2': '3+v.test', + '3 * 10 * 0.3 * v.test * 10 * v.x * 100' : '9000*v.test*v.x', + '3 + 10 + 1 * 0.2 * v.test * 10 * v.x * 100 + 10' : '23+200*v.test*v.x', + 'math.cos(((query.life_time - 2.0) * 180 / 3)) * 0.05 + 1' : 'math.cos((60*(query.life_time-2)))*0.05+1', + 'math.cos(((query.life_time - 2.0) / 180 / 3)) * 0.05 + 1' : 'math.cos(((query.life_time-2)/540))*0.05+1', + 'math.cos(((query.life_time - 2.0) / 180 * 3)) * 0.05 + 1' : 'math.cos((60*(query.life_time-2)))*0.05+1', + '3 / 10 * 0.3 * v.test * 20 * v.x / 100' : '0.018*v.test*v.x', + '3 - 10 + 0.3 + v.test * 20 * v.x+ 20 - 100' : '-86.7+v.test*20*v.x', + '3 - 10 - 2 - v.test - 2 - 5 - 20' : '-36-v.test', + '3 - 10 - 2 - v.test - 2 - 5 + 1 - 20' : '-35-v.test', + '3 - 10 - 2 - v.test - 2 - 5 + 1 - 20 + (34 * 10 - 30) / 2' : '120-v.test', + '3 - 10 - 2 - v.test - 2 - 5 + 1 - 20 + (34 * 10 - 30 * v.x) / 2' : '-35-v.test+(340-30*v.x)/2', + + //More Tests, making sure of the order of operations + '1 + 2 * 3': '7', + '(1 + 2) * 3': '9', + '1 + 2 * 3 + 4': '11', + '(1 + 2) * (3 + 4)': '21', + '1 + 2 * 3 + 4 * 5': '27', + + //Test common subexpression elimination + 'v.test + v.test': '2*v.test', + 'v.test - v.test': '0', } for (const [test, result] of Object.entries(tests)) { diff --git a/__tests__/stringify.ts b/__tests__/stringify.ts index f5217f6..399d809 100644 --- a/__tests__/stringify.ts +++ b/__tests__/stringify.ts @@ -27,6 +27,12 @@ test('Parse & stringify statements', () => { 'array.t[v.t]': 'array.t[v.t]', 'return -(1+1);': 'return -(1+1);', 'return .5;': 'return .5;', + 'return 1.5;': 'return 1.5;', + 'v.x++': '(v.x=v.x+1;0)+v.x-1', + 'v.x++;': '(v.x=v.x+1;0)+v.x-1;', + 'v.x--': '(v.x=v.x-1;0)+v.x+1', + 'v.x--;': '(v.x=v.x-1;0)+v.x+1;', + 'v.x = 1 + v.y++ + 2;': 'v.x=1+(v.y=v.y+1;0)+v.y-1+2;', } for (const [test, result] of Object.entries(tests)) { diff --git a/lib/Molang.ts b/lib/Molang.ts index f101d27..980ce44 100644 --- a/lib/Molang.ts +++ b/lib/Molang.ts @@ -1,7 +1,8 @@ import { ExecutionEnvironment } from './env/env' import { IExpression, IParserConfig } from './main' -import { NameExpression } from './parser/expressions' +import { NameExpression, PrefixExpression } from './parser/expressions' import { GenericOperatorExpression } from './parser/expressions/genericOperator' +import { plusHelper, minusHelper, multiplyHelper, divideHelper } from './parser/parselets/binaryOperator' import { StaticExpression } from './parser/expressions/static' import { StringExpression } from './parser/expressions/string' import { MolangParser } from './parser/molang' @@ -117,8 +118,85 @@ export class Molang { return abstractSyntaxTree } + rearrangeOptimally(ast: IExpression): IExpression { + + let lastAst + do { + lastAst = ast.toString() + ast = ast.walk((expr) => { + if (expr instanceof GenericOperatorExpression) { + + let leftExpr = expr.allExpressions[0] + let rightExpr = expr.allExpressions[1] + + if (leftExpr instanceof GenericOperatorExpression && rightExpr.isStatic()) { + + let rightSubExpr = leftExpr.allExpressions[1] + let leftSubExpr = leftExpr.allExpressions[0] + + //If leftmost is nonstatic and right is, swap + if (!leftSubExpr.isStatic() && !(leftSubExpr instanceof GenericOperatorExpression) && rightSubExpr.isStatic()) { + let temp = leftSubExpr + leftSubExpr = rightSubExpr + rightSubExpr = temp + } + + if (!rightSubExpr.isStatic()) { + + //Both are additions + if (expr.operator === '+' && leftExpr.operator === '+') { + const newSubExpr = new GenericOperatorExpression(leftSubExpr, rightExpr, '+', plusHelper); + return new GenericOperatorExpression(newSubExpr, rightSubExpr, '+', plusHelper) + } + + //Both are subtractions + if (expr.operator === '-' && leftExpr.operator === '-') { + const newSubExpr = new GenericOperatorExpression(leftSubExpr, rightExpr, '-', minusHelper); + return new GenericOperatorExpression(newSubExpr, rightSubExpr, '-', minusHelper) + } + + //Both are multiplications + if (expr.operator === '*' && leftExpr.operator === '*') { + const newSubExpr = new GenericOperatorExpression(leftSubExpr, rightExpr, '*', multiplyHelper); + return new GenericOperatorExpression(newSubExpr, rightSubExpr, '*', multiplyHelper) + } + + //One is a division, other is a multiplication + if (expr.operator === '/' && leftExpr.operator === '*' || expr.operator === '*' && leftExpr.operator === '/') { + const newSubExpr = new GenericOperatorExpression(leftSubExpr, rightExpr, '/', divideHelper); + return new GenericOperatorExpression(newSubExpr, rightSubExpr, '*', multiplyHelper) + } + + //Two divisions + if (expr.operator === '/' && leftExpr.operator === '/') { + const newSubExpr = new GenericOperatorExpression(leftSubExpr, rightExpr, '*', multiplyHelper); + return new GenericOperatorExpression(rightSubExpr, newSubExpr, '/', divideHelper) + } + + //First is a subtraction, other is an addition + if (expr.operator === '-' && leftExpr.operator === '+') { + const newSubExpr = new GenericOperatorExpression(leftSubExpr, rightExpr, '-', minusHelper); + return new GenericOperatorExpression(newSubExpr, rightSubExpr, '+', plusHelper) + } + + //First is an addition, other is an subtraction + if (expr.operator === '+' && leftExpr.operator === '-') { + const newSubExpr = new GenericOperatorExpression(leftSubExpr, rightExpr, '+', plusHelper); + return new GenericOperatorExpression(newSubExpr, rightSubExpr, '-', minusHelper) + } + } + } + } + }) + } while (ast.toString() !== lastAst) + + return ast; + } + + resolveStatic(ast: IExpression) { - // 0. TODO: Rearrange statements so all static expressions can be resolved + // 0. Rearrange statements so all static expressions can be resolved + ast = this.rearrangeOptimally(ast) // 1. Resolve all static expressions ast = ast.walk((expr) => { @@ -138,10 +216,18 @@ export class Molang { const zeroEquivalentOperand = expr.allExpressions.find( (expr) => expr.isStatic() && expr.eval() === 0 ) + //We check if the first operand is the zero equivalent operand + const firstOperand = expr.allExpressions[0] === zeroEquivalentOperand if (zeroEquivalentOperand) { - return expr.allExpressions.find( + const otherOperand = expr.allExpressions.find( (expr) => expr !== zeroEquivalentOperand ) + //If have subtraction and the first operand is the zero equivalent operand, we need to negate the other operand + if (expr.operator === '-' && firstOperand && otherOperand) { + return new PrefixExpression('MINUS', otherOperand) + } + //Fallback to only returning the other operand + return otherOperand } break @@ -155,23 +241,55 @@ export class Molang { if (zeroEquivalentOperand) { return new StaticExpression(0) } - } - case '*': - case '/': { + // If one of the two operands is 1, // we can simplify the expression to only return the other operand const oneEquivalentOperand = expr.allExpressions.find( (expr) => expr.isStatic() && expr.eval() === 1 ) if (oneEquivalentOperand) { - return expr.allExpressions.find( + const otherOperand = expr.allExpressions.find( (expr) => expr !== oneEquivalentOperand ) + return otherOperand + } + } + case '/': { + + const leftOperand = expr.allExpressions[0] + const rightOperand = expr.allExpressions[1] + // If the right operand is 1, we can simplify the expression to only return the left operand + if (rightOperand.isStatic() && rightOperand.eval() === 1) { + return leftOperand + } + + // If the left operand is 0, we can simplify the expression to 0 + if (leftOperand.isStatic() && leftOperand.eval() === 0) { + return new StaticExpression(0) } break } } + + //Limited common subexpression elimination + switch (expr.operator) { + case '+': { + const leftOperand = expr.allExpressions[0] + const rightOperand = expr.allExpressions[1] + if (leftOperand.toString() === rightOperand.toString()) { + return new GenericOperatorExpression(new StaticExpression(2), leftOperand, '*', multiplyHelper) + } + break + } + case '-':{ + const leftOperand = expr.allExpressions[0] + const rightOperand = expr.allExpressions[1] + if (leftOperand.toString() === rightOperand.toString()) { + return new StaticExpression(0) + } + } + } } }) diff --git a/lib/parser/expressions/name.ts b/lib/parser/expressions/name.ts index efd99f5..c914b7a 100644 --- a/lib/parser/expressions/name.ts +++ b/lib/parser/expressions/name.ts @@ -28,6 +28,11 @@ export class NameExpression extends Expression { setFunctionCall(value = true) { this.isFunctionCall = value } + + isFunction() { + return this.isFunctionCall + } + setName(name: string) { this.name = name } diff --git a/lib/parser/expressions/postfix.ts b/lib/parser/expressions/postfix.ts index c023e62..e80ce1b 100644 --- a/lib/parser/expressions/postfix.ts +++ b/lib/parser/expressions/postfix.ts @@ -1,3 +1,4 @@ +import { ExecutionEnvironment } from '../../env/env' import { TTokenType } from '../../tokenizer/token' import { Expression, IExpression } from '../expression' @@ -5,6 +6,7 @@ export class PostfixExpression extends Expression { type = 'PostfixExpression' constructor( + public executionEnv: ExecutionEnvironment, protected expression: IExpression, protected tokenType: TTokenType ) { @@ -19,14 +21,24 @@ export class PostfixExpression extends Expression { } isStatic() { - return this.expression.isStatic() + return false; } eval() { + return this.expression.eval() + } + + //TODO: Replace this by ast.walk transformation + toString(): string { + const expr = this.expression.toString() switch (this.tokenType) { - case 'X': { - // DO SOMETHING + case 'PLUS': { + return `(${expr}=${expr}+1;0)+${expr}-1` + } + case 'MINUS': { + return `(${expr}=${expr}-1;0)+${expr}+1` } } + throw new Error(`Unknown postfix operator: "${this.tokenType}"`) } } diff --git a/lib/parser/molang.ts b/lib/parser/molang.ts index 66c52e3..9276271 100644 --- a/lib/parser/molang.ts +++ b/lib/parser/molang.ts @@ -24,6 +24,7 @@ import { OrOperator } from './parselets/OrOperator' import { SmallerOperator } from './parselets/SmallerOperator' import { GreaterOperator } from './parselets/GreaterOperator' import { QuestionOperator } from './parselets/QuestionOperator' +import { PostInDecrementOperator } from './parselets/postInDecremet' export class MolangParser extends Parser { constructor(config: Partial) { @@ -84,5 +85,7 @@ export class MolangParser extends Parser { this.registerInfix('AND', new AndOperator(EPrecedence.AND)) this.registerInfix('OR', new OrOperator(EPrecedence.OR)) this.registerInfix('ASSIGN', new BinaryOperator(EPrecedence.ASSIGNMENT)) + this.registerInfix('PLUSPLUS', new PostInDecrementOperator(EPrecedence.POSTFIX)) + this.registerInfix('MINUSMINUS', new PostInDecrementOperator(EPrecedence.POSTFIX)) } } diff --git a/lib/parser/parse.ts b/lib/parser/parse.ts index 2ebc865..3e399c5 100644 --- a/lib/parser/parse.ts +++ b/lib/parser/parse.ts @@ -55,6 +55,12 @@ export class Parser { if (tokenType === 'EQUALS' && !this.match('EQUALS')) { tokenType = 'ASSIGN' } + if(tokenType === 'PLUS' && this.match('PLUS')) { + tokenType = 'PLUSPLUS' + } + if(tokenType === 'MINUS' && this.match('MINUS')) { + tokenType = 'MINUSMINUS' + } const infix = this.infixParselets.get(tokenType) if (!infix) diff --git a/lib/parser/parselets/binaryOperator.ts b/lib/parser/parselets/binaryOperator.ts index 3cd7d32..ad3ca7e 100644 --- a/lib/parser/parselets/binaryOperator.ts +++ b/lib/parser/parselets/binaryOperator.ts @@ -4,7 +4,7 @@ import { IExpression } from '../expression' import { Token } from '../../tokenizer/token' import { GenericOperatorExpression } from '../expressions/genericOperator' -const plusHelper = ( +export const plusHelper = ( leftExpression: IExpression, rightExpression: IExpression ) => { @@ -20,7 +20,7 @@ const plusHelper = ( //@ts-ignore return leftValue + rightValue } -const minusHelper = ( +export const minusHelper = ( leftExpression: IExpression, rightExpression: IExpression ) => { @@ -36,7 +36,7 @@ const minusHelper = ( //@ts-ignore return leftValue - rightValue } -const divideHelper = ( +export const divideHelper = ( leftExpression: IExpression, rightExpression: IExpression ) => { @@ -52,7 +52,7 @@ const divideHelper = ( //@ts-ignore return leftValue / rightValue } -const multiplyHelper = ( +export const multiplyHelper = ( leftExpression: IExpression, rightExpression: IExpression ) => { @@ -68,7 +68,7 @@ const multiplyHelper = ( //@ts-ignore return leftValue * rightValue } -const assignHelper = ( +export const assignHelper = ( leftExpression: IExpression, rightExpression: IExpression ) => { diff --git a/lib/parser/parselets/postInDecremet.ts b/lib/parser/parselets/postInDecremet.ts new file mode 100644 index 0000000..68651c4 --- /dev/null +++ b/lib/parser/parselets/postInDecremet.ts @@ -0,0 +1,21 @@ +import { Parser } from '../../parser/parse' +import { Token } from '../../tokenizer/token' +import { IExpression } from '../expression' +import { NameExpression, PostfixExpression } from '../expressions' +import { IPostfixParselet } from './postfix' + +export class PostInDecrementOperator implements IPostfixParselet { + constructor(public precedence = 0) {} + + parse(parser: Parser, leftExpression: IExpression, token: Token) { + + if(!(leftExpression instanceof NameExpression) || (leftExpression as NameExpression).isFunction()) + throw new Error(`Cannot use postfix ${token.getText()} on ${leftExpression.toString()}`) + + return new PostfixExpression( + parser.executionEnv, + leftExpression, + token.getType() + ) + } +}