diff --git a/src/lang/abstractSyntaxTree.test.ts b/src/lang/abstractSyntaxTree.test.ts index 33de5ee1e..95f521635 100644 --- a/src/lang/abstractSyntaxTree.test.ts +++ b/src/lang/abstractSyntaxTree.test.ts @@ -2,6 +2,7 @@ import { abstractSyntaxTree, findClosingBrace, hasPipeOperator, + findEndOfBinaryExpression, } from './abstractSyntaxTree' import { lexer } from './tokeniser' @@ -1521,3 +1522,334 @@ describe('testing pipe operator special', () => { ]) }) }) + +describe('nests binary expressions correctly', () => { + it('it works with the simple case', () => { + const code = `const yo = 1 + 2` + const { body } = abstractSyntaxTree(lexer(code)) + expect(body[0]).toEqual({ + type: 'VariableDeclaration', + start: 0, + end: 16, + kind: 'const', + declarations: [ + { + type: 'VariableDeclarator', + start: 6, + end: 16, + id: { + type: 'Identifier', + start: 6, + end: 8, + name: 'yo', + }, + init: { + type: 'BinaryExpression', + start: 11, + end: 16, + left: { + type: 'Literal', + start: 11, + end: 12, + value: 1, + raw: '1', + }, + operator: '+', + right: { + type: 'Literal', + start: 15, + end: 16, + value: 2, + raw: '2', + }, + }, + }, + ], + }) + }) + it('it should nest according to precedence with multiply first', () => { + // should be binExp { binExp { lit-1 * lit-2 } + lit} + const code = `const yo = 1 * 2 + 3` + const { body } = abstractSyntaxTree(lexer(code)) + expect(body[0]).toEqual({ + type: 'VariableDeclaration', + start: 0, + end: 20, + kind: 'const', + declarations: [ + { + type: 'VariableDeclarator', + start: 6, + end: 20, + id: { + type: 'Identifier', + start: 6, + end: 8, + name: 'yo', + }, + init: { + type: 'BinaryExpression', + start: 11, + end: 20, + left: { + type: 'BinaryExpression', + start: 11, + end: 16, + left: { + type: 'Literal', + start: 11, + end: 12, + value: 1, + raw: '1', + }, + operator: '*', + right: { + type: 'Literal', + start: 15, + end: 16, + value: 2, + raw: '2', + }, + }, + operator: '+', + right: { + type: 'Literal', + start: 19, + end: 20, + value: 3, + raw: '3', + }, + }, + }, + ], + }) + }) + it('it should nest according to precedence with sum first', () => { + // should be binExp { lit-1 + binExp { lit-2 * lit-3 } } + const code = `const yo = 1 + 2 * 3` + const { body } = abstractSyntaxTree(lexer(code)) + expect(body[0]).toEqual({ + type: 'VariableDeclaration', + start: 0, + end: 20, + kind: 'const', + declarations: [ + { + type: 'VariableDeclarator', + start: 6, + end: 20, + id: { + type: 'Identifier', + start: 6, + end: 8, + name: 'yo', + }, + init: { + type: 'BinaryExpression', + start: 11, + end: 20, + left: { + type: 'Literal', + start: 11, + end: 12, + value: 1, + raw: '1', + }, + operator: '+', + right: { + type: 'BinaryExpression', + start: 15, + end: 20, + left: { + type: 'Literal', + start: 15, + end: 16, + value: 2, + raw: '2', + }, + operator: '*', + right: { + type: 'Literal', + start: 19, + end: 20, + value: 3, + raw: '3', + }, + }, + }, + }, + ], + }) + }) + it('it should nest properly with two opperators of equal precedence', () => { + const code = `const yo = 1 + 2 - 3` + const { body } = abstractSyntaxTree(lexer(code)) + expect((body[0] as any).declarations[0].init).toEqual({ + type: 'BinaryExpression', + start: 11, + end: 20, + left: { + type: 'BinaryExpression', + start: 11, + end: 16, + left: { + type: 'Literal', + start: 11, + end: 12, + value: 1, + raw: '1', + }, + operator: '+', + right: { + type: 'Literal', + start: 15, + end: 16, + value: 2, + raw: '2', + }, + }, + operator: '-', + right: { + type: 'Literal', + start: 19, + end: 20, + value: 3, + raw: '3', + }, + }) + }) + it('it should nest properly with two opperators of equal (but higher) precedence', () => { + const code = `const yo = 1 * 2 / 3` + const { body } = abstractSyntaxTree(lexer(code)) + expect((body[0] as any).declarations[0].init).toEqual({ + type: 'BinaryExpression', + start: 11, + end: 20, + left: { + type: 'BinaryExpression', + start: 11, + end: 16, + left: { + type: 'Literal', + start: 11, + end: 12, + value: 1, + raw: '1', + }, + operator: '*', + right: { + type: 'Literal', + start: 15, + end: 16, + value: 2, + raw: '2', + }, + }, + operator: '/', + right: { + type: 'Literal', + start: 19, + end: 20, + value: 3, + raw: '3', + }, + }) + }) + it('it should nest properly with longer example', () => { + const code = `const yo = 1 + 2 * (3 - 4) / 5 + 6` + const { body } = abstractSyntaxTree(lexer(code)) + const init = (body[0] as any).declarations[0].init + expect(init).toEqual({ + type: 'BinaryExpression', + operator: '+', + start: 11, + end: 34, + left: { + type: 'BinaryExpression', + operator: '+', + start: 11, + end: 30, + left: { type: 'Literal', value: 1, raw: '1', start: 11, end: 12 }, + right: { + type: 'BinaryExpression', + operator: '/', + start: 15, + end: 30, + left: { + type: 'BinaryExpression', + operator: '*', + start: 15, + end: 26, + left: { type: 'Literal', value: 2, raw: '2', start: 15, end: 16 }, + right: { + type: 'BinaryExpression', + operator: '-', + start: 20, + end: 25, + left: { type: 'Literal', value: 3, raw: '3', start: 20, end: 21 }, + right: { + type: 'Literal', + value: 4, + raw: '4', + start: 24, + end: 25, + }, + }, + }, + right: { type: 'Literal', value: 5, raw: '5', start: 29, end: 30 }, + }, + }, + right: { type: 'Literal', value: 6, raw: '6', start: 33, end: 34 }, + }) + }) +}) + +describe('testing findEndofBinaryExpression', () => { + it('1 + 2 * 3', () => { + const code = `1 + 2 * 3\nconst yo = 5` + const tokens = lexer(code) + const end = findEndOfBinaryExpression(tokens, 0) + expect(end).toBe(8) + }) + it('(1 + 2) / 5 - 3', () => { + const code = `(1 + 25) / 5 - 3\nconst yo = 5` + const tokens = lexer(code) + const end = findEndOfBinaryExpression(tokens, 0) + expect(end).toBe(14) + + // expect to have the same end if started later in the string at a legitimate place + const indexOf5 = code.indexOf('5') + const endStartingAtThe5 = findEndOfBinaryExpression(tokens, indexOf5) + expect(endStartingAtThe5).toBe(end) + }) + it('whole thing wraped: ((1 + 2) / 5 - 3)', () => { + const code = '((1 + 2) / 5 - 3)\nconst yo = 5' + const tokens = lexer(code) + const end = findEndOfBinaryExpression(tokens, 0) + expect(end).toBe(code.indexOf('3)') + 1) + }) + it('whole thing wraped but given index after the first brace: ((1 + 2) / 5 - 3)', () => { + const code = '((1 + 2) / 5 - 3)\nconst yo = 5' + const tokens = lexer(code) + const end = findEndOfBinaryExpression(tokens, 1) + expect(end).toBe(code.indexOf('3')) + }) + it('given the index of a small wrapped section i.e. `1 + 2` in ((1 + 2) / 5 - 3)', () => { + const code = '((1 + 2) / 5 - 3)\nconst yo = 5' + const tokens = lexer(code) + const end = findEndOfBinaryExpression(tokens, 2) + expect(end).toBe(code.indexOf('2')) + }) + it('lots of silly nesting: (1 + 2) / (5 - (3))', () => { + const code = '(1 + 2) / (5 - (3))\nconst yo = 5' + const tokens = lexer(code) + const end = findEndOfBinaryExpression(tokens, 0) + expect(end).toBe(code.indexOf('))') + 1) + }) + it('with pipe operator at the end', () => { + const code = '(1 + 2) / (5 - (3))\n |> fn(%)' + const tokens = lexer(code) + const end = findEndOfBinaryExpression(tokens, 0) + expect(end).toBe(code.indexOf('))') + 1) + }) +}) diff --git a/src/lang/abstractSyntaxTree.ts b/src/lang/abstractSyntaxTree.ts index ba004e2c3..7a046cc59 100644 --- a/src/lang/abstractSyntaxTree.ts +++ b/src/lang/abstractSyntaxTree.ts @@ -1,5 +1,6 @@ import { PathToNode } from './executor' import { Token } from './tokeniser' +import { parseExpression } from './astMathExpressions' type syntaxType = | 'Program' @@ -473,8 +474,7 @@ function makeVariableDeclarators( } } -export type BinaryPart = Literal | Identifier -// | BinaryExpression +export type BinaryPart = Literal | Identifier | BinaryExpression // | CallExpression // | MemberExpression // | ArrayExpression @@ -764,7 +764,7 @@ export interface BinaryExpression extends GeneralStatement { function makeBinaryPart( tokens: Token[], index: number -): { part: BinaryPart; lastIndex: number } { +): { part: Literal | Identifier; lastIndex: number } { const currentToken = tokens[index] if (currentToken.type === 'word') { const identifier = makeIdentifier(tokens, index) @@ -783,28 +783,43 @@ function makeBinaryPart( throw new Error('Expected a previous BinaryPart if statement to match') } +export function findEndOfBinaryExpression( + tokens: Token[], + index: number +): number { + const currentToken = tokens[index] + if (currentToken.type === 'brace' && currentToken.value === '(') { + const closingParenthesis = findClosingBrace(tokens, index) + const maybeAnotherOperator = nextMeaningfulToken(tokens, closingParenthesis) + if ( + maybeAnotherOperator?.token?.type !== 'operator' || + maybeAnotherOperator?.token?.value === '|>' + ) { + return closingParenthesis + } + const nextRight = nextMeaningfulToken(tokens, maybeAnotherOperator.index) + return findEndOfBinaryExpression(tokens, nextRight.index) + } + const maybeOperator = nextMeaningfulToken(tokens, index) + if ( + maybeOperator?.token?.type !== 'operator' || + maybeOperator?.token?.value === '|>' + ) { + return index + } + const nextRight = nextMeaningfulToken(tokens, maybeOperator.index) + return findEndOfBinaryExpression(tokens, nextRight.index) +} + function makeBinaryExpression( tokens: Token[], index: number ): { expression: BinaryExpression; lastIndex: number } { - const currentToken = tokens[index] - const { part: left } = makeBinaryPart(tokens, index) - const { token: operatorToken, index: operatorIndex } = nextMeaningfulToken( - tokens, - index - ) - const rightToken = nextMeaningfulToken(tokens, operatorIndex) - const { part: right } = makeBinaryPart(tokens, rightToken.index) + const endIndex = findEndOfBinaryExpression(tokens, index) + const expression = parseExpression(tokens.slice(index, endIndex + 1)) return { - expression: { - type: 'BinaryExpression', - start: currentToken.start, - end: right.end, - left, - operator: operatorToken.value, - right, - }, - lastIndex: rightToken.index, + expression, + lastIndex: endIndex, } } @@ -1296,6 +1311,61 @@ export function findClosingBrace( return findClosingBrace(tokens, index + 1, _braceCount, searchOpeningBrace) } +// function findOpeningBrace( +// tokens: Token[], +// index: number, +// _braceCount: number = 0, +// _searchClosingBrace: string = '' +// ): number { +// // should be called with the index of the opening brace +// const closingBraceMap: { [key: string]: string } = { +// ')': '(', +// '}': '{', +// ']': '[', +// } +// const currentToken = tokens[index] +// let searchClosingBrace = _searchClosingBrace + +// const isFirstCall = !searchClosingBrace && _braceCount === 0 +// if (isFirstCall) { +// searchClosingBrace = currentToken.value +// if (![')', '}', ']'].includes(searchClosingBrace)) { +// throw new Error( +// `expected to be started on a opening brace ( { [, instead found '${searchClosingBrace}'` +// ) +// } +// } + +// const foundOpeningBrace = +// _braceCount === 1 && +// currentToken.value === closingBraceMap[searchClosingBrace] +// const foundAnotherClosingBrace = currentToken.value === searchClosingBrace +// const foundAnotherOpeningBrace = +// currentToken.value === closingBraceMap[searchClosingBrace] + +// if (foundOpeningBrace) { +// return index +// } +// if (foundAnotherClosingBrace) { +// return findOpeningBrace( +// tokens, +// index - 1, +// _braceCount + 1, +// searchClosingBrace +// ) +// } +// if (foundAnotherOpeningBrace) { +// return findOpeningBrace( +// tokens, +// index - 1, +// _braceCount - 1, +// searchClosingBrace +// ) +// } +// // non-brace token, increment and continue +// return findOpeningBrace(tokens, index - 1, _braceCount, searchClosingBrace) +// } + function isCallExpression(tokens: Token[], index: number): number { const currentToken = tokens[index] const veryNextToken = tokens[index + 1] // i.e. no whitespace diff --git a/src/lang/astMathExpressions.test.ts b/src/lang/astMathExpressions.test.ts new file mode 100644 index 000000000..5c3ab5b18 --- /dev/null +++ b/src/lang/astMathExpressions.test.ts @@ -0,0 +1,204 @@ +import { parseExpression, reversePolishNotation } from './astMathExpressions' +import { lexer } from './tokeniser' + +describe('parseExpression', () => { + it('parses a simple expression', () => { + const result = parseExpression(lexer('1 + 2')) + expect(result).toEqual({ + type: 'BinaryExpression', + operator: '+', + start: 0, + end: 5, + left: { type: 'Literal', value: 1, raw: '1', start: 0, end: 1 }, + right: { type: 'Literal', value: 2, raw: '2', start: 4, end: 5 }, + }) + }) + it('parses a more complex expression + followed by *', () => { + const tokens = lexer('1 + 2 * 3') + const result = parseExpression(tokens) + expect(result).toEqual({ + type: 'BinaryExpression', + operator: '+', + start: 0, + end: 9, + left: { type: 'Literal', value: 1, raw: '1', start: 0, end: 1 }, + right: { + type: 'BinaryExpression', + operator: '*', + start: 4, + end: 9, + left: { type: 'Literal', value: 2, raw: '2', start: 4, end: 5 }, + right: { type: 'Literal', value: 3, raw: '3', start: 8, end: 9 }, + }, + }) + }) + it('parses a more complex expression with parentheses: 1 * ( 2 + 3 )', () => { + const result = parseExpression(lexer('1 * ( 2 + 3 )')) + expect(result).toEqual({ + type: 'BinaryExpression', + operator: '*', + start: 0, + end: 13, + left: { type: 'Literal', value: 1, raw: '1', start: 0, end: 1 }, + right: { + type: 'BinaryExpression', + operator: '+', + start: 6, + end: 11, + left: { type: 'Literal', value: 2, raw: '2', start: 6, end: 7 }, + right: { type: 'Literal', value: 3, raw: '3', start: 10, end: 11 }, + }, + }) + }) + it('parses a more complex expression with parentheses with more ', () => { + const result = parseExpression(lexer('1 * ( 2 + 3 ) / 4')) + expect(result).toEqual({ + type: 'BinaryExpression', + operator: '/', + start: 0, + end: 17, + left: { + type: 'BinaryExpression', + operator: '*', + start: 0, + end: 13, + left: { type: 'Literal', value: 1, raw: '1', start: 0, end: 1 }, + right: { + type: 'BinaryExpression', + operator: '+', + start: 6, + end: 11, + left: { type: 'Literal', value: 2, raw: '2', start: 6, end: 7 }, + right: { type: 'Literal', value: 3, raw: '3', start: 10, end: 11 }, + }, + }, + right: { type: 'Literal', value: 4, raw: '4', start: 16, end: 17 }, + }) + }) + it('same as last one but with a 1 + at the start ', () => { + const result = parseExpression(lexer('1 + ( 2 + 3 ) / 4')) + expect(result).toEqual({ + type: 'BinaryExpression', + operator: '+', + start: 0, + end: 17, + left: { type: 'Literal', value: 1, raw: '1', start: 0, end: 1 }, + right: { + type: 'BinaryExpression', + operator: '/', + start: 4, + end: 17, + left: { + type: 'BinaryExpression', + operator: '+', + start: 6, + end: 11, + left: { type: 'Literal', value: 2, raw: '2', start: 6, end: 7 }, + right: { type: 'Literal', value: 3, raw: '3', start: 10, end: 11 }, + }, + right: { type: 'Literal', value: 4, raw: '4', start: 16, end: 17 }, + }, + }) + }) + it('nested braces ', () => { + const result = parseExpression(lexer('1 * (( 2 + 3 ) / 4 + 5 )')) + expect(result).toEqual({ + type: 'BinaryExpression', + operator: '*', + start: 0, + end: 24, + left: { type: 'Literal', value: 1, raw: '1', start: 0, end: 1 }, + right: { + type: 'BinaryExpression', + operator: '+', + start: 5, + end: 22, + left: { + type: 'BinaryExpression', + operator: '/', + start: 5, + end: 18, + left: { + type: 'BinaryExpression', + operator: '+', + start: 7, + end: 12, + left: { type: 'Literal', value: 2, raw: '2', start: 7, end: 8 }, + right: { + type: 'Literal', + value: 3, + raw: '3', + start: 11, + end: 12, + }, + }, + right: { type: 'Literal', value: 4, raw: '4', start: 17, end: 18 }, + }, + right: { type: 'Literal', value: 5, raw: '5', start: 21, end: 22 }, + }, + }) + }) + it('multiple braces around the same thing ', () => { + const result = parseExpression(lexer('1 * ((( 2 + 3 )))')) + expect(result).toEqual({ + type: 'BinaryExpression', + operator: '*', + start: 0, + end: 17, + left: { type: 'Literal', value: 1, raw: '1', start: 0, end: 1 }, + right: { + type: 'BinaryExpression', + operator: '+', + start: 8, + end: 13, + left: { type: 'Literal', value: 2, raw: '2', start: 8, end: 9 }, + right: { type: 'Literal', value: 3, raw: '3', start: 12, end: 13 }, + }, + }) + }) + it('multiple braces around a sing literal', () => { + const code = '2 + (((3)))' + const result = parseExpression(lexer(code)) + expect(result).toEqual({ + type: 'BinaryExpression', + operator: '+', + start: 0, + end: code.indexOf(')))') + 3, + left: { type: 'Literal', value: 2, raw: '2', start: 0, end: 1 }, + right: { type: 'Literal', value: 3, raw: '3', start: 7, end: 8 }, + }) + }) +}) + +describe('reversePolishNotation', () => { + it('converts a simple expression', () => { + const result = reversePolishNotation(lexer('1 + 2')) + expect(result).toEqual([ + { type: 'number', value: '1', start: 0, end: 1 }, + { type: 'number', value: '2', start: 4, end: 5 }, + { type: 'operator', value: '+', start: 2, end: 3 }, + ]) + }) + it('converts a more complex expression', () => { + const result = reversePolishNotation(lexer('1 + 2 * 3')) + expect(result).toEqual([ + { type: 'number', value: '1', start: 0, end: 1 }, + { type: 'number', value: '2', start: 4, end: 5 }, + { type: 'number', value: '3', start: 8, end: 9 }, + { type: 'operator', value: '*', start: 6, end: 7 }, + { type: 'operator', value: '+', start: 2, end: 3 }, + ]) + }) + it('converts a more complex expression with parentheses', () => { + const result = reversePolishNotation(lexer('1 * ( 2 + 3 )')) + expect(result).toEqual([ + { type: 'number', value: '1', start: 0, end: 1 }, + { type: 'brace', value: '(', start: 4, end: 5 }, + { type: 'number', value: '2', start: 6, end: 7 }, + { type: 'number', value: '3', start: 10, end: 11 }, + { type: 'operator', value: '+', start: 8, end: 9 }, + { type: 'brace', value: ')', start: 12, end: 13 }, + { type: 'operator', value: '*', start: 2, end: 3 }, + ]) + }) +}) diff --git a/src/lang/astMathExpressions.ts b/src/lang/astMathExpressions.ts new file mode 100644 index 000000000..922abe07b --- /dev/null +++ b/src/lang/astMathExpressions.ts @@ -0,0 +1,218 @@ +import { BinaryExpression, Literal, Identifier } from './abstractSyntaxTree' +import { Token } from './tokeniser' + +interface Tree { + value: string + left?: Tree + right?: Tree +} + +export function reversePolishNotation( + tokens: Token[], + previousPostfix: Token[] = [], + operators: Token[] = [] +): Token[] { + if (tokens.length === 0) { + return [...previousPostfix, ...operators.slice().reverse()] // reverse mutates, so clone is needed + } + const currentToken = tokens[0] + if ( + currentToken.type === 'number' || + currentToken.type === 'word' || + currentToken.type === 'string' + ) { + return reversePolishNotation( + tokens.slice(1), + [...previousPostfix, currentToken], + operators + ) + } else if (['+', '-', '*', '/', '%'].includes(currentToken.value)) { + if ( + operators.length > 0 && + _precedence(operators[operators.length - 1]) >= _precedence(currentToken) + ) { + return reversePolishNotation( + tokens, + [...previousPostfix, operators[operators.length - 1]], + operators.slice(0, -1) + ) + } + return reversePolishNotation(tokens.slice(1), previousPostfix, [ + ...operators, + currentToken, + ]) + } else if (currentToken.value === '(') { + // push current token to both stacks as it is a legitimate operator + // but later we'll need to pop other operators off the stack until we find the matching ')' + return reversePolishNotation( + tokens.slice(1), + [...previousPostfix, currentToken], + [...operators, currentToken] + ) + } else if (currentToken.value === ')') { + if (operators[operators.length - 1]?.value !== '(') { + // pop operators off the stack and pust them to postFix until we find the matching '(' + return reversePolishNotation( + tokens, + [...previousPostfix, operators[operators.length - 1]], + operators.slice(0, -1) + ) + } + return reversePolishNotation( + tokens.slice(1), + [...previousPostfix, currentToken], + operators.slice(0, -1) + ) + } + if (currentToken.type === 'whitespace') { + return reversePolishNotation(tokens.slice(1), previousPostfix, operators) + } + throw new Error('Unknown token') +} + +interface ParenthesisToken { + type: 'parenthesis' + value: '(' | ')' + start: number + end: number +} + +interface ExtendedBinaryExpression extends BinaryExpression { + startExtended?: number + endExtended?: number +} + +const buildTree = ( + reversePolishNotationTokens: Token[], + stack: ( + | ExtendedBinaryExpression + | Literal + | Identifier + | ParenthesisToken + )[] = [] +): BinaryExpression => { + if (reversePolishNotationTokens.length === 0) { + return stack[0] as BinaryExpression + } + const currentToken = reversePolishNotationTokens[0] + if (currentToken.type === 'number' || currentToken.type === 'string') { + return buildTree(reversePolishNotationTokens.slice(1), [ + ...stack, + { + type: 'Literal', + value: + currentToken.type === 'number' + ? Number(currentToken.value) + : currentToken.value.slice(1, -1), + raw: currentToken.value, + start: currentToken.start, + end: currentToken.end, + }, + ]) + } else if (currentToken.type === 'word') { + return buildTree(reversePolishNotationTokens.slice(1), [ + ...stack, + { + type: 'Identifier', + name: currentToken.value, + start: currentToken.start, + end: currentToken.end, + }, + ]) + } else if (currentToken.type === 'brace' && currentToken.value === '(') { + const paranToken: ParenthesisToken = { + type: 'parenthesis', + value: '(', + start: currentToken.start, + end: currentToken.end, + } + return buildTree(reversePolishNotationTokens.slice(1), [ + ...stack, + paranToken, + ]) + } else if (currentToken.type === 'brace' && currentToken.value === ')') { + const innerNode = stack[stack.length - 1] + + const paran = stack[stack.length - 2] + + const binExp: ExtendedBinaryExpression = { + ...innerNode, + startExtended: paran.start, + endExtended: currentToken.end, + } as ExtendedBinaryExpression + + return buildTree(reversePolishNotationTokens.slice(1), [ + ...stack.slice(0, -2), + binExp, + ]) + } + + const left = { ...stack[stack.length - 2] } + let start = left.start + if (left.type === 'BinaryExpression') { + start = left?.startExtended || left.start + delete left.startExtended + delete left.endExtended + } + + const right = { ...stack[stack.length - 1] } + let end = right.end + if (right.type === 'BinaryExpression') { + end = right?.endExtended || right.end + delete right.startExtended + delete right.endExtended + } + + const binExp: BinaryExpression = { + type: 'BinaryExpression', + operator: currentToken.value, + start, + end, + left: left as any, + right: right as any, + } + return buildTree(reversePolishNotationTokens.slice(1), [ + ...stack.slice(0, -2), + binExp, + ]) +} + +export function parseExpression(tokens: Token[]): BinaryExpression { + const treeWithMabyeBadTopLevelStartEnd = buildTree( + reversePolishNotation(tokens) + ) + const left = treeWithMabyeBadTopLevelStartEnd?.left as any + const start = left?.startExtended || treeWithMabyeBadTopLevelStartEnd?.start + delete left.startExtended + delete left.endExtended + + const right = treeWithMabyeBadTopLevelStartEnd?.right as any + const end = right?.endExtended || treeWithMabyeBadTopLevelStartEnd?.end + delete right.startExtended + delete right.endExtended + + const tree: BinaryExpression = { + ...treeWithMabyeBadTopLevelStartEnd, + start, + end, + left, + right, + } + return tree +} + +function _precedence(operator: Token): number { + return precedence(operator.value) +} + +export function precedence(operator: string): number { + // might be useful for refenecne to make it match + // another commonly used lang https://www.w3schools.com/js/js_precedence.asp + if (['+', '-'].includes(operator)) { + return 11 + } else if (['*', '/', '%'].includes(operator)) { + return 12 + } else { + return 0 + } +} diff --git a/src/lang/executor.test.ts b/src/lang/executor.test.ts index 46c41a36d..7a15c67bc 100644 --- a/src/lang/executor.test.ts +++ b/src/lang/executor.test.ts @@ -296,6 +296,63 @@ show(mySketch) }) }) +describe('testing math operators', () => { + it('it can sum', () => { + const code = ['const myVar = 1 + 2'].join('\n') + const { root } = exe(code) + expect(root.myVar.value).toBe(3) + }) + it('it can subtract', () => { + const code = ['const myVar = 1 - 2'].join('\n') + const { root } = exe(code) + expect(root.myVar.value).toBe(-1) + }) + it('it can multiply', () => { + const code = ['const myVar = 1 * 2'].join('\n') + const { root } = exe(code) + expect(root.myVar.value).toBe(2) + }) + it('it can divide', () => { + const code = ['const myVar = 1 / 2'].join('\n') + const { root } = exe(code) + expect(root.myVar.value).toBe(0.5) + }) + it('it can modulus', () => { + const code = ['const myVar = 5 % 2'].join('\n') + const { root } = exe(code) + expect(root.myVar.value).toBe(1) + }) + it('it can do multiple operations', () => { + const code = ['const myVar = 1 + 2 * 3'].join('\n') + const { root } = exe(code) + expect(root.myVar.value).toBe(7) + }) + it('big example with parans', () => { + const code = ['const myVar = 1 + 2 * (3 - 4) / -5 + 6'].join('\n') + const { root } = exe(code) + expect(root.myVar.value).toBe(7.4) + }) + it('with identifier', () => { + const code = ['const yo = 6', 'const myVar = yo / 2'].join('\n') + const { root } = exe(code) + expect(root.myVar.value).toBe(3) + }) + it('with identifier', () => { + const code = ['const myVar = 2 * ((2 + 3 ) / 4 + 5)'].join('\n') + const { root } = exe(code) + expect(root.myVar.value).toBe(12.5) + }) + // TODO + // it('with callExpression', () => { + // const code = [ + // 'const yo = (a) => a * 2', + // 'const myVar = yo(2) + 2' + // ].join('\n') + // const { root } = exe(code) + // expect(root.myVar.value).toBe(6) + // }) +}) + // helpers function exe( diff --git a/src/lang/executor.ts b/src/lang/executor.ts index d8718dc45..de3bd795b 100644 --- a/src/lang/executor.ts +++ b/src/lang/executor.ts @@ -407,16 +407,22 @@ function getBinaryExpressionResult( expression: BinaryExpression, programMemory: ProgramMemory ) { - const getVal = (part: BinaryPart) => { + const getVal = (part: BinaryPart): any => { if (part.type === 'Literal') { return part.value } else if (part.type === 'Identifier') { return programMemory.root[part.name].value + } else if (part.type === 'BinaryExpression') { + return getBinaryExpressionResult(part, programMemory) } } const left = getVal(expression.left) const right = getVal(expression.right) - return left + right + if (expression.operator === '+') return left + right + if (expression.operator === '-') return left - right + if (expression.operator === '*') return left * right + if (expression.operator === '/') return left / right + if (expression.operator === '%') return left % right } function getPipeExpressionResult( diff --git a/src/lang/recast.test.ts b/src/lang/recast.test.ts index 399978588..9839d3011 100644 --- a/src/lang/recast.test.ts +++ b/src/lang/recast.test.ts @@ -99,6 +99,36 @@ show(mySketch) const recasted = recast(ast) expect(recasted).toBe(code.trim()) }) + it('recast nested binary expression', () => { + const code = ['const myVar = 1 + 2 * 5'].join('\n') + const { ast } = code2ast(code) + const recasted = recast(ast) + expect(recasted).toBe(code.trim()) + }) + it('recast nested binary expression with parans', () => { + const code = ['const myVar = 1 + (1 + 2) * 5'].join('\n') + const { ast } = code2ast(code) + const recasted = recast(ast) + expect(recasted).toBe(code.trim()) + }) + it('unnecessary paran wrap will be remove', () => { + const code = ['const myVar = 1 + (2 * 5)'].join('\n') + const { ast } = code2ast(code) + const recasted = recast(ast) + expect(recasted).toBe(code.replace('(', '').replace(')', '')) + }) + it('complex nested binary expression', () => { + const code = ['1 * ((2 + 3) / 4 + 5)'].join('\n') + const { ast } = code2ast(code) + const recasted = recast(ast) + expect(recasted).toBe(code.trim()) + }) + it('multiplied paren expressions', () => { + const code = ['3 + (1 + 2) * (3 + 4)'].join('\n') + const { ast } = code2ast(code) + const recasted = recast(ast) + expect(recasted).toBe(code.trim()) + }) it('recast array declaration', () => { const code = ['const three = 3', "const yo = [1, '2', three, 4 + 5]"].join( '\n' diff --git a/src/lang/recast.ts b/src/lang/recast.ts index ccf62c543..15bace0c0 100644 --- a/src/lang/recast.ts +++ b/src/lang/recast.ts @@ -11,6 +11,7 @@ import { ObjectExpression, MemberExpression, } from './abstractSyntaxTree' +import { precedence } from './astMathExpressions' export function recast( ast: Program, @@ -54,9 +55,18 @@ export function recast( } function recastBinaryExpression(expression: BinaryExpression): string { - return `${recastBinaryPart(expression.left)} ${ + const maybeWrapIt = (a: string, doit: boolean) => (doit ? `(${a})` : a) + + const shouldWrapRight = + expression.right.type === 'BinaryExpression' && + precedence(expression.operator) > precedence(expression.right.operator) + const shouldWrapLeft = + expression.left.type === 'BinaryExpression' && + precedence(expression.operator) > precedence(expression.left.operator) + + return `${maybeWrapIt(recastBinaryPart(expression.left), shouldWrapLeft)} ${ expression.operator - } ${recastBinaryPart(expression.right)}` + } ${maybeWrapIt(recastBinaryPart(expression.right), shouldWrapRight)}` } function recastArrayExpression( @@ -102,8 +112,11 @@ function recastBinaryPart(part: BinaryPart): string { return recastLiteral(part) } else if (part.type === 'Identifier') { return part.name + } else if (part.type === 'BinaryExpression') { + return recastBinaryExpression(part) } - throw new Error(`Cannot recast BinaryPart ${part}`) + return '' + // throw new Error(`Cannot recast BinaryPart ${part}`) } function recastLiteral(literal: Literal): string {