From 20bf06d1ca1fa6f064487a1a2c3c9358bffeb26f Mon Sep 17 00:00:00 2001 From: Ivan Shubin Date: Tue, 28 Jan 2025 21:06:32 +0100 Subject: [PATCH] added the very basic stack tracking in SchemioScript to improve the error message --- src/ui/templater/ast.js | 6 +++--- src/ui/templater/error.js | 19 +++++++++++++++++++ src/ui/templater/nodes.js | 28 +++++++++++++++++++--------- src/ui/templater/scope.js | 7 ++++--- src/ui/templater/struct.js | 2 +- src/ui/templater/templater.js | 9 +++++++-- 6 files changed, 53 insertions(+), 18 deletions(-) create mode 100644 src/ui/templater/error.js diff --git a/src/ui/templater/ast.js b/src/ui/templater/ast.js index 90ba16c6d..c02a582ae 100644 --- a/src/ui/templater/ast.js +++ b/src/ui/templater/ast.js @@ -442,7 +442,7 @@ class ASTParser extends TokenScanner { throw new Error(`Expected function body declaration for "${funcNameToken.v}" function, got: "${funcBodyToken.text}"`); } - const functionAST = parseFunctionDeclarationUsing(funcArgsToken, funcBodyToken); + const functionAST = parseFunctionDeclarationUsing(funcArgsToken, funcBodyToken, funcNameToken.v); return new ASTAssign(new ASTVarRef(funcNameToken.v), functionAST); } @@ -545,7 +545,7 @@ class ASTParser extends TokenScanner { * @param {ScriptToken} argsToken * @param {ScriptToken} bodyToken */ -function parseFunctionDeclarationUsing(argsToken, bodyToken) { +function parseFunctionDeclarationUsing(argsToken, bodyToken, funcName = '< anonymous >') { if (!argsToken) { throw new Error('Cannot parse function declaration. Missing arguments definition'); } @@ -579,7 +579,7 @@ function parseFunctionDeclarationUsing(argsToken, bodyToken) { const funcBody = parseAST(bodyToken.groupTokens); - return new ASTFunctionDeclaration(argNames, funcBody); + return new ASTFunctionDeclaration(argNames, funcBody, funcName); } /** diff --git a/src/ui/templater/error.js b/src/ui/templater/error.js new file mode 100644 index 000000000..77406273a --- /dev/null +++ b/src/ui/templater/error.js @@ -0,0 +1,19 @@ +export class SchemioScriptError extends Error{ + constructor(message, scope, error) { + super(message); + this.message = message; + this.scope = scope; + this.error = error; + } + + print() { + let fullMessage = this.message; + + let scope = this.scope; + while(scope) { + fullMessage += '\n\tat ' + scope.stackName; + scope = scope.parent; + } + console.error(fullMessage); + } +} \ No newline at end of file diff --git a/src/ui/templater/nodes.js b/src/ui/templater/nodes.js index 097f1f259..5425db6fe 100644 --- a/src/ui/templater/nodes.js +++ b/src/ui/templater/nodes.js @@ -11,6 +11,7 @@ import { parseColor } from "../colors"; import { Color } from "./color"; import { Area } from "./area"; import { Fill } from "./fill"; +import { SchemioScriptError } from "./error"; const FUNC_INVOKE = 'funcInvoke'; const VAR_REF = 'var-ref'; @@ -205,8 +206,8 @@ export class ASTWhileStatement extends ASTNode { */ evalNode(scope) { let lastResult = null; - while (this.whileExpression.evalNode(scope.newScope())) { - lastResult = this.whileBlock.evalNode(scope.newScope()); + while (this.whileExpression.evalNode(scope.newScope('while expression'))) { + lastResult = this.whileBlock.evalNode(scope.newScope('while block')); } return lastResult; } @@ -235,7 +236,7 @@ export class ASTForLoop extends ASTNode { * @param {Scope} scope */ evalNode(scope) { - scope = scope.newScope(); + scope = scope.newScope('for loop'); this.init.evalNode(scope); while(this.condition.evalNode(scope)) { @@ -258,14 +259,14 @@ export class ASTIFStatement extends ASTNode { } evalNode(scope) { - const result = this.conditionExpression.evalNode(scope.newScope()); + const result = this.conditionExpression.evalNode(scope.newScope('if statement')); if (result) { if (!this.trueBlock) { return null; } - return this.trueBlock.evalNode(scope.newScope()); + return this.trueBlock.evalNode(scope.newScope('if block')); } else if (this.falseBlock) { - return this.falseBlock.evalNode(scope.newScope()); + return this.falseBlock.evalNode(scope.newScope('else block')); } return null; } @@ -680,10 +681,11 @@ export class ASTFunctionDeclaration extends ASTNode { * @param {Array} argNames * @param {ASTNode} body */ - constructor(argNames, body) { + constructor(argNames, body, funcName) { super('function'); this.argNames = argNames; this.body = body; + this.funcName = funcName; } print() { return `(${this.argNames.join(',')}) => {(${this.body.print()})}`; @@ -694,11 +696,19 @@ export class ASTFunctionDeclaration extends ASTNode { */ evalNode(scope) { return (...args) => { - const funcScope = scope.newScope(); + const funcScope = scope.newScope('func ' + this.funcName); for (let i = 0; i < args.length && i < this.argNames.length; i++) { funcScope.setLocal(this.argNames[i], args[i]); } - return this.body.evalNode(funcScope); + try { + return this.body.evalNode(funcScope); + } catch (err) { + if (err instanceof SchemioScriptError) { + throw err; + } else { + throw new SchemioScriptError(err.message, funcScope, err); + } + } }; } } diff --git a/src/ui/templater/scope.js b/src/ui/templater/scope.js index 5004ab176..7e20127f2 100644 --- a/src/ui/templater/scope.js +++ b/src/ui/templater/scope.js @@ -5,8 +5,9 @@ export class Scope { * @param {Scope|null} parent * @param {function(string): any} externalObjectProvider */ - constructor(data, parent, externalObjectProvider) { + constructor(data, parent, externalObjectProvider, stackName = '< unknown >') { this.data = data || {}; + this.stackName = stackName; this.parent = parent; this.externalObjectProvider = externalObjectProvider; } @@ -54,8 +55,8 @@ export class Scope { this.data[varName] = value; } - newScope(data = {}) { - return new Scope(data, this, this.externalObjectProvider); + newScope(stackName = '< unknown >', data = {}) { + return new Scope(data, this, this.externalObjectProvider, stackName); } getData() { diff --git a/src/ui/templater/struct.js b/src/ui/templater/struct.js index cf2d75792..7961d3f07 100644 --- a/src/ui/templater/struct.js +++ b/src/ui/templater/struct.js @@ -34,7 +34,7 @@ export class ASTStructNode extends ASTNode { evalNode(scope) { const initFunc = (...args) => { const structObj = {}; - const structScope = scope.newScope({'this': structObj}); + const structScope = scope.newScope('struct ' + this.name, {'this': structObj}); this.fieldDefinitions.forEach((fieldDef, idx) => { if (idx < args.length) { diff --git a/src/ui/templater/templater.js b/src/ui/templater/templater.js index aaa5b25d5..577f0e443 100644 --- a/src/ui/templater/templater.js +++ b/src/ui/templater/templater.js @@ -1,4 +1,5 @@ import { ASTNode, parseExpression } from "./ast"; +import { SchemioScriptError } from "./error"; import { List } from "./list"; import { Scope } from './scope'; import { parseStringExpression } from "./strings"; @@ -192,7 +193,11 @@ function compileExpression(expr) { return ast.evalNode(scope); } catch(ex) { console.error('Failed to evaluate expression:\n' + expr); - console.error(ex); + if (ex instanceof SchemioScriptError) { + ex.print(); + } else { + console.error(ex); + } } }; } @@ -356,7 +361,7 @@ function compileRecursiveBuilder(item, $recurse, customDefinitions) { } const data = {}; data[iteratorName] = sourceObject; - const iteratorScope = scope.newScope(data); + const iteratorScope = scope.newScope('buildObject', data); const processedObj = itemProcessor(iteratorScope); const children = childrenFetcher.evalNode(iteratorScope);