diff --git a/__tests__/conditionals.ts b/__tests__/conditionals.ts new file mode 100644 index 0000000..aaad587 --- /dev/null +++ b/__tests__/conditionals.ts @@ -0,0 +1,65 @@ +import { expect, test } from 'vitest' +import { Molang } from '../lib/Molang' + +const molang = new Molang() + +test('Basic if statement', () => { + const statement = ` + if (true) { + return 1; + }` + + expect(molang.execute(statement)).toBe(1) +}) + +test('Elif chain with variables', () => { + const statement = ` + v.test = 10; + + if (v.test == 0) { + return 1; + } elif (v.test == 5) { + return 2; + } elif (v.test == 10) { + return 3; + }` + + expect(molang.execute(statement)).toBe(3) +}) + +test('Else statement', () => { + const statement = ` + v.test = 50; + + if (v.test == 0) { + return 1; + } elif (v.test == 5) { + return 2; + } elif (v.test == 10) { + return 3; + } else { + return 4; + }` + + expect(molang.execute(statement)).toBe(4) +}) + +test('Nested conditionals', () => { + const statement = ` + v.test = 10; + v.test2 = 5; + + if (v.test == 0) { + return 1; + } elif (v.test == 5) { + return 2; + } elif (v.test == 10) { + if (v.test2 == 2) { + return 3; + } else { + return 4; + } + }` + + expect(molang.execute(statement)).toBe(4) +}) diff --git a/lib/parser/expressions/if.ts b/lib/parser/expressions/if.ts new file mode 100644 index 0000000..77909d2 --- /dev/null +++ b/lib/parser/expressions/if.ts @@ -0,0 +1,80 @@ +import { Expression, IExpression } from '../expression' + +export class IfExpression extends Expression { + type = 'IfExpression' + + constructor( + protected test: IExpression, + protected consequent: IExpression, + protected elifClauses: IfExpression[] = [], + protected alternate?: IExpression + ) { + super() + } + + get isReturn(): boolean { + return ( + this.consequent.isReturn || + this.elifClauses.some((clause) => clause.isReturn) + ) + } + + get allExpressions(): IExpression[] { + return [ + this.test, + this.consequent, + ...this.elifClauses.flatMap((clause) => clause.allExpressions), + this.alternate, + ].filter((expr) => expr !== undefined) + } + setExpressionAt(index: number, expr: IExpression) { + if (index === 0) this.test = expr + else if (index === 1) this.consequent = expr + else if (index > 1 && index < 2 + this.elifClauses.length) { + const clauseIndex = index - 2 + this.elifClauses[clauseIndex].setExpressionAt(0, expr) + } else if (index === 2 + this.elifClauses.length) this.alternate = expr + } + + isStatic(): boolean { + return ( + this.test.isStatic() && + this.consequent.isStatic() && + this.elifClauses.every((clause) => clause.isStatic()) && + (!this.alternate || this.alternate.isStatic()) + ) + } + + eval() { + if (this.test.eval()) { + const val = this.consequent.eval() + + return val + } + + for (let i = 0; i < this.elifClauses.length; i++) { + if (this.elifClauses[i].test.eval()) { + return this.elifClauses[i].consequent.eval() + } + } + + if (this.alternate) { + return this.alternate.eval() + } + + return null + } + + toString() { + const elifString = this.elifClauses + .map( + (clause) => + `elif (${clause.test.toString()}) {${clause.consequent.toString()}}` + ) + .join(' ') + const elseString = this.alternate + ? `else {${this.alternate.toString()}}` + : '' + return `if (${this.test.toString()}) {${this.consequent.toString()}} ${elifString} ${elseString}` + } +} diff --git a/lib/parser/expressions/index.ts b/lib/parser/expressions/index.ts index e83ea1c..6ba3418 100644 --- a/lib/parser/expressions/index.ts +++ b/lib/parser/expressions/index.ts @@ -17,3 +17,4 @@ export { StaticExpression } from './static' export { StringExpression } from './string' export { TernaryExpression } from './ternary' export { VoidExpression } from './void' +export { IfExpression } from './if' diff --git a/lib/parser/molang.ts b/lib/parser/molang.ts index 66c52e3..9560c28 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 { IfParselet } from './parselets/if' export class MolangParser extends Parser { constructor(config: Partial) { @@ -40,6 +41,7 @@ export class MolangParser extends Parser { this.registerPrefix('BREAK', new BreakParselet()) this.registerPrefix('LOOP', new LoopParselet()) this.registerPrefix('FOR_EACH', new ForEachParselet()) + this.registerPrefix('IF', new IfParselet()) this.registerInfix( 'QUESTION', new QuestionOperator(EPrecedence.CONDITIONAL) diff --git a/lib/parser/parselets/if.ts b/lib/parser/parselets/if.ts new file mode 100644 index 0000000..fce3fd9 --- /dev/null +++ b/lib/parser/parselets/if.ts @@ -0,0 +1,41 @@ +import { Token } from '../../tokenizer/token' +import { IfExpression } from '../expressions' +import { Parser } from '../parse' +import { IPrefixParselet } from './prefix' + +export class IfParselet implements IPrefixParselet { + constructor(public precedence = 0) {} + + parse(parser: Parser, token: Token) { + parser.consume('LEFT_PARENT') + const condition = parser.parseExpression(this.precedence) + parser.consume('RIGHT_PARENT') + + parser.consume('CURLY_LEFT') + const consequent = parser.parseExpression(this.precedence) + parser.consume('CURLY_RIGHT') + + const elifClauses: IfExpression[] = [] + + while (parser.match('ELIF')) { + parser.consume('LEFT_PARENT') + const elifCondition = parser.parseExpression(this.precedence) + parser.consume('RIGHT_PARENT') + + parser.consume('CURLY_LEFT') + const elifConsequent = parser.parseExpression(this.precedence) + parser.consume('CURLY_RIGHT') + + elifClauses.push(new IfExpression(elifCondition, elifConsequent)) + } + + let alternate + if (parser.match('ELSE')) { + parser.consume('CURLY_LEFT') + alternate = parser.parseExpression(this.precedence) + parser.consume('CURLY_RIGHT') + } + + return new IfExpression(condition, consequent, elifClauses, alternate) + } +} diff --git a/lib/tokenizer/tokenTypes.ts b/lib/tokenizer/tokenTypes.ts index aca6666..334d2e4 100644 --- a/lib/tokenizer/tokenTypes.ts +++ b/lib/tokenizer/tokenTypes.ts @@ -29,4 +29,7 @@ export const KeywordTokens = new Set([ 'loop', 'false', 'true', + 'if', + 'elif', + 'else', ])