diff --git a/src/lang/abstractSyntaxTree.test.ts b/src/lang/abstractSyntaxTree.test.ts index a5c3d0284..60ca4e327 100644 --- a/src/lang/abstractSyntaxTree.test.ts +++ b/src/lang/abstractSyntaxTree.test.ts @@ -995,4 +995,77 @@ describe('testing pipe operator special', () => { }, ]) }) + test('array expression', () => { + let code = `const yo = [1, '2', three, 4 + 5]` + const tokens = lexer(code) + const { body } = abstractSyntaxTree(tokens) + expect(body).toEqual([ + { + type: 'VariableDeclaration', + start: 0, + end: 33, + kind: 'const', + declarations: [ + { + type: 'VariableDeclarator', + start: 6, + end: 33, + id: { + type: 'Identifier', + start: 6, + end: 8, + name: 'yo', + }, + init: { + type: 'ArrayExpression', + start: 11, + end: 33, + elements: [ + { + type: 'Literal', + start: 12, + end: 13, + value: 1, + raw: '1', + }, + { + type: 'Literal', + start: 15, + end: 18, + value: '2', + raw: "'2'", + }, + { + type: 'Identifier', + start: 20, + end: 25, + name: 'three', + }, + { + type: 'BinaryExpression', + start: 27, + end: 32, + left: { + type: 'Literal', + start: 27, + end: 28, + value: 4, + raw: '4', + }, + operator: '+', + right: { + type: 'Literal', + start: 31, + end: 32, + value: 5, + raw: '5', + }, + }, + ], + }, + }, + ], + }, + ]) + }) }) diff --git a/src/lang/abstractSyntaxTree.ts b/src/lang/abstractSyntaxTree.ts index 4405c8a08..49b10d224 100644 --- a/src/lang/abstractSyntaxTree.ts +++ b/src/lang/abstractSyntaxTree.ts @@ -270,6 +270,7 @@ export type Value = | SketchExpression | PipeExpression | PipeSubstitution + | ArrayExpression function makeValue( tokens: Token[], @@ -379,6 +380,16 @@ function makeVariableDeclarators( const callExInfo = makeCallExpression(tokens, contentsStartToken.index) init = callExInfo.expression lastIndex = callExInfo.lastIndex + } else if ( + contentsStartToken.token.type === 'brace' && + contentsStartToken.token.value === '[' + ) { + const arrayExpression = makeArrayExpression( + tokens, + contentsStartToken.index + ) + init = arrayExpression.expression + lastIndex = arrayExpression.lastIndex } else { init = makeLiteral(tokens, contentsStartToken.index) } @@ -443,6 +454,66 @@ function makeLiteral(tokens: Token[], index: number): Literal { } } +export interface ArrayExpression extends GeneralStatement { + type: 'ArrayExpression' + elements: Value[] +} + +function makeArrayElements( + tokens: Token[], + index: number, + previousElements: Value[] = [] +): { elements: ArrayExpression['elements']; lastIndex: number } { + // should be called with the first token after the opening brace + const firstElementToken = tokens[index] + if (firstElementToken.type === 'brace' && firstElementToken.value === ']') { + return { + elements: previousElements, + lastIndex: index, + } + } + const currentElement = makeValue(tokens, index) + const nextToken = nextMeaningfulToken(tokens, currentElement.lastIndex) + const isClosingBrace = + nextToken.token.type === 'brace' && nextToken.token.value === ']' + const isComma = nextToken.token.type === 'comma' + if (!isClosingBrace && !isComma) { + throw new Error('Expected a comma or closing brace') + } + const nextCallIndex = isClosingBrace + ? nextToken.index + : nextMeaningfulToken(tokens, nextToken.index).index + return makeArrayElements(tokens, nextCallIndex, [ + ...previousElements, + currentElement.value, + ]) +} + +function makeArrayExpression( + tokens: Token[], + index: number +): { + expression: ArrayExpression + lastIndex: number +} { + // should be called array opening brace '[' index + const openingBraceToken = tokens[index] + const firstElementToken = nextMeaningfulToken(tokens, index) + const { elements, lastIndex } = makeArrayElements( + tokens, + firstElementToken.index + ) + return { + expression: { + type: 'ArrayExpression', + start: openingBraceToken.start, + end: tokens[lastIndex].end, + elements, + }, + lastIndex, + } +} + export interface BinaryExpression extends GeneralStatement { type: 'BinaryExpression' operator: string @@ -554,9 +625,9 @@ function makePipeBody( let value: Value let lastIndex: number if (currentToken.type === 'operator') { - const beep = makeValue(tokens, expressionStart.index) - value = beep.value - lastIndex = beep.lastIndex + const val = makeValue(tokens, expressionStart.index) + value = val.value + lastIndex = val.lastIndex } else if (currentToken.type === 'brace' && currentToken.value === '{') { const sketch = makeSketchExpression(tokens, index) value = sketch.expression diff --git a/src/lang/executor.test.ts b/src/lang/executor.test.ts index f299d3a48..de578397a 100644 --- a/src/lang/executor.test.ts +++ b/src/lang/executor.test.ts @@ -180,6 +180,16 @@ show(mySketch) // sourceRange: [77, 86], // }) }) + it('execute array expression', () => { + const code = ['const three = 3', "const yo = [1, '2', three, 4 + 5]"].join( + '\n' + ) + const { root } = exe(code) + expect(root).toEqual({ + three: 3, + yo: [1, '2', 3, 9], + }) + }) }) // helpers diff --git a/src/lang/executor.ts b/src/lang/executor.ts index d6a919ba7..95851f6a1 100644 --- a/src/lang/executor.ts +++ b/src/lang/executor.ts @@ -43,6 +43,24 @@ export const executor = ( declaration.init, _programMemory ) + } else if (declaration.init.type === 'ArrayExpression') { + _programMemory.root[variableName] = declaration.init.elements.map( + (element) => { + if (element.type === 'Literal') { + return element.value + } else if (element.type === 'BinaryExpression') { + return getBinaryExpressionResult(element, _programMemory) + } else if (element.type === 'PipeExpression') { + return getPipeExpressionResult(element, _programMemory) + } else if (element.type === 'Identifier') { + return _programMemory.root[element.name] + } else { + throw new Error( + `Unexpected element type ${element.type} in array expression` + ) + } + } + ) } else if (declaration.init.type === 'SketchExpression') { const sketchInit = declaration.init const fnMemory: ProgramMemory = { diff --git a/src/lang/recast.test.ts b/src/lang/recast.test.ts index 7c65c2cdf..f6371afeb 100644 --- a/src/lang/recast.test.ts +++ b/src/lang/recast.test.ts @@ -99,6 +99,29 @@ show(mySketch) 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' + ) + const { ast } = code2ast(code) + const recasted = recast(ast) + expect(recasted).toBe(code.trim()) + }) + it('recast long array declaration', () => { + const code = [ + 'const three = 3', + 'const yo = [', + ' 1,', + " '2',", + ' three,', + ' 4 + 5,', + " 'hey oooooo really long long long'", + ']', + ].join('\n') + const { ast } = code2ast(code) + const recasted = recast(ast) + expect(recasted).toBe(code.trim()) + }) }) // helpers diff --git a/src/lang/recast.ts b/src/lang/recast.ts index 8e6b96db7..e4d609f7c 100644 --- a/src/lang/recast.ts +++ b/src/lang/recast.ts @@ -7,6 +7,7 @@ import { Value, FunctionExpression, SketchExpression, + ArrayExpression, } from './abstractSyntaxTree' export function recast( @@ -19,6 +20,8 @@ export function recast( if (statement.type === 'ExpressionStatement') { if (statement.expression.type === 'BinaryExpression') { return indentation + recastBinaryExpression(statement.expression) + } else if (statement.expression.type === 'ArrayExpression') { + return indentation + recastArrayExpression(statement.expression) } else if (statement.expression.type === 'CallExpression') { return indentation + recastCallExpression(statement.expression) } @@ -52,6 +55,25 @@ function recastBinaryExpression(expression: BinaryExpression): string { } ${recastBinaryPart(expression.right)}` } +function recastArrayExpression( + expression: ArrayExpression, + indentation = '' +): string { + const flatRecast = `[${expression.elements + .map((el) => recastValue(el)) + .join(', ')}]` + const maxArrayLength = 40 + if (flatRecast.length > maxArrayLength) { + const _indentation = indentation + ' ' + return `[ +${_indentation}${expression.elements + .map((el) => recastValue(el)) + .join(`,\n${_indentation}`)} +]` + } + return flatRecast +} + function recastBinaryPart(part: BinaryPart): string { if (part.type === 'Literal') { return recastLiteral(part) @@ -82,6 +104,8 @@ function recastArgument(argument: Value): string { return argument.name } else if (argument.type === 'BinaryExpression') { return recastBinaryExpression(argument) + } else if (argument.type === 'ArrayExpression') { + return recastArrayExpression(argument) } else if (argument.type === 'CallExpression') { return recastCallExpression(argument) } else if (argument.type === 'FunctionExpression') { @@ -110,12 +134,16 @@ ${recast(expression.body, '', indentation + ' ')} function recastValue(node: Value, indentation = ''): string { if (node.type === 'BinaryExpression') { return recastBinaryExpression(node) + } else if (node.type === 'ArrayExpression') { + return recastArrayExpression(node, indentation) } else if (node.type === 'Literal') { return recastLiteral(node) } else if (node.type === 'FunctionExpression') { return recastFunction(node) } else if (node.type === 'CallExpression') { return recastCallExpression(node) + } else if (node.type === 'Identifier') { + return node.name } else if (node.type === 'SketchExpression') { return recastSketchExpression(node, indentation) } else if (node.type === 'PipeExpression') { diff --git a/src/lang/tokeniser.test.ts b/src/lang/tokeniser.test.ts index c37d6a3d3..ba1abf234 100644 --- a/src/lang/tokeniser.test.ts +++ b/src/lang/tokeniser.test.ts @@ -332,6 +332,23 @@ describe('testing lexer', () => { "brace ')' from 54 to 55", ]) }) + it('testing array declaration', () => { + const result = stringSummaryLexer(`const yo = [1, 2]`) + expect(result).toEqual([ + "word 'const' from 0 to 5", + "whitespace ' ' from 5 to 6", + "word 'yo' from 6 to 8", + "whitespace ' ' from 8 to 9", + "operator '=' from 9 to 10", + "whitespace ' ' from 10 to 11", + "brace '[' from 11 to 12", + "number '1' from 12 to 13", + "comma ',' from 13 to 14", + "whitespace ' ' from 14 to 15", + "number '2' from 15 to 16", + "brace ']' from 16 to 17", + ]) + }) }) // helpers diff --git a/src/lang/tokeniser.ts b/src/lang/tokeniser.ts index 955eb2c9c..d8faa7a55 100644 --- a/src/lang/tokeniser.ts +++ b/src/lang/tokeniser.ts @@ -12,6 +12,8 @@ const BLOCK_START = /^\{/ const BLOCK_END = /^\}/ const PARAN_START = /^\(/ const PARAN_END = /^\)/ +const ARRAY_START = /^\[/ +const ARRAY_END = /^\]/ const COMMA = /^,/ export const isNumber = (character: string) => NUMBER.test(character) @@ -23,6 +25,8 @@ export const isBlockStart = (character: string) => BLOCK_START.test(character) export const isBlockEnd = (character: string) => BLOCK_END.test(character) export const isParanStart = (character: string) => PARAN_START.test(character) export const isParanEnd = (character: string) => PARAN_END.test(character) +export const isArrayStart = (character: string) => ARRAY_START.test(character) +export const isArrayEnd = (character: string) => ARRAY_END.test(character) export const isComma = (character: string) => COMMA.test(character) function matchFirst(str: string, regex: RegExp) { @@ -75,6 +79,12 @@ const returnTokenAtIndex = (str: string, startIndex: number): Token | null => { if (isBlockEnd(strFromIndex)) { return makeToken('brace', matchFirst(strFromIndex, BLOCK_END), startIndex) } + if (isArrayStart(strFromIndex)) { + return makeToken('brace', matchFirst(strFromIndex, ARRAY_START), startIndex) + } + if (isArrayEnd(strFromIndex)) { + return makeToken('brace', matchFirst(strFromIndex, ARRAY_END), startIndex) + } if (isComma(strFromIndex)) { return makeToken('comma', matchFirst(strFromIndex, COMMA), startIndex) }