Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Postfix Operators #17

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions __tests__/minimize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
39 changes: 37 additions & 2 deletions __tests__/resolveStatic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,47 @@ test('Molang.resolveStatic(expr)', () => {

const tests: Record<string, string> = {
'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)) {
Expand Down
6 changes: 6 additions & 0 deletions __tests__/stringify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
132 changes: 125 additions & 7 deletions lib/Molang.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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) => {
Expand All @@ -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
Expand All @@ -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)
}
}
}
}
})

Expand Down
5 changes: 5 additions & 0 deletions lib/parser/expressions/name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
18 changes: 15 additions & 3 deletions lib/parser/expressions/postfix.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { ExecutionEnvironment } from '../../env/env'
import { TTokenType } from '../../tokenizer/token'
import { Expression, IExpression } from '../expression'

export class PostfixExpression extends Expression {
type = 'PostfixExpression'

constructor(
public executionEnv: ExecutionEnvironment,
protected expression: IExpression,
protected tokenType: TTokenType
) {
Expand All @@ -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}"`)
}
}
3 changes: 3 additions & 0 deletions lib/parser/molang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<IParserConfig>) {
Expand Down Expand Up @@ -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))
}
}
6 changes: 6 additions & 0 deletions lib/parser/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <IInfixParselet>this.infixParselets.get(tokenType)
if (!infix)
Expand Down
10 changes: 5 additions & 5 deletions lib/parser/parselets/binaryOperator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
) => {
Expand All @@ -20,7 +20,7 @@ const plusHelper = (
//@ts-ignore
return leftValue + rightValue
}
const minusHelper = (
export const minusHelper = (
leftExpression: IExpression,
rightExpression: IExpression
) => {
Expand All @@ -36,7 +36,7 @@ const minusHelper = (
//@ts-ignore
return leftValue - rightValue
}
const divideHelper = (
export const divideHelper = (
leftExpression: IExpression,
rightExpression: IExpression
) => {
Expand All @@ -52,7 +52,7 @@ const divideHelper = (
//@ts-ignore
return leftValue / rightValue
}
const multiplyHelper = (
export const multiplyHelper = (
leftExpression: IExpression,
rightExpression: IExpression
) => {
Expand All @@ -68,7 +68,7 @@ const multiplyHelper = (
//@ts-ignore
return leftValue * rightValue
}
const assignHelper = (
export const assignHelper = (
leftExpression: IExpression,
rightExpression: IExpression
) => {
Expand Down
21 changes: 21 additions & 0 deletions lib/parser/parselets/postInDecremet.ts
Original file line number Diff line number Diff line change
@@ -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()
)
}
}