diff --git a/action-parser/src/action.interfaces.ts b/action-parser/src/action.interfaces.ts index 9aa7045..0fc651b 100644 --- a/action-parser/src/action.interfaces.ts +++ b/action-parser/src/action.interfaces.ts @@ -267,5 +267,6 @@ export interface ActionCtx { } export interface ActionsCtx { - action: CstNode[] + action?: CstNode[] + consumeError?: CstNode[] } diff --git a/action-parser/src/action.parser.ts b/action-parser/src/action.parser.ts index 50e354a..1f8c314 100644 --- a/action-parser/src/action.parser.ts +++ b/action-parser/src/action.parser.ts @@ -639,17 +639,102 @@ export class ActionParser extends CstParser { return {[trigger]: commands} }) + consumeError = this.RULE('consumeError', () => { + this.AT_LEAST_ONE(() => { + this.OR([ + { + GATE: () => this.LA(1).tokenType.name !== 'Equals', + ALT: () => this.CONSUME(Resource) + }, + {ALT: () => this.CONSUME(Comma)}, + {ALT: () => this.CONSUME(Equals)} + ]) + }) + }) + actions = this.RULE('actions', () => { - const actionList = [this.SUBRULE(this.action)] + const actionList = [] + this.OPTION(() => { + this.OR([ + { + GATE: () => + ['Create', 'Activate', 'Bump', 'Adone'].includes( + this.LA(1).tokenType.name + ), + ALT: () => actionList.push(this.SUBRULE(this.action)) + }, + {ALT: () => this.SUBRULE(this.consumeError)} + ]) + }) + this.MANY(() => { this.CONSUME(Semicolon) - const nextAction = this.SUBRULE1(this.action) - if (nextAction) { - actionList.push(nextAction) - } + this.OPTION1(() => { + this.OR1([ + { + GATE: () => + ['Create', 'Activate', 'Bump', 'Adone'].includes( + this.LA(1).tokenType.name + ), + ALT: () => { + const nextAction = this.SUBRULE1(this.action) + if (nextAction) { + actionList.push(nextAction) + } + } + }, + { + GATE: () => this.LA(1).tokenType.name !== 'Semicolon', + ALT: () => this.SUBRULE1(this.consumeError) + } + ]) + }) }) - // Optional trailing semicolons - this.OPTION(() => this.MANY1(() => this.CONSUME1(Semicolon))) + + // This OPTION for trailing semicolons is tricky because MANY loop above consumes semicolons. + // However, if we have ;;;, the MANY consumes one ;, then enters OPTION1. + // Inside OPTION1, we check if Semicolon. If Semicolon, GATE fails (name !== 'Semicolon'). + // So OPTION1 matches nothing (empty). + // Then MANY repeats. Next char is ;. + // So MANY consumes ;. + // So multiple semicolons should be consumed by MANY loop. + // BUT, the GATE in consumeError was preventing consumption of Semicolon? + // consumeError consumes Resource, Comma, Equals. + // If I have ;;; + // 1. ; consumed by CONSUME(Semicolon) + // 2. Next is ;. OPTION1 enters. + // GATE !Semicolon -> false. + // So OPTION1 is empty. + // 3. MANY repeats. Next is ;. + // CONSUME(Semicolon). + // 4. ... + // So existing logic should handle multiple semicolons. + // Why did `whitespace and semicolons do not matter` fail? + // `create color abcdef;;;;;;` + // Maybe `abcdef` was consumed by `consumeError`? + // `create color` -> action. + // `abcdef` -> invalid command? + // Wait, `create color abcdef` is parsed as `create color` with resource `abcdef`. + // The test says: + // `create color abcdef;;;;;;` -> `color: {r: 171, g: 205, b: 239}`. + // `abcdef` is a hex color? #abcdef is r=171, g=205, b=239. + // Ah, `create color` consumes `Resource`. `abcdef` is a resource. + // So `create color abcdef` is a valid action. + // The semicolons follow. + // `action` returns. + // MANY loop starts. + // CONSUME(Semicolon). + // Remaining ;;;;; + // OPTION1 -> GATE !Semicolon -> false. Empty. + // MANY repeats. + // CONSUME(Semicolon). + // ... + // Until EOF. + // So it should work. + + // Let's remove the extra trailing semicolon rule as it might be redundant or causing ambiguity if MANY doesn't consume all. + // this.OPTION(() => this.MANY1(() => this.CONSUME1(Semicolon))) + return actionList }) } diff --git a/action-parser/src/action.visitor.ts b/action-parser/src/action.visitor.ts index 57caa39..3101223 100644 --- a/action-parser/src/action.visitor.ts +++ b/action-parser/src/action.visitor.ts @@ -594,6 +594,9 @@ class ActionVisitor extends BaseActionVisitor { } action(ctx: ActionCtx) { + if (!ctx.trigger || !ctx.command) { + return {} + } const type = this.visit(ctx.trigger) const commands = ctx.command.map((command) => this.visit(command)) const result: Record = {} @@ -618,14 +621,18 @@ class ActionVisitor extends BaseActionVisitor { return result } + consumeError() { + return {} + } + actions(ctx: ActionsCtx) { - const actions = ctx.action.map((action) => this.visit(action)) + const actions = (ctx.action || []).map((action) => this.visit(action)) // Filter out duplicate actions of the same type, keeping only the first one const actionsMap = new Map() actions.forEach((action) => { const [key] = Object.keys(action) - if (!actionsMap.has(key)) { + if (key && !actionsMap.has(key)) { actionsMap.set(key, action) } }) @@ -655,16 +662,40 @@ export class Action { parserInstance.input = lexResult.tokens const cst = parserInstance.actions() - return parserInstance.errors.length > 0 ? {} : this.visitor.visit(cst) + + if (parserInstance.errors.length > 0) { + console.error(parserInstance.errors) + } + + return this.visitor.visit(cst) } debug(inputText: string) { const lexResult = this.lexer.tokenize(inputText) parserInstance.input = lexResult.tokens - parserInstance.actions() - return parserInstance.errors.length > 0 - ? parserInstance.errors[0].message - : 'OK' + const cst = parserInstance.actions() + + if (parserInstance.errors.length > 0) { + return parserInstance.errors[0].message + } + + // Check if consumeError was used in CST + const checkForError = (node: any): boolean => { + if (!node) return false + if (node.name === 'consumeError') return true + if (node.children) { + return Object.values(node.children).some((children: any) => + children.some((child: any) => checkForError(child)) + ) + } + return false + } + + if (checkForError(cst)) { + return 'Invalid action' + } + + return 'OK' } } diff --git a/package-lock.json b/package-lock.json index f441575..c2ea146 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1390,7 +1390,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1433,7 +1433,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.5" @@ -1483,7 +1483,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -5685,20 +5685,6 @@ "url": "https://dotenvx.com" } }, - "node_modules/@prisma/config/node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" - } - }, "node_modules/@prisma/debug": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", @@ -6720,7 +6706,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=10" } @@ -6738,7 +6723,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=10" } @@ -6756,7 +6740,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=10" } @@ -6774,7 +6757,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=10" } @@ -6792,7 +6774,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=10" } @@ -6810,7 +6791,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=10" } @@ -6828,7 +6808,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=10" } @@ -6846,7 +6825,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=10" } @@ -6864,7 +6842,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=10" } @@ -6882,7 +6859,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=10" } @@ -15939,7 +15915,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0"