diff --git a/src/constEval.ts b/src/constEval.ts index 16e509da2..b895f9873 100644 --- a/src/constEval.ts +++ b/src/constEval.ts @@ -18,13 +18,13 @@ import { import { ExpressionTransformer } from "./optimizer/types"; import { StandardOptimizer } from "./optimizer/standardOptimizer"; import { - Interpreter, - InterpreterConfig, ensureInt, evalBinaryOp, evalUnaryOp, - throwNonFatalErrorConstEval, -} from "./interpreter"; + InterpreterConfig, + TactInterpreter, +} from "./interpreters/standard"; +import { throwNonFatalErrorConstEval } from "./interpreters/util"; // Utility Exception class to interrupt the execution // of functions that cannot evaluate a tree fully into a value. @@ -46,21 +46,30 @@ function partiallyEvalUnaryOp( operand: AstExpression, source: SrcInfo, ctx: CompilerContext, + interpreter: TactInterpreter, ): AstExpression { if (operand.kind === "number" && op === "-") { // emulating negative integer literals - return makeValueExpression(ensureInt(-operand.value, source)); + return makeValueExpression( + ensureInt(-operand.value, source), + operand.loc, + ); } - const simplOperand = partiallyEvalExpression(operand, ctx); + const simplOperand = partiallyEvalExpression(operand, ctx, interpreter); if (isValue(simplOperand)) { const valueOperand = extractValue(simplOperand as AstValue); - const result = evalUnaryOp(op, valueOperand, simplOperand.loc, source); + const result = evalUnaryOp( + op, + () => valueOperand, + simplOperand.loc, + source, + ); // Wrap the value into a Tree to continue simplifications - return makeValueExpression(result); + return makeValueExpression(result, source); } else { - const newAst = makeUnaryExpression(op, simplOperand); + const newAst = makeUnaryExpression(op, simplOperand, source); return optimizer.applyRules(newAst); } } @@ -71,8 +80,9 @@ function partiallyEvalBinaryOp( right: AstExpression, source: SrcInfo, ctx: CompilerContext, + interpreter: TactInterpreter, ): AstExpression { - const leftOperand = partiallyEvalExpression(left, ctx); + const leftOperand = partiallyEvalExpression(left, ctx, interpreter); if (isValue(leftOperand)) { // Because of short-circuiting, we must delay evaluation of the right operand @@ -84,7 +94,11 @@ function partiallyEvalBinaryOp( valueLeftOperand, // We delay the evaluation of the right operand inside a continuation () => { - const rightOperand = partiallyEvalExpression(right, ctx); + const rightOperand = partiallyEvalExpression( + right, + ctx, + interpreter, + ); if (isValue(rightOperand)) { // If the right operand reduces to a value, then we can let the function // evalBinaryOp finish its normal execution by returning the value @@ -104,12 +118,17 @@ function partiallyEvalBinaryOp( source, ); - return makeValueExpression(result); + return makeValueExpression(result, source); } catch (e) { if (e instanceof PartiallyEvaluatedTree) { // The right operand did not evaluate to a value. Hence, // time to symbolically simplify the full tree. - const newAst = makeBinaryExpression(op, leftOperand, e.tree); + const newAst = makeBinaryExpression( + op, + leftOperand, + e.tree, + source, + ); return optimizer.applyRules(newAst); } else { throw e; @@ -119,8 +138,13 @@ function partiallyEvalBinaryOp( // Since the left operand does not reduce to a value, no immediate short-circuiting will occur. // Hence, we can partially evaluate the right operand and let the rules // simplify the tree. - const rightOperand = partiallyEvalExpression(right, ctx); - const newAst = makeBinaryExpression(op, leftOperand, rightOperand); + const rightOperand = partiallyEvalExpression(right, ctx, interpreter); + const newAst = makeBinaryExpression( + op, + leftOperand, + rightOperand, + source, + ); return optimizer.applyRules(newAst); } } @@ -130,7 +154,7 @@ export function evalConstantExpression( ctx: CompilerContext, interpreterConfig?: InterpreterConfig, ): Value { - const interpreter = new Interpreter(ctx, interpreterConfig); + const interpreter = new TactInterpreter(ctx, interpreterConfig); const result = interpreter.interpretExpression(ast); return result; } @@ -138,13 +162,15 @@ export function evalConstantExpression( export function partiallyEvalExpression( ast: AstExpression, ctx: CompilerContext, - interpreterConfig?: InterpreterConfig, + interpreter: TactInterpreter = new TactInterpreter(ctx), ): AstExpression { - const interpreter = new Interpreter(ctx, interpreterConfig); switch (ast.kind) { case "id": try { - return makeValueExpression(interpreter.interpretName(ast)); + return makeValueExpression( + interpreter.interpretName(ast), + ast.loc, + ); } catch (e) { if (e instanceof TactConstEvalError) { if (!e.fatal) { @@ -156,7 +182,10 @@ export function partiallyEvalExpression( } case "method_call": // Does not partially evaluate at the moment. Will attempt to fully evaluate - return makeValueExpression(interpreter.interpretMethodCall(ast)); + return makeValueExpression( + interpreter.interpretMethodCall(ast), + ast.loc, + ); case "init_of": throwNonFatalErrorConstEval( "initOf is not supported at this moment", @@ -168,11 +197,24 @@ export function partiallyEvalExpression( case "boolean": return ast; case "number": - return makeValueExpression(interpreter.interpretNumber(ast)); + return makeValueExpression( + interpreter.interpretNumber(ast), + ast.loc, + ); case "string": - return makeValueExpression(interpreter.interpretString(ast)); - case "op_unary": - return partiallyEvalUnaryOp(ast.op, ast.operand, ast.loc, ctx); + return makeValueExpression( + interpreter.interpretString(ast), + ast.loc, + ); + case "op_unary": // The fact that we are passing the interpreter around, probably signals + // that the partial evaluator itself is an instance of an abstract interpreter + return partiallyEvalUnaryOp( + ast.op, + ast.operand, + ast.loc, + ctx, + interpreter, + ); case "op_binary": return partiallyEvalBinaryOp( ast.op, @@ -180,20 +222,31 @@ export function partiallyEvalExpression( ast.right, ast.loc, ctx, + interpreter, ); case "conditional": // Does not partially evaluate at the moment. Will attempt to fully evaluate - return makeValueExpression(interpreter.interpretConditional(ast)); + return makeValueExpression( + interpreter.interpretConditional(ast), + ast.loc, + ); case "struct_instance": // Does not partially evaluate at the moment. Will attempt to fully evaluate return makeValueExpression( interpreter.interpretStructInstance(ast), + ast.loc, ); case "field_access": // Does not partially evaluate at the moment. Will attempt to fully evaluate - return makeValueExpression(interpreter.interpretFieldAccess(ast)); + return makeValueExpression( + interpreter.interpretFieldAccess(ast), + ast.loc, + ); case "static_call": // Does not partially evaluate at the moment. Will attempt to fully evaluate - return makeValueExpression(interpreter.interpretStaticCall(ast)); + return makeValueExpression( + interpreter.interpretStaticCall(ast), + ast.loc, + ); } } diff --git a/src/generator/writers/writeExpression.spec.ts b/src/generator/writers/writeExpression.spec.ts index 19b9bd17f..8b2f19e87 100644 --- a/src/generator/writers/writeExpression.spec.ts +++ b/src/generator/writers/writeExpression.spec.ts @@ -26,7 +26,7 @@ struct A { b: Int; } -fun main() { +fun main(x: Int?) { let a: Int = 1; let b: Int = 2; let c: Int = a + b; @@ -42,7 +42,7 @@ fun main() { let m: Int = -j.b + a; let n: Int = -j.b + a + (+b); let o: Int? = null; - let p: Int? = o!! + 1; + let p: Int? = x!! + 1; let q: Cell = j.toCell(); } `; @@ -63,7 +63,7 @@ const golden: string[] = [ `((- $j'b) + $a)`, `(((- $j'b) + $a) + (+ $b))`, "null()", - "(__tact_not_null($o) + 1)", + "(__tact_not_null($x) + 1)", `$A$_store_cell(($j'a, $j'b))`, ]; diff --git a/src/generator/writers/writeExpression.ts b/src/generator/writers/writeExpression.ts index 819047865..df1e33845 100644 --- a/src/generator/writers/writeExpression.ts +++ b/src/generator/writers/writeExpression.ts @@ -170,13 +170,7 @@ export function writePathExpression(path: AstId[]): string { export function writeExpression(f: AstExpression, wCtx: WriterContext): string { // literals and constant expressions are covered here try { - // Let us put a limit of 2 ^ 12 = 4096 iterations on loops to increase compiler responsiveness. - // If a loop takes more than such number of iterations, the interpreter will fail evaluation. - // I think maxLoopIterations should be a command line option in case a user wants to wait more - // during evaluation. - const value = evalConstantExpression(f, wCtx.ctx, { - maxLoopIterations: 2n ** 12n, - }); + const value = evalConstantExpression(f, wCtx.ctx); return writeValue(value, wCtx); } catch (error) { if (!(error instanceof TactConstEvalError) || error.fatal) throw error; diff --git a/src/grammar/ast.ts b/src/grammar/ast.ts index 536d31346..b2cc17a7d 100644 --- a/src/grammar/ast.ts +++ b/src/grammar/ast.ts @@ -649,7 +649,12 @@ export type AstNull = { loc: SrcInfo; }; -export type AstValue = AstNumber | AstBoolean | AstNull | AstString; +export type AstValue = + | AstNumber + | AstBoolean + | AstNull + | AstString + | AstStructInstance; export type AstConstantAttribute = | { type: "virtual"; loc: SrcInfo } diff --git a/src/interpreter.ts b/src/interpreter.ts index 8a40737c3..323b0f324 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -1,17 +1,8 @@ -import { Address, beginCell, BitString, Cell, Slice, toNano } from "@ton/core"; -import { paddedBufferToBits } from "@ton/core/dist/boc/utils/paddedBits"; -import * as crc32 from "crc-32"; import { evalConstantExpression } from "./constEval"; import { CompilerContext } from "./context"; +import { TactConstEvalError, TactParseError } from "./errors"; import { - TactConstEvalError, - TactParseError, - idTextErr, - throwConstEvalError, - throwInternalCompilerError, -} from "./errors"; -import { - AstBinaryOperation, + AstAsmFunctionDef, AstBoolean, AstCondition, AstConditional, @@ -49,555 +40,14 @@ import { AstStructDecl, AstStructInstance, AstTrait, - AstUnaryOperation, - eqNames, - idText, - isSelfId, } from "./grammar/ast"; -import { SrcInfo, dummySrcInfo, parseExpression } from "./grammar/grammar"; -import { divFloor, modFloor } from "./optimizer/util"; -import { - getStaticConstant, - getStaticFunction, - getType, - hasStaticConstant, - hasStaticFunction, -} from "./types/resolveDescriptors"; -import { getExpType } from "./types/resolveExpression"; -import { - CommentValue, - StructValue, - TypeRef, - Value, - showValue, -} from "./types/types"; -import { sha256_sync } from "@ton/crypto"; -import { enabledMasterchain } from "./config/features"; - -// TVM integers are signed 257-bit integers -const minTvmInt: bigint = -(2n ** 256n); -const maxTvmInt: bigint = 2n ** 256n - 1n; - -// Range allowed in repeat statements -const minRepeatStatement: bigint = -(2n ** 256n); // Note it is the same as minimum for TVM -const maxRepeatStatement: bigint = 2n ** 31n - 1n; - -// Throws a non-fatal const-eval error, in the sense that const-eval as a compiler -// optimization cannot be applied, e.g. to `let`-statements. -// Note that for const initializers this is a show-stopper. -export function throwNonFatalErrorConstEval( - msg: string, - source: SrcInfo, -): never { - throwConstEvalError( - `Cannot evaluate expression to a constant: ${msg}`, - false, - source, - ); -} +import { parseExpression } from "./grammar/grammar"; +import { Value } from "./types/types"; -// Throws a fatal const-eval, meaning this is a meaningless program, -// so compilation should be aborted in all cases -function throwErrorConstEval(msg: string, source: SrcInfo): never { - throwConstEvalError( - `Cannot evaluate expression to a constant: ${msg}`, - true, - source, - ); -} type EvalResult = | { kind: "ok"; value: Value } | { kind: "error"; message: string }; -export function ensureInt(val: Value, source: SrcInfo): bigint { - if (typeof val !== "bigint") { - throwErrorConstEval( - `integer expected, but got '${showValue(val)}'`, - source, - ); - } - if (minTvmInt <= val && val <= maxTvmInt) { - return val; - } else { - throwErrorConstEval( - `integer '${showValue(val)}' does not fit into TVM Int type`, - source, - ); - } -} - -function ensureRepeatInt(val: Value, source: SrcInfo): bigint { - if (typeof val !== "bigint") { - throwErrorConstEval( - `integer expected, but got '${showValue(val)}'`, - source, - ); - } - if (minRepeatStatement <= val && val <= maxRepeatStatement) { - return val; - } else { - throwErrorConstEval( - `repeat argument must be a number between -2^256 (inclusive) and 2^31 - 1 (inclusive)`, - source, - ); - } -} - -function ensureBoolean(val: Value, source: SrcInfo): boolean { - if (typeof val !== "boolean") { - throwErrorConstEval( - `boolean expected, but got '${showValue(val)}'`, - source, - ); - } - return val; -} - -function ensureString(val: Value, source: SrcInfo): string { - if (typeof val !== "string") { - throwErrorConstEval( - `string expected, but got '${showValue(val)}'`, - source, - ); - } - return val; -} - -function ensureFunArity(arity: number, args: AstExpression[], source: SrcInfo) { - if (args.length !== arity) { - throwErrorConstEval( - `function expects ${arity} argument(s), but got ${args.length}`, - source, - ); - } -} - -function ensureMethodArity( - arity: number, - args: AstExpression[], - source: SrcInfo, -) { - if (args.length !== arity) { - throwErrorConstEval( - `method expects ${arity} argument(s), but got ${args.length}`, - source, - ); - } -} - -export function evalUnaryOp( - op: AstUnaryOperation, - valOperand: Value, - operandLoc: SrcInfo = dummySrcInfo, - source: SrcInfo = dummySrcInfo, -): Value { - switch (op) { - case "+": - return ensureInt(valOperand, operandLoc); - case "-": - return ensureInt(-ensureInt(valOperand, operandLoc), source); - case "~": - return ~ensureInt(valOperand, operandLoc); - case "!": - return !ensureBoolean(valOperand, operandLoc); - case "!!": - if (valOperand === null) { - throwErrorConstEval( - "non-null value expected but got null", - operandLoc, - ); - } - return valOperand; - } -} - -export function evalBinaryOp( - op: AstBinaryOperation, - valLeft: Value, - valRightContinuation: () => Value, // It needs to be a continuation, because some binary operators short-circuit - locLeft: SrcInfo = dummySrcInfo, - locRight: SrcInfo = dummySrcInfo, - source: SrcInfo = dummySrcInfo, -): Value { - switch (op) { - case "+": - return ensureInt( - ensureInt(valLeft, locLeft) + - ensureInt(valRightContinuation(), locRight), - source, - ); - case "-": - return ensureInt( - ensureInt(valLeft, locLeft) - - ensureInt(valRightContinuation(), locRight), - source, - ); - case "*": - return ensureInt( - ensureInt(valLeft, locLeft) * - ensureInt(valRightContinuation(), locRight), - source, - ); - case "/": { - // The semantics of integer division for TVM (and by extension in Tact) - // is a non-conventional one: by default it rounds towards negative infinity, - // meaning, for instance, -1 / 5 = -1 and not zero, as in many mainstream languages. - // Still, the following holds: a / b * b + a % b == a, for all b != 0. - const r = ensureInt(valRightContinuation(), locRight); - if (r === 0n) - throwErrorConstEval( - "divisor expression must be non-zero", - locRight, - ); - return ensureInt(divFloor(ensureInt(valLeft, locLeft), r), source); - } - case "%": { - // Same as for division, see the comment above - // Example: -1 % 5 = 4 - const r = ensureInt(valRightContinuation(), locRight); - if (r === 0n) - throwErrorConstEval( - "divisor expression must be non-zero", - locRight, - ); - return ensureInt(modFloor(ensureInt(valLeft, locLeft), r), source); - } - case "&": - return ( - ensureInt(valLeft, locLeft) & - ensureInt(valRightContinuation(), locRight) - ); - case "|": - return ( - ensureInt(valLeft, locLeft) | - ensureInt(valRightContinuation(), locRight) - ); - case "^": - return ( - ensureInt(valLeft, locLeft) ^ - ensureInt(valRightContinuation(), locRight) - ); - case "<<": { - const valNum = ensureInt(valLeft, locLeft); - const valBits = ensureInt(valRightContinuation(), locRight); - if (0n > valBits || valBits > 256n) { - throwErrorConstEval( - `the number of bits shifted ('${valBits}') must be within [0..256] range`, - locRight, - ); - } - try { - return ensureInt(valNum << valBits, source); - } catch (e) { - if (e instanceof RangeError) - // this actually should not happen - throwErrorConstEval( - `integer does not fit into TVM Int type`, - source, - ); - throw e; - } - } - case ">>": { - const valNum = ensureInt(valLeft, locLeft); - const valBits = ensureInt(valRightContinuation(), locRight); - if (0n > valBits || valBits > 256n) { - throwErrorConstEval( - `the number of bits shifted ('${valBits}') must be within [0..256] range`, - locRight, - ); - } - try { - return ensureInt(valNum >> valBits, source); - } catch (e) { - if (e instanceof RangeError) - // this is actually should not happen - throwErrorConstEval( - `integer does not fit into TVM Int type`, - source, - ); - throw e; - } - } - case ">": - return ( - ensureInt(valLeft, locLeft) > - ensureInt(valRightContinuation(), locRight) - ); - case "<": - return ( - ensureInt(valLeft, locLeft) < - ensureInt(valRightContinuation(), locRight) - ); - case ">=": - return ( - ensureInt(valLeft, locLeft) >= - ensureInt(valRightContinuation(), locRight) - ); - case "<=": - return ( - ensureInt(valLeft, locLeft) <= - ensureInt(valRightContinuation(), locRight) - ); - case "==": { - const valR = valRightContinuation(); - - // the null comparisons account for optional types, e.g. - // a const x: Int? = 42 can be compared to null - if ( - typeof valLeft !== typeof valR && - valLeft !== null && - valR !== null - ) { - throwErrorConstEval( - "operands of `==` must have same type", - source, - ); - } - return valLeft === valR; - } - case "!=": { - const valR = valRightContinuation(); - if (typeof valLeft !== typeof valR) { - throwErrorConstEval( - "operands of `!=` must have same type", - source, - ); - } - return valLeft !== valR; - } - case "&&": - return ( - ensureBoolean(valLeft, locLeft) && - ensureBoolean(valRightContinuation(), locRight) - ); - case "||": - return ( - ensureBoolean(valLeft, locLeft) || - ensureBoolean(valRightContinuation(), locRight) - ); - } -} - -function interpretEscapeSequences(stringLiteral: string, source: SrcInfo) { - return stringLiteral.replace( - /\\\\|\\"|\\n|\\r|\\t|\\v|\\b|\\f|\\u{([0-9A-Fa-f]{1,6})}|\\u([0-9A-Fa-f]{4})|\\x([0-9A-Fa-f]{2})/g, - (match, unicodeCodePoint, unicodeEscape, hexEscape) => { - switch (match) { - case "\\\\": - return "\\"; - case '\\"': - return '"'; - case "\\n": - return "\n"; - case "\\r": - return "\r"; - case "\\t": - return "\t"; - case "\\v": - return "\v"; - case "\\b": - return "\b"; - case "\\f": - return "\f"; - default: - // Handle Unicode code point escape - if (unicodeCodePoint) { - const codePoint = parseInt(unicodeCodePoint, 16); - if (codePoint > 0x10ffff) { - throwErrorConstEval( - `unicode code point is outside of valid range 000000-10FFFF: ${stringLiteral}`, - source, - ); - } - return String.fromCodePoint(codePoint); - } - // Handle Unicode escape - if (unicodeEscape) { - const codeUnit = parseInt(unicodeEscape, 16); - return String.fromCharCode(codeUnit); - } - // Handle hex escape - if (hexEscape) { - const hexValue = parseInt(hexEscape, 16); - return String.fromCharCode(hexValue); - } - return match; - } - }, - ); -} - -class ReturnSignal extends Error { - private value?: Value; - - constructor(value?: Value) { - super(); - this.value = value; - } - - public getValue(): Value | undefined { - return this.value; - } -} - -export type InterpreterConfig = { - // Options that tune the interpreter's behavior. - - // 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; -}; - -const WILDCARD_NAME: string = "_"; - -type Environment = { values: Map; parent?: Environment }; - -class EnvironmentStack { - private currentEnv: Environment; - - constructor() { - this.currentEnv = { values: new Map() }; - } - - private findBindingMap(name: string): Map | undefined { - let env: Environment | undefined = this.currentEnv; - while (env !== undefined) { - if (env.values.has(name)) { - return env.values; - } else { - env = env.parent; - } - } - return undefined; - } - - /* - Sets a binding for "name" in the **current** environment of the stack. - If a binding for "name" already exists in the current environment, it - overwrites the binding with the provided value. - As a special case, name "_" is ignored. - - Note that this method does not check if binding "name" already exists in - a parent environment. - This means that if binding "name" already exists in a parent environment, - it will be shadowed by the provided value in the current environment. - This shadowing behavior is useful for modelling recursive function calls. - For example, consider the recursive implementation of factorial - (for simplification purposes, it returns 1 for the factorial of - negative numbers): - - 1 fun factorial(a: Int): Int { - 2 if (a <= 1) { - 3 return 1; - 4 } else { - 5 return a * factorial(a - 1); - 6 } - - Just before factorial(4) finishes its execution, the environment stack will - look as follows (the arrows point to their parent environment): - - a = 4 <------- a = 3 <-------- a = 2 <------- a = 1 - - Note how each child environment shadows variable a, because each - recursive call to factorial at line 5 creates a child - environment with a new binding for a. - - When factorial(1) = 1 finishes execution, the environment at the top - of the stack is popped: - - a = 4 <------- a = 3 <-------- a = 2 - - and execution resumes at line 5 in the environment where a = 2, - so that the return at line 5 is 2 * 1 = 2. - - This in turn causes the stack to pop the environment at the top: - - a = 4 <------- a = 3 - - so that the return at line 5 (now in the environment a = 3) will - produce 3 * 2 = 6, and so on. - */ - public setNewBinding(name: string, val: Value) { - if (name !== WILDCARD_NAME) { - this.currentEnv.values.set(name, val); - } - } - - /* - Searches the binding "name" in the stack, starting at the current - environment and moving towards the parent environments. - If it finds the binding, it updates its value - to "val". If it does not find "name", the stack is unchanged. - As a special case, name "_" is always ignored. - */ - public updateBinding(name: string, val: Value) { - if (name !== WILDCARD_NAME) { - const bindings = this.findBindingMap(name); - if (bindings !== undefined) { - bindings.set(name, val); - } - } - } - - /* - Searches the binding "name" in the stack, starting at the current - environment and moving towards the parent environments. - If it finds "name", it returns its value. - If it does not find "name", it returns undefined. - As a special case, name "_" always returns undefined. - */ - public getBinding(name: string): Value | undefined { - if (name === WILDCARD_NAME) { - return undefined; - } - const bindings = this.findBindingMap(name); - if (bindings !== undefined) { - return bindings.get(name); - } else { - return undefined; - } - } - - public selfInEnvironment(): boolean { - 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 - execution, the new environment is automatically popped from - the stack. - - This method is useful for starting a new local variables scope, - like in a function call. - */ - public executeInNewEnvironment( - code: () => T, - initialBindings: { names: string[]; values: Value[] } = { - names: [], - values: [], - }, - ): T { - const names = initialBindings.names; - const values = initialBindings.values; - - const oldEnv = this.currentEnv; - this.currentEnv = { values: new Map(), parent: oldEnv }; - - names.forEach((name, index) => { - this.setNewBinding(name, values[index]!); - }, this); - - try { - return code(); - } finally { - this.currentEnv = oldEnv; - } - } -} - export function parseAndEvalExpression(sourceCode: string): EvalResult { try { const ast = parseExpression(sourceCode); @@ -616,65 +66,8 @@ export function parseAndEvalExpression(sourceCode: string): EvalResult { } } -const defaultInterpreterConfig: InterpreterConfig = { - // We set the default max number of loop iterations - // to the maximum number allowed for repeat loops - maxLoopIterations: maxRepeatStatement, -}; - -/* -Interprets Tact AST trees. -The constructor receives an optional CompilerContext which includes -all external declarations that the interpreter will use during interpretation. -If no CompilerContext is provided, the interpreter will use an empty -CompilerContext. - -**IMPORTANT**: if a custom CompilerContext is provided, it should be the -CompilerContext provided by the typechecker. - -The reason for requiring a CompilerContext is that the interpreter should work -in the use case where the interpreter only knows part of the code. -For example, consider the following code (I marked with brackets [ ] the places -where the interpreter gets called during expression simplification in the -compilation phase): - -const C: Int = [1]; - -contract TestContract { - - get fun test(): Int { - return [C + 1]; - } -} - -When the interpreter gets called inside the brackets, it does not know what -other code is surrounding those brackets, because the interpreter did not execute the -code outside the brackets. Hence, it relies on the typechecker to receive the -CompilerContext that includes the declarations in the code -(the constant C for example). - -Since the interpreter relies on the typechecker, it assumes that the given AST tree -is already a valid Tact program. - -Internally, the interpreter uses a stack of environments to keep track of -variables at different scopes. Each environment in the stack contains a map -that binds a variable name to its corresponding value. -*/ -export class Interpreter { - private envStack: EnvironmentStack; - private context: CompilerContext; - private config: InterpreterConfig; - - constructor( - context: CompilerContext = new CompilerContext(), - config: InterpreterConfig = defaultInterpreterConfig, - ) { - this.envStack = new EnvironmentStack(); - this.context = context; - this.config = config; - } - - public interpretModuleItem(ast: AstModuleItem): void { +export abstract class InterpreterInterface { + public interpretModuleItem(ast: AstModuleItem) { switch (ast.kind) { case "constant_def": this.interpretConstantDef(ast); @@ -683,10 +76,7 @@ export class Interpreter { this.interpretFunctionDef(ast); break; case "asm_function_def": - throwNonFatalErrorConstEval( - "Asm functions are currently not supported.", - ast.loc, - ); + this.interpretAsmFunctionDef(ast); break; case "struct_decl": this.interpretStructDecl(ast); @@ -695,7 +85,7 @@ export class Interpreter { this.interpretMessageDecl(ast); break; case "native_function_decl": - this.interpretFunctionDecl(ast); + this.interpretNativeFunctionDecl(ast); break; case "primitive_type_decl": this.interpretPrimitiveTypeDecl(ast); @@ -709,63 +99,27 @@ export class Interpreter { } } - public interpretConstantDef(ast: AstConstantDef) { - throwNonFatalErrorConstEval( - "Constant definitions are currently not supported.", - ast.loc, - ); - } + public abstract interpretConstantDef(ast: AstConstantDef): void; - public interpretFunctionDef(ast: AstFunctionDef) { - throwNonFatalErrorConstEval( - "Function definitions are currently not supported.", - ast.loc, - ); - } + public abstract interpretFunctionDef(ast: AstFunctionDef): void; - public interpretStructDecl(ast: AstStructDecl) { - throwNonFatalErrorConstEval( - "Struct declarations are currently not supported.", - ast.loc, - ); - } + public abstract interpretAsmFunctionDef(ast: AstAsmFunctionDef): void; - public interpretMessageDecl(ast: AstMessageDecl) { - throwNonFatalErrorConstEval( - "Message declarations are currently not supported.", - ast.loc, - ); - } + public abstract interpretStructDecl(ast: AstStructDecl): void; - public interpretPrimitiveTypeDecl(ast: AstPrimitiveTypeDecl) { - throwNonFatalErrorConstEval( - "Primitive type declarations are currently not supported.", - ast.loc, - ); - } + public abstract interpretMessageDecl(ast: AstMessageDecl): void; - public interpretFunctionDecl(ast: AstNativeFunctionDecl) { - throwNonFatalErrorConstEval( - "Native function declarations are currently not supported.", - ast.loc, - ); - } + public abstract interpretPrimitiveTypeDecl(ast: AstPrimitiveTypeDecl): void; - public interpretContract(ast: AstContract) { - throwNonFatalErrorConstEval( - "Contract declarations are currently not supported.", - ast.loc, - ); - } + public abstract interpretNativeFunctionDecl( + ast: AstNativeFunctionDecl, + ): void; - public interpretTrait(ast: AstTrait) { - throwNonFatalErrorConstEval( - "Trait declarations are currently not supported.", - ast.loc, - ); - } + public abstract interpretContract(ast: AstContract): void; + + public abstract interpretTrait(ast: AstTrait): void; - public interpretExpression(ast: AstExpression): Value { + public interpretExpression(ast: AstExpression): T { switch (ast.kind) { case "id": return this.interpretName(ast); @@ -796,594 +150,33 @@ export class Interpreter { } } - public interpretName(ast: AstId): Value { - if (hasStaticConstant(this.context, idText(ast))) { - const constant = getStaticConstant(this.context, idText(ast)); - if (constant.value !== undefined) { - return constant.value; - } else { - throwErrorConstEval( - `cannot evaluate declared constant ${idTextErr(ast)} as it does not have a body`, - ast.loc, - ); - } - } - const variableBinding = this.envStack.getBinding(idText(ast)); - if (variableBinding !== undefined) { - return variableBinding; - } - throwNonFatalErrorConstEval("cannot evaluate a variable", ast.loc); - } + public abstract interpretName(ast: AstId): T; - public interpretMethodCall(ast: AstMethodCall): Value { - switch (idText(ast.method)) { - case "asComment": { - ensureMethodArity(0, ast.args, ast.loc); - const comment = ensureString( - this.interpretExpression(ast.self), - ast.self.loc, - ); - return new CommentValue(comment); - } - default: - throwNonFatalErrorConstEval( - `calls of ${idTextErr(ast.method)} are not supported at this moment`, - ast.loc, - ); - } - } + public abstract interpretMethodCall(ast: AstMethodCall): T; - public interpretInitOf(ast: AstInitOf): Value { - throwNonFatalErrorConstEval( - "initOf is not supported at this moment", - ast.loc, - ); - } + public abstract interpretInitOf(ast: AstInitOf): T; - public interpretNull(_ast: AstNull): null { - return null; - } + public abstract interpretNull(ast: AstNull): T; - public interpretBoolean(ast: AstBoolean): boolean { - return ast.value; - } + public abstract interpretBoolean(ast: AstBoolean): T; - public interpretNumber(ast: AstNumber): bigint { - return ensureInt(ast.value, ast.loc); - } - - public interpretString(ast: AstString): string { - return ensureString( - interpretEscapeSequences(ast.value, ast.loc), - ast.loc, - ); - } - - public interpretUnaryOp(ast: AstOpUnary): Value { - // Tact grammar does not have negative integer literals, - // so in order to avoid errors for `-115792089237316195423570985008687907853269984665640564039457584007913129639936` - // which is `-(2**256)` we need to have a special case for it - - if (ast.operand.kind === "number" && ast.op === "-") { - // emulating negative integer literals - return ensureInt(-ast.operand.value, ast.loc); - } - - const valOperand = this.interpretExpression(ast.operand); - - return evalUnaryOp(ast.op, valOperand, ast.operand.loc, ast.loc); - } + public abstract interpretNumber(ast: AstNumber): T; - public interpretBinaryOp(ast: AstOpBinary): Value { - const valLeft = this.interpretExpression(ast.left); - const valRightContinuation = () => this.interpretExpression(ast.right); + public abstract interpretString(ast: AstString): T; - return evalBinaryOp( - ast.op, - valLeft, - valRightContinuation, - ast.left.loc, - ast.right.loc, - ast.loc, - ); - } + public abstract interpretUnaryOp(ast: AstOpUnary): T; - public interpretConditional(ast: AstConditional): Value { - // here we rely on the typechecker that both branches have the same type - const valCond = ensureBoolean( - this.interpretExpression(ast.condition), - ast.condition.loc, - ); - if (valCond) { - return this.interpretExpression(ast.thenBranch); - } else { - return this.interpretExpression(ast.elseBranch); - } - } + public abstract interpretBinaryOp(ast: AstOpBinary): T; - public interpretStructInstance(ast: AstStructInstance): StructValue { - const structTy = getType(this.context, ast.type); + public abstract interpretConditional(ast: AstConditional): T; - // initialize the resulting struct value with - // the default values for fields with initializers - // or null for uninitialized optional fields - const resultWithDefaultFields: StructValue = structTy.fields.reduce( - (resObj, field) => { - if (field.default !== undefined) { - resObj[field.name] = field.default; - } else { - if (field.type.kind === "ref" && field.type.optional) { - resObj[field.name] = null; - } - } - return resObj; - }, - { $tactStruct: idText(ast.type) } as StructValue, - ); + public abstract interpretStructInstance(ast: AstStructInstance): T; - // this will override default fields set above - return ast.args.reduce((resObj, fieldWithInit) => { - resObj[idText(fieldWithInit.field)] = this.interpretExpression( - fieldWithInit.initializer, - ); - return resObj; - }, resultWithDefaultFields); - } + public abstract interpretFieldAccess(ast: AstFieldAccess): T; - public interpretFieldAccess(ast: AstFieldAccess): Value { - // special case for contract/trait constant accesses via `self.constant` - // interpret "self" as a contract/trait access only if "self" - // is not already assigned in the environment (this would mean - // we are executing inside an extends function) - if ( - ast.aggregate.kind === "id" && - isSelfId(ast.aggregate) && - !this.envStack.selfInEnvironment() - ) { - const selfTypeRef = getExpType(this.context, ast.aggregate); - if (selfTypeRef.kind === "ref") { - const contractTypeDescription = getType( - this.context, - selfTypeRef.name, - ); - const foundContractConst = - contractTypeDescription.constants.find((constId) => - eqNames(ast.field, constId.name), - ); - if (foundContractConst === undefined) { - // not a constant, e.g. `self.storageVariable` - throwNonFatalErrorConstEval( - "cannot evaluate non-constant self field access", - ast.aggregate.loc, - ); - } - if (foundContractConst.value !== undefined) { - return foundContractConst.value; - } else { - throwErrorConstEval( - `cannot evaluate declared contract/trait constant ${idTextErr(ast.field)} as it does not have a body`, - ast.field.loc, - ); - } - } - } - const valStruct = this.interpretExpression(ast.aggregate); - if ( - valStruct === null || - typeof valStruct !== "object" || - !("$tactStruct" in valStruct) - ) { - throwErrorConstEval( - `constant struct expected, but got ${showValue(valStruct)}`, - ast.aggregate.loc, - ); - } - if (idText(ast.field) in valStruct) { - return valStruct[idText(ast.field)]!; - } else { - // this cannot happen in a well-typed program - throwInternalCompilerError( - `struct field ${idTextErr(ast.field)} is missing`, - ast.aggregate.loc, - ); - } - } + public abstract interpretStaticCall(ast: AstStaticCall): T; - public interpretStaticCall(ast: AstStaticCall): Value { - switch (idText(ast.function)) { - case "ton": { - ensureFunArity(1, ast.args, ast.loc); - const tons = ensureString( - this.interpretExpression(ast.args[0]!), - ast.args[0]!.loc, - ); - try { - return ensureInt( - BigInt(toNano(tons).toString(10)), - ast.loc, - ); - } catch (e) { - if (e instanceof Error && e.message === "Invalid number") { - throwErrorConstEval( - `invalid ${idTextErr(ast.function)} argument`, - ast.loc, - ); - } - throw e; - } - } - case "pow": { - ensureFunArity(2, ast.args, ast.loc); - const valBase = ensureInt( - this.interpretExpression(ast.args[0]!), - ast.args[0]!.loc, - ); - const valExp = ensureInt( - this.interpretExpression(ast.args[1]!), - ast.args[1]!.loc, - ); - if (valExp < 0n) { - throwErrorConstEval( - `${idTextErr(ast.function)} builtin called with negative exponent ${valExp}`, - ast.loc, - ); - } - try { - return ensureInt(valBase ** valExp, ast.loc); - } catch (e) { - if (e instanceof RangeError) { - // even TS bigint type cannot hold it - throwErrorConstEval( - "integer does not fit into TVM Int type", - ast.loc, - ); - } - throw e; - } - } - case "pow2": { - ensureFunArity(1, ast.args, ast.loc); - const valExponent = ensureInt( - this.interpretExpression(ast.args[0]!), - ast.args[0]!.loc, - ); - if (valExponent < 0n) { - throwErrorConstEval( - `${idTextErr(ast.function)} builtin called with negative exponent ${valExponent}`, - ast.loc, - ); - } - try { - return ensureInt(2n ** valExponent, ast.loc); - } catch (e) { - if (e instanceof RangeError) { - // even TS bigint type cannot hold it - throwErrorConstEval( - "integer does not fit into TVM Int type", - ast.loc, - ); - } - throw e; - } - } - case "sha256": { - ensureFunArity(1, ast.args, ast.loc); - const expr = this.interpretExpression(ast.args[0]!); - if (expr instanceof Slice) { - throwNonFatalErrorConstEval( - "slice argument is currently not supported", - ast.loc, - ); - } - const str = ensureString(expr, ast.args[0]!.loc); - return BigInt("0x" + sha256_sync(str).toString("hex")); - } - case "emptyMap": { - ensureFunArity(0, ast.args, ast.loc); - return null; - } - case "cell": - { - ensureFunArity(1, ast.args, ast.loc); - const str = ensureString( - this.interpretExpression(ast.args[0]!), - ast.args[0]!.loc, - ); - try { - return Cell.fromBase64(str); - } catch (_) { - throwErrorConstEval( - `invalid base64 encoding for a cell: ${str}`, - ast.loc, - ); - } - } - break; - case "slice": - { - ensureFunArity(1, ast.args, ast.loc); - const str = ensureString( - this.interpretExpression(ast.args[0]!), - ast.args[0]!.loc, - ); - try { - return Cell.fromBase64(str).asSlice(); - } catch (_) { - throwErrorConstEval( - `invalid base64 encoding for a cell: ${str}`, - ast.loc, - ); - } - } - break; - case "rawSlice": - { - ensureFunArity(1, ast.args, ast.loc); - const str = ensureString( - this.interpretExpression(ast.args[0]!), - ast.args[0]!.loc, - ); - - if (!/^[0-9a-fA-F]*_?$/.test(str)) { - throwErrorConstEval( - `invalid hex string: ${str}`, - ast.loc, - ); - } - - // Remove underscores from the hex string - const hex = str.replace("_", ""); - const paddedHex = hex.length % 2 === 0 ? hex : "0" + hex; - const buffer = Buffer.from(paddedHex, "hex"); - - // Initialize the BitString - let bits = new BitString( - buffer, - hex.length % 2 === 0 ? 0 : 4, - hex.length * 4, - ); - - // Handle the case where the string ends with an underscore - if (str.endsWith("_")) { - const paddedBits = paddedBufferToBits(buffer); - - // Ensure there's enough length to apply the offset - const offset = hex.length % 2 === 0 ? 0 : 4; - if (paddedBits.length >= offset) { - bits = paddedBits.substring( - offset, - paddedBits.length - offset, - ); - } else { - bits = new BitString(Buffer.from(""), 0, 0); - } - } - - // Ensure the bit length is within acceptable limits - if (bits.length > 1023) { - throwErrorConstEval( - `slice constant is too long, expected up to 1023 bits, got ${bits.length}`, - ast.loc, - ); - } - - // Return the constructed slice - return beginCell().storeBits(bits).endCell().asSlice(); - } - break; - case "ascii": - { - ensureFunArity(1, ast.args, ast.loc); - const str = ensureString( - this.interpretExpression(ast.args[0]!), - ast.args[0]!.loc, - ); - const hex = Buffer.from(str).toString("hex"); - if (hex.length > 64) { - throwErrorConstEval( - `ascii string is too long, expected up to 32 bytes, got ${Math.floor(hex.length / 2)}`, - ast.loc, - ); - } - if (hex.length == 0) { - throwErrorConstEval( - `ascii string cannot be empty`, - ast.loc, - ); - } - return BigInt("0x" + hex); - } - break; - case "crc32": - { - ensureFunArity(1, ast.args, ast.loc); - const str = ensureString( - this.interpretExpression(ast.args[0]!), - ast.args[0]!.loc, - ); - return BigInt(crc32.str(str) >>> 0); // >>> 0 converts to unsigned - } - break; - case "address": - { - ensureFunArity(1, ast.args, ast.loc); - const str = ensureString( - this.interpretExpression(ast.args[0]!), - ast.args[0]!.loc, - ); - try { - const address = Address.parse(str); - if ( - address.workChain !== 0 && - address.workChain !== -1 - ) { - throwErrorConstEval( - `${str} is invalid address`, - ast.loc, - ); - } - if ( - !enabledMasterchain(this.context) && - address.workChain !== 0 - ) { - throwErrorConstEval( - `address ${str} is from masterchain which is not enabled for this contract`, - ast.loc, - ); - } - return address; - } catch (_) { - throwErrorConstEval( - `invalid address encoding: ${str}`, - ast.loc, - ); - } - } - break; - case "newAddress": { - ensureFunArity(2, ast.args, ast.loc); - const wc = ensureInt( - this.interpretExpression(ast.args[0]!), - ast.args[0]!.loc, - ); - const addr = Buffer.from( - ensureInt( - this.interpretExpression(ast.args[1]!), - ast.args[1]!.loc, - ) - .toString(16) - .padStart(64, "0"), - "hex", - ); - if (wc !== 0n && wc !== -1n) { - throwErrorConstEval( - `expected workchain of an address to be equal 0 or -1, received: ${wc}`, - ast.loc, - ); - } - if (!enabledMasterchain(this.context) && wc !== 0n) { - throwErrorConstEval( - `${wc}:${addr.toString("hex")} address is from masterchain which is not enabled for this contract`, - ast.loc, - ); - } - return new Address(Number(wc), addr); - } - default: - if (hasStaticFunction(this.context, idText(ast.function))) { - const functionDescription = getStaticFunction( - this.context, - idText(ast.function), - ); - switch (functionDescription.ast.kind) { - case "function_def": - // Currently, no attribute is supported - if (functionDescription.ast.attributes.length > 0) { - throwNonFatalErrorConstEval( - "calls to functions with attributes are currently not supported", - ast.loc, - ); - } - return this.evalStaticFunction( - functionDescription.ast, - ast.args, - functionDescription.returns, - ); - - case "asm_function_def": - throwNonFatalErrorConstEval( - `${idTextErr(ast.function)} cannot be interpreted because it's an asm-function`, - ast.loc, - ); - break; - case "function_decl": - throwNonFatalErrorConstEval( - `${idTextErr(ast.function)} cannot be interpreted because it does not have a body`, - ast.loc, - ); - break; - case "native_function_decl": - throwNonFatalErrorConstEval( - "native function calls are currently not supported", - ast.loc, - ); - break; - } - } else { - throwNonFatalErrorConstEval( - `function ${idTextErr(ast.function)} is not declared`, - ast.loc, - ); - } - } - } - - private evalStaticFunction( - functionCode: AstFunctionDef, - args: AstExpression[], - returns: TypeRef, - ): Value { - // Evaluate the arguments in the current environment - const argValues = args.map(this.interpretExpression, this); - // Extract the parameter names - const paramNames = functionCode.params.map((param) => - idText(param.name), - ); - // Check parameter names do not shadow constants - if ( - paramNames.some((paramName) => - hasStaticConstant(this.context, paramName), - ) - ) { - throwInternalCompilerError( - `some parameter of function ${idText(functionCode.name)} shadows a constant with the same name`, - functionCode.loc, - ); - } - // Call function inside a new environment - return this.envStack.executeInNewEnvironment( - () => { - // Interpret all the statements - try { - functionCode.statements.forEach( - this.interpretStatement, - this, - ); - // At this point, the function did not execute a return. - // Execution continues after the catch. - } catch (e) { - if (e instanceof ReturnSignal) { - const val = e.getValue(); - if (val !== undefined) { - return val; - } - // The function executed a return without a value. - // Execution continues after the catch. - } else { - throw e; - } - } - // If execution reaches this point, it means that - // the function had no return statement or executed a return - // without a value. This is an error only if the return type of the - // function is not void - if (returns.kind !== "void") { - throwInternalCompilerError( - `function ${idText(functionCode.name)} must return a value`, - functionCode.loc, - ); - } else { - // The function does not return a value. - // We rely on the typechecker so that the function is called as a statement. - // Hence, we can return a dummy null, since the null will be discarded anyway. - return null; - } - }, - { names: paramNames, values: argValues }, - ); - } - - public interpretStatement(ast: AstStatement): void { + public interpretStatement(ast: AstStatement) { switch (ast.kind) { case "statement_let": this.interpretLetStatement(ast); @@ -1427,225 +220,39 @@ export class Interpreter { } } - public interpretLetStatement(ast: AstStatementLet) { - if (hasStaticConstant(this.context, idText(ast.name))) { - // Attempt of shadowing a constant in a let declaration - throwInternalCompilerError( - `declaration of ${idText(ast.name)} shadows a constant with the same name`, - ast.loc, - ); - } - const val = this.interpretExpression(ast.expression); - this.envStack.setNewBinding(idText(ast.name), val); - } - - public interpretDestructStatement(ast: AstStatementDestruct) { - for (const [_, name] of ast.identifiers.values()) { - if (hasStaticConstant(this.context, idText(name))) { - // Attempt of shadowing a constant in a destructuring declaration - throwInternalCompilerError( - `declaration of ${idText(name)} shadows a constant with the same name`, - ast.loc, - ); - } - } - const val = this.interpretExpression(ast.expression); - if ( - val === null || - typeof val !== "object" || - !("$tactStruct" in val) - ) { - throwErrorConstEval( - `destructuring assignment expected a struct, but got ${showValue( - val, - )}`, - ast.expression.loc, - ); - } + public abstract interpretLetStatement(ast: AstStatementLet): void; - for (const [field, name] of ast.identifiers.values()) { - if (name.text === "_") { - continue; - } - const v = val[idText(field)]; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (v === undefined) { - throwErrorConstEval( - `destructuring assignment expected field ${idTextErr( - field, - )}`, - ast.loc, - ); - } - this.envStack.setNewBinding(idText(name), v); - } - } + public abstract interpretDestructStatement(ast: AstStatementDestruct): void; - public interpretAssignStatement(ast: AstStatementAssign) { - if (ast.path.kind === "id") { - const val = this.interpretExpression(ast.expression); - this.envStack.updateBinding(idText(ast.path), val); - } else { - throwNonFatalErrorConstEval( - "only identifiers are currently supported as path expressions", - ast.path.loc, - ); - } - } + public abstract interpretAssignStatement(ast: AstStatementAssign): void; - public interpretAugmentedAssignStatement(ast: AstStatementAugmentedAssign) { - if (ast.path.kind === "id") { - const updateVal = () => this.interpretExpression(ast.expression); - const currentPathValue = this.envStack.getBinding(idText(ast.path)); - if (currentPathValue === undefined) { - throwNonFatalErrorConstEval( - "undeclared identifier", - ast.path.loc, - ); - } - const newVal = evalBinaryOp( - ast.op, - currentPathValue, - updateVal, - ast.path.loc, - ast.expression.loc, - ast.loc, - ); - this.envStack.updateBinding(idText(ast.path), newVal); - } else { - throwNonFatalErrorConstEval( - "only identifiers are currently supported as path expressions", - ast.path.loc, - ); - } - } - - public interpretConditionStatement(ast: AstCondition) { - const condition = ensureBoolean( - this.interpretExpression(ast.condition), - ast.condition.loc, - ); - if (condition) { - this.envStack.executeInNewEnvironment(() => { - ast.trueStatements.forEach(this.interpretStatement, this); - }); - } else if (ast.falseStatements !== null) { - this.envStack.executeInNewEnvironment(() => { - ast.falseStatements!.forEach(this.interpretStatement, this); - }); - } - } + public abstract interpretAugmentedAssignStatement( + ast: AstStatementAugmentedAssign, + ): void; - public interpretExpressionStatement(ast: AstStatementExpression) { - this.interpretExpression(ast.expression); - } + public abstract interpretConditionStatement(ast: AstCondition): void; - public interpretForEachStatement(ast: AstStatementForEach) { - throwNonFatalErrorConstEval("foreach currently not supported", ast.loc); - } + public abstract interpretExpressionStatement( + ast: AstStatementExpression, + ): void; - public interpretRepeatStatement(ast: AstStatementRepeat) { - const iterations = ensureRepeatInt( - this.interpretExpression(ast.iterations), - ast.iterations.loc, - ); - if (iterations > 0) { - // We can create a single environment for all the iterations in the loop - // (instead of a fresh environment for each iteration) - // because the typechecker ensures that variables do not leak outside - // 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(() => { - for (let i = 1; i <= iterations; i++) { - ast.statements.forEach(this.interpretStatement, this); - } - }); - } - } + public abstract interpretForEachStatement(ast: AstStatementForEach): void; - public interpretReturnStatement(ast: AstStatementReturn) { - if (ast.expression !== null) { - const val = this.interpretExpression(ast.expression); - throw new ReturnSignal(val); - } else { - throw new ReturnSignal(); - } - } + public abstract interpretRepeatStatement(ast: AstStatementRepeat): void; - public interpretTryStatement(ast: AstStatementTry) { - throwNonFatalErrorConstEval( - "try statements currently not supported", - ast.loc, - ); - } + public abstract interpretReturnStatement(ast: AstStatementReturn): void; - public interpretTryCatchStatement(ast: AstStatementTryCatch) { - throwNonFatalErrorConstEval( - "try-catch statements currently not supported", - ast.loc, - ); - } + public abstract interpretTryStatement(ast: AstStatementTry): void; - public interpretUntilStatement(ast: AstStatementUntil) { - let condition; - let iterCount = 0; - // We can create a single environment for all the iterations in the loop - // (instead of a fresh environment for each iteration) - // because the typechecker ensures that variables do not leak outside - // 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(() => { - do { - ast.statements.forEach(this.interpretStatement, this); + public abstract interpretTryCatchStatement(ast: AstStatementTryCatch): void; - iterCount++; - if (iterCount >= this.config.maxLoopIterations) { - throwNonFatalErrorConstEval( - "loop timeout reached", - ast.loc, - ); - } - // The typechecker ensures that the condition does not refer to - // variables declared inside the loop. - condition = ensureBoolean( - this.interpretExpression(ast.condition), - ast.condition.loc, - ); - } while (!condition); - }); - } + public abstract interpretUntilStatement(ast: AstStatementUntil): void; - public interpretWhileStatement(ast: AstStatementWhile) { - let condition; - let iterCount = 0; - // We can create a single environment for all the iterations in the loop - // (instead of a fresh environment for each iteration) - // because the typechecker ensures that variables do not leak outside - // 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(() => { - do { - // The typechecker ensures that the condition does not refer to - // variables declared inside the loop. - condition = ensureBoolean( - this.interpretExpression(ast.condition), - ast.condition.loc, - ); - if (condition) { - ast.statements.forEach(this.interpretStatement, this); + public abstract interpretWhileStatement(ast: AstStatementWhile): void; - iterCount++; - if (iterCount >= this.config.maxLoopIterations) { - throwNonFatalErrorConstEval( - "loop timeout reached", - ast.loc, - ); - } - } - } while (condition); - }); + protected executeStatements(statements: AstStatement[]) { + statements.forEach((currStmt) => { + this.interpretStatement(currStmt); + }, this); } } diff --git a/src/interpreters/constantPropagation.ts b/src/interpreters/constantPropagation.ts new file mode 100644 index 000000000..080bfb1cf --- /dev/null +++ b/src/interpreters/constantPropagation.ts @@ -0,0 +1,1523 @@ +import { MapFunctions } from "../abi/map"; +import { StructFunctions } from "../abi/struct"; +import { partiallyEvalExpression } from "../constEval"; +import { CompilerContext } from "../context"; +import { TactConstEvalError, throwInternalCompilerError } from "../errors"; +import { + AstId, + AstMethodCall, + AstNull, + AstBoolean, + AstNumber, + AstString, + AstOpUnary, + AstOpBinary, + AstStructInstance, + AstFieldAccess, + AstStaticCall, + AstFunctionDef, + AstStatementAugmentedAssign, + AstStatementLet, + AstStatement, + AstStatementAssign, + tryExtractPath, + AstCondition, + AstStatementExpression, + AstStatementForEach, + AstStatementRepeat, + AstStatementReturn, + AstStatementTry, + AstStatementTryCatch, + AstStatementUntil, + AstStatementWhile, + AstExpression, + idText, + AstAsmFunctionDef, + AstConstantDef, + AstContract, + AstMessageDecl, + AstNativeFunctionDecl, + AstPrimitiveTypeDecl, + AstStructDecl, + AstTrait, + SrcInfo, + AstFunctionDecl, + AstContractInit, + AstReceiver, + AstConditional, + AstInitOf, + isValue, + AstValue, + AstStatementDestruct, +} from "../grammar/ast"; +import { InterpreterInterface } from "../interpreter"; +import { extractValue } from "../optimizer/util"; +import { + getAllStaticFunctions, + getAllTypes, + getType, +} from "../types/resolveDescriptors"; +import { getExpType } from "../types/resolveExpression"; +import { + copyValue, + eqValues, + StructValue, + TypeRef, + Value, +} from "../types/types"; +import { + defaultInterpreterConfig, + ensureBoolean, + ensureRepeatInt, + Environment, + EnvironmentStack, + InterpreterConfig, + TactInterpreter, +} from "./standard"; + +class UndefinedValueSignal extends Error {} + +/* +This corresponds to the following lattice: + + any_value + / | \ + val1 val2 val3 ........ + \ | / + no_value +*/ +type LatticeValue = + | { + value: Value; + kind: "value"; + } + | { + kind: "any_value"; // This is the top element + } + | { + kind: "no_value"; // This is the bottom element + }; + +function toLatticeValue(val: Value | undefined): LatticeValue { + if (val !== undefined) { + return { value: val, kind: "value" }; + } else { + return anyValue; + } +} + +function eqLatticeValues(val1: LatticeValue, val2: LatticeValue): boolean { + if (val1.kind === "value" && val2.kind === "value") { + return eqValues(val1.value, val2.value); + } else { + return val1.kind === val2.kind; + } +} + +const anyValue: LatticeValue = { kind: "any_value" }; + +function joinLatticeValues( + val1: LatticeValue, + val2: LatticeValue, +): LatticeValue { + if (val1.kind === "any_value" || val2.kind === "any_value") { + return anyValue; + } else if (val1.kind === "no_value") { + return val2; + } else if (val2.kind === "no_value") { + return val1; + } else { + const commonSubValue = extractCommonSubValue(val1.value, val2.value); + if (commonSubValue !== undefined) { + return toLatticeValue(commonSubValue); + } else { + return anyValue; + } + } +} + +function copyLatticeValue(val: LatticeValue): LatticeValue { + if (val.kind !== "value") { + return val; + } else { + return toLatticeValue(copyValue(val.value)); + } +} + +// The following constants store all the ABI functions known by the analyzer. +// We need to keep them like this because ABI functions are not registered +// in the CompilerContext, and so, it is not possible to determine which +// ABI functions are mutation functions and which are not. +// So, we need to state that info explicitly. + +const knownStructABIFunctions = ["toCell", "fromCell", "toSlice", "fromSlice"]; +const knownStructABIMutationFunctions: string[] = []; +const knownMapABIFunctions = [ + "set", + "get", + "del", + "asCell", + "isEmpty", + "exists", + "deepEquals", + "replace", + "replaceGet", +]; +const knownMapABIMutationFunctions = ["set", "del", "replace", "replaceGet"]; + +export class ConstantPropagationAnalyzer extends InterpreterInterface { + protected interpreter: TactInterpreter; + protected envStack: EnvironmentStack; + protected context: CompilerContext; + protected config: InterpreterConfig; + + constructor( + context: CompilerContext = new CompilerContext(), + config: InterpreterConfig = defaultInterpreterConfig, + ) { + super(); + this.context = context; + this.config = config; + this.envStack = new EnvironmentStack(copyLatticeValue); + this.interpreter = new TactInterpreter(context, config); + } + + public startAnalysis() { + // Check that the ABI Functions known by the analyzer are still the ones in StructFunctions and MapFunctions + if ( + StructFunctions.size !== knownStructABIFunctions.length || + knownStructABIFunctions.some((name) => !StructFunctions.has(name)) + ) { + throwInternalCompilerError( + "There are new Struct ABI functions unknown to the Constant Propagation Analyzer. Please add them to the Constant Propagation Analyzer.", + ); + } + + if ( + MapFunctions.size !== knownMapABIFunctions.length || + knownMapABIFunctions.some((name) => !MapFunctions.has(name)) + ) { + throwInternalCompilerError( + "There are new Map ABI functions unknown to the Constant Propagation Analyzer. Please add them to the Constant Propagation Analyzer.", + ); + } + + this.envStack = new EnvironmentStack(copyLatticeValue); + + // Process all functions + for (const f of getAllStaticFunctions(this.context)) { + if (f.ast.kind === "function_def") { + this.interpretFunctionDef(f.ast); + } else if (f.ast.kind === "native_function_decl") { + this.interpretNativeFunctionDecl(f.ast); + } else if (f.ast.kind === "asm_function_def") { + this.interpretAsmFunctionDef(f.ast); + } else { + this.interpretFunctionDecl(f.ast); + } + } + + // Process all types + for (const t of getAllTypes(this.context)) { + // Process init + if (t.init) { + // Prepare the self struct (contracts are treated as structs by the analyzer) + const selfStruct: StructValue = { $tactStruct: t.name }; + for (const f of t.fields) { + if (f.default !== undefined) { + selfStruct[f.name] = f.default; + } + } + + // Include also the constants + for (const c of t.constants) { + if (c.value !== undefined) { + selfStruct[c.name] = c.value; + } + } + + this.envStack.setNewBinding("self", toLatticeValue(selfStruct)); + + this.interpretInitDef(t.init.ast); + } + + // Update again the self variable for all the following + // receivers and functions. They will all need a fresh self + // that includes all the constants. + + // Process receivers + for (const r of t.receivers) { + const selfStruct: StructValue = { $tactStruct: t.name }; + for (const c of t.constants) { + if (c.value !== undefined) { + selfStruct[c.name] = c.value; + } + } + + this.envStack.setNewBinding("self", toLatticeValue(selfStruct)); + + this.interpretReceiver(r.ast); + } + + // Process methods + for (const m of t.functions.values()) { + if (t.kind === "contract" || t.kind === "trait") { + // Attach a self variable + const selfStruct: StructValue = { $tactStruct: t.name }; + for (const c of t.constants) { + if (c.value !== undefined) { + selfStruct[c.name] = c.value; + } + } + + this.envStack.setNewBinding( + "self", + toLatticeValue(selfStruct), + ); + } else { + // reset the self variable + this.envStack.setNewBinding("self", anyValue); + } + + if (m.ast.kind === "function_def") { + this.interpretFunctionDef(m.ast); + } else if (m.ast.kind === "native_function_decl") { + this.interpretNativeFunctionDecl(m.ast); + } else if (m.ast.kind === "asm_function_def") { + this.interpretAsmFunctionDef(m.ast); + } else { + this.interpretFunctionDecl(m.ast); + } + } + } + } + + public interpretFunctionDef(ast: AstFunctionDef) { + // The arguments are all undetermined. + const argNames = ast.params.map((param) => idText(param.name)); + const argValues = ast.params.map((_) => anyValue); + + this.envStack.executeInNewEnvironment( + () => { + this.executeStatements(ast.statements); + }, + { names: argNames, values: argValues }, + ); + } + + public interpretAsmFunctionDef(_ast: AstAsmFunctionDef) { + // Currently not supported. Do nothing + } + + public interpretInitDef(ast: AstContractInit) { + // The arguments are all undetermined. + const argNames = ast.params.map((param) => idText(param.name)); + const argValues = ast.params.map((_) => anyValue); + + this.envStack.executeInNewEnvironment( + () => { + this.executeStatements(ast.statements); + }, + { names: argNames, values: argValues }, + ); + } + + public interpretNativeFunctionDecl(_ast: AstNativeFunctionDecl) { + // Currently not supported. Do nothing + } + + public interpretFunctionDecl(_ast: AstFunctionDecl) { + // Do nothing + } + + public interpretReceiver(ast: AstReceiver) { + switch (ast.selector.kind) { + case "internal-simple": + case "bounce": + case "external-simple": { + // The only argument is undetermined. + const argName = idText(ast.selector.param.name); + this.envStack.executeInNewEnvironment( + () => { + this.executeStatements(ast.statements); + }, + { names: [argName], values: [anyValue] }, + ); + + break; + } + case "external-comment": + case "external-fallback": + case "internal-comment": + case "internal-fallback": + // These do not have a named argument + this.envStack.executeInNewEnvironment(() => { + this.executeStatements(ast.statements); + }); + break; + } + } + + /* These methods are required by the interpreter interface, but are not used by the analyzer. + They are already subsumed by the startAnalysis method. */ + + public interpretConstantDef(_ast: AstConstantDef) {} + + public interpretStructDecl(_ast: AstStructDecl) {} + + public interpretMessageDecl(_ast: AstMessageDecl) {} + + public interpretPrimitiveTypeDecl(_ast: AstPrimitiveTypeDecl) {} + + public interpretContract(_ast: AstContract) {} + + public interpretTrait(_ast: AstTrait) {} + + /* Required but not used methods end here */ + + public interpretName(ast: AstId): LatticeValue { + return toLatticeValue( + this.prepareForStandardInterpreter(() => + this.interpreter.interpretName(ast), + ), + ); + } + + public interpretMethodCall(ast: AstMethodCall): LatticeValue { + // For the moment do not analyze. Just treat all mutation function calls as black boxes + // that could assign to their self argument any value. + + // Also, evaluate all the arguments, just to check for errors. + this.interpretExpression(ast.self); + ast.args.forEach((expr) => this.interpretExpression(expr), this); + + // Now, undefine the path if assigned by a mutation function + const path = tryExtractPath(ast.self); + if (path !== null) { + const src = getExpType(this.context, ast.self); + + if (src.kind === "ref") { + const srcT = getType(this.context, src.name); + if (srcT.kind === "struct") { + if ( + knownStructABIMutationFunctions.includes( + idText(ast.method), + ) + ) { + this.updateBinding( + path, + ast.self, + anyValue, + ast.self.loc, + ); + } + } + + const f = srcT.functions.get(idText(ast.method))?.isMutating; + if (f) { + this.updateBinding(path, ast.self, anyValue, ast.self.loc); + } + } + + if (src.kind === "map") { + if (knownMapABIMutationFunctions.includes(idText(ast.method))) { + this.updateBinding(path, ast.self, anyValue, ast.self.loc); + } + } + } + // If the ast.self is not a path expression, i.e., it has the form: a.b.f().g()... + // then there is nothing to update in the environment because a.b.f().g() is not a full path to a variable. + + // Since we are not analyzing the function, just return that it could have produced any value. + return anyValue; + } + + public interpretInitOf(ast: AstInitOf): LatticeValue { + // Currently not supported. + + // Just evaluate the arguments, but do nothing else + ast.args.forEach((expr) => this.interpretExpression(expr), this); + + return anyValue; + } + + public interpretNull(ast: AstNull): LatticeValue { + return toLatticeValue( + this.prepareForStandardInterpreter(() => + this.interpreter.interpretNull(ast), + ), + ); + } + + public interpretBoolean(ast: AstBoolean): LatticeValue { + return toLatticeValue( + this.prepareForStandardInterpreter(() => + this.interpreter.interpretBoolean(ast), + ), + ); + } + + public interpretNumber(ast: AstNumber): LatticeValue { + return toLatticeValue( + this.prepareForStandardInterpreter(() => + this.interpreter.interpretNumber(ast), + ), + ); + } + + public interpretString(ast: AstString): LatticeValue { + return toLatticeValue( + this.prepareForStandardInterpreter(() => + this.interpreter.interpretString(ast), + ), + ); + } + + public interpretUnaryOp(ast: AstOpUnary): LatticeValue { + const operandEvaluator = () => { + const result = this.interpretExpression(ast.operand); + if (result.kind !== "value") { + throw new UndefinedValueSignal(); + } + return result.value; + }; + + return toLatticeValue( + this.prepareForStandardInterpreter(() => + this.interpreter.evalUnaryOp(ast, operandEvaluator), + ), + ); + } + + public interpretBinaryOp(ast: AstOpBinary): LatticeValue { + const leftValue = this.interpretExpression(ast.left); + + // Process the rest of the operators, they do not short-circuit + + if (leftValue.kind === "value") { + const rightEvaluator = () => { + const result = this.interpretExpression(ast.right); + if (result.kind !== "value") { + throw new UndefinedValueSignal(); + } + return result.value; + }; + + return toLatticeValue( + this.prepareForStandardInterpreter(() => + this.interpreter.evalBinaryOp( + ast, + leftValue.value, + rightEvaluator, + ), + ), + ); + } else { + // Operators || and && must be processed differently, because they short-circuit. + // Essentially, || and && produce two potential branches, because the left operand is undetermined. + // One branch is the "do nothing" branch, and the other one is the processing of the right operand. + if (ast.op === "||" || ast.op === "&&") { + const rightEnv = this.envStack.simulate(() => + this.interpretExpression(ast.right), + ).env; + + // Join the environments + this.envStack.setCurrentEnvironment( + joinEnvironments([ + rightEnv, + this.envStack.getCurrentEnvironment(), + ]), + ); + } else { + // The rest of the operators do not short-circuit, so simply process the right operand + this.interpretExpression(ast.right); + } + + // Since the left operand is undetermined, the whole operation is undetermined + return anyValue; + } + } + + public interpretConditional(ast: AstConditional): LatticeValue { + // Attempt to evaluate the condition + const condition = this.interpretExpression(ast.condition); + + // Simulate the true and false branches. + // We always need to analyze both branches to capture FunC behavior, even if only + // one branch is taken ultimately. + const trueEnv = this.envStack.simulate(() => + this.interpretExpression(ast.thenBranch), + ); + const falseEnv = this.envStack.simulate(() => + this.interpretExpression(ast.elseBranch), + ); + + // If the condition produced a value, take the corresponding environment. + // If not, join the two environments. + if (condition.kind === "value") { + if (ensureBoolean(condition.value, ast.condition.loc)) { + this.envStack.setCurrentEnvironment(trueEnv.env); + return trueEnv.val; + } else { + this.envStack.setCurrentEnvironment(falseEnv.env); + return falseEnv.val; + } + } else { + this.envStack.setCurrentEnvironment( + joinEnvironments([trueEnv.env, falseEnv.env]), + ); + return joinLatticeValues(trueEnv.val, falseEnv.val); + } + } + + public interpretStructInstance(ast: AstStructInstance): LatticeValue { + const structTy = getType(this.context, ast.type); + + // Since linter does not like removing dynamic keys from objects using the "delete" operator + // we will work with maps. The final map will be transformed into an object at the end. + + const initialMap: Map = new Map(); + initialMap.set("$tactStruct", idText(ast.type)); + + const resultWithDefaultFields = structTy.fields.reduce( + (resObj, field) => { + if (field.default !== undefined) { + resObj.set(field.name, field.default); + } else { + if (field.type.kind === "ref" && field.type.optional) { + resObj.set(field.name, null); + } + } + return resObj; + }, + initialMap, + ); + + // this will override default fields set above + // This is the only part that differs from the standard interpreter: + // if an initializer produces an undefined expression, remove such + // field because it is now undetermined. + const finalMap = ast.args.reduce((resObj, fieldWithInit) => { + const val = this.interpretExpression(fieldWithInit.initializer); + if (val.kind === "value") { + resObj.set(idText(fieldWithInit.field), val.value); + } else { + // Delete it, just in case a default value was added + resObj.delete(idText(fieldWithInit.field)); + } + return resObj; + }, resultWithDefaultFields); + + return toLatticeValue(Object.fromEntries(finalMap)); + } + + public interpretFieldAccess(ast: AstFieldAccess): LatticeValue { + const val = this.interpretExpression(ast.aggregate); + if (val.kind === "value") { + // The typechecker already made all the checks, + // so, val is ensured to be struct-like. + const structValue = val.value as StructValue; + return toLatticeValue(structValue[idText(ast.field)]); + } + return val; + } + + public interpretStaticCall(ast: AstStaticCall): LatticeValue { + // For the moment, do not analyze. I need to find a way to handle recursive calls, + // it is tricky. + + // Just evaluate the arguments + ast.args.forEach((expr) => this.interpretExpression(expr), this); + return anyValue; + } + + public interpretLetStatement(ast: AstStatementLet) { + const val = this.analyzeTopLevelExpression(ast.expression); + this.storeNewBinding(ast.name, val); + } + + public interpretDestructStatement(ast: AstStatementDestruct): void { + const val = this.analyzeTopLevelExpression(ast.expression); + + if (val.kind === "value") { + // Typechecker ensures val is a struct-like object (contracts and traits treated as structs by analyzer). + const valStruct = val.value as StructValue; + + for (const [field, name] of ast.identifiers.values()) { + const val = valStruct[idText(field)]; + // No need to check for wildcard name "_" because the environment stack handles it + + this.storeNewBinding(name, toLatticeValue(val)); + } + } else { + // All the names in the destruct statement are undetermined + for (const [_, name] of ast.identifiers.values()) { + this.storeNewBinding(name, anyValue); + } + } + } + + public interpretAssignStatement(ast: AstStatementAssign) { + const fullPath = tryExtractPath(ast.path); + + if (fullPath !== null && fullPath.length > 0) { + const val = this.analyzeTopLevelExpression(ast.expression); + + this.updateBinding(fullPath, ast.path, val, ast.loc); + } else { + throwInternalCompilerError( + "assignments allow path expressions only", + ast.path.loc, + ); + } + } + + public interpretAugmentedAssignStatement(ast: AstStatementAugmentedAssign) { + const fullPath = tryExtractPath(ast.path); + + if (fullPath !== null && fullPath.length > 0) { + let currentPathValue: LatticeValue = anyValue; + + // In an assignment, the path is either a field access or an id + if (ast.path.kind === "field_access") { + currentPathValue = this.interpretFieldAccess(ast.path); + } else if (ast.path.kind === "id") { + currentPathValue = this.interpretName(ast.path); + } else { + throwInternalCompilerError( + "assignments allow path expressions only", + ast.path.loc, + ); + } + + if (currentPathValue.kind === "value") { + const updateExprEvaluator = () => { + const result = this.interpretExpression(ast.expression); + if (result.kind !== "value") { + throw new UndefinedValueSignal(); + } + return result.value; + }; + + const newVal = this.prepareForStandardInterpreter(() => + this.interpreter.evalBinaryOpInAugmentedAssign( + ast, + currentPathValue.value, + updateExprEvaluator, + ), + ); + this.updateBinding( + fullPath, + ast.path, + toLatticeValue(newVal), + ast.loc, + ); + } else { + // As was the case with binary operators, the || and && short-circuit, so + // we need to do branch analysis on them. + if (ast.op === "||" || ast.op === "&&") { + const rightEnv = this.envStack.simulate(() => + this.interpretExpression(ast.expression), + ).env; + + // Join the environments + this.envStack.setCurrentEnvironment( + joinEnvironments([ + rightEnv, + this.envStack.getCurrentEnvironment(), + ]), + ); + } else { + // The rest of the operators do not short-circuit, so simply process the expression + this.interpretExpression(ast.expression); + } + + // Since originally the path was undetermined, the final result of the operator is undetermined + this.updateBinding(fullPath, ast.path, anyValue, ast.loc); + } + } else { + throwInternalCompilerError( + "assignments allow path expressions only", + ast.path.loc, + ); + } + } + + public interpretConditionStatement(ast: AstCondition) { + // Collect the true and false branches. + const trueBranch = () => { + this.executeStatements(ast.trueStatements); + }; + let falseBranch: () => void; + + if (ast.falseStatements !== null) { + // The typechecker ensures that there is no elseif branch + falseBranch = () => { + this.executeStatements(ast.falseStatements!); + }; + } else if (ast.elseif !== null) { + falseBranch = () => { + this.interpretConditionStatement(ast.elseif!); + }; + } else { + // The "do nothing" branch + falseBranch = () => {}; + } + + // Attempt to evaluate the condition + const conditionValue = this.analyzeTopLevelExpression(ast.condition); + let condition: boolean | undefined = undefined; + + // If the condition produced a value, transform it to boolean. + if (conditionValue.kind === "value") { + condition = ensureBoolean(conditionValue.value, ast.condition.loc); + } + + this.processConditionBranches(trueBranch, falseBranch, condition); + } + + public interpretExpressionStatement(ast: AstStatementExpression) { + // Keep executing if a non-fatal error occurs. + this.analyzeTopLevelExpression(ast.expression); + } + + public interpretForEachStatement(ast: AstStatementForEach) { + // Attempt to evaluate the map expression. + // Currently, the analyzer does not trace the + // set method for maps. Therefore, it is not safe + // to attempt to determine if the map expression is empty or not. + this.analyzeTopLevelExpression(ast.map); + + const loopBodyBranch = () => { + this.executeStatements(ast.statements); + }; + + // Simulate the loop body as if it executes once. + // This is necessary to emulate the behavior of FunC + this.envStack.simulateInNewEnvironment(loopBodyBranch); + + // Since it is not known if the map expression is empty or not, + // it is time to compute the fix-point of the loop + + const finalEnv = this.computeLoopEnv( + this.envStack.getCurrentEnvironment(), + loopBodyBranch, + ); + this.envStack.setCurrentEnvironment(finalEnv); + } + + public interpretRepeatStatement(ast: AstStatementRepeat) { + // Attempt to evaluate the iterations + const iterationsValue = this.analyzeTopLevelExpression(ast.iterations); + + let iterations: bigint | undefined = undefined; + + // If it produced a value, transform it to integer + // and execute the loop body + if (iterationsValue.kind === "value") { + iterations = ensureRepeatInt( + iterationsValue.value, + ast.iterations.loc, + ); + } + + this.processRepeatBranches(ast.statements, iterations); + } + + public interpretReturnStatement(ast: AstStatementReturn) { + // Interpret the expression, but do nothing with it + if (ast.expression !== null) { + this.analyzeTopLevelExpression(ast.expression); + } + } + + public interpretTryStatement(ast: AstStatementTry) { + // Simulate the try branch + const tryEnv = this.envStack.simulateInNewEnvironment(() => { + this.executeStatements(ast.statements); + }); + + // Join the try branch and the "empty catch" branch. + // The later is represented by the current environment + this.envStack.setCurrentEnvironment( + joinEnvironments([ + tryEnv.env, + this.envStack.getCurrentEnvironment(), + ]), + ); + } + + public interpretTryCatchStatement(ast: AstStatementTryCatch) { + // Simulate the try and catch branches + const tryEnv = this.envStack.simulateInNewEnvironment(() => { + this.executeStatements(ast.statements); + }); + const catchEnv = this.envStack.simulateInNewEnvironment(() => { + this.executeStatements(ast.catchStatements); + }); + + // Join the try and catch branches + this.envStack.setCurrentEnvironment( + joinEnvironments([tryEnv.env, catchEnv.env]), + ); + } + + public interpretUntilStatement(ast: AstStatementUntil): void { + // The loop body always executes at least once + this.executeStatements(ast.statements); + + // Attempt to evaluate the condition + const conditionValue = this.analyzeTopLevelExpression(ast.condition); + + let condition: boolean | undefined = undefined; + + // If it produced a value, transform it to boolean + if (conditionValue.kind === "value") { + condition = ensureBoolean(conditionValue.value, ast.condition.loc); + } + + this.processUntilBranches(ast.statements, ast.condition, condition); + } + + public interpretWhileStatement(ast: AstStatementWhile) { + // Attempt to evaluate the condition + const conditionValue = this.analyzeTopLevelExpression(ast.condition); + + let condition: boolean | undefined = undefined; + + // If it produced a value, transform it to boolean + // and execute the loop body. + if (conditionValue.kind === "value") { + condition = ensureBoolean(conditionValue.value, ast.condition.loc); + } + + this.processWhileBranches(ast.statements, ast.condition, condition); + } + + private storeNewBinding(id: AstId, exprValue: LatticeValue) { + // If exprValue is undefined, then the variable is undetermined. + if (exprValue.kind === "value") { + // Make a copy of exprValue, because everything is assigned by value + this.envStack.setNewBinding( + idText(id), + toLatticeValue(copyValue(exprValue.value)), + ); + } else { + this.envStack.setNewBinding(idText(id), exprValue); + } + } + + private updateBinding( + path: AstId[], + pathExpr: AstExpression, + exprValue: LatticeValue, + src: SrcInfo, + ) { + if (exprValue.kind === "value") { + // Typechecking ensures that there is at least one element in path + if (path.length === 1) { + // Make a copy of exprValue, because everything is assigned by value + this.envStack.updateBinding( + idText(path[0]!), + toLatticeValue(copyValue(exprValue.value)), + ); + } else { + const baseVal = this.interpretName(path[0]!); + let currentStruct: StructValue = {}; + const pathTypes = this.extractPathTypes(pathExpr); + + if (baseVal.kind === "value") { + // Typechecking ensures that path[0] is a struct-like object (contract/traits are treated as structs by the analyzer). + currentStruct = baseVal.value as StructValue; + } else { + // Create a partially constructed struct-like object. + currentStruct["$tactStruct"] = pathTypes[0]!; + } + + const baseStruct = currentStruct; + + // Fill fields in the path from 1 to path.length-2 (if they are not filled already) + for (let i = 1; i <= path.length - 2; i++) { + const field = idText(path[i]!); + if (field in currentStruct) { + // Since we are not accessing the last field, the typechecker + // ensures that the field stores a struct-like object + currentStruct = currentStruct[field] as StructValue; + } else { + // Create a dummy struct at the field + const tempStruct: StructValue = { + $tactStruct: pathTypes[i]!, + }; + currentStruct[field] = tempStruct; + currentStruct = tempStruct; + } + } + + // Store the constructed struct + this.storeNewBinding(path[0]!, toLatticeValue(baseStruct)); + // And now proceed as in the standard interpreter + this.prepareForStandardInterpreter(() => { + this.interpreter.updateBinding(path, exprValue.value, src); + }); + } + } else { + // Because the value is undefined, the full path is undetermined + + if (path.length === 1) { + this.envStack.updateBinding(idText(path[0]!), exprValue); + } else { + // We will need to delete the last field of the last struct in the path[0..length-2] in case path[0..length-2] is defined. + // Since the linter does not allow deleting dynamic keys from objects using the "delete" operator, + // we do the following workaround: + + // Keep track of the current struct of the path AND the parent struct of the current struct. + // Once you arrive to the final struct, extract its entries, remove the entry corresponding to the field + // you want to remove, and transform the list of entries back to an object. + // Finally, assign the modified struct back into the parent to simulate the "delete" operator. + + // Complication: the parent struct is only defined when the path has at least length 3. Hence, we need + // to treat the case of path length 2 separately. + + // Now the rant: the above logic would be much easier if "delete" operator was allowed because in such case + // there is no need to keep track of the parent struct. Suggestions to improve this logic are welcomed. + + // If while looking up the path, it fails to resolve to a value, there is no need to continue, because + // the full path is going to be undetermined anyway. + const baseVal = this.interpretName(path[0]!); + + if (baseVal.kind !== "value") { + return; + } + + // Typechecking ensures that path[0] is a struct-like object (contract/traits are treated as structs by the analyzer). + let currentStruct = baseVal.value as StructValue; + const pathNames = path.map(idText); + + if (!(pathNames[1]! in currentStruct)) { + return; + } + + // So, at this moment, "currentStruct" is the struct at path[0], and it has + // the field at path[1]. + + // Handle the special case. + if (path.length === 2) { + // Field at path[1] needs to be removed + const filteredEntries = new Map( + Object.entries(currentStruct), + ); + filteredEntries.delete(pathNames[1]!); + const finalStruct = Object.fromEntries(filteredEntries); + this.envStack.updateBinding( + pathNames[0]!, + toLatticeValue(finalStruct), + ); + return; + } + + // Now the case when the path has at least length 3. + // Initially, the parent struct is the one at path[0] and current struct is the struct after + // accessing field at path[1]; + let parentStruct = currentStruct; + currentStruct = currentStruct[pathNames[1]!] as StructValue; // Since the path has length at least 3, this is not + // the last field in the path. Hence, the typechecker + // ensures this is a struct-like object. + // Also, at this moment, we know field path[1] exists + // in currentStruct. + + for (let i = 2; i <= path.length - 2; i++) { + const field = pathNames[i]!; + if (field in currentStruct) { + // Since we are not accessing the last field, the typechecker + // ensures that the field stores a struct-like object + parentStruct = currentStruct; + currentStruct = currentStruct[field] as StructValue; + } else { + // Field is not in the struct. This means that the full path + // failed to resolve to a value. + // Hence, stop here, since the full path is already undetermined. + return; + } + } + + // Now, remove the last field from the current struct + const filteredEntries = new Map(Object.entries(currentStruct)); + filteredEntries.delete(pathNames[path.length - 1]!); + const finalStruct = Object.fromEntries(filteredEntries); + // Assign it back into the parent + parentStruct[pathNames[path.length - 2]!] = finalStruct; + } + } + } + + // Here, the partial evaluator is needed to detect some symbolic cases of + // division by zero. + private analyzeTopLevelExpression(expr: AstExpression): LatticeValue { + try { + this.interpreter.setEnvironmentStack( + new WrapperStack(this.envStack), + ); + const result = this.envStack.simulate(() => { + const result = partiallyEvalExpression( + expr, + this.context, + this.interpreter, + ); + if (isValue(result)) { + return toLatticeValue(extractValue(result as AstValue)); + } else { + return anyValue; + } + }); + this.envStack.setCurrentEnvironment(result.env); + return result.val; + } catch (e) { + if (e instanceof TactConstEvalError) { + if (!e.fatal) { + return this.interpretExpression(expr); + } + } + throw e; + } + } + + private prepareForStandardInterpreter(code: () => T): T | undefined { + try { + this.interpreter.setEnvironmentStack( + new WrapperStack(this.envStack), + ); + const result = code(); + return result; + } catch (e) { + if (e instanceof TactConstEvalError) { + if (!e.fatal) { + return undefined; + } + } + if (e instanceof UndefinedValueSignal) { + return undefined; + } + throw e; + } + } + + protected processConditionBranches( + trueBranch: () => void, + falseBranch: () => void, + condition?: boolean, + ): void { + // Simulate the true and false branches + // We need to analyze both branches to emulate FunC, + // independently of the branch that ends up executing. + const trueEnv = this.envStack.simulateInNewEnvironment(trueBranch); + const falseEnv = this.envStack.simulateInNewEnvironment(falseBranch); + + // If the condition is actually defined, take the corresponding environment. + // If not, join the two environments. + if (condition !== undefined) { + if (condition) { + this.envStack.setCurrentEnvironment(trueEnv.env); + } else { + this.envStack.setCurrentEnvironment(falseEnv.env); + } + } else { + this.envStack.setCurrentEnvironment( + joinEnvironments([trueEnv.env, falseEnv.env]), + ); + } + } + + protected processRepeatBranches( + statements: AstStatement[], + iterations?: bigint, + ) { + const repeatBodyBranch = () => { + this.executeStatements(statements); + }; + + // Simulate the loop body as if it executes once + // This is necessary to emulate the behavior of FunC + const oneIterEnv = + this.envStack.simulateInNewEnvironment(repeatBodyBranch); + + if (iterations !== undefined) { + if (iterations > 0) { + if (iterations <= this.config.maxLoopIterations) { + // Actually execute the loop and set the resulting environment + const finalEnv = this.envStack.simulateInNewEnvironment( + () => { + for (let i = 1n; i <= iterations; i++) { + repeatBodyBranch(); + } + }, + ); + this.envStack.setCurrentEnvironment(finalEnv.env); + } else { + // Take the loop body branch, compute the fix-point starting from + // the environment that resulted from executing the loop once + + const finalEnv = this.computeLoopEnv( + oneIterEnv.env, + repeatBodyBranch, + ); + this.envStack.setCurrentEnvironment(finalEnv); + } + } + // Take the "do nothing" branch. In other words, leave the environment as currently is + } else { + // Take both branches, compute the fix-point starting from + // the current environment (i.e., the "do nothing" environment) + const finalEnv = this.computeLoopEnv( + this.envStack.getCurrentEnvironment(), + repeatBodyBranch, + ); + this.envStack.setCurrentEnvironment(finalEnv); + } + } + + protected processUntilBranches( + statements: AstStatement[], + conditionAst: AstExpression, + condition?: boolean, + ) { + const loopBodyBranch = () => { + this.executeStatements(statements); + // After executing the body, we need to execute the condition again. + this.interpretExpression(conditionAst); + }; + + if (condition !== undefined) { + if (!condition) { + // Take the loop body branch, compute the fix-point starting from a second iteration of the loop + const twiceLoopEnv = + this.envStack.simulateInNewEnvironment(loopBodyBranch); + const finalEnv = this.computeLoopEnv( + twiceLoopEnv.env, + loopBodyBranch, + ); + this.envStack.setCurrentEnvironment(finalEnv); + } + // Take the "do nothing" branch. In other words, leave the environment as currently is + } else { + // Take both branches, compute the fix-point starting from + // the current environment (i.e., the "do nothing" environment) + const finalEnv = this.computeLoopEnv( + this.envStack.getCurrentEnvironment(), + loopBodyBranch, + ); + this.envStack.setCurrentEnvironment(finalEnv); + } + } + + protected processWhileBranches( + statements: AstStatement[], + conditionAst: AstExpression, + condition?: boolean, + ) { + const loopBodyBranch = () => { + this.executeStatements(statements); + // After executing the body, we need to execute the condition again. + this.interpretExpression(conditionAst); + }; + + // Simulate the loop body as if it executes once + // This is necessary to emulate the behavior of FunC analyzer + const oneIterEnv = + this.envStack.simulateInNewEnvironment(loopBodyBranch); + + if (condition !== undefined) { + if (condition) { + // Take the loop body branch, compute the fix-point starting from the environment + // that resulted from executing the loop once + + const finalEnv = this.computeLoopEnv( + oneIterEnv.env, + loopBodyBranch, + ); + this.envStack.setCurrentEnvironment(finalEnv); + } + // Take the "do nothing" branch. In other words, leave the environment as currently is + } else { + // Take both branches, compute the fix-point starting from + // the current environment (i.e., the "do nothing" environment) + const finalEnv = this.computeLoopEnv( + this.envStack.getCurrentEnvironment(), + loopBodyBranch, + ); + this.envStack.setCurrentEnvironment(finalEnv); + } + } + + protected extractPathTypes(path: AstExpression): string[] { + function buildStep(parentTypes: string[], expType: TypeRef): string[] { + if (expType.kind === "ref" || expType.kind === "ref_bounced") { + return [...parentTypes, expType.name]; + } else if (expType.kind === "map") { + return [...parentTypes, `map<${expType.key},${expType.value}>`]; + } else if (expType.kind === "void") { + return [...parentTypes, "void"]; + } else { + return [...parentTypes, "null"]; + } + } + + // A path expression can either be a field access or an id. + if (path.kind === "field_access") { + const expType = getExpType(this.context, path); + const parentTypes = this.extractPathTypes(path.aggregate); + return buildStep(parentTypes, expType); + } else if (path.kind === "id") { + const expType = getExpType(this.context, path); + return buildStep([], expType); + } else { + throwInternalCompilerError( + "only path expressions allowed.", + path.loc, + ); + } + } + + /* Computes the fix-point starting from startEnv by executing the loopCode repeatedly until the + * environment changes no more. + */ + protected computeLoopEnv( + startEnv: Environment, + loopCode: () => void, + ): Environment { + const finalEnv = this.envStack.simulateInNewEnvironment(() => { + let equalEnvs = false; + while (!equalEnvs) { + const prevEnv = this.envStack.getCurrentEnvironment(); + const loopEnv = this.envStack.simulate(loopCode).env; + const newEnv = joinEnvironments([prevEnv, loopEnv]); + this.envStack.setCurrentEnvironment(newEnv); + equalEnvs = eqEnvironments(prevEnv, newEnv); + } + }, startEnv); + return finalEnv.env; + } +} + +/** + * Joins the target environments (including their ancestor environments). + * The procedure assumes that the target environments have the same number of ancestor environments. This should be true if opening + * environment nodes is only done through the method envStack.executeInNewEnvironment. + */ +function joinEnvironments( + targets: Environment[], +): Environment { + const pivotEnv = targets.pop(); + + if (pivotEnv === undefined) { + return { values: new Map() }; + } + + // Intersect the keys in the values map using the pivot as base + let intersectedKeys = [...pivotEnv.values.keys()]; + for (const env of targets) { + const targetKeys = new Set(env.values.keys()); + intersectedKeys = intersectedKeys.filter((key) => targetKeys.has(key)); + } + + // For those variables that survived in intersectedKeys, keep those that + // have the same value in all the provided targets + + const finalVars: Map = new Map(); + + // First, fill the initial values as found in the pivot environment + for (const key of intersectedKeys) { + finalVars.set(key, pivotEnv.values.get(key)!); // key is ensured to be in pivotEnv because keys is the intersection of all envs. + } + + // Now, join the values of all the target environments, for each key. + for (const key of intersectedKeys) { + // key is ensured to be in finalVars because finalVars was initialized with "intersectedKeys" + let currentVal = finalVars.get(key)!; + + for (const env of targets) { + const alternativeVal = env.values.get(key)!; // key is ensured to be in env because keys is the intersection of all envs. + + currentVal = joinLatticeValues(currentVal, alternativeVal); + } + + finalVars.set(key, currentVal); + } + + // Put the pivot environment back into the targets + targets.push(pivotEnv); + + // Now, time to join the parent environments + + let joinedParents: Environment | undefined = undefined; + + const targetParents = targets + .map((env) => env.parent) + .filter((env) => env !== undefined); + + // Sanity check + if (targetParents.length !== 0 && targetParents.length !== targets.length) { + // This should not happen. This is a programmer's error + throwInternalCompilerError( + "Cannot join target environments because they have different ancestor lengths.", + ); + } + + // Now join the parent environments + if (targetParents.length > 0) { + joinedParents = joinEnvironments(targetParents); + } + + return { values: finalVars, parent: joinedParents }; +} + +function eqEnvironments( + env1: Environment, + env2: Environment, +): boolean { + const map1 = env1.values; + const map2 = env2.values; + + if (map1.size !== map2.size) { + return false; + } + + for (const [key, val1] of map1) { + if (!map2.has(key)) { + return false; + } + const val2 = map2.get(key)!; + if (!eqLatticeValues(val1, val2)) { + return false; + } + } + + // Up to here, the maps are equal, now check that the ancestor environments are also equal + const parent1 = env1.parent; + const parent2 = env2.parent; + + if (parent1 !== undefined && parent2 !== undefined) { + return eqEnvironments(parent1, parent2); + } else if (parent1 === undefined && parent2 === undefined) { + return true; + } else { + return false; + } +} + +/** + * Extracts the common part of two values. For atomic types, this just compares if the values are equal. + * For StructValues, this extracts the part of both structs that is common to them. This is necessary + * because the analyzer works with partially constructed structs. So, when joining different branches, + * we need to keep the part of the structs that remain the same in the branches. + */ +function extractCommonSubValue(val1: Value, val2: Value): Value | undefined { + if (val1 === null) { + return eqValues(val1, val2) ? val1 : undefined; + } else if (typeof val1 === "object" && "$tactStruct" in val1) { + if ( + typeof val2 === "object" && + val2 !== null && + "$tactStruct" in val2 + ) { + // Compute the intersection of their keys + const val1Keys = Object.keys(val1); + const val2Keys = new Set(Object.keys(val2)); + const commonKeys = val1Keys.filter((key) => val2Keys.has(key)); + // Since "$tactStruct" is in both val1 and val2, + // commonKeys contains at least "$tactStruct". + + const result: StructValue = {}; + + for (const key of commonKeys) { + const commonVal = extractCommonSubValue(val1[key]!, val2[key]!); + if (commonVal !== undefined) { + result[key] = commonVal; + } + } + + return result; + } else { + return undefined; + } + } else { + // The rest of values, since they do not have further sub structure, + // just compare for equality as in the case for null + return eqValues(val1, val2) ? val1 : undefined; + } +} + +class WrapperStack extends EnvironmentStack { + private env: EnvironmentStack; + + constructor(env: EnvironmentStack) { + super(copyValue); + this.env = env; + } + + // Overwrite all the public methods and just pass the logic to the private environment + public setNewBinding(name: string, val: Value) { + this.env.setNewBinding(name, toLatticeValue(val)); + } + + public updateBinding(name: string, val: Value) { + this.env.updateBinding(name, toLatticeValue(val)); + } + + public getBinding(name: string): Value | undefined { + const binding = this.env.getBinding(name); + if (binding !== undefined && binding.kind === "value") { + return binding.value; + } else { + return undefined; + } + } + + public selfInEnvironment(): boolean { + const binding = this.env.getBinding("self"); + return binding !== undefined ? binding.kind === "value" : false; + } + + public executeInNewEnvironment( + code: () => T, + initialBindings: { names: string[]; values: Value[] } = { + names: [], + values: [], + }, + ): T { + const wrappedValues = initialBindings.values.map(toLatticeValue); + return this.env.executeInNewEnvironment(code, { + ...initialBindings, + values: wrappedValues, + }); + } + + public simulate( + _code: () => T, + _startEnv: Environment = { values: new Map() }, + ): { env: Environment; val: T } { + throwInternalCompilerError( + "simulate method is currently not supported in WrapperStack", + ); + } + + public simulateInNewEnvironment( + _code: () => T, + _startEnv: Environment = { values: new Map() }, + ): { env: Environment; val: T } { + throwInternalCompilerError( + "simulateInNewEnvironment method is currently not supported in WrapperStack", + ); + } + + public setCurrentEnvironment(_env: Environment) { + throwInternalCompilerError( + "setCurrentEnvironment method is currently not supported in WrapperStack", + ); + } + + public getCurrentEnvironment(): Environment { + throwInternalCompilerError( + "getCurrentEnvironment method is currently not supported in WrapperStack", + ); + } +} diff --git a/src/interpreters/standard.ts b/src/interpreters/standard.ts new file mode 100644 index 000000000..460ba0f72 --- /dev/null +++ b/src/interpreters/standard.ts @@ -0,0 +1,1942 @@ +import { Address, beginCell, BitString, Cell, Slice, toNano } from "@ton/core"; +import { paddedBufferToBits } from "@ton/core/dist/boc/utils/paddedBits"; +import * as crc32 from "crc-32"; +import { CompilerContext } from "../context"; +import { idTextErr, throwInternalCompilerError } from "../errors"; +import { + AstAsmFunctionDef, + AstBinaryOperation, + AstBoolean, + AstCondition, + AstConditional, + AstConstantDef, + AstContract, + AstExpression, + AstFieldAccess, + AstFunctionDef, + AstId, + AstInitOf, + AstMessageDecl, + AstMethodCall, + AstNativeFunctionDecl, + AstNull, + AstNumber, + AstOpBinary, + AstOpUnary, + AstPrimitiveTypeDecl, + AstStatementAssign, + AstStatementAugmentedAssign, + AstStatementDestruct, + AstStatementExpression, + AstStatementForEach, + AstStatementLet, + AstStatementRepeat, + AstStatementReturn, + AstStatementTry, + AstStatementTryCatch, + AstStatementUntil, + AstStatementWhile, + AstStaticCall, + AstString, + AstStructDecl, + AstStructInstance, + AstTrait, + AstUnaryOperation, + eqNames, + idText, + isSelfId, + SrcInfo, + tryExtractPath, +} from "../grammar/ast"; +import { + getStaticConstant, + getStaticFunction, + getType, + hasStaticConstant, + hasStaticFunction, +} from "../types/resolveDescriptors"; +import { getExpType } from "../types/resolveExpression"; +import { + CommentValue, + copyValue, + showValue, + StructValue, + Value, +} from "../types/types"; +import { sha256_sync } from "@ton/crypto"; +import { enabledMasterchain } from "../config/features"; +import { dummySrcInfo } from "../grammar/grammar"; +import { + divFloor, + modFloor, + throwErrorConstEval, + throwNonFatalErrorConstEval, +} from "./util"; +import { InterpreterInterface } from "../interpreter"; + +// TVM integers are signed 257-bit integers +const minTvmInt: bigint = -(2n ** 256n); +const maxTvmInt: bigint = 2n ** 256n - 1n; + +// Range allowed in repeat statements +const minRepeatStatement: bigint = -(2n ** 256n); // Note it is the same as minimum for TVM +const maxRepeatStatement: bigint = 2n ** 31n - 1n; + +class ReturnSignal extends Error { + private value?: Value; + + constructor(value?: Value) { + super(); + this.value = value; + } + + public getValue(): Value | undefined { + return this.value; + } +} + +export type InterpreterConfig = { + // Options that tune the interpreter's behavior. + + // Maximum number of iterations inside a loop before a time out is issued. + // If a loop takes more than such number of iterations, the interpreter will fail evaluation. + // This option applies to: do...until, while and repeat loops. + maxLoopIterations: bigint; + + // Whenever a field or id does not exist, throw a fatal error if true; + // throw non fatal error otherwise. + missingFieldsAndIdsAreAlwaysFatal: boolean; +}; + +const WILDCARD_NAME: string = "_"; + +/* An Environment consists of a map of variable names to their values +(which have the generic type V), and a reference to the (optional) parent +environment. In other words, an Environment acts as a node in the linked list +representing the environments stack. + +The elements in the map are called "bindings". +*/ +export type Environment = { + values: Map; + parent?: Environment; +}; + +/* +An environment stack is a linked list of Environment nodes. + +The type of the values stored in the environments is represented by the +generic type V. +*/ +export class EnvironmentStack { + private currentEnv: Environment; + private copyValue: (val: V) => V; + + constructor(copyValueMethod: (val: V) => V) { + this.currentEnv = { values: new Map() }; + this.copyValue = copyValueMethod; + } + + private copyEnvironment(env: Environment): Environment { + const newMap: Map = new Map(); + + for (const [name, val] of env.values) { + newMap.set(name, this.copyValue(val)); + } + + let newParent: Environment | undefined = undefined; + + if (env.parent !== undefined) { + newParent = this.copyEnvironment(env.parent); + } + + return { values: newMap, parent: newParent }; + } + + private findBindingMap(name: string): Map | undefined { + let env: Environment | undefined = this.currentEnv; + while (env !== undefined) { + if (env.values.has(name)) { + return env.values; + } else { + env = env.parent; + } + } + return undefined; + } + + /* + Sets a binding for "name" in the **current** environment of the stack. + If a binding for "name" already exists in the current environment, it + overwrites the binding with the provided value. + As a special case, name "_" is ignored. + + Note that this method does not check if binding "name" already exists in + a parent environment. + This means that if binding "name" already exists in a parent environment, + it will be shadowed by the provided value in the current environment. + This shadowing behavior is useful for modelling recursive function calls. + For example, consider the recursive implementation of factorial + (for simplification purposes, it returns 1 for the factorial of + negative numbers): + + 1 fun factorial(a: Int): Int { + 2 if (a <= 1) { + 3 return 1; + 4 } else { + 5 return a * factorial(a - 1); + 6 } + + Just before factorial(4) finishes its execution, the environment stack will + look as follows (the arrows point to their parent environment): + + a = 4 <------- a = 3 <-------- a = 2 <------- a = 1 + + Note how each child environment shadows variable a, because each + recursive call to factorial at line 5 creates a child + environment with a new binding for a. + + When factorial(1) = 1 finishes execution, the environment at the top + of the stack is popped: + + a = 4 <------- a = 3 <-------- a = 2 + + and execution resumes at line 5 in the environment where a = 2, + so that the return at line 5 is 2 * 1 = 2. + + This in turn causes the stack to pop the environment at the top: + + a = 4 <------- a = 3 + + so that the return at line 5 (now in the environment a = 3) will + produce 3 * 2 = 6, and so on. + */ + public setNewBinding(name: string, val: V) { + if (name !== WILDCARD_NAME) { + this.currentEnv.values.set(name, val); + } + } + + /* + Searches the binding "name" in the stack, starting at the current + environment and moving towards the parent environments. + If it finds the binding, it updates its value + to "val". If it does not find "name", the stack is unchanged. + As a special case, name "_" is always ignored. + */ + public updateBinding(name: string, val: V) { + if (name !== WILDCARD_NAME) { + const bindings = this.findBindingMap(name); + if (bindings !== undefined) { + bindings.set(name, val); + } + } + } + + /* + Searches the binding "name" in the stack, starting at the current + environment and moving towards the parent environments. + If it finds "name", it returns its value. + If it does not find "name", it returns undefined. + As a special case, name "_" always returns undefined. + */ + public getBinding(name: string): V | undefined { + if (name === WILDCARD_NAME) { + return undefined; + } + const bindings = this.findBindingMap(name); + if (bindings !== undefined) { + return bindings.get(name); + } else { + return undefined; + } + } + + public selfInEnvironment(): boolean { + 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 + execution, the new environment is automatically popped from + the stack. + + This method is useful for starting a new local variables scope, + like in a function call. + */ + public executeInNewEnvironment( + code: () => T, + initialBindings: { names: string[]; values: V[] } = { + names: [], + values: [], + }, + ): T { + const names = initialBindings.names; + const values = initialBindings.values; + + // Create a new node in the stack + this.currentEnv = { values: new Map(), parent: this.currentEnv }; + + names.forEach((name, index) => { + this.setNewBinding(name, values[index]!); + }, this); + + try { + return code(); + } finally { + // Drop the current node in the stack + this.currentEnv = this.currentEnv.parent!; + } + } + + public simulate( + code: () => T, + startEnv: Environment = this.currentEnv, + ): { env: Environment; val: T } { + // Make a copy of the start environment + const envCopy = this.copyEnvironment(startEnv); + + // Save the current environment for restoring it once + // the execution finishes + const currentEnv = this.currentEnv; + + // All the changes will be made to the copy + this.currentEnv = envCopy; + + try { + const result = code(); + return { env: this.currentEnv, val: result }; + } finally { + // Restore the environment as it was before execution of the code + this.currentEnv = currentEnv; + } + } + + public simulateInNewEnvironment( + code: () => T, + startEnv: Environment = this.currentEnv, + ): { env: Environment; val: T } { + return this.simulate( + () => this.executeInNewEnvironment(code), + startEnv, + ); + } + + public setCurrentEnvironment(env: Environment) { + this.currentEnv = env; + } + + public getCurrentEnvironment(): Environment { + return this.currentEnv; + } +} + +export const defaultInterpreterConfig: InterpreterConfig = { + // Let us put a limit of 2 ^ 12 = 4096 iterations on loops to increase compiler responsiveness + // I think maxLoopIterations should be a command line option in case a user wants to wait more + // during evaluation. + maxLoopIterations: 2n ** 12n, + + missingFieldsAndIdsAreAlwaysFatal: false, +}; + +/* +A parameterizable Tact interpreter. +The standard Tact interpreter extends this abstract class. + +Generic type T is the expressions' result type. +*/ +abstract class AbstractInterpreter extends InterpreterInterface { + protected copyValue: (val: T) => T; + + constructor(copyValueMethod: (val: T) => T) { + super(); + this.copyValue = copyValueMethod; + } + + public interpretConstantDef(ast: AstConstantDef) { + throwNonFatalErrorConstEval( + "Constant definitions are currently not supported.", + ast.loc, + ); + } + + public interpretFunctionDef(ast: AstFunctionDef) { + throwNonFatalErrorConstEval( + "Function definitions are currently not supported.", + ast.loc, + ); + } + + public interpretAsmFunctionDef(ast: AstAsmFunctionDef) { + throwNonFatalErrorConstEval( + "Asm functions are currently not supported.", + ast.loc, + ); + } + + public interpretStructDecl(ast: AstStructDecl) { + throwNonFatalErrorConstEval( + "Struct declarations are currently not supported.", + ast.loc, + ); + } + + public interpretMessageDecl(ast: AstMessageDecl) { + throwNonFatalErrorConstEval( + "Message declarations are currently not supported.", + ast.loc, + ); + } + + public interpretPrimitiveTypeDecl(ast: AstPrimitiveTypeDecl) { + throwNonFatalErrorConstEval( + "Primitive type declarations are currently not supported.", + ast.loc, + ); + } + + public interpretNativeFunctionDecl(ast: AstNativeFunctionDecl) { + throwNonFatalErrorConstEval( + "Native function declarations are currently not supported.", + ast.loc, + ); + } + + public interpretContract(ast: AstContract) { + throwNonFatalErrorConstEval( + "Contract declarations are currently not supported.", + ast.loc, + ); + } + + public interpretTrait(ast: AstTrait) { + throwNonFatalErrorConstEval( + "Trait declarations are currently not supported.", + ast.loc, + ); + } + + public interpretName(ast: AstId): T { + return this.lookupBinding(ast); + } + + public interpretMethodCall(ast: AstMethodCall): T { + const selfValue = this.interpretExpression(ast.self); + + const argValues = ast.args.map( + (expr) => this.copyValue(this.interpretExpression(expr)), + this, + ); + + const builtinResult = this.evalBuiltinOnSelf(ast, selfValue, argValues); + if (builtinResult !== undefined) { + return builtinResult; + } + + // We have a call to a user-defined function. + + throwNonFatalErrorConstEval( + `calls of ${idTextErr(ast.method)} are not supported at this moment`, + ast.loc, + ); + } + + public interpretInitOf(ast: AstInitOf): T { + throwNonFatalErrorConstEval( + "initOf is not supported at this moment", + ast.loc, + ); + } + + public interpretUnaryOp(ast: AstOpUnary): T { + // Instead of immediately evaluating the operand, we surround the + // operand evaluation in a continuation, because some + // unary operators need to perform some previous checks before + // evaluating the operand. + const operandEvaluator = () => this.interpretExpression(ast.operand); + return this.evalUnaryOp(ast, operandEvaluator); + } + + public interpretBinaryOp(ast: AstOpBinary): T { + const leftValue = this.interpretExpression(ast.left); + + // As done with unary operators, we surround the evaluation + // of the right argument in a continuation, just in case + // the semantics need to do some special action before evaluating + // the right argument, like short-circuiting, for example. + const rightEvaluator = () => this.interpretExpression(ast.right); + + return this.evalBinaryOp(ast, leftValue, rightEvaluator); + } + + public interpretConditional(ast: AstConditional): T { + const conditionValue = this.toBoolean( + this.interpretExpression(ast.condition), + ast.condition.loc, + ); + if (conditionValue) { + return this.interpretExpression(ast.thenBranch); + } else { + return this.interpretExpression(ast.elseBranch); + } + } + + public interpretStructInstance(ast: AstStructInstance): T { + // Make each of the field initializers a continuation + const argEvaluators = ast.args.map( + (fieldWithInit) => () => + this.interpretExpression(fieldWithInit.initializer), + this, + ); + + return this.evalStructInstance(ast, argEvaluators); + } + + public interpretFieldAccess(ast: AstFieldAccess): T { + const aggregateEvaluator = () => + this.interpretExpression(ast.aggregate); + + return this.evalFieldAccess(ast, aggregateEvaluator); + } + + public interpretStaticCall(ast: AstStaticCall): T { + const argValues = ast.args.map( + (expr) => this.copyValue(this.interpretExpression(expr)), + this, + ); + + const builtinResult = this.evalBuiltin(ast, argValues); + if (builtinResult !== undefined) { + return builtinResult; + } + + // We have a call to a user-defined function. + + const functionDef = this.lookupFunction(ast); + + // Extract the parameter names + const paramNames = functionDef.params.map((param) => + idText(param.name), + ); + + // Transform the statements into continuations + const statementsEvaluator = () => { + this.executeStatements(functionDef.statements); + }; + + // Now call the function + return this.evalStaticCall(ast, functionDef, statementsEvaluator, { + names: paramNames, + values: argValues, + }); + } + + public interpretLetStatement(ast: AstStatementLet) { + const val = this.interpretExpression(ast.expression); + this.storeNewBinding(ast.name, val); + } + + public interpretDestructStatement(ast: AstStatementDestruct) { + const exprEvaluator = () => this.interpretExpression(ast.expression); + this.evalDestructStatement(ast, exprEvaluator); + } + + public interpretAssignStatement(ast: AstStatementAssign) { + const fullPath = tryExtractPath(ast.path); + + if (fullPath !== null && fullPath.length > 0) { + const val = this.interpretExpression(ast.expression); + this.updateBinding(fullPath, val, ast.loc); + } else { + throwInternalCompilerError( + "assignments allow path expressions only", + ast.path.loc, + ); + } + } + + public interpretAugmentedAssignStatement(ast: AstStatementAugmentedAssign) { + const fullPath = tryExtractPath(ast.path); + + if (fullPath !== null && fullPath.length > 0) { + const updateEvaluator = () => + this.interpretExpression(ast.expression); + + let currentPathValue: T; + + // In an assignment, the path is either a field access or an id + if (ast.path.kind === "field_access") { + currentPathValue = this.interpretFieldAccess(ast.path); + } else if (ast.path.kind === "id") { + currentPathValue = this.lookupBinding(ast.path); + } else { + throwInternalCompilerError( + "assignments allow path expressions only", + ast.path.loc, + ); + } + + const newVal = this.evalBinaryOpInAugmentedAssign( + ast, + currentPathValue, + updateEvaluator, + ); + this.updateBinding(fullPath, newVal, ast.loc); + } else { + throwInternalCompilerError( + "assignments allow path expressions only", + ast.path.loc, + ); + } + } + + public interpretConditionStatement(ast: AstCondition) { + const condition = this.toBoolean( + this.interpretExpression(ast.condition), + ast.condition.loc, + ); + + if (condition) { + this.runInNewEnvironment(() => { + this.executeStatements(ast.trueStatements); + }); + } else if (ast.falseStatements !== null) { + // We are in an else branch. The typechecker ensures that + // the elseif branch does not exist. + this.runInNewEnvironment(() => { + this.executeStatements(ast.falseStatements!); + }); + } else if (ast.elseif !== null) { + // We are in an elseif branch + this.interpretConditionStatement(ast.elseif); + } + } + + public interpretExpressionStatement(ast: AstStatementExpression) { + this.interpretExpression(ast.expression); + } + + public interpretForEachStatement(ast: AstStatementForEach) { + throwNonFatalErrorConstEval("foreach currently not supported", ast.loc); + } + + public interpretRepeatStatement(ast: AstStatementRepeat) { + const iterations = this.toRepeatInteger( + this.interpretExpression(ast.iterations), + ast.iterations.loc, + ); + + if (iterations > 0) { + // We can create a single environment for all the iterations in the loop + // (instead of a fresh environment for each iteration) + // because the typechecker ensures that variables do not leak outside + // 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.runInNewEnvironment(() => { + for (let i = 1n; i <= iterations; i++) { + this.runOneIteration(i, ast.loc, () => { + this.executeStatements(ast.statements); + }); + } + }); + } + } + + public interpretReturnStatement(ast: AstStatementReturn) { + if (ast.expression !== null) { + const val = this.interpretExpression(ast.expression); + this.evalReturn(val); + } else { + this.evalReturn(); + } + } + + public interpretTryStatement(ast: AstStatementTry) { + throwNonFatalErrorConstEval( + "try statements currently not supported", + ast.loc, + ); + } + + public interpretTryCatchStatement(ast: AstStatementTryCatch) { + throwNonFatalErrorConstEval( + "try-catch statements currently not supported", + ast.loc, + ); + } + + public interpretUntilStatement(ast: AstStatementUntil): void { + let condition: boolean; + let iterCount = 1n; + // We can create a single environment for all the iterations in the loop + // (instead of a fresh environment for each iteration) + // because the typechecker ensures that variables do not leak outside + // 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.runInNewEnvironment(() => { + do { + this.runOneIteration(iterCount, ast.loc, () => { + this.executeStatements(ast.statements); + + // The typechecker ensures that the condition does not refer to + // variables declared inside the loop. + condition = this.toBoolean( + this.interpretExpression(ast.condition), + ast.condition.loc, + ); + }); + + iterCount++; + } while (!condition); + }); + } + + public interpretWhileStatement(ast: AstStatementWhile) { + let condition = this.toBoolean( + this.interpretExpression(ast.condition), + ast.condition.loc, + ); + + let iterCount = 1n; + // We can create a single environment for all the iterations in the loop + // (instead of a fresh environment for each iteration) + // because the typechecker ensures that variables do not leak outside + // 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.runInNewEnvironment(() => { + while (condition) { + this.runOneIteration(iterCount, ast.loc, () => { + this.executeStatements(ast.statements); + + // The typechecker ensures that the condition does not refer to + // variables declared inside the loop. + condition = this.toBoolean( + this.interpretExpression(ast.condition), + ast.condition.loc, + ); + }); + + iterCount++; + } + }); + } + + /******** ABSTRACT METHODS ****/ + + /* + Executes calls to built-in functions of the form self.method(args). + Should return "undefined" if method is not a built-in function. + */ + public abstract evalBuiltinOnSelf( + ast: AstMethodCall, + self: T, + argValues: T[], + ): T | undefined; + + /* + Evaluates the unary operation. Parameter operandEvaluator is a continuation + that computes the operator's operand. The reason for using a continuation + is that certain operators may execute some logic **before** evaluation + of the operand. + */ + public abstract evalUnaryOp(ast: AstOpUnary, operandEvaluator: () => T): T; + + /* + Evaluates the binary operator. Parameter rightEvaluator is a continuation + that computes the value of the right operand. The reason for using a continuation + is that certain operators may execute some logic **before** evaluation + of the right operand (for example, short-circuiting). + */ + public abstract evalBinaryOp( + ast: AstOpBinary, + leftValue: T, + rightEvaluator: () => T, + ): T; + + public abstract toBoolean(value: T, src: SrcInfo): boolean; + + /* + Evaluates the struct instance. Parameter initializerEvaluators is a list of continuations. + Each continuation computes the result of executing the initializer. + */ + public abstract evalStructInstance( + ast: AstStructInstance, + initializerEvaluators: (() => T)[], + ): T; + + /* + Evaluates a field access of the form "path.field". Parameter aggregateEvaluator is a continuation. + The continuation computes the value of "path". + */ + public abstract evalFieldAccess( + ast: AstFieldAccess, + aggregateEvaluator: () => T, + ): T; + + /* + Executes calls to built-in functions of the form method(args). + Should return "undefined" if method is not a built-in function. + */ + public abstract evalBuiltin( + ast: AstStaticCall, + argValues: T[], + ): T | undefined; + + public abstract lookupFunction(ast: AstStaticCall): AstFunctionDef; + + /* + Calls function "functionDef" using parameters "args". The body of "functionDef" can be + executed by invoking continuation "functionBodyEvaluator". + */ + public abstract evalStaticCall( + ast: AstStaticCall, + functionDef: AstFunctionDef, + functionBodyEvaluator: () => void, + args: { names: string[]; values: T[] }, + ): T; + + /* + Evaluates the binary operator implicit in an augment assignment. + Parameter rightEvaluator is a continuation + that computes the value of the right operand. The reason for using a continuation + is that certain operators may execute some logic **before** evaluation + of the right operand (for example, short-circuiting). + */ + public abstract evalBinaryOpInAugmentedAssign( + ast: AstStatementAugmentedAssign, + leftValue: T, + rightEvaluator: () => T, + ): T; + + public abstract evalDestructStatement( + ast: AstStatementDestruct, + exprEvaluator: () => T, + ): void; + + public abstract lookupBinding(path: AstId): T; + + public abstract storeNewBinding(id: AstId, exprValue: T): void; + + public abstract updateBinding( + path: AstId[], + exprValue: T, + src: SrcInfo, + ): void; + + /* + Runs the continuation statementsEvaluator in a new environment. + In the standard semantics, this means opening a new environment in + the stack and closing the environment when statementsEvaluator finishes execution. + */ + public abstract runInNewEnvironment(statementsEvaluator: () => void): void; + + public abstract toInteger(value: T, src: SrcInfo): bigint; + + public abstract toRepeatInteger(value: T, src: SrcInfo): bigint; + + /* + Runs one iteration of the body of a loop. The body of the loop is executed by + calling the continuation "iterationEvaluator". The iteration number is provided + for further custom logic. + */ + public abstract runOneIteration( + iterationNumber: bigint, + src: SrcInfo, + iterationEvaluator: () => void, + ): void; + + public abstract evalReturn(val?: T): void; +} + +/* +The standard Tact interpreter. + +The constructor receives an optional CompilerContext which includes +all external declarations that the interpreter will use during interpretation. +If no CompilerContext is provided, the semantics will use an empty +CompilerContext. + +**IMPORTANT**: if a custom CompilerContext is provided, it should be the +CompilerContext provided by the typechecker. + +The reason for requiring a CompilerContext is that the interpreter should work +in the use case where the interpreter only knows part of the code. +For example, consider the following code (I marked with brackets [ ] the places +where the interpreter gets called during expression simplification in the +compilation phase): + +const C: Int = [1]; + +contract TestContract { + + get fun test(): Int { + return [C + 1]; + } +} + +When the interpreter gets called inside the brackets, it does not know what +other code is surrounding those brackets, because the interpreter did not execute the +code outside the brackets. Hence, it relies on the typechecker to receive the +CompilerContext that includes the declarations in the code +(the constant C for example). + +Since the interpreter relies on the typechecker, this semantics assume that the +interpreter will only be called on AST trees +that are already valid Tact programs. + +Internally, the semantics use a stack of environments to keep track of +variables at different scopes. Each environment in the stack contains a map +that binds a variable name to its corresponding value. +*/ +export class TactInterpreter extends AbstractInterpreter { + protected envStack: EnvironmentStack; + protected context: CompilerContext; + protected config: InterpreterConfig; + + constructor( + context: CompilerContext = new CompilerContext(), + config: InterpreterConfig = defaultInterpreterConfig, + ) { + super(copyValue); + this.envStack = new EnvironmentStack(copyValue); + this.context = context; + this.config = config; + } + + public setEnvironmentStack(envStack: EnvironmentStack) { + this.envStack = envStack; + } + + protected emitFieldOrIdError(message: string, src: SrcInfo): never { + if (this.config.missingFieldsAndIdsAreAlwaysFatal) { + throwErrorConstEval(message, src); + } else { + throwNonFatalErrorConstEval(message, src); + } + } + + public lookupBinding(name: AstId): Value { + if (hasStaticConstant(this.context, idText(name))) { + const constant = getStaticConstant(this.context, idText(name)); + if (constant.value !== undefined) { + return constant.value; + } else { + this.emitFieldOrIdError( + `cannot evaluate declared constant ${idText(name)} as it does not have a body`, + name.loc, + ); + } + } + const variableBinding = this.envStack.getBinding(idText(name)); + if (variableBinding !== undefined) { + return variableBinding; + } + this.emitFieldOrIdError("cannot evaluate a variable", name.loc); + } + + public evalBuiltinOnSelf( + ast: AstMethodCall, + self: Value, + _argValues: Value[], + ): Value | undefined { + switch (idText(ast.method)) { + case "asComment": { + ensureMethodArity(0, ast.args, ast.loc); + const comment = ensureString(self, ast.self.loc); + return new CommentValue(comment); + } + default: + return undefined; + } + } + + public evalCallOnSelf( + ast: AstMethodCall, + _self: Value, + _argValues: Value[], + ): Value { + throwNonFatalErrorConstEval( + `calls of ${idTextErr(ast.method)} are not supported at this moment`, + ast.loc, + ); + } + + public interpretNull(_ast: AstNull): Value { + return null; + } + + public interpretBoolean(ast: AstBoolean): Value { + return ast.value; + } + + public interpretNumber(ast: AstNumber): Value { + return ensureInt(ast.value, ast.loc); + } + + public interpretString(ast: AstString): Value { + return ensureString( + interpretEscapeSequences(ast.value, ast.loc), + ast.loc, + ); + } + + public evalUnaryOp(ast: AstOpUnary, operandEvaluator: () => Value): Value { + // Tact grammar does not have negative integer literals, + // so in order to avoid errors for `-115792089237316195423570985008687907853269984665640564039457584007913129639936` + // which is `-(2**256)` we need to have a special case for it + + if (ast.operand.kind === "number" && ast.op === "-") { + // emulating negative integer literals + return ensureInt(-ast.operand.value, ast.loc); + } + + return evalUnaryOp(ast.op, operandEvaluator, ast.operand.loc, ast.loc); + } + + public evalBinaryOp( + ast: AstOpBinary, + leftValue: Value, + rightEvaluator: () => Value, + ): Value { + return evalBinaryOp( + ast.op, + leftValue, + rightEvaluator, + ast.left.loc, + ast.right.loc, + ast.loc, + ); + } + + public evalBinaryOpInAugmentedAssign( + ast: AstStatementAugmentedAssign, + leftValue: Value, + rightEvaluator: () => Value, + ): Value { + return evalBinaryOp( + ast.op, + leftValue, + rightEvaluator, + ast.path.loc, + ast.expression.loc, + ast.loc, + ); + } + + public toBoolean(value: Value, src: SrcInfo): boolean { + return ensureBoolean(value, src); + } + + public evalStructInstance( + ast: AstStructInstance, + initializerEvaluators: (() => Value)[], + ): Value { + if (ast.args.length !== initializerEvaluators.length) { + throwInternalCompilerError( + "Number of arguments in ast must match the number of argument evaluators.", + ); + } + + const structTy = getType(this.context, ast.type); + + // initialize the resulting struct value with + // the default values for fields with initializers + // or null for uninitialized optional fields + const resultWithDefaultFields: StructValue = structTy.fields.reduce( + (resObj, field) => { + if (field.default !== undefined) { + resObj[field.name] = field.default; + } else { + if (field.type.kind === "ref" && field.type.optional) { + resObj[field.name] = null; + } + } + return resObj; + }, + { $tactStruct: idText(ast.type) } as StructValue, + ); + + // this will override default fields set above + return ast.args.reduce((resObj, fieldWithInit, index) => { + resObj[idText(fieldWithInit.field)] = + initializerEvaluators[index]!(); + return resObj; + }, resultWithDefaultFields); + } + + public evalFieldAccess( + ast: AstFieldAccess, + aggregateEvaluator: () => Value, + ): Value { + // special case for contract/trait constant accesses via `self.constant` + // interpret "self" as a contract/trait access only if "self" + // is not already assigned in the environment (this would mean + // we are executing inside an extends function) + if ( + ast.aggregate.kind === "id" && + isSelfId(ast.aggregate) && + !this.envStack.selfInEnvironment() + ) { + const selfTypeRef = getExpType(this.context, ast.aggregate); + if (selfTypeRef.kind === "ref") { + const contractTypeDescription = getType( + this.context, + selfTypeRef.name, + ); + const foundContractConst = + contractTypeDescription.constants.find((constId) => + eqNames(ast.field, constId.name), + ); + if (foundContractConst === undefined) { + // not a constant, e.g. `self.storageVariable` + this.emitFieldOrIdError( + "cannot evaluate non-constant self field access", + ast.aggregate.loc, + ); + } + if (foundContractConst.value !== undefined) { + return foundContractConst.value; + } else { + this.emitFieldOrIdError( + `cannot evaluate declared contract/trait constant ${idTextErr(ast.field)} as it does not have a body`, + ast.field.loc, + ); + } + } + } + const valStruct = aggregateEvaluator(); + if ( + valStruct === null || + typeof valStruct !== "object" || + !("$tactStruct" in valStruct) + ) { + throwErrorConstEval( + `constant struct expected, but got ${showValue(valStruct)}`, + ast.aggregate.loc, + ); + } + return this.extractFieldFromStruct( + valStruct, + ast.field, + ast.aggregate.loc, + ); + } + + protected extractFieldFromStruct( + struct: StructValue, + field: AstId, + src: SrcInfo, + ): Value { + if (idText(field) in struct) { + return struct[idText(field)]!; + } else { + this.emitFieldOrIdError( + `struct field ${idTextErr(field)} is missing`, + src, + ); + } + } + + public evalBuiltin( + ast: AstStaticCall, + argValues: Value[], + ): Value | undefined { + switch (idText(ast.function)) { + case "ton": { + ensureFunArity(1, ast.args, ast.loc); + const tons = ensureString(argValues[0]!, ast.args[0]!.loc); + try { + return ensureInt( + BigInt(toNano(tons).toString(10)), + ast.loc, + ); + } catch (e) { + if (e instanceof Error && e.message === "Invalid number") { + throwErrorConstEval( + `invalid ${idTextErr(ast.function)} argument`, + ast.loc, + ); + } + throw e; + } + } + case "pow": { + ensureFunArity(2, ast.args, ast.loc); + const valBase = ensureInt(argValues[0]!, ast.args[0]!.loc); + const valExp = ensureInt(argValues[1]!, ast.args[1]!.loc); + if (valExp < 0n) { + throwErrorConstEval( + `${idTextErr(ast.function)} builtin called with negative exponent ${valExp}`, + ast.loc, + ); + } + try { + return ensureInt(valBase ** valExp, ast.loc); + } catch (e) { + if (e instanceof RangeError) { + // even TS bigint type cannot hold it + throwErrorConstEval( + "integer does not fit into TVM Int type", + ast.loc, + ); + } + throw e; + } + } + case "pow2": { + ensureFunArity(1, ast.args, ast.loc); + const valExponent = ensureInt(argValues[0]!, ast.args[0]!.loc); + if (valExponent < 0n) { + throwErrorConstEval( + `${idTextErr(ast.function)} builtin called with negative exponent ${valExponent}`, + ast.loc, + ); + } + try { + return ensureInt(2n ** valExponent, ast.loc); + } catch (e) { + if (e instanceof RangeError) { + // even TS bigint type cannot hold it + throwErrorConstEval( + "integer does not fit into TVM Int type", + ast.loc, + ); + } + throw e; + } + } + case "sha256": { + ensureFunArity(1, ast.args, ast.loc); + const expr = argValues[0]!; + if (expr instanceof Slice) { + throwNonFatalErrorConstEval( + "slice argument is currently not supported", + ast.loc, + ); + } + const str = ensureString(expr, ast.args[0]!.loc); + return BigInt("0x" + sha256_sync(str).toString("hex")); + } + case "emptyMap": { + ensureFunArity(0, ast.args, ast.loc); + return null; + } + case "cell": + { + ensureFunArity(1, ast.args, ast.loc); + const str = ensureString(argValues[0]!, ast.args[0]!.loc); + try { + return Cell.fromBase64(str); + } catch (_) { + throwErrorConstEval( + `invalid base64 encoding for a cell: ${str}`, + ast.loc, + ); + } + } + break; + case "slice": + { + ensureFunArity(1, ast.args, ast.loc); + const str = ensureString(argValues[0]!, ast.args[0]!.loc); + try { + return Cell.fromBase64(str).asSlice(); + } catch (_) { + throwErrorConstEval( + `invalid base64 encoding for a cell: ${str}`, + ast.loc, + ); + } + } + break; + case "rawSlice": + { + ensureFunArity(1, ast.args, ast.loc); + const str = ensureString(argValues[0]!, ast.args[0]!.loc); + + if (!/^[0-9a-fA-F]*_?$/.test(str)) { + throwErrorConstEval( + `invalid hex string: ${str}`, + ast.loc, + ); + } + + // Remove underscores from the hex string + const hex = str.replace("_", ""); + const paddedHex = hex.length % 2 === 0 ? hex : "0" + hex; + const buffer = Buffer.from(paddedHex, "hex"); + + // Initialize the BitString + let bits = new BitString( + buffer, + hex.length % 2 === 0 ? 0 : 4, + hex.length * 4, + ); + + // Handle the case where the string ends with an underscore + if (str.endsWith("_")) { + const paddedBits = paddedBufferToBits(buffer); + + // Ensure there's enough length to apply the offset + const offset = hex.length % 2 === 0 ? 0 : 4; + if (paddedBits.length >= offset) { + bits = paddedBits.substring( + offset, + paddedBits.length - offset, + ); + } else { + bits = new BitString(Buffer.from(""), 0, 0); + } + } + + // Ensure the bit length is within acceptable limits + if (bits.length > 1023) { + throwErrorConstEval( + `slice constant is too long, expected up to 1023 bits, got ${bits.length}`, + ast.loc, + ); + } + + // Return the constructed slice + return beginCell().storeBits(bits).endCell().asSlice(); + } + break; + case "ascii": + { + ensureFunArity(1, ast.args, ast.loc); + const str = ensureString(argValues[0]!, ast.args[0]!.loc); + const hex = Buffer.from(str).toString("hex"); + if (hex.length > 64) { + throwErrorConstEval( + `ascii string is too long, expected up to 32 bytes, got ${Math.floor(hex.length / 2)}`, + ast.loc, + ); + } + if (hex.length == 0) { + throwErrorConstEval( + `ascii string cannot be empty`, + ast.loc, + ); + } + return BigInt("0x" + hex); + } + break; + case "crc32": + { + ensureFunArity(1, ast.args, ast.loc); + const str = ensureString(argValues[0]!, ast.args[0]!.loc); + return BigInt(crc32.str(str) >>> 0); // >>> 0 converts to unsigned + } + break; + case "address": + { + ensureFunArity(1, ast.args, ast.loc); + const str = ensureString(argValues[0]!, ast.args[0]!.loc); + try { + const address = Address.parse(str); + if ( + address.workChain !== 0 && + address.workChain !== -1 + ) { + throwErrorConstEval( + `${str} is invalid address`, + ast.loc, + ); + } + if ( + !enabledMasterchain(this.context) && + address.workChain !== 0 + ) { + throwErrorConstEval( + `address ${str} is from masterchain which is not enabled for this contract`, + ast.loc, + ); + } + return address; + } catch (_) { + throwErrorConstEval( + `invalid address encoding: ${str}`, + ast.loc, + ); + } + } + break; + case "newAddress": { + ensureFunArity(2, ast.args, ast.loc); + const wc = ensureInt(argValues[0]!, ast.args[0]!.loc); + const addr = Buffer.from( + ensureInt(argValues[1]!, ast.args[1]!.loc) + .toString(16) + .padStart(64, "0"), + "hex", + ); + if (wc !== 0n && wc !== -1n) { + throwErrorConstEval( + `expected workchain of an address to be equal 0 or -1, received: ${wc}`, + ast.loc, + ); + } + if (!enabledMasterchain(this.context) && wc !== 0n) { + throwErrorConstEval( + `${wc}:${addr.toString("hex")} address is from masterchain which is not enabled for this contract`, + ast.loc, + ); + } + return new Address(Number(wc), addr); + } + default: + return undefined; + } + } + + public lookupFunction(ast: AstStaticCall): AstFunctionDef { + if (hasStaticFunction(this.context, idText(ast.function))) { + const functionDescription = getStaticFunction( + this.context, + idText(ast.function), + ); + switch (functionDescription.ast.kind) { + case "function_def": + // Currently, no attribute is supported + if (functionDescription.ast.attributes.length > 0) { + throwNonFatalErrorConstEval( + "calls to functions with attributes are currently not supported", + ast.loc, + ); + } + return functionDescription.ast; + + case "function_decl": + throwNonFatalErrorConstEval( + `${idTextErr(ast.function)} cannot be interpreted because it does not have a body`, + ast.loc, + ); + break; + case "native_function_decl": + throwNonFatalErrorConstEval( + "native function calls are currently not supported", + ast.loc, + ); + break; + case "asm_function_def": + throwNonFatalErrorConstEval( + `${idTextErr(ast.function)} cannot be interpreted because it's an asm-function`, + ast.loc, + ); + break; + } + } else { + throwNonFatalErrorConstEval( + `function ${idTextErr(ast.function)} is not declared`, + ast.loc, + ); + } + } + + public evalStaticCall( + _ast: AstStaticCall, + _functionDef: AstFunctionDef, + functionBodyEvaluator: () => undefined, + args: { names: string[]; values: Value[] }, + ): Value { + // Call function inside a new environment + return this.envStack.executeInNewEnvironment( + () => { + // Interpret all the statements + try { + functionBodyEvaluator(); + // At this point, the function did not execute a return. + // Execution continues after the catch. + } catch (e) { + if (e instanceof ReturnSignal) { + const val = e.getValue(); + if (val !== undefined) { + return val; + } + // The function executed a return without a value. + // Execution continues after the catch. + } else { + throw e; + } + } + // If execution reaches this point, it means that + // the function had no return statement or executed a return + // without a value. In summary, the function does not return a value. + // We rely on the typechecker so that the function is called as a statement. + // Hence, we can return a dummy null, since the null will be discarded anyway. + return null; + }, + { names: args.names, values: args.values }, + ); + } + + public evalDestructStatement( + ast: AstStatementDestruct, + exprEvaluator: () => Value, + ) { + for (const [_, name] of ast.identifiers.values()) { + if (hasStaticConstant(this.context, idText(name))) { + // Attempt of shadowing a constant in a destructuring declaration + throwInternalCompilerError( + `declaration of ${idText(name)} shadows a constant with the same name`, + ast.loc, + ); + } + } + const val = exprEvaluator(); + if ( + val === null || + typeof val !== "object" || + !("$tactStruct" in val) + ) { + throwInternalCompilerError( + `destructuring assignment expected a struct, but got ${showValue( + val, + )}`, + ast.expression.loc, + ); + } + if (ast.identifiers.size !== Object.keys(val).length - 1) { + this.emitFieldOrIdError( + `destructuring assignment expected ${Object.keys(val).length - 1} fields, but got ${ + ast.identifiers.size + }`, + ast.loc, + ); + } + + for (const [field, name] of ast.identifiers.values()) { + if (name.text === "_") { + continue; + } + const v = val[idText(field)]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (v === undefined) { + this.emitFieldOrIdError( + `destructuring assignment expected field ${idTextErr( + field, + )}`, + ast.loc, + ); + } + this.envStack.setNewBinding(idText(name), v); + } + } + + public storeNewBinding(id: AstId, exprValue: Value) { + if (hasStaticConstant(this.context, idText(id))) { + // Attempt of shadowing a constant in a let declaration + throwInternalCompilerError( + `declaration of ${idText(id)} shadows a constant with the same name`, + id.loc, + ); + } + + // Make a copy of exprValue, because everything is assigned by value + this.envStack.setNewBinding(idText(id), this.copyValue(exprValue)); + } + + public updateBinding(path: AstId[], exprValue: Value, src: SrcInfo) { + if (path.length === 0) { + throwInternalCompilerError( + `path expression must be non-empty`, + src, + ); + } + if (path.length === 1) { + // Make a copy of exprValue, because everything is assigned by value + this.envStack.updateBinding( + idText(path[0]!), + this.copyValue(exprValue), + ); + return; + } + + // the path expression contains at least 2 identifiers + + // Look up the first identifier + const baseStruct = this.envStack.getBinding(idText(path[0]!)); + if (baseStruct !== undefined) { + // The typechecker ensures that baseStruct is a contract or a struct, + // which are treated identically by the interpreter as StructValues + + // Carry out look ups from ids 1 to path.length-2 (inclusive) + let innerValue = baseStruct as StructValue; + for (let i = 1; i <= path.length - 2; i++) { + const fieldName = idText(path[i]!); + if (fieldName in innerValue) { + const tempValue = innerValue[fieldName]!; + // The typechecker ensures that tempValue is a StructValue + // (because we are not accessing the last id in the path) + innerValue = tempValue as StructValue; + } else { + this.emitFieldOrIdError( + `cannot find field ${fieldName}`, + path[i]!.loc, + ); + } + } + + // Update the final field + // Make a copy of exprValue, because everything is assigned by value + innerValue[idText(path[path.length - 1]!)] = + this.copyValue(exprValue); + } else { + this.emitFieldOrIdError( + `cannot find identifier ${idText(path[0]!)}`, + path[0]!.loc, + ); + } + } + + public runInNewEnvironment(statementsEvaluator: () => void) { + this.envStack.executeInNewEnvironment(statementsEvaluator); + } + + public toInteger(value: Value, src: SrcInfo): bigint { + return ensureInt(value, src); + } + + public evalReturn(val?: Value) { + throw new ReturnSignal(val); + } + + public runOneIteration( + iterationNumber: bigint, + src: SrcInfo, + iterationEvaluator: () => void, + ) { + iterationEvaluator(); + if (iterationNumber >= this.config.maxLoopIterations) { + throwNonFatalErrorConstEval("loop timeout reached", src); + } + } + + public toRepeatInteger(value: Value, src: SrcInfo): bigint { + return ensureRepeatInt(value, src); + } +} + +export function ensureInt(val: Value, source: SrcInfo): bigint { + if (typeof val !== "bigint") { + throwErrorConstEval( + `integer expected, but got '${showValue(val)}'`, + source, + ); + } + if (minTvmInt <= val && val <= maxTvmInt) { + return val; + } else { + throwErrorConstEval( + `integer '${showValue(val)}' does not fit into TVM Int type`, + source, + ); + } +} + +export function ensureRepeatInt(val: Value, source: SrcInfo): bigint { + if (typeof val !== "bigint") { + throwErrorConstEval( + `integer expected, but got '${showValue(val)}'`, + source, + ); + } + if (minRepeatStatement <= val && val <= maxRepeatStatement) { + return val; + } else { + throwErrorConstEval( + `repeat argument must be a number between -2^256 (inclusive) and 2^31 - 1 (inclusive)`, + source, + ); + } +} + +export function ensureBoolean(val: Value, source: SrcInfo): boolean { + if (typeof val !== "boolean") { + throwErrorConstEval( + `boolean expected, but got '${showValue(val)}'`, + source, + ); + } + return val; +} + +function ensureString(val: Value, source: SrcInfo): string { + if (typeof val !== "string") { + throwErrorConstEval( + `string expected, but got '${showValue(val)}'`, + source, + ); + } + return val; +} + +function ensureFunArity(arity: number, args: AstExpression[], source: SrcInfo) { + if (args.length !== arity) { + throwErrorConstEval( + `function expects ${arity} argument(s), but got ${args.length}`, + source, + ); + } +} + +function ensureMethodArity( + arity: number, + args: AstExpression[], + source: SrcInfo, +) { + if (args.length !== arity) { + throwErrorConstEval( + `method expects ${arity} argument(s), but got ${args.length}`, + source, + ); + } +} + +export function evalUnaryOp( + op: AstUnaryOperation, + operandContinuation: () => Value, + operandLoc: SrcInfo = dummySrcInfo, + source: SrcInfo = dummySrcInfo, +): Value { + switch (op) { + case "+": + return ensureInt(operandContinuation(), operandLoc); + case "-": + return ensureInt( + -ensureInt(operandContinuation(), operandLoc), + source, + ); + case "~": + return ~ensureInt(operandContinuation(), operandLoc); + case "!": + return !ensureBoolean(operandContinuation(), operandLoc); + case "!!": { + const valOperand = operandContinuation(); + if (valOperand === null) { + throwErrorConstEval( + "non-null value expected but got null", + operandLoc, + ); + } + return valOperand; + } + } +} + +export function evalBinaryOp( + op: AstBinaryOperation, + valLeft: Value, + valRightContinuation: () => Value, // It needs to be a continuation, because some binary operators short-circuit + locLeft: SrcInfo = dummySrcInfo, + locRight: SrcInfo = dummySrcInfo, + source: SrcInfo = dummySrcInfo, +): Value { + switch (op) { + case "+": + return ensureInt( + ensureInt(valLeft, locLeft) + + ensureInt(valRightContinuation(), locRight), + source, + ); + case "-": + return ensureInt( + ensureInt(valLeft, locLeft) - + ensureInt(valRightContinuation(), locRight), + source, + ); + case "*": + return ensureInt( + ensureInt(valLeft, locLeft) * + ensureInt(valRightContinuation(), locRight), + source, + ); + case "/": { + // The semantics of integer division for TVM (and by extension in Tact) + // is a non-conventional one: by default it rounds towards negative infinity, + // meaning, for instance, -1 / 5 = -1 and not zero, as in many mainstream languages. + // Still, the following holds: a / b * b + a % b == a, for all b != 0. + const r = ensureInt(valRightContinuation(), locRight); + if (r === 0n) + throwErrorConstEval("divisor must be non-zero", locRight); + return ensureInt(divFloor(ensureInt(valLeft, locLeft), r), source); + } + case "%": { + // Same as for division, see the comment above + // Example: -1 % 5 = 4 + const r = ensureInt(valRightContinuation(), locRight); + if (r === 0n) + throwErrorConstEval("divisor must be non-zero", locRight); + return ensureInt(modFloor(ensureInt(valLeft, locLeft), r), source); + } + case "&": + return ( + ensureInt(valLeft, locLeft) & + ensureInt(valRightContinuation(), locRight) + ); + case "|": + return ( + ensureInt(valLeft, locLeft) | + ensureInt(valRightContinuation(), locRight) + ); + case "^": + return ( + ensureInt(valLeft, locLeft) ^ + ensureInt(valRightContinuation(), locRight) + ); + case "<<": { + const valNum = ensureInt(valLeft, locLeft); + const valBits = ensureInt(valRightContinuation(), locRight); + if (0n > valBits || valBits > 256n) { + throwErrorConstEval( + `the number of bits shifted ('${valBits}') must be within [0..256] range`, + locRight, + ); + } + try { + return ensureInt(valNum << valBits, source); + } catch (e) { + if (e instanceof RangeError) + // this actually should not happen + throwErrorConstEval( + `integer does not fit into TVM Int type`, + source, + ); + throw e; + } + } + case ">>": { + const valNum = ensureInt(valLeft, locLeft); + const valBits = ensureInt(valRightContinuation(), locRight); + if (0n > valBits || valBits > 256n) { + throwErrorConstEval( + `the number of bits shifted ('${valBits}') must be within [0..256] range`, + locRight, + ); + } + try { + return ensureInt(valNum >> valBits, source); + } catch (e) { + if (e instanceof RangeError) + // this is actually should not happen + throwErrorConstEval( + `integer does not fit into TVM Int type`, + source, + ); + throw e; + } + } + case ">": + return ( + ensureInt(valLeft, locLeft) > + ensureInt(valRightContinuation(), locRight) + ); + case "<": + return ( + ensureInt(valLeft, locLeft) < + ensureInt(valRightContinuation(), locRight) + ); + case ">=": + return ( + ensureInt(valLeft, locLeft) >= + ensureInt(valRightContinuation(), locRight) + ); + case "<=": + return ( + ensureInt(valLeft, locLeft) <= + ensureInt(valRightContinuation(), locRight) + ); + case "==": { + const valR = valRightContinuation(); + + // the null comparisons account for optional types, e.g. + // a const x: Int? = 42 can be compared to null + if ( + typeof valLeft !== typeof valR && + valLeft !== null && + valR !== null + ) { + throwErrorConstEval( + "operands of `==` must have same type", + source, + ); + } + return valLeft === valR; + } + case "!=": { + const valR = valRightContinuation(); + if (typeof valLeft !== typeof valR) { + throwErrorConstEval( + "operands of `!=` must have same type", + source, + ); + } + return valLeft !== valR; + } + case "&&": + return ( + ensureBoolean(valLeft, locLeft) && + ensureBoolean(valRightContinuation(), locRight) + ); + case "||": + return ( + ensureBoolean(valLeft, locLeft) || + ensureBoolean(valRightContinuation(), locRight) + ); + } +} + +function interpretEscapeSequences(stringLiteral: string, source: SrcInfo) { + return stringLiteral.replace( + /\\\\|\\"|\\n|\\r|\\t|\\v|\\b|\\f|\\u{([0-9A-Fa-f]{1,6})}|\\u([0-9A-Fa-f]{4})|\\x([0-9A-Fa-f]{2})/g, + (match, unicodeCodePoint, unicodeEscape, hexEscape) => { + switch (match) { + case "\\\\": + return "\\"; + case '\\"': + return '"'; + case "\\n": + return "\n"; + case "\\r": + return "\r"; + case "\\t": + return "\t"; + case "\\v": + return "\v"; + case "\\b": + return "\b"; + case "\\f": + return "\f"; + default: + // Handle Unicode code point escape + if (unicodeCodePoint) { + const codePoint = parseInt(unicodeCodePoint, 16); + if (codePoint > 0x10ffff) { + throwErrorConstEval( + `unicode code point is outside of valid range 000000-10FFFF: ${stringLiteral}`, + source, + ); + } + return String.fromCodePoint(codePoint); + } + // Handle Unicode escape + if (unicodeEscape) { + const codeUnit = parseInt(unicodeEscape, 16); + return String.fromCharCode(codeUnit); + } + // Handle hex escape + if (hexEscape) { + const hexValue = parseInt(hexEscape, 16); + return String.fromCharCode(hexValue); + } + return match; + } + }, + ); +} diff --git a/src/interpreters/test/__snapshots__/constant-propagation.spec.ts.snap b/src/interpreters/test/__snapshots__/constant-propagation.spec.ts.snap new file mode 100644 index 000000000..14e6d7047 --- /dev/null +++ b/src/interpreters/test/__snapshots__/constant-propagation.spec.ts.snap @@ -0,0 +1,2333 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`constant-propagation should fail constant propagation analysis for var-track-assignments 1`] = ` +":12:14: Cannot evaluate expression: divisor must be non-zero +Line 12, col 14: + 11 | b += a + 3; // 9 +> 12 | 1 / (b - 9); // Division by zero + ^~~~~ + 13 | return 0; +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-init 1`] = ` +":34:13: Cannot evaluate expression: divisor must be non-zero +Line 34, col 13: + 33 | self.g.Bb = 0; +> 34 | 1 / self.g.Bb; // Division by zero. + ^~~~~~~~~ + 35 | } +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-initof-1 1`] = ` +":9:18: Cannot evaluate expression: divisor must be non-zero +Line 9, col 18: + 8 | init(v: Int) { +> 9 | self.a / self.a; // Division by zero + ^~~~~~ + 10 | } +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-initof-2 1`] = ` +":12:9: Cannot evaluate expression: divisor must be non-zero +Line 12, col 9: + 11 | self.a = 0; +> 12 | v / self.a; // Division by zero + ^~~~~~~~~~ + 13 | } +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-inside-do-until 1`] = ` +":10:25: Cannot evaluate expression: divisor must be non-zero +Line 10, col 25: + 9 | do { +> 10 | return 1 / (a - 5); // Since the loop executes at least once, and a = 5, + ^~~~~ + 11 | } until (x > 0); // a division by zero will occur. +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-inside-foreach 1`] = ` +":10:25: Cannot evaluate expression: divisor must be non-zero +Line 10, col 25: + 9 | foreach (k, val in xMap) { // Currently, the analyzer cannot determine if a map is empty or not +> 10 | return 1 / (a - 5); // because it does not track map mutations using the set function. + ^~~~~ + 11 | } // So, it will treat foreach loops as if their number of iterations is undetermined. +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-inside-if 1`] = ` +":9:25: Cannot evaluate expression: divisor must be non-zero +Line 9, col 25: + 8 | if (a > 5) { // Even though runtime execution will not enter the if, +> 9 | return 1 / (a - 5); // we still need to report a division by zero inside the if, because + ^~~~~ + 10 | } // the error would occur IF the conditional was executed: i.e., +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-inside-if-else-false-branch 1`] = ` +":11:25: Cannot evaluate expression: divisor must be non-zero +Line 11, col 25: + 10 | } else { // the error would occur IF the branch was executed: i.e., +> 11 | return 1 / (a - 5); // it is a potential division by zero. + ^~~~~ + 12 | } // Also, we need to imitate the behavior of FunC, which actually +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-inside-if-else-true-branch 1`] = ` +":9:25: Cannot evaluate expression: divisor must be non-zero +Line 9, col 25: + 8 | if (a > 5) { // Even though runtime execution will not enter the true branch, +> 9 | return 1 / (a - 5); // we still need to report a division by zero inside, because + ^~~~~ + 10 | } else { // the error would occur IF the branch was executed: i.e., +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-inside-repeat 1`] = ` +":10:25: Cannot evaluate expression: divisor must be non-zero +Line 10, col 25: + 9 | repeat (x) { // Even though runtime execution will not enter the loop body, +> 10 | return 1 / (a - 5); // we still need to report a division by zero inside, because + ^~~~~ + 11 | } // the error would occur IF the loop was executed: i.e., +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-inside-try 1`] = ` +":11:25: Cannot evaluate expression: divisor must be non-zero +Line 11, col 25: + 10 | x += v; +> 11 | return 1 / (a - 5); // If try reaches the end, it would produce a division by zero + ^~~~~ + 12 | } +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-inside-try-catch 1`] = ` +":13:25: Cannot evaluate expression: divisor must be non-zero +Line 13, col 25: + 12 | } catch (e) { +> 13 | return 1 / (a - 5); // If an error occurs during the try (for example, if v = 0), + ^~~~~ + 14 | } // the catch would produce a division by zero. +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-inside-while 1`] = ` +":10:25: Cannot evaluate expression: divisor must be non-zero +Line 10, col 25: + 9 | while (x > 0) { // Even though runtime execution will not enter the loop body, +> 10 | return 1 / (a - 5); // we still need to report a division by zero inside, because + ^~~~~ + 11 | } // the error would occur IF the loop was executed: i.e., +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-outside-do-until-no-iterations 1`] = ` +":12:21: Cannot evaluate expression: divisor must be non-zero +Line 12, col 21: + 11 | } until (x >= 0); +> 12 | return 1 / (a - 6); // Loop does not execute more than once. Hence, after the loop, a = 6, + ^~~~~ + 13 | } // which means division by zero. +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-outside-do-until-undetermined 1`] = ` +":13:21: Cannot evaluate expression: divisor must be non-zero +Line 13, col 21: + 12 | } until (v > 0); // v does not have a value at compile time +> 13 | return 1 / (a - 5); // Unknown if loop executes. But if it does or not, all paths lead to a = 5, + ^~~~~ + 14 | } // which means division by zero after the loop. +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-outside-do-until-with-iterations 1`] = ` +":14:21: Cannot evaluate expression: divisor must be non-zero +Line 14, col 21: + 13 | } until (x > 0); +> 14 | return 1 / (a - 10); // Loop executes more than once. Hence, but after each loop iteration, a = 10, + ^~~~~~ + 15 | } // which means division by zero. +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-outside-foreach-undetermined 1`] = ` +":13:21: Cannot evaluate expression: divisor must be non-zero +Line 13, col 21: + 12 | } +> 13 | return 1 / (a - 5); // If loop executes or not, all possible paths assign a = 5. Hence, there is a division by zero. + ^~~~~ + 14 | } +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-outside-if-else-false-branch 1`] = ` +":13:21: Cannot evaluate expression: divisor must be non-zero +Line 13, col 21: + 12 | } +> 13 | return 1 / (a - 3); // Division by zero, because condition in the if is false at compile time, + ^~~~~ + 14 | } // which means that a = 3 after the conditional. +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-outside-if-else-true-branch 1`] = ` +":13:21: Cannot evaluate expression: divisor must be non-zero +Line 13, col 21: + 12 | } +> 13 | return 1 / (a - 10); // Division by zero, because condition in the if is true at compile time, + ^~~~~~ + 14 | } // which means that a = 10 after the conditional. +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-outside-if-else-undetermined 1`] = ` +":13:21: Cannot evaluate expression: divisor must be non-zero +Line 13, col 21: + 12 | } +> 13 | return 1 / (a - 5); // Division by zero, even though condition cannot be determined at compile time + ^~~~~ + 14 | } // all branches inside the if also assign a = 5. +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-outside-if-else-undetermined-no-assign-false-branch 1`] = ` +":14:21: Cannot evaluate expression: divisor must be non-zero +Line 14, col 21: + 13 | } +> 14 | return 1 / (a - 5); // Division by zero, even though condition cannot be determined at compile time + ^~~~~ + 15 | } // all paths before the return lead to a = 5. +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-outside-if-else-undetermined-no-assign-true-branch 1`] = ` +":14:21: Cannot evaluate expression: divisor must be non-zero +Line 14, col 21: + 13 | } +> 14 | return 1 / (a - 5); // Division by zero, even though condition cannot be determined at compile time + ^~~~~ + 15 | } // all paths before the return lead to a = 5. +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-outside-if-elseif-false-branch 1`] = ` +":13:21: Cannot evaluate expression: divisor must be non-zero +Line 13, col 21: + 12 | } +> 13 | return 1 / (a - 3); // Division by zero, because conditions can be determined at compile time, + ^~~~~ + 14 | } // which means that a = 3 after the conditional. +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-outside-if-elseif-true-branch 1`] = ` +":13:21: Cannot evaluate expression: divisor must be non-zero +Line 13, col 21: + 12 | } +> 13 | return 1 / (a - 10); // Division by zero, because conditions can be determined at compile time, + ^~~~~~ + 14 | } // which means that a = 10 after the conditional. +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-outside-if-elseif-undetermined 1`] = ` +":13:21: Cannot evaluate expression: divisor must be non-zero +Line 13, col 21: + 12 | } +> 13 | return 1 / (a - 5); // Division by zero, even though condition cannot be determined at compile time + ^~~~~ + 14 | } // all branches inside the if also assign a = 5. +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-outside-if-elseif-undetermined-no-assign-false-branch 1`] = ` +":14:21: Cannot evaluate expression: divisor must be non-zero +Line 14, col 21: + 13 | } +> 14 | return 1 / (a - 5); // Division by zero, even though condition cannot be determined at compile time + ^~~~~ + 15 | } // all paths before the return lead to a = 5. +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-outside-if-elseif-undetermined-no-assign-true-branch 1`] = ` +":14:21: Cannot evaluate expression: divisor must be non-zero +Line 14, col 21: + 13 | } +> 14 | return 1 / (a - 5); // Division by zero, even though condition cannot be determined at compile time + ^~~~~ + 15 | } // all paths before the return lead to a = 5. +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-outside-if-false 1`] = ` +":11:21: Cannot evaluate expression: divisor must be non-zero +Line 11, col 21: + 10 | } +> 11 | return 1 / (a - 5); // Division by zero, because condition in the if is false at compile time, + ^~~~~ + 12 | } // which means that a = 5 after the conditional. +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-outside-if-true 1`] = ` +":11:21: Cannot evaluate expression: divisor must be non-zero +Line 11, col 21: + 10 | } +> 11 | return 1 / (a - 10); // Division by zero, because condition in the if is true at compile time, + ^~~~~~ + 12 | } // which means that a = 10 after the conditional. +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-outside-if-undetermined 1`] = ` +":11:21: Cannot evaluate expression: divisor must be non-zero +Line 11, col 21: + 10 | } +> 11 | return 1 / (a - 5); // Division by zero, even though condition cannot be determined at compile time + ^~~~~ + 12 | } // the branch inside the if also assigns a = 5. +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-outside-repeat-no-iterations 1`] = ` +":12:21: Cannot evaluate expression: divisor must be non-zero +Line 12, col 21: + 11 | } +> 12 | return 1 / (a - 5); // Loop does not execute. Hence, after the loop, a = 5, + ^~~~~ + 13 | } // which means division by zero. +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-outside-repeat-undetermined 1`] = ` +":13:21: Cannot evaluate expression: divisor must be non-zero +Line 13, col 21: + 12 | } +> 13 | return 1 / (a - 5); // Unknown if loop executes. But if it does or not, all paths lead to a = 5, + ^~~~~ + 14 | } // which means division by zero after the loop. +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-outside-repeat-with-iterations 1`] = ` +":13:21: Cannot evaluate expression: divisor must be non-zero +Line 13, col 21: + 12 | } +> 13 | return 1 / (a - 3); // Loop executes. After each iteration, a = 3, + ^~~~~ + 14 | } // which means division by zero after the loop. +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-outside-try-catch-undetermined 1`] = ` +":15:21: Cannot evaluate expression: divisor must be non-zero +Line 15, col 21: + 14 | } +> 15 | return 1 / (a - 5); // Independently if the try successfully executes or not, all paths lead to a = 5. + ^~~~~ + 16 | } // Hence, division by zero. Note that if the catch executes, it also assigns a = 5. +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-outside-try-undetermined 1`] = ` +":13:21: Cannot evaluate expression: divisor must be non-zero +Line 13, col 21: + 12 | } +> 13 | return 1 / (a - 5); // Independently if the try successfully executes or not, all paths lead to a = 5. + ^~~~~ + 14 | } // Hence, division by zero. Note that the catch is empty, which means that variable +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-outside-while-no-iterations 1`] = ` +":12:21: Cannot evaluate expression: divisor must be non-zero +Line 12, col 21: + 11 | } +> 12 | return 1 / (a - 5); // Loop does not execute. Hence, after the loop, a = 5, + ^~~~~ + 13 | } // which means division by zero. +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-outside-while-undetermined 1`] = ` +":13:21: Cannot evaluate expression: divisor must be non-zero +Line 13, col 21: + 12 | } +> 13 | return 1 / (a - 5); // Unknown if loop executes. But if it does or not, all paths lead to a = 5, + ^~~~~ + 14 | } // which means division by zero after the loop. +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-outside-while-with-iterations 1`] = ` +":14:21: Cannot evaluate expression: divisor must be non-zero +Line 14, col 21: + 13 | } +> 14 | return 1 / (a - 3); // Loop executes. After each iteration, a = 3, + ^~~~~ + 15 | } // which means division by zero after the loop. +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-structs 1`] = ` +":35:13: Cannot evaluate expression: divisor must be non-zero +Line 35, col 13: + 34 | 1 / a.Ab.Bc; // OK. +> 35 | 1 / a.Ab.Bb; // Division by zero. + ^~~~~~~ + 36 | return 0; +" +`; + +exports[`constant-propagation should fail constant propagation analysis for var-track-structs-in-parameter 1`] = ` +":30:20: Cannot evaluate expression: divisor must be non-zero +Line 30, col 20: + 29 | let b = self; // The only determined field in b is b.f.Ca.Ab.Bc +> 30 | return 1 / b.f.Ca.Ab.Bc; // Division by zero. + ^~~~~~~~~~~~ + 31 | } +" +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-do-until 1`] = ` +[ + [ + "5", + "Int", + ], + [ + "10", + "Int", + ], + [ + "v", + "Int", + ], + [ + "0", + "Int", + ], + [ + "v > 0", + "Bool", + ], + [ + "a", + "Int", + ], + [ + "10", + "Int", + ], + [ + "a", + "Int", + ], + [ + "v", + "Int", + ], + [ + "v", + "Int", + ], + [ + "v - v", + "Int", + ], + [ + "6", + "Int", + ], + [ + "v - v + 6", + "Int", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-do-until-mutatingFun 1`] = ` +[ + [ + "self", + "Int", + ], + [ + "5", + "Int", + ], + [ + "5", + "Int", + ], + [ + "10", + "Int", + ], + [ + "v", + "Int", + ], + [ + "0", + "Int", + ], + [ + "v > 0", + "Bool", + ], + [ + "a", + "Int", + ], + [ + "10", + "Int", + ], + [ + "a", + "Int", + ], + [ + "a.mutateFun()", + "", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-foreach 1`] = ` +[ + [ + "5", + "Int", + ], + [ + "emptyMap()", + "", + ], + [ + "xMap", + "map", + ], + [ + "a", + "Int", + ], + [ + "10", + "Int", + ], + [ + "k", + "Int", + ], + [ + "10 + k", + "Int", + ], + [ + "a", + "Int", + ], + [ + "6", + "Int", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-foreach-mutatingFun 1`] = ` +[ + [ + "self", + "Int", + ], + [ + "5", + "Int", + ], + [ + "5", + "Int", + ], + [ + "emptyMap()", + "", + ], + [ + "xMap", + "map", + ], + [ + "a", + "Int", + ], + [ + "10", + "Int", + ], + [ + "k", + "Int", + ], + [ + "10 + k", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a", + "Int", + ], + [ + "a.mutateFun()", + "", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-if 1`] = ` +[ + [ + "5", + "Int", + ], + [ + "v", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v >= 5", + "Bool", + ], + [ + "a", + "Int", + ], + [ + "6", + "Int", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-if-else 1`] = ` +[ + [ + "5", + "Int", + ], + [ + "v", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v >= 5", + "Bool", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a", + "Int", + ], + [ + "6", + "Int", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-if-else-mutatingFun 1`] = ` +[ + [ + "self", + "Int", + ], + [ + "5", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v >= 5", + "Bool", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a", + "Int", + ], + [ + "a.mutateFun()", + "", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-if-else-no-assign-false-branch 1`] = ` +[ + [ + "5", + "Int", + ], + [ + "10", + "Int", + ], + [ + "v", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v >= 5", + "Bool", + ], + [ + "a", + "Int", + ], + [ + "6", + "Int", + ], + [ + "x", + "Int", + ], + [ + "7", + "Int", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-if-else-no-assign-false-branch-mutatingFun 1`] = ` +[ + [ + "self", + "Int", + ], + [ + "5", + "Int", + ], + [ + "5", + "Int", + ], + [ + "10", + "Int", + ], + [ + "v", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v >= 5", + "Bool", + ], + [ + "a", + "Int", + ], + [ + "a.mutateFun()", + "", + ], + [ + "x", + "Int", + ], + [ + "7", + "Int", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-if-else-no-assign-true-branch 1`] = ` +[ + [ + "5", + "Int", + ], + [ + "10", + "Int", + ], + [ + "v", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v >= 5", + "Bool", + ], + [ + "x", + "Int", + ], + [ + "3", + "Int", + ], + [ + "a", + "Int", + ], + [ + "6", + "Int", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-if-else-no-assign-true-branch-mutatingFun 1`] = ` +[ + [ + "self", + "Int", + ], + [ + "5", + "Int", + ], + [ + "5", + "Int", + ], + [ + "10", + "Int", + ], + [ + "v", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v >= 5", + "Bool", + ], + [ + "x", + "Int", + ], + [ + "3", + "Int", + ], + [ + "a", + "Int", + ], + [ + "a.mutateFun()", + "", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-if-elseif 1`] = ` +[ + [ + "5", + "Int", + ], + [ + "v", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v >= 5", + "Bool", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v > 5", + "Bool", + ], + [ + "a", + "Int", + ], + [ + "6", + "Int", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-if-elseif-else 1`] = ` +[ + [ + "5", + "Int", + ], + [ + "10", + "Int", + ], + [ + "v", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v >= 5", + "Bool", + ], + [ + "a", + "Int", + ], + [ + "6", + "Int", + ], + [ + "x", + "Int", + ], + [ + "5", + "Int", + ], + [ + "x > 5", + "Bool", + ], + [ + "a", + "Int", + ], + [ + "7", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "7", + "Int", + ], + [ + "a - 7", + "Int", + ], + [ + "1 / (a - 7)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-if-elseif-else-mutatingFun 1`] = ` +[ + [ + "self", + "Int", + ], + [ + "5", + "Int", + ], + [ + "5", + "Int", + ], + [ + "10", + "Int", + ], + [ + "v", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v >= 5", + "Bool", + ], + [ + "a", + "Int", + ], + [ + "a.mutateFun()", + "", + ], + [ + "x", + "Int", + ], + [ + "5", + "Int", + ], + [ + "x > 5", + "Bool", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a", + "Int", + ], + [ + "7", + "Int", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-if-elseif-mutatingFun 1`] = ` +[ + [ + "self", + "Int", + ], + [ + "5", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v >= 5", + "Bool", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v > 5", + "Bool", + ], + [ + "a", + "Int", + ], + [ + "a.mutateFun()", + "", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-if-elseif-no-assign-false-branch 1`] = ` +[ + [ + "5", + "Int", + ], + [ + "10", + "Int", + ], + [ + "v", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v >= 5", + "Bool", + ], + [ + "a", + "Int", + ], + [ + "6", + "Int", + ], + [ + "v", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v > 5", + "Bool", + ], + [ + "x", + "Int", + ], + [ + "7", + "Int", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-if-elseif-no-assign-false-branch-mutatingFun 1`] = ` +[ + [ + "self", + "Int", + ], + [ + "5", + "Int", + ], + [ + "5", + "Int", + ], + [ + "10", + "Int", + ], + [ + "v", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v >= 5", + "Bool", + ], + [ + "a", + "Int", + ], + [ + "a.mutateFun()", + "", + ], + [ + "v", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v > 5", + "Bool", + ], + [ + "x", + "Int", + ], + [ + "7", + "Int", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-if-elseif-no-assign-true-branch 1`] = ` +[ + [ + "5", + "Int", + ], + [ + "10", + "Int", + ], + [ + "v", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v >= 5", + "Bool", + ], + [ + "x", + "Int", + ], + [ + "3", + "Int", + ], + [ + "v", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v > 5", + "Bool", + ], + [ + "a", + "Int", + ], + [ + "6", + "Int", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-if-elseif-no-assign-true-branch-mutatingFun 1`] = ` +[ + [ + "self", + "Int", + ], + [ + "5", + "Int", + ], + [ + "5", + "Int", + ], + [ + "10", + "Int", + ], + [ + "v", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v >= 5", + "Bool", + ], + [ + "x", + "Int", + ], + [ + "3", + "Int", + ], + [ + "v", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v > 5", + "Bool", + ], + [ + "a", + "Int", + ], + [ + "a.mutateFun()", + "", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-if-mutatingFun 1`] = ` +[ + [ + "self", + "Int", + ], + [ + "5", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v", + "Int", + ], + [ + "5", + "Int", + ], + [ + "v >= 5", + "Bool", + ], + [ + "a", + "Int", + ], + [ + "a.mutateFun()", + "", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-repeat 1`] = ` +[ + [ + "5", + "Int", + ], + [ + "10", + "Int", + ], + [ + "v", + "Int", + ], + [ + "a", + "Int", + ], + [ + "10", + "Int", + ], + [ + "a", + "Int", + ], + [ + "v", + "Int", + ], + [ + "v", + "Int", + ], + [ + "v - v", + "Int", + ], + [ + "6", + "Int", + ], + [ + "v - v + 6", + "Int", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-repeat-mutatingFun 1`] = ` +[ + [ + "self", + "Int", + ], + [ + "5", + "Int", + ], + [ + "5", + "Int", + ], + [ + "10", + "Int", + ], + [ + "v", + "Int", + ], + [ + "a", + "Int", + ], + [ + "10", + "Int", + ], + [ + "a", + "Int", + ], + [ + "a.mutateFun()", + "", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-try 1`] = ` +[ + [ + "5", + "Int", + ], + [ + "0", + "Int", + ], + [ + "x", + "Int", + ], + [ + "v", + "Int", + ], + [ + "a", + "Int", + ], + [ + "x", + "Int", + ], + [ + "x", + "Int", + ], + [ + "x - x", + "Int", + ], + [ + "6", + "Int", + ], + [ + "x - x + 6", + "Int", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-try-catch 1`] = ` +[ + [ + "5", + "Int", + ], + [ + "0", + "Int", + ], + [ + "x", + "Int", + ], + [ + "v", + "Int", + ], + [ + "a", + "Int", + ], + [ + "x", + "Int", + ], + [ + "x", + "Int", + ], + [ + "x - x", + "Int", + ], + [ + "6", + "Int", + ], + [ + "x - x + 6", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-try-catch-mutatingFun 1`] = ` +[ + [ + "self", + "Int", + ], + [ + "5", + "Int", + ], + [ + "5", + "Int", + ], + [ + "0", + "Int", + ], + [ + "x", + "Int", + ], + [ + "v", + "Int", + ], + [ + "a", + "Int", + ], + [ + "a.mutateFun()", + "", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-try-mutatingFun 1`] = ` +[ + [ + "self", + "Int", + ], + [ + "5", + "Int", + ], + [ + "5", + "Int", + ], + [ + "0", + "Int", + ], + [ + "x", + "Int", + ], + [ + "v", + "Int", + ], + [ + "a", + "Int", + ], + [ + "a.mutateFun()", + "", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-while 1`] = ` +[ + [ + "5", + "Int", + ], + [ + "10", + "Int", + ], + [ + "v", + "Int", + ], + [ + "0", + "Int", + ], + [ + "v > 0", + "Bool", + ], + [ + "a", + "Int", + ], + [ + "10", + "Int", + ], + [ + "a", + "Int", + ], + [ + "v", + "Int", + ], + [ + "v", + "Int", + ], + [ + "v - v", + "Int", + ], + [ + "6", + "Int", + ], + [ + "v - v + 6", + "Int", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; + +exports[`constant-propagation should pass constant propagation analysis for var-track-outside-while-mutatingFun 1`] = ` +[ + [ + "self", + "Int", + ], + [ + "5", + "Int", + ], + [ + "5", + "Int", + ], + [ + "10", + "Int", + ], + [ + "v", + "Int", + ], + [ + "0", + "Int", + ], + [ + "v > 0", + "Bool", + ], + [ + "a", + "Int", + ], + [ + "10", + "Int", + ], + [ + "a", + "Int", + ], + [ + "a.mutateFun()", + "", + ], + [ + "1", + "Int", + ], + [ + "a", + "Int", + ], + [ + "5", + "Int", + ], + [ + "a - 5", + "Int", + ], + [ + "1 / (a - 5)", + "Int", + ], +] +`; diff --git a/src/interpreters/test/constant-propagation.spec.ts b/src/interpreters/test/constant-propagation.spec.ts new file mode 100644 index 000000000..9789c1daa --- /dev/null +++ b/src/interpreters/test/constant-propagation.spec.ts @@ -0,0 +1,44 @@ +import { featureEnable } from "../../config/features"; +import { CompilerContext } from "../../context"; +import { __DANGER_resetNodeId } from "../../grammar/ast"; +import { openContext } from "../../grammar/store"; +import { resolveDescriptors } from "../../types/resolveDescriptors"; +import { getAllExpressionTypes } from "../../types/resolveExpression"; +import { resolveStatements } from "../../types/resolveStatements"; +import { loadCases } from "../../utils/loadCases"; +import { ConstantPropagationAnalyzer } from "../constantPropagation"; + +describe("constant-propagation", () => { + beforeEach(() => { + __DANGER_resetNodeId(); + }); + for (const r of loadCases(__dirname + "/success/")) { + it("should pass constant propagation analysis for " + r.name, () => { + let ctx = openContext( + new CompilerContext(), + [{ code: r.code, path: "", origin: "user" }], + [], + ); + ctx = featureEnable(ctx, "external"); + ctx = resolveDescriptors(ctx); + ctx = resolveStatements(ctx); + new ConstantPropagationAnalyzer(ctx).startAnalysis(); + expect(getAllExpressionTypes(ctx)).toMatchSnapshot(); + }); + } + for (const r of loadCases(__dirname + "/failed/")) { + it("should fail constant propagation analysis for " + r.name, () => { + let ctx = openContext( + new CompilerContext(), + [{ code: r.code, path: "", origin: "user" }], + [], + ); + ctx = featureEnable(ctx, "external"); + ctx = resolveDescriptors(ctx); + ctx = resolveStatements(ctx); + expect(() => { + new ConstantPropagationAnalyzer(ctx).startAnalysis(); + }).toThrowErrorMatchingSnapshot(); + }); + } +}); diff --git a/src/interpreters/test/failed/var-track-assignments.tact b/src/interpreters/test/failed/var-track-assignments.tact new file mode 100644 index 000000000..b8c1b03e1 --- /dev/null +++ b/src/interpreters/test/failed/var-track-assignments.tact @@ -0,0 +1,15 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let b: Int = 6; + let c: Int = a - b + v; // -1 + v + a = c - c; // 0, note that c cannot be evaluated to a value, because v is undetermined, but algebraically, a = 0. + b += a + 3; // 9 + 1 / (b - 9); // Division by zero + return 0; + } +} \ No newline at end of file diff --git a/src/interpreters/test/failed/var-track-init.tact b/src/interpreters/test/failed/var-track-init.tact new file mode 100644 index 000000000..fd1507d99 --- /dev/null +++ b/src/interpreters/test/failed/var-track-init.tact @@ -0,0 +1,36 @@ +primitive Int; +primitive Bool; +trait BaseTrait {} + +struct SA { + Aa: Int; + Ab: SB; +} + +struct SB { + Ba: Bool; + Bb: Int; + Bc: Int; +} + +contract Test { + + f: SB = SB {Ba: false, Bb: 0, Bc: 2 + 2}; + g: SB; + + init(v: Int) { + let a: SA = SA {Aa: 0, Ab: SB {Ba: true, Bb: 5, Bc: 0}}; + a.Aa = v; // Since v is undetermined, field Aa is also undetermined. + 1 / a.Aa; // OK because a.Aa is undetermined. + 1 / a.Ab.Bb; // OK + a.Ab = self.f; // Overwrites all fields in a.Ab with the default struct in self.f. + // 1 / a.Ab.Bb; // Commented because it would cause a division by zero. + a.Ab.Ba = false; + a.Ab.Bb = 2; + a.Ab.Bc = 10; + self.g = a.Ab; + 1 / self.g.Bb; // OK + self.g.Bb = 0; + 1 / self.g.Bb; // Division by zero. + } +} diff --git a/src/interpreters/test/failed/var-track-initof-1.tact b/src/interpreters/test/failed/var-track-initof-1.tact new file mode 100644 index 000000000..33d8ab850 --- /dev/null +++ b/src/interpreters/test/failed/var-track-initof-1.tact @@ -0,0 +1,11 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + a: Int = 0; + + init(v: Int) { + self.a / self.a; // Division by zero + } +} \ No newline at end of file diff --git a/src/interpreters/test/failed/var-track-initof-2.tact b/src/interpreters/test/failed/var-track-initof-2.tact new file mode 100644 index 000000000..1fafbcad8 --- /dev/null +++ b/src/interpreters/test/failed/var-track-initof-2.tact @@ -0,0 +1,14 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + a: Int; + + init(v: Int) { + self.a = 5; + v / self.a; // OK + self.a = 0; + v / self.a; // Division by zero + } +} \ No newline at end of file diff --git a/src/interpreters/test/failed/var-track-inside-do-until.tact b/src/interpreters/test/failed/var-track-inside-do-until.tact new file mode 100644 index 000000000..a5948f509 --- /dev/null +++ b/src/interpreters/test/failed/var-track-inside-do-until.tact @@ -0,0 +1,13 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 0; + do { + return 1 / (a - 5); // Since the loop executes at least once, and a = 5, + } until (x > 0); // a division by zero will occur. + } +} diff --git a/src/interpreters/test/failed/var-track-inside-foreach.tact b/src/interpreters/test/failed/var-track-inside-foreach.tact new file mode 100644 index 000000000..e87f045d7 --- /dev/null +++ b/src/interpreters/test/failed/var-track-inside-foreach.tact @@ -0,0 +1,14 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let xMap: map = emptyMap(); + foreach (k, val in xMap) { // Currently, the analyzer cannot determine if a map is empty or not + return 1 / (a - 5); // because it does not track map mutations using the set function. + } // So, it will treat foreach loops as if their number of iterations is undetermined. + return 0; // In case the loop executes, it will produce a division by zero. + } +} diff --git a/src/interpreters/test/failed/var-track-inside-if-else-false-branch.tact b/src/interpreters/test/failed/var-track-inside-if-else-false-branch.tact new file mode 100644 index 000000000..21adc7de4 --- /dev/null +++ b/src/interpreters/test/failed/var-track-inside-if-else-false-branch.tact @@ -0,0 +1,14 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + if (a >= 5) { // Even though runtime execution will not enter the false branch, + return 0; // we still need to report a division by zero inside, because + } else { // the error would occur IF the branch was executed: i.e., + return 1 / (a - 5); // it is a potential division by zero. + } // Also, we need to imitate the behavior of FunC, which actually + } // reports a division by zero in this case. +} \ No newline at end of file diff --git a/src/interpreters/test/failed/var-track-inside-if-else-true-branch.tact b/src/interpreters/test/failed/var-track-inside-if-else-true-branch.tact new file mode 100644 index 000000000..ccc0f7530 --- /dev/null +++ b/src/interpreters/test/failed/var-track-inside-if-else-true-branch.tact @@ -0,0 +1,14 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + if (a > 5) { // Even though runtime execution will not enter the true branch, + return 1 / (a - 5); // we still need to report a division by zero inside, because + } else { // the error would occur IF the branch was executed: i.e., + return 0; // it is a potential division by zero. + } // Also, we need to imitate the behavior of FunC, which actually + } // reports a division by zero in this case. +} \ No newline at end of file diff --git a/src/interpreters/test/failed/var-track-inside-if.tact b/src/interpreters/test/failed/var-track-inside-if.tact new file mode 100644 index 000000000..0ee4987a2 --- /dev/null +++ b/src/interpreters/test/failed/var-track-inside-if.tact @@ -0,0 +1,13 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + if (a > 5) { // Even though runtime execution will not enter the if, + return 1 / (a - 5); // we still need to report a division by zero inside the if, because + } // the error would occur IF the conditional was executed: i.e., + return 0; // it is a potential division by zero. + } // Also, we need to imitate the behavior of FunC, which actually +} // reports a division by zero in this case. \ No newline at end of file diff --git a/src/interpreters/test/failed/var-track-inside-repeat.tact b/src/interpreters/test/failed/var-track-inside-repeat.tact new file mode 100644 index 000000000..43e005b5d --- /dev/null +++ b/src/interpreters/test/failed/var-track-inside-repeat.tact @@ -0,0 +1,14 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 0; + repeat (x) { // Even though runtime execution will not enter the loop body, + return 1 / (a - 5); // we still need to report a division by zero inside, because + } // the error would occur IF the loop was executed: i.e., + return 0; // it is a potential division by zero. + } // Also, we need to imitate the behavior of FunC, which actually +} // reports a division by zero in this case. diff --git a/src/interpreters/test/failed/var-track-inside-try-catch.tact b/src/interpreters/test/failed/var-track-inside-try-catch.tact new file mode 100644 index 000000000..1fcb12497 --- /dev/null +++ b/src/interpreters/test/failed/var-track-inside-try-catch.tact @@ -0,0 +1,16 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 0; + try { + x += v; + return 1 / v; // Note tha analyzer cannot issue a division by zero here, because v is undetermined. + } catch (e) { + return 1 / (a - 5); // If an error occurs during the try (for example, if v = 0), + } // the catch would produce a division by zero. + } +} diff --git a/src/interpreters/test/failed/var-track-inside-try.tact b/src/interpreters/test/failed/var-track-inside-try.tact new file mode 100644 index 000000000..1a4a0bc4f --- /dev/null +++ b/src/interpreters/test/failed/var-track-inside-try.tact @@ -0,0 +1,15 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 0; + try { + x += v; + return 1 / (a - 5); // If try reaches the end, it would produce a division by zero + } + return 0; + } +} diff --git a/src/interpreters/test/failed/var-track-inside-while.tact b/src/interpreters/test/failed/var-track-inside-while.tact new file mode 100644 index 000000000..cfe5cb747 --- /dev/null +++ b/src/interpreters/test/failed/var-track-inside-while.tact @@ -0,0 +1,14 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 0; + while (x > 0) { // Even though runtime execution will not enter the loop body, + return 1 / (a - 5); // we still need to report a division by zero inside, because + } // the error would occur IF the loop was executed: i.e., + return 0; // it is a potential division by zero. + } // Also, we need to imitate the behavior of FunC, which actually +} // reports a division by zero in this case. diff --git a/src/interpreters/test/failed/var-track-outside-do-until-no-iterations.tact b/src/interpreters/test/failed/var-track-outside-do-until-no-iterations.tact new file mode 100644 index 000000000..1e26a5d73 --- /dev/null +++ b/src/interpreters/test/failed/var-track-outside-do-until-no-iterations.tact @@ -0,0 +1,14 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 0; + do { + a += 1; + } until (x >= 0); + return 1 / (a - 6); // Loop does not execute more than once. Hence, after the loop, a = 6, + } // which means division by zero. +} diff --git a/src/interpreters/test/failed/var-track-outside-do-until-undetermined.tact b/src/interpreters/test/failed/var-track-outside-do-until-undetermined.tact new file mode 100644 index 000000000..0c78a63a6 --- /dev/null +++ b/src/interpreters/test/failed/var-track-outside-do-until-undetermined.tact @@ -0,0 +1,15 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 10; + do { + a += 10; + a = v - v + 5; + } until (v > 0); // v does not have a value at compile time + return 1 / (a - 5); // Unknown if loop executes. But if it does or not, all paths lead to a = 5, + } // which means division by zero after the loop. +} diff --git a/src/interpreters/test/failed/var-track-outside-do-until-with-iterations.tact b/src/interpreters/test/failed/var-track-outside-do-until-with-iterations.tact new file mode 100644 index 000000000..65365d721 --- /dev/null +++ b/src/interpreters/test/failed/var-track-outside-do-until-with-iterations.tact @@ -0,0 +1,16 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 10; + do { + a += 1; + x -= 1; + a = x - x + 10; + } until (x > 0); + return 1 / (a - 10); // Loop executes more than once. Hence, but after each loop iteration, a = 10, + } // which means division by zero. +} diff --git a/src/interpreters/test/failed/var-track-outside-foreach-undetermined.tact b/src/interpreters/test/failed/var-track-outside-foreach-undetermined.tact new file mode 100644 index 000000000..7ff08bc2f --- /dev/null +++ b/src/interpreters/test/failed/var-track-outside-foreach-undetermined.tact @@ -0,0 +1,15 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let xMap: map = emptyMap(); + foreach (k, val in xMap) { // Currently, the analyzer cannot determine if a map is empty or not + a = 10 + k; // because it does not track map mutations using the set function. + a = 5; // So, it will treat foreach loops as if their number of iterations is undetermined. + } + return 1 / (a - 5); // If loop executes or not, all possible paths assign a = 5. Hence, there is a division by zero. + } +} diff --git a/src/interpreters/test/failed/var-track-outside-if-else-false-branch.tact b/src/interpreters/test/failed/var-track-outside-if-else-false-branch.tact new file mode 100644 index 000000000..960240ceb --- /dev/null +++ b/src/interpreters/test/failed/var-track-outside-if-else-false-branch.tact @@ -0,0 +1,16 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + if (a > 5) { + a = 10; + } else { + a = 3; + } + return 1 / (a - 3); // Division by zero, because condition in the if is false at compile time, + } // which means that a = 3 after the conditional. +} + \ No newline at end of file diff --git a/src/interpreters/test/failed/var-track-outside-if-else-true-branch.tact b/src/interpreters/test/failed/var-track-outside-if-else-true-branch.tact new file mode 100644 index 000000000..8e5d071a5 --- /dev/null +++ b/src/interpreters/test/failed/var-track-outside-if-else-true-branch.tact @@ -0,0 +1,16 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + if (a >= 5) { + a = 10; + } else { + a = 3; + } + return 1 / (a - 10); // Division by zero, because condition in the if is true at compile time, + } // which means that a = 10 after the conditional. +} + \ No newline at end of file diff --git a/src/interpreters/test/failed/var-track-outside-if-else-undetermined-no-assign-false-branch.tact b/src/interpreters/test/failed/var-track-outside-if-else-undetermined-no-assign-false-branch.tact new file mode 100644 index 000000000..7c0e3d632 --- /dev/null +++ b/src/interpreters/test/failed/var-track-outside-if-else-undetermined-no-assign-false-branch.tact @@ -0,0 +1,17 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 10; + if (v >= 5) { + a = 5; + } else { + x = 7; + } + return 1 / (a - 5); // Division by zero, even though condition cannot be determined at compile time + } // all paths before the return lead to a = 5. +} + \ No newline at end of file diff --git a/src/interpreters/test/failed/var-track-outside-if-else-undetermined-no-assign-true-branch.tact b/src/interpreters/test/failed/var-track-outside-if-else-undetermined-no-assign-true-branch.tact new file mode 100644 index 000000000..ea2a7c605 --- /dev/null +++ b/src/interpreters/test/failed/var-track-outside-if-else-undetermined-no-assign-true-branch.tact @@ -0,0 +1,17 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 10; + if (v >= 5) { + x = 3; + } else { + a = 5; + } + return 1 / (a - 5); // Division by zero, even though condition cannot be determined at compile time + } // all paths before the return lead to a = 5. +} + \ No newline at end of file diff --git a/src/interpreters/test/failed/var-track-outside-if-else-undetermined.tact b/src/interpreters/test/failed/var-track-outside-if-else-undetermined.tact new file mode 100644 index 000000000..fb4045c6a --- /dev/null +++ b/src/interpreters/test/failed/var-track-outside-if-else-undetermined.tact @@ -0,0 +1,16 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + if (v >= 5) { + a = 5; + } else { + a = 5; + } + return 1 / (a - 5); // Division by zero, even though condition cannot be determined at compile time + } // all branches inside the if also assign a = 5. +} + \ No newline at end of file diff --git a/src/interpreters/test/failed/var-track-outside-if-elseif-false-branch.tact b/src/interpreters/test/failed/var-track-outside-if-elseif-false-branch.tact new file mode 100644 index 000000000..69d1bac92 --- /dev/null +++ b/src/interpreters/test/failed/var-track-outside-if-elseif-false-branch.tact @@ -0,0 +1,16 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + if (a > 5) { + a = 10; + } else if (a >= 5) { + a = 3; + } + return 1 / (a - 3); // Division by zero, because conditions can be determined at compile time, + } // which means that a = 3 after the conditional. +} + \ No newline at end of file diff --git a/src/interpreters/test/failed/var-track-outside-if-elseif-true-branch.tact b/src/interpreters/test/failed/var-track-outside-if-elseif-true-branch.tact new file mode 100644 index 000000000..bb54d2383 --- /dev/null +++ b/src/interpreters/test/failed/var-track-outside-if-elseif-true-branch.tact @@ -0,0 +1,16 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + if (a >= 5) { + a = 10; + } else if (a > 5) { + a = 3; + } + return 1 / (a - 10); // Division by zero, because conditions can be determined at compile time, + } // which means that a = 10 after the conditional. +} + \ No newline at end of file diff --git a/src/interpreters/test/failed/var-track-outside-if-elseif-undetermined-no-assign-false-branch.tact b/src/interpreters/test/failed/var-track-outside-if-elseif-undetermined-no-assign-false-branch.tact new file mode 100644 index 000000000..0b9f35eb2 --- /dev/null +++ b/src/interpreters/test/failed/var-track-outside-if-elseif-undetermined-no-assign-false-branch.tact @@ -0,0 +1,17 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 10; + if (v >= 5) { + a = 5; + } else if (v > 5) { + x = 7; + } + return 1 / (a - 5); // Division by zero, even though condition cannot be determined at compile time + } // all paths before the return lead to a = 5. +} + \ No newline at end of file diff --git a/src/interpreters/test/failed/var-track-outside-if-elseif-undetermined-no-assign-true-branch.tact b/src/interpreters/test/failed/var-track-outside-if-elseif-undetermined-no-assign-true-branch.tact new file mode 100644 index 000000000..9e08185d8 --- /dev/null +++ b/src/interpreters/test/failed/var-track-outside-if-elseif-undetermined-no-assign-true-branch.tact @@ -0,0 +1,17 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 10; + if (v >= 5) { + x = 3; + } else if (v > 5) { + a = 5; + } + return 1 / (a - 5); // Division by zero, even though condition cannot be determined at compile time + } // all paths before the return lead to a = 5. +} + \ No newline at end of file diff --git a/src/interpreters/test/failed/var-track-outside-if-elseif-undetermined.tact b/src/interpreters/test/failed/var-track-outside-if-elseif-undetermined.tact new file mode 100644 index 000000000..6d26b9c3c --- /dev/null +++ b/src/interpreters/test/failed/var-track-outside-if-elseif-undetermined.tact @@ -0,0 +1,16 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + if (v >= 5) { + a = 5; + } else if (v > 5) { + a = 5; + } + return 1 / (a - 5); // Division by zero, even though condition cannot be determined at compile time + } // all branches inside the if also assign a = 5. +} + \ No newline at end of file diff --git a/src/interpreters/test/failed/var-track-outside-if-false.tact b/src/interpreters/test/failed/var-track-outside-if-false.tact new file mode 100644 index 000000000..719a03cae --- /dev/null +++ b/src/interpreters/test/failed/var-track-outside-if-false.tact @@ -0,0 +1,14 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + if (a > 5) { + a = 10; + } + return 1 / (a - 5); // Division by zero, because condition in the if is false at compile time, + } // which means that a = 5 after the conditional. +} + \ No newline at end of file diff --git a/src/interpreters/test/failed/var-track-outside-if-true.tact b/src/interpreters/test/failed/var-track-outside-if-true.tact new file mode 100644 index 000000000..434761556 --- /dev/null +++ b/src/interpreters/test/failed/var-track-outside-if-true.tact @@ -0,0 +1,14 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + if (a >= 5) { + a = 10; + } + return 1 / (a - 10); // Division by zero, because condition in the if is true at compile time, + } // which means that a = 10 after the conditional. +} + \ No newline at end of file diff --git a/src/interpreters/test/failed/var-track-outside-if-undetermined.tact b/src/interpreters/test/failed/var-track-outside-if-undetermined.tact new file mode 100644 index 000000000..050c3444d --- /dev/null +++ b/src/interpreters/test/failed/var-track-outside-if-undetermined.tact @@ -0,0 +1,14 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + if (v >= 5) { + a = 5; + } + return 1 / (a - 5); // Division by zero, even though condition cannot be determined at compile time + } // the branch inside the if also assigns a = 5. +} + \ No newline at end of file diff --git a/src/interpreters/test/failed/var-track-outside-repeat-no-iterations.tact b/src/interpreters/test/failed/var-track-outside-repeat-no-iterations.tact new file mode 100644 index 000000000..6afb18be3 --- /dev/null +++ b/src/interpreters/test/failed/var-track-outside-repeat-no-iterations.tact @@ -0,0 +1,14 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 0; + repeat (x) { + a = 10; + } + return 1 / (a - 5); // Loop does not execute. Hence, after the loop, a = 5, + } // which means division by zero. +} diff --git a/src/interpreters/test/failed/var-track-outside-repeat-undetermined.tact b/src/interpreters/test/failed/var-track-outside-repeat-undetermined.tact new file mode 100644 index 000000000..c02aad334 --- /dev/null +++ b/src/interpreters/test/failed/var-track-outside-repeat-undetermined.tact @@ -0,0 +1,15 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 10; + repeat (v) { // v does not have a value at compile time + a += 10; + a = v - v + 5; + } + return 1 / (a - 5); // Unknown if loop executes. But if it does or not, all paths lead to a = 5, + } // which means division by zero after the loop. +} diff --git a/src/interpreters/test/failed/var-track-outside-repeat-with-iterations.tact b/src/interpreters/test/failed/var-track-outside-repeat-with-iterations.tact new file mode 100644 index 000000000..929b6367f --- /dev/null +++ b/src/interpreters/test/failed/var-track-outside-repeat-with-iterations.tact @@ -0,0 +1,15 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 10; + repeat (x) { + a += 10; + a = v - v + 3; + } + return 1 / (a - 3); // Loop executes. After each iteration, a = 3, + } // which means division by zero after the loop. +} diff --git a/src/interpreters/test/failed/var-track-outside-try-catch-undetermined.tact b/src/interpreters/test/failed/var-track-outside-try-catch-undetermined.tact new file mode 100644 index 000000000..56b027b7f --- /dev/null +++ b/src/interpreters/test/failed/var-track-outside-try-catch-undetermined.tact @@ -0,0 +1,17 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 0; + try { + x += v; + a = x - x + 5; + } catch (e) { + a = 5; + } + return 1 / (a - 5); // Independently if the try successfully executes or not, all paths lead to a = 5. + } // Hence, division by zero. Note that if the catch executes, it also assigns a = 5. +} diff --git a/src/interpreters/test/failed/var-track-outside-try-undetermined.tact b/src/interpreters/test/failed/var-track-outside-try-undetermined.tact new file mode 100644 index 000000000..520dd89c6 --- /dev/null +++ b/src/interpreters/test/failed/var-track-outside-try-undetermined.tact @@ -0,0 +1,15 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 0; + try { + x += v; + a = x - x + 5; + } + return 1 / (a - 5); // Independently if the try successfully executes or not, all paths lead to a = 5. + } // Hence, division by zero. Note that the catch is empty, which means that variable +} // a remains unchanged (meaning a = 5 also) if the catch executes. diff --git a/src/interpreters/test/failed/var-track-outside-while-no-iterations.tact b/src/interpreters/test/failed/var-track-outside-while-no-iterations.tact new file mode 100644 index 000000000..eb1b5a5b0 --- /dev/null +++ b/src/interpreters/test/failed/var-track-outside-while-no-iterations.tact @@ -0,0 +1,14 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 0; + while (x > 0) { + a = 10; + } + return 1 / (a - 5); // Loop does not execute. Hence, after the loop, a = 5, + } // which means division by zero. +} diff --git a/src/interpreters/test/failed/var-track-outside-while-undetermined.tact b/src/interpreters/test/failed/var-track-outside-while-undetermined.tact new file mode 100644 index 000000000..81383d191 --- /dev/null +++ b/src/interpreters/test/failed/var-track-outside-while-undetermined.tact @@ -0,0 +1,15 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 10; + while (v > 0) { // v does not have a value at compile time + a += 10; + a = v - v + 5; + } + return 1 / (a - 5); // Unknown if loop executes. But if it does or not, all paths lead to a = 5, + } // which means division by zero after the loop. +} diff --git a/src/interpreters/test/failed/var-track-outside-while-with-iterations.tact b/src/interpreters/test/failed/var-track-outside-while-with-iterations.tact new file mode 100644 index 000000000..7372dd68f --- /dev/null +++ b/src/interpreters/test/failed/var-track-outside-while-with-iterations.tact @@ -0,0 +1,16 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 10; + while (x >= 10) { + a += 10; + a = v - v + 3; + x -= 1; + } + return 1 / (a - 3); // Loop executes. After each iteration, a = 3, + } // which means division by zero after the loop. +} diff --git a/src/interpreters/test/failed/var-track-structs-in-parameter.tact b/src/interpreters/test/failed/var-track-structs-in-parameter.tact new file mode 100644 index 000000000..41b3b9b90 --- /dev/null +++ b/src/interpreters/test/failed/var-track-structs-in-parameter.tact @@ -0,0 +1,32 @@ +primitive Int; +primitive Bool; +trait BaseTrait {} + +struct SA { + Aa: Int; + Ab: SB; +} + +struct SB { + Ba: Bool; + Bb: Int; + Bc: Int; +} + +struct SC { + Ca: SA; +} + +contract Test { + + f: SC = SC {Ca: SA {Aa: 5, Ab: SB {Ba: false, Bb: 6, Bc: 10}}}; + + fun foo(v: SC): Int { + v.Ca.Ab.Bc = 0; // Every other field in v is undetermined (including nested fields). + let a = v; // The only determined field in a is a.Ca.Ab.Bc + 1 / a.Ca.Ab.Bb; // OK, since value of a.Ca.Ab.Bb is unknown. + self.f = a; // The only determined field in self.f is self.f.Ca.Ab.Bc + let b = self; // The only determined field in b is b.f.Ca.Ab.Bc + return 1 / b.f.Ca.Ab.Bc; // Division by zero. + } +} diff --git a/src/interpreters/test/failed/var-track-structs.tact b/src/interpreters/test/failed/var-track-structs.tact new file mode 100644 index 000000000..405c0f06b --- /dev/null +++ b/src/interpreters/test/failed/var-track-structs.tact @@ -0,0 +1,38 @@ +primitive Int; +primitive Bool; +trait BaseTrait {} + +struct SA { + Aa: Int; + Ab: SB; +} + +struct SB { + Ba: Bool; + Bb: Int; + Bc: Int; +} + +contract Test { + + f: SB = SB {Ba: false, Bb: 10, Bc: 2 + 2}; + + fun foo(v: Int): Int { + let a: SA = SA {Aa: 0, Ab: SB {Ba: true, Bb: 5, Bc: 0}}; + a.Aa = v; // Since v is undetermined, field Aa is also undetermined. + 1 / a.Aa; // OK because a.Aa is undetermined. + 1 / a.Ab.Bb; // OK + // 1 / a.Ab.Bc; // Commented because it would cause a division by zero. + a.Ab = self.f; // All fields in a.Ab become undetermined. + // Even though self.f has a default value, the analyzer cannot assume + // that self.f remained unchanged before calling foo, since other + // parts of the code could change self.f. + 1 / a.Ab.Bc; // This no longer causes a division by zero at compile time. + 1 / a.Ab.Bb; // OK as well + self.f.Bb = 0; // Determine only the Bb field of self.f. The rest of fields remain undetermined. + a.Ab = self.f; // Only field a.Ab.Bb is determined, the rest of fields in a.Ab is undetermined. + 1 / a.Ab.Bc; // OK. + 1 / a.Ab.Bb; // Division by zero. + return 0; + } +} \ No newline at end of file diff --git a/src/interpreters/test/success/var-track-outside-do-until-mutatingFun.tact b/src/interpreters/test/success/var-track-outside-do-until-mutatingFun.tact new file mode 100644 index 000000000..4d5644979 --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-do-until-mutatingFun.tact @@ -0,0 +1,20 @@ +primitive Int; +trait BaseTrait {} + +extends mutates fun mutateFun(self: Int) { + self = 5; +} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 10; + do { + a += 10; + a.mutateFun(); // The analyzer treats mutating functions as black boxes. "a" becomes undetermined at this point. + } until (v > 0); // v does not have a value at compile time + return 1 / (a - 5); // Unknown if loop executes. But if it does or not, not all paths lead to a = 5, + // because "a" becomes undetermined inside the loop. + } // which means no division by zero detected. +} diff --git a/src/interpreters/test/success/var-track-outside-do-until.tact b/src/interpreters/test/success/var-track-outside-do-until.tact new file mode 100644 index 000000000..45a849f3c --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-do-until.tact @@ -0,0 +1,15 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 10; + do { + a += 10; + a = v - v + 6; + } until (v > 0); // v does not have a value at compile time + return 1 / (a - 5); // Unknown if loop executes. But if it does or not, not all paths lead to a = 5, + } // which means no division by zero detected. +} diff --git a/src/interpreters/test/success/var-track-outside-foreach-mutatingFun.tact b/src/interpreters/test/success/var-track-outside-foreach-mutatingFun.tact new file mode 100644 index 000000000..f8746ec16 --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-foreach-mutatingFun.tact @@ -0,0 +1,22 @@ +primitive Int; +trait BaseTrait {} + +extends mutates fun mutateFun(self: Int) { + self = 5; +} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let xMap: map = emptyMap(); + foreach (k, val in xMap) { // Currently, the analyzer cannot determine if a map is empty or not + a = 10 + k; // because it does not track map mutations using the set function. + a = 5; // So, it will treat foreach loops as if their number of iterations is undetermined. + a.mutateFun(); // The analyzer treats mutating functions as black boxes. "a" becomes undetermined at this point. + } + return 1 / (a - 5); // If loop executes or not, not all paths assign a = 5. + // because "a" becomes undetermined inside the loop. + // Hence, there is no division by zero detected. + } +} diff --git a/src/interpreters/test/success/var-track-outside-foreach.tact b/src/interpreters/test/success/var-track-outside-foreach.tact new file mode 100644 index 000000000..8569dd2cc --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-foreach.tact @@ -0,0 +1,16 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let xMap: map = emptyMap(); + foreach (k, val in xMap) { // Currently, the analyzer cannot determine if a map is empty or not + a = 10 + k; // because it does not track map mutations using the set function. + a = 6; // So, it will treat foreach loops as if their number of iterations is undetermined. + } + return 1 / (a - 5); // If loop executes or not, not all paths assign a = 5. + // Hence, there is no division by zero detected. + } +} diff --git a/src/interpreters/test/success/var-track-outside-if-else-mutatingFun.tact b/src/interpreters/test/success/var-track-outside-if-else-mutatingFun.tact new file mode 100644 index 000000000..81fa8a90b --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-if-else-mutatingFun.tact @@ -0,0 +1,20 @@ +primitive Int; +trait BaseTrait {} + +extends mutates fun mutateFun(self: Int) { + self = 5; +} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + if (v >= 5) { + a = 5; + } else { + a.mutateFun(); // The analyzer treats mutating functions as black boxes. "a" becomes undetermined at this point. + } + return 1 / (a - 5); // No division by zero detected: condition cannot be determined at compile time + } // and not all branches inside the if assign a = 5, because "a" becomes undetermined in one branch. +} + \ No newline at end of file diff --git a/src/interpreters/test/success/var-track-outside-if-else-no-assign-false-branch-mutatingFun.tact b/src/interpreters/test/success/var-track-outside-if-else-no-assign-false-branch-mutatingFun.tact new file mode 100644 index 000000000..ae4234dde --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-if-else-no-assign-false-branch-mutatingFun.tact @@ -0,0 +1,21 @@ +primitive Int; +trait BaseTrait {} + +extends mutates fun mutateFun(self: Int) { + self = 5; +} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 10; + if (v >= 5) { + a.mutateFun(); // The analyzer treats mutating functions as black boxes. "a" becomes undetermined at this point. + } else { + x = 7; + } + return 1 / (a - 5); // No division by zero detected: condition cannot be determined at compile time + } // and not all paths lead to a = 5. +} + \ No newline at end of file diff --git a/src/interpreters/test/success/var-track-outside-if-else-no-assign-false-branch.tact b/src/interpreters/test/success/var-track-outside-if-else-no-assign-false-branch.tact new file mode 100644 index 000000000..5d96e246a --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-if-else-no-assign-false-branch.tact @@ -0,0 +1,17 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 10; + if (v >= 5) { + a = 6; + } else { + x = 7; + } + return 1 / (a - 5); // No division by zero detected: condition cannot be determined at compile time + } // and not all paths lead to a = 5. +} + \ No newline at end of file diff --git a/src/interpreters/test/success/var-track-outside-if-else-no-assign-true-branch-mutatingFun.tact b/src/interpreters/test/success/var-track-outside-if-else-no-assign-true-branch-mutatingFun.tact new file mode 100644 index 000000000..fce86cfa9 --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-if-else-no-assign-true-branch-mutatingFun.tact @@ -0,0 +1,21 @@ +primitive Int; +trait BaseTrait {} + +extends mutates fun mutateFun(self: Int) { + self = 5; +} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 10; + if (v >= 5) { + x = 3; + } else { + a.mutateFun(); // The analyzer treats mutating functions as black boxes. "a" becomes undetermined at this point. + } + return 1 / (a - 5); // No division by zero detected: condition cannot be determined at compile time + } // and not all paths lead to a = 5. +} + \ No newline at end of file diff --git a/src/interpreters/test/success/var-track-outside-if-else-no-assign-true-branch.tact b/src/interpreters/test/success/var-track-outside-if-else-no-assign-true-branch.tact new file mode 100644 index 000000000..1983dcc0f --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-if-else-no-assign-true-branch.tact @@ -0,0 +1,17 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 10; + if (v >= 5) { + x = 3; + } else { + a = 6; + } + return 1 / (a - 5); // No division by zero detected: condition cannot be determined at compile time + } // and not all paths lead to a = 5. +} + \ No newline at end of file diff --git a/src/interpreters/test/success/var-track-outside-if-else.tact b/src/interpreters/test/success/var-track-outside-if-else.tact new file mode 100644 index 000000000..9120beb55 --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-if-else.tact @@ -0,0 +1,16 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + if (v >= 5) { + a = 5; + } else { + a = 6; + } + return 1 / (a - 5); // No division by zero detected: condition cannot be determined at compile time + } // and not all branches inside the if assign a = 5. +} + \ No newline at end of file diff --git a/src/interpreters/test/success/var-track-outside-if-elseif-else-mutatingFun.tact b/src/interpreters/test/success/var-track-outside-if-elseif-else-mutatingFun.tact new file mode 100644 index 000000000..d7a91057d --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-if-elseif-else-mutatingFun.tact @@ -0,0 +1,23 @@ +primitive Int; +trait BaseTrait {} + +extends mutates fun mutateFun(self: Int) { + self = 5; +} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 10; + if (v >= 5) { + a.mutateFun(); // The analyzer treats mutating functions as black boxes. "a" becomes undetermined at this point. + } else if (x > 5) { // Since x > 5 is true, a = 5 after the nested conditional. + a = 5; // However, v >= 5 cannot be evaluated, so, two branches lead to "a = undetermined" and a = 5. + } else { + a = 7; + } + return 1 / (a - 5); // No division by zero detected. + } +} + \ No newline at end of file diff --git a/src/interpreters/test/success/var-track-outside-if-elseif-else.tact b/src/interpreters/test/success/var-track-outside-if-elseif-else.tact new file mode 100644 index 000000000..e0cb4001b --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-if-elseif-else.tact @@ -0,0 +1,19 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 10; + if (v >= 5) { + a = 6; + } else if (x > 5) { // Since the condition is true, a = 7 after the nested conditional. + a = 7; // However, v >= 5 cannot be evaluated, so, two branches lead to a = 6 and a = 7. + } else { + a = 5; + } + return 1 / (a - 7); // No division by zero detected. + } +} + \ No newline at end of file diff --git a/src/interpreters/test/success/var-track-outside-if-elseif-mutatingFun.tact b/src/interpreters/test/success/var-track-outside-if-elseif-mutatingFun.tact new file mode 100644 index 000000000..a01f7bafe --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-if-elseif-mutatingFun.tact @@ -0,0 +1,20 @@ +primitive Int; +trait BaseTrait {} + +extends mutates fun mutateFun(self: Int) { + self = 5; +} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + if (v >= 5) { + a = 5; + } else if (v > 5) { + a.mutateFun(); // The analyzer treats mutating functions as black boxes. "a" becomes undetermined at this point. + } + return 1 / (a - 5); // No division by zero detected: condition cannot be determined at compile time + } // and not all branches assign a = 5. +} + \ No newline at end of file diff --git a/src/interpreters/test/success/var-track-outside-if-elseif-no-assign-false-branch-mutatingFun.tact b/src/interpreters/test/success/var-track-outside-if-elseif-no-assign-false-branch-mutatingFun.tact new file mode 100644 index 000000000..38c82941d --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-if-elseif-no-assign-false-branch-mutatingFun.tact @@ -0,0 +1,21 @@ +primitive Int; +trait BaseTrait {} + +extends mutates fun mutateFun(self: Int) { + self = 5; +} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 10; + if (v >= 5) { + a.mutateFun(); // The analyzer treats mutating functions as black boxes. "a" becomes undetermined at this point. + } else if (v > 5) { + x = 7; + } + return 1 / (a - 5); // No division by zero detected: condition cannot be determined at compile time + } // and not all paths before the return lead to a = 5. +} + \ No newline at end of file diff --git a/src/interpreters/test/success/var-track-outside-if-elseif-no-assign-false-branch.tact b/src/interpreters/test/success/var-track-outside-if-elseif-no-assign-false-branch.tact new file mode 100644 index 000000000..d578c45ec --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-if-elseif-no-assign-false-branch.tact @@ -0,0 +1,17 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 10; + if (v >= 5) { + a = 6; + } else if (v > 5) { + x = 7; + } + return 1 / (a - 5); // No division by zero detected: condition cannot be determined at compile time + } // and not all paths before the return lead to a = 5. +} + \ No newline at end of file diff --git a/src/interpreters/test/success/var-track-outside-if-elseif-no-assign-true-branch-mutatingFun.tact b/src/interpreters/test/success/var-track-outside-if-elseif-no-assign-true-branch-mutatingFun.tact new file mode 100644 index 000000000..7f4d07563 --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-if-elseif-no-assign-true-branch-mutatingFun.tact @@ -0,0 +1,21 @@ +primitive Int; +trait BaseTrait {} + +extends mutates fun mutateFun(self: Int) { + self = 5; +} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 10; + if (v >= 5) { + x = 3; + } else if (v > 5) { + a.mutateFun(); // The analyzer treats mutating functions as black boxes. "a" becomes undetermined at this point. + } + return 1 / (a - 5); // No division by zero detected: condition cannot be determined at compile time + } // and not all paths before the return lead to a = 5. +} + \ No newline at end of file diff --git a/src/interpreters/test/success/var-track-outside-if-elseif-no-assign-true-branch.tact b/src/interpreters/test/success/var-track-outside-if-elseif-no-assign-true-branch.tact new file mode 100644 index 000000000..3157f4c8e --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-if-elseif-no-assign-true-branch.tact @@ -0,0 +1,17 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 10; + if (v >= 5) { + x = 3; + } else if (v > 5) { + a = 6; + } + return 1 / (a - 5); // No division by zero detected: condition cannot be determined at compile time + } // and not all paths before the return lead to a = 5. +} + \ No newline at end of file diff --git a/src/interpreters/test/success/var-track-outside-if-elseif.tact b/src/interpreters/test/success/var-track-outside-if-elseif.tact new file mode 100644 index 000000000..e258293f1 --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-if-elseif.tact @@ -0,0 +1,16 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + if (v >= 5) { + a = 5; + } else if (v > 5) { + a = 6; + } + return 1 / (a - 5); // No division by zero detected: condition cannot be determined at compile time + } // and not all branches assign a = 5. +} + \ No newline at end of file diff --git a/src/interpreters/test/success/var-track-outside-if-mutatingFun.tact b/src/interpreters/test/success/var-track-outside-if-mutatingFun.tact new file mode 100644 index 000000000..78b474f4a --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-if-mutatingFun.tact @@ -0,0 +1,18 @@ +primitive Int; +trait BaseTrait {} + +extends mutates fun mutateFun(self: Int) { + self = 5; +} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + if (v >= 5) { + a.mutateFun(); // The analyzer treats mutating functions as black boxes. "a" becomes undetermined at this point. + } + return 1 / (a - 5); // No division by zero detected: condition cannot be determined at compile time + } // and not all branches assign a = 5. +} + \ No newline at end of file diff --git a/src/interpreters/test/success/var-track-outside-if.tact b/src/interpreters/test/success/var-track-outside-if.tact new file mode 100644 index 000000000..df2431712 --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-if.tact @@ -0,0 +1,14 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + if (v >= 5) { + a = 6; + } + return 1 / (a - 5); // No division by zero detected: condition cannot be determined at compile time + } // and not all branches assign a = 5. +} + \ No newline at end of file diff --git a/src/interpreters/test/success/var-track-outside-repeat-mutatingFun.tact b/src/interpreters/test/success/var-track-outside-repeat-mutatingFun.tact new file mode 100644 index 000000000..015250de4 --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-repeat-mutatingFun.tact @@ -0,0 +1,19 @@ +primitive Int; +trait BaseTrait {} + +extends mutates fun mutateFun(self: Int) { + self = 5; +} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 10; + repeat (v) { // v does not have a value at compile time + a += 10; + a.mutateFun(); // The analyzer treats mutating functions as black boxes. "a" becomes undetermined at this point. + } + return 1 / (a - 5); // Unknown if loop executes. But if it does or not, not all paths lead to a = 5. + } // Hence, no division by zero detected. +} diff --git a/src/interpreters/test/success/var-track-outside-repeat.tact b/src/interpreters/test/success/var-track-outside-repeat.tact new file mode 100644 index 000000000..24ba87a22 --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-repeat.tact @@ -0,0 +1,15 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 10; + repeat (v) { // v does not have a value at compile time + a += 10; + a = v - v + 6; + } + return 1 / (a - 5); // Unknown if loop executes. But if it does or not, not all paths lead to a = 5. + } // Hence, no division by zero detected. +} diff --git a/src/interpreters/test/success/var-track-outside-try-catch-mutatingFun.tact b/src/interpreters/test/success/var-track-outside-try-catch-mutatingFun.tact new file mode 100644 index 000000000..da2e0cbfb --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-try-catch-mutatingFun.tact @@ -0,0 +1,21 @@ +primitive Int; +trait BaseTrait {} + +extends mutates fun mutateFun(self: Int) { + self = 5; +} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 0; + try { + x += v; + a.mutateFun(); // The analyzer treats mutating functions as black boxes. "a" becomes undetermined at this point. + } catch (e) { + a = 5; + } + return 1 / (a - 5); // Independently if the try successfully executes or not, not all paths lead to a = 5. + } // Hence, no division by zero detected. +} diff --git a/src/interpreters/test/success/var-track-outside-try-catch.tact b/src/interpreters/test/success/var-track-outside-try-catch.tact new file mode 100644 index 000000000..11831a36a --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-try-catch.tact @@ -0,0 +1,17 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 0; + try { + x += v; + a = x - x + 6; + } catch (e) { + a = 5; + } + return 1 / (a - 5); // Independently if the try successfully executes or not, not all paths lead to a = 5. + } // Hence, no division by zero detected. +} diff --git a/src/interpreters/test/success/var-track-outside-try-mutatingFun.tact b/src/interpreters/test/success/var-track-outside-try-mutatingFun.tact new file mode 100644 index 000000000..fa0bbf475 --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-try-mutatingFun.tact @@ -0,0 +1,19 @@ +primitive Int; +trait BaseTrait {} + +extends mutates fun mutateFun(self: Int) { + self = 5; +} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 0; + try { + x += v; + a.mutateFun(); // The analyzer treats mutating functions as black boxes. "a" becomes undetermined at this point. + } + return 1 / (a - 5); // Independently if the try successfully executes or not, not all paths lead to a = 5. + } // Hence, no division by zero detected. +} \ No newline at end of file diff --git a/src/interpreters/test/success/var-track-outside-try.tact b/src/interpreters/test/success/var-track-outside-try.tact new file mode 100644 index 000000000..ae0c916a7 --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-try.tact @@ -0,0 +1,15 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 0; + try { + x += v; + a = x - x + 6; + } + return 1 / (a - 5); // Independently if the try successfully executes or not, not all paths lead to a = 5. + } // Hence, no division by zero detected. +} \ No newline at end of file diff --git a/src/interpreters/test/success/var-track-outside-while-mutatingFun.tact b/src/interpreters/test/success/var-track-outside-while-mutatingFun.tact new file mode 100644 index 000000000..b7eb7ae62 --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-while-mutatingFun.tact @@ -0,0 +1,19 @@ +primitive Int; +trait BaseTrait {} + +extends mutates fun mutateFun(self: Int) { + self = 5; +} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 10; + while (v > 0) { // v does not have a value at compile time + a += 10; + a.mutateFun(); // The analyzer treats mutating functions as black boxes. "a" becomes undetermined at this point. + } + return 1 / (a - 5); // Unknown if loop executes. But if it does or not, not all paths lead to a = 5, + } // which means no division by zero detected. +} diff --git a/src/interpreters/test/success/var-track-outside-while.tact b/src/interpreters/test/success/var-track-outside-while.tact new file mode 100644 index 000000000..690c22542 --- /dev/null +++ b/src/interpreters/test/success/var-track-outside-while.tact @@ -0,0 +1,15 @@ +primitive Int; +trait BaseTrait {} + +contract Test { + + get fun foo(v: Int): Int { + let a: Int = 5; + let x: Int = 10; + while (v > 0) { // v does not have a value at compile time + a += 10; + a = v - v + 6; + } + return 1 / (a - 5); // Unknown if loop executes. But if it does or not, not all paths lead to a = 5, + } // which means no division by zero detected. +} diff --git a/src/interpreters/util.ts b/src/interpreters/util.ts new file mode 100644 index 000000000..f0fc6da63 --- /dev/null +++ b/src/interpreters/util.ts @@ -0,0 +1,47 @@ +// Throws a non-fatal const-eval error, in the sense that const-eval as a compiler +// optimization cannot be applied, e.g. to `let`-statements. + +import { throwConstEvalError } from "../errors"; +import { SrcInfo } from "../grammar/ast"; + +// Note that for const initializers this is a show-stopper. +export function throwNonFatalErrorConstEval( + msg: string, + source: SrcInfo, +): never { + throwConstEvalError(`Cannot evaluate expression: ${msg}`, false, source); +} + +// Throws a fatal const-eval, meaning this is a meaningless program, +// so compilation should be aborted in all cases +export function throwErrorConstEval(msg: string, source: SrcInfo): never { + throwConstEvalError(`Cannot evaluate expression: ${msg}`, true, source); +} + +// bigint arithmetic + +// precondition: the divisor is not zero +// rounds the division result towards negative infinity +export function divFloor(a: bigint, b: bigint): bigint { + const almostSameSign = a > 0n === b > 0n; + if (almostSameSign) { + return a / b; + } + return a / b + (a % b === 0n ? 0n : -1n); +} + +export function abs(a: bigint): bigint { + return a < 0n ? -a : a; +} + +export function sign(a: bigint): bigint { + if (a === 0n) return 0n; + else return a < 0n ? -1n : 1n; +} + +// precondition: the divisor is not zero +// rounds the result towards negative infinity +// Uses the fact that a / b * b + a % b == a, for all b != 0. +export function modFloor(a: bigint, b: bigint): bigint { + return a - divFloor(a, b) * b; +} diff --git a/src/optimizer/algebraic.ts b/src/optimizer/algebraic.ts index 27c1bdb86..5bee93ce9 100644 --- a/src/optimizer/algebraic.ts +++ b/src/optimizer/algebraic.ts @@ -6,6 +6,7 @@ import { eqExpressions, isValue, } from "../grammar/ast"; +import { throwErrorConstEval } from "../interpreters/util"; import { ExpressionTransformer, Rule } from "./types"; import { checkIsBinaryOpNode, @@ -28,20 +29,14 @@ export class AddZero extends Rule { if (checkIsBinaryOpNode(ast)) { const topLevelNode = ast as AstOpBinary; if (this.additiveOperators.includes(topLevelNode.op)) { - if ( - !isValue(topLevelNode.left) && - checkIsNumber(topLevelNode.right, 0n) - ) { + if (checkIsNumber(topLevelNode.right, 0n)) { // The tree has this form: // x op 0 const x = topLevelNode.left; return x; - } else if ( - checkIsNumber(topLevelNode.left, 0n) && - !isValue(topLevelNode.right) - ) { + } else if (checkIsNumber(topLevelNode.left, 0n)) { // The tree has this form: // 0 op x @@ -49,7 +44,7 @@ export class AddZero extends Rule { const op = topLevelNode.op; if (op === "-") { - return makeUnaryExpression("-", x); + return makeUnaryExpression("-", x, ast.loc); } else { return x; } @@ -72,21 +67,23 @@ export class MultiplyZero extends Rule { const topLevelNode = ast as AstOpBinary; if (topLevelNode.op === "*") { if ( - checkIsName(topLevelNode.left) && + (checkIsName(topLevelNode.left) || + isValue(topLevelNode.left)) && checkIsNumber(topLevelNode.right, 0n) ) { // The tree has this form: - // x * 0, where x is an identifier + // x * 0, where x is an identifier or a value - return makeValueExpression(0n); + return makeValueExpression(0n, ast.loc); } else if ( checkIsNumber(topLevelNode.left, 0n) && - checkIsName(topLevelNode.right) + (checkIsName(topLevelNode.right) || + isValue(topLevelNode.right)) ) { // The tree has this form: - // 0 * x, where x is an identifier + // 0 * x, where x is an identifier or a value - return makeValueExpression(0n); + return makeValueExpression(0n, ast.loc); } } } @@ -105,20 +102,14 @@ export class MultiplyOne extends Rule { if (checkIsBinaryOpNode(ast)) { const topLevelNode = ast as AstOpBinary; if (topLevelNode.op === "*") { - if ( - !isValue(topLevelNode.left) && - checkIsNumber(topLevelNode.right, 1n) - ) { + if (checkIsNumber(topLevelNode.right, 1n)) { // The tree has this form: // x * 1 const x = topLevelNode.left; return x; - } else if ( - checkIsNumber(topLevelNode.left, 1n) && - !isValue(topLevelNode.right) - ) { + } else if (checkIsNumber(topLevelNode.left, 1n)) { // The tree has this form: // 1 * x @@ -144,8 +135,10 @@ export class SubtractSelf extends Rule { const topLevelNode = ast as AstOpBinary; if (topLevelNode.op === "-") { if ( - checkIsName(topLevelNode.left) && - checkIsName(topLevelNode.right) + (checkIsName(topLevelNode.left) || + isValue(topLevelNode.left)) && + (checkIsName(topLevelNode.right) || + isValue(topLevelNode.right)) ) { // The tree has this form: // x - y @@ -155,7 +148,7 @@ export class SubtractSelf extends Rule { const y = topLevelNode.right; if (eqExpressions(x, y)) { - return makeValueExpression(0n); + return makeValueExpression(0n, ast.loc); } } } @@ -175,27 +168,46 @@ export class AddSelf extends Rule { if (checkIsBinaryOpNode(ast)) { const topLevelNode = ast as AstOpBinary; if (topLevelNode.op === "+") { - if ( - !isValue(topLevelNode.left) && - !isValue(topLevelNode.right) - ) { - // The tree has this form: - // x + y - // We need to check that x and y are equal + // The tree has this form: + // x + y + // We need to check that x and y are equal - const x = topLevelNode.left; - const y = topLevelNode.right; + const x = topLevelNode.left; + const y = topLevelNode.right; - if (eqExpressions(x, y)) { - const res = makeBinaryExpression( - "*", - x, - makeValueExpression(2n), - ); - // Since we joined the tree, there is further opportunity - // for simplification - return optimizer.applyRules(res); - } + if (eqExpressions(x, y)) { + const res = makeBinaryExpression( + "*", + x, + makeValueExpression(2n, ast.loc), + ast.loc, + ); + // Since we joined the tree, there is further opportunity + // for simplification + return optimizer.applyRules(res); + } + } + } + + // If execution reaches here, it means that the rule could not be applied fully + // so, we return the original tree + return ast; + } +} + +export class DivByZero extends Rule { + public applyRule( + ast: AstExpression, + _optimizer: ExpressionTransformer, + ): AstExpression { + if (checkIsBinaryOpNode(ast)) { + const topLevelNode = ast as AstOpBinary; + if (topLevelNode.op === "/") { + if (checkIsNumber(topLevelNode.right, 0n)) { + // The tree has this form: + // x / 0 + + throwErrorConstEval("divisor must be non-zero", ast.loc); } } } @@ -222,12 +234,12 @@ export class OrTrue extends Rule { // The tree has this form: // x || true, where x is an identifier or a value - return makeValueExpression(true); + return makeValueExpression(true, ast.loc); } else if (checkIsBoolean(topLevelNode.left, true)) { // The tree has this form: // true || x - return makeValueExpression(true); + return makeValueExpression(true, ast.loc); } } } @@ -254,12 +266,12 @@ export class AndFalse extends Rule { // The tree has this form: // x && false, where x is an identifier or a value - return makeValueExpression(false); + return makeValueExpression(false, ast.loc); } else if (checkIsBoolean(topLevelNode.left, false)) { // The tree has this form: // false && x - return makeValueExpression(false); + return makeValueExpression(false, ast.loc); } } } @@ -411,7 +423,7 @@ export class ExcludedMiddle extends Rule { (checkIsName(x) || isValue(x)) && eqExpressions(x, y) ) { - return makeValueExpression(true); + return makeValueExpression(true, ast.loc); } } } else if (checkIsUnaryOpNode(topLevelNode.left)) { @@ -429,7 +441,7 @@ export class ExcludedMiddle extends Rule { (checkIsName(x) || isValue(x)) && eqExpressions(x, y) ) { - return makeValueExpression(true); + return makeValueExpression(true, ast.loc); } } } @@ -465,7 +477,7 @@ export class Contradiction extends Rule { (checkIsName(x) || isValue(x)) && eqExpressions(x, y) ) { - return makeValueExpression(false); + return makeValueExpression(false, ast.loc); } } } else if (checkIsUnaryOpNode(topLevelNode.left)) { @@ -483,7 +495,7 @@ export class Contradiction extends Rule { (checkIsName(x) || isValue(x)) && eqExpressions(x, y) ) { - return makeValueExpression(false); + return makeValueExpression(false, ast.loc); } } } @@ -536,7 +548,7 @@ export class NegateTrue extends Rule { // The tree has this form // !true - return makeValueExpression(false); + return makeValueExpression(false, ast.loc); } } } @@ -559,7 +571,7 @@ export class NegateFalse extends Rule { // The tree has this form // !false - return makeValueExpression(true); + return makeValueExpression(true, ast.loc); } } } diff --git a/src/optimizer/associative.ts b/src/optimizer/associative.ts index 865872588..a0a546149 100644 --- a/src/optimizer/associative.ts +++ b/src/optimizer/associative.ts @@ -6,19 +6,19 @@ import { AstOpBinary, AstValue, isValue, + SrcInfo, } from "../grammar/ast"; -import * as interpreterModule from "../interpreter"; +import * as interpreterModule from "../interpreters/standard"; +import { abs, sign } from "../interpreters/util"; import { Value } from "../types/types"; import { ExpressionTransformer, Rule } from "./types"; import { - abs, checkIsBinaryOpNode, checkIsBinaryOp_With_RightValue, checkIsBinaryOp_With_LeftValue, extractValue, makeBinaryExpression, makeValueExpression, - sign, } from "./util"; type TransformData = { @@ -26,10 +26,15 @@ type TransformData = { safetyCondition: boolean; }; -type Transform = (x1: AstExpression, c1: Value, c2: Value) => TransformData; +type Transform = ( + x1: AstExpression, + c1: Value, + c2: Value, + loc: SrcInfo, +) => TransformData; /* A simple wrapper function to transform the right value in a binary operator to a continuation - so that we can call the evaluation function in the interpreter module + so that we can call the evaluation function in the standard semantics module */ function evalBinaryOp(op: AstBinaryOperation, valL: Value, valR: Value): Value { return interpreterModule.evalBinaryOp(op, valL, () => valR); @@ -172,10 +177,15 @@ export class AssociativeRule1 extends AllowableOpRule { // there is further opportunity of simplification, // So, we ask the evaluator to apply all the rules in the subtree. const newLeft = optimizer.applyRules( - makeBinaryExpression(op1, x1, x2), + makeBinaryExpression(op1, x1, x2, ast.loc), + ); + const newRight = makeValueExpression(val, ast.loc); + return makeBinaryExpression( + op, + newLeft, + newRight, + ast.loc, ); - const newRight = makeValueExpression(val); - return makeBinaryExpression(op, newLeft, newRight); } catch (e) { // Do nothing: will exit rule without modifying tree } @@ -225,11 +235,11 @@ export class AssociativeRule1 extends AllowableOpRule { // Because we are joining x1 and val, // there is further opportunity of simplification, // So, we ask the evaluator to apply all the rules in the subtree. - const newValNode = makeValueExpression(val); + const newValNode = makeValueExpression(val, ast.loc); const newLeft = optimizer.applyRules( - makeBinaryExpression(op1, x1, newValNode), + makeBinaryExpression(op1, x1, newValNode, ast.loc), ); - return makeBinaryExpression(op2, newLeft, x2); + return makeBinaryExpression(op2, newLeft, x2, ast.loc); } catch (e) { // Do nothing: will exit rule without modifying tree } @@ -281,11 +291,11 @@ export class AssociativeRule1 extends AllowableOpRule { // Because we are joining x2 and val, // there is further opportunity of simplification, // So, we ask the evaluator to apply all the rules in the subtree. - const newValNode = makeValueExpression(val); + const newValNode = makeValueExpression(val, ast.loc); const newLeft = optimizer.applyRules( - makeBinaryExpression(op2, x2, newValNode), + makeBinaryExpression(op2, x2, newValNode, ast.loc), ); - return makeBinaryExpression(op1, newLeft, x1); + return makeBinaryExpression(op1, newLeft, x1, ast.loc); } catch (e) { // Do nothing: will exit rule without modifying tree } @@ -336,10 +346,15 @@ export class AssociativeRule1 extends AllowableOpRule { // there is further opportunity of simplification, // So, we ask the evaluator to apply all the rules in the subtree. const newRight = optimizer.applyRules( - makeBinaryExpression(op2, x1, x2), + makeBinaryExpression(op2, x1, x2, ast.loc), + ); + const newLeft = makeValueExpression(val, ast.loc); + return makeBinaryExpression( + op, + newLeft, + newRight, + ast.loc, ); - const newLeft = makeValueExpression(val); - return makeBinaryExpression(op, newLeft, newRight); } catch (e) { // Do nothing: will exit rule without modifying tree } @@ -397,9 +412,9 @@ export class AssociativeRule2 extends AllowableOpRule { // there is further opportunity of simplification, // So, we ask the evaluator to apply all the rules in the subtree. const newLeft = optimizer.applyRules( - makeBinaryExpression(op1, x1, x2), + makeBinaryExpression(op1, x1, x2, ast.loc), ); - return makeBinaryExpression(op, newLeft, c1); + return makeBinaryExpression(op, newLeft, c1, ast.loc); } } else if ( checkIsBinaryOp_With_LeftValue(topLevelNode.left) && @@ -432,9 +447,9 @@ export class AssociativeRule2 extends AllowableOpRule { // there is further opportunity of simplification, // So, we ask the evaluator to apply all the rules in the subtree. const newRight = optimizer.applyRules( - makeBinaryExpression(op, x1, x2), + makeBinaryExpression(op, x1, x2, ast.loc), ); - return makeBinaryExpression(op1, c1, newRight); + return makeBinaryExpression(op1, c1, newRight, ast.loc); } } else if ( !isValue(topLevelNode.left) && @@ -467,9 +482,9 @@ export class AssociativeRule2 extends AllowableOpRule { // there is further opportunity of simplification, // So, we ask the evaluator to apply all the rules in the subtree. const newLeft = optimizer.applyRules( - makeBinaryExpression(op, x2, x1), + makeBinaryExpression(op, x2, x1, ast.loc), ); - return makeBinaryExpression(op1, newLeft, c1); + return makeBinaryExpression(op1, newLeft, c1, ast.loc); } } else if ( !isValue(topLevelNode.left) && @@ -504,9 +519,9 @@ export class AssociativeRule2 extends AllowableOpRule { // there is further opportunity of simplification, // So, we ask the evaluator to apply all the rules in the subtree. const newRight = optimizer.applyRules( - makeBinaryExpression(op1, x2, x1), + makeBinaryExpression(op1, x2, x1, ast.loc), ); - return makeBinaryExpression(op, c1, newRight); + return makeBinaryExpression(op, c1, newRight, ast.loc); } } } @@ -622,15 +637,16 @@ export class AssociativeRule3 extends Rule { [ "+", // original expression: (x1 + c1) + c2 - (x1, c1, c2) => { + (x1, c1, c2, loc) => { // final expression: x1 + (c1 + c2) const val_ = evalBinaryOp("+", c1, c2); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); return { simplifiedExpression: makeBinaryExpression( "+", x1, val_node, + loc, ), safetyCondition: this.standardAdditiveCondition( c1 as bigint, @@ -644,15 +660,16 @@ export class AssociativeRule3 extends Rule { [ "-", // original expression: (x1 + c1) - c2 - (x1, c1, c2) => { + (x1, c1, c2, loc) => { // final expression: x1 + (c1 - c2) const val_ = evalBinaryOp("-", c1, c2); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); return { simplifiedExpression: makeBinaryExpression( "+", x1, val_node, + loc, ), safetyCondition: this.standardAdditiveCondition( c1 as bigint, @@ -671,15 +688,16 @@ export class AssociativeRule3 extends Rule { [ "+", // original expression: (x1 - c1) + c2 - (x1, c1, c2) => { + (x1, c1, c2, loc) => { // final expression x1 - (c1 - c2) const val_ = evalBinaryOp("-", c1, c2); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); return { simplifiedExpression: makeBinaryExpression( "-", x1, val_node, + loc, ), safetyCondition: this.standardAdditiveCondition( c1 as bigint, @@ -693,15 +711,16 @@ export class AssociativeRule3 extends Rule { [ "-", // original expression: (x1 - c1) - c2 - (x1, c1, c2) => { + (x1, c1, c2, loc) => { // final expression x1 - (c1 + c2) const val_ = evalBinaryOp("+", c1, c2); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); return { simplifiedExpression: makeBinaryExpression( "-", x1, val_node, + loc, ), safetyCondition: this.standardAdditiveCondition( c1 as bigint, @@ -720,15 +739,16 @@ export class AssociativeRule3 extends Rule { [ "*", // original expression: (x1 * c1) * c2 - (x1, c1, c2) => { + (x1, c1, c2, loc) => { // final expression x1 * (c1 * c2) const val_ = evalBinaryOp("*", c1, c2); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); return { simplifiedExpression: makeBinaryExpression( "*", x1, val_node, + loc, ), safetyCondition: this.standardMultiplicativeCondition( @@ -747,15 +767,16 @@ export class AssociativeRule3 extends Rule { [ "&&", // original expression: (x1 && c1) && c2 - (x1, c1, c2) => { + (x1, c1, c2, loc) => { // final expression x1 && (c1 && c2) const val_ = evalBinaryOp("&&", c1, c2); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); return { simplifiedExpression: makeBinaryExpression( "&&", x1, val_node, + loc, ), safetyCondition: true, }; @@ -770,15 +791,16 @@ export class AssociativeRule3 extends Rule { [ "||", // original expression: (x1 || c1) || c2 - (x1, c1, c2) => { + (x1, c1, c2, loc) => { // final expression x1 || (c1 || c2) const val_ = evalBinaryOp("||", c1, c2); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); return { simplifiedExpression: makeBinaryExpression( "||", x1, val_node, + loc, ), safetyCondition: true, }; @@ -807,15 +829,16 @@ export class AssociativeRule3 extends Rule { [ "+", // original expression: c2 + (c1 + x1) - (x1, c1, c2) => { + (x1, c1, c2, loc) => { // final expression (c2 + c1) + x1 const val_ = evalBinaryOp("+", c2, c1); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); return { simplifiedExpression: makeBinaryExpression( "+", val_node, x1, + loc, ), safetyCondition: this.standardAdditiveCondition( c1 as bigint, @@ -829,15 +852,16 @@ export class AssociativeRule3 extends Rule { [ "-", // original expression: c2 + (c1 - x1) - (x1, c1, c2) => { + (x1, c1, c2, loc) => { // final expression (c2 + c1) - x1 const val_ = evalBinaryOp("+", c2, c1); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); return { simplifiedExpression: makeBinaryExpression( "-", val_node, x1, + loc, ), safetyCondition: this.standardAdditiveCondition( c1 as bigint, @@ -856,15 +880,16 @@ export class AssociativeRule3 extends Rule { [ "+", // original expression: c2 - (c1 + x1) - (x1, c1, c2) => { + (x1, c1, c2, loc) => { // final expression (c2 - c1) - x1 const val_ = evalBinaryOp("-", c2, c1); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); return { simplifiedExpression: makeBinaryExpression( "-", val_node, x1, + loc, ), safetyCondition: this.oppositeAdditiveCondition( c1 as bigint, @@ -878,15 +903,16 @@ export class AssociativeRule3 extends Rule { [ "-", // original expression: c2 - (c1 - x1) - (x1, c1, c2) => { + (x1, c1, c2, loc) => { // final expression (c2 - c1) + x1 const val_ = evalBinaryOp("-", c2, c1); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); return { simplifiedExpression: makeBinaryExpression( "+", val_node, x1, + loc, ), safetyCondition: this.oppositeAdditiveCondition( c1 as bigint, @@ -906,15 +932,16 @@ export class AssociativeRule3 extends Rule { "*", // original expression: c2 * (c1 * x1) - (x1, c1, c2) => { + (x1, c1, c2, loc) => { // final expression (c2 * c1) * x1 const val_ = evalBinaryOp("*", c2, c1); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); return { simplifiedExpression: makeBinaryExpression( "*", val_node, x1, + loc, ), safetyCondition: this.standardMultiplicativeCondition( @@ -934,15 +961,16 @@ export class AssociativeRule3 extends Rule { "&&", // original expression: c2 && (c1 && x1) - (x1, c1, c2) => { + (x1, c1, c2, loc) => { // final expression (c2 && c1) && x1 const val_ = evalBinaryOp("&&", c2, c1); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); return { simplifiedExpression: makeBinaryExpression( "&&", val_node, x1, + loc, ), safetyCondition: true, }; @@ -958,15 +986,16 @@ export class AssociativeRule3 extends Rule { "||", // original expression: c2 || (c1 || x1) - (x1, c1, c2) => { + (x1, c1, c2, loc) => { // final expression (c2 || c1) || x1 const val_ = evalBinaryOp("||", c2, c1); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); return { simplifiedExpression: makeBinaryExpression( "||", val_node, x1, + loc, ), safetyCondition: true, }; @@ -995,15 +1024,16 @@ export class AssociativeRule3 extends Rule { [ "+", // original expression: c2 + (x1 + c1) - (x1, c1, c2) => { + (x1, c1, c2, loc) => { // final expression x1 + (c2 + c1) const val_ = evalBinaryOp("+", c2, c1); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); return { simplifiedExpression: makeBinaryExpression( "+", x1, val_node, + loc, ), safetyCondition: this.standardAdditiveCondition( c1 as bigint, @@ -1017,15 +1047,16 @@ export class AssociativeRule3 extends Rule { [ "-", // original expression: c2 + (x1 - c1) - (x1, c1, c2) => { + (x1, c1, c2, loc) => { // final expression x1 - (c1 - c2) const val_ = evalBinaryOp("-", c1, c2); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); return { simplifiedExpression: makeBinaryExpression( "-", x1, val_node, + loc, ), safetyCondition: this.standardAdditiveCondition( c1 as bigint, @@ -1044,15 +1075,16 @@ export class AssociativeRule3 extends Rule { [ "+", // original expression: c2 - (x1 + c1) - (x1, c1, c2) => { + (x1, c1, c2, loc) => { // final expression (c2 - c1) - x1 const val_ = evalBinaryOp("-", c2, c1); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); return { simplifiedExpression: makeBinaryExpression( "-", val_node, x1, + loc, ), safetyCondition: this.oppositeAdditiveCondition( c1 as bigint, @@ -1066,15 +1098,16 @@ export class AssociativeRule3 extends Rule { [ "-", // original expression: c2 - (x1 - c1) - (x1, c1, c2) => { + (x1, c1, c2, loc) => { // final expression (c2 + c1) - x1 const val_ = evalBinaryOp("+", c2, c1); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); return { simplifiedExpression: makeBinaryExpression( "-", val_node, x1, + loc, ), safetyCondition: this.shiftedAdditiveCondition( c1 as bigint, @@ -1094,15 +1127,16 @@ export class AssociativeRule3 extends Rule { [ "*", // original expression: c2 * (x1 * c1) - (x1, c1, c2) => { + (x1, c1, c2, loc) => { // Final expression x1 * (c2 * c1) const val_ = evalBinaryOp("*", c2, c1); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); return { simplifiedExpression: makeBinaryExpression( "*", x1, val_node, + loc, ), safetyCondition: this.standardMultiplicativeCondition( c1 as bigint, @@ -1120,9 +1154,9 @@ export class AssociativeRule3 extends Rule { [ "&&", // original expression: c2 && (x1 && c1) - (x1, c1, c2) => { + (x1, c1, c2, loc) => { const val_ = evalBinaryOp("&&", c2, c1); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); let final_expr; if (c2 === true) { // Final expression x1 && (c2 && c1) @@ -1130,6 +1164,7 @@ export class AssociativeRule3 extends Rule { "&&", x1, val_node, + loc, ); } else { // Final expression (c2 && c1) && x1 @@ -1140,6 +1175,7 @@ export class AssociativeRule3 extends Rule { "&&", val_node, x1, + loc, ); } return { @@ -1157,9 +1193,9 @@ export class AssociativeRule3 extends Rule { [ "||", // original expression: c2 || (x1 || c1) - (x1, c1, c2) => { + (x1, c1, c2, loc) => { const val_ = evalBinaryOp("||", c2, c1); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); let final_expr; if (c2 === false) { // Final expression x1 || (c2 || c1) @@ -1167,6 +1203,7 @@ export class AssociativeRule3 extends Rule { "||", x1, val_node, + loc, ); } else { // Final expression (c2 || c1) || x1 @@ -1177,6 +1214,7 @@ export class AssociativeRule3 extends Rule { "||", val_node, x1, + loc, ); } return { @@ -1207,15 +1245,16 @@ export class AssociativeRule3 extends Rule { [ "+", // original expression: (c1 + x1) + c2 - (x1, c1, c2) => { + (x1, c1, c2, loc) => { // Final expression (c1 + c2) + x1 const val_ = evalBinaryOp("+", c1, c2); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); return { simplifiedExpression: makeBinaryExpression( "+", val_node, x1, + loc, ), safetyCondition: this.standardAdditiveCondition( c1 as bigint, @@ -1229,15 +1268,16 @@ export class AssociativeRule3 extends Rule { [ "-", // original expression: (c1 + x1) - c2 - (x1, c1, c2) => { + (x1, c1, c2, loc) => { // Final expression (c1 - c2) + x1 const val_ = evalBinaryOp("-", c1, c2); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); return { simplifiedExpression: makeBinaryExpression( "+", val_node, x1, + loc, ), safetyCondition: this.standardAdditiveCondition( c1 as bigint, @@ -1256,15 +1296,16 @@ export class AssociativeRule3 extends Rule { [ "+", // original expression: (c1 - x1) + c2 - (x1, c1, c2) => { + (x1, c1, c2, loc) => { // Final expression (c1 + c2) - x1 const val_ = evalBinaryOp("+", c1, c2); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); return { simplifiedExpression: makeBinaryExpression( "-", val_node, x1, + loc, ), safetyCondition: this.standardAdditiveCondition( c1 as bigint, @@ -1278,15 +1319,16 @@ export class AssociativeRule3 extends Rule { [ "-", // original expression: (c1 - x1) - c2 - (x1, c1, c2) => { + (x1, c1, c2, loc) => { // Final expression (c1 - c2) - x1 const val_ = evalBinaryOp("-", c1, c2); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); return { simplifiedExpression: makeBinaryExpression( "-", val_node, x1, + loc, ), safetyCondition: this.standardAdditiveCondition( c1 as bigint, @@ -1305,15 +1347,16 @@ export class AssociativeRule3 extends Rule { [ "*", // original expression: (c1 * x1) * c2 - (x1, c1, c2) => { + (x1, c1, c2, loc) => { // Final expression (c1 * c2) * x1 const val_ = evalBinaryOp("*", c1, c2); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); return { simplifiedExpression: makeBinaryExpression( "*", val_node, x1, + loc, ), safetyCondition: this.standardMultiplicativeCondition( @@ -1332,9 +1375,9 @@ export class AssociativeRule3 extends Rule { [ "&&", // original expression: (c1 && x1) && c2 - (x1, c1, c2) => { + (x1, c1, c2, loc) => { const val_ = evalBinaryOp("&&", c1, c2); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); let final_expr; if (c2 === true) { // Final expression (c1 && c2) && x1 @@ -1342,6 +1385,7 @@ export class AssociativeRule3 extends Rule { "&&", val_node, x1, + loc, ); } else { // Final expression x1 && (c1 && c2) @@ -1352,6 +1396,7 @@ export class AssociativeRule3 extends Rule { "&&", x1, val_node, + loc, ); } return { @@ -1369,9 +1414,9 @@ export class AssociativeRule3 extends Rule { [ "||", // original expression: (c1 || x1) || c2 - (x1, c1, c2) => { + (x1, c1, c2, loc) => { const val_ = evalBinaryOp("||", c1, c2); - const val_node = makeValueExpression(val_); + const val_node = makeValueExpression(val_, loc); let final_expr; if (c2 === false) { // Final expression (c1 || c2) || x1 @@ -1379,6 +1424,7 @@ export class AssociativeRule3 extends Rule { "||", val_node, x1, + loc, ); } else { // Final expression x1 || (c1 || c2) @@ -1389,6 +1435,7 @@ export class AssociativeRule3 extends Rule { "||", x1, val_node, + loc, ); } return { @@ -1483,6 +1530,7 @@ export class AssociativeRule3 extends Rule { x1, c1, c2, + ast.loc, ); if (data.safetyCondition) { // Since the tree is simpler now, there is further @@ -1516,6 +1564,7 @@ export class AssociativeRule3 extends Rule { x1, c1, c2, + ast.loc, ); if (data.safetyCondition) { // Since the tree is simpler now, there is further @@ -1549,6 +1598,7 @@ export class AssociativeRule3 extends Rule { x1, c1, c2, + ast.loc, ); if (data.safetyCondition) { // Since the tree is simpler now, there is further @@ -1582,6 +1632,7 @@ export class AssociativeRule3 extends Rule { x1, c1, c2, + ast.loc, ); if (data.safetyCondition) { // Since the tree is simpler now, there is further diff --git a/src/optimizer/standardOptimizer.ts b/src/optimizer/standardOptimizer.ts index 1558be4b2..c3a97642b 100644 --- a/src/optimizer/standardOptimizer.ts +++ b/src/optimizer/standardOptimizer.ts @@ -6,6 +6,7 @@ import { AndSelf, AndTrue, Contradiction, + DivByZero, DoubleNegation, ExcludedMiddle, MultiplyOne, @@ -42,17 +43,18 @@ export class StandardOptimizer extends ExpressionTransformer { { priority: 5, rule: new MultiplyOne() }, { priority: 6, rule: new SubtractSelf() }, { priority: 7, rule: new AddSelf() }, - { priority: 8, rule: new OrTrue() }, - { priority: 9, rule: new AndFalse() }, - { priority: 10, rule: new OrFalse() }, - { priority: 11, rule: new AndTrue() }, - { priority: 12, rule: new OrSelf() }, - { priority: 13, rule: new AndSelf() }, - { priority: 14, rule: new ExcludedMiddle() }, - { priority: 15, rule: new Contradiction() }, - { priority: 16, rule: new DoubleNegation() }, - { priority: 17, rule: new NegateTrue() }, - { priority: 18, rule: new NegateFalse() }, + { priority: 8, rule: new DivByZero() }, + { priority: 9, rule: new OrTrue() }, + { priority: 10, rule: new AndFalse() }, + { priority: 11, rule: new OrFalse() }, + { priority: 12, rule: new AndTrue() }, + { priority: 13, rule: new OrSelf() }, + { priority: 14, rule: new AndSelf() }, + { priority: 15, rule: new ExcludedMiddle() }, + { priority: 16, rule: new Contradiction() }, + { priority: 17, rule: new DoubleNegation() }, + { priority: 18, rule: new NegateTrue() }, + { priority: 19, rule: new NegateFalse() }, ]; // Sort according to the priorities: smaller number means greater priority. diff --git a/src/optimizer/test/partial-eval.spec.ts b/src/optimizer/test/partial-eval.spec.ts index db01b6c09..85f070285 100644 --- a/src/optimizer/test/partial-eval.spec.ts +++ b/src/optimizer/test/partial-eval.spec.ts @@ -6,13 +6,13 @@ import { eqExpressions, isValue, } from "../../grammar/ast"; -import { parseExpression } from "../../grammar/grammar"; +import { dummySrcInfo, parseExpression } from "../../grammar/grammar"; import { extractValue, makeValueExpression } from "../util"; import { partiallyEvalExpression } from "../../constEval"; import { CompilerContext } from "../../context"; import { ExpressionTransformer, Rule } from "../types"; import { AssociativeRule3 } from "../associative"; -import { evalBinaryOp, evalUnaryOp } from "../../interpreter"; +import { evalBinaryOp, evalUnaryOp } from "../../interpreters/standard"; const MAX: string = "115792089237316195423570985008687907853269984665640564039457584007913129639935"; @@ -373,11 +373,10 @@ function dummyEval(ast: AstExpression): AstExpression { newNode = cloneAstNode(ast); newNode.operand = dummyEval(ast.operand); if (isValue(newNode.operand)) { + const operandValue = extractValue(newNode.operand as AstValue); return makeValueExpression( - evalUnaryOp( - ast.op, - extractValue(newNode.operand as AstValue), - ), + evalUnaryOp(ast.op, () => operandValue), + dummySrcInfo, ); } return newNode; @@ -393,6 +392,7 @@ function dummyEval(ast: AstExpression): AstExpression { extractValue(newNode.left as AstValue), () => valR, ), + dummySrcInfo, ); } return newNode; diff --git a/src/optimizer/util.ts b/src/optimizer/util.ts index 4b98ab779..b97df82ff 100644 --- a/src/optimizer/util.ts +++ b/src/optimizer/util.ts @@ -5,11 +5,17 @@ import { createAstNode, AstValue, isValue, + AstStructFieldInitializer, + AstId, + idText, + SrcInfo, } from "../grammar/ast"; -import { dummySrcInfo } from "../grammar/grammar"; -import { throwInternalCompilerError } from "../errors"; -import { Value } from "../types/types"; +import { throwNonFatalErrorConstEval } from "../interpreters/util"; +import { StructValue, Value } from "../types/types"; +// This function assumes that the parameter is already a value. +// i.e., that the user called the isValue function to check +// if the parameter is a value. export function extractValue(ast: AstValue): Value { switch ( ast.kind // Missing structs @@ -22,14 +28,24 @@ export function extractValue(ast: AstValue): Value { return ast.value; case "string": return ast.value; + case "struct_instance": + return ast.args.reduce( + (resObj, fieldWithInit) => { + resObj[idText(fieldWithInit.field)] = extractValue( + fieldWithInit.initializer as AstValue, + ); + return resObj; + }, + { $tactStruct: idText(ast.type) } as StructValue, + ); } } -export function makeValueExpression(value: Value): AstValue { +export function makeValueExpression(value: Value, loc: SrcInfo): AstValue { if (value === null) { const result = createAstNode({ kind: "null", - loc: dummySrcInfo, + loc: loc, }); return result as AstValue; } @@ -37,7 +53,7 @@ export function makeValueExpression(value: Value): AstValue { const result = createAstNode({ kind: "string", value: value, - loc: dummySrcInfo, + loc: loc, }); return result as AstValue; } @@ -46,7 +62,7 @@ export function makeValueExpression(value: Value): AstValue { kind: "number", base: 10, value: value, - loc: dummySrcInfo, + loc: loc, }); return result as AstValue; } @@ -54,24 +70,54 @@ export function makeValueExpression(value: Value): AstValue { const result = createAstNode({ kind: "boolean", value: value, - loc: dummySrcInfo, + loc: loc, }); return result as AstValue; } - throwInternalCompilerError( - `structs, addresses, cells, and comment values are not supported at the moment.`, + if (typeof value === "object" && "$tactStruct" in value) { + const fields = Object.entries(value) + .filter(([name, _]) => name !== "$tactStruct") + .map(([name, val]) => { + return createAstNode({ + kind: "struct_field_initializer", + field: makeIdExpression(name, loc), + initializer: makeValueExpression(val, loc), + loc: loc, + }) as AstStructFieldInitializer; + }); + const result = createAstNode({ + kind: "struct_instance", + type: makeIdExpression(value["$tactStruct"] as string, loc), + args: fields, + loc: loc, + }); + return result as AstValue; + } + throwNonFatalErrorConstEval( + `addresses, cells, and comment values cannot be transformed into AST nodes.`, + loc, ); } +function makeIdExpression(name: string, loc: SrcInfo): AstId { + const result = createAstNode({ + kind: "id", + text: name, + loc: loc, + }); + return result as AstId; +} + export function makeUnaryExpression( op: AstUnaryOperation, operand: AstExpression, + loc: SrcInfo, ): AstExpression { const result = createAstNode({ kind: "op_unary", op: op, operand: operand, - loc: dummySrcInfo, + loc: loc, }); return result as AstExpression; } @@ -80,13 +126,14 @@ export function makeBinaryExpression( op: AstBinaryOperation, left: AstExpression, right: AstExpression, + loc: SrcInfo, ): AstExpression { const result = createAstNode({ kind: "op_binary", op: op, left: left, right: right, - loc: dummySrcInfo, + loc: loc, }); return result as AstExpression; } @@ -126,31 +173,3 @@ export function checkIsName(ast: AstExpression): boolean { export function checkIsBoolean(ast: AstExpression, b: boolean): boolean { return ast.kind === "boolean" ? ast.value == b : false; } - -// bigint arithmetic - -// precondition: the divisor is not zero -// rounds the division result towards negative infinity -export function divFloor(a: bigint, b: bigint): bigint { - const almostSameSign = a > 0n === b > 0n; - if (almostSameSign) { - return a / b; - } - return a / b + (a % b === 0n ? 0n : -1n); -} - -export function abs(a: bigint): bigint { - return a < 0n ? -a : a; -} - -export function sign(a: bigint): bigint { - if (a === 0n) return 0n; - else return a < 0n ? -1n : 1n; -} - -// precondition: the divisor is not zero -// rounds the result towards negative infinity -// Uses the fact that a / b * b + a % b == a, for all b != 0. -export function modFloor(a: bigint, b: bigint): bigint { - return a - divFloor(a, b) * b; -} diff --git a/src/pipeline/precompile.ts b/src/pipeline/precompile.ts index 8ada419da..d37a603ee 100644 --- a/src/pipeline/precompile.ts +++ b/src/pipeline/precompile.ts @@ -7,6 +7,7 @@ import { resolveErrors } from "../types/resolveErrors"; import { resolveSignatures } from "../types/resolveSignatures"; import { resolveImports } from "../imports/resolveImports"; import { VirtualFileSystem } from "../vfs/VirtualFileSystem"; +import { ConstantPropagationAnalyzer } from "../interpreters/constantPropagation"; import { AstModule } from "../grammar/ast"; export function precompile( @@ -38,6 +39,9 @@ export function precompile( // This extracts error messages ctx = resolveErrors(ctx); + // This runs the AST constant propagation analyzer + new ConstantPropagationAnalyzer(ctx).startAnalysis(); + // Prepared context return ctx; } diff --git a/src/test/compilation-failed/const-eval-failed.spec.ts b/src/test/compilation-failed/const-eval-failed.spec.ts index f84d1c7e3..3a0377cd2 100644 --- a/src/test/compilation-failed/const-eval-failed.spec.ts +++ b/src/test/compilation-failed/const-eval-failed.spec.ts @@ -8,57 +8,55 @@ describe("fail-const-eval", () => { itShouldNotCompile({ testName: "const-eval-div-by-zero", - errorMessage: - "Cannot evaluate expression to a constant: divisor expression must be non-zero", + errorMessage: "Cannot evaluate expression: divisor must be non-zero", }); itShouldNotCompile({ testName: "const-eval-mod-by-zero", - errorMessage: - "Cannot evaluate expression to a constant: divisor expression must be non-zero", + errorMessage: "Cannot evaluate expression: divisor must be non-zero", }); itShouldNotCompile({ testName: "const-eval-int-overflow-positive-literal", errorMessage: - "Cannot evaluate expression to a constant: integer '115792089237316195423570985008687907853269984665640564039457584007913129639936' does not fit into TVM Int type", + "Cannot evaluate expression: integer '115792089237316195423570985008687907853269984665640564039457584007913129639936' does not fit into TVM Int type", }); itShouldNotCompile({ testName: "const-eval-int-overflow-negative-literal", errorMessage: - "Cannot evaluate expression to a constant: integer '-115792089237316195423570985008687907853269984665640564039457584007913129639937' does not fit into TVM Int type", + "Cannot evaluate expression: integer '-115792089237316195423570985008687907853269984665640564039457584007913129639937' does not fit into TVM Int type", }); itShouldNotCompile({ testName: "const-eval-int-overflow-add", errorMessage: - "Cannot evaluate expression to a constant: integer '115792089237316195423570985008687907853269984665640564039457584007913129639936' does not fit into TVM Int type", + "Cannot evaluate expression: integer '115792089237316195423570985008687907853269984665640564039457584007913129639936' does not fit into TVM Int type", }); itShouldNotCompile({ testName: "const-eval-int-overflow-sub", errorMessage: - "Cannot evaluate expression to a constant: integer '-115792089237316195423570985008687907853269984665640564039457584007913129639937' does not fit into TVM Int type", + "Cannot evaluate expression: integer '-115792089237316195423570985008687907853269984665640564039457584007913129639937' does not fit into TVM Int type", }); itShouldNotCompile({ testName: "const-eval-int-overflow-mul1", errorMessage: - "Cannot evaluate expression to a constant: integer '231584178474632390847141970017375815706539969331281128078915168015826259279870' does not fit into TVM Int type", + "Cannot evaluate expression: integer '231584178474632390847141970017375815706539969331281128078915168015826259279870' does not fit into TVM Int type", }); itShouldNotCompile({ testName: "const-eval-int-overflow-mul2", errorMessage: - "Cannot evaluate expression to a constant: integer '-231584178474632390847141970017375815706539969331281128078915168015826259279872' does not fit into TVM Int type", + "Cannot evaluate expression: integer '-231584178474632390847141970017375815706539969331281128078915168015826259279872' does not fit into TVM Int type", }); itShouldNotCompile({ testName: "const-eval-int-overflow-div", errorMessage: - "Cannot evaluate expression to a constant: integer '115792089237316195423570985008687907853269984665640564039457584007913129639936' does not fit into TVM Int type", + "Cannot evaluate expression: integer '115792089237316195423570985008687907853269984665640564039457584007913129639936' does not fit into TVM Int type", }); itShouldNotCompile({ testName: "const-eval-int-overflow-ton1", - errorMessage: `Cannot evaluate expression to a constant: invalid "ton" argument`, + errorMessage: `Cannot evaluate expression: invalid "ton" argument`, }); itShouldNotCompile({ testName: "const-eval-int-overflow-ton2", errorMessage: - "Cannot evaluate expression to a constant: integer '115792089237316195423570985008687907853269984665640564039457584007913129639936' does not fit into TVM Int type", + "Cannot evaluate expression: integer '115792089237316195423570985008687907853269984665640564039457584007913129639936' does not fit into TVM Int type", }); itShouldNotCompile({ testName: "const-eval-int-overflow-pow-1", @@ -67,7 +65,7 @@ describe("fail-const-eval", () => { itShouldNotCompile({ testName: "const-eval-int-overflow-pow-2", errorMessage: - "Cannot evaluate expression to a constant: integer '115792089237316195423570985008687907853269984665640564039457584007913129639936' does not fit into TVM Int type", + "Cannot evaluate expression: integer '115792089237316195423570985008687907853269984665640564039457584007913129639936' does not fit into TVM Int type", }); itShouldNotCompile({ testName: "const-eval-int-overflow-pow2-1", @@ -76,32 +74,32 @@ describe("fail-const-eval", () => { itShouldNotCompile({ testName: "const-eval-int-overflow-pow2-2", errorMessage: - "Cannot evaluate expression to a constant: integer '115792089237316195423570985008687907853269984665640564039457584007913129639936' does not fit into TVM Int type", + "Cannot evaluate expression: integer '115792089237316195423570985008687907853269984665640564039457584007913129639936' does not fit into TVM Int type", }); itShouldNotCompile({ testName: "const-eval-int-overflow-shl1", errorMessage: - "Cannot evaluate expression to a constant: integer '115792089237316195423570985008687907853269984665640564039457584007913129639936' does not fit into TVM Int type", + "Cannot evaluate expression: integer '115792089237316195423570985008687907853269984665640564039457584007913129639936' does not fit into TVM Int type", }); itShouldNotCompile({ testName: "const-eval-int-overflow-shl2", errorMessage: - "Cannot evaluate expression to a constant: integer '-13407807929942597099574024998205846127479365820592393377723561443721764030073546976801874298166903427690031858186486050853753882811946569946433649006084096' does not fit into TVM Int type", + "Cannot evaluate expression: integer '-13407807929942597099574024998205846127479365820592393377723561443721764030073546976801874298166903427690031858186486050853753882811946569946433649006084096' does not fit into TVM Int type", }); itShouldNotCompile({ testName: "const-eval-int-overflow-struct-instance", errorMessage: - "Cannot evaluate expression to a constant: integer '115792089237316195423570985008687907853269984665640564039457584007913129639936' does not fit into TVM Int type", + "Cannot evaluate expression: integer '115792089237316195423570985008687907853269984665640564039457584007913129639936' does not fit into TVM Int type", }); itShouldNotCompile({ testName: "const-eval-shl-invalid-bits1", errorMessage: - "Cannot evaluate expression to a constant: the number of bits shifted ('257') must be within [0..256] range", + "Cannot evaluate expression: the number of bits shifted ('257') must be within [0..256] range", }); itShouldNotCompile({ testName: "const-eval-shl-invalid-bits2", errorMessage: - "Cannot evaluate expression to a constant: the number of bits shifted ('-1') must be within [0..256] range", + "Cannot evaluate expression: the number of bits shifted ('-1') must be within [0..256] range", }); itShouldNotCompile({ testName: "const-eval-unboxing-null", @@ -110,92 +108,90 @@ describe("fail-const-eval", () => { itShouldNotCompile({ testName: "const-eval-invalid-address", errorMessage: - "Cannot evaluate expression to a constant: invalid address encoding: FQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N", + "Cannot evaluate expression: invalid address encoding: FQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N", }); itShouldNotCompile({ testName: "const-eval-div-by-zero-in-fun", - errorMessage: - "Cannot evaluate expression to a constant: divisor expression must be non-zero", + errorMessage: "Cannot evaluate expression: divisor must be non-zero", }); itShouldNotCompile({ testName: "const-eval-int-overflow-add-in-fun", errorMessage: - "Cannot evaluate expression to a constant: integer '115792089237316195423570985008687907853269984665640564039457584007913129639936' does not fit into TVM Int type", + "Cannot evaluate expression: integer '115792089237316195423570985008687907853269984665640564039457584007913129639936' does not fit into TVM Int type", }); itShouldNotCompile({ testName: "const-eval-int-overflow-div-in-fun", errorMessage: - "Cannot evaluate expression to a constant: integer '115792089237316195423570985008687907853269984665640564039457584007913129639936' does not fit into TVM Int type", + "Cannot evaluate expression: integer '115792089237316195423570985008687907853269984665640564039457584007913129639936' does not fit into TVM Int type", }); itShouldNotCompile({ testName: "const-eval-int-overflow-mul1-in-fun", errorMessage: - "Cannot evaluate expression to a constant: integer '231584178474632390847141970017375815706539969331281128078915168015826259279870' does not fit into TVM Int type", + "Cannot evaluate expression: integer '231584178474632390847141970017375815706539969331281128078915168015826259279870' does not fit into TVM Int type", }); itShouldNotCompile({ testName: "const-eval-int-overflow-mul2-in-fun", errorMessage: - "Cannot evaluate expression to a constant: integer '-231584178474632390847141970017375815706539969331281128078915168015826259279872' does not fit into TVM Int type", + "Cannot evaluate expression: integer '-231584178474632390847141970017375815706539969331281128078915168015826259279872' does not fit into TVM Int type", }); itShouldNotCompile({ testName: "const-eval-int-overflow-positive-literal-in-fun", errorMessage: - "Cannot evaluate expression to a constant: integer '115792089237316195423570985008687907853269984665640564039457584007913129639936' does not fit into TVM Int type", + "Cannot evaluate expression: integer '115792089237316195423570985008687907853269984665640564039457584007913129639936' does not fit into TVM Int type", }); itShouldNotCompile({ testName: "const-eval-int-overflow-negative-literal-in-fun", errorMessage: - "Cannot evaluate expression to a constant: integer '-115792089237316195423570985008687907853269984665640564039457584007913129639937' does not fit into TVM Int type", + "Cannot evaluate expression: integer '-115792089237316195423570985008687907853269984665640564039457584007913129639937' does not fit into TVM Int type", }); itShouldNotCompile({ testName: "const-eval-int-overflow-struct-instance-in-fun", errorMessage: - "Cannot evaluate expression to a constant: integer '115792089237316195423570985008687907853269984665640564039457584007913129639936' does not fit into TVM Int type", + "Cannot evaluate expression: integer '115792089237316195423570985008687907853269984665640564039457584007913129639936' does not fit into TVM Int type", }); itShouldNotCompile({ testName: "const-eval-repeat-lower-bound", errorMessage: - "Cannot evaluate expression to a constant: integer '-115792089237316195423570985008687907853269984665640564039457584007913129639937' does not fit into TVM Int type", + "Cannot evaluate expression: integer '-115792089237316195423570985008687907853269984665640564039457584007913129639937' does not fit into TVM Int type", }); itShouldNotCompile({ testName: "const-eval-repeat-upper-bound", errorMessage: - "Cannot evaluate expression to a constant: repeat argument must be a number between -2^256 (inclusive) and 2^31 - 1 (inclusive)", + "Cannot evaluate expression: repeat argument must be a number between -2^256 (inclusive) and 2^31 - 1 (inclusive)", }); itShouldNotCompile({ testName: "const-eval-ascii-overflow", errorMessage: - "Cannot evaluate expression to a constant: ascii string is too long, expected up to 32 bytes, got 33", + "Cannot evaluate expression: ascii string is too long, expected up to 32 bytes, got 33", }); itShouldNotCompile({ testName: "const-eval-ascii-overflow-2", errorMessage: - "Cannot evaluate expression to a constant: ascii string is too long, expected up to 32 bytes, got 33", + "Cannot evaluate expression: ascii string is too long, expected up to 32 bytes, got 33", }); itShouldNotCompile({ testName: "const-eval-rawslice-not-hex", errorMessage: - "Cannot evaluate expression to a constant: invalid hex string: hello world", + "Cannot evaluate expression: invalid hex string: hello world", }); itShouldNotCompile({ testName: "const-eval-rawslice-overflow", errorMessage: - "Cannot evaluate expression to a constant: slice constant is too long, expected up to 1023 bits, got 1024", + "Cannot evaluate expression: slice constant is too long, expected up to 1023 bits, got 1024", }); itShouldNotCompile({ testName: "const-eval-rawslice-overflow-padded", errorMessage: - "Cannot evaluate expression to a constant: slice constant is too long, expected up to 1023 bits, got 1024", + "Cannot evaluate expression: slice constant is too long, expected up to 1023 bits, got 1024", }); itShouldNotCompile({ testName: "const-eval-rawslice-invalid", - errorMessage: - "Cannot evaluate expression to a constant: invalid hex string: 4a__", + errorMessage: "Cannot evaluate expression: invalid hex string: 4a__", }); itShouldNotCompile({ testName: "const-eval-ascii-empty", errorMessage: - "Cannot evaluate expression to a constant: ascii string cannot be empty", + "Cannot evaluate expression: ascii string cannot be empty", }); }); diff --git a/src/test/exit-codes/contracts/repeat-range.tact b/src/test/exit-codes/contracts/repeat-range.tact index 1b6136a68..bbf6c2866 100644 --- a/src/test/exit-codes/contracts/repeat-range.tact +++ b/src/test/exit-codes/contracts/repeat-range.tact @@ -14,10 +14,10 @@ contract RepeatRangeTester { } /// from 2^{31} to +∞ — repeat range is too big - get fun testInvalidRange(): Bool { + get fun testInvalidRange(x: Int): Bool { try { let counter = 0; - repeat (pow(2, 31)) { counter += 1 } + repeat (pow(2, x)) { counter += 1 } return false; } catch (exitCode) { diff --git a/src/test/exit-codes/contracts/tact-reserved-contract-errors.tact b/src/test/exit-codes/contracts/tact-reserved-contract-errors.tact index f4498f444..4196ce024 100644 --- a/src/test/exit-codes/contracts/tact-reserved-contract-errors.tact +++ b/src/test/exit-codes/contracts/tact-reserved-contract-errors.tact @@ -3,6 +3,12 @@ import "@stdlib/dns"; message(1478) SpanishInquisition {} +message(128) ExitCode128 { gotcha: String? } + +message(136) ExitCode136 { unsupportedChainId: Int } + +message(137) ExitCode137 { masterchainId: Int } + contract ReservedContractErrorsTester with Ownable { /// To make Ownable work owner: Address; @@ -14,9 +20,8 @@ contract ReservedContractErrorsTester with Ownable { receive() {} /// Exit code 128 - receive("128") { - let gotcha: String? = null; - dump(gotcha!!); + receive(msg: ExitCode128) { + dump(msg.gotcha!!); } /// Exit code 130 @@ -98,20 +103,18 @@ contract ReservedContractErrorsTester with Ownable { } /// Exit code 136 - receive("136") { - let unsupportedChainId = 1; + receive(msg: ExitCode136) { dump( // Zero address in unsupported workchain - newAddress(unsupportedChainId, 0) + newAddress(msg.unsupportedChainId, 0) ); } /// Exit code 137 - receive("137") { - let masterchainId = -1; + receive(msg: ExitCode137) { dump( // Zero address in masterchain without the config option set - newAddress(masterchainId, 0) + newAddress(msg.masterchainId, 0) ); } } diff --git a/src/test/exit-codes/repeat-range.spec.ts b/src/test/exit-codes/repeat-range.spec.ts index e3451f316..53515693b 100644 --- a/src/test/exit-codes/repeat-range.spec.ts +++ b/src/test/exit-codes/repeat-range.spec.ts @@ -36,7 +36,7 @@ describe("repeat range", () => { expect(await contract.getTestIgnoredRange()).toEqual(true); // invalid range - expect(await contract.getTestInvalidRange()).toEqual(true); + expect(await contract.getTestInvalidRange(31n)).toEqual(true); // min effective range expect(await contract.getTestMinEffectiveRange()).toEqual(true); diff --git a/src/test/exit-codes/tact-reserved-contract-errors.spec.ts b/src/test/exit-codes/tact-reserved-contract-errors.spec.ts index d29ecddff..a0a7e86a5 100644 --- a/src/test/exit-codes/tact-reserved-contract-errors.spec.ts +++ b/src/test/exit-codes/tact-reserved-contract-errors.spec.ts @@ -1,6 +1,11 @@ import { toNano } from "@ton/core"; import { Blockchain, SandboxContract, TreasuryContract } from "@ton/sandbox"; -import { ReservedContractErrorsTester as TestContract } from "./contracts/output/tact-reserved-contract-errors_ReservedContractErrorsTester"; +import { + ExitCode128, + ExitCode136, + ExitCode137, + ReservedContractErrorsTester as TestContract, +} from "./contracts/output/tact-reserved-contract-errors_ReservedContractErrorsTester"; import "@ton/test-utils"; describe("Tact-reserved contract errors", () => { @@ -84,12 +89,33 @@ async function testReservedExitCode( expect(code).toBeGreaterThanOrEqual(128); expect(code).toBeLessThan(256); expect([128, 130, 132, 134, 136, 137]).toContain(code); - type testedExitCodes = "128" | "130" | "132" | "134" | "136" | "137"; + type testedExitCodes = + | ExitCode128 + | "130" + | "132" + | "134" + | ExitCode136 + | ExitCode137; const sendResult = await contract.send( treasure.getSender(), { value: toNano("10") }, - code.toString(10) as testedExitCodes, + code === 128 + ? { + $$type: "ExitCode128", + gotcha: null, + } + : code === 136 + ? { + $$type: "ExitCode136", + unsupportedChainId: 1n, + } + : code === 137 + ? { + $$type: "ExitCode137", + masterchainId: -1n, + } + : (code.toString(10) as testedExitCodes), ); expect(sendResult.transactions).toHaveTransaction({ diff --git a/src/types/__snapshots__/resolveDescriptors.spec.ts.snap b/src/types/__snapshots__/resolveDescriptors.spec.ts.snap index e4173d1ff..470cae20a 100644 --- a/src/types/__snapshots__/resolveDescriptors.spec.ts.snap +++ b/src/types/__snapshots__/resolveDescriptors.spec.ts.snap @@ -81,7 +81,7 @@ Line 4, col 17: `; exports[`resolveDescriptors should fail descriptors for const-eval-overflow 1`] = ` -":8:26: Cannot evaluate expression to a constant: the number of bits shifted ('-1073741824') must be within [0..256] range +":8:26: Cannot evaluate expression: the number of bits shifted ('-1073741824') must be within [0..256] range Line 8, col 26: 7 | > 8 | const a: Int = 1 + (1 >> -1073741824); diff --git a/src/types/resolveStatements.ts b/src/types/resolveStatements.ts index 760c4b950..e4cf331b1 100644 --- a/src/types/resolveStatements.ts +++ b/src/types/resolveStatements.ts @@ -29,8 +29,8 @@ import { import { getExpType, resolveExpression } from "./resolveExpression"; import { FunctionDescription, printTypeRef, TypeRef } from "./types"; import { evalConstantExpression } from "../constEval"; -import { ensureInt } from "../interpreter"; import { crc16 } from "../utils/crc16"; +import { ensureInt } from "../interpreters/standard"; export type StatementContext = { root: SrcInfo; diff --git a/src/types/types.ts b/src/types/types.ts index 8b34e74cf..698d336c0 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -108,6 +108,77 @@ export function showValue(val: Value): string { } } +export function eqValues(val1: Value, val2: Value): boolean { + if (val1 === null) { + return val2 === null; + } else if (val1 instanceof CommentValue) { + return val2 instanceof CommentValue + ? val1.comment === val2.comment + : false; + } else if (typeof val1 === "object" && "$tactStruct" in val1) { + if ( + typeof val2 === "object" && + val2 !== null && + "$tactStruct" in val2 + ) { + const map1 = new Map(Object.entries(val1)); + const map2 = new Map(Object.entries(val2)); + + if (map1.size !== map2.size) { + return false; + } + + for (const [key, val1] of map1) { + if (!map2.has(key)) { + return false; + } + const val2 = map2.get(key)!; + if (!eqValues(val1, val2)) { + return false; + } + } + + return true; + } else { + return false; + } + } else if (Address.isAddress(val1)) { + return Address.isAddress(val2) ? val1.equals(val2) : false; + } else if (val1 instanceof Cell) { + return val2 instanceof Cell ? val1.equals(val2) : false; + } else if (val1 instanceof Slice) { + return val2 instanceof Slice + ? val1.asCell().equals(val2.asCell()) + : false; + } else { + return val1 === val2; + } +} + +export function copyValue(val: Value): Value { + if (val === null) { + return null; + } else if (val instanceof CommentValue) { + return new CommentValue(val.comment); + } else if (typeof val === "object" && "$tactStruct" in val) { + const result: StructValue = {}; + + for (const [key, value] of Object.entries(val)) { + result[key] = copyValue(value); + } + return result; + } else if (Address.isAddress(val)) { + return val; // What is the proper way to copy an Address? + } else if (val instanceof Cell) { + return val; // What is the proper way to copy a Cell? + } else if (val instanceof Slice) { + return val.clone(); // Is this the proper way of copying a Slice? + } else { + // These are atomic values. There is no need to copy them + return val; + } +} + export type FieldDescription = { name: string; index: number;