diff --git a/src/generator/writers/writeExpression.ts b/src/generator/writers/writeExpression.ts index 474e0efc1..2225b9b49 100644 --- a/src/generator/writers/writeExpression.ts +++ b/src/generator/writers/writeExpression.ts @@ -42,6 +42,7 @@ import { idText, tryExtractPath, } from "../../ast/ast-helpers"; +import { defaultInterpreterConfig } from "../../optimizer/interpreter"; function isNull(wCtx: WriterContext, expr: A.AstExpression): boolean { return getExpType(wCtx.ctx, expr).kind === "null"; @@ -188,6 +189,7 @@ export function writeExpression( // during evaluation. const value = evalConstantExpression(f, wCtx.ctx, util, { maxLoopIterations: 2n ** 12n, + maxStackDeepness: defaultInterpreterConfig.maxStackDeepness, }); return writeValue(value, wCtx); } catch (error) { diff --git a/src/optimizer/interpreter.ts b/src/optimizer/interpreter.ts index 783f967d1..5754b2cc2 100644 --- a/src/optimizer/interpreter.ts +++ b/src/optimizer/interpreter.ts @@ -496,17 +496,31 @@ export type InterpreterConfig = { // Maximum number of iterations inside a loop before a time out is issued. // This option only applies to: do...until and while loops maxLoopIterations: bigint; + + // Maximum deepness of the Environment stack. + maxStackDeepness: bigint; }; const WILDCARD_NAME: string = "_"; -type Environment = { values: Map; parent?: Environment }; +type Environment = { + values: Map; + parent?: Environment; + deepness: bigint; +}; + +/** + * This exception is thrown when the stack reaches its maximum deepness. + */ +class MaxStackDeepnessReached extends Error {} class EnvironmentStack { private currentEnv: Environment; + private maxStackDeepness: bigint; - constructor() { - this.currentEnv = { values: new Map() }; + constructor(maxStackDeepness: bigint) { + this.currentEnv = { values: new Map(), deepness: 0n }; + this.maxStackDeepness = maxStackDeepness; } private findBindingMap( @@ -614,7 +628,7 @@ class EnvironmentStack { return this.findBindingMap("self") !== undefined; } - /* + /** Executes "code" in a fresh environment that is placed at the top of the environment stack. The fresh environment is initialized with the bindings in "initialBindings". Once "code" finishes @@ -623,6 +637,10 @@ class EnvironmentStack { This method is useful for starting a new local variables scope, like in a function call. + + @param code The code to execute in the fresh environment. + @param initialBindings The initial bindings to add to the fresh environment. + @throws MaxStackDeepnessReached */ public executeInNewEnvironment( code: () => T, @@ -635,7 +653,16 @@ class EnvironmentStack { const values = initialBindings.values; const oldEnv = this.currentEnv; - this.currentEnv = { values: new Map(), parent: oldEnv }; + + if (oldEnv.deepness >= this.maxStackDeepness) { + throw new MaxStackDeepnessReached(); + } + + this.currentEnv = { + values: new Map(), + parent: oldEnv, + deepness: oldEnv.deepness + 1n, + }; names.forEach((name, index) => { this.setNewBinding(name, values[index]!); @@ -673,10 +700,13 @@ export function parseAndEvalExpression( } } -const defaultInterpreterConfig: InterpreterConfig = { +export const defaultInterpreterConfig: InterpreterConfig = { // We set the default max number of loop iterations // to the maximum number allowed for repeat loops maxLoopIterations: maxRepeatStatement, + + // Default set to 500. This is within the bounds of the call stack in TypeScript. + maxStackDeepness: 500n, }; /* @@ -740,7 +770,7 @@ export class Interpreter { context: CompilerContext = new CompilerContext(), config: InterpreterConfig = defaultInterpreterConfig, ) { - this.envStack = new EnvironmentStack(); + this.envStack = new EnvironmentStack(config.maxStackDeepness); this.context = context; this.config = config; this.util = util; @@ -1452,6 +1482,7 @@ export class Interpreter { () => this.evalStaticFunction( functionNode, + ast, ast.args, functionDescription.returns, ), @@ -1487,6 +1518,7 @@ export class Interpreter { private evalStaticFunction( functionCode: A.AstFunctionDef, + functionCall: A.AstStaticCall, args: A.AstExpression[], returns: TypeRef, ): A.AstLiteral { @@ -1508,7 +1540,7 @@ export class Interpreter { ); } // Call function inside a new environment - return this.envStack.executeInNewEnvironment( + return this.runInNewEnvironment( () => { // Interpret all the statements try { @@ -1546,6 +1578,7 @@ export class Interpreter { return this.util.makeNullLiteral(dummySrcInfo); } }, + functionCall.loc, { names: paramNames, values: argValues }, ); } @@ -1692,15 +1725,15 @@ export class Interpreter { this.interpretExpression(ast.condition), ); if (condition.value) { - this.envStack.executeInNewEnvironment(() => { + this.runInNewEnvironment(() => { ast.trueStatements.forEach(this.interpretStatement, this); - }); + }, ast.loc); } else if (ast.elseif !== null) { this.interpretConditionStatement(ast.elseif); } else if (ast.falseStatements !== null) { - this.envStack.executeInNewEnvironment(() => { + this.runInNewEnvironment(() => { ast.falseStatements!.forEach(this.interpretStatement, this); - }); + }, ast.loc); } } @@ -1723,11 +1756,11 @@ export class Interpreter { // the loop. Also, the language requires that all declared variables inside the // loop be initialized, which means that we can overwrite its value in the environment // in each iteration. - this.envStack.executeInNewEnvironment(() => { + this.runInNewEnvironment(() => { for (let i = 1; i <= iterations.value; i++) { ast.statements.forEach(this.interpretStatement, this); } - }); + }, ast.loc); } } @@ -1756,7 +1789,7 @@ export class Interpreter { // the loop. Also, the language requires that all declared variables inside the // loop be initialized, which means that we can overwrite its value in the environment // in each iteration. - this.envStack.executeInNewEnvironment(() => { + this.runInNewEnvironment(() => { do { ast.statements.forEach(this.interpretStatement, this); @@ -1773,7 +1806,7 @@ export class Interpreter { this.interpretExpression(ast.condition), ); } while (!condition.value); - }); + }, ast.loc); } public interpretWhileStatement(ast: A.AstStatementWhile) { @@ -1785,7 +1818,7 @@ export class Interpreter { // the loop. Also, the language requires that all declared variables inside the // loop be initialized, which means that we can overwrite its value in the environment // in each iteration. - this.envStack.executeInNewEnvironment(() => { + this.runInNewEnvironment(() => { do { // The typechecker ensures that the condition does not refer to // variables declared inside the loop. @@ -1804,13 +1837,13 @@ export class Interpreter { } } } while (condition.value); - }); + }, ast.loc); } public interpretBlockStatement(ast: A.AstStatementBlock) { - this.envStack.executeInNewEnvironment(() => { + this.runInNewEnvironment(() => { ast.statements.forEach(this.interpretStatement, this); - }); + }, ast.loc); } private inComputationPath(path: string, cb: () => T) { @@ -1834,4 +1867,25 @@ export class Interpreter { return `${shortPath.join(" -> ")} -> ${name}`; } + + private runInNewEnvironment( + code: () => T, + loc: SrcInfo, + initialBindings: { names: string[]; values: A.AstLiteral[] } = { + names: [], + values: [], + }, + ): T { + try { + return this.envStack.executeInNewEnvironment(code, initialBindings); + } catch (e) { + if (e instanceof MaxStackDeepnessReached) { + throwNonFatalErrorConstEval( + "Execution stack reached maximum level", + loc, + ); + } + throw e; + } + } } diff --git a/src/optimizer/test/__snapshots__/stack-deepness.spec.ts.snap b/src/optimizer/test/__snapshots__/stack-deepness.spec.ts.snap new file mode 100644 index 000000000..cc3d5d4d7 --- /dev/null +++ b/src/optimizer/test/__snapshots__/stack-deepness.spec.ts.snap @@ -0,0 +1,190 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`stack-deepness stack-deepness-deep-call should fail compilation 1`] = ` +":16:25: Cannot evaluate expression to a constant: Execution stack reached maximum level + 15 | { +> 16 | { + ^ + 17 | { +" +`; + +exports[`stack-deepness stack-deepness-infinite-recursion should fail compilation 1`] = ` +":6:12: Cannot evaluate expression to a constant: Execution stack reached maximum level + 5 | fun recurse(): Int { +> 6 | return recurse(); + ^~~~~~~~~ + 7 | } +" +`; + +exports[`stack-deepness stack-deepness-near-miss should fail compilation 1`] = ` +":12:20: Cannot evaluate expression to a constant: Execution stack reached maximum level + 11 | } else { +> 12 | return 2 + multiplyByTwo(n - 1); // When it enters this branch, each call will open two levels in the environment stack. + ^~~~~~~~~~~~~~~~~~~~ + 13 | } // Since the method is called 250 times, it creates around 500 levels in the stack. +" +`; + +exports[`stack-deepness stack-deepness-near-miss should pass compilation 1`] = ` +[ + [ + "249", + "Int", + ], + [ + "multiplyByTwo(249)", + "Int", + ], + [ + "n", + "Int", + ], + [ + "0", + "Int", + ], + [ + "n < 0", + "Bool", + ], + [ + "n", + "Int", + ], + [ + "-n", + "Int", + ], + [ + "multiplyByTwo(-n)", + "Int", + ], + [ + "-multiplyByTwo(-n)", + "Int", + ], + [ + "n", + "Int", + ], + [ + "0", + "Int", + ], + [ + "n == 0", + "Bool", + ], + [ + "0", + "Int", + ], + [ + "2", + "Int", + ], + [ + "n", + "Int", + ], + [ + "1", + "Int", + ], + [ + "n - 1", + "Int", + ], + [ + "multiplyByTwo(n - 1)", + "Int", + ], + [ + "2 + multiplyByTwo(n - 1)", + "Int", + ], +] +`; + +exports[`stack-deepness stack-deepness-recursion should pass compilation 1`] = ` +[ + [ + "200", + "Int", + ], + [ + "multiplyByTwo(200)", + "Int", + ], + [ + "n", + "Int", + ], + [ + "0", + "Int", + ], + [ + "n < 0", + "Bool", + ], + [ + "n", + "Int", + ], + [ + "-n", + "Int", + ], + [ + "multiplyByTwo(-n)", + "Int", + ], + [ + "-multiplyByTwo(-n)", + "Int", + ], + [ + "n", + "Int", + ], + [ + "0", + "Int", + ], + [ + "n == 0", + "Bool", + ], + [ + "0", + "Int", + ], + [ + "2", + "Int", + ], + [ + "n", + "Int", + ], + [ + "1", + "Int", + ], + [ + "n - 1", + "Int", + ], + [ + "multiplyByTwo(n - 1)", + "Int", + ], + [ + "2 + multiplyByTwo(n - 1)", + "Int", + ], +] +`; diff --git a/src/optimizer/test/failed/stack-deepness-deep-call.tact b/src/optimizer/test/failed/stack-deepness-deep-call.tact new file mode 100644 index 000000000..ea28dbd2d --- /dev/null +++ b/src/optimizer/test/failed/stack-deepness-deep-call.tact @@ -0,0 +1,48 @@ +primitive Int; + +// This call fails with the default execution stack max level of 500. +const A: Int = multiplyByTwoDeepBlocks(200); + +fun multiplyByTwoDeepBlocks(n: Int): Int { + if (n < 0) { + return -multiplyByTwoDeepBlocks(-n); + } else if (n == 0) { + return 0; + } else { // When it enters this branch, each call creates 18 levels in the stack. + { // Since the method is called 200 times, it would create around 3600 levels. + { + { + { + { + { + { + { + { + { + { + { + { + { + { + { + { + return 2 + multiplyByTwoDeepBlocks(n - 1); + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/optimizer/test/failed/stack-deepness-infinite-recursion.tact b/src/optimizer/test/failed/stack-deepness-infinite-recursion.tact new file mode 100644 index 000000000..245c3ba6a --- /dev/null +++ b/src/optimizer/test/failed/stack-deepness-infinite-recursion.tact @@ -0,0 +1,7 @@ +primitive Int; + +const A: Int = recurse(); + +fun recurse(): Int { + return recurse(); +} diff --git a/src/optimizer/test/failed/stack-deepness-near-miss.tact b/src/optimizer/test/failed/stack-deepness-near-miss.tact new file mode 100644 index 000000000..65d0453e5 --- /dev/null +++ b/src/optimizer/test/failed/stack-deepness-near-miss.tact @@ -0,0 +1,14 @@ +primitive Int; + +// This call fails with the default execution stack max level of 500. +const A: Int = multiplyByTwo(250); + +fun multiplyByTwo(n: Int): Int { + if (n < 0) { + return -multiplyByTwo(-n); + } else if (n == 0) { + return 0; + } else { + return 2 + multiplyByTwo(n - 1); // When it enters this branch, each call will open two levels in the environment stack. + } // Since the method is called 250 times, it creates around 500 levels in the stack. +} // But the initial call adds another level, so it exceeds the limit during execution. \ No newline at end of file diff --git a/src/optimizer/test/stack-deepness.spec.ts b/src/optimizer/test/stack-deepness.spec.ts new file mode 100644 index 000000000..f7bb3ebd1 --- /dev/null +++ b/src/optimizer/test/stack-deepness.spec.ts @@ -0,0 +1,47 @@ +import { getAstFactory } from "../../ast/ast-helpers"; +import { featureEnable } from "../../config/features"; +import { CompilerContext } from "../../context/context"; +import { openContext } from "../../context/store"; +import { getParser } from "../../grammar"; +import { defaultParser } from "../../grammar/grammar"; +import { resolveDescriptors } from "../../types/resolveDescriptors"; +import { getAllExpressionTypes } from "../../types/resolveExpression"; +import { resolveSignatures } from "../../types/resolveSignatures"; +import { resolveStatements } from "../../types/resolveStatements"; +import { loadCases } from "../../utils/loadCases"; + +describe("stack-deepness", () => { + for (const r of loadCases(__dirname + "/success/")) { + it(`${r.name} should pass compilation`, () => { + const Ast = getAstFactory(); + let ctx = openContext( + new CompilerContext(), + [{ code: r.code, path: "", origin: "user" }], + [], + getParser(Ast, defaultParser), + ); + ctx = featureEnable(ctx, "external"); + ctx = resolveDescriptors(ctx, Ast); + ctx = resolveStatements(ctx, Ast); + ctx = resolveSignatures(ctx, Ast); + expect(getAllExpressionTypes(ctx)).toMatchSnapshot(); + }); + } + for (const r of loadCases(__dirname + "/failed/")) { + it(`${r.name} should fail compilation`, () => { + const Ast = getAstFactory(); + let ctx = openContext( + new CompilerContext(), + [{ code: r.code, path: "", origin: "user" }], + [], + getParser(Ast, defaultParser), + ); + ctx = featureEnable(ctx, "external"); + expect(() => { + ctx = resolveDescriptors(ctx, Ast); + ctx = resolveStatements(ctx, Ast); + ctx = resolveSignatures(ctx, Ast); + }).toThrowErrorMatchingSnapshot(); + }); + } +}); diff --git a/src/optimizer/test/success/stack-deepness-near-miss.tact b/src/optimizer/test/success/stack-deepness-near-miss.tact new file mode 100644 index 000000000..1515e421b --- /dev/null +++ b/src/optimizer/test/success/stack-deepness-near-miss.tact @@ -0,0 +1,15 @@ +primitive Int; + +// This call succeeds with the default execution stack max level of 500. +const A: Int = multiplyByTwo(249); + +fun multiplyByTwo(n: Int): Int { + if (n < 0) { + return -multiplyByTwo(-n); + } else if (n == 0) { + return 0; + } else { + return 2 + multiplyByTwo(n - 1); // When it enters this branch, each call will open two levels in the environment stack. + } // Since the method is called 249 times, it creates around 498 levels in the stack. +} // But the initial call adds another level, plus the final level in the n = 0 case, + // gives around 500 levels, so it remains within the limit. \ No newline at end of file diff --git a/src/optimizer/test/success/stack-deepness-recursion.tact b/src/optimizer/test/success/stack-deepness-recursion.tact new file mode 100644 index 000000000..71fd6af5c --- /dev/null +++ b/src/optimizer/test/success/stack-deepness-recursion.tact @@ -0,0 +1,14 @@ +primitive Int; + +// This call succeeds with the default execution stack max level of 500. +const A: Int = multiplyByTwo(200); + +fun multiplyByTwo(n: Int): Int { + if (n < 0) { + return -multiplyByTwo(-n); + } else if (n == 0) { + return 0; + } else { + return 2 + multiplyByTwo(n - 1); // When it enters this branch, each call will open two levels in the environment stack. + } // Since the method is called 200 times, it creates around 400 levels in the stack, +} // plus the initial call, plus the final level for the n = 0 case, remains within the limit. \ No newline at end of file