From 56c80905beeeab6da0db0a355dc4729512ae77d0 Mon Sep 17 00:00:00 2001 From: Pavlo Date: Thu, 10 Nov 2022 22:55:42 +0000 Subject: [PATCH 1/4] #12 created new chainingCallsNode and initial setup --- index.html | 8 +------- src/common/ast-types.ts | 8 ++++++++ src/common/token-types.ts | 42 +++++++++++++++++++++++++++++++++++---- src/parser/parser.ts | 17 +++++++++++++++- 4 files changed, 63 insertions(+), 12 deletions(-) diff --git a/index.html b/index.html index 86bbb49..f678355 100644 --- a/index.html +++ b/index.html @@ -29,13 +29,7 @@

JSPython development console

-m = '' -try: - raise 'My Message' - x.push(1) -except: - m = error.message -m +"1,2,3".split(',')[0]
diff --git a/src/common/ast-types.ts b/src/common/ast-types.ts index a26edac..bf98fcc 100644 --- a/src/common/ast-types.ts +++ b/src/common/ast-types.ts @@ -9,6 +9,7 @@ export type AstNodeType = | 'getSingleVar' | 'setSingleVar' | 'dotObjectAccess' + | 'chainingCalls' | 'bracketObjectAccess' | 'funcCall' | 'funcDef' @@ -218,6 +219,13 @@ export class DotObjectAccessNode extends AstNode { } } +export class ChainingCallsNode extends AstNode { + constructor(public innerNodes: AstNode[], public loc: Uint16Array) { + super('chainingCalls'); + this.loc = loc; + } +} + export class CreateObjectNode extends AstNode { constructor(public props: ObjectPropertyInfo[], public loc: Uint16Array) { super('createObject'); diff --git a/src/common/token-types.ts b/src/common/token-types.ts index c61fe49..1d4edbb 100644 --- a/src/common/token-types.ts +++ b/src/common/token-types.ts @@ -65,19 +65,19 @@ export function getEndColumn(token: Token): number { return token[1][4]; } -export function splitTokens(tokens: Token[], separator: string): Token[][] { +export function splitTokensByIndexes(tokens: Token[], sepIndexes: number[]): Token[][] { const result: Token[][] = []; if (!tokens.length) { return []; } - const sepIndexes = findTokenValueIndexes(tokens, value => value === separator); - let start = 0; for (let i = 0; i < sepIndexes.length; i++) { const ind = sepIndexes[i]; - result.push(tokens.slice(start, ind)); + if (start !== ind) { + result.push(tokens.slice(start, ind)); + } start = ind + 1; } @@ -85,6 +85,14 @@ export function splitTokens(tokens: Token[], separator: string): Token[][] { return result; } +export function splitTokens(tokens: Token[], separator: string): Token[][] { + if (!tokens.length) { + return []; + } + const sepIndexes = findTokenValueIndexes(tokens, value => value === separator); + return splitTokensByIndexes(tokens, sepIndexes); +} + export function findTokenValueIndex( tokens: Token[], predicate: (value: TokenValue) => boolean, @@ -109,6 +117,32 @@ export function findTokenValueIndex( return -1; } +export function findChainingCallTokensIndexes(tokens: Token[]): number[] { + const opIndexes: number[] = []; + + for (let i = 0; i < tokens.length; i++) { + const tValue = getTokenValue(tokens[i]); + const tType = getTokenType(tokens[i]); + + if (tType === TokenTypes.LiteralString) { + continue; + } + + if (tValue === '.') { + opIndexes.push(i); + } else if (tValue === '(') { + i = skipInnerBrackets(tokens, i, '(', ')'); + } else if (tValue === '[') { + opIndexes.push(i); + i = skipInnerBrackets(tokens, i, '[', ']'); + } else if (tValue === '{') { + i = skipInnerBrackets(tokens, i, '{', '}'); + } + } + + return opIndexes; +} + export function findTokenValueIndexes( tokens: Token[], predicate: (value: TokenValue) => boolean diff --git a/src/parser/parser.ts b/src/parser/parser.ts index 6a61f2c..7cb7685 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -44,7 +44,10 @@ import { ComparisonOperators, TryExceptNode, ExceptBody, - RaiseNode + RaiseNode, + findChainingCallTokensIndexes, + splitTokensByIndexes, + ChainingCallsNode } from '../common'; import { JspyParserError } from '../common/utils'; @@ -604,6 +607,18 @@ export class Parser { return prevNode; } + // create chaining calls + const inds = findChainingCallTokensIndexes(tokens); + + if (inds.length > 0) { + const chainingGroup = splitTokensByIndexes(tokens, inds); + const chainingCallsNode = new ChainingCallsNode([], getTokenLoc(tokens[0])); + + console.log('inds ==>', inds, chainingGroup); + + return chainingCallsNode; + } + // create DotObjectAccessNode const subObjects = splitTokens(tokens, '.'); if (subObjects.length > 1) { From 6400013869c06af611bcb9ea80169a74932995be Mon Sep 17 00:00:00 2001 From: Pavlo Date: Thu, 10 Nov 2022 22:56:15 +0000 Subject: [PATCH 2/4] fixed parser error --- src/parser/parser.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/parser/parser.ts b/src/parser/parser.ts index 7cb7685..dad05c3 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -611,12 +611,12 @@ export class Parser { const inds = findChainingCallTokensIndexes(tokens); if (inds.length > 0) { - const chainingGroup = splitTokensByIndexes(tokens, inds); - const chainingCallsNode = new ChainingCallsNode([], getTokenLoc(tokens[0])); + //const chainingGroup = splitTokensByIndexes(tokens, inds); + //const chainingCallsNode = new ChainingCallsNode([], getTokenLoc(tokens[0])); - console.log('inds ==>', inds, chainingGroup); + // console.log('inds ==>', inds, chainingGroup); - return chainingCallsNode; + // return chainingCallsNode; } // create DotObjectAccessNode From 70f17b1e575b96bac69625f1a7abf97402a3c834 Mon Sep 17 00:00:00 2001 From: Pavlo Date: Tue, 15 Nov 2022 08:56:18 +0000 Subject: [PATCH 3/4] added parser logic to create chaining nodes --- package.json | 4 +- src/common/ast-types.ts | 17 ++++++- src/common/token-types.ts | 10 ++-- src/parser/parser.spec.ts | 103 +++++++++++++++++++++++++++++++++++++- src/parser/parser.ts | 67 +++++++++++++++++-------- 5 files changed, 172 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index f281ed8..6f40d7c 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ }, "husky": { "hooks": { - "pre-commit": "npm run lint && npm run test" + "pre-commit": "npm run lint && npm run test -- parser.spec.ts" } -} + } } diff --git a/src/common/ast-types.ts b/src/common/ast-types.ts index bf98fcc..fc2982f 100644 --- a/src/common/ast-types.ts +++ b/src/common/ast-types.ts @@ -10,6 +10,7 @@ export type AstNodeType = | 'setSingleVar' | 'dotObjectAccess' | 'chainingCalls' + | 'chainingObjectAccess' | 'bracketObjectAccess' | 'funcCall' | 'funcDef' @@ -212,6 +213,7 @@ export class GetSingleVarNode extends AstNode implements IsNullCoelsing { } } +// should be deprecated export class DotObjectAccessNode extends AstNode { constructor(public nestedProps: AstNode[], public loc: Uint16Array) { super('dotObjectAccess'); @@ -240,11 +242,12 @@ export class CreateArrayNode extends AstNode { } } +// should be deprecated export class BracketObjectAccessNode extends AstNode { constructor( public propertyName: string, public bracketBody: AstNode, - public nullCoalescing: boolean | undefined = undefined, + public nullCoelsing: boolean | undefined = undefined, public loc: Uint16Array ) { super('bracketObjectAccess'); @@ -252,6 +255,18 @@ export class BracketObjectAccessNode extends AstNode { } } +export class ChainingObjectAccessNode extends AstNode { + constructor( + public indexerBody: AstNode, + public nullCoelsing: boolean | undefined = undefined, + public loc: Uint16Array + ) { + super('chainingObjectAccess'); + this.loc = loc; + } +} + + export interface LogicalNodeItem { node: AstNode; op: LogicalOperators | undefined; diff --git a/src/common/token-types.ts b/src/common/token-types.ts index 1d4edbb..f57107d 100644 --- a/src/common/token-types.ts +++ b/src/common/token-types.ts @@ -75,12 +75,16 @@ export function splitTokensByIndexes(tokens: Token[], sepIndexes: number[]): Tok let start = 0; for (let i = 0; i < sepIndexes.length; i++) { const ind = sepIndexes[i]; - if (start !== ind) { - result.push(tokens.slice(start, ind)); + if (getTokenValue(tokens[start - 1]) === '[') { + start = start - 1; } + result.push(tokens.slice(start, ind)); start = ind + 1; } + if (getTokenValue(tokens[start - 1]) === '[') { + start = start - 1; + } result.push(tokens.slice(start, tokens.length)); return result; } @@ -132,7 +136,7 @@ export function findChainingCallTokensIndexes(tokens: Token[]): number[] { opIndexes.push(i); } else if (tValue === '(') { i = skipInnerBrackets(tokens, i, '(', ')'); - } else if (tValue === '[') { + } else if (tValue === '[' && i !== 0) { opIndexes.push(i); i = skipInnerBrackets(tokens, i, '[', ']'); } else if (tValue === '{') { diff --git a/src/parser/parser.spec.ts b/src/parser/parser.spec.ts index ff1d102..d8b55d5 100644 --- a/src/parser/parser.spec.ts +++ b/src/parser/parser.spec.ts @@ -1,4 +1,4 @@ -import { BinOpNode, ConstNode, ImportNode } from '../common'; +import { BinOpNode, ChainingCallsNode, ChainingObjectAccessNode, ConstNode, ImportNode } from '../common'; import { Tokenizer } from '../tokenizer'; import { Parser } from './parser'; @@ -58,4 +58,105 @@ describe('Parser => ', () => { expect(importNode.parts[1].alias).toBe('f'); } }); + + it('chaining calls 1 ', async () => { + const script = `"1,2,3".split(',')[0]`; + const ast = new Parser().parse(new Tokenizer().tokenize(script)); + expect(ast.body.length).toBe(1); + expect(ast.body[0].type).toBe("chainingCalls"); + const innerNodes = (ast.body[0] as ChainingCallsNode).innerNodes; + expect(innerNodes.length).toBe(3); + expect(innerNodes[2].type).toBe("chainingObjectAccess"); + + }); + + it('chaining calls 2 starts with JSON array', async () => { + const script = `["1,2,3"][0].split(',')[0]`; + const ast = new Parser().parse(new Tokenizer().tokenize(script)); + expect(ast.body[0].type).toBe("chainingCalls"); + expect((ast.body[0] as ChainingCallsNode).innerNodes.length).toBe(4); + expect((ast.body[0] as ChainingCallsNode).innerNodes[0].type).toBe("createArray"); + expect((ast.body[0] as ChainingCallsNode).innerNodes[1].type).toBe("chainingObjectAccess"); + expect((ast.body[0] as ChainingCallsNode).innerNodes[3].type).toBe("chainingObjectAccess"); + }); + + it('chaining calls 3 start with JSON Object', async () => { + const script = `{value: "1,2,3"}["value"].split(',')[0]`; + const ast = new Parser().parse(new Tokenizer().tokenize(script)); + expect(ast.body[0].type).toBe("chainingCalls"); + expect((ast.body[0] as ChainingCallsNode).innerNodes.length).toBe(4); + expect((ast.body[0] as ChainingCallsNode).innerNodes[0].type).toBe("createObject"); + expect((ast.body[0] as ChainingCallsNode).innerNodes[1].type).toBe("chainingObjectAccess"); + expect((ast.body[0] as ChainingCallsNode).innerNodes[3].type).toBe("chainingObjectAccess"); + }); + + it('chaining calls 1 with ? ', async () => { + const script = `"1,2,3".split(',')?[0]`; + const ast = new Parser().parse(new Tokenizer().tokenize(script)); + expect(ast.body.length).toBe(1); + const innerNodes = (ast.body[0] as ChainingCallsNode).innerNodes; + + expect(ast.body[0].type).toBe("chainingCalls"); + expect(innerNodes.length).toBe(3); + expect(innerNodes[2].type).toBe("chainingObjectAccess"); + + expect(!!(innerNodes[0] as ChainingObjectAccessNode).nullCoelsing).toBe(false); + expect((innerNodes[1] as ChainingObjectAccessNode).nullCoelsing).toBe(true); + expect(!!(innerNodes[2] as ChainingObjectAccessNode).nullCoelsing).toBe(false); + }); + + it('chaining calls 2 with ? starts with JSON array', async () => { + const script = `["1,2,3"][0]?.split(',')?[0]`; + const ast = new Parser().parse(new Tokenizer().tokenize(script)); + expect(ast.body[0].type).toBe("chainingCalls"); + const innerNodes = (ast.body[0] as ChainingCallsNode).innerNodes; + expect(innerNodes.length).toBe(4); + expect(innerNodes[0].type).toBe("createArray"); + expect(innerNodes[1].type).toBe("chainingObjectAccess"); + expect(innerNodes[1].type).toBe("chainingObjectAccess"); + expect(innerNodes[3].type).toBe("chainingObjectAccess"); + + expect(!!(innerNodes[0] as ChainingObjectAccessNode).nullCoelsing).toBe(false); + expect((innerNodes[1] as ChainingObjectAccessNode).nullCoelsing).toBe(true); + expect((innerNodes[2] as ChainingObjectAccessNode).nullCoelsing).toBe(true); + expect(!!(innerNodes[3] as ChainingObjectAccessNode).nullCoelsing).toBe(false); + }); + + it('chaining calls 3 with ? start with JSON Object', async () => { + const script = `{value: "1,2,3"}["value"]?.split(',')?[0]`; + const ast = new Parser().parse(new Tokenizer().tokenize(script)); + expect(ast.body[0].type).toBe("chainingCalls"); + const innerNodes = (ast.body[0] as ChainingCallsNode).innerNodes; + expect(innerNodes.length).toBe(4); + expect(innerNodes[0].type).toBe("createObject"); + expect(innerNodes[1].type).toBe("chainingObjectAccess"); + expect(innerNodes[3].type).toBe("chainingObjectAccess"); + + expect(!!(innerNodes[0] as ChainingObjectAccessNode).nullCoelsing).toBe(false); + expect((innerNodes[1] as ChainingObjectAccessNode).nullCoelsing).toBe(true); + expect((innerNodes[2] as ChainingObjectAccessNode).nullCoelsing).toBe(true); + expect(!!(innerNodes[3] as ChainingObjectAccessNode).nullCoelsing).toBe(false); + }); + + it('chaining calls 4 with ? 2d array access and ?', async () => { + const script = `["1,2,3"][0]?[0]?[0]?.split(',')?[0]`; + const ast = new Parser().parse(new Tokenizer().tokenize(script)); + expect(ast.body[0].type).toBe("chainingCalls"); + const innerNodes = (ast.body[0] as ChainingCallsNode).innerNodes; + expect(innerNodes.length).toBe(6); + expect(innerNodes[0].type).toBe("createArray"); + expect(innerNodes[1].type).toBe("chainingObjectAccess"); + expect(innerNodes[2].type).toBe("chainingObjectAccess"); + expect(innerNodes[3].type).toBe("chainingObjectAccess"); + expect(innerNodes[4].type).toBe("funcCall"); + expect(innerNodes[5].type).toBe("chainingObjectAccess"); + + expect(!!(innerNodes[0] as ChainingObjectAccessNode).nullCoelsing).toBe(false); + expect((innerNodes[1] as ChainingObjectAccessNode).nullCoelsing).toBe(true); + expect((innerNodes[2] as ChainingObjectAccessNode).nullCoelsing).toBe(true); + expect(!!(innerNodes[3] as ChainingObjectAccessNode).nullCoelsing).toBe(true); + expect(!!(innerNodes[4] as ChainingObjectAccessNode).nullCoelsing).toBe(true); + expect(!!(innerNodes[5] as ChainingObjectAccessNode).nullCoelsing).toBe(false); + }); + }); diff --git a/src/parser/parser.ts b/src/parser/parser.ts index dad05c3..31569e8 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -47,7 +47,8 @@ import { RaiseNode, findChainingCallTokensIndexes, splitTokensByIndexes, - ChainingCallsNode + ChainingCallsNode, + ChainingObjectAccessNode } from '../common'; import { JspyParserError } from '../common/utils'; @@ -607,27 +608,6 @@ export class Parser { return prevNode; } - // create chaining calls - const inds = findChainingCallTokensIndexes(tokens); - - if (inds.length > 0) { - //const chainingGroup = splitTokensByIndexes(tokens, inds); - //const chainingCallsNode = new ChainingCallsNode([], getTokenLoc(tokens[0])); - - // console.log('inds ==>', inds, chainingGroup); - - // return chainingCallsNode; - } - - // create DotObjectAccessNode - const subObjects = splitTokens(tokens, '.'); - if (subObjects.length > 1) { - return new DotObjectAccessNode( - subObjects.map(tkns => this.createExpressionNode(tkns)), - getTokenLoc(tokens[0]) - ); - } - // create function call node if (tokens.length > 2 && getTokenValue(tokens[1]) === '(') { const isNullCoelsing = getTokenValue(tokens[tokens.length - 1]) === '?'; @@ -644,6 +624,40 @@ export class Parser { return node; } + // create chaining calls + const inds = findChainingCallTokensIndexes(tokens); + + if (inds.length > 0) { + const chainingGroup = splitTokensByIndexes(tokens, inds); + const innerNodes: AstNode[] = []; + + for (let i = 0; i < chainingGroup.length; i++) { + const chainLinkTokenks = chainingGroup[i]; + + if (i !== 0 && getTokenValue(chainLinkTokenks[0]) === '[') { + const nullCoelsing = getTokenValue(chainLinkTokenks[chainLinkTokenks.length - 1]) === '?'; + if (nullCoelsing) { + chainLinkTokenks.pop(); + } + const paramsTokensSlice = chainLinkTokenks.slice(1, chainLinkTokenks.length - 1); + const paramsNodes = this.createExpressionNode(paramsTokensSlice); + + innerNodes.push( + new ChainingObjectAccessNode( + paramsNodes, + nullCoelsing, + getTokenLoc(chainLinkTokenks[0]) + ) + ); + continue; + } + + innerNodes.push(this.createExpressionNode(chainLinkTokenks)); + } + + return new ChainingCallsNode(innerNodes, getTokenLoc(tokens[0])); + } + // create Object Node if (getTokenValue(tokens[0]) === '{' && getTokenValue(tokens[tokens.length - 1]) === '}') { const keyValueTokens = splitTokens(tokens.splice(1, tokens.length - 2), ','); @@ -697,6 +711,15 @@ export class Parser { return new CreateArrayNode(items, getTokenLoc(tokens[0])); } + // create DotObjectAccessNode + const subObjects = splitTokens(tokens, '.'); + if (subObjects.length > 1) { + return new DotObjectAccessNode( + subObjects.map(tkns => this.createExpressionNode(tkns)), + getTokenLoc(tokens[0]) + ); + } + // bracket access object node if (tokens.length > 2 && getTokenValue(tokens[1]) === '[') { const name = getTokenValue(tokens[0]) as string; From 35dffca6eb9e9e5df2199040d729b348b52429b1 Mon Sep 17 00:00:00 2001 From: Pavlo Date: Wed, 16 Nov 2022 22:04:07 +0000 Subject: [PATCH 4/4] added chaining calls evaluations --- package.json | 2 +- src/common/ast-types.ts | 23 ----- src/common/token-types.ts | 2 + src/evaluator/evaluator.ts | 164 +++++++++++++++---------------- src/evaluator/evaluatorAsync.ts | 165 +++++++++++++++----------------- src/interpreter.spec.ts | 100 +++++++++++++++++-- src/parser/parser.ts | 52 ++++------ 7 files changed, 271 insertions(+), 237 deletions(-) diff --git a/package.json b/package.json index 6f40d7c..b491c98 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ }, "husky": { "hooks": { - "pre-commit": "npm run lint && npm run test -- parser.spec.ts" + "pre-commit": "npm run lint && npm run test" } } } diff --git a/src/common/ast-types.ts b/src/common/ast-types.ts index fc2982f..f4b7185 100644 --- a/src/common/ast-types.ts +++ b/src/common/ast-types.ts @@ -8,10 +8,8 @@ export type AstNodeType = | 'logicalOp' | 'getSingleVar' | 'setSingleVar' - | 'dotObjectAccess' | 'chainingCalls' | 'chainingObjectAccess' - | 'bracketObjectAccess' | 'funcCall' | 'funcDef' | 'arrowFuncDef' @@ -213,14 +211,6 @@ export class GetSingleVarNode extends AstNode implements IsNullCoelsing { } } -// should be deprecated -export class DotObjectAccessNode extends AstNode { - constructor(public nestedProps: AstNode[], public loc: Uint16Array) { - super('dotObjectAccess'); - this.loc = loc; - } -} - export class ChainingCallsNode extends AstNode { constructor(public innerNodes: AstNode[], public loc: Uint16Array) { super('chainingCalls'); @@ -242,19 +232,6 @@ export class CreateArrayNode extends AstNode { } } -// should be deprecated -export class BracketObjectAccessNode extends AstNode { - constructor( - public propertyName: string, - public bracketBody: AstNode, - public nullCoelsing: boolean | undefined = undefined, - public loc: Uint16Array - ) { - super('bracketObjectAccess'); - this.loc = loc; - } -} - export class ChainingObjectAccessNode extends AstNode { constructor( public indexerBody: AstNode, diff --git a/src/common/token-types.ts b/src/common/token-types.ts index f57107d..5b632fa 100644 --- a/src/common/token-types.ts +++ b/src/common/token-types.ts @@ -136,6 +136,8 @@ export function findChainingCallTokensIndexes(tokens: Token[]): number[] { opIndexes.push(i); } else if (tValue === '(') { i = skipInnerBrackets(tokens, i, '(', ')'); + } else if (tValue === '[' && i === 0) { + i = skipInnerBrackets(tokens, i, '[', ']'); } else if (tValue === '[' && i !== 0) { opIndexes.push(i); i = skipInnerBrackets(tokens, i, '[', ']'); diff --git a/src/evaluator/evaluator.ts b/src/evaluator/evaluator.ts index eb77f2f..214577c 100644 --- a/src/evaluator/evaluator.ts +++ b/src/evaluator/evaluator.ts @@ -4,11 +4,11 @@ import { AstBlock, AstNode, BinOpNode, - BracketObjectAccessNode, + ChainingCallsNode, + ChainingObjectAccessNode, ConstNode, CreateArrayNode, CreateObjectNode, - DotObjectAccessNode, ForNode, FuncDefNode, FunctionCallNode, @@ -369,104 +369,48 @@ export class Evaluator { if (assignNode.target.type === 'getSingleVar') { const node = assignNode.target as SetSingleVarNode; - blockContext.blockScope.set(node.name, this.evalNode(assignNode.source, blockContext)); - } else if (assignNode.target.type === 'dotObjectAccess') { - const targetNode = assignNode.target as DotObjectAccessNode; + blockContext.blockScope.set( + node.name, + this.evalNode(assignNode.source, blockContext) + ); + } else if (assignNode.target.type === 'chainingCalls') { + const targetNode = assignNode.target as ChainingCallsNode; // create a node for all but last property token // potentially it can go to parser - const targetObjectNode = new DotObjectAccessNode( - targetNode.nestedProps.slice(0, targetNode.nestedProps.length - 1), + const targetObjectNode = new ChainingCallsNode( + targetNode.innerNodes.slice(0, targetNode.innerNodes.length - 1), targetNode.loc ); - const targetObject = this.evalNode(targetObjectNode, blockContext) as Record< + const targetObject = (this.evalNode(targetObjectNode, blockContext)) as Record< string, unknown >; - // not sure nested properties should be GetSingleVarNode - // can be factored in the parser - const lastPropertyName = ( - targetNode.nestedProps[targetNode.nestedProps.length - 1] as GetSingleVarNode - ).name; + const lastInnerNode = targetNode.innerNodes[targetNode.innerNodes.length - 1]; + + let lastPropertyName = ''; + if (lastInnerNode.type === 'getSingleVar') { + lastPropertyName = (lastInnerNode as GetSingleVarNode).name; + } else if (lastInnerNode.type === 'chainingObjectAccess') { + lastPropertyName = (this.evalNode( + (lastInnerNode as ChainingObjectAccessNode).indexerBody, + blockContext + )) as string; + } else { + throw Error('Not implemented Assign operation with chaining calls'); + } targetObject[lastPropertyName] = this.evalNode(assignNode.source, blockContext); - } else if (assignNode.target.type === 'bracketObjectAccess') { - const targetNode = assignNode.target as BracketObjectAccessNode; - const keyValue = this.evalNode(targetNode.bracketBody, blockContext) as string | number; - const targetObject = blockContext.blockScope.get( - targetNode.propertyName as string - ) as Record; - - targetObject[keyValue] = this.evalNode(assignNode.source, blockContext); - } else { - throw Error('Not implemented Assign operation'); - // get chaining calls } return null; } - if (node.type === 'bracketObjectAccess') { - const sbNode = node as BracketObjectAccessNode; - const key = this.evalNode(sbNode.bracketBody, blockContext) as string; - const obj = blockContext.blockScope.get(sbNode.propertyName as string) as Record< - string, - unknown - >; - return obj[key] === undefined ? null : obj[key]; + if (node.type === 'chainingCalls') { + return this.resolveChainingCallsNode(node as ChainingCallsNode, blockContext); } - if (node.type === 'dotObjectAccess') { - const dotObject = node as DotObjectAccessNode; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let startObject = this.evalNode(dotObject.nestedProps[0], blockContext) as any; - for (let i = 1; i < dotObject.nestedProps.length; i++) { - const nestedProp = dotObject.nestedProps[i]; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if ((dotObject.nestedProps[i - 1] as any).nullCoelsing && !startObject) { - startObject = {}; - } - - if (nestedProp.type === 'getSingleVar') { - startObject = startObject[(nestedProp as SetSingleVarNode).name] as unknown; - } else if (nestedProp.type === 'bracketObjectAccess') { - const node = nestedProp as BracketObjectAccessNode; - startObject = startObject[node.propertyName] as unknown; - startObject = startObject[ - this.evalNode(node.bracketBody, blockContext) as string - ] as unknown; - } else if (nestedProp.type === 'funcCall') { - const funcCallNode = nestedProp as FunctionCallNode; - const func = startObject[funcCallNode.name] as (...args: unknown[]) => unknown; - - if ( - (func === undefined || func === null) && - (dotObject.nestedProps[i - 1] as unknown as IsNullCoelsing).nullCoelsing - ) { - startObject = null; - continue; - } - - if (typeof func !== 'function') { - throw Error(`'${funcCallNode.name}' is not a function or not defined.`); - } - const pms = funcCallNode.paramNodes?.map(n => this.evalNode(n, blockContext)) || []; - startObject = this.invokeFunction(func.bind(startObject), pms, { - moduleName: blockContext.moduleName, - line: funcCallNode.loc[0], - column: funcCallNode.loc[1] - }); - } else { - throw Error("Can't resolve dotObjectAccess node"); - } - } - - // no undefined values, make it rather null - return startObject === undefined ? null : startObject; - } if (node.type === 'createObject') { const createObjectNode = node as CreateObjectNode; @@ -490,4 +434,60 @@ export class Evaluator { return res; } } + + private resolveChainingCallsNode( + chNode: ChainingCallsNode, + blockContext: BlockContext + ): unknown { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let startObject = (this.evalNode(chNode.innerNodes[0], blockContext)) as any; + + for (let i = 1; i < chNode.innerNodes.length; i++) { + const nestedProp = chNode.innerNodes[i]; + + if ((chNode.innerNodes[i - 1] as unknown as IsNullCoelsing).nullCoelsing && !startObject) { + startObject = {}; + } + + if (nestedProp.type === 'getSingleVar') { + startObject = startObject[(nestedProp as SetSingleVarNode).name] as unknown; + } else if (nestedProp.type === 'chainingObjectAccess') { + const node = nestedProp as ChainingObjectAccessNode; + // startObject = startObject[node.] as unknown; + startObject = startObject[ + (this.evalNode(node.indexerBody, blockContext)) as string + ] as unknown; + } else if (nestedProp.type === 'funcCall') { + const funcCallNode = nestedProp as FunctionCallNode; + const func = startObject[funcCallNode.name] as (...args: unknown[]) => unknown; + + if ( + (func === undefined || func === null) && + (chNode.innerNodes[i - 1] as unknown as IsNullCoelsing).nullCoelsing + ) { + startObject = null; + continue; + } + + if (typeof func !== 'function') { + throw Error(`'${funcCallNode.name}' is not a function or not defined.`); + } + const pms = []; + for (const p of funcCallNode.paramNodes || []) { + pms.push(this.evalNode(p, blockContext)); + } + + startObject = this.invokeFunction(func.bind(startObject), pms, { + moduleName: blockContext.moduleName, + line: funcCallNode.loc[0], + column: funcCallNode.loc[0] + }); + } else { + throw Error("Can't resolve chainingCalls node"); + } + } + + return startObject === undefined ? null : startObject; + } + } diff --git a/src/evaluator/evaluatorAsync.ts b/src/evaluator/evaluatorAsync.ts index 9e9fbf5..01fa6ab 100644 --- a/src/evaluator/evaluatorAsync.ts +++ b/src/evaluator/evaluatorAsync.ts @@ -4,11 +4,11 @@ import { AstBlock, AstNode, BinOpNode, - BracketObjectAccessNode, + ChainingCallsNode, + ChainingObjectAccessNode, ConstNode, CreateArrayNode, CreateObjectNode, - DotObjectAccessNode, ForNode, FuncDefNode, FunctionCallNode, @@ -480,13 +480,13 @@ export class EvaluatorAsync { node.name, await this.evalNodeAsync(assignNode.source, blockContext) ); - } else if (assignNode.target.type === 'dotObjectAccess') { - const targetNode = assignNode.target as DotObjectAccessNode; + } else if (assignNode.target.type === 'chainingCalls') { + const targetNode = assignNode.target as ChainingCallsNode; // create a node for all but last property token // potentially it can go to parser - const targetObjectNode = new DotObjectAccessNode( - targetNode.nestedProps.slice(0, targetNode.nestedProps.length - 1), + const targetObjectNode = new ChainingCallsNode( + targetNode.innerNodes.slice(0, targetNode.innerNodes.length - 1), targetNode.loc ); const targetObject = (await this.evalNodeAsync(targetObjectNode, blockContext)) as Record< @@ -494,96 +494,28 @@ export class EvaluatorAsync { unknown >; - // not sure nested properties should be GetSingleVarNode - // can be factored in the parser - const lastPropertyName = ( - targetNode.nestedProps[targetNode.nestedProps.length - 1] as GetSingleVarNode - ).name; + const lastInnerNode = targetNode.innerNodes[targetNode.innerNodes.length - 1]; + + let lastPropertyName = ''; + if (lastInnerNode.type === 'getSingleVar') { + lastPropertyName = (lastInnerNode as GetSingleVarNode).name; + } else if (lastInnerNode.type === 'chainingObjectAccess') { + lastPropertyName = (await this.evalNodeAsync( + (lastInnerNode as ChainingObjectAccessNode).indexerBody, + blockContext + )) as string; + } else { + throw Error('Not implemented Assign operation with chaining calls'); + } targetObject[lastPropertyName] = await this.evalNodeAsync(assignNode.source, blockContext); - } else if (assignNode.target.type === 'bracketObjectAccess') { - const targetNode = assignNode.target as BracketObjectAccessNode; - const keyValue = (await this.evalNodeAsync(targetNode.bracketBody, blockContext)) as - | string - | number; - const targetObject = blockContext.blockScope.get( - targetNode.propertyName as string - ) as Record; - - targetObject[keyValue] = await this.evalNodeAsync(assignNode.source, blockContext); - } else { - throw Error('Not implemented Assign operation'); - // get chaining calls } return null; } - if (node.type === 'bracketObjectAccess') { - const sbNode = node as BracketObjectAccessNode; - const key = (await this.evalNodeAsync(sbNode.bracketBody, blockContext)) as string; - const obj = blockContext.blockScope.get(sbNode.propertyName as string) as Record< - string, - unknown - >; - return obj[key] === undefined ? null : obj[key]; - } - - if (node.type === 'dotObjectAccess') { - const dotObject = node as DotObjectAccessNode; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let startObject = (await this.evalNodeAsync(dotObject.nestedProps[0], blockContext)) as any; - for (let i = 1; i < dotObject.nestedProps.length; i++) { - const nestedProp = dotObject.nestedProps[i]; - - if ( - (dotObject.nestedProps[i - 1] as unknown as IsNullCoelsing).nullCoelsing && - !startObject - ) { - startObject = {}; - } - - if (nestedProp.type === 'getSingleVar') { - startObject = startObject[(nestedProp as SetSingleVarNode).name] as unknown; - } else if (nestedProp.type === 'bracketObjectAccess') { - const node = nestedProp as BracketObjectAccessNode; - startObject = startObject[node.propertyName] as unknown; - startObject = startObject[ - (await this.evalNodeAsync(node.bracketBody, blockContext)) as string - ] as unknown; - } else if (nestedProp.type === 'funcCall') { - const funcCallNode = nestedProp as FunctionCallNode; - const func = startObject[funcCallNode.name] as (...args: unknown[]) => unknown; - - if ( - (func === undefined || func === null) && - (dotObject.nestedProps[i - 1] as unknown as IsNullCoelsing).nullCoelsing - ) { - startObject = null; - continue; - } - - if (typeof func !== 'function') { - throw Error(`'${funcCallNode.name}' is not a function or not defined.`); - } - const pms = []; - for (const p of funcCallNode.paramNodes || []) { - pms.push(await this.evalNodeAsync(p, blockContext)); - } - - startObject = await this.invokeFunctionAsync(func.bind(startObject), pms, { - moduleName: blockContext.moduleName, - line: funcCallNode.loc[0], - column: funcCallNode.loc[0] - }); - } else { - throw Error("Can't resolve dotObjectAccess node"); - } - } - - // no undefined values, make it rather null - return startObject === undefined ? null : startObject; + if (node.type === 'chainingCalls') { + return await this.resolveChainingCallsNode(node as ChainingCallsNode, blockContext); } if (node.type === 'createObject') { @@ -611,4 +543,59 @@ export class EvaluatorAsync { return res; } } + + private async resolveChainingCallsNode( + chNode: ChainingCallsNode, + blockContext: BlockContext + ): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let startObject = (await this.evalNodeAsync(chNode.innerNodes[0], blockContext)) as any; + + for (let i = 1; i < chNode.innerNodes.length; i++) { + const nestedProp = chNode.innerNodes[i]; + + if ((chNode.innerNodes[i - 1] as unknown as IsNullCoelsing).nullCoelsing && !startObject) { + startObject = {}; + } + + if (nestedProp.type === 'getSingleVar') { + startObject = startObject[(nestedProp as SetSingleVarNode).name] as unknown; + } else if (nestedProp.type === 'chainingObjectAccess') { + const node = nestedProp as ChainingObjectAccessNode; + // startObject = startObject[node.] as unknown; + startObject = startObject[ + (await this.evalNodeAsync(node.indexerBody, blockContext)) as string + ] as unknown; + } else if (nestedProp.type === 'funcCall') { + const funcCallNode = nestedProp as FunctionCallNode; + const func = startObject[funcCallNode.name] as (...args: unknown[]) => unknown; + + if ( + (func === undefined || func === null) && + (chNode.innerNodes[i - 1] as unknown as IsNullCoelsing).nullCoelsing + ) { + startObject = null; + continue; + } + + if (typeof func !== 'function') { + throw Error(`'${funcCallNode.name}' is not a function or not defined.`); + } + const pms = []; + for (const p of funcCallNode.paramNodes || []) { + pms.push(await this.evalNodeAsync(p, blockContext)); + } + + startObject = await this.invokeFunctionAsync(func.bind(startObject), pms, { + moduleName: blockContext.moduleName, + line: funcCallNode.loc[0], + column: funcCallNode.loc[0] + }); + } else { + throw Error("Can't resolve chainingCalls node"); + } + } + + return startObject === undefined ? null : startObject; + } } diff --git a/src/interpreter.spec.ts b/src/interpreter.spec.ts index 2a17670..3fac785 100644 --- a/src/interpreter.spec.ts +++ b/src/interpreter.spec.ts @@ -413,7 +413,7 @@ describe('Interpreter', () => { }; check(e.eval(script) as number[]); - check(await e.evaluate(script) as any); + check((await e.evaluate(script)) as any); }); it('try catch no error', async () => { @@ -436,7 +436,7 @@ describe('Interpreter', () => { expect(result[2]).toBe(3); }; - check(await e.evaluate(script) as any); + check((await e.evaluate(script)) as any); check(e.eval(script) as number[]); }); @@ -728,11 +728,11 @@ describe('Interpreter', () => { `); }); - const res = await interpreter.evaluate(` + const res = (await interpreter.evaluate(` import './some.json' as obj return obj - `) as any; + `)) as any; expect(res.x).toBe('test1'); expect(res.n).toBe(22); @@ -742,13 +742,13 @@ describe('Interpreter', () => { const interpreter = Interpreter.create(); interpreter.registerPackagesLoader(path => - path === 'service' + (path === 'service' ? { add: (x: number, y: number): number => x + y, remove: (x: number, y: number): number => x - y, times: (x: number, y: number): number => x * y } - : null + : null) as any ); interpreter.registerModuleLoader(() => { @@ -784,7 +784,7 @@ describe('Interpreter', () => { it('semicolon as a string', async () => { const interpreter = Interpreter.create(); - const res = await interpreter.evaluate(`"first;second".split(';')`) as any; + const res = (await interpreter.evaluate(`"first;second".split(';')`)) as any; expect(res.length).toBe(2); }); @@ -815,4 +815,90 @@ describe('Interpreter', () => { expect(await interpreter.evalAsync(script)).toBe(55); expect(interpreter.eval(script)).toBe(55); }); + + it('complex chaining call', async () => { + const interpreter = Interpreter.create(); + + const script = ` + p = {f:{}} + p.f.x = 9 + p.f.o = {v: 9} + p["test" + p.f.x] = "test9 value" + return p.test9 + `; + expect(await interpreter.evalAsync(script)).toBe('test9 value'); + expect(interpreter.eval(script)).toBe('test9 value'); + }); + + it('chaining calls - split', async () => { + const interpreter = Interpreter.create(); + + const script = ` + "12,13,14".split(',')[1] + `; + expect(await interpreter.evalAsync(script)).toBe('13'); + expect(interpreter.eval(script)).toBe('13'); + }); + + it('chaining calls - array indexer', async () => { + const interpreter = Interpreter.create(); + + const script = ` +[ + ["ss1", "ss21", 5], + ["ss2", "test value", 6], + ["ss3", "2020-03-07", 7], + [] +][1][1] + `; + expect(await interpreter.evalAsync(script)).toBe('test value'); + expect(interpreter.eval(script)).toBe('test value'); + }); + + it('chaining calls - array indexer with ?', async () => { + const interpreter = Interpreter.create(); + + const script = ` +[ + ["ss1", "ss21", 5], + ["ss2", "test value", 6], + ["ss3", "2020-03-07", 7], + [] +][1]?[1] + `; + expect(await interpreter.evalAsync(script)).toBe('test value'); + expect(interpreter.eval(script)).toBe('test value'); + }); + + it('chaining calls - object indexer with ?', async () => { + const interpreter = Interpreter.create(); + + const script = ` + {x: 5, y: 10}.x + `; + expect(await interpreter.evalAsync(script)).toBe(5); + expect(interpreter.eval(script)).toBe(5); + }); + + it('chaining calls - object indexer with ?', async () => { + const interpreter = Interpreter.create(); + + const script = ` + {x: 5, y: 10, xy: 15}["x"+"y"] + `; + expect(await interpreter.evalAsync(script)).toBe(15); + expect(interpreter.eval(script)).toBe(15); + }); + + it('chaining calls - object indexer with ?', async () => { + const interpreter = Interpreter.create(); + + const script = ` + {value: "1st,2nd,3d"}["value"]?.split(',')?[1] + `; + expect(await interpreter.evalAsync(script)).toBe('2nd'); + expect(interpreter.eval(script)).toBe('2nd'); + }); + +// }); diff --git a/src/parser/parser.ts b/src/parser/parser.ts index 31569e8..7222ccc 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -18,8 +18,6 @@ import { getEndLine, findOperators, splitTokens, - DotObjectAccessNode, - BracketObjectAccessNode, findTokenValueIndex, FunctionDefNode, CreateObjectNode, @@ -608,23 +606,8 @@ export class Parser { return prevNode; } - // create function call node - if (tokens.length > 2 && getTokenValue(tokens[1]) === '(') { - const isNullCoelsing = getTokenValue(tokens[tokens.length - 1]) === '?'; - if (isNullCoelsing) { - // remove '?' - tokens.pop(); - } - const name = getTokenValue(tokens[0]) as string; - const paramsTokensSlice = tokens.slice(2, tokens.length - 1); - const paramsTokens = splitTokens(paramsTokensSlice, ','); - const paramsNodes = paramsTokens.map(tkns => this.createExpressionNode(tkns)); - const node = new FunctionCallNode(name, paramsNodes, getTokenLoc(tokens[0])); - node.nullCoelsing = isNullCoelsing || undefined; - return node; - } - // create chaining calls + const inds = findChainingCallTokensIndexes(tokens); if (inds.length > 0) { @@ -658,6 +641,22 @@ export class Parser { return new ChainingCallsNode(innerNodes, getTokenLoc(tokens[0])); } + // create function call node + if (tokens.length > 2 && getTokenValue(tokens[1]) === '(') { + const isNullCoelsing = getTokenValue(tokens[tokens.length - 1]) === '?'; + if (isNullCoelsing) { + // remove '?' + tokens.pop(); + } + const name = getTokenValue(tokens[0]) as string; + const paramsTokensSlice = tokens.slice(2, tokens.length - 1); + const paramsTokens = splitTokens(paramsTokensSlice, ','); + const paramsNodes = paramsTokens.map(tkns => this.createExpressionNode(tkns)); + const node = new FunctionCallNode(name, paramsNodes, getTokenLoc(tokens[0])); + node.nullCoelsing = isNullCoelsing || undefined; + return node; + } + // create Object Node if (getTokenValue(tokens[0]) === '{' && getTokenValue(tokens[tokens.length - 1]) === '}') { const keyValueTokens = splitTokens(tokens.splice(1, tokens.length - 2), ','); @@ -711,23 +710,6 @@ export class Parser { return new CreateArrayNode(items, getTokenLoc(tokens[0])); } - // create DotObjectAccessNode - const subObjects = splitTokens(tokens, '.'); - if (subObjects.length > 1) { - return new DotObjectAccessNode( - subObjects.map(tkns => this.createExpressionNode(tkns)), - getTokenLoc(tokens[0]) - ); - } - - // bracket access object node - if (tokens.length > 2 && getTokenValue(tokens[1]) === '[') { - const name = getTokenValue(tokens[0]) as string; - const paramsTokensSlice = tokens.slice(2, tokens.length - 1); - const paramsNodes = this.createExpressionNode(paramsTokensSlice); - return new BracketObjectAccessNode(name, paramsNodes, false, getTokenLoc(tokens[0])); - } - throw Error(`Undefined node '${getTokenValue(tokens[0])}'.`); } }