From fa5bb0589151f1ff345471f1e293e3fb7b692625 Mon Sep 17 00:00:00 2001 From: jeshecdom Date: Fri, 28 Jun 2024 13:12:29 +0200 Subject: [PATCH 01/14] First commit for partial evaluator. There are still some TODOs (so, this is only a partial commit): - Implementation of association rule 3 and simple algebraic rules (to be added to file algebraic.ts) described in comments of issue: https://github.com/tact-lang/tact/issues/435 - Processing of structs, function calls, id lookups, ternary conditional operator. - Extensive documentation, specially in the description of what each rule does. - Extensive testing of the rewrite rules. --- src/constEval.ts | 241 ++++++++++++--- src/optimizer/algebraic.ts | 1 + src/optimizer/associative.ts | 451 +++++++++++++++++++++++++++++ src/optimizer/standardOptimizer.ts | 29 ++ src/optimizer/types.ts | 52 ++++ src/optimizer/util.ts | 127 ++++++++ 6 files changed, 853 insertions(+), 48 deletions(-) create mode 100644 src/optimizer/algebraic.ts create mode 100644 src/optimizer/associative.ts create mode 100644 src/optimizer/standardOptimizer.ts create mode 100644 src/optimizer/types.ts create mode 100644 src/optimizer/util.ts diff --git a/src/constEval.ts b/src/constEval.ts index aaf3cdbc2..0b344e558 100644 --- a/src/constEval.ts +++ b/src/constEval.ts @@ -7,11 +7,20 @@ import { ASTId, ASTNewParameter, ASTRef, - ASTUnaryOperation, + ASTUnaryOperation } from "./grammar/ast"; import { throwConstEvalError } from "./errors"; import { CommentValue, StructValue, Value } from "./types/types"; import { sha256_sync } from "@ton/crypto"; +import { + isValue, + extractValue, + makeValueExpression, + makeUnaryExpression, + makeBinaryExpression +} from "./optimizer/util"; +import { DUMMY_AST_REF, ExpressionTransformer, ValueExpression } from "./optimizer/types"; +import { StandardOptimizer } from "./optimizer/standardOptimizer"; import { getStaticConstant, getType, @@ -23,6 +32,10 @@ import { getExpType } from "./types/resolveExpression"; const minTvmInt: bigint = -(2n ** 256n); const maxTvmInt: bigint = 2n ** 256n - 1n; +// The optimizer that applies the rewriting rules during partial evaluation. +// For the moment we use an optimizer that respects overflows. +const optimizer: ExpressionTransformer = new StandardOptimizer(); + // 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. @@ -94,40 +107,79 @@ function ensureMethodArity( } } -function evalUnaryOp( +export function evalUnaryOp( op: ASTUnaryOperation, - operand: ASTExpression, - source: ASTRef, - ctx: CompilerContext, + valOperand: Value +): Value { + return __evalUnaryOp(op, valOperand, DUMMY_AST_REF, DUMMY_AST_REF); +} + +function __evalUnaryOp( + op: ASTUnaryOperation, + valOperand: Value, + operandRef: ASTRef, + source: ASTRef ): 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 (operand.kind === "number" && op === "-") { - // emulating negative integer literals - return ensureInt(-operand.value, source); - } - const valOperand = evalConstantExpression(operand, ctx); switch (op) { case "+": - return ensureInt(valOperand, operand.ref); + return ensureInt(valOperand, operandRef); case "-": - return ensureInt(-ensureInt(valOperand, operand.ref), source); + return ensureInt(-ensureInt(valOperand, operandRef), source); case "~": - return ~ensureInt(valOperand, operand.ref); + return ~ensureInt(valOperand, operandRef); case "!": - return !ensureBoolean(valOperand, operand.ref); + return !ensureBoolean(valOperand, operandRef); case "!!": if (valOperand === null) { throwErrorConstEval( "non-null value expected but got null", - operand.ref, + operandRef, ); } return valOperand; } } + +function fullyEvalUnaryOp( + op: ASTUnaryOperation, + operand: ASTExpression, + source: ASTRef, + ctx: CompilerContext, +): 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 (operand.kind === "number" && op === "-") { + // emulating negative integer literals + return ensureInt(-operand.value, source); + } + + const valOperand = evalConstantExpression(operand, ctx); + + return __evalUnaryOp(op, valOperand, operand.ref, source); +} + +function partiallyEvalUnaryOp( + op: ASTUnaryOperation, + oper: ASTExpression, + source: ASTRef, + ctx: CompilerContext, +): ASTExpression { + const operand = partiallyEvalExpression(oper, ctx); + + if (isValue(operand)) { + const valueOperand = extractValue(operand as ValueExpression); + const result = __evalUnaryOp(op, valueOperand, operand.ref, source); + // Wrap the value into a Tree to continue simplifications + return makeValueExpression(result); + } else { + const newAst = makeUnaryExpression(op, operand); + return optimizer.applyRules(newAst); + } +} + // precondition: the divisor is not zero // rounds the division result towards negative infinity function divFloor(a: bigint, b: bigint): bigint { @@ -145,29 +197,71 @@ function modFloor(a: bigint, b: bigint): bigint { return a - divFloor(a, b) * b; } -function evalBinaryOp( +function fullyEvalBinaryOp( op: ASTBinaryOperation, left: ASTExpression, right: ASTExpression, source: ASTRef, - ctx: CompilerContext, + ctx: CompilerContext ): Value { const valLeft = evalConstantExpression(left, ctx); const valRight = evalConstantExpression(right, ctx); + + return __evalBinaryOp(op, valLeft, valRight, left.ref, right.ref, source); +} + +function partiallyEvalBinaryOp( + op: ASTBinaryOperation, + left: ASTExpression, + right: ASTExpression, + source: ASTRef, + ctx: CompilerContext +): ASTExpression { + const leftOperand = partiallyEvalExpression(left, ctx); + const rightOperand = partiallyEvalExpression(right, ctx); + + if (isValue(leftOperand) && isValue(rightOperand)) { + const valueLeftOperand = extractValue(leftOperand as ValueExpression); + const valueRightOperand = extractValue(rightOperand as ValueExpression); + const result = __evalBinaryOp(op, valueLeftOperand, valueRightOperand, leftOperand.ref, rightOperand.ref, source); + // Wrap the value into a Tree to continue simplifications + return makeValueExpression(result); + } else { + const newAst = makeBinaryExpression(op, leftOperand, rightOperand); + return optimizer.applyRules(newAst); + } +} + +export function evalBinaryOp( + op: ASTBinaryOperation, + valLeft: Value, + valRight: Value +): Value { + return __evalBinaryOp(op, valLeft, valRight, DUMMY_AST_REF, DUMMY_AST_REF, DUMMY_AST_REF); +} + +function __evalBinaryOp( + op: ASTBinaryOperation, + valLeft: Value, + valRight: Value, + refLeft: ASTRef, + refRight: ASTRef, + source: ASTRef +): Value { switch (op) { case "+": return ensureInt( - ensureInt(valLeft, left.ref) + ensureInt(valRight, right.ref), + ensureInt(valLeft, refLeft) + ensureInt(valRight, refRight), source, ); case "-": return ensureInt( - ensureInt(valLeft, left.ref) - ensureInt(valRight, right.ref), + ensureInt(valLeft, refLeft) - ensureInt(valRight, refRight), source, ); case "*": return ensureInt( - ensureInt(valLeft, left.ref) * ensureInt(valRight, right.ref), + ensureInt(valLeft, refLeft) * ensureInt(valRight, refRight), source, ); case "/": { @@ -175,44 +269,44 @@ function evalBinaryOp( // 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(valRight, right.ref); + const r = ensureInt(valRight, refRight); if (r === 0n) throwErrorConstEval( "divisor expression must be non-zero", - right.ref, + refRight, ); - return ensureInt(divFloor(ensureInt(valLeft, left.ref), r), source); + return ensureInt(divFloor(ensureInt(valLeft, refLeft), r), source); } case "%": { // Same as for division, see the comment above // Example: -1 % 5 = 4 - const r = ensureInt(valRight, right.ref); + const r = ensureInt(valRight, refRight); if (r === 0n) throwErrorConstEval( "divisor expression must be non-zero", - right.ref, + refRight, ); - return ensureInt(modFloor(ensureInt(valLeft, left.ref), r), source); + return ensureInt(modFloor(ensureInt(valLeft, refLeft), r), source); } case "&": return ( - ensureInt(valLeft, left.ref) & ensureInt(valRight, right.ref) + ensureInt(valLeft, refLeft) & ensureInt(valRight, refRight) ); case "|": return ( - ensureInt(valLeft, left.ref) | ensureInt(valRight, right.ref) + ensureInt(valLeft, refLeft) | ensureInt(valRight, refRight) ); case "^": return ( - ensureInt(valLeft, left.ref) ^ ensureInt(valRight, right.ref) + ensureInt(valLeft, refLeft) ^ ensureInt(valRight, refRight) ); case "<<": { - const valNum = ensureInt(valLeft, left.ref); - const valBits = ensureInt(valRight, right.ref); + const valNum = ensureInt(valLeft, refLeft); + const valBits = ensureInt(valRight, refRight); if (0n > valBits || valBits > 256n) { throwErrorConstEval( `the number of bits shifted ('${valBits}') must be within [0..256] range`, - right.ref, + refRight, ); } try { @@ -228,12 +322,12 @@ function evalBinaryOp( } } case ">>": { - const valNum = ensureInt(valLeft, left.ref); - const valBits = ensureInt(valRight, right.ref); + const valNum = ensureInt(valLeft, refLeft); + const valBits = ensureInt(valRight, refRight); if (0n > valBits || valBits > 256n) { throwErrorConstEval( `the number of bits shifted ('${valBits}') must be within [0..256] range`, - right.ref, + refRight, ); } try { @@ -250,19 +344,19 @@ function evalBinaryOp( } case ">": return ( - ensureInt(valLeft, left.ref) > ensureInt(valRight, right.ref) + ensureInt(valLeft, refLeft) > ensureInt(valRight, refRight) ); case "<": return ( - ensureInt(valLeft, left.ref) < ensureInt(valRight, right.ref) + ensureInt(valLeft, refLeft) < ensureInt(valRight, refRight) ); case ">=": return ( - ensureInt(valLeft, left.ref) >= ensureInt(valRight, right.ref) + ensureInt(valLeft, refLeft) >= ensureInt(valRight, refRight) ); case "<=": return ( - ensureInt(valLeft, left.ref) <= ensureInt(valRight, right.ref) + ensureInt(valLeft, refLeft) <= ensureInt(valRight, refRight) ); case "==": // the null comparisons account for optional types, e.g. @@ -288,13 +382,13 @@ function evalBinaryOp( return valLeft !== valRight; case "&&": return ( - ensureBoolean(valLeft, left.ref) && - ensureBoolean(valRight, right.ref) + ensureBoolean(valLeft, refLeft) && + ensureBoolean(valRight, refRight) ); case "||": return ( - ensureBoolean(valLeft, left.ref) || - ensureBoolean(valRight, right.ref) + ensureBoolean(valLeft, refLeft) || + ensureBoolean(valRight, refRight) ); } } @@ -667,9 +761,9 @@ export function evalConstantExpression( case "string": return ensureString(interpretEscapeSequences(ast.value), ast.ref); case "op_unary": - return evalUnaryOp(ast.op, ast.right, ast.ref, ctx); + return fullyEvalUnaryOp(ast.op, ast.right, ast.ref, ctx); case "op_binary": - return evalBinaryOp(ast.op, ast.left, ast.right, ast.ref, ctx); + return fullyEvalBinaryOp(ast.op, ast.left, ast.right, ast.ref, ctx); case "conditional": return evalConditional( ast.condition, @@ -685,3 +779,54 @@ export function evalConstantExpression( return evalBuiltins(ast.name, ast.args, ast.ref, ctx); } } + + +function partiallyEvalExpression(ast: ASTExpression, ctx: CompilerContext): ASTExpression { + switch (ast.kind) { + case "id": + // For the moment, id look up is not supported. I just return the node for the moment. + return ast; + case "op_call": + // Not supported yet. I just return the node for the moment. + return ast; + case "init_of": + // Not supported yet. I just return the node for the moment. + return ast; + case "null": + return ast; + case "boolean": + return ast; + case "number": + ensureInt(ast.value, ast.ref); + return ast; + // TODO: ensure string is representable + case "string": + return ast; + case "op_unary": + return partiallyEvalUnaryOp(ast.op, ast.right, ast.ref, ctx); + case "op_binary": + return partiallyEvalBinaryOp(ast.op, ast.left, ast.right, ast.ref, ctx); + case "conditional": + // Not supported yet. I just return the node for the moment. + return ast; + //return evalConditional( + // ast.condition, + // ast.thenBranch, + // ast.elseBranch, + // ctx, + //); + case "op_new": + // Not supported yet. I just return the node for the moment. + return ast; + //return evalStructInstance(ast.type, ast.args, ctx); + case "op_field": + // Not supported yet. I just return the node for the moment. + return ast; + //return evalFieldAccess(ast.src, ast.name, ctx); + case "op_static_call": + // Not supported yet. I just return the node for the moment. + return ast; + //return evalBuiltins(ast.name, ast.args, ast.ref, ctx); + } +} + diff --git a/src/optimizer/algebraic.ts b/src/optimizer/algebraic.ts new file mode 100644 index 000000000..6c9af66d8 --- /dev/null +++ b/src/optimizer/algebraic.ts @@ -0,0 +1 @@ +// This module will include the simpler algebraic rules (i.e., those not involving associativity) diff --git a/src/optimizer/associative.ts b/src/optimizer/associative.ts new file mode 100644 index 000000000..1577fbebc --- /dev/null +++ b/src/optimizer/associative.ts @@ -0,0 +1,451 @@ +// This module includes rules involving associative rewritings of expressions + +import { evalBinaryOp } from "../constEval"; +import { ASTBinaryOperation, ASTExpression, ASTOpBinary } from "../grammar/ast"; +import { ExpressionTransformer, Rule, ValueExpression } from "./types"; +import { + checkIsBinaryOpNode, + checkIsBinaryOp_NonValue_Value, + checkIsBinaryOp_Value_NonValue, + extractValue, + isValue, + makeBinaryExpression, + makeValueExpression +} from "./util"; + +export abstract class AssociativeRewriteRule extends Rule { + + // An entry (op, S) in the map means "operator op associates with all operators in set S", + // mathematically: all op2 \in S. (a op b) op2 c = a op (b op2 c) + private associativeOps: Map>; + + // This set contains all operators that commute. + // Mathematically: all op \in commutativeOps. a op b = b op a + private commutativeOps: Set; + + constructor(priority: number) { + super(priority); + + // + associates with these on the right: + // i.e., all op \in plusAssoc. (a + b) op c = a + (b op c) + const plusAssoc = new Set([ + "+", "-" + ]); + + // - does not associate with any operator on the right + + // * associates with these on the right: + const multAssoc = new Set([ + "*", "<<" + ]); + + // Division / does not associate with any on the right + + // Modulus % does not associate with any on the right + + // TODO: shifts, bitwise integer operators, boolean operators + + this.associativeOps = new Map>([ + ["+", plusAssoc], + ["*", multAssoc] + ]); + + this.commutativeOps = new Set( + ["+", "*", "!=", "==", "&&", "||"] // TODO: bitwise integer operators + ); + } + + public areAssociative(op1: ASTBinaryOperation, op2: ASTBinaryOperation): boolean { + if (this.associativeOps.has(op1)) { + var rightAssocs = this.associativeOps.get(op1)!; + return rightAssocs.has(op2); + } else { + return false; + } + } + + public isCommutative(op: ASTBinaryOperation): boolean { + return this.commutativeOps.has(op); + } +} + +export abstract class AllowableOpRule extends AssociativeRewriteRule { + + private allowedOps: Set; + + constructor(priority: number) { + super(priority); + + this.allowedOps = new Set( + // Recall that integer operators +,-,*,/,% are not safe with this rule, because + // there is a risk that they will not preserve overflows in the unknown operands. + ["&&", "||"] // TODO: check bitwise integer operators + ); + } + + public isAllowedOp(op: ASTBinaryOperation): boolean { + return this.allowedOps.has(op); + } + + public areAllowedOps(op: ASTBinaryOperation[]): boolean { + return op.reduce((prev,curr) => prev && this.allowedOps.has(curr), true); + } +} + +export class AssociativeRule1 extends AllowableOpRule { + + constructor(priority: number) { + super(priority); + } + + + public applyRule(ast: ASTExpression, optimizer: ExpressionTransformer): ASTExpression { + if (checkIsBinaryOpNode(ast)) { + const topLevelNode = ast as ASTOpBinary; + if (checkIsBinaryOp_NonValue_Value(topLevelNode.left) && checkIsBinaryOp_NonValue_Value(topLevelNode.right)) { + // The tree has this form: + // (x1 op1 c1) op (x2 op2 c2) + const leftTree = topLevelNode.left as ASTOpBinary; + const rightTree = topLevelNode.right as ASTOpBinary; + + const x1 = leftTree.left; + const c1 = leftTree.right as ValueExpression; + const op1 = leftTree.op; + + const x2 = rightTree.left; + const c2 = rightTree.right as ValueExpression; + const op2 = rightTree.op; + + const op = topLevelNode.op; + + // Check that: + // the operators are allowed + // op1 and op associate + // op and op2 asociate + // op commutes + if ( + this.areAllowedOps([op1, op, op2]) && + this.areAssociative(op1, op) && + this.areAssociative(op, op2) && + this.isCommutative(op) + ) { + // Agglutinate the constants and compute their final value + try { + // If an error occurs, we abandon the simplification + const val = evalBinaryOp(op2, extractValue(c1), extractValue(c2)); + + // The final expression is + // (x1 op1 x2) op val + + // Because we are joining x1 and x2, + // 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)); + const newRight = makeValueExpression(val); + return makeBinaryExpression(op, newLeft, newRight); + } catch (e) { + } + } + + } else if (checkIsBinaryOp_NonValue_Value(topLevelNode.left) && checkIsBinaryOp_Value_NonValue(topLevelNode.right)) { + // The tree has this form: + // (x1 op1 c1) op (c2 op2 x2) + const leftTree = topLevelNode.left as ASTOpBinary; + const rightTree = topLevelNode.right as ASTOpBinary; + + const x1 = leftTree.left; + const c1 = leftTree.right as ValueExpression; + const op1 = leftTree.op; + + const x2 = rightTree.right; + const c2 = rightTree.left as ValueExpression; + const op2 = rightTree.op; + + const op = topLevelNode.op; + + // Check that: + // the operators are allowed + // op1 and op associate + // op and op2 asociate + if ( + this.areAllowedOps([op1, op, op2]) && + this.areAssociative(op1, op) && + this.areAssociative(op, op2) + ) { + // Agglutinate the constants and compute their final value + try { + // If an error occurs, we abandon the simplification + const val = evalBinaryOp(op, extractValue(c1), extractValue(c2)); + + // The current expression could be either + // x1 op1 (val op2 x2) or + // (x1 op1 val) op2 x2 <--- we choose this form. + // Other rules will attempt to extract the constant outside the expression. + + // 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 newLeft = optimizer.applyRules(makeBinaryExpression(op1, x1, newValNode)); + return makeBinaryExpression(op2, newLeft, x2); + } catch (e) { + } + } + + } else if (checkIsBinaryOp_Value_NonValue(topLevelNode.left) && checkIsBinaryOp_NonValue_Value(topLevelNode.right)) { + // The tree has this form: + // (c1 op1 x1) op (x2 op2 c2) + const leftTree = topLevelNode.left as ASTOpBinary; + const rightTree = topLevelNode.right as ASTOpBinary; + + const x1 = leftTree.right; + const c1 = leftTree.left as ValueExpression; + const op1 = leftTree.op; + + const x2 = rightTree.left; + const c2 = rightTree.right as ValueExpression; + const op2 = rightTree.op; + + const op = topLevelNode.op; + + // Check that: + // the operators are allowed + // op and op1 associate + // op2 and op asociate + // op commutes + if ( + this.areAllowedOps([op1, op, op2]) && + this.areAssociative(op, op1) && + this.areAssociative(op2, op) && + this.isCommutative(op) + ) { + // Agglutinate the constants and compute their final value + try { + // If an error occurs, we abandon the simplification + const val = evalBinaryOp(op, extractValue(c2), extractValue(c1)); + + // The current expression could be either + // x2 op2 (val op1 x1) or + // (x2 op2 val) op1 x1 <--- we choose this form. + // Other rules will attempt to extract the constant outside the expression. + + // 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 newLeft = optimizer.applyRules(makeBinaryExpression(op2, x2, newValNode)); + return makeBinaryExpression(op1, newLeft, x1); + } catch (e) { + } + } + } else if (checkIsBinaryOp_Value_NonValue(topLevelNode.left) && checkIsBinaryOp_Value_NonValue(topLevelNode.right)) { + // The tree has this form: + // (c1 op1 x1) op (c2 op2 x2) + const leftTree = topLevelNode.left as ASTOpBinary; + const rightTree = topLevelNode.right as ASTOpBinary; + + const x1 = leftTree.right; + const c1 = leftTree.left as ValueExpression; + const op1 = leftTree.op; + + const x2 = rightTree.right; + const c2 = rightTree.left as ValueExpression; + const op2 = rightTree.op; + + const op = topLevelNode.op; + + // Check that: + // the operators are allowed + // op1 and op associate + // op and op2 asociate + // op commutes + if ( + this.areAllowedOps([op1, op, op2]) && + this.areAssociative(op1, op) && + this.areAssociative(op, op2) && + this.isCommutative(op) + ) { + // Agglutinate the constants and compute their final value + try { + // If an error occurs, we abandon the simplification + const val = evalBinaryOp(op1, extractValue(c1), extractValue(c2)); + + // The final expression is + // val op (x1 op2 x2) + + // Because we are joining x1 and x2, + // 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)); + const newLeft = makeValueExpression(val); + return makeBinaryExpression(op, newLeft, newRight); + } catch (e) { + } + } + } + } + + // If execution reaches here, it means that the rule could not be applied fully + // so, we return the original tree + return ast; + } + +} + +export class AssociativeRule2 extends AllowableOpRule { + + constructor(priority: number) { + super(priority); + } + + public applyRule(ast: ASTExpression, optimizer: ExpressionTransformer): ASTExpression { + if (checkIsBinaryOpNode(ast)) { + const topLevelNode = ast as ASTOpBinary; + if (checkIsBinaryOp_NonValue_Value(topLevelNode.left) && !isValue(topLevelNode.right)) { + // The tree has this form: + // (x1 op1 c1) op x2 + const leftTree = topLevelNode.left as ASTOpBinary; + const rightTree = topLevelNode.right; + + const x1 = leftTree.left; + const c1 = leftTree.right as ValueExpression; + const op1 = leftTree.op; + + const x2 = rightTree; + + const op = topLevelNode.op; + + // Check that: + // the operators are allowed + // op1 and op associate + // op commutes + if ( + this.areAllowedOps([op1, op]) && + this.areAssociative(op1, op) && + this.isCommutative(op) + ) { + // The final expression is + // (x1 op1 x2) op c1 + + // Because we are joining x1 and x2, + // 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)); + return makeBinaryExpression(op, newLeft, c1); + } + + } else if (checkIsBinaryOp_Value_NonValue(topLevelNode.left) && !isValue(topLevelNode.right)) { + // The tree has this form: + // (c1 op1 x1) op x2 + const leftTree = topLevelNode.left as ASTOpBinary; + const rightTree = topLevelNode.right; + + const x1 = leftTree.right; + const c1 = leftTree.left as ValueExpression; + const op1 = leftTree.op; + + const x2 = rightTree; + + const op = topLevelNode.op; + + // Check that: + // the operators are allowed + // op1 and op associate + if ( + this.areAllowedOps([op1, op]) && + this.areAssociative(op1, op) + ) { + // The final expression is + // c1 op1 (x1 op x2) + + // Because we are joining x1 and x2, + // 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)); + return makeBinaryExpression(op1, c1, newRight); + } + } else if (!isValue(topLevelNode.left) && checkIsBinaryOp_NonValue_Value(topLevelNode.right)) { + // The tree has this form: + // x2 op (x1 op1 c1) + const leftTree = topLevelNode.left; + const rightTree = topLevelNode.right as ASTOpBinary; + + const x1 = rightTree.left; + const c1 = rightTree.right as ValueExpression; + const op1 = rightTree.op; + + const x2 = leftTree; + + const op = topLevelNode.op; + + // Check that: + // the operators are allowed + // op and op1 associate + if ( + this.areAllowedOps([op, op1]) && + this.areAssociative(op, op1) + ) { + // The final expression is + // (x2 op x1) op1 c1 + + // Because we are joining x1 and x2, + // 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)); + return makeBinaryExpression(op1, newLeft, c1); + } + } else if (!isValue(topLevelNode.left) && checkIsBinaryOp_Value_NonValue(topLevelNode.right)) { + // The tree has this form: + // x2 op (c1 op1 x1) + const leftTree = topLevelNode.left; + const rightTree = topLevelNode.right as ASTOpBinary; + + const x1 = rightTree.right; + const c1 = rightTree.left as ValueExpression; + const op1 = rightTree.op; + + const x2 = leftTree; + + const op = topLevelNode.op; + + // Check that: + // the operators are allowed + // op and op1 associate + // op is commutative + if ( + this.areAllowedOps([op, op1]) && + this.areAssociative(op, op1) && + this.isCommutative(op) + ) { + // The final expression is + // c1 op (x2 op1 x1) + + // Because we are joining x1 and x2, + // 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)); + return makeBinaryExpression(op, c1, newRight); + } + } + } + + // If execution reaches here, it means that the rule could not be applied fully + // so, we return the original tree + return ast; + } +} + +export class AssociativeRule3 extends AssociativeRewriteRule { + + constructor(priority: number) { + super(priority); + } + + public applyRule(ast: ASTExpression, optimizer: ExpressionTransformer): ASTExpression { + // TODO: Implementation of rule 3 in the comments of the repository + + // If execution reaches here, it means that the rule could not be applied fully + // so, we return the original tree + return ast; + } +} diff --git a/src/optimizer/standardOptimizer.ts b/src/optimizer/standardOptimizer.ts new file mode 100644 index 000000000..b64769205 --- /dev/null +++ b/src/optimizer/standardOptimizer.ts @@ -0,0 +1,29 @@ +import { ASTExpression } from "../grammar/ast"; +import { AssociativeRule1, AssociativeRule2, AssociativeRule3 } from "./associative"; +import { Rule, ExpressionTransformer } from "./types"; + +// This optimizer uses rules that preserve overflows in integer expressions. +export class StandardOptimizer implements ExpressionTransformer { + + private rules: Rule[]; + + constructor() { + this.rules = [ + new AssociativeRule1(0), + new AssociativeRule2(1), + new AssociativeRule3(3) + // TODO: add simpler algebraic rules that will be added to algebraic.ts + ]; + + // Sort according to the priorities: smaller number means greater priority. + // So, the rules will be sorted increasingly according to their priority number. + this.rules.sort((r1, r2) => r1.getPriority() - r2.getPriority()); + } + + public applyRules(ast: ASTExpression): ASTExpression { + var result = ast; + this.rules.forEach(rule => result = rule.applyRule(result, this)); + return result; + } + +} \ No newline at end of file diff --git a/src/optimizer/types.ts b/src/optimizer/types.ts new file mode 100644 index 000000000..7fc0ff09d --- /dev/null +++ b/src/optimizer/types.ts @@ -0,0 +1,52 @@ +import { Interval } from "ohm-js"; +import { + ASTExpression, + ASTNumber, + ASTBoolean, + ASTNull, + ASTString, + ASTOpBinary, + ASTRef + } from "../grammar/ast"; + +export type ValueExpression = ASTNumber | ASTBoolean | ASTNull | ASTString; + +export const DUMMY_INTERVAL: Interval = { + sourceString: "", + startIdx: 0, + endIdx: 10, + contents: "mock contents", + minus: jest.fn().mockReturnThis(), + relativeTo: jest.fn().mockReturnThis(), + subInterval: jest.fn().mockReturnThis(), + collapsedLeft: jest.fn().mockReturnThis(), + collapsedRight: jest.fn().mockReturnThis(), + trimmed: jest.fn().mockReturnThis(), + coverageWith: jest.fn().mockReturnThis(), + getLineAndColumnMessage: jest.fn().mockReturnValue(`Line 1, Column 0`), + getLineAndColumn: jest.fn().mockReturnValue({ line: 1, column: 0 }), + }; +export const DUMMY_AST_REF: ASTRef = new ASTRef(DUMMY_INTERVAL, "dummy"); + + +export interface ExpressionTransformer { + applyRules(ast: ASTExpression): ASTExpression +} + +export abstract class Rule { + + private priority: number; + + constructor(priority: number) { + this.priority = priority; + } + + public abstract applyRule(ast: ASTExpression, optimizer: ExpressionTransformer): ASTExpression; + + // A smaller number means greater priority. + // Hence, negative numbers have higher priority than positive numbers. + public getPriority(): number { + return this.priority; + } + +} \ No newline at end of file diff --git a/src/optimizer/util.ts b/src/optimizer/util.ts new file mode 100644 index 000000000..7bf736768 --- /dev/null +++ b/src/optimizer/util.ts @@ -0,0 +1,127 @@ +import { + ASTExpression, + ASTRef, + ASTUnaryOperation, + ASTBinaryOperation, + createNode +} from "../grammar/ast"; +import { Value } from "../types/types"; +import { DUMMY_AST_REF, ValueExpression } from "./types"; + +export function isValue(ast: ASTExpression): boolean { + switch (ast.kind) { // Missing structs + case "null": + case "boolean": + case "number": + case "string": + return true; + + case "id": + case "op_call": + case "init_of": + case "op_unary": + case "op_binary": + case "conditional": + case "op_new": + case "op_field": + case "op_static_call": + return false; + } +} + +export function extractValue(ast: ValueExpression): Value { + switch (ast.kind) { // Missing structs + case "null": + return null; + case "boolean": + return ast.value; + case "number": + return ast.value; + case "string": + return ast.value; + } +} + +export function makeValueExpression(value: Value): ValueExpression { + if (value === null) { + const result = createNode({ + kind: "null", + ref: DUMMY_AST_REF + }); + return result as ValueExpression; + } + if (typeof value === "string") { + const result = createNode({ + kind: "string", + value: value, + ref: DUMMY_AST_REF + }); + return result as ValueExpression; + } + if (typeof value === "bigint") { + const result = createNode({ + kind: "number", + value: value, + ref: DUMMY_AST_REF + }); + return result as ValueExpression; + } + if (typeof value === "boolean") { + const result = createNode({ + kind: "boolean", + value: value, + ref: DUMMY_AST_REF + }); + return result as ValueExpression; + } + throw `Unsupported value ${value}`; +} + + +export function makeUnaryExpression(op: ASTUnaryOperation, operand: ASTExpression): ASTExpression { + const result = createNode({ + kind: "op_unary", + op: op, + right: operand, + ref: DUMMY_AST_REF + }); + return result as ASTExpression; +} + +export function makeBinaryExpression(op: ASTBinaryOperation, left: ASTExpression, right: ASTExpression): ASTExpression { + const result = createNode({ + kind: "op_binary", + op: op, + left: left, + right: right, + ref: DUMMY_AST_REF + }); + return result as ASTExpression; +} + +// Checks if the top level node is a binary op node +export function checkIsBinaryOpNode(ast: ASTExpression): boolean { + return (ast.kind === "op_binary"); +} + +// Checks if top level node is a binary op node +// with a non-value node on the left and +// value node on the right +export function checkIsBinaryOp_NonValue_Value(ast: ASTExpression): boolean { + if (ast.kind === "op_binary") { + return (!isValue(ast.left) && isValue(ast.right)) + } else { + return false; + } +} + +// Checks if top level node is a binary op node +// with a value node on the left and +// non-value node on the right +export function checkIsBinaryOp_Value_NonValue(ast: ASTExpression): boolean { + if (ast.kind === "op_binary") { + return (isValue(ast.left) && !isValue(ast.right)) + } else { + return false; + } +} \ No newline at end of file From aae1dd07fa26212c91491ba50abc3c2d6cb7850b Mon Sep 17 00:00:00 2001 From: jeshecdom Date: Tue, 2 Jul 2024 18:35:11 +0200 Subject: [PATCH 02/14] Finished associative rule 3. Refactored Rule type into an interface. Priorities are no longer a property of rules, but are managed by the optimizer. --- src/constEval.ts | 125 ++++++++------- src/optimizer/associative.ts | 241 ++++++++++++++++++++++++++--- src/optimizer/standardOptimizer.ts | 16 +- src/optimizer/types.ts | 62 ++++---- src/optimizer/util.ts | 31 +++- 5 files changed, 360 insertions(+), 115 deletions(-) diff --git a/src/constEval.ts b/src/constEval.ts index 0b344e558..ec2dec9f0 100644 --- a/src/constEval.ts +++ b/src/constEval.ts @@ -9,7 +9,7 @@ import { ASTRef, ASTUnaryOperation } from "./grammar/ast"; -import { throwConstEvalError } from "./errors"; +import { TactConstEvalError, throwConstEvalError } from "./errors"; import { CommentValue, StructValue, Value } from "./types/types"; import { sha256_sync } from "@ton/crypto"; import { @@ -17,7 +17,9 @@ import { extractValue, makeValueExpression, makeUnaryExpression, - makeBinaryExpression + makeBinaryExpression, + divFloor, + modFloor } from "./optimizer/util"; import { DUMMY_AST_REF, ExpressionTransformer, ValueExpression } from "./optimizer/types"; import { StandardOptimizer } from "./optimizer/standardOptimizer"; @@ -180,23 +182,6 @@ function partiallyEvalUnaryOp( } } -// precondition: the divisor is not zero -// rounds the division result towards negative infinity -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); -} - -// 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. -function modFloor(a: bigint, b: bigint): bigint { - return a - divFloor(a, b) * b; -} - function fullyEvalBinaryOp( op: ASTBinaryOperation, left: ASTExpression, @@ -725,25 +710,28 @@ function interpretEscapeSequences(stringLiteral: string) { ); } +function lookupID(ast: ASTId, ctx: CompilerContext): Value { + if (hasStaticConstant(ctx, ast.value)) { + const constant = getStaticConstant(ctx, ast.value); + if (constant.value !== undefined) { + return constant.value; + } else { + throwErrorConstEval( + `cannot evaluate declared constant "${ast.value}" as it does not have a body`, + ast.ref, + ); + } + } + throwNonFatalErrorConstEval("cannot evaluate a variable", ast.ref); +} + export function evalConstantExpression( ast: ASTExpression, ctx: CompilerContext, ): Value { switch (ast.kind) { case "id": - if (hasStaticConstant(ctx, ast.value)) { - const constant = getStaticConstant(ctx, ast.value); - if (constant.value !== undefined) { - return constant.value; - } else { - throwErrorConstEval( - `cannot evaluate declared constant "${ast.value}" as it does not have a body`, - ast.ref, - ); - } - } - throwNonFatalErrorConstEval("cannot evaluate a variable", ast.ref); - break; + return lookupID(ast, ctx); case "op_call": return evalMethod(ast.name, ast.src, ast.args, ast.ref, ctx); case "init_of": @@ -778,55 +766,74 @@ export function evalConstantExpression( case "op_static_call": return evalBuiltins(ast.name, ast.args, ast.ref, ctx); } + /*const res = partiallyEvalExpression(ast, ctx); + if (isValue(res)) { + return extractValue(res as ValueExpression); + } else { + throwNonFatalErrorConstEval( + ``, + ast.ref + ); + }*/ } function partiallyEvalExpression(ast: ASTExpression, ctx: CompilerContext): ASTExpression { switch (ast.kind) { case "id": - // For the moment, id look up is not supported. I just return the node for the moment. - return ast; + try { + return makeValueExpression(lookupID(ast, ctx)); + } catch(e) { + if (e instanceof TactConstEvalError) { + if (!e.fatal) { + // If a non-fatal error occurs during lookup, just return the symbol + return ast; + } + } + throw e; + } case "op_call": - // Not supported yet. I just return the node for the moment. - return ast; + // Does not partially evaluate at the monent. Will attemp to fully evaluate + const resCall = evalMethod(ast.name, ast.src, ast.args, ast.ref, ctx); + return makeValueExpression(resCall); case "init_of": - // Not supported yet. I just return the node for the moment. - return ast; + throwNonFatalErrorConstEval( + "initOf is not supported at this moment", + ast.ref, + ); case "null": return ast; case "boolean": return ast; case "number": - ensureInt(ast.value, ast.ref); - return ast; - // TODO: ensure string is representable + return makeValueExpression(ensureInt(ast.value, ast.ref)); case "string": - return ast; + return makeValueExpression(ensureString(interpretEscapeSequences(ast.value), ast.ref)); case "op_unary": return partiallyEvalUnaryOp(ast.op, ast.right, ast.ref, ctx); case "op_binary": return partiallyEvalBinaryOp(ast.op, ast.left, ast.right, ast.ref, ctx); case "conditional": - // Not supported yet. I just return the node for the moment. - return ast; - //return evalConditional( - // ast.condition, - // ast.thenBranch, - // ast.elseBranch, - // ctx, - //); + // Does not partially evaluate at the monent. Will attemp to fully evaluate + const resCond = evalConditional( + ast.condition, + ast.thenBranch, + ast.elseBranch, + ctx, + ); + return makeValueExpression(resCond); case "op_new": - // Not supported yet. I just return the node for the moment. - return ast; - //return evalStructInstance(ast.type, ast.args, ctx); + // Does not partially evaluate at the monent. Will attemp to fully evaluate + const resStruct = evalStructInstance(ast.type, ast.args, ctx); + return makeValueExpression(resStruct); case "op_field": - // Not supported yet. I just return the node for the moment. - return ast; - //return evalFieldAccess(ast.src, ast.name, ctx); + // Does not partially evaluate at the monent. Will attemp to fully evaluate + const resField = evalFieldAccess(ast.src, ast.name, ast.ref, ctx); + return makeValueExpression(resField); case "op_static_call": - // Not supported yet. I just return the node for the moment. - return ast; - //return evalBuiltins(ast.name, ast.args, ast.ref, ctx); + // Does not partially evaluate at the monent. Will attemp to fully evaluate + const resStaticCall = evalBuiltins(ast.name, ast.args, ast.ref, ctx); + return makeValueExpression(resStaticCall); } } diff --git a/src/optimizer/associative.ts b/src/optimizer/associative.ts index 1577fbebc..1e24da338 100644 --- a/src/optimizer/associative.ts +++ b/src/optimizer/associative.ts @@ -2,18 +2,21 @@ import { evalBinaryOp } from "../constEval"; import { ASTBinaryOperation, ASTExpression, ASTOpBinary } from "../grammar/ast"; +import { Value } from "../types/types"; import { ExpressionTransformer, Rule, ValueExpression } from "./types"; import { + abs, checkIsBinaryOpNode, checkIsBinaryOp_NonValue_Value, checkIsBinaryOp_Value_NonValue, extractValue, isValue, makeBinaryExpression, - makeValueExpression + makeValueExpression, + sign } from "./util"; -export abstract class AssociativeRewriteRule extends Rule { +export abstract class AssociativeRewriteRule implements Rule { // An entry (op, S) in the map means "operator op associates with all operators in set S", // mathematically: all op2 \in S. (a op b) op2 c = a op (b op2 c) @@ -23,9 +26,7 @@ export abstract class AssociativeRewriteRule extends Rule { // Mathematically: all op \in commutativeOps. a op b = b op a private commutativeOps: Set; - constructor(priority: number) { - super(priority); - + constructor() { // + associates with these on the right: // i.e., all op \in plusAssoc. (a + b) op c = a + (b op c) const plusAssoc = new Set([ @@ -55,6 +56,10 @@ export abstract class AssociativeRewriteRule extends Rule { ); } + + public abstract applyRule(ast: ASTExpression, optimizer: ExpressionTransformer): ASTExpression; + + public areAssociative(op1: ASTBinaryOperation, op2: ASTBinaryOperation): boolean { if (this.associativeOps.has(op1)) { var rightAssocs = this.associativeOps.get(op1)!; @@ -73,9 +78,9 @@ export abstract class AllowableOpRule extends AssociativeRewriteRule { private allowedOps: Set; - constructor(priority: number) { - super(priority); - + constructor() { + super(); + this.allowedOps = new Set( // Recall that integer operators +,-,*,/,% are not safe with this rule, because // there is a risk that they will not preserve overflows in the unknown operands. @@ -94,11 +99,6 @@ export abstract class AllowableOpRule extends AssociativeRewriteRule { export class AssociativeRule1 extends AllowableOpRule { - constructor(priority: number) { - super(priority); - } - - public applyRule(ast: ASTExpression, optimizer: ExpressionTransformer): ASTExpression { if (checkIsBinaryOpNode(ast)) { const topLevelNode = ast as ASTOpBinary; @@ -294,10 +294,6 @@ export class AssociativeRule1 extends AllowableOpRule { export class AssociativeRule2 extends AllowableOpRule { - constructor(priority: number) { - super(priority); - } - public applyRule(ast: ASTExpression, optimizer: ExpressionTransformer): ASTExpression { if (checkIsBinaryOpNode(ast)) { const topLevelNode = ast as ASTOpBinary; @@ -435,17 +431,220 @@ export class AssociativeRule2 extends AllowableOpRule { } } +function ensureInt(val: Value): bigint { + if (typeof val !== "bigint") { + throw `integer expected, but got '${val}'`; + } + return val; +} + export class AssociativeRule3 extends AssociativeRewriteRule { - constructor(priority: number) { - super(priority); + private extraOpCondition: Map boolean>; + + public constructor() { + super(); + + this.extraOpCondition = new Map boolean>([ + ["+", (c1, c2, val) => { + const n1 = ensureInt(c1); + const res = ensureInt(val); + return sign(n1) === sign(res) && abs(n1) <= abs(res); + } + ], + + ["-", (c1, c2, val) => { + const n1 = ensureInt(c1); + const res = ensureInt(val); + return sign(n1) === sign(res) && abs(n1) <= abs(res); + } + ], + + ["*", (c1, c2, val) => { + const n1 = ensureInt(c1); + const res = ensureInt(val); + if (n1 < 0n) { + if (sign(n1) === sign(res)) { + return abs(n1) <= abs(res); + } else { + return abs(n1) < abs(res); + } + } else if (n1 === 0n) { + return true; + } else { + return abs(n1) <= abs(res); + } + } + ], + ]); + + } + + protected opSatisfiesConditions(op: ASTBinaryOperation, c1: Value, c2: Value, res: Value): boolean { + if (this.extraOpCondition.has(op)) { + return this.extraOpCondition.get(op)!(c1, c2, res); + } else { + return false; + } } public applyRule(ast: ASTExpression, optimizer: ExpressionTransformer): ASTExpression { - // TODO: Implementation of rule 3 in the comments of the repository + if (checkIsBinaryOpNode(ast)) { + const topLevelNode = ast as ASTOpBinary; + if (checkIsBinaryOp_NonValue_Value(topLevelNode.left) && isValue(topLevelNode.right)) { + // The tree has this form: + // (x1 op1 c1) op c2 + const leftTree = topLevelNode.left as ASTOpBinary; + const rightTree = topLevelNode.right as ValueExpression; + + const x1 = leftTree.left; + const c1 = extractValue(leftTree.right as ValueExpression); + const op1 = leftTree.op; + + const c2 = extractValue(rightTree); + + const op = topLevelNode.op; + + // Agglutinate the constants and compute their final value + try { + // If an error occurs, we abandon the simplification + const val = evalBinaryOp(op, c1, c2); + + // Check that: + // op1 and op associate + // the extra conditions on op1 + + if ( + this.areAssociative(op1, op) && + this.opSatisfiesConditions(op1, c1, c2, val) + ) { + + // The final expression is + // x1 op1 val + + const newConstant = makeValueExpression(val); + return makeBinaryExpression(op1, x1, newConstant); + } + } catch(e) { + } + + } else if (checkIsBinaryOp_Value_NonValue(topLevelNode.left) && isValue(topLevelNode.right)) { + // The tree has this form: + // (c1 op1 x1) op c2 + const leftTree = topLevelNode.left as ASTOpBinary; + const rightTree = topLevelNode.right as ValueExpression; + + const x1 = leftTree.right; + const c1 = extractValue(leftTree.left as ValueExpression); + const op1 = leftTree.op; + + const c2 = extractValue(rightTree); + + const op = topLevelNode.op; + + // Agglutinate the constants and compute their final value + try { + // If an error occurs, we abandon the simplification + const val = evalBinaryOp(op, c1, c2); + + // Check that: + // op1 and op associate + // op1 commutes + // the extra conditions on op1 + + if ( + this.areAssociative(op1, op) && + this.isCommutative(op1) && + this.opSatisfiesConditions(op1, c1, c2, val) + ) { + + // The final expression is + // x1 op1 val + + const newConstant = makeValueExpression(val); + return makeBinaryExpression(op1, x1, newConstant); + } + } catch(e) { + } + } else if (isValue(topLevelNode.left) && checkIsBinaryOp_NonValue_Value(topLevelNode.right)) { + // The tree has this form: + // c2 op (x1 op1 c1) + const leftTree = topLevelNode.left as ValueExpression; + const rightTree = topLevelNode.right as ASTOpBinary; + + const x1 = rightTree.left; + const c1 = extractValue(rightTree.right as ValueExpression); + const op1 = rightTree.op; + + const c2 = extractValue(leftTree); + + const op = topLevelNode.op; + + // Agglutinate the constants and compute their final value + try { + // If an error occurs, we abandon the simplification + const val = evalBinaryOp(op, c2, c1); + + // Check that: + // op and op1 associate + // op1 commutes + // the extra conditions on op1 + + if ( + this.areAssociative(op, op1) && + this.isCommutative(op1) && + this.opSatisfiesConditions(op1, c1, c2, val) + ) { + + // The final expression is + // x1 op1 val + + const newConstant = makeValueExpression(val); + return makeBinaryExpression(op1, x1, newConstant); + } + } catch(e) { + } + } else if (isValue(topLevelNode.left) && checkIsBinaryOp_Value_NonValue(topLevelNode.right)) { + // The tree has this form: + // c2 op (c1 op1 x1) + const leftTree = topLevelNode.left as ValueExpression; + const rightTree = topLevelNode.right as ASTOpBinary; + + const x1 = rightTree.right; + const c1 = extractValue(rightTree.left as ValueExpression); + const op1 = rightTree.op; + + const c2 = extractValue(leftTree); + + const op = topLevelNode.op; + + // Agglutinate the constants and compute their final value + try { + // If an error occurs, we abandon the simplification + const val = evalBinaryOp(op, c2, c1); + + // Check that: + // op and op1 associate + // the extra conditions on op1 + + if ( + this.areAssociative(op, op1) && + this.opSatisfiesConditions(op1, c1, c2, val) + ) { + + // The final expression is + // val op1 x1 + + const newConstant = makeValueExpression(val); + return makeBinaryExpression(op1, newConstant, x1); + } + } catch(e) { + } + } + } // If execution reaches here, it means that the rule could not be applied fully // so, we return the original tree return ast; - } + } } diff --git a/src/optimizer/standardOptimizer.ts b/src/optimizer/standardOptimizer.ts index b64769205..8de353f9b 100644 --- a/src/optimizer/standardOptimizer.ts +++ b/src/optimizer/standardOptimizer.ts @@ -2,28 +2,28 @@ import { ASTExpression } from "../grammar/ast"; import { AssociativeRule1, AssociativeRule2, AssociativeRule3 } from "./associative"; import { Rule, ExpressionTransformer } from "./types"; +type PrioritizedRule = {priority: number, rule: Rule}; + // This optimizer uses rules that preserve overflows in integer expressions. export class StandardOptimizer implements ExpressionTransformer { - private rules: Rule[]; + private rules: PrioritizedRule[]; constructor() { this.rules = [ - new AssociativeRule1(0), - new AssociativeRule2(1), - new AssociativeRule3(3) + {priority: 0, rule: new AssociativeRule1()}, + {priority: 1, rule: new AssociativeRule2()}, + {priority: 2, rule: new AssociativeRule3()} // TODO: add simpler algebraic rules that will be added to algebraic.ts ]; // Sort according to the priorities: smaller number means greater priority. // So, the rules will be sorted increasingly according to their priority number. - this.rules.sort((r1, r2) => r1.getPriority() - r2.getPriority()); + this.rules.sort((r1, r2) => r1.priority - r2.priority); } public applyRules(ast: ASTExpression): ASTExpression { - var result = ast; - this.rules.forEach(rule => result = rule.applyRule(result, this)); - return result; + return this.rules.reduce((prev, prioritizedRule) => prioritizedRule.rule.applyRule(prev, this), ast); } } \ No newline at end of file diff --git a/src/optimizer/types.ts b/src/optimizer/types.ts index 7fc0ff09d..a9088b307 100644 --- a/src/optimizer/types.ts +++ b/src/optimizer/types.ts @@ -5,7 +5,6 @@ import { ASTBoolean, ASTNull, ASTString, - ASTOpBinary, ASTRef } from "../grammar/ast"; @@ -16,15 +15,40 @@ export const DUMMY_INTERVAL: Interval = { startIdx: 0, endIdx: 10, contents: "mock contents", - minus: jest.fn().mockReturnThis(), - relativeTo: jest.fn().mockReturnThis(), - subInterval: jest.fn().mockReturnThis(), - collapsedLeft: jest.fn().mockReturnThis(), - collapsedRight: jest.fn().mockReturnThis(), - trimmed: jest.fn().mockReturnThis(), - coverageWith: jest.fn().mockReturnThis(), - getLineAndColumnMessage: jest.fn().mockReturnValue(`Line 1, Column 0`), - getLineAndColumn: jest.fn().mockReturnValue({ line: 1, column: 0 }), + minus(that) { + return [this]; + }, + relativeTo(that) { + return this; + }, + subInterval(offset, len) { + return this; + }, + collapsedLeft() { + return this; + }, + collapsedRight() { + return this; + }, + trimmed() { + return this; + }, + coverageWith(...intervals) { + return this; + }, + getLineAndColumnMessage() { + return `Line 1, Column 0`; + }, + getLineAndColumn() { + return { + offset: 0, + lineNum: 1, + colNum: 0, + line: "1", + nextLine: "1", + prevLine: "1" + }; + } }; export const DUMMY_AST_REF: ASTRef = new ASTRef(DUMMY_INTERVAL, "dummy"); @@ -33,20 +57,6 @@ export interface ExpressionTransformer { applyRules(ast: ASTExpression): ASTExpression } -export abstract class Rule { - - private priority: number; - - constructor(priority: number) { - this.priority = priority; - } - - public abstract applyRule(ast: ASTExpression, optimizer: ExpressionTransformer): ASTExpression; - - // A smaller number means greater priority. - // Hence, negative numbers have higher priority than positive numbers. - public getPriority(): number { - return this.priority; - } - +export interface Rule { + applyRule(ast: ASTExpression, optimizer: ExpressionTransformer): ASTExpression; } \ No newline at end of file diff --git a/src/optimizer/util.ts b/src/optimizer/util.ts index 7bf736768..151078084 100644 --- a/src/optimizer/util.ts +++ b/src/optimizer/util.ts @@ -1,6 +1,5 @@ import { ASTExpression, - ASTRef, ASTUnaryOperation, ASTBinaryOperation, createNode @@ -124,4 +123,34 @@ export function checkIsBinaryOp_Value_NonValue(ast: ASTExpression): boolean { } else { return 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; } \ No newline at end of file From 34970cb373f0bec4071821ea8e9ac3a6a1c53b2d Mon Sep 17 00:00:00 2001 From: jeshecdom Date: Thu, 4 Jul 2024 09:47:40 +0200 Subject: [PATCH 03/14] Added jest test cases for partial evaluator. --- src/optimizer/util.ts | 111 +++++++++++- src/test/e2e-emulated/partial-eval.spec.ts | 195 +++++++++++++++++++++ 2 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 src/test/e2e-emulated/partial-eval.spec.ts diff --git a/src/optimizer/util.ts b/src/optimizer/util.ts index a23512d4c..8867578d2 100644 --- a/src/optimizer/util.ts +++ b/src/optimizer/util.ts @@ -2,7 +2,8 @@ import { ASTExpression, ASTUnaryOperation, ASTBinaryOperation, - createNode + createNode, + ASTNewParameter } from "../grammar/ast"; import { Value } from "../types/types"; import { DUMMY_LOCATION, ValueExpression } from "./types"; @@ -153,4 +154,112 @@ export function sign(a: bigint): bigint { // 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; +} + +// Test equality of ASTExpressions. +export function areEqualExpressions(ast1: ASTExpression, ast2: ASTExpression): boolean { + switch (ast1.kind) { + case "null": + return ast2.kind === "null"; + case "boolean": + return ast2.kind === "boolean" ? ast1.value === ast2.value : false; + case "number": + return ast2.kind === "number" ? ast1.value === ast2.value : false; + case "string": + return ast2.kind === "string" ? ast1.value === ast2.value : false; + case "id": + return ast2.kind === "id" ? ast1.text === ast2.text : false; + case "op_call": + if (ast2.kind === "op_call") { + return ast1.name.text === ast2.name.text && + areEqualExpressions(ast1.src, ast2.src) && + areEqualExpressionArrays(ast1.args, ast2.args); + } else { + return false; + } + case "init_of": + if (ast2.kind === "init_of") { + return ast1.name.text === ast2.name.text && + areEqualExpressionArrays(ast1.args, ast2.args); + } else { + return false; + } + case "op_unary": + if (ast2.kind === "op_unary") { + return ast1.op === ast2.op && + areEqualExpressions(ast1.right, ast2.right); + } else { + return false; + } + case "op_binary": + if (ast2.kind === "op_binary") { + return ast1.op === ast2.op && + areEqualExpressions(ast1.left, ast2.left) && + areEqualExpressions(ast1.right, ast2.right); + } else { + return false; + } + case "conditional": + if (ast2.kind === "conditional") { + return areEqualExpressions(ast1.condition, ast2.condition) && + areEqualExpressions(ast1.thenBranch, ast2.thenBranch) && + areEqualExpressions(ast1.elseBranch, ast2.elseBranch); + } else { + return false; + } + case "op_new": + if (ast2.kind === "op_new") { + return ast1.type.text === ast2.type.text && + areEqualParameterArrays(ast1.args, ast2.args); + } else { + return false; + } + case "op_field": + if (ast2.kind === "op_field") { + return ast1.name.text === ast2.name.text && + areEqualExpressions(ast1.src, ast2.src); + } else { + return false; + } + case "op_static_call": + if (ast2.kind === "op_static_call") { + return ast1.name.text === ast2.name.text && + areEqualExpressionArrays(ast1.args, ast2.args); + } else { + return false; + } + } +} + +function areEqualParameters(arg1: ASTNewParameter, arg2: ASTNewParameter): boolean { + return arg1.name.text === arg2.name.text && + areEqualExpressions(arg1.exp, arg2.exp); +} + +function areEqualParameterArrays(arr1: ASTNewParameter[], arr2: ASTNewParameter[]): boolean { + if (arr1.length !== arr2.length) { + return false; + } + + for (var i = 0; i < arr1.length; i++) { + if (!areEqualParameters(arr1[i], arr2[i])) { + return false; + } + } + + return true; +} + +function areEqualExpressionArrays(arr1: ASTExpression[], arr2: ASTExpression[]): boolean { + if (arr1.length !== arr2.length) { + return false; + } + + for (var i = 0; i < arr1.length; i++) { + if (!areEqualExpressions(arr1[i], arr2[i])) { + return false; + } + } + + return true; } \ No newline at end of file diff --git a/src/test/e2e-emulated/partial-eval.spec.ts b/src/test/e2e-emulated/partial-eval.spec.ts new file mode 100644 index 000000000..6e5f3bd55 --- /dev/null +++ b/src/test/e2e-emulated/partial-eval.spec.ts @@ -0,0 +1,195 @@ +import { ASTExpression, __DANGER_resetNodeId, cloneASTNode } from "../../grammar/ast"; +import { parseExpression } from "../../grammar/grammar"; +import { areEqualExpressions, extractValue, isValue, makeValueExpression } from "../../optimizer/util"; +import { evalUnaryOp, partiallyEvalExpression } from "../../constEval"; +import { CompilerContext } from "../../context"; +import { ValueExpression } from "../../optimizer/types"; + +const additiveExpressions = [ + {original: "X + 3 + 1", simplified: "X + 4"}, + {original: "3 + X + 1", simplified: "X + 4"}, + {original: "1 + (X + 3)", simplified: "X + 4"}, + {original: "1 + (3 + X)", simplified: "4 + X"}, + + // Should NOT simplify to X + 2, because X could be MAX - 2, + // so that X + 3 causes an overflow, but X + 2 does not overflow + {original: "X + 3 - 1", simplified: "X + 3 - 1"}, + {original: "3 + X - 1", simplified: "3 + X - 1"}, + + // Should NOT simplify to X - 2, because X could be MIN + 2 + {original: "1 + (X - 3)", simplified: "1 + (X - 3)"}, + + {original: "1 + (3 - X)", simplified: "4 - X"}, + + {original: "X + 3 - (-1)", simplified: "X + 4"}, + {original: "3 + X - (-1)", simplified: "X + 4"}, + + // Should NOT simplify, because the current rules require that - commutes, + // which does not. This could be fixed in future rules. + {original: "-1 + (X - 3)", simplified: "-1 + (X - 3)"}, + + // Should NOT simplify to 2 - X, because X could be MIN + 3, + // so that 3 - X = -MIN = MAX + 1 causes an overflow, + // but 2 - X = -MIN - 1 = MAX does not + {original: "-1 + (3 - X)", simplified: "-1 + (3 - X)"}, + + // All the following cases should NOT simplify because - + // does not associate on the left with - or +. + // The following "associative rule" for - will be added in the future: + // (x - c1) op c2 -----> x + (-c1 op c2), where op \in {-,+} + {original: "1 - (X + 3)", simplified: "1 - (X + 3)"}, + {original: "1 - (3 + X)", simplified: "1 - (3 + X)"}, + {original: "1 - X + 3", simplified: "1 - X + 3"}, + {original: "X - 1 + 3", simplified: "X - 1 + 3"}, + {original: "1 - (X - 3)", simplified: "1 - (X - 3)"}, + {original: "1 - (3 - X)", simplified: "1 - (3 - X)"}, + {original: "1 - X - 3", simplified: "1 - X - 3"}, + {original: "X - 1 - 3", simplified: "X - 1 - 3"} +]; + +const multiplicativeExpressions = [ + {original: "X * 3 * 2", simplified: "X * 6"}, + {original: "3 * X * 2", simplified: "X * 6"}, + {original: "2 * (X * 3)", simplified: "X * 6"}, + {original: "2 * (3 * X)", simplified: "6 * X"}, + + {original: "X * -3 * -2", simplified: "X * 6"}, + {original: "-3 * X * -2", simplified: "X * 6"}, + {original: "-2 * (X * -3)", simplified: "X * 6"}, + {original: "-2 * (-3 * X)", simplified: "6 * X"}, + + // The following 4 cases should NOT simplify to X * 0. + // the reason is that X could be MAX, so that X*3 causes + // an overflow, but X*0 does not. + {original: "X * 3 * 0", simplified: "X * 3 * 0"}, + {original: "3 * X * 0", simplified: "3 * X * 0"}, + {original: "0 * (X * 3)", simplified: "0 * (X * 3)"}, + {original: "0 * (3 * X)", simplified: "0 * (3 * X)"}, + + {original: "X * 0 * 3", simplified: "X * 0"}, + {original: "0 * X * 3", simplified: "X * 0"}, + {original: "3 * (X * 0)", simplified: "X * 0"}, + {original: "3 * (0 * X)", simplified: "0 * X"}, + + // This expression cannot be further simplified to X, + // because X could be MIN, so that X * -1 causes an overflow + {original: "X * -1 * 1 * -1", simplified: "X * -1 * -1"}, + + // This expression could be further simplified to X * -1 + // but, currently, there are no rules that reduce three multiplied -1 + // to a single -1. This should be fixed in the future. + {original: "X * -1 * 1 * -1 * -1", simplified: "X * -1 * -1 * -1"}, + + // Even though, X * -1 * 1 * -1 cannot be simplified to X, + // when we multiply with a number with absolute value bigger than 1, + // we ensure that the overflows are preserved, so that we can simplify + // the expression. + {original: "X * -1 * 1 * -1 * 2", simplified: "X * 2"}, + + // Should NOT simplify to X * 2, because X could be MIN/2 = -2^255, + // so that X * -2 = 2^256 = MAX + 1 causes an overflow, + // but X * 2 = -2^256 does not. + {original: "X * -2 * -1", simplified: "X * -2 * -1"}, + + // Note however that multiplying first by -1 allow us + // to simplify the expression, because if X * -1 overflows/underflows, + // X * 2 will also. + {original: "X * -1 * -2", simplified: "X * 2"} +]; + +function testExpression(original: string, simplified: string) { + expect( + areEqualExpressions( + partiallyEvalExpression( + parseExpression(original), + new CompilerContext() + ), + unaryNegNodesToNumbers(parseExpression(simplified)) + ) + ).toBe(true); +} + +// Evaluates UnaryOp nodes with operator - into a single a node having a value. +// The reason for doing this is that the partial evaluator will transform negative +// numbers in an expression, e.g., "-1" into a tree with a single node with value -1, so that +// when comparing the tree with those produced by the parser, the two trees +// do not match, because the parser will produce a UnaryOp node with a child node with value 1. +// This is so because Tact does not have a way to write negative literals, but indirectly trough +// the use of the unary - operator. +function unaryNegNodesToNumbers(ast: ASTExpression): ASTExpression { + switch (ast.kind) { + case "null": + return ast; + case "boolean": + return ast; + case "number": + return ast; + case "string": + return ast; + case "id": + return ast; + case "op_call": + const newCallNode = cloneASTNode(ast); + newCallNode.args = ast.args.map(unaryNegNodesToNumbers); + newCallNode.src = unaryNegNodesToNumbers(ast.src); + return newCallNode; + case "init_of": + const newInitOfNode = cloneASTNode(ast); + newInitOfNode.args = ast.args.map(unaryNegNodesToNumbers); + return newInitOfNode; + case "op_unary": + if (ast.op === "-") { + if (isValue(ast.right)) { + return makeValueExpression( + evalUnaryOp(ast.op, extractValue(ast.right as ValueExpression)) + ); + } + } + const newUnaryNode = cloneASTNode(ast); + newUnaryNode.right = unaryNegNodesToNumbers(ast.right); + return newUnaryNode; + case "op_binary": + const newBinaryNode = cloneASTNode(ast); + newBinaryNode.left = unaryNegNodesToNumbers(ast.left); + newBinaryNode.right = unaryNegNodesToNumbers(ast.right); + return newBinaryNode; + case "conditional": + const newConditionalNode = cloneASTNode(ast); + newConditionalNode.thenBranch = unaryNegNodesToNumbers(ast.thenBranch); + newConditionalNode.elseBranch = unaryNegNodesToNumbers(ast.elseBranch); + return newConditionalNode; + case "op_new": + const newStructNode = cloneASTNode(ast); + newStructNode.args = ast.args.map(param => { + const newParam = cloneASTNode(param); + newParam.exp = unaryNegNodesToNumbers(param.exp); + return newParam; + } + ); + return newStructNode; + case "op_field": + const newFieldNode = cloneASTNode(ast); + newFieldNode.src = unaryNegNodesToNumbers(ast.src); + return newFieldNode; + case "op_static_call": + const newStaticCallNode = cloneASTNode(ast); + newStaticCallNode.args = ast.args.map(unaryNegNodesToNumbers); + return newStaticCallNode; + } +} + +describe("partial-evaluator", () => { + beforeEach(() => { + __DANGER_resetNodeId(); + }); + it("should correctly simplify partial expressions involving + and -", () => { + additiveExpressions.forEach( + pair => testExpression(pair.original, pair.simplified) + ) + }); + it("should correctly simplify partial expressions involving *", () => { + multiplicativeExpressions.forEach( + pair => testExpression(pair.original, pair.simplified) + ) + }); +}); From 99b687ff5fb0bb5c3ba2534c64fca90f13b25a21 Mon Sep 17 00:00:00 2001 From: jeshecdom Date: Thu, 4 Jul 2024 12:32:41 +0200 Subject: [PATCH 04/14] Fixes for the linter, spell checker, and code prettifier. --- src/constEval.ts | 166 +++++---- src/interpreter.ts | 21 +- src/optimizer/associative.ts | 389 +++++++++++++-------- src/optimizer/standardOptimizer.ts | 26 +- src/optimizer/types.ts | 54 +-- src/optimizer/util.ts | 134 ++++--- src/test/e2e-emulated/partial-eval.spec.ts | 194 +++++----- 7 files changed, 577 insertions(+), 407 deletions(-) diff --git a/src/constEval.ts b/src/constEval.ts index 757712e65..debb39611 100644 --- a/src/constEval.ts +++ b/src/constEval.ts @@ -15,16 +15,20 @@ import { import { TactConstEvalError, idTextErr, throwConstEvalError } from "./errors"; import { CommentValue, StructValue, Value } from "./types/types"; import { sha256_sync } from "@ton/crypto"; -import { - isValue, - extractValue, - makeValueExpression, +import { + isValue, + extractValue, + makeValueExpression, makeUnaryExpression, makeBinaryExpression, divFloor, - modFloor + modFloor, } from "./optimizer/util"; -import { DUMMY_LOCATION, ExpressionTransformer, ValueExpression } from "./optimizer/types"; +import { + DUMMY_LOCATION, + ExpressionTransformer, + ValueExpression, +} from "./optimizer/types"; import { StandardOptimizer } from "./optimizer/standardOptimizer"; import { getStaticConstant, @@ -37,7 +41,7 @@ import { getExpType } from "./types/resolveExpression"; const minTvmInt: bigint = -(2n ** 256n); const maxTvmInt: bigint = 2n ** 256n - 1n; -// The optimizer that applies the rewriting rules during partial evaluation. +// The optimizer that applies the rewriting rules during partial evaluation. // For the moment we use an optimizer that respects overflows. const optimizer: ExpressionTransformer = new StandardOptimizer(); @@ -112,10 +116,7 @@ function ensureMethodArity( } } -export function evalUnaryOp( - op: AstUnaryOperation, - valOperand: Value -): Value { +export function evalUnaryOp(op: AstUnaryOperation, valOperand: Value): Value { return __evalUnaryOp(op, valOperand, DUMMY_LOCATION, DUMMY_LOCATION); } @@ -123,7 +124,7 @@ function __evalUnaryOp( op: AstUnaryOperation, valOperand: Value, operandRef: SrcInfo, - source: SrcInfo + source: SrcInfo, ): Value { switch (op) { case "+": @@ -145,7 +146,6 @@ function __evalUnaryOp( } } - function fullyEvalUnaryOp( op: AstUnaryOperation, operand: AstExpression, @@ -155,7 +155,7 @@ function fullyEvalUnaryOp( // 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 (operand.kind === "number" && op === "-") { // emulating negative integer literals return ensureInt(-operand.value, source); @@ -168,19 +168,24 @@ function fullyEvalUnaryOp( function partiallyEvalUnaryOp( op: AstUnaryOperation, - oper: AstExpression, + operand: AstExpression, source: SrcInfo, ctx: CompilerContext, ): AstExpression { - const operand = partiallyEvalExpression(oper, ctx); - - if (isValue(operand)) { - const valueOperand = extractValue(operand as ValueExpression); - const result = __evalUnaryOp(op, valueOperand, operand.loc, source); + const simplOperand = partiallyEvalExpression(operand, ctx); + + if (isValue(simplOperand)) { + const valueOperand = extractValue(simplOperand as ValueExpression); + const result = __evalUnaryOp( + op, + valueOperand, + simplOperand.loc, + source, + ); // Wrap the value into a Tree to continue simplifications return makeValueExpression(result); } else { - const newAst = makeUnaryExpression(op, operand); + const newAst = makeUnaryExpression(op, simplOperand); return optimizer.applyRules(newAst); } } @@ -203,15 +208,22 @@ function partiallyEvalBinaryOp( left: AstExpression, right: AstExpression, source: SrcInfo, - ctx: CompilerContext + ctx: CompilerContext, ): AstExpression { const leftOperand = partiallyEvalExpression(left, ctx); const rightOperand = partiallyEvalExpression(right, ctx); - + if (isValue(leftOperand) && isValue(rightOperand)) { const valueLeftOperand = extractValue(leftOperand as ValueExpression); const valueRightOperand = extractValue(rightOperand as ValueExpression); - const result = __evalBinaryOp(op, valueLeftOperand, valueRightOperand, leftOperand.loc, rightOperand.loc, source); + const result = __evalBinaryOp( + op, + valueLeftOperand, + valueRightOperand, + leftOperand.loc, + rightOperand.loc, + source, + ); // Wrap the value into a Tree to continue simplifications return makeValueExpression(result); } else { @@ -223,9 +235,16 @@ function partiallyEvalBinaryOp( export function evalBinaryOp( op: AstBinaryOperation, valLeft: Value, - valRight: Value + valRight: Value, ): Value { - return __evalBinaryOp(op, valLeft, valRight, DUMMY_LOCATION, DUMMY_LOCATION, DUMMY_LOCATION); + return __evalBinaryOp( + op, + valLeft, + valRight, + DUMMY_LOCATION, + DUMMY_LOCATION, + DUMMY_LOCATION, + ); } function __evalBinaryOp( @@ -234,7 +253,7 @@ function __evalBinaryOp( valRight: Value, refLeft: SrcInfo, refRight: SrcInfo, - source: SrcInfo + source: SrcInfo, ): Value { switch (op) { case "+": @@ -277,17 +296,11 @@ function __evalBinaryOp( return ensureInt(modFloor(ensureInt(valLeft, refLeft), r), source); } case "&": - return ( - ensureInt(valLeft, refLeft) & ensureInt(valRight, refRight) - ); + return ensureInt(valLeft, refLeft) & ensureInt(valRight, refRight); case "|": - return ( - ensureInt(valLeft, refLeft) | ensureInt(valRight, refRight) - ); + return ensureInt(valLeft, refLeft) | ensureInt(valRight, refRight); case "^": - return ( - ensureInt(valLeft, refLeft) ^ ensureInt(valRight, refRight) - ); + return ensureInt(valLeft, refLeft) ^ ensureInt(valRight, refRight); case "<<": { const valNum = ensureInt(valLeft, refLeft); const valBits = ensureInt(valRight, refRight); @@ -331,21 +344,13 @@ function __evalBinaryOp( } } case ">": - return ( - ensureInt(valLeft, refLeft) > ensureInt(valRight, refRight) - ); + return ensureInt(valLeft, refLeft) > ensureInt(valRight, refRight); case "<": - return ( - ensureInt(valLeft, refLeft) < ensureInt(valRight, refRight) - ); + return ensureInt(valLeft, refLeft) < ensureInt(valRight, refRight); case ">=": - return ( - ensureInt(valLeft, refLeft) >= ensureInt(valRight, refRight) - ); + return ensureInt(valLeft, refLeft) >= ensureInt(valRight, refRight); case "<=": - return ( - ensureInt(valLeft, refLeft) <= ensureInt(valRight, refRight) - ); + return ensureInt(valLeft, refLeft) <= ensureInt(valRight, refRight); case "==": // the null comparisons account for optional types, e.g. // a const x: Int? = 42 can be compared to null @@ -773,13 +778,15 @@ export function evalConstantExpression( } } - -export function partiallyEvalExpression(ast: AstExpression, ctx: CompilerContext): AstExpression { +export function partiallyEvalExpression( + ast: AstExpression, + ctx: CompilerContext, +): AstExpression { switch (ast.kind) { case "id": try { return makeValueExpression(lookupID(ast, ctx)); - } catch(e) { + } catch (e) { if (e instanceof TactConstEvalError) { if (!e.fatal) { // If a non-fatal error occurs during lookup, just return the symbol @@ -789,14 +796,16 @@ export function partiallyEvalExpression(ast: AstExpression, ctx: CompilerContext throw e; } case "method_call": - // Does not partially evaluate at the monent. Will attemp to fully evaluate - const resCall = evalMethod(ast.method, ast.self, ast.args, ast.loc, ctx); - return makeValueExpression(resCall); + // Does not partially evaluate at the moment. Will attempt to fully evaluate + return makeValueExpression( + evalMethod(ast.method, ast.self, ast.args, ast.loc, ctx), + ); case "init_of": throwNonFatalErrorConstEval( "initOf is not supported at this moment", ast.loc, ); + break; case "null": return ast; case "boolean": @@ -804,32 +813,43 @@ export function partiallyEvalExpression(ast: AstExpression, ctx: CompilerContext case "number": return makeValueExpression(ensureInt(ast.value, ast.loc)); case "string": - return makeValueExpression(ensureString(interpretEscapeSequences(ast.value), ast.loc)); + return makeValueExpression( + ensureString(interpretEscapeSequences(ast.value), ast.loc), + ); case "op_unary": return partiallyEvalUnaryOp(ast.op, ast.operand, ast.loc, ctx); case "op_binary": - return partiallyEvalBinaryOp(ast.op, ast.left, ast.right, ast.loc, ctx); - case "conditional": - // Does not partially evaluate at the monent. Will attemp to fully evaluate - const resCond = evalConditional( - ast.condition, - ast.thenBranch, - ast.elseBranch, + return partiallyEvalBinaryOp( + ast.op, + ast.left, + ast.right, + ast.loc, ctx, ); - return makeValueExpression(resCond); + case "conditional": + // Does not partially evaluate at the moment. Will attempt to fully evaluate + return makeValueExpression( + evalConditional( + ast.condition, + ast.thenBranch, + ast.elseBranch, + ctx, + ), + ); case "struct_instance": - // Does not partially evaluate at the monent. Will attemp to fully evaluate - const resStruct = evalStructInstance(ast.type, ast.args, ctx); - return makeValueExpression(resStruct); + // Does not partially evaluate at the moment. Will attempt to fully evaluate + return makeValueExpression( + evalStructInstance(ast.type, ast.args, ctx), + ); case "field_access": - // Does not partially evaluate at the monent. Will attemp to fully evaluate - const resField = evalFieldAccess(ast.aggregate, ast.field, ast.loc, ctx); - return makeValueExpression(resField); + // Does not partially evaluate at the moment. Will attempt to fully evaluate + return makeValueExpression( + evalFieldAccess(ast.aggregate, ast.field, ast.loc, ctx), + ); case "static_call": - // Does not partially evaluate at the monent. Will attemp to fully evaluate - const resStaticCall = evalBuiltins(ast.function, ast.args, ast.loc, ctx); - return makeValueExpression(resStaticCall); + // Does not partially evaluate at the moment. Will attempt to fully evaluate + return makeValueExpression( + evalBuiltins(ast.function, ast.args, ast.loc, ctx), + ); } } - diff --git a/src/interpreter.ts b/src/interpreter.ts index a34909dca..c77febfd8 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -1,12 +1,11 @@ import { evalConstantExpression, partiallyEvalExpression } from "./constEval"; import { CompilerContext } from "./context"; import { TactConstEvalError, TactParseError } from "./errors"; -import { AstExpression } from "./grammar/ast"; import { parseExpression } from "./grammar/grammar"; import { Value } from "./types/types"; export type EvalResult = - | { kind: "ok"; value: Value | AstExpression } + | { kind: "ok"; value: Value } | { kind: "error"; message: string }; export function parseAndEvalExpression(sourceCode: string): EvalResult { @@ -26,21 +25,3 @@ export function parseAndEvalExpression(sourceCode: string): EvalResult { throw error; } } - -export function parseAndPartiallyEvalExpression(sourceCode: string): EvalResult { - try { - const ast = parseExpression(sourceCode); - const evalResult = partiallyEvalExpression( - ast, - new CompilerContext(), - ); - return { kind: "ok", value: evalResult }; - } catch (error) { - if ( - error instanceof TactParseError || - error instanceof TactConstEvalError - ) - return { kind: "error", message: error.message }; - throw error; - } -} \ No newline at end of file diff --git a/src/optimizer/associative.ts b/src/optimizer/associative.ts index 88cc13d47..afe2ff5ac 100644 --- a/src/optimizer/associative.ts +++ b/src/optimizer/associative.ts @@ -1,27 +1,26 @@ -// This module includes rules involving associative rewritings of expressions +// This module includes rules involving associative rewrites of expressions import { evalBinaryOp } from "../constEval"; import { AstBinaryOperation, AstExpression, AstOpBinary } from "../grammar/ast"; import { Value } from "../types/types"; import { ExpressionTransformer, Rule, ValueExpression } from "./types"; -import { +import { abs, - checkIsBinaryOpNode, - checkIsBinaryOp_NonValue_Value, - checkIsBinaryOp_Value_NonValue, - extractValue, - isValue, - makeBinaryExpression, - makeValueExpression, - sign + checkIsBinaryOpNode, + checkIsBinaryOp_NonValue_Value, + checkIsBinaryOp_Value_NonValue, + extractValue, + isValue, + makeBinaryExpression, + makeValueExpression, + sign, } from "./util"; export abstract class AssociativeRewriteRule implements Rule { - - // An entry (op, S) in the map means "operator op associates with all operators in set S", + // An entry (op, S) in the map means "operator op associates with all operators in set S", // mathematically: all op2 \in S. (a op b) op2 c = a op (b op2 c) - private associativeOps: Map>; - + private associativeOps: Map>; + // This set contains all operators that commute. // Mathematically: all op \in commutativeOps. a op b = b op a private commutativeOps: Set; @@ -29,16 +28,12 @@ export abstract class AssociativeRewriteRule implements Rule { constructor() { // + associates with these on the right: // i.e., all op \in plusAssoc. (a + b) op c = a + (b op c) - const plusAssoc = new Set([ - "+", "-" - ]); + const additiveAssoc = new Set(["+", "-"]); // - does not associate with any operator on the right // * associates with these on the right: - const multAssoc = new Set([ - "*", "<<" - ]); + const multiplicativeAssoc = new Set(["*", "<<"]); // Division / does not associate with any on the right @@ -46,24 +41,31 @@ export abstract class AssociativeRewriteRule implements Rule { // TODO: shifts, bitwise integer operators, boolean operators - this.associativeOps = new Map>([ - ["+", plusAssoc], - ["*", multAssoc] + this.associativeOps = new Map< + AstBinaryOperation, + Set + >([ + ["+", additiveAssoc], + ["*", multiplicativeAssoc], ]); this.commutativeOps = new Set( - ["+", "*", "!=", "==", "&&", "||"] // TODO: bitwise integer operators + ["+", "*", "!=", "==", "&&", "||"], // TODO: bitwise integer operators ); } + public abstract applyRule( + ast: AstExpression, + optimizer: ExpressionTransformer, + ): AstExpression; - public abstract applyRule(ast: AstExpression, optimizer: ExpressionTransformer): AstExpression; - - - public areAssociative(op1: AstBinaryOperation, op2: AstBinaryOperation): boolean { + public areAssociative( + op1: AstBinaryOperation, + op2: AstBinaryOperation, + ): boolean { if (this.associativeOps.has(op1)) { - var rightAssocs = this.associativeOps.get(op1)!; - return rightAssocs.has(op2); + const rightOperators = this.associativeOps.get(op1)!; + return rightOperators.has(op2); } else { return false; } @@ -75,7 +77,6 @@ export abstract class AssociativeRewriteRule implements Rule { } export abstract class AllowableOpRule extends AssociativeRewriteRule { - private allowedOps: Set; constructor() { @@ -84,7 +85,7 @@ export abstract class AllowableOpRule extends AssociativeRewriteRule { this.allowedOps = new Set( // Recall that integer operators +,-,*,/,% are not safe with this rule, because // there is a risk that they will not preserve overflows in the unknown operands. - ["&&", "||"] // TODO: check bitwise integer operators + ["&&", "||"], // TODO: check bitwise integer operators ); } @@ -93,16 +94,24 @@ export abstract class AllowableOpRule extends AssociativeRewriteRule { } public areAllowedOps(op: AstBinaryOperation[]): boolean { - return op.reduce((prev,curr) => prev && this.allowedOps.has(curr), true); + return op.reduce( + (prev, curr) => prev && this.allowedOps.has(curr), + true, + ); } } export class AssociativeRule1 extends AllowableOpRule { - - public applyRule(ast: AstExpression, optimizer: ExpressionTransformer): AstExpression { + public applyRule( + ast: AstExpression, + optimizer: ExpressionTransformer, + ): AstExpression { if (checkIsBinaryOpNode(ast)) { const topLevelNode = ast as AstOpBinary; - if (checkIsBinaryOp_NonValue_Value(topLevelNode.left) && checkIsBinaryOp_NonValue_Value(topLevelNode.right)) { + if ( + checkIsBinaryOp_NonValue_Value(topLevelNode.left) && + checkIsBinaryOp_NonValue_Value(topLevelNode.right) + ) { // The tree has this form: // (x1 op1 c1) op (x2 op2 c2) const leftTree = topLevelNode.left as AstOpBinary; @@ -111,17 +120,17 @@ export class AssociativeRule1 extends AllowableOpRule { const x1 = leftTree.left; const c1 = leftTree.right as ValueExpression; const op1 = leftTree.op; - + const x2 = rightTree.left; const c2 = rightTree.right as ValueExpression; const op2 = rightTree.op; const op = topLevelNode.op; - + // Check that: // the operators are allowed // op1 and op associate - // op and op2 asociate + // op and op2 associate // op commutes if ( this.areAllowedOps([op1, op, op2]) && @@ -132,7 +141,11 @@ export class AssociativeRule1 extends AllowableOpRule { // Agglutinate the constants and compute their final value try { // If an error occurs, we abandon the simplification - const val = evalBinaryOp(op2, extractValue(c1), extractValue(c2)); + const val = evalBinaryOp( + op2, + extractValue(c1), + extractValue(c2), + ); // The final expression is // (x1 op1 x2) op val @@ -140,14 +153,19 @@ export class AssociativeRule1 extends AllowableOpRule { // Because we are joining x1 and x2, // 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)); + const newLeft = optimizer.applyRules( + makeBinaryExpression(op1, x1, x2), + ); const newRight = makeValueExpression(val); return makeBinaryExpression(op, newLeft, newRight); } catch (e) { + // Do nothing: will exit rule without modifying tree } } - - } else if (checkIsBinaryOp_NonValue_Value(topLevelNode.left) && checkIsBinaryOp_Value_NonValue(topLevelNode.right)) { + } else if ( + checkIsBinaryOp_NonValue_Value(topLevelNode.left) && + checkIsBinaryOp_Value_NonValue(topLevelNode.right) + ) { // The tree has this form: // (x1 op1 c1) op (c2 op2 x2) const leftTree = topLevelNode.left as AstOpBinary; @@ -156,7 +174,7 @@ export class AssociativeRule1 extends AllowableOpRule { const x1 = leftTree.left; const c1 = leftTree.right as ValueExpression; const op1 = leftTree.op; - + const x2 = rightTree.right; const c2 = rightTree.left as ValueExpression; const op2 = rightTree.op; @@ -166,7 +184,7 @@ export class AssociativeRule1 extends AllowableOpRule { // Check that: // the operators are allowed // op1 and op associate - // op and op2 asociate + // op and op2 associate if ( this.areAllowedOps([op1, op, op2]) && this.areAssociative(op1, op) && @@ -175,24 +193,33 @@ export class AssociativeRule1 extends AllowableOpRule { // Agglutinate the constants and compute their final value try { // If an error occurs, we abandon the simplification - const val = evalBinaryOp(op, extractValue(c1), extractValue(c2)); + const val = evalBinaryOp( + op, + extractValue(c1), + extractValue(c2), + ); // The current expression could be either // x1 op1 (val op2 x2) or - // (x1 op1 val) op2 x2 <--- we choose this form. + // (x1 op1 val) op2 x2 <--- we choose this form. // Other rules will attempt to extract the constant outside the expression. // 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 newLeft = optimizer.applyRules(makeBinaryExpression(op1, x1, newValNode)); + const newLeft = optimizer.applyRules( + makeBinaryExpression(op1, x1, newValNode), + ); return makeBinaryExpression(op2, newLeft, x2); } catch (e) { + // Do nothing: will exit rule without modifying tree } } - - } else if (checkIsBinaryOp_Value_NonValue(topLevelNode.left) && checkIsBinaryOp_NonValue_Value(topLevelNode.right)) { + } else if ( + checkIsBinaryOp_Value_NonValue(topLevelNode.left) && + checkIsBinaryOp_NonValue_Value(topLevelNode.right) + ) { // The tree has this form: // (c1 op1 x1) op (x2 op2 c2) const leftTree = topLevelNode.left as AstOpBinary; @@ -201,7 +228,7 @@ export class AssociativeRule1 extends AllowableOpRule { const x1 = leftTree.right; const c1 = leftTree.left as ValueExpression; const op1 = leftTree.op; - + const x2 = rightTree.left; const c2 = rightTree.right as ValueExpression; const op2 = rightTree.op; @@ -211,7 +238,7 @@ export class AssociativeRule1 extends AllowableOpRule { // Check that: // the operators are allowed // op and op1 associate - // op2 and op asociate + // op2 and op associate // op commutes if ( this.areAllowedOps([op1, op, op2]) && @@ -222,23 +249,33 @@ export class AssociativeRule1 extends AllowableOpRule { // Agglutinate the constants and compute their final value try { // If an error occurs, we abandon the simplification - const val = evalBinaryOp(op, extractValue(c2), extractValue(c1)); + const val = evalBinaryOp( + op, + extractValue(c2), + extractValue(c1), + ); // The current expression could be either // x2 op2 (val op1 x1) or - // (x2 op2 val) op1 x1 <--- we choose this form. + // (x2 op2 val) op1 x1 <--- we choose this form. // Other rules will attempt to extract the constant outside the expression. // 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 newLeft = optimizer.applyRules(makeBinaryExpression(op2, x2, newValNode)); + const newLeft = optimizer.applyRules( + makeBinaryExpression(op2, x2, newValNode), + ); return makeBinaryExpression(op1, newLeft, x1); } catch (e) { + // Do nothing: will exit rule without modifying tree } } - } else if (checkIsBinaryOp_Value_NonValue(topLevelNode.left) && checkIsBinaryOp_Value_NonValue(topLevelNode.right)) { + } else if ( + checkIsBinaryOp_Value_NonValue(topLevelNode.left) && + checkIsBinaryOp_Value_NonValue(topLevelNode.right) + ) { // The tree has this form: // (c1 op1 x1) op (c2 op2 x2) const leftTree = topLevelNode.left as AstOpBinary; @@ -247,7 +284,7 @@ export class AssociativeRule1 extends AllowableOpRule { const x1 = leftTree.right; const c1 = leftTree.left as ValueExpression; const op1 = leftTree.op; - + const x2 = rightTree.right; const c2 = rightTree.left as ValueExpression; const op2 = rightTree.op; @@ -257,7 +294,7 @@ export class AssociativeRule1 extends AllowableOpRule { // Check that: // the operators are allowed // op1 and op associate - // op and op2 asociate + // op and op2 associate // op commutes if ( this.areAllowedOps([op1, op, op2]) && @@ -268,7 +305,11 @@ export class AssociativeRule1 extends AllowableOpRule { // Agglutinate the constants and compute their final value try { // If an error occurs, we abandon the simplification - const val = evalBinaryOp(op1, extractValue(c1), extractValue(c2)); + const val = evalBinaryOp( + op1, + extractValue(c1), + extractValue(c2), + ); // The final expression is // val op (x1 op2 x2) @@ -276,10 +317,13 @@ export class AssociativeRule1 extends AllowableOpRule { // Because we are joining x1 and x2, // 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)); + const newRight = optimizer.applyRules( + makeBinaryExpression(op2, x1, x2), + ); const newLeft = makeValueExpression(val); return makeBinaryExpression(op, newLeft, newRight); } catch (e) { + // Do nothing: will exit rule without modifying tree } } } @@ -289,15 +333,19 @@ export class AssociativeRule1 extends AllowableOpRule { // so, we return the original tree return ast; } - } export class AssociativeRule2 extends AllowableOpRule { - - public applyRule(ast: AstExpression, optimizer: ExpressionTransformer): AstExpression { + public applyRule( + ast: AstExpression, + optimizer: ExpressionTransformer, + ): AstExpression { if (checkIsBinaryOpNode(ast)) { const topLevelNode = ast as AstOpBinary; - if (checkIsBinaryOp_NonValue_Value(topLevelNode.left) && !isValue(topLevelNode.right)) { + if ( + checkIsBinaryOp_NonValue_Value(topLevelNode.left) && + !isValue(topLevelNode.right) + ) { // The tree has this form: // (x1 op1 c1) op x2 const leftTree = topLevelNode.left as AstOpBinary; @@ -306,11 +354,11 @@ export class AssociativeRule2 extends AllowableOpRule { const x1 = leftTree.left; const c1 = leftTree.right as ValueExpression; const op1 = leftTree.op; - + const x2 = rightTree; const op = topLevelNode.op; - + // Check that: // the operators are allowed // op1 and op associate @@ -326,11 +374,15 @@ export class AssociativeRule2 extends AllowableOpRule { // Because we are joining x1 and x2, // 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)); + const newLeft = optimizer.applyRules( + makeBinaryExpression(op1, x1, x2), + ); return makeBinaryExpression(op, newLeft, c1); } - - } else if (checkIsBinaryOp_Value_NonValue(topLevelNode.left) && !isValue(topLevelNode.right)) { + } else if ( + checkIsBinaryOp_Value_NonValue(topLevelNode.left) && + !isValue(topLevelNode.right) + ) { // The tree has this form: // (c1 op1 x1) op x2 const leftTree = topLevelNode.left as AstOpBinary; @@ -339,11 +391,11 @@ export class AssociativeRule2 extends AllowableOpRule { const x1 = leftTree.right; const c1 = leftTree.left as ValueExpression; const op1 = leftTree.op; - + const x2 = rightTree; const op = topLevelNode.op; - + // Check that: // the operators are allowed // op1 and op associate @@ -357,10 +409,15 @@ export class AssociativeRule2 extends AllowableOpRule { // Because we are joining x1 and x2, // 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)); + const newRight = optimizer.applyRules( + makeBinaryExpression(op, x1, x2), + ); return makeBinaryExpression(op1, c1, newRight); } - } else if (!isValue(topLevelNode.left) && checkIsBinaryOp_NonValue_Value(topLevelNode.right)) { + } else if ( + !isValue(topLevelNode.left) && + checkIsBinaryOp_NonValue_Value(topLevelNode.right) + ) { // The tree has this form: // x2 op (x1 op1 c1) const leftTree = topLevelNode.left; @@ -369,11 +426,11 @@ export class AssociativeRule2 extends AllowableOpRule { const x1 = rightTree.left; const c1 = rightTree.right as ValueExpression; const op1 = rightTree.op; - + const x2 = leftTree; const op = topLevelNode.op; - + // Check that: // the operators are allowed // op and op1 associate @@ -387,10 +444,15 @@ export class AssociativeRule2 extends AllowableOpRule { // Because we are joining x1 and x2, // 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)); + const newLeft = optimizer.applyRules( + makeBinaryExpression(op, x2, x1), + ); return makeBinaryExpression(op1, newLeft, c1); } - } else if (!isValue(topLevelNode.left) && checkIsBinaryOp_Value_NonValue(topLevelNode.right)) { + } else if ( + !isValue(topLevelNode.left) && + checkIsBinaryOp_Value_NonValue(topLevelNode.right) + ) { // The tree has this form: // x2 op (c1 op1 x1) const leftTree = topLevelNode.left; @@ -399,11 +461,11 @@ export class AssociativeRule2 extends AllowableOpRule { const x1 = rightTree.right; const c1 = rightTree.left as ValueExpression; const op1 = rightTree.op; - + const x2 = leftTree; const op = topLevelNode.op; - + // Check that: // the operators are allowed // op and op1 associate @@ -419,7 +481,9 @@ export class AssociativeRule2 extends AllowableOpRule { // Because we are joining x1 and x2, // 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)); + const newRight = optimizer.applyRules( + makeBinaryExpression(op1, x2, x1), + ); return makeBinaryExpression(op, c1, newRight); } } @@ -428,7 +492,7 @@ export class AssociativeRule2 extends AllowableOpRule { // If execution reaches here, it means that the rule could not be applied fully // so, we return the original tree return ast; - } + } } function ensureInt(val: Value): bigint { @@ -439,48 +503,63 @@ function ensureInt(val: Value): bigint { } export class AssociativeRule3 extends AssociativeRewriteRule { - - private extraOpCondition: Map boolean>; + private extraOpCondition: Map< + AstBinaryOperation, + (c1: Value, c2: Value, val: Value) => boolean + >; public constructor() { super(); - this.extraOpCondition = new Map boolean>([ - ["+", (c1, c2, val) => { - const n1 = ensureInt(c1); - const res = ensureInt(val); - return sign(n1) === sign(res) && abs(n1) <= abs(res); - } + this.extraOpCondition = new Map< + AstBinaryOperation, + (c1: Value, c2: Value, val: Value) => boolean + >([ + [ + "+", + (c1, c2, val) => { + const n1 = ensureInt(c1); + const res = ensureInt(val); + return sign(n1) === sign(res) && abs(n1) <= abs(res); + }, ], - ["-", (c1, c2, val) => { - const n1 = ensureInt(c1); - const res = ensureInt(val); - return sign(n1) === sign(res) && abs(n1) <= abs(res); - } + [ + "-", + (c1, c2, val) => { + const n1 = ensureInt(c1); + const res = ensureInt(val); + return sign(n1) === sign(res) && abs(n1) <= abs(res); + }, ], - ["*", (c1, c2, val) => { - const n1 = ensureInt(c1); - const res = ensureInt(val); - if (n1 < 0n) { - if (sign(n1) === sign(res)) { - return abs(n1) <= abs(res); + [ + "*", + (c1, c2, val) => { + const n1 = ensureInt(c1); + const res = ensureInt(val); + if (n1 < 0n) { + if (sign(n1) === sign(res)) { + return abs(n1) <= abs(res); + } else { + return abs(n1) < abs(res); + } + } else if (n1 === 0n) { + return true; } else { - return abs(n1) < abs(res); + return abs(n1) <= abs(res); } - } else if (n1 === 0n) { - return true; - } else { - return abs(n1) <= abs(res); - } - } + }, ], ]); - } - protected opSatisfiesConditions(op: AstBinaryOperation, c1: Value, c2: Value, res: Value): boolean { + protected opSatisfiesConditions( + op: AstBinaryOperation, + c1: Value, + c2: Value, + res: Value, + ): boolean { if (this.extraOpCondition.has(op)) { return this.extraOpCondition.get(op)!(c1, c2, res); } else { @@ -488,10 +567,16 @@ export class AssociativeRule3 extends AssociativeRewriteRule { } } - public applyRule(ast: AstExpression, optimizer: ExpressionTransformer): AstExpression { + public applyRule( + ast: AstExpression, + optimizer: ExpressionTransformer, + ): AstExpression { if (checkIsBinaryOpNode(ast)) { const topLevelNode = ast as AstOpBinary; - if (checkIsBinaryOp_NonValue_Value(topLevelNode.left) && isValue(topLevelNode.right)) { + if ( + checkIsBinaryOp_NonValue_Value(topLevelNode.left) && + isValue(topLevelNode.right) + ) { // The tree has this form: // (x1 op1 c1) op c2 const leftTree = topLevelNode.left as AstOpBinary; @@ -500,16 +585,16 @@ export class AssociativeRule3 extends AssociativeRewriteRule { const x1 = leftTree.left; const c1 = extractValue(leftTree.right as ValueExpression); const op1 = leftTree.op; - + const c2 = extractValue(rightTree); const op = topLevelNode.op; - + // Agglutinate the constants and compute their final value try { // If an error occurs, we abandon the simplification const val = evalBinaryOp(op, c1, c2); - + // Check that: // op1 and op associate // the extra conditions on op1 @@ -518,20 +603,24 @@ export class AssociativeRule3 extends AssociativeRewriteRule { this.areAssociative(op1, op) && this.opSatisfiesConditions(op1, c1, c2, val) ) { - // The final expression is // x1 op1 val const newConstant = makeValueExpression(val); // Since the tree is simpler now, there is further - // popportunity for simplification that was missed + // opportunity for simplification that was missed // previously - return optimizer.applyRules(makeBinaryExpression(op1, x1, newConstant)); + return optimizer.applyRules( + makeBinaryExpression(op1, x1, newConstant), + ); } - } catch(e) { + } catch (e) { + // Do nothing: will exit rule without modifying tree } - - } else if (checkIsBinaryOp_Value_NonValue(topLevelNode.left) && isValue(topLevelNode.right)) { + } else if ( + checkIsBinaryOp_Value_NonValue(topLevelNode.left) && + isValue(topLevelNode.right) + ) { // The tree has this form: // (c1 op1 x1) op c2 const leftTree = topLevelNode.left as AstOpBinary; @@ -540,16 +629,16 @@ export class AssociativeRule3 extends AssociativeRewriteRule { const x1 = leftTree.right; const c1 = extractValue(leftTree.left as ValueExpression); const op1 = leftTree.op; - + const c2 = extractValue(rightTree); const op = topLevelNode.op; - + // Agglutinate the constants and compute their final value try { // If an error occurs, we abandon the simplification const val = evalBinaryOp(op, c1, c2); - + // Check that: // op1 and op associate // op1 commutes @@ -560,19 +649,24 @@ export class AssociativeRule3 extends AssociativeRewriteRule { this.isCommutative(op1) && this.opSatisfiesConditions(op1, c1, c2, val) ) { - // The final expression is // x1 op1 val const newConstant = makeValueExpression(val); // Since the tree is simpler now, there is further - // popportunity for simplification that was missed + // opportunity for simplification that was missed // previously - return optimizer.applyRules(makeBinaryExpression(op1, x1, newConstant)); + return optimizer.applyRules( + makeBinaryExpression(op1, x1, newConstant), + ); } - } catch(e) { + } catch (e) { + // Do nothing: will exit rule without modifying tree } - } else if (isValue(topLevelNode.left) && checkIsBinaryOp_NonValue_Value(topLevelNode.right)) { + } else if ( + isValue(topLevelNode.left) && + checkIsBinaryOp_NonValue_Value(topLevelNode.right) + ) { // The tree has this form: // c2 op (x1 op1 c1) const leftTree = topLevelNode.left as ValueExpression; @@ -581,16 +675,16 @@ export class AssociativeRule3 extends AssociativeRewriteRule { const x1 = rightTree.left; const c1 = extractValue(rightTree.right as ValueExpression); const op1 = rightTree.op; - + const c2 = extractValue(leftTree); const op = topLevelNode.op; - + // Agglutinate the constants and compute their final value try { // If an error occurs, we abandon the simplification const val = evalBinaryOp(op, c2, c1); - + // Check that: // op and op1 associate // op1 commutes @@ -601,19 +695,24 @@ export class AssociativeRule3 extends AssociativeRewriteRule { this.isCommutative(op1) && this.opSatisfiesConditions(op1, c1, c2, val) ) { - // The final expression is // x1 op1 val const newConstant = makeValueExpression(val); // Since the tree is simpler now, there is further - // popportunity for simplification that was missed + // opportunity for simplification that was missed // previously - return optimizer.applyRules(makeBinaryExpression(op1, x1, newConstant)); + return optimizer.applyRules( + makeBinaryExpression(op1, x1, newConstant), + ); } - } catch(e) { + } catch (e) { + // Do nothing: will exit rule without modifying tree } - } else if (isValue(topLevelNode.left) && checkIsBinaryOp_Value_NonValue(topLevelNode.right)) { + } else if ( + isValue(topLevelNode.left) && + checkIsBinaryOp_Value_NonValue(topLevelNode.right) + ) { // The tree has this form: // c2 op (c1 op1 x1) const leftTree = topLevelNode.left as ValueExpression; @@ -622,16 +721,16 @@ export class AssociativeRule3 extends AssociativeRewriteRule { const x1 = rightTree.right; const c1 = extractValue(rightTree.left as ValueExpression); const op1 = rightTree.op; - + const c2 = extractValue(leftTree); const op = topLevelNode.op; - + // Agglutinate the constants and compute their final value try { // If an error occurs, we abandon the simplification const val = evalBinaryOp(op, c2, c1); - + // Check that: // op and op1 associate // the extra conditions on op1 @@ -640,17 +739,19 @@ export class AssociativeRule3 extends AssociativeRewriteRule { this.areAssociative(op, op1) && this.opSatisfiesConditions(op1, c1, c2, val) ) { - // The final expression is // val op1 x1 const newConstant = makeValueExpression(val); // Since the tree is simpler now, there is further - // popportunity for simplification that was missed + // opportunity for simplification that was missed // previously - return optimizer.applyRules(makeBinaryExpression(op1, newConstant, x1)); + return optimizer.applyRules( + makeBinaryExpression(op1, newConstant, x1), + ); } - } catch(e) { + } catch (e) { + // Do nothing: will exit rule without modifying tree } } } @@ -658,5 +759,5 @@ export class AssociativeRule3 extends AssociativeRewriteRule { // If execution reaches here, it means that the rule could not be applied fully // so, we return the original tree return ast; - } + } } diff --git a/src/optimizer/standardOptimizer.ts b/src/optimizer/standardOptimizer.ts index e4def8473..808d9083d 100644 --- a/src/optimizer/standardOptimizer.ts +++ b/src/optimizer/standardOptimizer.ts @@ -1,29 +1,35 @@ import { AstExpression } from "../grammar/ast"; -import { AssociativeRule1, AssociativeRule2, AssociativeRule3 } from "./associative"; +import { + AssociativeRule1, + AssociativeRule2, + AssociativeRule3, +} from "./associative"; import { Rule, ExpressionTransformer } from "./types"; -type PrioritizedRule = {priority: number, rule: Rule}; +type PrioritizedRule = { priority: number; rule: Rule }; // This optimizer uses rules that preserve overflows in integer expressions. export class StandardOptimizer implements ExpressionTransformer { - private rules: PrioritizedRule[]; constructor() { this.rules = [ - {priority: 0, rule: new AssociativeRule1()}, - {priority: 1, rule: new AssociativeRule2()}, - {priority: 2, rule: new AssociativeRule3()} + { priority: 0, rule: new AssociativeRule1() }, + { priority: 1, rule: new AssociativeRule2() }, + { priority: 2, rule: new AssociativeRule3() }, // TODO: add simpler algebraic rules that will be added to algebraic.ts ]; - // Sort according to the priorities: smaller number means greater priority. + // Sort according to the priorities: smaller number means greater priority. // So, the rules will be sorted increasingly according to their priority number. this.rules.sort((r1, r2) => r1.priority - r2.priority); } public applyRules(ast: AstExpression): AstExpression { - return this.rules.reduce((prev, prioritizedRule) => prioritizedRule.rule.applyRule(prev, this), ast); + return this.rules.reduce( + (prev, prioritizedRule) => + prioritizedRule.rule.applyRule(prev, this), + ast, + ); } - -} \ No newline at end of file +} diff --git a/src/optimizer/types.ts b/src/optimizer/types.ts index b28ed8553..84c338496 100644 --- a/src/optimizer/types.ts +++ b/src/optimizer/types.ts @@ -1,14 +1,14 @@ import { Interval } from "ohm-js"; -import { +import { AstExpression, AstNumber, AstBoolean, AstNull, AstString, - SrcInfo - } from "../grammar/ast"; + SrcInfo, +} from "../grammar/ast"; -export type ValueExpression = AstNumber | AstBoolean | AstNull | AstString; +export type ValueExpression = AstNumber | AstBoolean | AstNull | AstString; export const DUMMY_INTERVAL: Interval = { sourceString: "", @@ -16,13 +16,16 @@ export const DUMMY_INTERVAL: Interval = { endIdx: 10, contents: "mock contents", minus(that) { - return [this]; + // Returned the parameter so that the linter stops complaining + return [that]; }, relativeTo(that) { - return this; + // Returned the parameter so that the linter stops complaining + return that; }, subInterval(offset, len) { - return this; + // Did this so that the linter stops complaining + return offset == len ? this : this; }, collapsedLeft() { return this; @@ -34,29 +37,36 @@ export const DUMMY_INTERVAL: Interval = { return this; }, coverageWith(...intervals) { - return this; + // This this so that the linter stops complaining + return intervals.length == 0 ? this : this; }, getLineAndColumnMessage() { return `Line 1, Column 0`; }, getLineAndColumn() { - return { - offset: 0, - lineNum: 1, - colNum: 0, - line: "1", - nextLine: "1", - prevLine: "1" + return { + offset: 0, + lineNum: 1, + colNum: 0, + line: "1", + nextLine: "1", + prevLine: "1", }; - } - }; -export const DUMMY_LOCATION: SrcInfo = new SrcInfo(DUMMY_INTERVAL, null, "user"); + }, +}; +export const DUMMY_LOCATION: SrcInfo = new SrcInfo( + DUMMY_INTERVAL, + null, + "user", +); - export interface ExpressionTransformer { - applyRules(ast: AstExpression): AstExpression + applyRules(ast: AstExpression): AstExpression; } export interface Rule { - applyRule(ast: AstExpression, optimizer: ExpressionTransformer): AstExpression; -} \ No newline at end of file + applyRule( + ast: AstExpression, + optimizer: ExpressionTransformer, + ): AstExpression; +} diff --git a/src/optimizer/util.ts b/src/optimizer/util.ts index 09e7752b3..f6abde3d8 100644 --- a/src/optimizer/util.ts +++ b/src/optimizer/util.ts @@ -3,13 +3,15 @@ import { AstUnaryOperation, AstBinaryOperation, createAstNode, - AstStructFieldInitializer + AstStructFieldInitializer, } from "../grammar/ast"; import { Value } from "../types/types"; import { DUMMY_LOCATION, ValueExpression } from "./types"; export function isValue(ast: AstExpression): boolean { - switch (ast.kind) { // Missing structs + switch ( + ast.kind // Missing structs + ) { case "null": case "boolean": case "number": @@ -30,7 +32,9 @@ export function isValue(ast: AstExpression): boolean { } export function extractValue(ast: ValueExpression): Value { - switch (ast.kind) { // Missing structs + switch ( + ast.kind // Missing structs + ) { case "null": return null; case "boolean": @@ -46,7 +50,7 @@ export function makeValueExpression(value: Value): ValueExpression { if (value === null) { const result = createAstNode({ kind: "null", - loc: DUMMY_LOCATION + loc: DUMMY_LOCATION, }); return result as ValueExpression; } @@ -54,7 +58,7 @@ export function makeValueExpression(value: Value): ValueExpression { const result = createAstNode({ kind: "string", value: value, - loc: DUMMY_LOCATION + loc: DUMMY_LOCATION, }); return result as ValueExpression; } @@ -62,7 +66,7 @@ export function makeValueExpression(value: Value): ValueExpression { const result = createAstNode({ kind: "number", value: value, - loc: DUMMY_LOCATION + loc: DUMMY_LOCATION, }); return result as ValueExpression; } @@ -70,38 +74,44 @@ export function makeValueExpression(value: Value): ValueExpression { const result = createAstNode({ kind: "boolean", value: value, - loc: DUMMY_LOCATION + loc: DUMMY_LOCATION, }); return result as ValueExpression; } - throw `Unsupported value ${value}`; + throw `structs, addresses, cells, and comment values are not supported at the moment`; } - -export function makeUnaryExpression(op: AstUnaryOperation, operand: AstExpression): AstExpression { +export function makeUnaryExpression( + op: AstUnaryOperation, + operand: AstExpression, +): AstExpression { const result = createAstNode({ kind: "op_unary", op: op, operand: operand, - loc: DUMMY_LOCATION + loc: DUMMY_LOCATION, }); return result as AstExpression; } -export function makeBinaryExpression(op: AstBinaryOperation, left: AstExpression, right: AstExpression): AstExpression { +export function makeBinaryExpression( + op: AstBinaryOperation, + left: AstExpression, + right: AstExpression, +): AstExpression { const result = createAstNode({ kind: "op_binary", op: op, left: left, right: right, - loc: DUMMY_LOCATION + loc: DUMMY_LOCATION, }); return result as AstExpression; } // Checks if the top level node is a binary op node export function checkIsBinaryOpNode(ast: AstExpression): boolean { - return (ast.kind === "op_binary"); + return ast.kind === "op_binary"; } // Checks if top level node is a binary op node @@ -109,7 +119,7 @@ export function checkIsBinaryOpNode(ast: AstExpression): boolean { // value node on the right export function checkIsBinaryOp_NonValue_Value(ast: AstExpression): boolean { if (ast.kind === "op_binary") { - return (!isValue(ast.left) && isValue(ast.right)) + return !isValue(ast.left) && isValue(ast.right); } else { return false; } @@ -120,7 +130,7 @@ export function checkIsBinaryOp_NonValue_Value(ast: AstExpression): boolean { // non-value node on the right export function checkIsBinaryOp_Value_NonValue(ast: AstExpression): boolean { if (ast.kind === "op_binary") { - return (isValue(ast.left) && !isValue(ast.right)) + return isValue(ast.left) && !isValue(ast.right); } else { return false; } @@ -143,10 +153,8 @@ export function abs(a: bigint): bigint { } export function sign(a: bigint): bigint { - if (a === 0n) - return 0n; - else - return a < 0n ? -1n : 1n; + if (a === 0n) return 0n; + else return a < 0n ? -1n : 1n; } // precondition: the divisor is not zero @@ -157,7 +165,10 @@ export function modFloor(a: bigint, b: bigint): bigint { } // Test equality of ASTExpressions. -export function areEqualExpressions(ast1: AstExpression, ast2: AstExpression): boolean { +export function areEqualExpressions( + ast1: AstExpression, + ast2: AstExpression, +): boolean { switch (ast1.kind) { case "null": return ast2.kind === "null"; @@ -171,77 +182,101 @@ export function areEqualExpressions(ast1: AstExpression, ast2: AstExpression): b return ast2.kind === "id" ? ast1.text === ast2.text : false; case "method_call": if (ast2.kind === "method_call") { - return ast1.method.text === ast2.method.text && - areEqualExpressions(ast1.self, ast2.self) && - areEqualExpressionArrays(ast1.args, ast2.args); + return ( + ast1.method.text === ast2.method.text && + areEqualExpressions(ast1.self, ast2.self) && + areEqualExpressionArrays(ast1.args, ast2.args) + ); } else { return false; } case "init_of": if (ast2.kind === "init_of") { - return ast1.contract.text === ast2.contract.text && - areEqualExpressionArrays(ast1.args, ast2.args); + return ( + ast1.contract.text === ast2.contract.text && + areEqualExpressionArrays(ast1.args, ast2.args) + ); } else { return false; } case "op_unary": if (ast2.kind === "op_unary") { - return ast1.op === ast2.op && - areEqualExpressions(ast1.operand, ast2.operand); + return ( + ast1.op === ast2.op && + areEqualExpressions(ast1.operand, ast2.operand) + ); } else { return false; } case "op_binary": if (ast2.kind === "op_binary") { - return ast1.op === ast2.op && - areEqualExpressions(ast1.left, ast2.left) && - areEqualExpressions(ast1.right, ast2.right); + return ( + ast1.op === ast2.op && + areEqualExpressions(ast1.left, ast2.left) && + areEqualExpressions(ast1.right, ast2.right) + ); } else { return false; } case "conditional": if (ast2.kind === "conditional") { - return areEqualExpressions(ast1.condition, ast2.condition) && - areEqualExpressions(ast1.thenBranch, ast2.thenBranch) && - areEqualExpressions(ast1.elseBranch, ast2.elseBranch); + return ( + areEqualExpressions(ast1.condition, ast2.condition) && + areEqualExpressions(ast1.thenBranch, ast2.thenBranch) && + areEqualExpressions(ast1.elseBranch, ast2.elseBranch) + ); } else { return false; } case "struct_instance": if (ast2.kind === "struct_instance") { - return ast1.type.text === ast2.type.text && - areEqualParameterArrays(ast1.args, ast2.args); + return ( + ast1.type.text === ast2.type.text && + areEqualParameterArrays(ast1.args, ast2.args) + ); } else { return false; } case "field_access": if (ast2.kind === "field_access") { - return ast1.field.text === ast2.field.text && - areEqualExpressions(ast1.aggregate, ast2.aggregate); + return ( + ast1.field.text === ast2.field.text && + areEqualExpressions(ast1.aggregate, ast2.aggregate) + ); } else { return false; } case "static_call": if (ast2.kind === "static_call") { - return ast1.function.text === ast2.function.text && - areEqualExpressionArrays(ast1.args, ast2.args); + return ( + ast1.function.text === ast2.function.text && + areEqualExpressionArrays(ast1.args, ast2.args) + ); } else { return false; } } } -function areEqualParameters(arg1: AstStructFieldInitializer, arg2: AstStructFieldInitializer): boolean { - return arg1.field.text === arg2.field.text && - areEqualExpressions(arg1.initializer, arg2.initializer); +function areEqualParameters( + arg1: AstStructFieldInitializer, + arg2: AstStructFieldInitializer, +): boolean { + return ( + arg1.field.text === arg2.field.text && + areEqualExpressions(arg1.initializer, arg2.initializer) + ); } -function areEqualParameterArrays(arr1: AstStructFieldInitializer[], arr2: AstStructFieldInitializer[]): boolean { +function areEqualParameterArrays( + arr1: AstStructFieldInitializer[], + arr2: AstStructFieldInitializer[], +): boolean { if (arr1.length !== arr2.length) { return false; } - for (var i = 0; i < arr1.length; i++) { + for (let i = 0; i < arr1.length; i++) { if (!areEqualParameters(arr1[i], arr2[i])) { return false; } @@ -250,16 +285,19 @@ function areEqualParameterArrays(arr1: AstStructFieldInitializer[], arr2: AstStr return true; } -function areEqualExpressionArrays(arr1: AstExpression[], arr2: AstExpression[]): boolean { +function areEqualExpressionArrays( + arr1: AstExpression[], + arr2: AstExpression[], +): boolean { if (arr1.length !== arr2.length) { return false; } - for (var i = 0; i < arr1.length; i++) { + for (let i = 0; i < arr1.length; i++) { if (!areEqualExpressions(arr1[i], arr2[i])) { return false; } } return true; -} \ No newline at end of file +} diff --git a/src/test/e2e-emulated/partial-eval.spec.ts b/src/test/e2e-emulated/partial-eval.spec.ts index dcc88cf86..08cebe1d9 100644 --- a/src/test/e2e-emulated/partial-eval.spec.ts +++ b/src/test/e2e-emulated/partial-eval.spec.ts @@ -1,111 +1,120 @@ -import { AstExpression, __DANGER_resetNodeId, cloneAstNode } from "../../grammar/ast"; +import { + AstExpression, + __DANGER_resetNodeId, + cloneAstNode, +} from "../../grammar/ast"; import { parseExpression } from "../../grammar/grammar"; -import { areEqualExpressions, extractValue, isValue, makeValueExpression } from "../../optimizer/util"; +import { + areEqualExpressions, + extractValue, + isValue, + makeValueExpression, +} from "../../optimizer/util"; import { evalUnaryOp, partiallyEvalExpression } from "../../constEval"; import { CompilerContext } from "../../context"; import { ValueExpression } from "../../optimizer/types"; const additiveExpressions = [ - {original: "X + 3 + 1", simplified: "X + 4"}, - {original: "3 + X + 1", simplified: "X + 4"}, - {original: "1 + (X + 3)", simplified: "X + 4"}, - {original: "1 + (3 + X)", simplified: "4 + X"}, + { original: "X + 3 + 1", simplified: "X + 4" }, + { original: "3 + X + 1", simplified: "X + 4" }, + { original: "1 + (X + 3)", simplified: "X + 4" }, + { original: "1 + (3 + X)", simplified: "4 + X" }, // Should NOT simplify to X + 2, because X could be MAX - 2, // so that X + 3 causes an overflow, but X + 2 does not overflow - {original: "X + 3 - 1", simplified: "X + 3 - 1"}, - {original: "3 + X - 1", simplified: "3 + X - 1"}, + { original: "X + 3 - 1", simplified: "X + 3 - 1" }, + { original: "3 + X - 1", simplified: "3 + X - 1" }, // Should NOT simplify to X - 2, because X could be MIN + 2 - {original: "1 + (X - 3)", simplified: "1 + (X - 3)"}, + { original: "1 + (X - 3)", simplified: "1 + (X - 3)" }, - {original: "1 + (3 - X)", simplified: "4 - X"}, + { original: "1 + (3 - X)", simplified: "4 - X" }, + + { original: "X + 3 - (-1)", simplified: "X + 4" }, + { original: "3 + X - (-1)", simplified: "X + 4" }, - {original: "X + 3 - (-1)", simplified: "X + 4"}, - {original: "3 + X - (-1)", simplified: "X + 4"}, - // Should NOT simplify, because the current rules require that - commutes, // which does not. This could be fixed in future rules. - {original: "-1 + (X - 3)", simplified: "-1 + (X - 3)"}, + { original: "-1 + (X - 3)", simplified: "-1 + (X - 3)" }, // Should NOT simplify to 2 - X, because X could be MIN + 3, // so that 3 - X = -MIN = MAX + 1 causes an overflow, // but 2 - X = -MIN - 1 = MAX does not - {original: "-1 + (3 - X)", simplified: "-1 + (3 - X)"}, + { original: "-1 + (3 - X)", simplified: "-1 + (3 - X)" }, // All the following cases should NOT simplify because - // does not associate on the left with - or +. // The following "associative rule" for - will be added in the future: - // (x - c1) op c2 -----> x + (-c1 op c2), where op \in {-,+} - {original: "1 - (X + 3)", simplified: "1 - (X + 3)"}, - {original: "1 - (3 + X)", simplified: "1 - (3 + X)"}, - {original: "1 - X + 3", simplified: "1 - X + 3"}, - {original: "X - 1 + 3", simplified: "X - 1 + 3"}, - {original: "1 - (X - 3)", simplified: "1 - (X - 3)"}, - {original: "1 - (3 - X)", simplified: "1 - (3 - X)"}, - {original: "1 - X - 3", simplified: "1 - X - 3"}, - {original: "X - 1 - 3", simplified: "X - 1 - 3"} + // (x - c1) op c2 -----> x + (-c1 op c2), where op \in {-,+} + { original: "1 - (X + 3)", simplified: "1 - (X + 3)" }, + { original: "1 - (3 + X)", simplified: "1 - (3 + X)" }, + { original: "1 - X + 3", simplified: "1 - X + 3" }, + { original: "X - 1 + 3", simplified: "X - 1 + 3" }, + { original: "1 - (X - 3)", simplified: "1 - (X - 3)" }, + { original: "1 - (3 - X)", simplified: "1 - (3 - X)" }, + { original: "1 - X - 3", simplified: "1 - X - 3" }, + { original: "X - 1 - 3", simplified: "X - 1 - 3" }, ]; const multiplicativeExpressions = [ - {original: "X * 3 * 2", simplified: "X * 6"}, - {original: "3 * X * 2", simplified: "X * 6"}, - {original: "2 * (X * 3)", simplified: "X * 6"}, - {original: "2 * (3 * X)", simplified: "6 * X"}, + { original: "X * 3 * 2", simplified: "X * 6" }, + { original: "3 * X * 2", simplified: "X * 6" }, + { original: "2 * (X * 3)", simplified: "X * 6" }, + { original: "2 * (3 * X)", simplified: "6 * X" }, - {original: "X * -3 * -2", simplified: "X * 6"}, - {original: "-3 * X * -2", simplified: "X * 6"}, - {original: "-2 * (X * -3)", simplified: "X * 6"}, - {original: "-2 * (-3 * X)", simplified: "6 * X"}, + { original: "X * -3 * -2", simplified: "X * 6" }, + { original: "-3 * X * -2", simplified: "X * 6" }, + { original: "-2 * (X * -3)", simplified: "X * 6" }, + { original: "-2 * (-3 * X)", simplified: "6 * X" }, // The following 4 cases should NOT simplify to X * 0. - // the reason is that X could be MAX, so that X*3 causes + // the reason is that X could be MAX, so that X*3 causes // an overflow, but X*0 does not. - {original: "X * 3 * 0", simplified: "X * 3 * 0"}, - {original: "3 * X * 0", simplified: "3 * X * 0"}, - {original: "0 * (X * 3)", simplified: "0 * (X * 3)"}, - {original: "0 * (3 * X)", simplified: "0 * (3 * X)"}, + { original: "X * 3 * 0", simplified: "X * 3 * 0" }, + { original: "3 * X * 0", simplified: "3 * X * 0" }, + { original: "0 * (X * 3)", simplified: "0 * (X * 3)" }, + { original: "0 * (3 * X)", simplified: "0 * (3 * X)" }, - {original: "X * 0 * 3", simplified: "X * 0"}, - {original: "0 * X * 3", simplified: "X * 0"}, - {original: "3 * (X * 0)", simplified: "X * 0"}, - {original: "3 * (0 * X)", simplified: "0 * X"}, + { original: "X * 0 * 3", simplified: "X * 0" }, + { original: "0 * X * 3", simplified: "X * 0" }, + { original: "3 * (X * 0)", simplified: "X * 0" }, + { original: "3 * (0 * X)", simplified: "0 * X" }, // This expression cannot be further simplified to X, // because X could be MIN, so that X * -1 causes an overflow - {original: "X * -1 * 1 * -1", simplified: "X * -1 * -1"}, + { original: "X * -1 * 1 * -1", simplified: "X * -1 * -1" }, // This expression could be further simplified to X * -1 // but, currently, there are no rules that reduce three multiplied -1 // to a single -1. This should be fixed in the future. - {original: "X * -1 * 1 * -1 * -1", simplified: "X * -1 * -1 * -1"}, + { original: "X * -1 * 1 * -1 * -1", simplified: "X * -1 * -1 * -1" }, // Even though, X * -1 * 1 * -1 cannot be simplified to X, // when we multiply with a number with absolute value bigger than 1, // we ensure that the overflows are preserved, so that we can simplify // the expression. - {original: "X * -1 * 1 * -1 * 2", simplified: "X * 2"}, + { original: "X * -1 * 1 * -1 * 2", simplified: "X * 2" }, // Should NOT simplify to X * 2, because X could be MIN/2 = -2^255, // so that X * -2 = 2^256 = MAX + 1 causes an overflow, // but X * 2 = -2^256 does not. - {original: "X * -2 * -1", simplified: "X * -2 * -1"}, + { original: "X * -2 * -1", simplified: "X * -2 * -1" }, - // Note however that multiplying first by -1 allow us - // to simplify the expression, because if X * -1 overflows/underflows, + // Note however that multiplying first by -1 allow us + // to simplify the expression, because if X * -1 overflows, // X * 2 will also. - {original: "X * -1 * -2", simplified: "X * 2"} + { original: "X * -1 * -2", simplified: "X * 2" }, ]; function testExpression(original: string, simplified: string) { expect( areEqualExpressions( partiallyEvalExpression( - parseExpression(original), - new CompilerContext() + parseExpression(original), + new CompilerContext(), ), - unaryNegNodesToNumbers(parseExpression(simplified)) - ) + unaryNegNodesToNumbers(parseExpression(simplified)), + ), ).toBe(true); } @@ -113,10 +122,11 @@ function testExpression(original: string, simplified: string) { // The reason for doing this is that the partial evaluator will transform negative // numbers in an expression, e.g., "-1" into a tree with a single node with value -1, so that // when comparing the tree with those produced by the parser, the two trees -// do not match, because the parser will produce a UnaryOp node with a child node with value 1. +// do not match, because the parser will produce a UnaryOp node with a child node with value 1. // This is so because Tact does not have a way to write negative literals, but indirectly trough // the use of the unary - operator. function unaryNegNodesToNumbers(ast: AstExpression): AstExpression { + let newNode: AstExpression; switch (ast.kind) { case "null": return ast; @@ -129,52 +139,56 @@ function unaryNegNodesToNumbers(ast: AstExpression): AstExpression { case "id": return ast; case "method_call": - const newCallNode = cloneAstNode(ast); - newCallNode.args = ast.args.map(unaryNegNodesToNumbers); - newCallNode.self = unaryNegNodesToNumbers(ast.self); - return newCallNode; + newNode = cloneAstNode(ast); + newNode.args = ast.args.map(unaryNegNodesToNumbers); + newNode.self = unaryNegNodesToNumbers(ast.self); + return newNode; case "init_of": - const newInitOfNode = cloneAstNode(ast); - newInitOfNode.args = ast.args.map(unaryNegNodesToNumbers); - return newInitOfNode; + newNode = cloneAstNode(ast); + newNode.args = ast.args.map(unaryNegNodesToNumbers); + return newNode; case "op_unary": if (ast.op === "-") { if (isValue(ast.operand)) { return makeValueExpression( - evalUnaryOp(ast.op, extractValue(ast.operand as ValueExpression)) + evalUnaryOp( + ast.op, + extractValue(ast.operand as ValueExpression), + ), ); } } - const newUnaryNode = cloneAstNode(ast); - newUnaryNode.operand = unaryNegNodesToNumbers(ast.operand); - return newUnaryNode; + newNode = cloneAstNode(ast); + newNode.operand = unaryNegNodesToNumbers(ast.operand); + return newNode; case "op_binary": - const newBinaryNode = cloneAstNode(ast); - newBinaryNode.left = unaryNegNodesToNumbers(ast.left); - newBinaryNode.right = unaryNegNodesToNumbers(ast.right); - return newBinaryNode; + newNode = cloneAstNode(ast); + newNode.left = unaryNegNodesToNumbers(ast.left); + newNode.right = unaryNegNodesToNumbers(ast.right); + return newNode; case "conditional": - const newConditionalNode = cloneAstNode(ast); - newConditionalNode.thenBranch = unaryNegNodesToNumbers(ast.thenBranch); - newConditionalNode.elseBranch = unaryNegNodesToNumbers(ast.elseBranch); - return newConditionalNode; + newNode = cloneAstNode(ast); + newNode.thenBranch = unaryNegNodesToNumbers(ast.thenBranch); + newNode.elseBranch = unaryNegNodesToNumbers(ast.elseBranch); + return newNode; case "struct_instance": - const newStructNode = cloneAstNode(ast); - newStructNode.args = ast.args.map(param => { + newNode = cloneAstNode(ast); + newNode.args = ast.args.map((param) => { const newParam = cloneAstNode(param); - newParam.initializer = unaryNegNodesToNumbers(param.initializer); + newParam.initializer = unaryNegNodesToNumbers( + param.initializer, + ); return newParam; - } - ); - return newStructNode; + }); + return newNode; case "field_access": - const newFieldNode = cloneAstNode(ast); - newFieldNode.aggregate = unaryNegNodesToNumbers(ast.aggregate); - return newFieldNode; + newNode = cloneAstNode(ast); + newNode.aggregate = unaryNegNodesToNumbers(ast.aggregate); + return newNode; case "static_call": - const newStaticCallNode = cloneAstNode(ast); - newStaticCallNode.args = ast.args.map(unaryNegNodesToNumbers); - return newStaticCallNode; + newNode = cloneAstNode(ast); + newNode.args = ast.args.map(unaryNegNodesToNumbers); + return newNode; } } @@ -183,13 +197,13 @@ describe("partial-evaluator", () => { __DANGER_resetNodeId(); }); it("should correctly simplify partial expressions involving + and -", () => { - additiveExpressions.forEach( - pair => testExpression(pair.original, pair.simplified) - ) + additiveExpressions.forEach((pair) => + testExpression(pair.original, pair.simplified), + ); }); it("should correctly simplify partial expressions involving *", () => { - multiplicativeExpressions.forEach( - pair => testExpression(pair.original, pair.simplified) - ) + multiplicativeExpressions.forEach((pair) => + testExpression(pair.original, pair.simplified), + ); }); }); From 2688771ed83fb1d412914ee460710bd636c3c420 Mon Sep 17 00:00:00 2001 From: jeshecdom Date: Thu, 4 Jul 2024 12:43:24 +0200 Subject: [PATCH 05/14] Fixed another linter problem. --- src/interpreter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interpreter.ts b/src/interpreter.ts index c77febfd8..79463ca88 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -1,4 +1,4 @@ -import { evalConstantExpression, partiallyEvalExpression } from "./constEval"; +import { evalConstantExpression } from "./constEval"; import { CompilerContext } from "./context"; import { TactConstEvalError, TactParseError } from "./errors"; import { parseExpression } from "./grammar/grammar"; From cdb7cb267f679ba898b291df9b7006d77003172b Mon Sep 17 00:00:00 2001 From: jeshecdom Date: Fri, 5 Jul 2024 12:12:19 +0200 Subject: [PATCH 06/14] Fixed compilation errors after merge. --- src/optimizer/util.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/optimizer/util.ts b/src/optimizer/util.ts index f6abde3d8..f09308faa 100644 --- a/src/optimizer/util.ts +++ b/src/optimizer/util.ts @@ -277,7 +277,7 @@ function areEqualParameterArrays( } for (let i = 0; i < arr1.length; i++) { - if (!areEqualParameters(arr1[i], arr2[i])) { + if (!areEqualParameters(arr1[i]!, arr2[i]!)) { return false; } } @@ -294,7 +294,7 @@ function areEqualExpressionArrays( } for (let i = 0; i < arr1.length; i++) { - if (!areEqualExpressions(arr1[i], arr2[i])) { + if (!areEqualExpressions(arr1[i]!, arr2[i]!)) { return false; } } From bfb37afa354d77c38cc1bb0dc07ce1c79e32046f Mon Sep 17 00:00:00 2001 From: jeshecdom Date: Fri, 5 Jul 2024 12:38:30 +0200 Subject: [PATCH 07/14] Fixed problems pointed out by linter and prettier. --- src/optimizer/associative.ts | 23 ++++++++++------------ src/optimizer/util.ts | 4 +++- src/test/e2e-emulated/partial-eval.spec.ts | 12 +++++------ 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/optimizer/associative.ts b/src/optimizer/associative.ts index afe2ff5ac..0849ed7c3 100644 --- a/src/optimizer/associative.ts +++ b/src/optimizer/associative.ts @@ -28,12 +28,15 @@ export abstract class AssociativeRewriteRule implements Rule { constructor() { // + associates with these on the right: // i.e., all op \in plusAssoc. (a + b) op c = a + (b op c) - const additiveAssoc = new Set(["+", "-"]); + const additiveAssoc: Set = new Set(["+", "-"]); // - does not associate with any operator on the right // * associates with these on the right: - const multiplicativeAssoc = new Set(["*", "<<"]); + const multiplicativeAssoc: Set = new Set([ + "*", + "<<", + ]); // Division / does not associate with any on the right @@ -41,15 +44,12 @@ export abstract class AssociativeRewriteRule implements Rule { // TODO: shifts, bitwise integer operators, boolean operators - this.associativeOps = new Map< - AstBinaryOperation, - Set - >([ + this.associativeOps = new Map([ ["+", additiveAssoc], ["*", multiplicativeAssoc], ]); - this.commutativeOps = new Set( + this.commutativeOps = new Set( ["+", "*", "!=", "==", "&&", "||"], // TODO: bitwise integer operators ); } @@ -82,7 +82,7 @@ export abstract class AllowableOpRule extends AssociativeRewriteRule { constructor() { super(); - this.allowedOps = new Set( + this.allowedOps = new Set( // Recall that integer operators +,-,*,/,% are not safe with this rule, because // there is a risk that they will not preserve overflows in the unknown operands. ["&&", "||"], // TODO: check bitwise integer operators @@ -497,7 +497,7 @@ export class AssociativeRule2 extends AllowableOpRule { function ensureInt(val: Value): bigint { if (typeof val !== "bigint") { - throw `integer expected, but got '${val}'`; + throw new Error(`integer expected`); } return val; } @@ -511,10 +511,7 @@ export class AssociativeRule3 extends AssociativeRewriteRule { public constructor() { super(); - this.extraOpCondition = new Map< - AstBinaryOperation, - (c1: Value, c2: Value, val: Value) => boolean - >([ + this.extraOpCondition = new Map([ [ "+", (c1, c2, val) => { diff --git a/src/optimizer/util.ts b/src/optimizer/util.ts index f09308faa..1b5a03126 100644 --- a/src/optimizer/util.ts +++ b/src/optimizer/util.ts @@ -78,7 +78,9 @@ export function makeValueExpression(value: Value): ValueExpression { }); return result as ValueExpression; } - throw `structs, addresses, cells, and comment values are not supported at the moment`; + throw new Error( + `structs, addresses, cells, and comment values are not supported at the moment.`, + ); } export function makeUnaryExpression( diff --git a/src/test/e2e-emulated/partial-eval.spec.ts b/src/test/e2e-emulated/partial-eval.spec.ts index 08cebe1d9..f6589268a 100644 --- a/src/test/e2e-emulated/partial-eval.spec.ts +++ b/src/test/e2e-emulated/partial-eval.spec.ts @@ -197,13 +197,13 @@ describe("partial-evaluator", () => { __DANGER_resetNodeId(); }); it("should correctly simplify partial expressions involving + and -", () => { - additiveExpressions.forEach((pair) => - testExpression(pair.original, pair.simplified), - ); + additiveExpressions.forEach((pair) => { + testExpression(pair.original, pair.simplified); + }); }); it("should correctly simplify partial expressions involving *", () => { - multiplicativeExpressions.forEach((pair) => - testExpression(pair.original, pair.simplified), - ); + multiplicativeExpressions.forEach((pair) => { + testExpression(pair.original, pair.simplified); + }); }); }); From 648f188808a3886485654e860a982db8e35b063e Mon Sep 17 00:00:00 2001 From: jeshecdom Date: Fri, 5 Jul 2024 18:43:14 +0200 Subject: [PATCH 08/14] - Changed all `Ref` in parameters for `Loc`. (https://github.com/tact-lang/tact/pull/528/files#r1666596822) - Added `dummySrcInfo` as default parameter values to functions `__evalUnaryOp` and `__evalBinaryOp`. This way I was able to eliminate them and simply leave `evalUnaryOp` and `evalBinaryOp` as exportable functions. (https://github.com/tact-lang/tact/pull/528/files#r1666598821) - Renamed ValueExpression for AstValue (https://github.com/tact-lang/tact/pull/528/files#r1666602072) - Used Dummy interval in grammar.ts (https://github.com/tact-lang/tact/pull/528/files#r1666602072) - Changed interfaces for abstract classes (https://github.com/tact-lang/tact/pull/528/files#r1666604615) - Renamed `areEqualExpressions` to `eqExpressions`, and moved it to `ast.ts`. Used `eqNames` and moved `ast1.kind === ast2.kind` check before switch. (https://github.com/tact-lang/tact/pull/528/files#r1666609433) --- src/constEval.ts | 110 ++++++-------- src/grammar/ast.ts | 110 ++++++++++++++ src/optimizer/associative.ts | 51 +++---- src/optimizer/standardOptimizer.ts | 4 +- src/optimizer/types.ts | 63 +------- src/optimizer/util.ts | 167 ++------------------- src/test/e2e-emulated/partial-eval.spec.ts | 8 +- 7 files changed, 208 insertions(+), 305 deletions(-) diff --git a/src/constEval.ts b/src/constEval.ts index ebb81e3e0..c4ff518d1 100644 --- a/src/constEval.ts +++ b/src/constEval.ts @@ -25,9 +25,8 @@ import { modFloor, } from "./optimizer/util"; import { - DUMMY_LOCATION, ExpressionTransformer, - ValueExpression, + AstValue, } from "./optimizer/types"; import { StandardOptimizer } from "./optimizer/standardOptimizer"; import { @@ -36,6 +35,7 @@ import { hasStaticConstant, } from "./types/resolveDescriptors"; import { getExpType } from "./types/resolveExpression"; +import { dummySrcInfo } from "./grammar/grammar"; // TVM integers are signed 257-bit integers const minTvmInt: bigint = -(2n ** 256n); @@ -125,30 +125,27 @@ function ensureMethodArity( } } -export function evalUnaryOp(op: AstUnaryOperation, valOperand: Value): Value { - return __evalUnaryOp(op, valOperand, DUMMY_LOCATION, DUMMY_LOCATION); -} -function __evalUnaryOp( +export function evalUnaryOp( op: AstUnaryOperation, valOperand: Value, - operandRef: SrcInfo, - source: SrcInfo, + operandLoc: SrcInfo = dummySrcInfo, + source: SrcInfo = dummySrcInfo ): Value { switch (op) { case "+": - return ensureInt(valOperand, operandRef); + return ensureInt(valOperand, operandLoc); case "-": - return ensureInt(-ensureInt(valOperand, operandRef), source); + return ensureInt(-ensureInt(valOperand, operandLoc), source); case "~": - return ~ensureInt(valOperand, operandRef); + return ~ensureInt(valOperand, operandLoc); case "!": - return !ensureBoolean(valOperand, operandRef); + return !ensureBoolean(valOperand, operandLoc); case "!!": if (valOperand === null) { throwErrorConstEval( "non-null value expected but got null", - operandRef, + operandLoc, ); } return valOperand; @@ -172,7 +169,7 @@ function fullyEvalUnaryOp( const valOperand = evalConstantExpression(operand, ctx); - return __evalUnaryOp(op, valOperand, operand.loc, source); + return evalUnaryOp(op, valOperand, operand.loc, source); } function partiallyEvalUnaryOp( @@ -184,8 +181,8 @@ function partiallyEvalUnaryOp( const simplOperand = partiallyEvalExpression(operand, ctx); if (isValue(simplOperand)) { - const valueOperand = extractValue(simplOperand as ValueExpression); - const result = __evalUnaryOp( + const valueOperand = extractValue(simplOperand as AstValue); + const result = evalUnaryOp( op, valueOperand, simplOperand.loc, @@ -209,7 +206,7 @@ function fullyEvalBinaryOp( const valLeft = evalConstantExpression(left, ctx); const valRight = evalConstantExpression(right, ctx); - return __evalBinaryOp(op, valLeft, valRight, left.loc, right.loc, source); + return evalBinaryOp(op, valLeft, valRight, left.loc, right.loc, source); } function partiallyEvalBinaryOp( @@ -223,9 +220,9 @@ function partiallyEvalBinaryOp( const rightOperand = partiallyEvalExpression(right, ctx); if (isValue(leftOperand) && isValue(rightOperand)) { - const valueLeftOperand = extractValue(leftOperand as ValueExpression); - const valueRightOperand = extractValue(rightOperand as ValueExpression); - const result = __evalBinaryOp( + const valueLeftOperand = extractValue(leftOperand as AstValue); + const valueRightOperand = extractValue(rightOperand as AstValue); + const result = evalBinaryOp( op, valueLeftOperand, valueRightOperand, @@ -245,39 +242,24 @@ export function evalBinaryOp( op: AstBinaryOperation, valLeft: Value, valRight: Value, -): Value { - return __evalBinaryOp( - op, - valLeft, - valRight, - DUMMY_LOCATION, - DUMMY_LOCATION, - DUMMY_LOCATION, - ); -} - -function __evalBinaryOp( - op: AstBinaryOperation, - valLeft: Value, - valRight: Value, - refLeft: SrcInfo, - refRight: SrcInfo, - source: SrcInfo, + locLeft: SrcInfo = dummySrcInfo, + locRight: SrcInfo = dummySrcInfo, + source: SrcInfo = dummySrcInfo ): Value { switch (op) { case "+": return ensureInt( - ensureInt(valLeft, refLeft) + ensureInt(valRight, refRight), + ensureInt(valLeft, locLeft) + ensureInt(valRight, locRight), source, ); case "-": return ensureInt( - ensureInt(valLeft, refLeft) - ensureInt(valRight, refRight), + ensureInt(valLeft, locLeft) - ensureInt(valRight, locRight), source, ); case "*": return ensureInt( - ensureInt(valLeft, refLeft) * ensureInt(valRight, refRight), + ensureInt(valLeft, locLeft) * ensureInt(valRight, locRight), source, ); case "/": { @@ -285,38 +267,38 @@ function __evalBinaryOp( // 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(valRight, refRight); + const r = ensureInt(valRight, locRight); if (r === 0n) throwErrorConstEval( "divisor expression must be non-zero", - refRight, + locRight, ); - return ensureInt(divFloor(ensureInt(valLeft, refLeft), r), source); + return ensureInt(divFloor(ensureInt(valLeft, locLeft), r), source); } case "%": { // Same as for division, see the comment above // Example: -1 % 5 = 4 - const r = ensureInt(valRight, refRight); + const r = ensureInt(valRight, locRight); if (r === 0n) throwErrorConstEval( "divisor expression must be non-zero", - refRight, + locRight, ); - return ensureInt(modFloor(ensureInt(valLeft, refLeft), r), source); + return ensureInt(modFloor(ensureInt(valLeft, locLeft), r), source); } case "&": - return ensureInt(valLeft, refLeft) & ensureInt(valRight, refRight); + return ensureInt(valLeft, locLeft) & ensureInt(valRight, locRight); case "|": - return ensureInt(valLeft, refLeft) | ensureInt(valRight, refRight); + return ensureInt(valLeft, locLeft) | ensureInt(valRight, locRight); case "^": - return ensureInt(valLeft, refLeft) ^ ensureInt(valRight, refRight); + return ensureInt(valLeft, locLeft) ^ ensureInt(valRight, locRight); case "<<": { - const valNum = ensureInt(valLeft, refLeft); - const valBits = ensureInt(valRight, refRight); + const valNum = ensureInt(valLeft, locLeft); + const valBits = ensureInt(valRight, locRight); if (0n > valBits || valBits > 256n) { throwErrorConstEval( `the number of bits shifted ('${valBits}') must be within [0..256] range`, - refRight, + locRight, ); } try { @@ -332,12 +314,12 @@ function __evalBinaryOp( } } case ">>": { - const valNum = ensureInt(valLeft, refLeft); - const valBits = ensureInt(valRight, refRight); + const valNum = ensureInt(valLeft, locLeft); + const valBits = ensureInt(valRight, locRight); if (0n > valBits || valBits > 256n) { throwErrorConstEval( `the number of bits shifted ('${valBits}') must be within [0..256] range`, - refRight, + locRight, ); } try { @@ -353,13 +335,13 @@ function __evalBinaryOp( } } case ">": - return ensureInt(valLeft, refLeft) > ensureInt(valRight, refRight); + return ensureInt(valLeft, locLeft) > ensureInt(valRight, locRight); case "<": - return ensureInt(valLeft, refLeft) < ensureInt(valRight, refRight); + return ensureInt(valLeft, locLeft) < ensureInt(valRight, locRight); case ">=": - return ensureInt(valLeft, refLeft) >= ensureInt(valRight, refRight); + return ensureInt(valLeft, locLeft) >= ensureInt(valRight, locRight); case "<=": - return ensureInt(valLeft, refLeft) <= ensureInt(valRight, refRight); + return ensureInt(valLeft, locLeft) <= ensureInt(valRight, locRight); case "==": // the null comparisons account for optional types, e.g. // a const x: Int? = 42 can be compared to null @@ -384,13 +366,13 @@ function __evalBinaryOp( return valLeft !== valRight; case "&&": return ( - ensureBoolean(valLeft, refLeft) && - ensureBoolean(valRight, refRight) + ensureBoolean(valLeft, locLeft) && + ensureBoolean(valRight, locRight) ); case "||": return ( - ensureBoolean(valLeft, refLeft) || - ensureBoolean(valRight, refRight) + ensureBoolean(valLeft, locLeft) || + ensureBoolean(valRight, locRight) ); } } diff --git a/src/grammar/ast.ts b/src/grammar/ast.ts index 3f2443e85..8a2d13583 100644 --- a/src/grammar/ast.ts +++ b/src/grammar/ast.ts @@ -679,6 +679,116 @@ export function __DANGER_resetNodeId() { nextId = 1; } +// Test equality of ASTExpressions. +export function eqExpressions( + ast1: AstExpression, + ast2: AstExpression, +): boolean { + if (ast1.kind !== ast2.kind) { + return false; + } + + switch (ast1.kind) { + case "null": + return true; + case "boolean": + return ast1.value === (ast2 as AstBoolean).value; + case "number": + return ast1.value === (ast2 as AstNumber).value; + case "string": + return ast1.value === (ast2 as AstString).value; + case "id": + return eqNames(ast1, ast2 as AstId); + case "method_call": + return ( + eqNames(ast1.method, (ast2 as AstMethodCall).method) && + eqExpressions(ast1.self, (ast2 as AstMethodCall).self) && + eqExpressionArrays(ast1.args, (ast2 as AstMethodCall).args) + ); + case "init_of": + return ( + eqNames(ast1.contract, (ast2 as AstInitOf).contract) && + eqExpressionArrays(ast1.args, (ast2 as AstInitOf).args) + ); + case "op_unary": + return ( + ast1.op === (ast2 as AstOpUnary).op && + eqExpressions(ast1.operand, (ast2 as AstOpUnary).operand) + ); + case "op_binary": + return ( + ast1.op === (ast2 as AstOpBinary).op && + eqExpressions(ast1.left, (ast2 as AstOpBinary).left) && + eqExpressions(ast1.right, (ast2 as AstOpBinary).right) + ); + case "conditional": + return ( + eqExpressions(ast1.condition, (ast2 as AstConditional).condition) && + eqExpressions(ast1.thenBranch, (ast2 as AstConditional).thenBranch) && + eqExpressions(ast1.elseBranch, (ast2 as AstConditional).elseBranch) + ); + case "struct_instance": + return ( + eqNames(ast1.type, (ast2 as AstStructInstance).type) && + eqParameterArrays(ast1.args, (ast2 as AstStructInstance).args) + ); + case "field_access": + return ( + eqNames(ast1.field, (ast2 as AstFieldAccess).field) && + eqExpressions(ast1.aggregate, (ast2 as AstFieldAccess).aggregate) + ); + case "static_call": + return ( + eqNames(ast1.function, (ast2 as AstStaticCall).function) && + eqExpressionArrays(ast1.args, (ast2 as AstStaticCall).args) + ); + } +} + +function eqParameters( + arg1: AstStructFieldInitializer, + arg2: AstStructFieldInitializer, +): boolean { + return ( + eqNames(arg1.field, arg2.field) && + eqExpressions(arg1.initializer, arg2.initializer) + ); +} + +function eqParameterArrays( + arr1: AstStructFieldInitializer[], + arr2: AstStructFieldInitializer[], +): boolean { + if (arr1.length !== arr2.length) { + return false; + } + + for (let i = 0; i < arr1.length; i++) { + if (!eqParameters(arr1[i]!, arr2[i]!)) { + return false; + } + } + + return true; +} + +function eqExpressionArrays( + arr1: AstExpression[], + arr2: AstExpression[], +): boolean { + if (arr1.length !== arr2.length) { + return false; + } + + for (let i = 0; i < arr1.length; i++) { + if (!eqExpressions(arr1[i]!, arr2[i]!)) { + return false; + } + } + + return true; +} + export function traverse(node: AstNode, callback: (node: AstNode) => void) { callback(node); diff --git a/src/optimizer/associative.ts b/src/optimizer/associative.ts index 0849ed7c3..0ade5c828 100644 --- a/src/optimizer/associative.ts +++ b/src/optimizer/associative.ts @@ -3,7 +3,7 @@ import { evalBinaryOp } from "../constEval"; import { AstBinaryOperation, AstExpression, AstOpBinary } from "../grammar/ast"; import { Value } from "../types/types"; -import { ExpressionTransformer, Rule, ValueExpression } from "./types"; +import { ExpressionTransformer, Rule, AstValue } from "./types"; import { abs, checkIsBinaryOpNode, @@ -16,7 +16,7 @@ import { sign, } from "./util"; -export abstract class AssociativeRewriteRule implements Rule { +export abstract class AssociativeRewriteRule extends Rule { // An entry (op, S) in the map means "operator op associates with all operators in set S", // mathematically: all op2 \in S. (a op b) op2 c = a op (b op2 c) private associativeOps: Map>; @@ -26,6 +26,8 @@ export abstract class AssociativeRewriteRule implements Rule { private commutativeOps: Set; constructor() { + super(); + // + associates with these on the right: // i.e., all op \in plusAssoc. (a + b) op c = a + (b op c) const additiveAssoc: Set = new Set(["+", "-"]); @@ -54,11 +56,6 @@ export abstract class AssociativeRewriteRule implements Rule { ); } - public abstract applyRule( - ast: AstExpression, - optimizer: ExpressionTransformer, - ): AstExpression; - public areAssociative( op1: AstBinaryOperation, op2: AstBinaryOperation, @@ -118,11 +115,11 @@ export class AssociativeRule1 extends AllowableOpRule { const rightTree = topLevelNode.right as AstOpBinary; const x1 = leftTree.left; - const c1 = leftTree.right as ValueExpression; + const c1 = leftTree.right as AstValue; const op1 = leftTree.op; const x2 = rightTree.left; - const c2 = rightTree.right as ValueExpression; + const c2 = rightTree.right as AstValue; const op2 = rightTree.op; const op = topLevelNode.op; @@ -172,11 +169,11 @@ export class AssociativeRule1 extends AllowableOpRule { const rightTree = topLevelNode.right as AstOpBinary; const x1 = leftTree.left; - const c1 = leftTree.right as ValueExpression; + const c1 = leftTree.right as AstValue; const op1 = leftTree.op; const x2 = rightTree.right; - const c2 = rightTree.left as ValueExpression; + const c2 = rightTree.left as AstValue; const op2 = rightTree.op; const op = topLevelNode.op; @@ -226,11 +223,11 @@ export class AssociativeRule1 extends AllowableOpRule { const rightTree = topLevelNode.right as AstOpBinary; const x1 = leftTree.right; - const c1 = leftTree.left as ValueExpression; + const c1 = leftTree.left as AstValue; const op1 = leftTree.op; const x2 = rightTree.left; - const c2 = rightTree.right as ValueExpression; + const c2 = rightTree.right as AstValue; const op2 = rightTree.op; const op = topLevelNode.op; @@ -282,11 +279,11 @@ export class AssociativeRule1 extends AllowableOpRule { const rightTree = topLevelNode.right as AstOpBinary; const x1 = leftTree.right; - const c1 = leftTree.left as ValueExpression; + const c1 = leftTree.left as AstValue; const op1 = leftTree.op; const x2 = rightTree.right; - const c2 = rightTree.left as ValueExpression; + const c2 = rightTree.left as AstValue; const op2 = rightTree.op; const op = topLevelNode.op; @@ -352,7 +349,7 @@ export class AssociativeRule2 extends AllowableOpRule { const rightTree = topLevelNode.right; const x1 = leftTree.left; - const c1 = leftTree.right as ValueExpression; + const c1 = leftTree.right as AstValue; const op1 = leftTree.op; const x2 = rightTree; @@ -389,7 +386,7 @@ export class AssociativeRule2 extends AllowableOpRule { const rightTree = topLevelNode.right; const x1 = leftTree.right; - const c1 = leftTree.left as ValueExpression; + const c1 = leftTree.left as AstValue; const op1 = leftTree.op; const x2 = rightTree; @@ -424,7 +421,7 @@ export class AssociativeRule2 extends AllowableOpRule { const rightTree = topLevelNode.right as AstOpBinary; const x1 = rightTree.left; - const c1 = rightTree.right as ValueExpression; + const c1 = rightTree.right as AstValue; const op1 = rightTree.op; const x2 = leftTree; @@ -459,7 +456,7 @@ export class AssociativeRule2 extends AllowableOpRule { const rightTree = topLevelNode.right as AstOpBinary; const x1 = rightTree.right; - const c1 = rightTree.left as ValueExpression; + const c1 = rightTree.left as AstValue; const op1 = rightTree.op; const x2 = leftTree; @@ -577,10 +574,10 @@ export class AssociativeRule3 extends AssociativeRewriteRule { // The tree has this form: // (x1 op1 c1) op c2 const leftTree = topLevelNode.left as AstOpBinary; - const rightTree = topLevelNode.right as ValueExpression; + const rightTree = topLevelNode.right as AstValue; const x1 = leftTree.left; - const c1 = extractValue(leftTree.right as ValueExpression); + const c1 = extractValue(leftTree.right as AstValue); const op1 = leftTree.op; const c2 = extractValue(rightTree); @@ -621,10 +618,10 @@ export class AssociativeRule3 extends AssociativeRewriteRule { // The tree has this form: // (c1 op1 x1) op c2 const leftTree = topLevelNode.left as AstOpBinary; - const rightTree = topLevelNode.right as ValueExpression; + const rightTree = topLevelNode.right as AstValue; const x1 = leftTree.right; - const c1 = extractValue(leftTree.left as ValueExpression); + const c1 = extractValue(leftTree.left as AstValue); const op1 = leftTree.op; const c2 = extractValue(rightTree); @@ -666,11 +663,11 @@ export class AssociativeRule3 extends AssociativeRewriteRule { ) { // The tree has this form: // c2 op (x1 op1 c1) - const leftTree = topLevelNode.left as ValueExpression; + const leftTree = topLevelNode.left as AstValue; const rightTree = topLevelNode.right as AstOpBinary; const x1 = rightTree.left; - const c1 = extractValue(rightTree.right as ValueExpression); + const c1 = extractValue(rightTree.right as AstValue); const op1 = rightTree.op; const c2 = extractValue(leftTree); @@ -712,11 +709,11 @@ export class AssociativeRule3 extends AssociativeRewriteRule { ) { // The tree has this form: // c2 op (c1 op1 x1) - const leftTree = topLevelNode.left as ValueExpression; + const leftTree = topLevelNode.left as AstValue; const rightTree = topLevelNode.right as AstOpBinary; const x1 = rightTree.right; - const c1 = extractValue(rightTree.left as ValueExpression); + const c1 = extractValue(rightTree.left as AstValue); const op1 = rightTree.op; const c2 = extractValue(leftTree); diff --git a/src/optimizer/standardOptimizer.ts b/src/optimizer/standardOptimizer.ts index 808d9083d..045dde87d 100644 --- a/src/optimizer/standardOptimizer.ts +++ b/src/optimizer/standardOptimizer.ts @@ -9,10 +9,12 @@ import { Rule, ExpressionTransformer } from "./types"; type PrioritizedRule = { priority: number; rule: Rule }; // This optimizer uses rules that preserve overflows in integer expressions. -export class StandardOptimizer implements ExpressionTransformer { +export class StandardOptimizer extends ExpressionTransformer { private rules: PrioritizedRule[]; constructor() { + super(); + this.rules = [ { priority: 0, rule: new AssociativeRule1() }, { priority: 1, rule: new AssociativeRule2() }, diff --git a/src/optimizer/types.ts b/src/optimizer/types.ts index 84c338496..713be2ec6 100644 --- a/src/optimizer/types.ts +++ b/src/optimizer/types.ts @@ -4,68 +4,17 @@ import { AstNumber, AstBoolean, AstNull, - AstString, - SrcInfo, + AstString } from "../grammar/ast"; -export type ValueExpression = AstNumber | AstBoolean | AstNull | AstString; +export type AstValue = AstNumber | AstBoolean | AstNull | AstString; -export const DUMMY_INTERVAL: Interval = { - sourceString: "", - startIdx: 0, - endIdx: 10, - contents: "mock contents", - minus(that) { - // Returned the parameter so that the linter stops complaining - return [that]; - }, - relativeTo(that) { - // Returned the parameter so that the linter stops complaining - return that; - }, - subInterval(offset, len) { - // Did this so that the linter stops complaining - return offset == len ? this : this; - }, - collapsedLeft() { - return this; - }, - collapsedRight() { - return this; - }, - trimmed() { - return this; - }, - coverageWith(...intervals) { - // This this so that the linter stops complaining - return intervals.length == 0 ? this : this; - }, - getLineAndColumnMessage() { - return `Line 1, Column 0`; - }, - getLineAndColumn() { - return { - offset: 0, - lineNum: 1, - colNum: 0, - line: "1", - nextLine: "1", - prevLine: "1", - }; - }, -}; -export const DUMMY_LOCATION: SrcInfo = new SrcInfo( - DUMMY_INTERVAL, - null, - "user", -); - -export interface ExpressionTransformer { - applyRules(ast: AstExpression): AstExpression; +export abstract class ExpressionTransformer { + public abstract applyRules(ast: AstExpression): AstExpression; } -export interface Rule { - applyRule( +export abstract class Rule { + public abstract applyRule( ast: AstExpression, optimizer: ExpressionTransformer, ): AstExpression; diff --git a/src/optimizer/util.ts b/src/optimizer/util.ts index 1b5a03126..030900fe6 100644 --- a/src/optimizer/util.ts +++ b/src/optimizer/util.ts @@ -2,11 +2,11 @@ import { AstExpression, AstUnaryOperation, AstBinaryOperation, - createAstNode, - AstStructFieldInitializer, + createAstNode } from "../grammar/ast"; +import { dummySrcInfo } from "../grammar/grammar"; import { Value } from "../types/types"; -import { DUMMY_LOCATION, ValueExpression } from "./types"; +import { AstValue } from "./types"; export function isValue(ast: AstExpression): boolean { switch ( @@ -31,7 +31,7 @@ export function isValue(ast: AstExpression): boolean { } } -export function extractValue(ast: ValueExpression): Value { +export function extractValue(ast: AstValue): Value { switch ( ast.kind // Missing structs ) { @@ -46,37 +46,37 @@ export function extractValue(ast: ValueExpression): Value { } } -export function makeValueExpression(value: Value): ValueExpression { +export function makeValueExpression(value: Value): AstValue { if (value === null) { const result = createAstNode({ kind: "null", - loc: DUMMY_LOCATION, + loc: dummySrcInfo, }); - return result as ValueExpression; + return result as AstValue; } if (typeof value === "string") { const result = createAstNode({ kind: "string", value: value, - loc: DUMMY_LOCATION, + loc: dummySrcInfo, }); - return result as ValueExpression; + return result as AstValue; } if (typeof value === "bigint") { const result = createAstNode({ kind: "number", value: value, - loc: DUMMY_LOCATION, + loc: dummySrcInfo, }); - return result as ValueExpression; + return result as AstValue; } if (typeof value === "boolean") { const result = createAstNode({ kind: "boolean", value: value, - loc: DUMMY_LOCATION, + loc: dummySrcInfo, }); - return result as ValueExpression; + return result as AstValue; } throw new Error( `structs, addresses, cells, and comment values are not supported at the moment.`, @@ -91,7 +91,7 @@ export function makeUnaryExpression( kind: "op_unary", op: op, operand: operand, - loc: DUMMY_LOCATION, + loc: dummySrcInfo, }); return result as AstExpression; } @@ -106,7 +106,7 @@ export function makeBinaryExpression( op: op, left: left, right: right, - loc: DUMMY_LOCATION, + loc: dummySrcInfo, }); return result as AstExpression; } @@ -166,140 +166,3 @@ export function modFloor(a: bigint, b: bigint): bigint { return a - divFloor(a, b) * b; } -// Test equality of ASTExpressions. -export function areEqualExpressions( - ast1: AstExpression, - ast2: AstExpression, -): boolean { - switch (ast1.kind) { - case "null": - return ast2.kind === "null"; - case "boolean": - return ast2.kind === "boolean" ? ast1.value === ast2.value : false; - case "number": - return ast2.kind === "number" ? ast1.value === ast2.value : false; - case "string": - return ast2.kind === "string" ? ast1.value === ast2.value : false; - case "id": - return ast2.kind === "id" ? ast1.text === ast2.text : false; - case "method_call": - if (ast2.kind === "method_call") { - return ( - ast1.method.text === ast2.method.text && - areEqualExpressions(ast1.self, ast2.self) && - areEqualExpressionArrays(ast1.args, ast2.args) - ); - } else { - return false; - } - case "init_of": - if (ast2.kind === "init_of") { - return ( - ast1.contract.text === ast2.contract.text && - areEqualExpressionArrays(ast1.args, ast2.args) - ); - } else { - return false; - } - case "op_unary": - if (ast2.kind === "op_unary") { - return ( - ast1.op === ast2.op && - areEqualExpressions(ast1.operand, ast2.operand) - ); - } else { - return false; - } - case "op_binary": - if (ast2.kind === "op_binary") { - return ( - ast1.op === ast2.op && - areEqualExpressions(ast1.left, ast2.left) && - areEqualExpressions(ast1.right, ast2.right) - ); - } else { - return false; - } - case "conditional": - if (ast2.kind === "conditional") { - return ( - areEqualExpressions(ast1.condition, ast2.condition) && - areEqualExpressions(ast1.thenBranch, ast2.thenBranch) && - areEqualExpressions(ast1.elseBranch, ast2.elseBranch) - ); - } else { - return false; - } - case "struct_instance": - if (ast2.kind === "struct_instance") { - return ( - ast1.type.text === ast2.type.text && - areEqualParameterArrays(ast1.args, ast2.args) - ); - } else { - return false; - } - case "field_access": - if (ast2.kind === "field_access") { - return ( - ast1.field.text === ast2.field.text && - areEqualExpressions(ast1.aggregate, ast2.aggregate) - ); - } else { - return false; - } - case "static_call": - if (ast2.kind === "static_call") { - return ( - ast1.function.text === ast2.function.text && - areEqualExpressionArrays(ast1.args, ast2.args) - ); - } else { - return false; - } - } -} - -function areEqualParameters( - arg1: AstStructFieldInitializer, - arg2: AstStructFieldInitializer, -): boolean { - return ( - arg1.field.text === arg2.field.text && - areEqualExpressions(arg1.initializer, arg2.initializer) - ); -} - -function areEqualParameterArrays( - arr1: AstStructFieldInitializer[], - arr2: AstStructFieldInitializer[], -): boolean { - if (arr1.length !== arr2.length) { - return false; - } - - for (let i = 0; i < arr1.length; i++) { - if (!areEqualParameters(arr1[i]!, arr2[i]!)) { - return false; - } - } - - return true; -} - -function areEqualExpressionArrays( - arr1: AstExpression[], - arr2: AstExpression[], -): boolean { - if (arr1.length !== arr2.length) { - return false; - } - - for (let i = 0; i < arr1.length; i++) { - if (!areEqualExpressions(arr1[i]!, arr2[i]!)) { - return false; - } - } - - return true; -} diff --git a/src/test/e2e-emulated/partial-eval.spec.ts b/src/test/e2e-emulated/partial-eval.spec.ts index f6589268a..a4c1484e3 100644 --- a/src/test/e2e-emulated/partial-eval.spec.ts +++ b/src/test/e2e-emulated/partial-eval.spec.ts @@ -2,17 +2,17 @@ import { AstExpression, __DANGER_resetNodeId, cloneAstNode, + eqExpressions, } from "../../grammar/ast"; import { parseExpression } from "../../grammar/grammar"; import { - areEqualExpressions, extractValue, isValue, makeValueExpression, } from "../../optimizer/util"; import { evalUnaryOp, partiallyEvalExpression } from "../../constEval"; import { CompilerContext } from "../../context"; -import { ValueExpression } from "../../optimizer/types"; +import { AstValue } from "../../optimizer/types"; const additiveExpressions = [ { original: "X + 3 + 1", simplified: "X + 4" }, @@ -108,7 +108,7 @@ const multiplicativeExpressions = [ function testExpression(original: string, simplified: string) { expect( - areEqualExpressions( + eqExpressions( partiallyEvalExpression( parseExpression(original), new CompilerContext(), @@ -153,7 +153,7 @@ function unaryNegNodesToNumbers(ast: AstExpression): AstExpression { return makeValueExpression( evalUnaryOp( ast.op, - extractValue(ast.operand as ValueExpression), + extractValue(ast.operand as AstValue), ), ); } From 042624512e6925297b25579b698ab13269aa9485 Mon Sep 17 00:00:00 2001 From: jeshecdom Date: Fri, 5 Jul 2024 18:48:10 +0200 Subject: [PATCH 09/14] fixes due to linter and prettier. --- src/constEval.ts | 17 ++----- src/grammar/ast.ts | 82 +++++++++++++++++------------- src/optimizer/associative.ts | 2 +- src/optimizer/standardOptimizer.ts | 2 +- src/optimizer/types.ts | 3 +- src/optimizer/util.ts | 3 +- 6 files changed, 55 insertions(+), 54 deletions(-) diff --git a/src/constEval.ts b/src/constEval.ts index c4ff518d1..3daa50b73 100644 --- a/src/constEval.ts +++ b/src/constEval.ts @@ -24,10 +24,7 @@ import { divFloor, modFloor, } from "./optimizer/util"; -import { - ExpressionTransformer, - AstValue, -} from "./optimizer/types"; +import { ExpressionTransformer, AstValue } from "./optimizer/types"; import { StandardOptimizer } from "./optimizer/standardOptimizer"; import { getStaticConstant, @@ -125,12 +122,11 @@ function ensureMethodArity( } } - export function evalUnaryOp( op: AstUnaryOperation, valOperand: Value, operandLoc: SrcInfo = dummySrcInfo, - source: SrcInfo = dummySrcInfo + source: SrcInfo = dummySrcInfo, ): Value { switch (op) { case "+": @@ -182,12 +178,7 @@ function partiallyEvalUnaryOp( 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); } else { @@ -244,7 +235,7 @@ export function evalBinaryOp( valRight: Value, locLeft: SrcInfo = dummySrcInfo, locRight: SrcInfo = dummySrcInfo, - source: SrcInfo = dummySrcInfo + source: SrcInfo = dummySrcInfo, ): Value { switch (op) { case "+": diff --git a/src/grammar/ast.ts b/src/grammar/ast.ts index 8a2d13583..e162193a7 100644 --- a/src/grammar/ast.ts +++ b/src/grammar/ast.ts @@ -700,48 +700,60 @@ export function eqExpressions( case "id": return eqNames(ast1, ast2 as AstId); case "method_call": - return ( - eqNames(ast1.method, (ast2 as AstMethodCall).method) && - eqExpressions(ast1.self, (ast2 as AstMethodCall).self) && - eqExpressionArrays(ast1.args, (ast2 as AstMethodCall).args) - ); + return ( + eqNames(ast1.method, (ast2 as AstMethodCall).method) && + eqExpressions(ast1.self, (ast2 as AstMethodCall).self) && + eqExpressionArrays(ast1.args, (ast2 as AstMethodCall).args) + ); case "init_of": - return ( - eqNames(ast1.contract, (ast2 as AstInitOf).contract) && - eqExpressionArrays(ast1.args, (ast2 as AstInitOf).args) - ); + return ( + eqNames(ast1.contract, (ast2 as AstInitOf).contract) && + eqExpressionArrays(ast1.args, (ast2 as AstInitOf).args) + ); case "op_unary": - return ( - ast1.op === (ast2 as AstOpUnary).op && - eqExpressions(ast1.operand, (ast2 as AstOpUnary).operand) - ); + return ( + ast1.op === (ast2 as AstOpUnary).op && + eqExpressions(ast1.operand, (ast2 as AstOpUnary).operand) + ); case "op_binary": - return ( - ast1.op === (ast2 as AstOpBinary).op && - eqExpressions(ast1.left, (ast2 as AstOpBinary).left) && - eqExpressions(ast1.right, (ast2 as AstOpBinary).right) - ); + return ( + ast1.op === (ast2 as AstOpBinary).op && + eqExpressions(ast1.left, (ast2 as AstOpBinary).left) && + eqExpressions(ast1.right, (ast2 as AstOpBinary).right) + ); case "conditional": - return ( - eqExpressions(ast1.condition, (ast2 as AstConditional).condition) && - eqExpressions(ast1.thenBranch, (ast2 as AstConditional).thenBranch) && - eqExpressions(ast1.elseBranch, (ast2 as AstConditional).elseBranch) - ); + return ( + eqExpressions( + ast1.condition, + (ast2 as AstConditional).condition, + ) && + eqExpressions( + ast1.thenBranch, + (ast2 as AstConditional).thenBranch, + ) && + eqExpressions( + ast1.elseBranch, + (ast2 as AstConditional).elseBranch, + ) + ); case "struct_instance": - return ( - eqNames(ast1.type, (ast2 as AstStructInstance).type) && - eqParameterArrays(ast1.args, (ast2 as AstStructInstance).args) - ); + return ( + eqNames(ast1.type, (ast2 as AstStructInstance).type) && + eqParameterArrays(ast1.args, (ast2 as AstStructInstance).args) + ); case "field_access": - return ( - eqNames(ast1.field, (ast2 as AstFieldAccess).field) && - eqExpressions(ast1.aggregate, (ast2 as AstFieldAccess).aggregate) - ); + return ( + eqNames(ast1.field, (ast2 as AstFieldAccess).field) && + eqExpressions( + ast1.aggregate, + (ast2 as AstFieldAccess).aggregate, + ) + ); case "static_call": - return ( - eqNames(ast1.function, (ast2 as AstStaticCall).function) && - eqExpressionArrays(ast1.args, (ast2 as AstStaticCall).args) - ); + return ( + eqNames(ast1.function, (ast2 as AstStaticCall).function) && + eqExpressionArrays(ast1.args, (ast2 as AstStaticCall).args) + ); } } diff --git a/src/optimizer/associative.ts b/src/optimizer/associative.ts index 0ade5c828..5e9a32196 100644 --- a/src/optimizer/associative.ts +++ b/src/optimizer/associative.ts @@ -27,7 +27,7 @@ export abstract class AssociativeRewriteRule extends Rule { constructor() { super(); - + // + associates with these on the right: // i.e., all op \in plusAssoc. (a + b) op c = a + (b op c) const additiveAssoc: Set = new Set(["+", "-"]); diff --git a/src/optimizer/standardOptimizer.ts b/src/optimizer/standardOptimizer.ts index 045dde87d..a623679a0 100644 --- a/src/optimizer/standardOptimizer.ts +++ b/src/optimizer/standardOptimizer.ts @@ -14,7 +14,7 @@ export class StandardOptimizer extends ExpressionTransformer { constructor() { super(); - + this.rules = [ { priority: 0, rule: new AssociativeRule1() }, { priority: 1, rule: new AssociativeRule2() }, diff --git a/src/optimizer/types.ts b/src/optimizer/types.ts index 713be2ec6..62f1a8dd4 100644 --- a/src/optimizer/types.ts +++ b/src/optimizer/types.ts @@ -1,10 +1,9 @@ -import { Interval } from "ohm-js"; import { AstExpression, AstNumber, AstBoolean, AstNull, - AstString + AstString, } from "../grammar/ast"; export type AstValue = AstNumber | AstBoolean | AstNull | AstString; diff --git a/src/optimizer/util.ts b/src/optimizer/util.ts index 030900fe6..cf8315f3c 100644 --- a/src/optimizer/util.ts +++ b/src/optimizer/util.ts @@ -2,7 +2,7 @@ import { AstExpression, AstUnaryOperation, AstBinaryOperation, - createAstNode + createAstNode, } from "../grammar/ast"; import { dummySrcInfo } from "../grammar/grammar"; import { Value } from "../types/types"; @@ -165,4 +165,3 @@ export function sign(a: bigint): bigint { export function modFloor(a: bigint, b: bigint): bigint { return a - divFloor(a, b) * b; } - From 45289f8609ab949c49248b554292c3352d54630f Mon Sep 17 00:00:00 2001 From: jeshecdom Date: Fri, 12 Jul 2024 19:07:48 +0200 Subject: [PATCH 10/14] - Added try/catch in switch of eqExpressions in ast.ts. - Added test cases for eqExpressions function in ast.ts. - Changed if statement for conditional expressions in optimizer/util.ts. - Changed lookupID for lookupName and evalX for fullyEvalX in constEval.ts. --- src/constEval.ts | 46 ++- src/grammar/ast.ts | 6 + src/optimizer/util.ts | 12 +- src/test/e2e-emulated/expr-equality.spec.ts | 367 ++++++++++++++++++++ 4 files changed, 403 insertions(+), 28 deletions(-) create mode 100644 src/test/e2e-emulated/expr-equality.spec.ts diff --git a/src/constEval.ts b/src/constEval.ts index 3daa50b73..a82da017d 100644 --- a/src/constEval.ts +++ b/src/constEval.ts @@ -368,7 +368,9 @@ export function evalBinaryOp( } } -function evalConditional( +// In the process of writing a partiallyEval version of this +// function for the partial evaluator +function fullyEvalConditional( condition: AstExpression, thenBranch: AstExpression, elseBranch: AstExpression, @@ -386,7 +388,9 @@ function evalConditional( } } -function evalStructInstance( +// In the process of writing a partiallyEval version of this +// function for the partial evaluator +function fullyEvalStructInstance( structTypeId: AstId, structFields: AstStructFieldInitializer[], ctx: CompilerContext, @@ -403,7 +407,9 @@ function evalStructInstance( ); } -function evalFieldAccess( +// In the process of writing a partiallyEval version of this +// function for the partial evaluator +function fullyEvalFieldAccess( structExpr: AstExpression, fieldId: AstId, source: SrcInfo, @@ -456,7 +462,9 @@ function evalFieldAccess( } } -function evalMethod( +// In the process of writing a partiallyEval version of this +// function for the partial evaluator +function fullyEvalMethod( methodName: AstId, object: AstExpression, args: AstExpression[], @@ -480,7 +488,9 @@ function evalMethod( } } -function evalBuiltins( +// In the process of writing a partiallyEval version of this +// function for the partial evaluator +function fullyEvalBuiltins( builtinName: AstId, args: AstExpression[], source: SrcInfo, @@ -702,7 +712,7 @@ function interpretEscapeSequences(stringLiteral: string) { ); } -function lookupID(ast: AstId, ctx: CompilerContext): Value { +function lookupName(ast: AstId, ctx: CompilerContext): Value { if (hasStaticConstant(ctx, ast.text)) { const constant = getStaticConstant(ctx, ast.text); if (constant.value !== undefined) { @@ -723,9 +733,9 @@ export function evalConstantExpression( ): Value { switch (ast.kind) { case "id": - return lookupID(ast, ctx); + return lookupName(ast, ctx); case "method_call": - return evalMethod(ast.method, ast.self, ast.args, ast.loc, ctx); + return fullyEvalMethod(ast.method, ast.self, ast.args, ast.loc, ctx); case "init_of": throwNonFatalErrorConstEval( "initOf is not supported at this moment", @@ -745,18 +755,18 @@ export function evalConstantExpression( case "op_binary": return fullyEvalBinaryOp(ast.op, ast.left, ast.right, ast.loc, ctx); case "conditional": - return evalConditional( + return fullyEvalConditional( ast.condition, ast.thenBranch, ast.elseBranch, ctx, ); case "struct_instance": - return evalStructInstance(ast.type, ast.args, ctx); + return fullyEvalStructInstance(ast.type, ast.args, ctx); case "field_access": - return evalFieldAccess(ast.aggregate, ast.field, ast.loc, ctx); + return fullyEvalFieldAccess(ast.aggregate, ast.field, ast.loc, ctx); case "static_call": - return evalBuiltins(ast.function, ast.args, ast.loc, ctx); + return fullyEvalBuiltins(ast.function, ast.args, ast.loc, ctx); } } @@ -767,7 +777,7 @@ export function partiallyEvalExpression( switch (ast.kind) { case "id": try { - return makeValueExpression(lookupID(ast, ctx)); + return makeValueExpression(lookupName(ast, ctx)); } catch (e) { if (e instanceof TactConstEvalError) { if (!e.fatal) { @@ -780,7 +790,7 @@ export function partiallyEvalExpression( case "method_call": // Does not partially evaluate at the moment. Will attempt to fully evaluate return makeValueExpression( - evalMethod(ast.method, ast.self, ast.args, ast.loc, ctx), + fullyEvalMethod(ast.method, ast.self, ast.args, ast.loc, ctx), ); case "init_of": throwNonFatalErrorConstEval( @@ -811,7 +821,7 @@ export function partiallyEvalExpression( case "conditional": // Does not partially evaluate at the moment. Will attempt to fully evaluate return makeValueExpression( - evalConditional( + fullyEvalConditional( ast.condition, ast.thenBranch, ast.elseBranch, @@ -821,17 +831,17 @@ export function partiallyEvalExpression( case "struct_instance": // Does not partially evaluate at the moment. Will attempt to fully evaluate return makeValueExpression( - evalStructInstance(ast.type, ast.args, ctx), + fullyEvalStructInstance(ast.type, ast.args, ctx), ); case "field_access": // Does not partially evaluate at the moment. Will attempt to fully evaluate return makeValueExpression( - evalFieldAccess(ast.aggregate, ast.field, ast.loc, ctx), + fullyEvalFieldAccess(ast.aggregate, ast.field, ast.loc, ctx), ); case "static_call": // Does not partially evaluate at the moment. Will attempt to fully evaluate return makeValueExpression( - evalBuiltins(ast.function, ast.args, ast.loc, ctx), + fullyEvalBuiltins(ast.function, ast.args, ast.loc, ctx), ); } } diff --git a/src/grammar/ast.ts b/src/grammar/ast.ts index e162193a7..018e5d920 100644 --- a/src/grammar/ast.ts +++ b/src/grammar/ast.ts @@ -688,6 +688,7 @@ export function eqExpressions( return false; } + try { switch (ast1.kind) { case "null": return true; @@ -755,6 +756,11 @@ export function eqExpressions( eqExpressionArrays(ast1.args, (ast2 as AstStaticCall).args) ); } +} catch (e) { + // In principle, the assertions "as Ast___" should not fail + // because ast1 and ast2 have the same kind inside the switch. + return false; +} } function eqParameters( diff --git a/src/optimizer/util.ts b/src/optimizer/util.ts index cf8315f3c..69bffd20d 100644 --- a/src/optimizer/util.ts +++ b/src/optimizer/util.ts @@ -120,22 +120,14 @@ export function checkIsBinaryOpNode(ast: AstExpression): boolean { // with a non-value node on the left and // value node on the right export function checkIsBinaryOp_NonValue_Value(ast: AstExpression): boolean { - if (ast.kind === "op_binary") { - return !isValue(ast.left) && isValue(ast.right); - } else { - return false; - } + return ast.kind === "op_binary"? !isValue(ast.left) && isValue(ast.right) : false; } // Checks if top level node is a binary op node // with a value node on the left and // non-value node on the right export function checkIsBinaryOp_Value_NonValue(ast: AstExpression): boolean { - if (ast.kind === "op_binary") { - return isValue(ast.left) && !isValue(ast.right); - } else { - return false; - } + return ast.kind === "op_binary" ? isValue(ast.left) && !isValue(ast.right) : false; } // bigint arithmetic diff --git a/src/test/e2e-emulated/expr-equality.spec.ts b/src/test/e2e-emulated/expr-equality.spec.ts new file mode 100644 index 000000000..7c70b6322 --- /dev/null +++ b/src/test/e2e-emulated/expr-equality.spec.ts @@ -0,0 +1,367 @@ +import { __DANGER_resetNodeId, eqExpressions } from "../../grammar/ast"; +import { parseExpression } from "../../grammar/grammar"; + +type Test = {expr1: string, expr2: string, equality: boolean}; + +const valueExpressions: Test[] = [ + {expr1: "1", expr2: "1", equality: true}, + {expr1: "1", expr2: "true", equality: false}, + {expr1: "1", expr2: "\"one\"", equality: false}, + {expr1: "1", expr2: "null", equality: false}, + {expr1: "1", expr2: "g", equality: false}, + {expr1: "false", expr2: "true", equality: false}, + {expr1: "false", expr2: "\"false\"", equality: false}, + {expr1: "false", expr2: "false", equality: true}, + {expr1: "false", expr2: "null", equality: false}, + {expr1: "false", expr2: "g", equality: false}, + {expr1: "\"one\"", expr2: "\"one\"", equality: true}, + {expr1: "\"one\"", expr2: "\"onw\"", equality: false}, + {expr1: "\"one\"", expr2: "null", equality: false}, + {expr1: "\"one\"", expr2: "g", equality: false}, + {expr1: "null", expr2: "null", equality: true}, + {expr1: "null", expr2: "g", equality: false}, +]; + +const functionCallExpressions: Test[] = [ + {expr1: "f(1,4)", expr2: "f(1)", equality: false}, + {expr1: "f(1,4)", expr2: "f(1,4)", equality: true}, + {expr1: "f(1,4)", expr2: "1", equality: false}, + {expr1: "f(1,4)", expr2: "g(1,4)", equality: false}, + {expr1: "f(1,4)", expr2: "true", equality: false}, + {expr1: "f(1,4)", expr2: "null", equality: false}, + {expr1: "f(1,4)", expr2: "f", equality: false}, + {expr1: "f(\"a\",0)", expr2: "f(\"a\",0)", equality: true}, + {expr1: "f(\"a\",0)", expr2: "f(\"a\",null)", equality: false}, + {expr1: "f(true,0)", expr2: "f(0,true)", equality: false}, + {expr1: "f(true,0)", expr2: "f(true,0)", equality: true}, + {expr1: "f(g(1))", expr2: "g(f(1))", equality: false}, + + {expr1: "s.f(1,4)", expr2: "s.f(1)", equality: false}, + {expr1: "s.f(1,4)", expr2: "s.f(1,4)", equality: true}, + {expr1: "s.f(1,4)", expr2: "1", equality: false}, + {expr1: "s.f(1,4)", expr2: "s.g(1,4)", equality: false}, + {expr1: "s.f(1,4)", expr2: "true", equality: false}, + {expr1: "s.f(1,4)", expr2: "null", equality: false}, + {expr1: "s.f(\"a\",0)", expr2: "s.f(\"a\",0)", equality: true}, + {expr1: "s.f(\"a\",0)", expr2: "s.f(\"a\",null)", equality: false}, + {expr1: "s.f(true,0)", expr2: "s.f(0,true)", equality: false}, + {expr1: "s.f(true,0)", expr2: "s.f(true,0)", equality: true}, + {expr1: "s.f(s.g(1))", expr2: "s.g(s.f(1))", equality: false}, + + {expr1: "s.f(0)", expr2: "f(0)", equality: false}, +]; + +const unaryOpExpressions: Test[] = [ + {expr1: "+4", expr2: "+4", equality: true}, + {expr1: "+4", expr2: "-4", equality: false}, + {expr1: "+4", expr2: "!g", equality: false}, + {expr1: "+4", expr2: "!!g", equality: false}, + {expr1: "+4", expr2: "g!!", equality: false}, + {expr1: "+4", expr2: "~g", equality: false}, + {expr1: "-4", expr2: "-4", equality: true}, + {expr1: "-4", expr2: "!g", equality: false}, + {expr1: "-4", expr2: "!!g", equality: false}, + {expr1: "-4", expr2: "g!!", equality: false}, + {expr1: "-4", expr2: "~g", equality: false}, + {expr1: "!g", expr2: "!g", equality: true}, + {expr1: "!g", expr2: "!!g", equality: false}, + {expr1: "!g", expr2: "g!!", equality: false}, + {expr1: "!g", expr2: "~g", equality: false}, + {expr1: "g!!", expr2: "g!!", equality: true}, + {expr1: "g!!", expr2: "~g", equality: false}, + {expr1: "~g", expr2: "~g", equality: true}, +]; + +const binaryOpExpressions: Test[] = [ + {expr1: "g + r", expr2: "g + r", equality: true}, + {expr1: "g + r", expr2: "r + g", equality: false}, + {expr1: "g + r", expr2: "+r", equality: false}, + {expr1: "g + r", expr2: "g - r", equality: false}, + {expr1: "g + r", expr2: "g * r", equality: false}, + {expr1: "g + r", expr2: "g / r", equality: false}, + {expr1: "g + r", expr2: "g % r", equality: false}, + {expr1: "g + r", expr2: "g >> r", equality: false}, + {expr1: "g + r", expr2: "g << r", equality: false}, + {expr1: "g + r", expr2: "g & r", equality: false}, + {expr1: "g + r", expr2: "g | r", equality: false}, + {expr1: "g + r", expr2: "g ^ r", equality: false}, + {expr1: "g + r", expr2: "g != r", equality: false}, + {expr1: "g + r", expr2: "g > r", equality: false}, + {expr1: "g + r", expr2: "g < r", equality: false}, + {expr1: "g + r", expr2: "g >= r", equality: false}, + {expr1: "g + r", expr2: "g <= r", equality: false}, + {expr1: "g + r", expr2: "g == r", equality: false}, + {expr1: "g + r", expr2: "g && r", equality: false}, + {expr1: "g + r", expr2: "g || r", equality: false}, + {expr1: "g - r", expr2: "g - r", equality: true}, + {expr1: "g - r", expr2: "-r", equality: false}, + {expr1: "g - r", expr2: "r - g", equality: false}, + {expr1: "g - r", expr2: "g * r", equality: false}, + {expr1: "g - r", expr2: "g / r", equality: false}, + {expr1: "g - r", expr2: "g % r", equality: false}, + {expr1: "g - r", expr2: "g >> r", equality: false}, + {expr1: "g - r", expr2: "g << r", equality: false}, + {expr1: "g - r", expr2: "g & r", equality: false}, + {expr1: "g - r", expr2: "g | r", equality: false}, + {expr1: "g - r", expr2: "g ^ r", equality: false}, + {expr1: "g - r", expr2: "g != r", equality: false}, + {expr1: "g - r", expr2: "g > r", equality: false}, + {expr1: "g - r", expr2: "g < r", equality: false}, + {expr1: "g - r", expr2: "g >= r", equality: false}, + {expr1: "g - r", expr2: "g <= r", equality: false}, + {expr1: "g - r", expr2: "g == r", equality: false}, + {expr1: "g - r", expr2: "g && r", equality: false}, + {expr1: "g - r", expr2: "g || r", equality: false}, + {expr1: "g * r", expr2: "g * r", equality: true}, + {expr1: "g * r", expr2: "r * g", equality: false}, + {expr1: "g * r", expr2: "g / r", equality: false}, + {expr1: "g * r", expr2: "g % r", equality: false}, + {expr1: "g * r", expr2: "g >> r", equality: false}, + {expr1: "g * r", expr2: "g << r", equality: false}, + {expr1: "g * r", expr2: "g & r", equality: false}, + {expr1: "g * r", expr2: "g | r", equality: false}, + {expr1: "g * r", expr2: "g ^ r", equality: false}, + {expr1: "g * r", expr2: "g != r", equality: false}, + {expr1: "g * r", expr2: "g > r", equality: false}, + {expr1: "g * r", expr2: "g < r", equality: false}, + {expr1: "g * r", expr2: "g >= r", equality: false}, + {expr1: "g * r", expr2: "g <= r", equality: false}, + {expr1: "g * r", expr2: "g == r", equality: false}, + {expr1: "g * r", expr2: "g && r", equality: false}, + {expr1: "g * r", expr2: "g || r", equality: false}, + {expr1: "g / r", expr2: "g / r", equality: true}, + {expr1: "g / r", expr2: "r / g", equality: false}, + {expr1: "g / r", expr2: "g % r", equality: false}, + {expr1: "g / r", expr2: "g >> r", equality: false}, + {expr1: "g / r", expr2: "g << r", equality: false}, + {expr1: "g / r", expr2: "g & r", equality: false}, + {expr1: "g / r", expr2: "g | r", equality: false}, + {expr1: "g / r", expr2: "g ^ r", equality: false}, + {expr1: "g / r", expr2: "g != r", equality: false}, + {expr1: "g / r", expr2: "g > r", equality: false}, + {expr1: "g / r", expr2: "g < r", equality: false}, + {expr1: "g / r", expr2: "g >= r", equality: false}, + {expr1: "g / r", expr2: "g <= r", equality: false}, + {expr1: "g / r", expr2: "g == r", equality: false}, + {expr1: "g / r", expr2: "g && r", equality: false}, + {expr1: "g / r", expr2: "g || r", equality: false}, + {expr1: "g % r", expr2: "g % r", equality: true}, + {expr1: "g % r", expr2: "r % g", equality: false}, + {expr1: "g % r", expr2: "g >> r", equality: false}, + {expr1: "g % r", expr2: "g << r", equality: false}, + {expr1: "g % r", expr2: "g & r", equality: false}, + {expr1: "g % r", expr2: "g | r", equality: false}, + {expr1: "g % r", expr2: "g ^ r", equality: false}, + {expr1: "g % r", expr2: "g != r", equality: false}, + {expr1: "g % r", expr2: "g > r", equality: false}, + {expr1: "g % r", expr2: "g < r", equality: false}, + {expr1: "g % r", expr2: "g >= r", equality: false}, + {expr1: "g % r", expr2: "g <= r", equality: false}, + {expr1: "g % r", expr2: "g == r", equality: false}, + {expr1: "g % r", expr2: "g && r", equality: false}, + {expr1: "g % r", expr2: "g || r", equality: false}, + {expr1: "g >> r", expr2: "g >> r", equality: true}, + {expr1: "g >> r", expr2: "r >> g", equality: false}, + {expr1: "g >> r", expr2: "g << r", equality: false}, + {expr1: "g >> r", expr2: "g & r", equality: false}, + {expr1: "g >> r", expr2: "g | r", equality: false}, + {expr1: "g >> r", expr2: "g ^ r", equality: false}, + {expr1: "g >> r", expr2: "g != r", equality: false}, + {expr1: "g >> r", expr2: "g > r", equality: false}, + {expr1: "g >> r", expr2: "g < r", equality: false}, + {expr1: "g >> r", expr2: "g >= r", equality: false}, + {expr1: "g >> r", expr2: "g <= r", equality: false}, + {expr1: "g >> r", expr2: "g == r", equality: false}, + {expr1: "g >> r", expr2: "g && r", equality: false}, + {expr1: "g >> r", expr2: "g || r", equality: false}, + {expr1: "g << r", expr2: "g << r", equality: true}, + {expr1: "g << r", expr2: "r << g", equality: false}, + {expr1: "g << r", expr2: "g & r", equality: false}, + {expr1: "g << r", expr2: "g | r", equality: false}, + {expr1: "g << r", expr2: "g ^ r", equality: false}, + {expr1: "g << r", expr2: "g != r", equality: false}, + {expr1: "g << r", expr2: "g > r", equality: false}, + {expr1: "g << r", expr2: "g < r", equality: false}, + {expr1: "g << r", expr2: "g >= r", equality: false}, + {expr1: "g << r", expr2: "g <= r", equality: false}, + {expr1: "g << r", expr2: "g == r", equality: false}, + {expr1: "g << r", expr2: "g && r", equality: false}, + {expr1: "g << r", expr2: "g || r", equality: false}, + {expr1: "g & r", expr2: "g & r", equality: true}, + {expr1: "g & r", expr2: "r & g", equality: false}, + {expr1: "g & r", expr2: "g | r", equality: false}, + {expr1: "g & r", expr2: "g ^ r", equality: false}, + {expr1: "g & r", expr2: "g != r", equality: false}, + {expr1: "g & r", expr2: "g > r", equality: false}, + {expr1: "g & r", expr2: "g < r", equality: false}, + {expr1: "g & r", expr2: "g >= r", equality: false}, + {expr1: "g & r", expr2: "g <= r", equality: false}, + {expr1: "g & r", expr2: "g == r", equality: false}, + {expr1: "g & r", expr2: "g && r", equality: false}, + {expr1: "g & r", expr2: "g || r", equality: false}, + {expr1: "g | r", expr2: "g | r", equality: true}, + {expr1: "g | r", expr2: "r | g", equality: false}, + {expr1: "g | r", expr2: "g ^ r", equality: false}, + {expr1: "g | r", expr2: "g != r", equality: false}, + {expr1: "g | r", expr2: "g > r", equality: false}, + {expr1: "g | r", expr2: "g < r", equality: false}, + {expr1: "g | r", expr2: "g >= r", equality: false}, + {expr1: "g | r", expr2: "g <= r", equality: false}, + {expr1: "g | r", expr2: "g == r", equality: false}, + {expr1: "g | r", expr2: "g && r", equality: false}, + {expr1: "g | r", expr2: "g || r", equality: false}, + {expr1: "g ^ r", expr2: "g ^ r", equality: true}, + {expr1: "g ^ r", expr2: "r ^ g", equality: false}, + {expr1: "g ^ r", expr2: "g != r", equality: false}, + {expr1: "g ^ r", expr2: "g > r", equality: false}, + {expr1: "g ^ r", expr2: "g < r", equality: false}, + {expr1: "g ^ r", expr2: "g >= r", equality: false}, + {expr1: "g ^ r", expr2: "g <= r", equality: false}, + {expr1: "g ^ r", expr2: "g == r", equality: false}, + {expr1: "g ^ r", expr2: "g && r", equality: false}, + {expr1: "g ^ r", expr2: "g || r", equality: false}, + {expr1: "g != r", expr2: "g != r", equality: true}, + {expr1: "g != r", expr2: "r != g", equality: false}, + {expr1: "g != r", expr2: "g > r", equality: false}, + {expr1: "g != r", expr2: "g < r", equality: false}, + {expr1: "g != r", expr2: "g >= r", equality: false}, + {expr1: "g != r", expr2: "g <= r", equality: false}, + {expr1: "g != r", expr2: "g == r", equality: false}, + {expr1: "g != r", expr2: "g && r", equality: false}, + {expr1: "g != r", expr2: "g || r", equality: false}, + {expr1: "g > r", expr2: "g > r", equality: true}, + {expr1: "g > r", expr2: "r > g", equality: false}, + {expr1: "g > r", expr2: "g < r", equality: false}, + {expr1: "g > r", expr2: "g >= r", equality: false}, + {expr1: "g > r", expr2: "g <= r", equality: false}, + {expr1: "g > r", expr2: "g == r", equality: false}, + {expr1: "g > r", expr2: "g && r", equality: false}, + {expr1: "g > r", expr2: "g || r", equality: false}, + {expr1: "g < r", expr2: "g < r", equality: true}, + {expr1: "g < r", expr2: "r < g", equality: false}, + {expr1: "g < r", expr2: "g >= r", equality: false}, + {expr1: "g < r", expr2: "g <= r", equality: false}, + {expr1: "g < r", expr2: "g == r", equality: false}, + {expr1: "g < r", expr2: "g && r", equality: false}, + {expr1: "g < r", expr2: "g || r", equality: false}, + {expr1: "g >= r", expr2: "g >= r", equality: true}, + {expr1: "g >= r", expr2: "r >= g", equality: false}, + {expr1: "g >= r", expr2: "g <= r", equality: false}, + {expr1: "g >= r", expr2: "g == r", equality: false}, + {expr1: "g >= r", expr2: "g && r", equality: false}, + {expr1: "g >= r", expr2: "g || r", equality: false}, + {expr1: "g <= r", expr2: "g <= r", equality: true}, + {expr1: "g <= r", expr2: "r <= g", equality: false}, + {expr1: "g <= r", expr2: "g == r", equality: false}, + {expr1: "g <= r", expr2: "g && r", equality: false}, + {expr1: "g <= r", expr2: "g || r", equality: false}, + {expr1: "g == r", expr2: "g == r", equality: true}, + {expr1: "g == r", expr2: "r == g", equality: false}, + {expr1: "g == r", expr2: "g && r", equality: false}, + {expr1: "g == r", expr2: "g || r", equality: false}, + {expr1: "g && r", expr2: "g && r", equality: true}, + {expr1: "g && r", expr2: "r && g", equality: false}, + {expr1: "g && r", expr2: "g || r", equality: false}, + {expr1: "g || r", expr2: "g || r", equality: true}, + {expr1: "g || r", expr2: "r || g", equality: false}, +]; + +const conditionalExpressions: Test[] = [ + {expr1: "g ? a : b", expr2: "g ? a : b", equality: true}, + {expr1: "g ? a : b", expr2: "g ? b : a", equality: false}, + {expr1: "g ? a : b", expr2: "b ? g : a", equality: false}, + {expr1: "g ? a : b", expr2: "b ? a : g", equality: false}, + {expr1: "g ? a : b", expr2: "a ? b : g", equality: false}, + {expr1: "g ? a : b", expr2: "a ? g : b", equality: false}, + {expr1: "g ? a : b", expr2: "g", equality: false}, + {expr1: "g ? a : b", expr2: "b", equality: false}, + {expr1: "g ? a : b", expr2: "a", equality: false}, +]; + +const structExpressions: Test[] = [ + {expr1: "Test {f1: a, f2: b}", expr2: "Test {f1: a, f2: b}", equality: true}, + {expr1: "Test {f1: a, f2: b}", expr2: "Test2 {f1: a, f2: b}", equality: false}, + {expr1: "Test {f1: a, f2: b}", expr2: "Test {f3: a, f2: b}", equality: false}, + {expr1: "Test {f1: a, f2: b}", expr2: "Test {f1: a, f3: b}", equality: false}, + {expr1: "Test {f1: a, f2: b}", expr2: "Test {f1: c, f2: b}", equality: false}, + {expr1: "Test {f1: a, f2: b}", expr2: "Test {f1: a, f2: c}", equality: false}, + {expr1: "Test {f1: a, f2: b}", expr2: "Test {f1: a}", equality: false}, + {expr1: "Test {f1: a, f2: b}", expr2: "Test {f1: a, f2: b, f3: c}", equality: false}, + {expr1: "Test {f1: a, f2: b}", expr2: "Test", equality: false}, + {expr1: "Test {f1: a, f2: b}", expr2: "f1", equality: false}, + {expr1: "Test {f1: a, f2: b}", expr2: "f2", equality: false}, + {expr1: "Test {f1: a, f2: b}", expr2: "a", equality: false}, + {expr1: "Test {f1: a, f2: b}", expr2: "b", equality: false}, +]; + +const fieldAccessExpressions: Test[] = [ + {expr1: "s.a", expr2: "s.a", equality: true}, + {expr1: "s.a", expr2: "s.a(0)", equality: false}, + {expr1: "s.a", expr2: "a(0)", equality: false}, + {expr1: "s.a", expr2: "a", equality: false}, + {expr1: "s.a", expr2: "s.a.a", equality: false}, + {expr1: "s.a", expr2: "Test {a: e1, b: e2}.a", equality: false}, + {expr1: "s.a.a", expr2: "s.a.a", equality: true}, + {expr1: "s.a.a", expr2: "s.a.a(0)", equality: false}, + {expr1: "s.a.a", expr2: "s.a(0)", equality: false}, + {expr1: "s.a.a", expr2: "a(0)", equality: false}, + {expr1: "s.a.a", expr2: "a", equality: false}, + {expr1: "s.a.a", expr2: "Test {a: e1, b: e2}.a", equality: false}, + {expr1: "Test {a: e1, b: e2}.a", expr2: "Test {a: e1, b: e2}.a", equality: true}, + {expr1: "Test {a: e1, b: e2}.a", expr2: "a", equality: false}, + {expr1: "Test {a: e1, b: e2}.a", expr2: "s.a", equality: false}, + {expr1: "Test {a: e1, b: e2}.a", expr2: "s.a(0)", equality: false}, + {expr1: "Test {a: e1, b: e2}.a", expr2: "a(0)", equality: false}, + {expr1: "Test {a: e1, b: e2}.a", expr2: "s.a.a", equality: false}, + {expr1: "Test {a: e1, b: e2}.a", expr2: "Test {a: e1, b: e2}.b", equality: false}, +]; + +function testEquality(expr1: string, expr2: string, equal: boolean) { + expect( + eqExpressions( + parseExpression(expr1), + parseExpression(expr2), + ), + ).toBe(equal); +} + +describe("expression-equality", () => { + beforeEach(() => { + __DANGER_resetNodeId(); + }); + it("should correctly determine if two expressions involving values are equal or not.", () => { + valueExpressions.forEach((test) => { + testEquality(test.expr1, test.expr2, test.equality); + }); + }); + it("should correctly determine if two expressions involving function calls are equal or not.", () => { + functionCallExpressions.forEach((test) => { + testEquality(test.expr1, test.expr2, test.equality); + }); + }); + it("should correctly determine if two expressions involving unary operators are equal or not.", () => { + unaryOpExpressions.forEach((test) => { + testEquality(test.expr1, test.expr2, test.equality); + }); + }); + it("should correctly determine if two expressions involving binary operators are equal or not.", () => { + binaryOpExpressions.forEach((test) => { + testEquality(test.expr1, test.expr2, test.equality); + }); + }); + it("should correctly determine if two expressions involving conditionals are equal or not.", () => { + conditionalExpressions.forEach((test) => { + testEquality(test.expr1, test.expr2, test.equality); + }); + }); + it("should correctly determine if two expressions involving structs are equal or not.", () => { + structExpressions.forEach((test) => { + testEquality(test.expr1, test.expr2, test.equality); + }); + }); + it("should correctly determine if two expressions involving field accesses are equal or not.", () => { + fieldAccessExpressions.forEach((test) => { + testEquality(test.expr1, test.expr2, test.equality); + }); + }); +}); \ No newline at end of file From 9405f981cd056d0bc7cdb2e5d3aca4987587b58f Mon Sep 17 00:00:00 2001 From: jeshecdom Date: Fri, 12 Jul 2024 19:36:59 +0200 Subject: [PATCH 11/14] Forgot to add test cases for initOf. --- src/test/e2e-emulated/expr-equality.spec.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/test/e2e-emulated/expr-equality.spec.ts b/src/test/e2e-emulated/expr-equality.spec.ts index c67c8daff..498a3308a 100644 --- a/src/test/e2e-emulated/expr-equality.spec.ts +++ b/src/test/e2e-emulated/expr-equality.spec.ts @@ -352,6 +352,19 @@ const fieldAccessExpressions: Test[] = [ }, ]; +const initOfExpressions: Test[] = [ + { expr1: "initOf a(b,c,d)", expr2: "initOf a(b,c,d)", equality: true }, + { expr1: "initOf a(b,c,d)", expr2: "initOf g(b,c,d)", equality: false }, + { expr1: "initOf a(b,c,d)", expr2: "initOf a(f,c,d)", equality: false }, + { expr1: "initOf a(b,c,d)", expr2: "initOf a(b,f,d)", equality: false }, + { expr1: "initOf a(b,c,d)", expr2: "initOf a(b,c,f)", equality: false }, + { expr1: "initOf a(b,c,d)", expr2: "initOf a(b)", equality: false }, + { expr1: "initOf a(b,c,d)", expr2: "initOf a(b,c)", equality: false }, + { expr1: "initOf a(b,c,d)", expr2: "initOf a(b,c,d,e)", equality: false }, + { expr1: "initOf a(b,c,d)", expr2: "a(b,c,d)", equality: false }, + { expr1: "initOf a(b,c,d)", expr2: "s.a(b,c,d)", equality: false }, +]; + function testEquality(expr1: string, expr2: string, equal: boolean) { expect(eqExpressions(parseExpression(expr1), parseExpression(expr2))).toBe( equal, @@ -397,4 +410,9 @@ describe("expression-equality", () => { testEquality(test.expr1, test.expr2, test.equality); }); }); + it("should correctly determine if two expressions involving initOf are equal or not.", () => { + initOfExpressions.forEach((test) => { + testEquality(test.expr1, test.expr2, test.equality); + }); + }); }); From e10a838e9b8db11e2094bfb5c65b7346ced24214 Mon Sep 17 00:00:00 2001 From: jeshecdom Date: Fri, 12 Jul 2024 19:54:30 +0200 Subject: [PATCH 12/14] Fixed problems indicated by knit. --- src/optimizer/algebraic.ts | 1 - src/optimizer/associative.ts | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 src/optimizer/algebraic.ts diff --git a/src/optimizer/algebraic.ts b/src/optimizer/algebraic.ts deleted file mode 100644 index 6c9af66d8..000000000 --- a/src/optimizer/algebraic.ts +++ /dev/null @@ -1 +0,0 @@ -// This module will include the simpler algebraic rules (i.e., those not involving associativity) diff --git a/src/optimizer/associative.ts b/src/optimizer/associative.ts index 5e9a32196..cde394811 100644 --- a/src/optimizer/associative.ts +++ b/src/optimizer/associative.ts @@ -16,7 +16,7 @@ import { sign, } from "./util"; -export abstract class AssociativeRewriteRule extends Rule { +abstract class AssociativeRewriteRule extends Rule { // An entry (op, S) in the map means "operator op associates with all operators in set S", // mathematically: all op2 \in S. (a op b) op2 c = a op (b op2 c) private associativeOps: Map>; @@ -73,7 +73,7 @@ export abstract class AssociativeRewriteRule extends Rule { } } -export abstract class AllowableOpRule extends AssociativeRewriteRule { +abstract class AllowableOpRule extends AssociativeRewriteRule { private allowedOps: Set; constructor() { From 89918523672337f5ffc5c6db1dbbd0ff60201e17 Mon Sep 17 00:00:00 2001 From: jeshecdom Date: Mon, 15 Jul 2024 13:48:45 +0200 Subject: [PATCH 13/14] - Moved `isValue` to ast.ts. - Moved AstValue to ast.ts. - Removed try/catch from eqExpressions in ast.ts. - Removed `traverse` from ast.ts since it has been already moved to iterators.ts - Moved expr-equality.spec.ts and partial-eval.spec.ts to src/grammar/test - Added test cases for `isValue' function in ast.ts in src/grammar/test/expr-is-value.spec.ts. --- src/constEval.ts | 5 +- src/grammar/ast.ts | 339 +++++------------- .../test}/expr-equality.spec.ts | 4 +- src/grammar/test/expr-is-value.spec.ts | 72 ++++ .../test}/partial-eval.spec.ts | 11 +- src/optimizer/associative.ts | 11 +- src/optimizer/types.ts | 10 +- src/optimizer/util.ts | 26 +- 8 files changed, 179 insertions(+), 299 deletions(-) rename src/{test/e2e-emulated => grammar/test}/expr-equality.spec.ts (99%) create mode 100644 src/grammar/test/expr-is-value.spec.ts rename src/{test/e2e-emulated => grammar/test}/partial-eval.spec.ts (97%) diff --git a/src/constEval.ts b/src/constEval.ts index da7d8bbe3..cb486fc40 100644 --- a/src/constEval.ts +++ b/src/constEval.ts @@ -11,12 +11,13 @@ import { isSelfId, eqNames, idText, + AstValue, + isValue, } from "./grammar/ast"; import { TactConstEvalError, idTextErr, throwConstEvalError } from "./errors"; import { CommentValue, showValue, StructValue, Value } from "./types/types"; import { sha256_sync } from "@ton/crypto"; import { - isValue, extractValue, makeValueExpression, makeUnaryExpression, @@ -24,7 +25,7 @@ import { divFloor, modFloor, } from "./optimizer/util"; -import { ExpressionTransformer, AstValue } from "./optimizer/types"; +import { ExpressionTransformer } from "./optimizer/types"; import { StandardOptimizer } from "./optimizer/standardOptimizer"; import { getStaticConstant, diff --git a/src/grammar/ast.ts b/src/grammar/ast.ts index 521bf304e..f107a7170 100644 --- a/src/grammar/ast.ts +++ b/src/grammar/ast.ts @@ -569,6 +569,8 @@ export type AstNull = { loc: SrcInfo; }; +export type AstValue = AstNumber | AstBoolean | AstNull | AstString; + export type AstConstantAttribute = | { type: "virtual"; loc: SrcInfo } | { type: "overrides"; loc: SrcInfo } @@ -688,81 +690,72 @@ export function eqExpressions( return false; } - try { - switch (ast1.kind) { - case "null": - return true; - case "boolean": - return ast1.value === (ast2 as AstBoolean).value; - case "number": - return ast1.value === (ast2 as AstNumber).value; - case "string": - return ast1.value === (ast2 as AstString).value; - case "id": - return eqNames(ast1, ast2 as AstId); - case "method_call": - return ( - eqNames(ast1.method, (ast2 as AstMethodCall).method) && - eqExpressions(ast1.self, (ast2 as AstMethodCall).self) && - eqExpressionArrays(ast1.args, (ast2 as AstMethodCall).args) - ); - case "init_of": - return ( - eqNames(ast1.contract, (ast2 as AstInitOf).contract) && - eqExpressionArrays(ast1.args, (ast2 as AstInitOf).args) - ); - case "op_unary": - return ( - ast1.op === (ast2 as AstOpUnary).op && - eqExpressions(ast1.operand, (ast2 as AstOpUnary).operand) - ); - case "op_binary": - return ( - ast1.op === (ast2 as AstOpBinary).op && - eqExpressions(ast1.left, (ast2 as AstOpBinary).left) && - eqExpressions(ast1.right, (ast2 as AstOpBinary).right) - ); - case "conditional": - return ( - eqExpressions( - ast1.condition, - (ast2 as AstConditional).condition, - ) && - eqExpressions( - ast1.thenBranch, - (ast2 as AstConditional).thenBranch, - ) && - eqExpressions( - ast1.elseBranch, - (ast2 as AstConditional).elseBranch, - ) - ); - case "struct_instance": - return ( - eqNames(ast1.type, (ast2 as AstStructInstance).type) && - eqParameterArrays( - ast1.args, - (ast2 as AstStructInstance).args, - ) - ); - case "field_access": - return ( - eqNames(ast1.field, (ast2 as AstFieldAccess).field) && - eqExpressions( - ast1.aggregate, - (ast2 as AstFieldAccess).aggregate, - ) - ); - case "static_call": - return ( - eqNames(ast1.function, (ast2 as AstStaticCall).function) && - eqExpressionArrays(ast1.args, (ast2 as AstStaticCall).args) - ); - } - } catch (e) { - // In principle, the assertions "as Ast___" should not fail - // because ast1 and ast2 have the same kind inside the switch. - return false; + switch (ast1.kind) { + case "null": + return true; + case "boolean": + return ast1.value === (ast2 as AstBoolean).value; + case "number": + return ast1.value === (ast2 as AstNumber).value; + case "string": + return ast1.value === (ast2 as AstString).value; + case "id": + return eqNames(ast1, ast2 as AstId); + case "method_call": + return ( + eqNames(ast1.method, (ast2 as AstMethodCall).method) && + eqExpressions(ast1.self, (ast2 as AstMethodCall).self) && + eqExpressionArrays(ast1.args, (ast2 as AstMethodCall).args) + ); + case "init_of": + return ( + eqNames(ast1.contract, (ast2 as AstInitOf).contract) && + eqExpressionArrays(ast1.args, (ast2 as AstInitOf).args) + ); + case "op_unary": + return ( + ast1.op === (ast2 as AstOpUnary).op && + eqExpressions(ast1.operand, (ast2 as AstOpUnary).operand) + ); + case "op_binary": + return ( + ast1.op === (ast2 as AstOpBinary).op && + eqExpressions(ast1.left, (ast2 as AstOpBinary).left) && + eqExpressions(ast1.right, (ast2 as AstOpBinary).right) + ); + case "conditional": + return ( + eqExpressions( + ast1.condition, + (ast2 as AstConditional).condition, + ) && + eqExpressions( + ast1.thenBranch, + (ast2 as AstConditional).thenBranch, + ) && + eqExpressions( + ast1.elseBranch, + (ast2 as AstConditional).elseBranch, + ) + ); + case "struct_instance": + return ( + eqNames(ast1.type, (ast2 as AstStructInstance).type) && + eqParameterArrays(ast1.args, (ast2 as AstStructInstance).args) + ); + case "field_access": + return ( + eqNames(ast1.field, (ast2 as AstFieldAccess).field) && + eqExpressions( + ast1.aggregate, + (ast2 as AstFieldAccess).aggregate, + ) + ); + case "static_call": + return ( + eqNames(ast1.function, (ast2 as AstStaticCall).function) && + eqExpressionArrays(ast1.args, (ast2 as AstStaticCall).args) + ); } } @@ -810,185 +803,27 @@ function eqExpressionArrays( return true; } -export function traverse(node: AstNode, callback: (node: AstNode) => void) { - callback(node); - - if (node.kind === "module") { - for (const e of node.items) { - traverse(e, callback); - } - } - if (node.kind === "contract") { - for (const e of node.declarations) { - traverse(e, callback); - } - } - if (node.kind === "struct_decl") { - for (const e of node.fields) { - traverse(e, callback); - } - } - if (node.kind === "message_decl") { - for (const e of node.fields) { - traverse(e, callback); - } - } - if (node.kind === "trait") { - for (const e of node.declarations) { - traverse(e, callback); - } - } - - // - // Functions - // - - if (node.kind === "function_def") { - for (const e of node.params) { - traverse(e, callback); - } - for (const e of node.statements) { - traverse(e, callback); - } - } - if (node.kind === "function_decl") { - for (const e of node.params) { - traverse(e, callback); - } - } - if (node.kind === "contract_init") { - for (const e of node.params) { - traverse(e, callback); - } - for (const e of node.statements) { - traverse(e, callback); - } - } - if (node.kind === "receiver") { - for (const e of node.statements) { - traverse(e, callback); - } - } - if (node.kind === "native_function_decl") { - for (const e of node.params) { - traverse(e, callback); - } - } - if (node.kind === "field_decl") { - if (node.initializer) { - traverse(node.initializer, callback); - } - } - if (node.kind === "constant_def") { - traverse(node.initializer, callback); - } +export function isValue(ast: AstExpression): boolean { + switch (ast.kind) { + case "null": + case "boolean": + case "number": + case "string": + return true; - // - // Statements - // + case "struct_instance": + return ast.args.every((arg) => isValue(arg.initializer)); - if (node.kind === "statement_let") { - traverse(node.expression, callback); - } - if (node.kind === "statement_return") { - if (node.expression) { - traverse(node.expression, callback); - } - } - if (node.kind === "statement_expression") { - traverse(node.expression, callback); - } - if (node.kind === "statement_assign") { - traverse(node.path, callback); - traverse(node.expression, callback); - } - if (node.kind === "statement_augmentedassign") { - traverse(node.path, callback); - traverse(node.expression, callback); - } - if (node.kind === "statement_condition") { - traverse(node.condition, callback); - for (const e of node.trueStatements) { - traverse(e, callback); - } - if (node.falseStatements) { - for (const e of node.falseStatements) { - traverse(e, callback); - } - } - if (node.elseif) { - traverse(node.elseif, callback); - } - } - if (node.kind === "statement_while") { - traverse(node.condition, callback); - for (const e of node.statements) { - traverse(e, callback); - } - } - if (node.kind === "statement_until") { - traverse(node.condition, callback); - for (const e of node.statements) { - traverse(e, callback); - } - } - if (node.kind === "statement_repeat") { - traverse(node.iterations, callback); - for (const e of node.statements) { - traverse(e, callback); - } - } - if (node.kind === "statement_try") { - for (const e of node.statements) { - traverse(e, callback); - } - } - if (node.kind === "statement_try_catch") { - for (const e of node.statements) { - traverse(e, callback); - } - for (const e of node.catchStatements) { - traverse(e, callback); - } - } - if (node.kind === "statement_foreach") { - for (const e of node.statements) { - traverse(e, callback); - } - } - if (node.kind === "op_binary") { - traverse(node.left, callback); - traverse(node.right, callback); - } - if (node.kind === "op_unary") { - traverse(node.operand, callback); - } - if (node.kind === "field_access") { - traverse(node.aggregate, callback); - } - if (node.kind === "method_call") { - traverse(node.self, callback); - for (const e of node.args) { - traverse(e, callback); - } - } - if (node.kind === "static_call") { - for (const e of node.args) { - traverse(e, callback); - } - } - if (node.kind === "struct_instance") { - for (const e of node.args) { - traverse(e, callback); - } - } - if (node.kind === "struct_field_initializer") { - traverse(node.initializer, callback); - } - if (node.kind === "conditional") { - traverse(node.condition, callback); - traverse(node.thenBranch, callback); - traverse(node.elseBranch, callback); + case "id": + case "method_call": + case "init_of": + case "op_unary": + case "op_binary": + case "conditional": + case "field_access": + case "static_call": + return false; } } + export { SrcInfo }; diff --git a/src/test/e2e-emulated/expr-equality.spec.ts b/src/grammar/test/expr-equality.spec.ts similarity index 99% rename from src/test/e2e-emulated/expr-equality.spec.ts rename to src/grammar/test/expr-equality.spec.ts index 498a3308a..ff10859c6 100644 --- a/src/test/e2e-emulated/expr-equality.spec.ts +++ b/src/grammar/test/expr-equality.spec.ts @@ -1,5 +1,5 @@ -import { __DANGER_resetNodeId, eqExpressions } from "../../grammar/ast"; -import { parseExpression } from "../../grammar/grammar"; +import { __DANGER_resetNodeId, eqExpressions } from "../ast"; +import { parseExpression } from "../grammar"; type Test = { expr1: string; expr2: string; equality: boolean }; diff --git a/src/grammar/test/expr-is-value.spec.ts b/src/grammar/test/expr-is-value.spec.ts new file mode 100644 index 000000000..f26a23535 --- /dev/null +++ b/src/grammar/test/expr-is-value.spec.ts @@ -0,0 +1,72 @@ +//type Test = { expr: string; isValue: boolean }; + +import { __DANGER_resetNodeId, isValue } from "../ast"; +import { parseExpression } from "../grammar"; + +const valueExpressions: string[] = [ + "1", + "true", + "false", + '"one"', + "null", + "Test {f1: 0, f2: true}", + "Test {f1: 0, f2: true, f3: null}", + "Test {f1: Test2 {c:0}, f2: true}", +]; + +const notValueExpressions: string[] = [ + "g", + "Test {f1: 0, f2: b}", + "Test {f1: a, f2: true}", + "f(1)", + "f(1,4)", + "s.f(1,4)", + "+4", + "-4", + "!true", + "g!!", + "~6", + "0 + 1", + "0 - 1", + "0 * 2", + "1 / 3", + "2 % 4", + "10 >> 2", + "10 << 2", + "10 & 4", + "10 | 4", + "10 ^ 4", + "10 != 4", + "10 > 3", + "10 < 3", + "10 >= 5", + "10 <= 2", + "10 == 7", + "true && false", + "true || false", + "true ? 0 : 1", + "s.a", + "s.a.a", + "Test {a: 0, b: 1}.a", + "initOf a(0,1,null)", +]; + +function testIsValue(expr: string, testResult: boolean) { + expect(isValue(parseExpression(expr))).toBe(testResult); +} + +describe("expression-is-value", () => { + beforeEach(() => { + __DANGER_resetNodeId(); + }); + valueExpressions.forEach((test) => { + it(`should correctly determine that '${test}' is a value expression.`, () => { + testIsValue(test, true); + }); + }); + notValueExpressions.forEach((test) => { + it(`should correctly determine that '${test}' is NOT a value expression.`, () => { + testIsValue(test, false); + }); + }); +}); diff --git a/src/test/e2e-emulated/partial-eval.spec.ts b/src/grammar/test/partial-eval.spec.ts similarity index 97% rename from src/test/e2e-emulated/partial-eval.spec.ts rename to src/grammar/test/partial-eval.spec.ts index a4c1484e3..891ae1566 100644 --- a/src/test/e2e-emulated/partial-eval.spec.ts +++ b/src/grammar/test/partial-eval.spec.ts @@ -1,18 +1,15 @@ import { AstExpression, + AstValue, __DANGER_resetNodeId, cloneAstNode, eqExpressions, -} from "../../grammar/ast"; -import { parseExpression } from "../../grammar/grammar"; -import { - extractValue, isValue, - makeValueExpression, -} from "../../optimizer/util"; +} from "../ast"; +import { parseExpression } from "../grammar"; +import { extractValue, makeValueExpression } from "../../optimizer/util"; import { evalUnaryOp, partiallyEvalExpression } from "../../constEval"; import { CompilerContext } from "../../context"; -import { AstValue } from "../../optimizer/types"; const additiveExpressions = [ { original: "X + 3 + 1", simplified: "X + 4" }, diff --git a/src/optimizer/associative.ts b/src/optimizer/associative.ts index cde394811..f3fa9c578 100644 --- a/src/optimizer/associative.ts +++ b/src/optimizer/associative.ts @@ -1,16 +1,21 @@ // This module includes rules involving associative rewrites of expressions import { evalBinaryOp } from "../constEval"; -import { AstBinaryOperation, AstExpression, AstOpBinary } from "../grammar/ast"; +import { + AstBinaryOperation, + AstExpression, + AstOpBinary, + AstValue, + isValue, +} from "../grammar/ast"; import { Value } from "../types/types"; -import { ExpressionTransformer, Rule, AstValue } from "./types"; +import { ExpressionTransformer, Rule } from "./types"; import { abs, checkIsBinaryOpNode, checkIsBinaryOp_NonValue_Value, checkIsBinaryOp_Value_NonValue, extractValue, - isValue, makeBinaryExpression, makeValueExpression, sign, diff --git a/src/optimizer/types.ts b/src/optimizer/types.ts index 62f1a8dd4..bbd871efb 100644 --- a/src/optimizer/types.ts +++ b/src/optimizer/types.ts @@ -1,12 +1,4 @@ -import { - AstExpression, - AstNumber, - AstBoolean, - AstNull, - AstString, -} from "../grammar/ast"; - -export type AstValue = AstNumber | AstBoolean | AstNull | AstString; +import { AstExpression } from "../grammar/ast"; export abstract class ExpressionTransformer { public abstract applyRules(ast: AstExpression): AstExpression; diff --git a/src/optimizer/util.ts b/src/optimizer/util.ts index 76b2abf09..6b1e49dde 100644 --- a/src/optimizer/util.ts +++ b/src/optimizer/util.ts @@ -3,33 +3,11 @@ import { AstUnaryOperation, AstBinaryOperation, createAstNode, + AstValue, + isValue, } from "../grammar/ast"; import { dummySrcInfo } from "../grammar/grammar"; import { Value } from "../types/types"; -import { AstValue } from "./types"; - -export function isValue(ast: AstExpression): boolean { - switch ( - ast.kind // Missing structs - ) { - case "null": - case "boolean": - case "number": - case "string": - return true; - - case "id": - case "method_call": - case "init_of": - case "op_unary": - case "op_binary": - case "conditional": - case "struct_instance": - case "field_access": - case "static_call": - return false; - } -} export function extractValue(ast: AstValue): Value { switch ( From 607da81863fe553d1ca6e24479ac2b264455dfa4 Mon Sep 17 00:00:00 2001 From: jeshecdom Date: Mon, 15 Jul 2024 14:02:11 +0200 Subject: [PATCH 14/14] Fixed compilation error after merge with main. --- src/constEval.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/constEval.ts b/src/constEval.ts index 38fe5eeb3..612561ac8 100644 --- a/src/constEval.ts +++ b/src/constEval.ts @@ -822,7 +822,10 @@ export function partiallyEvalExpression( return makeValueExpression(ensureInt(ast.value, ast.loc)); case "string": return makeValueExpression( - ensureString(interpretEscapeSequences(ast.value), ast.loc), + ensureString( + interpretEscapeSequences(ast.value, ast.loc), + ast.loc, + ), ); case "op_unary": return partiallyEvalUnaryOp(ast.op, ast.operand, ast.loc, ctx);