diff --git a/bin/tact.js b/bin/tact.js index 148e68c1e..8733ca01c 100755 --- a/bin/tact.js +++ b/bin/tact.js @@ -106,7 +106,7 @@ void meowModule.then( switch (result.kind) { case "ok": { - console.log(result.value); + console.log(main.showValue(result.value)); process.exit(0); } break; diff --git a/src/abi/global.ts b/src/abi/global.ts index 87ef019df..4bd5ff988 100644 --- a/src/abi/global.ts +++ b/src/abi/global.ts @@ -4,19 +4,22 @@ import { writeAddress, writeCell, writeSlice, + writeString, } from "../generator/writers/writeConstant"; -import { - writeExpression, - writeValue, -} from "../generator/writers/writeExpression"; -import { TactConstEvalError, throwCompilationError } from "../errors"; -import { evalConstantExpression } from "../constEval"; +import { writeExpression } from "../generator/writers/writeExpression"; +import { throwCompilationError } from "../errors"; import { getErrorId } from "../types/resolveErrors"; import { AbiFunction } from "./AbiFunction"; import { sha256_sync } from "@ton/crypto"; import path from "path"; import { cwd } from "process"; import { posixNormalize } from "../utils/filePath"; +import { + ensureSimplifiedString, + ensureString, + interpretEscapeSequences, +} from "../interpreter"; +import { isLiteral } from "../grammar/ast"; export const GlobalFunctions: Map = new Map([ [ @@ -52,10 +55,13 @@ export const GlobalFunctions: Map = new Map([ ref, ); } - const str = evalConstantExpression( - resolved[0]!, - ctx.ctx, - ) as string; + const resolved0 = resolved[0]!; + // FIXME: When optimizer step added, change the following line to: + // const str = ensureSimplifiedString(resolved0).value; + const str = interpretEscapeSequences( + ensureString(resolved0).value, + resolved0.loc, + ); return toNano(str).toString(10); }, }, @@ -106,10 +112,13 @@ export const GlobalFunctions: Map = new Map([ ref, ); } - const str = evalConstantExpression( - resolved[1]!, - ctx.ctx, - ) as string; + const resolved1 = resolved[1]!; + // FIXME: When optimizer step added, change the following line to: + // const str = ensureSimplifiedString(resolved1).value; + const str = interpretEscapeSequences( + ensureString(resolved1).value, + resolved1.loc, + ); return `throw_unless(${getErrorId(str, ctx.ctx)}, ${writeExpression(resolved[0]!, ctx)})`; }, }, @@ -147,10 +156,13 @@ export const GlobalFunctions: Map = new Map([ ref, ); } - const str = evalConstantExpression( - resolved[0]!, - ctx.ctx, - ) as string; + const resolved0 = resolved[0]!; + // FIXME: When optimizer step added, change the following line to: + // const str = ensureSimplifiedString(resolved0).value; + const str = interpretEscapeSequences( + ensureString(resolved0).value, + resolved0.loc, + ); let address: Address; try { address = Address.parse(str); @@ -200,10 +212,13 @@ export const GlobalFunctions: Map = new Map([ } // Load cell data - const str = evalConstantExpression( - resolved[0]!, - ctx.ctx, - ) as string; + const resolved0 = resolved[0]!; + // FIXME: When optimizer step added, change the following line to: + // const str = ensureSimplifiedString(resolved0).value; + const str = interpretEscapeSequences( + ensureString(resolved0).value, + resolved0.loc, + ); let c: Cell; try { c = Cell.fromBase64(str); @@ -239,7 +254,9 @@ export const GlobalFunctions: Map = new Map([ : "unknown"; const lineCol = ref.interval.getLineAndColumn(); const debugPrint1 = `File ${filePath}:${lineCol.lineNum}:${lineCol.colNum}:`; - const debugPrint2 = writeValue(ref.interval.contents, ctx); + const contentsId = writeString(ref.interval.contents, ctx); + ctx.used(contentsId); + const debugPrint2 = `${contentsId}()`; if (arg0.kind === "map") { const exp = writeExpression(resolved[0]!, ctx); @@ -360,24 +377,12 @@ export const GlobalFunctions: Map = new Map([ // String case if (arg0.name === "String") { - let str: string | undefined; + const resolved0 = resolved[0]!; - // Try const-eval - try { - str = evalConstantExpression( - resolved[0]!, - ctx.ctx, - ) as string; - } catch (error) { - if ( - !(error instanceof TactConstEvalError) || - error.fatal - ) - throw error; - } - - // If const-eval did succeed - if (str !== undefined) { + if (isLiteral(resolved0)) { + // FIXME: This one does not need fixing, because it is carried out inside a "isLiteral" check. + // Remove this comment once the optimization step is added + const str = ensureSimplifiedString(resolved0).value; return BigInt( "0x" + sha256_sync(str).toString("hex"), ).toString(10); @@ -430,10 +435,13 @@ export const GlobalFunctions: Map = new Map([ } // Load slice data - const str = evalConstantExpression( - resolved[0]!, - ctx.ctx, - ) as string; + const resolved0 = resolved[0]!; + // FIXME: When optimizer step added, change the following line to: + // const str = ensureSimplifiedString(resolved0).value; + const str = interpretEscapeSequences( + ensureString(resolved0).value, + resolved0.loc, + ); let c: Cell; try { c = Cell.fromBase64(str); @@ -482,10 +490,13 @@ export const GlobalFunctions: Map = new Map([ } // Load slice data - const str = evalConstantExpression( - resolved[0]!, - ctx.ctx, - ) as string; + const resolved0 = resolved[0]!; + // FIXME: When optimizer step added, change the following line to: + // const str = ensureSimplifiedString(resolved0).value; + const str = interpretEscapeSequences( + ensureString(resolved0).value, + resolved0.loc, + ); let c: Cell; try { c = beginCell().storeBuffer(Buffer.from(str)).endCell(); @@ -528,10 +539,13 @@ export const GlobalFunctions: Map = new Map([ } // Load slice data - const str = evalConstantExpression( - resolved[0]!, - ctx.ctx, - ) as string; + const resolved0 = resolved[0]!; + // FIXME: When optimizer step added, change the following line to: + // const str = ensureSimplifiedString(resolved0).value; + const str = interpretEscapeSequences( + ensureString(resolved0).value, + resolved0.loc, + ); if (str.length > 32) { throwCompilationError( @@ -573,10 +587,13 @@ export const GlobalFunctions: Map = new Map([ } // Load slice data - const str = evalConstantExpression( - resolved[0]!, - ctx.ctx, - ) as string; + const resolved0 = resolved[0]!; + // FIXME: When optimizer step added, change the following line to: + // const str = ensureSimplifiedString(resolved0).value; + const str = interpretEscapeSequences( + ensureString(resolved0).value, + resolved0.loc, + ); return `"${str}"c`; }, diff --git a/src/constEval.ts b/src/constEval.ts index 630c901a7..37914006e 100644 --- a/src/constEval.ts +++ b/src/constEval.ts @@ -3,12 +3,11 @@ import { AstBinaryOperation, AstExpression, AstUnaryOperation, - AstValue, - isValue, + isLiteral, + AstLiteral, } from "./grammar/ast"; -import { TactConstEvalError } from "./errors"; -import { Value } from "./types/types"; -import { AstUtil, extractValue } from "./optimizer/util"; +import { TactConstEvalError, throwInternalCompilerError } from "./errors"; +import { AstUtil } from "./optimizer/util"; import { ExpressionTransformer } from "./optimizer/types"; import { StandardOptimizer } from "./optimizer/standardOptimizer"; import { @@ -45,21 +44,14 @@ export const getOptimizer = (util: AstUtil) => { ): AstExpression { if (operand.kind === "number" && op === "-") { // emulating negative integer literals - return util.makeValueExpression(ensureInt(-operand.value, source)); + return ensureInt(util.makeNumberLiteral(-operand.value, source)); } const simplOperand = partiallyEvalExpression(operand, ctx); - if (isValue(simplOperand)) { - const valueOperand = extractValue(simplOperand as AstValue); - const result = evalUnaryOp( - op, - valueOperand, - simplOperand.loc, - source, - ); - // Wrap the value into a Tree to continue simplifications - return util.makeValueExpression(result); + if (isLiteral(simplOperand)) { + const result = evalUnaryOp(op, simplOperand, source, util); + return result; } else { const newAst = util.makeUnaryExpression(op, simplOperand); return optimizer.applyRules(newAst); @@ -75,27 +67,25 @@ export const getOptimizer = (util: AstUtil) => { ): AstExpression { const leftOperand = partiallyEvalExpression(left, ctx); - if (isValue(leftOperand)) { + if (isLiteral(leftOperand)) { // Because of short-circuiting, we must delay evaluation of the right operand - const valueLeftOperand = extractValue(leftOperand as AstValue); - try { const result = evalBinaryOp( op, - valueLeftOperand, + leftOperand, // We delay the evaluation of the right operand inside a continuation () => { const rightOperand = partiallyEvalExpression( right, ctx, ); - if (isValue(rightOperand)) { - // If the right operand reduces to a value, then we can let the function - // evalBinaryOp finish its normal execution by returning the value - // in the right operand. - return extractValue(rightOperand as AstValue); + if (isLiteral(rightOperand)) { + // If the right operand reduced to a value, then we can let the function + // evalBinaryOp finish its normal execution by returning the + // right operand. + return rightOperand; } else { - // If the right operand does not reduce to a value, + // If the right operand does not reduce to a value,< // we interrupt the execution of the evalBinaryOp function // by returning an exception with the partially evaluated right operand. // The simplification rules will handle the partially evaluated tree in the catch @@ -103,12 +93,11 @@ export const getOptimizer = (util: AstUtil) => { throw new PartiallyEvaluatedTree(rightOperand); } }, - leftOperand.loc, - right.loc, source, + util, ); - return util.makeValueExpression(result); + return result; } catch (e) { if (e instanceof PartiallyEvaluatedTree) { // The right operand did not evaluate to a value. Hence, @@ -142,13 +131,11 @@ export const getOptimizer = (util: AstUtil) => { ctx: CompilerContext, interpreterConfig?: InterpreterConfig, ): AstExpression { - const interpreter = new Interpreter(ctx, interpreterConfig); + const interpreter = new Interpreter(util, ctx, interpreterConfig); switch (ast.kind) { case "id": try { - return util.makeValueExpression( - interpreter.interpretName(ast), - ); + return interpreter.interpretName(ast); } catch (e) { if (e instanceof TactConstEvalError) { if (!e.fatal) { @@ -160,9 +147,7 @@ export const getOptimizer = (util: AstUtil) => { } case "method_call": // Does not partially evaluate at the moment. Will attempt to fully evaluate - return util.makeValueExpression( - interpreter.interpretMethodCall(ast), - ); + return interpreter.interpretMethodCall(ast); case "init_of": throwNonFatalErrorConstEval( "initOf is not supported at this moment", @@ -174,13 +159,21 @@ export const getOptimizer = (util: AstUtil) => { case "boolean": return ast; case "number": - return util.makeValueExpression( - interpreter.interpretNumber(ast), - ); + return interpreter.interpretNumber(ast); case "string": - return util.makeValueExpression( - interpreter.interpretString(ast), - ); + return interpreter.interpretString(ast); + case "comment_value": + return ast; + case "simplified_string": + return ast; + case "struct_value": + return ast; + case "address": + return ast; + case "cell": + return ast; + case "slice": + return ast; case "op_unary": return partiallyEvalUnaryOp(ast.op, ast.operand, ast.loc, ctx); case "op_binary": @@ -193,24 +186,18 @@ export const getOptimizer = (util: AstUtil) => { ); case "conditional": // Does not partially evaluate at the moment. Will attempt to fully evaluate - return util.makeValueExpression( - interpreter.interpretConditional(ast), - ); + return interpreter.interpretConditional(ast); case "struct_instance": // Does not partially evaluate at the moment. Will attempt to fully evaluate - return util.makeValueExpression( - interpreter.interpretStructInstance(ast), - ); + return interpreter.interpretStructInstance(ast); case "field_access": // Does not partially evaluate at the moment. Will attempt to fully evaluate - return util.makeValueExpression( - interpreter.interpretFieldAccess(ast), - ); + return interpreter.interpretFieldAccess(ast); case "static_call": // Does not partially evaluate at the moment. Will attempt to fully evaluate - return util.makeValueExpression( - interpreter.interpretStaticCall(ast), - ); + return interpreter.interpretStaticCall(ast); + default: + throwInternalCompilerError("Unrecognized expression kind"); } } @@ -224,9 +211,10 @@ export const getOptimizer = (util: AstUtil) => { export function evalConstantExpression( ast: AstExpression, ctx: CompilerContext, + util: AstUtil, interpreterConfig?: InterpreterConfig, -): Value { - const interpreter = new Interpreter(ctx, interpreterConfig); +): AstLiteral { + const interpreter = new Interpreter(util, ctx, interpreterConfig); const result = interpreter.interpretExpression(ast); return result; } diff --git a/src/generator/writers/writeExpression.spec.ts b/src/generator/writers/writeExpression.spec.ts index 2b12b8d95..b7b2c0caa 100644 --- a/src/generator/writers/writeExpression.spec.ts +++ b/src/generator/writers/writeExpression.spec.ts @@ -79,7 +79,7 @@ describe("writeExpression", () => { getParser(ast, defaultParser), ); ctx = resolveDescriptors(ctx, ast); - ctx = resolveStatements(ctx); + ctx = resolveStatements(ctx, ast); const main = getStaticFunction(ctx, "main"); if (main.ast.kind !== "function_def") { throw Error("Unexpected function kind"); diff --git a/src/generator/writers/writeExpression.ts b/src/generator/writers/writeExpression.ts index 398eba4c4..193ac03d8 100644 --- a/src/generator/writers/writeExpression.ts +++ b/src/generator/writers/writeExpression.ts @@ -1,7 +1,9 @@ import { AstExpression, AstId, + AstLiteral, eqNames, + getAstFactory, idText, tryExtractPath, } from "../../grammar/ast"; @@ -9,6 +11,7 @@ import { idTextErr, TactConstEvalError, throwCompilationError, + throwInternalCompilerError, } from "../../errors"; import { getExpType } from "../../types/resolveExpression"; import { @@ -21,8 +24,6 @@ import { FieldDescription, printTypeRef, TypeDescription, - CommentValue, - Value, } from "../../types/types"; import { WriterContext } from "../Writer"; import { resolveFuncTypeUnpack } from "./resolveFuncTypeUnpack"; @@ -31,7 +32,6 @@ import { GlobalFunctions } from "../../abi/global"; import { funcIdOf } from "./id"; import { StructFunctions } from "../../abi/struct"; import { resolveFuncType } from "./resolveFuncType"; -import { Address, Cell, Slice } from "@ton/core"; import { writeAddress, writeCell, @@ -41,8 +41,9 @@ import { } from "./writeConstant"; import { ops } from "./ops"; import { writeCastedExpression } from "./writeFunction"; -import { evalConstantExpression } from "../../constEval"; import { isLvalue } from "../../types/resolveStatements"; +import { evalConstantExpression } from "../../constEval"; +import { getAstUtil } from "../../optimizer/util"; function isNull(wCtx: WriterContext, expr: AstExpression): boolean { return getExpType(wCtx.ctx, expr).kind === "null"; @@ -98,69 +99,72 @@ function writeStructConstructor( return name; } -export function writeValue(val: Value, wCtx: WriterContext): string { - if (typeof val === "bigint") { - return val.toString(10); - } - if (typeof val === "string") { - const id = writeString(val, wCtx); - wCtx.used(id); - return `${id}()`; - } - if (typeof val === "boolean") { - return val ? "true" : "false"; - } - if (Address.isAddress(val)) { - const res = writeAddress(val, wCtx); - wCtx.used(res); - return res + "()"; - } - if (val instanceof Cell) { - const res = writeCell(val, wCtx); - wCtx.used(res); - return `${res}()`; - } - if (val instanceof Slice) { - const res = writeSlice(val, wCtx); - wCtx.used(res); - return `${res}()`; - } - if (val === null) { - return "null()"; - } - if (val instanceof CommentValue) { - const id = writeComment(val.comment, wCtx); - wCtx.used(id); - return `${id}()`; - } - if (typeof val === "object" && "$tactStruct" in val) { - // this is a struct value - const structDescription = getType( - wCtx.ctx, - val["$tactStruct"] as string, - ); - const fields = structDescription.fields.map((field) => field.name); - const id = writeStructConstructor(structDescription, fields, wCtx); - wCtx.used(id); - const fieldValues = structDescription.fields.map((field) => { - if (field.name in val) { - if (field.type.kind === "ref" && field.type.optional) { - const ft = getType(wCtx.ctx, field.type.name); - if (ft.kind === "struct" && val[field.name] !== null) { - return `${ops.typeAsOptional(ft.name, wCtx)}(${writeValue(val[field.name]!, wCtx)})`; +export function writeValue(val: AstLiteral, wCtx: WriterContext): string { + switch (val.kind) { + case "number": + return val.value.toString(10); + case "simplified_string": { + const id = writeString(val.value, wCtx); + wCtx.used(id); + return `${id}()`; + } + case "boolean": + return val.value ? "true" : "false"; + case "address": { + const res = writeAddress(val.value, wCtx); + wCtx.used(res); + return res + "()"; + } + case "cell": { + const res = writeCell(val.value, wCtx); + wCtx.used(res); + return `${res}()`; + } + case "slice": { + const res = writeSlice(val.value, wCtx); + wCtx.used(res); + return `${res}()`; + } + case "null": + return "null()"; + case "comment_value": { + const id = writeComment(val.value, wCtx); + wCtx.used(id); + return `${id}()`; + } + case "struct_value": { + // Transform the struct fields into a map for lookup + const valMap: Map = new Map(); + for (const f of val.args) { + valMap.set(idText(f.field), f.initializer); + } + + const structDescription = getType(wCtx.ctx, val.type); + const fields = structDescription.fields.map((field) => field.name); + const id = writeStructConstructor(structDescription, fields, wCtx); + wCtx.used(id); + const fieldValues = structDescription.fields.map((field) => { + if (valMap.has(field.name)) { + const v = valMap.get(field.name)!; + if (field.type.kind === "ref" && field.type.optional) { + const ft = getType(wCtx.ctx, field.type.name); + if (ft.kind === "struct" && v.kind !== "null") { + return `${ops.typeAsOptional(ft.name, wCtx)}(${writeValue(v, wCtx)})`; + } } + return writeValue(v, wCtx); + } else { + throwInternalCompilerError( + `Struct value is missing a field: ${field.name}`, + val.loc, + ); } - return writeValue(val[field.name]!, wCtx); - } else { - throw Error( - `Struct value is missing a field: ${field.name}`, - val, - ); - } - }); - return `${id}(${fieldValues.join(", ")})`; + }); + return `${id}(${fieldValues.join(", ")})`; + } + default: + throwInternalCompilerError("Unrecognized ast literal kind"); } - throw Error("Invalid value", val); } export function writePathExpression(path: AstId[]): string { @@ -169,12 +173,19 @@ export function writePathExpression(path: AstId[]): string { export function writeExpression(f: AstExpression, wCtx: WriterContext): string { // literals and constant expressions are covered here + + // FIXME: Once optimization step is added, remove this try and replace it with this + // conditional: + // if (isLiteral(f)) { + // return writeValue(f, wCtx); + // } try { + const util = getAstUtil(getAstFactory()); // Let us put a limit of 2 ^ 12 = 4096 iterations on loops to increase compiler responsiveness. // If a loop takes more than such number of iterations, the interpreter will fail evaluation. // I think maxLoopIterations should be a command line option in case a user wants to wait more // during evaluation. - const value = evalConstantExpression(f, wCtx.ctx, { + const value = evalConstantExpression(f, wCtx.ctx, util, { maxLoopIterations: 2n ** 12n, }); return writeValue(value, wCtx); diff --git a/src/grammar/ast.ts b/src/grammar/ast.ts index 6c284068a..bfd460576 100644 --- a/src/grammar/ast.ts +++ b/src/grammar/ast.ts @@ -1,3 +1,5 @@ +import { Address, Cell, Slice } from "@ton/core"; +import { throwInternalCompilerError } from "../errors"; import { dummySrcInfo, SrcInfo } from "./src-info"; export type AstModule = { @@ -381,7 +383,10 @@ export type AstExpression = | AstExpressionPrimary | AstOpBinary | AstOpUnary - | AstConditional; + | AstConditional + // AstLiteral could be added in AstExpressionPrimary, + // but AstExpressionPrimary is planned to be removed. See issue #1290 (https://github.com/tact-lang/tact/issues/1290). + | AstLiteral; export type AstExpressionPrimary = | AstMethodCall @@ -649,7 +654,71 @@ export type AstNull = { loc: SrcInfo; }; -export type AstValue = AstNumber | AstBoolean | AstNull | AstString; +export type AstLiteral = + | AstNumber + | AstBoolean + | AstNull + // An AstSimplifiedString is a string in which escaping characters, like '\\' has been simplified, e.g., '\\' simplified to '\'. + // An AstString is not a literal because it may contain escaping characters that have not been simplified, like '\\'. + // AstSimplifiedString is always produced by the interpreter, never directly by the parser. The parser produces AstStrings, which + // then get transformed into AstSimplifiedString by the interpreter. + | AstSimplifiedString + | AstAddress + | AstCell + | AstSlice + | AstCommentValue + | AstStructValue; + +export type AstSimplifiedString = { + kind: "simplified_string"; + value: string; + id: number; + loc: SrcInfo; +}; + +export type AstAddress = { + kind: "address"; + value: Address; + id: number; + loc: SrcInfo; +}; + +export type AstCell = { + kind: "cell"; + value: Cell; + id: number; + loc: SrcInfo; +}; + +export type AstSlice = { + kind: "slice"; + value: Slice; + id: number; + loc: SrcInfo; +}; + +export type AstCommentValue = { + kind: "comment_value"; + value: string; + id: number; + loc: SrcInfo; +}; + +export type AstStructValue = { + kind: "struct_value"; + type: AstId; + args: AstStructFieldValue[]; + id: number; + loc: SrcInfo; +}; + +export type AstStructFieldValue = { + kind: "struct_field_value"; + field: AstId; + initializer: AstLiteral; + id: number; + loc: SrcInfo; +}; export type AstConstantAttributeName = "virtual" | "override" | "abstract"; @@ -741,6 +810,7 @@ export type AstNode = | AstModule | AstNativeFunctionDecl | AstStructFieldInitializer + | AstStructFieldValue | AstType | AstContractInit | AstReceiver @@ -789,6 +859,9 @@ export const getAstFactory = () => { export type FactoryAst = ReturnType; // Test equality of AstExpressions. +// Note this is syntactical equality of expressions. +// For example, two struct instances are equal if they have the same +// type and same fields in the same order. export function eqExpressions( ast1: AstExpression, ast2: AstExpression, @@ -808,16 +881,37 @@ export function eqExpressions( return ast1.value === (ast2 as AstString).value; case "id": return eqNames(ast1, ast2 as AstId); + case "address": + return ast1.value.equals((ast2 as AstAddress).value); + case "cell": + return ast1.value.equals((ast2 as AstCell).value); + case "slice": + return ast1.value + .asCell() + .equals((ast2 as AstSlice).value.asCell()); + case "comment_value": + return ast1.value === (ast2 as AstCommentValue).value; + case "simplified_string": + return ast1.value === (ast2 as AstSimplifiedString).value; + case "struct_value": + return ( + eqNames(ast1.type, (ast2 as AstStructValue).type) && + eqArrays( + ast1.args, + (ast2 as AstStructValue).args, + eqFieldValues, + ) + ); 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) + eqArrays(ast1.args, (ast2 as AstMethodCall).args, eqExpressions) ); case "init_of": return ( eqNames(ast1.contract, (ast2 as AstInitOf).contract) && - eqExpressionArrays(ast1.args, (ast2 as AstInitOf).args) + eqArrays(ast1.args, (ast2 as AstInitOf).args, eqExpressions) ); case "op_unary": return ( @@ -848,7 +942,11 @@ export function eqExpressions( case "struct_instance": return ( eqNames(ast1.type, (ast2 as AstStructInstance).type) && - eqParameterArrays(ast1.args, (ast2 as AstStructInstance).args) + eqArrays( + ast1.args, + (ast2 as AstStructInstance).args, + eqFieldInitializers, + ) ); case "field_access": return ( @@ -861,12 +959,14 @@ export function eqExpressions( case "static_call": return ( eqNames(ast1.function, (ast2 as AstStaticCall).function) && - eqExpressionArrays(ast1.args, (ast2 as AstStaticCall).args) + eqArrays(ast1.args, (ast2 as AstStaticCall).args, eqExpressions) ); + default: + throwInternalCompilerError("Unrecognized expression kind"); } } -function eqParameters( +function eqFieldInitializers( arg1: AstStructFieldInitializer, arg2: AstStructFieldInitializer, ): boolean { @@ -876,16 +976,27 @@ function eqParameters( ); } -function eqParameterArrays( - arr1: AstStructFieldInitializer[], - arr2: AstStructFieldInitializer[], +function eqFieldValues( + arg1: AstStructFieldValue, + arg2: AstStructFieldValue, +): boolean { + return ( + eqNames(arg1.field, arg2.field) && + eqExpressions(arg1.initializer, arg2.initializer) + ); +} + +function eqArrays( + arr1: T[], + arr2: T[], + eqElements: (elem1: T, elem2: T) => boolean, ): boolean { if (arr1.length !== arr2.length) { return false; } for (let i = 0; i < arr1.length; i++) { - if (!eqParameters(arr1[i]!, arr2[i]!)) { + if (!eqElements(arr1[i]!, arr2[i]!)) { return false; } } @@ -893,34 +1004,110 @@ function eqParameterArrays( return true; } -function eqExpressionArrays( - arr1: AstExpression[], - arr2: AstExpression[], -): boolean { - if (arr1.length !== arr2.length) { - return false; - } +/* +Functions that return guard types like "ast is AstLiteral" are unsafe to use in production code. +But there is a way to make them safe by introducing an intermediate function, like +the "checkLiteral" function defined below after "isLiteral". In principle, it is possible to use "checkLiteral" +directly in the code (which avoids the guard type altogether), but it produces code that reduces readability significantly. + +The pattern shown with "isLiteral" and "checkLiteral" can be generalized to other functions that produce a guard type +based on a decision of several cases. +For example, if we have the following function, where we assume that B is a subtype of A: + +function isB(d: A): d is B { + if (cond1(d)) { // It is assumed that cond1(d) determines d to be of type B inside the if + return true; + } else if (cond2(d)) { // It is assumed that cond2(d) determines d to be of type A but not of type B inside the if + return false; + } else if (cond3(d)) { // It is assumed that cond3(d) determines d to be of type B inside the if + return true; + } else { // It is assumed that d is of type A but not of type B inside the else + return false; + } +} - for (let i = 0; i < arr1.length; i++) { - if (!eqExpressions(arr1[i]!, arr2[i]!)) { - return false; - } - } +We can introduce a "checkB" function as follows: + +function checkB(d: A, t: (arg: B) => T, f: (arg: Exclude) => T): T { + if (cond1(d)) { + return t(d); + } else if (cond2(d)) { + return f(d); + } else if (cond3(d)) { + return t(d); + } else { + return f(d); + } +} - return true; +Here, all the "true" cases return t(d) and all the "false" cases return f(d). The names of the functions t and f help remember +that they correspond to the true and false cases, respectively. Observe that cond1(d) and cond3(d) determine the type of +d to be B, which means we can pass d to the t function. For the false cases, the type of d is determined to be +A but not B, which means we can pass d to function f, because f's argument type Exclude states +that the argument must be of type A but not of type B, i.e., of type "A - B" if we see the types as sets. + +checkB is safe because the compiler will complain if, for example, we use t(d) in the else case: + +function checkB(d: A, t: (arg: B) => T, f: (arg: Exclude) => T): T { + if (cond1(d)) { + return t(d); + } else if (cond2(d)) { + return f(d); + } else if (cond3(d)) { + return t(d); + } else { + return t(d); // Compiler will signal an error that d is not assignable to type B + } +} + +Contrary to the original function, where the compiler remains silent if we incorrectly return true in the else: + +function isB(d: A): d is B { + if (cond1(d)) { + return true; + } else if (cond2(d)) { + return false; + } else if (cond3(d)) { + return true; + } else { + return true; // Wrong, but compiler remains silent + } +} + +After we have our "checkB" function, we can define the "isB" function simply as: + +function isB(d: A): d is B { + return checkB(d, () => true, () => false); +} +*/ + +export function isLiteral(ast: AstExpression): ast is AstLiteral { + return checkLiteral( + ast, + () => true, + () => false, + ); } -export function isValue(ast: AstExpression): boolean { +export function checkLiteral( + ast: AstExpression, + t: (node: AstLiteral) => T, + f: (node: Exclude) => T, +): T { switch (ast.kind) { case "null": case "boolean": case "number": - case "string": - return true; + case "address": + case "cell": + case "slice": + case "comment_value": + case "simplified_string": + case "struct_value": + return t(ast); case "struct_instance": - return ast.args.every((arg) => isValue(arg.initializer)); - + case "string": case "id": case "method_call": case "init_of": @@ -929,6 +1116,9 @@ export function isValue(ast: AstExpression): boolean { case "conditional": case "field_access": case "static_call": - return false; + return f(ast); + + default: + throwInternalCompilerError("Unrecognized expression kind"); } } diff --git a/src/grammar/iterators.ts b/src/grammar/iterators.ts index 97454ccd5..7dbe015f5 100644 --- a/src/grammar/iterators.ts +++ b/src/grammar/iterators.ts @@ -256,10 +256,20 @@ export function traverse(node: AstNode, callback: (node: AstNode) => void) { traverse(e, callback); }); break; + case "struct_value": + traverse(node.type, callback); + node.args.forEach((e) => { + traverse(e, callback); + }); + break; case "struct_field_initializer": traverse(node.field, callback); traverse(node.initializer, callback); break; + case "struct_field_value": + traverse(node.field, callback); + traverse(node.initializer, callback); + break; case "init_of": traverse(node.contract, callback); node.args.forEach((e) => { @@ -277,6 +287,11 @@ export function traverse(node: AstNode, callback: (node: AstNode) => void) { case "boolean": case "string": case "null": + case "simplified_string": + case "comment_value": + case "address": + case "cell": + case "slice": break; case "typed_parameter": traverse(node.name, callback); diff --git a/src/grammar/test/expr-is-value.spec.ts b/src/grammar/test/expr-is-value.spec.ts index c4b75d3af..5ec49dd7e 100644 --- a/src/grammar/test/expr-is-value.spec.ts +++ b/src/grammar/test/expr-is-value.spec.ts @@ -1,22 +1,23 @@ //type Test = { expr: string; isValue: boolean }; -import { getAstFactory, isValue } from "../ast"; +import { getAstFactory, isLiteral } from "../ast"; import { getParser } from "../"; import { defaultParser } from "../grammar"; -const valueExpressions: string[] = [ - "1", - "true", - "false", +const valueExpressions: string[] = ["1", "true", "false", "null"]; + +const notValueExpressions: string[] = [ + "g", + + // Raw strings are not literals: they need to go through the interpreter to get transformed into simplified strings, which are literals. '"one"', - "null", + + // Even if these three struct instances have literal fields, raw struct instances are not literals because they need to go through + // the interpreter to get transformed into struct values. "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)", @@ -55,7 +56,7 @@ const notValueExpressions: string[] = [ function testIsValue(expr: string, testResult: boolean) { const ast = getAstFactory(); const { parseExpression } = getParser(ast, defaultParser); - expect(isValue(parseExpression(expr))).toBe(testResult); + expect(isLiteral(parseExpression(expr))).toBe(testResult); } describe("expression-is-value", () => { diff --git a/src/interpreter.ts b/src/interpreter.ts index be27bee88..ebc366d88 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -1,4 +1,4 @@ -import { Address, beginCell, BitString, Cell, Slice, toNano } from "@ton/core"; +import { Address, beginCell, BitString, Cell, toNano } from "@ton/core"; import { paddedBufferToBits } from "@ton/core/dist/boc/utils/paddedBits"; import * as crc32 from "crc-32"; import { evalConstantExpression } from "./constEval"; @@ -11,8 +11,11 @@ import { throwInternalCompilerError, } from "./errors"; import { + AstAddress, AstBinaryOperation, AstBoolean, + AstCell, + AstCommentValue, AstCondition, AstConditional, AstConstantDef, @@ -22,6 +25,7 @@ import { AstFunctionDef, AstId, AstInitOf, + AstLiteral, AstMessageDecl, AstMethodCall, AstModuleItem, @@ -31,6 +35,8 @@ import { AstOpBinary, AstOpUnary, AstPrimitiveTypeDecl, + AstSimplifiedString, + AstSlice, FactoryAst, AstStatement, AstStatementAssign, @@ -48,16 +54,18 @@ import { AstStaticCall, AstString, AstStructDecl, + AstStructFieldValue, AstStructInstance, + AstStructValue, AstTrait, AstUnaryOperation, + eqExpressions, eqNames, getAstFactory, idText, isSelfId, } from "./grammar/ast"; -import { SrcInfo, dummySrcInfo, Parser, getParser } from "./grammar"; -import { divFloor, modFloor } from "./optimizer/util"; +import { AstUtil, divFloor, getAstUtil, modFloor } from "./optimizer/util"; import { getStaticConstant, getStaticFunction, @@ -66,15 +74,10 @@ import { hasStaticFunction, } from "./types/resolveDescriptors"; import { getExpType } from "./types/resolveExpression"; -import { - CommentValue, - StructValue, - TypeRef, - Value, - showValue, -} from "./types/types"; +import { TypeRef, showValue } from "./types/types"; import { sha256_sync } from "@ton/crypto"; -import { defaultParser } from "./grammar/grammar"; +import { defaultParser, getParser, Parser } from "./grammar/grammar"; +import { dummySrcInfo, SrcInfo } from "./grammar"; // TVM integers are signed 257-bit integers const minTvmInt: bigint = -(2n ** 256n); @@ -84,6 +87,10 @@ const maxTvmInt: bigint = 2n ** 256n - 1n; const minRepeatStatement: bigint = -(2n ** 256n); // Note it is the same as minimum for TVM const maxRepeatStatement: bigint = 2n ** 31n - 1n; +// Util factory methods +// FIXME: pass util as argument +//const util = getAstUtil(getAstFactory()); + // 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. @@ -108,58 +115,92 @@ function throwErrorConstEval(msg: string, source: SrcInfo): never { ); } type EvalResult = - | { kind: "ok"; value: Value } + | { kind: "ok"; value: AstLiteral } | { kind: "error"; message: string }; -export function ensureInt(val: Value, source: SrcInfo): bigint { - if (typeof val !== "bigint") { +export function ensureInt(val: AstExpression): AstNumber { + if (val.kind !== "number") { throwErrorConstEval( - `integer expected, but got '${showValue(val)}'`, - source, + `integer expected, but got expression of kind '${val.kind}'`, + val.loc, ); } - if (minTvmInt <= val && val <= maxTvmInt) { + if (minTvmInt <= val.value && val.value <= maxTvmInt) { return val; } else { throwErrorConstEval( `integer '${showValue(val)}' does not fit into TVM Int type`, - source, + val.loc, ); } } -function ensureRepeatInt(val: Value, source: SrcInfo): bigint { - if (typeof val !== "bigint") { +function ensureArgumentForEquality(val: AstLiteral): AstLiteral { + switch (val.kind) { + case "address": + case "boolean": + case "cell": + case "comment_value": + case "null": + case "number": + case "simplified_string": + case "slice": + return val; + case "struct_value": + throwErrorConstEval( + `struct ${showValue(val)} cannot be an argument to == operator`, + val.loc, + ); + break; + default: + throwInternalCompilerError("Unrecognized ast literal kind"); + } +} + +function ensureRepeatInt(val: AstExpression): AstNumber { + if (val.kind !== "number") { throwErrorConstEval( - `integer expected, but got '${showValue(val)}'`, - source, + `integer expected, but got expression of kind '${val.kind}'`, + val.loc, ); } - if (minRepeatStatement <= val && val <= maxRepeatStatement) { + if (minRepeatStatement <= val.value && val.value <= maxRepeatStatement) { return val; } else { throwErrorConstEval( - `repeat argument must be a number between -2^256 (inclusive) and 2^31 - 1 (inclusive)`, - source, + `repeat argument '${showValue(val)}' must be a number between -2^256 (inclusive) and 2^31 - 1 (inclusive)`, + val.loc, ); } } -function ensureBoolean(val: Value, source: SrcInfo): boolean { - if (typeof val !== "boolean") { +export function ensureBoolean(val: AstExpression): AstBoolean { + if (val.kind !== "boolean") { throwErrorConstEval( - `boolean expected, but got '${showValue(val)}'`, - source, + `boolean expected, but got expression of kind '${val.kind}'`, + val.loc, ); } return val; } -function ensureString(val: Value, source: SrcInfo): string { - if (typeof val !== "string") { +export function ensureString(val: AstExpression): AstString { + if (val.kind !== "string") { throwErrorConstEval( - `string expected, but got '${showValue(val)}'`, - source, + `string expected, but got expression of kind '${val.kind}'`, + val.loc, + ); + } + return val; +} + +export function ensureSimplifiedString( + val: AstExpression, +): AstSimplifiedString { + if (val.kind !== "simplified_string") { + throwErrorConstEval( + `simplified string expected, but got expression of kind '${val.kind}'`, + val.loc, ); } return val; @@ -189,107 +230,126 @@ function ensureMethodArity( export function evalUnaryOp( op: AstUnaryOperation, - valOperand: Value, - operandLoc: SrcInfo = dummySrcInfo, - source: SrcInfo = dummySrcInfo, -): Value { + valOperand: AstLiteral, + source: SrcInfo, + util: AstUtil, +): AstLiteral { switch (op) { case "+": - return ensureInt(valOperand, operandLoc); - case "-": - return ensureInt(-ensureInt(valOperand, operandLoc), source); - case "~": - return ~ensureInt(valOperand, operandLoc); - case "!": - return !ensureBoolean(valOperand, operandLoc); + return ensureInt(valOperand); + case "-": { + const astNumber = ensureInt(valOperand); + const result = -astNumber.value; + return ensureInt(util.makeNumberLiteral(result, source)); + } + case "~": { + const astNumber = ensureInt(valOperand); + const result = ~astNumber.value; + return util.makeNumberLiteral(result, source); + } + case "!": { + const astBoolean = ensureBoolean(valOperand); + const result = !astBoolean.value; + return util.makeBooleanLiteral(result, source); + } case "!!": - if (valOperand === null) { + if (valOperand.kind === "null") { throwErrorConstEval( "non-null value expected but got null", - operandLoc, + valOperand.loc, ); } return valOperand; + default: + throwInternalCompilerError("Unrecognized operand"); } } export function evalBinaryOp( op: AstBinaryOperation, - valLeft: Value, - valRightContinuation: () => Value, // It needs to be a continuation, because some binary operators short-circuit - locLeft: SrcInfo = dummySrcInfo, - locRight: SrcInfo = dummySrcInfo, - source: SrcInfo = dummySrcInfo, -): Value { + valLeft: AstLiteral, + valRightContinuation: () => AstLiteral, // It needs to be a continuation, because some binary operators short-circuit + source: SrcInfo, + util: AstUtil, +): AstLiteral { switch (op) { - case "+": - return ensureInt( - ensureInt(valLeft, locLeft) + - ensureInt(valRightContinuation(), locRight), - source, - ); - case "-": - return ensureInt( - ensureInt(valLeft, locLeft) - - ensureInt(valRightContinuation(), locRight), - source, - ); - case "*": - return ensureInt( - ensureInt(valLeft, locLeft) * - ensureInt(valRightContinuation(), locRight), - source, - ); + case "+": { + const astLeft = ensureInt(valLeft); + const astRight = ensureInt(valRightContinuation()); + const result = astLeft.value + astRight.value; + return ensureInt(util.makeNumberLiteral(result, source)); + } + case "-": { + const astLeft = ensureInt(valLeft); + const astRight = ensureInt(valRightContinuation()); + const result = astLeft.value - astRight.value; + return ensureInt(util.makeNumberLiteral(result, source)); + } + case "*": { + const astLeft = ensureInt(valLeft); + const astRight = ensureInt(valRightContinuation()); + const result = astLeft.value * astRight.value; + return ensureInt(util.makeNumberLiteral(result, source)); + } case "/": { // The semantics of integer division for TVM (and by extension in Tact) // is a non-conventional one: by default it rounds towards negative infinity, // meaning, for instance, -1 / 5 = -1 and not zero, as in many mainstream languages. // Still, the following holds: a / b * b + a % b == a, for all b != 0. - const r = ensureInt(valRightContinuation(), locRight); - if (r === 0n) + + const astRight = ensureInt(valRightContinuation()); + if (astRight.value === 0n) throwErrorConstEval( "divisor expression must be non-zero", - locRight, + astRight.loc, ); - return ensureInt(divFloor(ensureInt(valLeft, locLeft), r), source); + const astLeft = ensureInt(valLeft); + const result = divFloor(astLeft.value, astRight.value); + return ensureInt(util.makeNumberLiteral(result, source)); } case "%": { // Same as for division, see the comment above // Example: -1 % 5 = 4 - const r = ensureInt(valRightContinuation(), locRight); - if (r === 0n) + const astRight = ensureInt(valRightContinuation()); + if (astRight.value === 0n) throwErrorConstEval( "divisor expression must be non-zero", - locRight, + astRight.loc, ); - return ensureInt(modFloor(ensureInt(valLeft, locLeft), r), source); + const astLeft = ensureInt(valLeft); + const result = modFloor(astLeft.value, astRight.value); + return ensureInt(util.makeNumberLiteral(result, source)); + } + case "&": { + const astLeft = ensureInt(valLeft); + const astRight = ensureInt(valRightContinuation()); + const result = astLeft.value & astRight.value; + return util.makeNumberLiteral(result, source); + } + case "|": { + const astLeft = ensureInt(valLeft); + const astRight = ensureInt(valRightContinuation()); + const result = astLeft.value | astRight.value; + return util.makeNumberLiteral(result, source); + } + case "^": { + const astLeft = ensureInt(valLeft); + const astRight = ensureInt(valRightContinuation()); + const result = astLeft.value ^ astRight.value; + return util.makeNumberLiteral(result, source); } - case "&": - return ( - ensureInt(valLeft, locLeft) & - ensureInt(valRightContinuation(), locRight) - ); - case "|": - return ( - ensureInt(valLeft, locLeft) | - ensureInt(valRightContinuation(), locRight) - ); - case "^": - return ( - ensureInt(valLeft, locLeft) ^ - ensureInt(valRightContinuation(), locRight) - ); case "<<": { - const valNum = ensureInt(valLeft, locLeft); - const valBits = ensureInt(valRightContinuation(), locRight); - if (0n > valBits || valBits > 256n) { + const astNum = ensureInt(valLeft); + const astBits = ensureInt(valRightContinuation()); + if (0n > astBits.value || astBits.value > 256n) { throwErrorConstEval( - `the number of bits shifted ('${valBits}') must be within [0..256] range`, - locRight, + `the number of bits shifted ('${astBits.value}') must be within [0..256] range`, + astBits.loc, ); } + const result = astNum.value << astBits.value; try { - return ensureInt(valNum << valBits, source); + return ensureInt(util.makeNumberLiteral(result, source)); } catch (e) { if (e instanceof RangeError) // this actually should not happen @@ -301,19 +361,20 @@ export function evalBinaryOp( } } case ">>": { - const valNum = ensureInt(valLeft, locLeft); - const valBits = ensureInt(valRightContinuation(), locRight); - if (0n > valBits || valBits > 256n) { + const astNum = ensureInt(valLeft); + const astBits = ensureInt(valRightContinuation()); + if (0n > astBits.value || astBits.value > 256n) { throwErrorConstEval( - `the number of bits shifted ('${valBits}') must be within [0..256] range`, - locRight, + `the number of bits shifted ('${astBits.value}') must be within [0..256] range`, + astBits.loc, ); } + const result = astNum.value >> astBits.value; try { - return ensureInt(valNum >> valBits, source); + return ensureInt(util.makeNumberLiteral(result, source)); } catch (e) { if (e instanceof RangeError) - // this is actually should not happen + // this actually should not happen throwErrorConstEval( `integer does not fit into TVM Int type`, source, @@ -321,67 +382,93 @@ export function evalBinaryOp( throw e; } } - case ">": - return ( - ensureInt(valLeft, locLeft) > - ensureInt(valRightContinuation(), locRight) - ); - case "<": - return ( - ensureInt(valLeft, locLeft) < - ensureInt(valRightContinuation(), locRight) - ); - case ">=": - return ( - ensureInt(valLeft, locLeft) >= - ensureInt(valRightContinuation(), locRight) - ); - case "<=": - return ( - ensureInt(valLeft, locLeft) <= - ensureInt(valRightContinuation(), locRight) - ); + case ">": { + const astLeft = ensureInt(valLeft); + const astRight = ensureInt(valRightContinuation()); + const result = astLeft.value > astRight.value; + return util.makeBooleanLiteral(result, source); + } + case "<": { + const astLeft = ensureInt(valLeft); + const astRight = ensureInt(valRightContinuation()); + const result = astLeft.value < astRight.value; + return util.makeBooleanLiteral(result, source); + } + case ">=": { + const astLeft = ensureInt(valLeft); + const astRight = ensureInt(valRightContinuation()); + const result = astLeft.value >= astRight.value; + return util.makeBooleanLiteral(result, source); + } + case "<=": { + const astLeft = ensureInt(valLeft); + const astRight = ensureInt(valRightContinuation()); + const result = astLeft.value <= astRight.value; + return util.makeBooleanLiteral(result, source); + } case "==": { const valR = valRightContinuation(); // the null comparisons account for optional types, e.g. // a const x: Int? = 42 can be compared to null if ( - typeof valLeft !== typeof valR && - valLeft !== null && - valR !== null + valLeft.kind !== valR.kind && + valLeft.kind !== "null" && + valR.kind !== "null" ) { throwErrorConstEval( "operands of `==` must have same type", source, ); } - return valLeft === valR; + const valLeft_ = ensureArgumentForEquality(valLeft); + const valR_ = ensureArgumentForEquality(valR); + + // Changed to equality testing (instead of ===) because cells, slices, address are equal by hashing + const result = eqExpressions(valLeft_, valR_); + return util.makeBooleanLiteral(result, source); } case "!=": { const valR = valRightContinuation(); - if (typeof valLeft !== typeof valR) { + + // Comparison to null should be checked as well + // otherwise it would give an error + if ( + valLeft.kind !== valR.kind && + valLeft.kind !== "null" && + valR.kind !== "null" + ) { throwErrorConstEval( "operands of `!=` must have same type", source, ); } - return valLeft !== valR; + const valLeft_ = ensureArgumentForEquality(valLeft); + const valR_ = ensureArgumentForEquality(valR); + + // Changed to equality testing (instead of ===) because cells, slices are equal by hashing + const result = !eqExpressions(valLeft_, valR_); + return util.makeBooleanLiteral(result, source); + } + case "&&": { + const astLeft = ensureBoolean(valLeft); + const astRight = ensureBoolean(valRightContinuation()); + const result = astLeft.value && astRight.value; + return util.makeBooleanLiteral(result, source); + } + case "||": { + const astLeft = ensureBoolean(valLeft); + const astRight = ensureBoolean(valRightContinuation()); + const result = astLeft.value || astRight.value; + return util.makeBooleanLiteral(result, source); } - case "&&": - return ( - ensureBoolean(valLeft, locLeft) && - ensureBoolean(valRightContinuation(), locRight) - ); - case "||": - return ( - ensureBoolean(valLeft, locLeft) || - ensureBoolean(valRightContinuation(), locRight) - ); } } -function interpretEscapeSequences(stringLiteral: string, source: SrcInfo) { +export function interpretEscapeSequences( + stringLiteral: string, + source: SrcInfo, +): string { return stringLiteral.replace( /\\\\|\\"|\\n|\\r|\\t|\\v|\\b|\\f|\\u{([0-9A-Fa-f]{1,6})}|\\u([0-9A-Fa-f]{4})|\\x([0-9A-Fa-f]{2})/g, (match, unicodeCodePoint, unicodeEscape, hexEscape) => { @@ -431,14 +518,14 @@ function interpretEscapeSequences(stringLiteral: string, source: SrcInfo) { } class ReturnSignal extends Error { - private value?: Value; + private value?: AstLiteral; - constructor(value?: Value) { + constructor(value?: AstLiteral) { super(); this.value = value; } - public getValue(): Value | undefined { + public getValue(): AstLiteral | undefined { return this.value; } } @@ -453,7 +540,7 @@ export type InterpreterConfig = { const WILDCARD_NAME: string = "_"; -type Environment = { values: Map; parent?: Environment }; +type Environment = { values: Map; parent?: Environment }; class EnvironmentStack { private currentEnv: Environment; @@ -462,7 +549,7 @@ class EnvironmentStack { this.currentEnv = { values: new Map() }; } - private findBindingMap(name: string): Map | undefined { + private findBindingMap(name: string): Map | undefined { let env: Environment | undefined = this.currentEnv; while (env !== undefined) { if (env.values.has(name)) { @@ -520,7 +607,7 @@ class EnvironmentStack { so that the return at line 5 (now in the environment a = 3) will produce 3 * 2 = 6, and so on. */ - public setNewBinding(name: string, val: Value) { + public setNewBinding(name: string, val: AstLiteral) { if (name !== WILDCARD_NAME) { this.currentEnv.values.set(name, val); } @@ -533,7 +620,7 @@ class EnvironmentStack { to "val". If it does not find "name", the stack is unchanged. As a special case, name "_" is always ignored. */ - public updateBinding(name: string, val: Value) { + public updateBinding(name: string, val: AstLiteral) { if (name !== WILDCARD_NAME) { const bindings = this.findBindingMap(name); if (bindings !== undefined) { @@ -549,7 +636,7 @@ class EnvironmentStack { If it does not find "name", it returns undefined. As a special case, name "_" always returns undefined. */ - public getBinding(name: string): Value | undefined { + public getBinding(name: string): AstLiteral | undefined { if (name === WILDCARD_NAME) { return undefined; } @@ -577,7 +664,7 @@ class EnvironmentStack { */ public executeInNewEnvironment( code: () => T, - initialBindings: { names: string[]; values: Value[] } = { + initialBindings: { names: string[]; values: AstLiteral[] } = { names: [], values: [], }, @@ -604,12 +691,14 @@ export function parseAndEvalExpression( sourceCode: string, ast: FactoryAst = getAstFactory(), parser: Parser = getParser(ast, defaultParser), + util: AstUtil = getAstUtil(ast), ): EvalResult { try { const ast = parser.parseExpression(sourceCode); const constEvalResult = evalConstantExpression( ast, new CompilerContext(), + util, ); return { kind: "ok", value: constEvalResult }; } catch (error) { @@ -670,14 +759,17 @@ export class Interpreter { private envStack: EnvironmentStack; private context: CompilerContext; private config: InterpreterConfig; + private util: AstUtil; constructor( + util: AstUtil, context: CompilerContext = new CompilerContext(), config: InterpreterConfig = defaultInterpreterConfig, ) { this.envStack = new EnvironmentStack(); this.context = context; this.config = config; + this.util = util; } public interpretModuleItem(ast: AstModuleItem): void { @@ -771,7 +863,7 @@ export class Interpreter { ); } - public interpretExpression(ast: AstExpression): Value { + public interpretExpression(ast: AstExpression): AstLiteral { switch (ast.kind) { case "id": return this.interpretName(ast); @@ -787,6 +879,16 @@ export class Interpreter { return this.interpretNumber(ast); case "string": return this.interpretString(ast); + case "comment_value": + return this.interpretCommentValue(ast); + case "simplified_string": + return this.interpretSimplifiedString(ast); + case "address": + return this.interpretAddress(ast); + case "cell": + return this.interpretCell(ast); + case "slice": + return this.interpretSlice(ast); case "op_unary": return this.interpretUnaryOp(ast); case "op_binary": @@ -795,14 +897,18 @@ export class Interpreter { return this.interpretConditional(ast); case "struct_instance": return this.interpretStructInstance(ast); + case "struct_value": + return this.interpretStructValue(ast); case "field_access": return this.interpretFieldAccess(ast); case "static_call": return this.interpretStaticCall(ast); + default: + throwInternalCompilerError("Unrecognized expression kind"); } } - public interpretName(ast: AstId): Value { + public interpretName(ast: AstId): AstLiteral { if (hasStaticConstant(this.context, idText(ast))) { const constant = getStaticConstant(this.context, idText(ast)); if (constant.value !== undefined) { @@ -821,15 +927,14 @@ export class Interpreter { throwNonFatalErrorConstEval("cannot evaluate a variable", ast.loc); } - public interpretMethodCall(ast: AstMethodCall): Value { + public interpretMethodCall(ast: AstMethodCall): AstLiteral { switch (idText(ast.method)) { case "asComment": { ensureMethodArity(0, ast.args, ast.loc); - const comment = ensureString( + const comment = ensureSimplifiedString( this.interpretExpression(ast.self), - ast.self.loc, - ); - return new CommentValue(comment); + ).value; + return this.util.makeCommentLiteral(comment, ast.loc); } default: throwNonFatalErrorConstEval( @@ -839,48 +944,72 @@ export class Interpreter { } } - public interpretInitOf(ast: AstInitOf): Value { + public interpretInitOf(ast: AstInitOf): AstLiteral { throwNonFatalErrorConstEval( "initOf is not supported at this moment", ast.loc, ); } - public interpretNull(_ast: AstNull): null { - return null; + public interpretNull(ast: AstNull): AstNull { + return ast; } - public interpretBoolean(ast: AstBoolean): boolean { - return ast.value; + public interpretBoolean(ast: AstBoolean): AstBoolean { + return ast; } - public interpretNumber(ast: AstNumber): bigint { - return ensureInt(ast.value, ast.loc); + public interpretNumber(ast: AstNumber): AstNumber { + return ensureInt(ast); } - public interpretString(ast: AstString): string { - return ensureString( + public interpretString(ast: AstString): AstSimplifiedString { + return this.util.makeSimplifiedStringLiteral( interpretEscapeSequences(ast.value, ast.loc), ast.loc, ); } - public interpretUnaryOp(ast: AstOpUnary): Value { + public interpretCommentValue(ast: AstCommentValue): AstCommentValue { + return ast; + } + + public interpretSimplifiedString( + ast: AstSimplifiedString, + ): AstSimplifiedString { + return ast; + } + + public interpretAddress(ast: AstAddress): AstAddress { + return ast; + } + + public interpretCell(ast: AstCell): AstCell { + return ast; + } + + public interpretSlice(ast: AstSlice): AstSlice { + return ast; + } + + public interpretUnaryOp(ast: AstOpUnary): AstLiteral { // Tact grammar does not have negative integer literals, // so in order to avoid errors for `-115792089237316195423570985008687907853269984665640564039457584007913129639936` // which is `-(2**256)` we need to have a special case for it if (ast.operand.kind === "number" && ast.op === "-") { // emulating negative integer literals - return ensureInt(-ast.operand.value, ast.loc); + return ensureInt( + this.util.makeNumberLiteral(-ast.operand.value, ast.loc), + ); } const valOperand = this.interpretExpression(ast.operand); - return evalUnaryOp(ast.op, valOperand, ast.operand.loc, ast.loc); + return evalUnaryOp(ast.op, valOperand, ast.loc, this.util); } - public interpretBinaryOp(ast: AstOpBinary): Value { + public interpretBinaryOp(ast: AstOpBinary): AstLiteral { const valLeft = this.interpretExpression(ast.left); const valRightContinuation = () => this.interpretExpression(ast.right); @@ -888,55 +1017,86 @@ export class Interpreter { ast.op, valLeft, valRightContinuation, - ast.left.loc, - ast.right.loc, ast.loc, + this.util, ); } - public interpretConditional(ast: AstConditional): Value { + public interpretConditional(ast: AstConditional): AstLiteral { // here we rely on the typechecker that both branches have the same type - const valCond = ensureBoolean( - this.interpretExpression(ast.condition), - ast.condition.loc, - ); - if (valCond) { + const valCond = ensureBoolean(this.interpretExpression(ast.condition)); + if (valCond.value) { return this.interpretExpression(ast.thenBranch); } else { return this.interpretExpression(ast.elseBranch); } } - public interpretStructInstance(ast: AstStructInstance): StructValue { + public interpretStructInstance(ast: AstStructInstance): AstStructValue { const structTy = getType(this.context, ast.type); // initialize the resulting struct value with // the default values for fields with initializers // or null for uninitialized optional fields - const resultWithDefaultFields: StructValue = structTy.fields.reduce( - (resObj, field) => { - if (field.default !== undefined) { - resObj[field.name] = field.default; - } else { - if (field.type.kind === "ref" && field.type.optional) { - resObj[field.name] = null; - } + const resultMap: Map = new Map(); + + for (const field of structTy.fields) { + if (typeof field.default !== "undefined") { + resultMap.set(field.name, field.default); + } else { + if (field.type.kind === "ref" && field.type.optional) { + resultMap.set( + field.name, + this.util.makeNullLiteral(ast.loc), + ); } - return resObj; - }, - { $tactStruct: idText(ast.type) } as StructValue, - ); + } + } // this will override default fields set above - return ast.args.reduce((resObj, fieldWithInit) => { - resObj[idText(fieldWithInit.field)] = this.interpretExpression( - fieldWithInit.initializer, + for (const fieldWithInit of ast.args) { + const v = this.interpretExpression(fieldWithInit.initializer); + resultMap.set(idText(fieldWithInit.field), v); + } + + // Create the field entries for the StructValue + // The previous loop ensures that the map resultMap cannot return + // undefined for each of the fields in ast.args + const structValueFields: AstStructFieldValue[] = []; + for (const [fieldName, fieldValue] of resultMap) { + // Find the source code declaration, if existent + const sourceField = ast.args.find( + (f) => idText(f.field) === fieldName, ); - return resObj; - }, resultWithDefaultFields); + if (typeof sourceField !== "undefined") { + structValueFields.push( + this.util.makeStructFieldValue( + fieldName, + fieldValue, + sourceField.loc, + ), + ); + } else { + // Use as source code location the entire struct + structValueFields.push( + this.util.makeStructFieldValue( + fieldName, + fieldValue, + ast.loc, + ), + ); + } + } + + return this.util.makeStructValue(structValueFields, ast.type, ast.loc); + } + + public interpretStructValue(ast: AstStructValue): AstStructValue { + // Struct values are already simplified to their simplest form + return ast; } - public interpretFieldAccess(ast: AstFieldAccess): Value { + public interpretFieldAccess(ast: AstFieldAccess): AstLiteral { // special case for contract/trait constant accesses via `self.constant` // interpret "self" as a contract/trait access only if "self" // is not already assigned in the environment (this would mean @@ -974,18 +1134,17 @@ export class Interpreter { } } const valStruct = this.interpretExpression(ast.aggregate); - if ( - valStruct === null || - typeof valStruct !== "object" || - !("$tactStruct" in valStruct) - ) { + if (valStruct.kind !== "struct_value") { throwErrorConstEval( `constant struct expected, but got ${showValue(valStruct)}`, ast.aggregate.loc, ); } - if (idText(ast.field) in valStruct) { - return valStruct[idText(ast.field)]!; + const field = valStruct.args.find( + (f) => idText(ast.field) === idText(f.field), + ); + if (typeof field !== "undefined") { + return field.initializer; } else { // this cannot happen in a well-typed program throwInternalCompilerError( @@ -995,18 +1154,19 @@ export class Interpreter { } } - public interpretStaticCall(ast: AstStaticCall): Value { + public interpretStaticCall(ast: AstStaticCall): AstLiteral { switch (idText(ast.function)) { case "ton": { ensureFunArity(1, ast.args, ast.loc); - const tons = ensureString( + const tons = ensureSimplifiedString( this.interpretExpression(ast.args[0]!), - ast.args[0]!.loc, ); try { return ensureInt( - BigInt(toNano(tons).toString(10)), - ast.loc, + this.util.makeNumberLiteral( + BigInt(toNano(tons.value).toString(10)), + ast.loc, + ), ); } catch (e) { if (e instanceof Error && e.message === "Invalid number") { @@ -1022,20 +1182,21 @@ export class Interpreter { ensureFunArity(2, ast.args, ast.loc); const valBase = ensureInt( this.interpretExpression(ast.args[0]!), - ast.args[0]!.loc, ); const valExp = ensureInt( this.interpretExpression(ast.args[1]!), - ast.args[1]!.loc, ); - if (valExp < 0n) { + if (valExp.value < 0n) { throwErrorConstEval( - `${idTextErr(ast.function)} builtin called with negative exponent ${valExp}`, + `${idTextErr(ast.function)} builtin called with negative exponent ${showValue(valExp)}`, ast.loc, ); } try { - return ensureInt(valBase ** valExp, ast.loc); + const result = valBase.value ** valExp.value; + return ensureInt( + this.util.makeNumberLiteral(result, ast.loc), + ); } catch (e) { if (e instanceof RangeError) { // even TS bigint type cannot hold it @@ -1051,16 +1212,18 @@ export class Interpreter { ensureFunArity(1, ast.args, ast.loc); const valExponent = ensureInt( this.interpretExpression(ast.args[0]!), - ast.args[0]!.loc, ); - if (valExponent < 0n) { + if (valExponent.value < 0n) { throwErrorConstEval( - `${idTextErr(ast.function)} builtin called with negative exponent ${valExponent}`, + `${idTextErr(ast.function)} builtin called with negative exponent ${showValue(valExponent)}`, ast.loc, ); } try { - return ensureInt(2n ** valExponent, ast.loc); + const result = 2n ** valExponent.value; + return ensureInt( + this.util.makeNumberLiteral(result, ast.loc), + ); } catch (e) { if (e instanceof RangeError) { // even TS bigint type cannot hold it @@ -1075,31 +1238,36 @@ export class Interpreter { case "sha256": { ensureFunArity(1, ast.args, ast.loc); const expr = this.interpretExpression(ast.args[0]!); - if (expr instanceof Slice) { + if (expr.kind === "slice") { throwNonFatalErrorConstEval( "slice argument is currently not supported", ast.loc, ); } - const str = ensureString(expr, ast.args[0]!.loc); - return BigInt("0x" + sha256_sync(str).toString("hex")); + const str = ensureSimplifiedString(expr); + return this.util.makeNumberLiteral( + BigInt("0x" + sha256_sync(str.value).toString("hex")), + ast.loc, + ); } case "emptyMap": { ensureFunArity(0, ast.args, ast.loc); - return null; + return this.util.makeNullLiteral(ast.loc); } case "cell": { ensureFunArity(1, ast.args, ast.loc); - const str = ensureString( + const str = ensureSimplifiedString( this.interpretExpression(ast.args[0]!), - ast.args[0]!.loc, ); try { - return Cell.fromBase64(str); + return this.util.makeCellLiteral( + Cell.fromBase64(str.value), + ast.loc, + ); } catch (_) { throwErrorConstEval( - `invalid base64 encoding for a cell: ${str}`, + `invalid base64 encoding for a cell: ${showValue(str)}`, ast.loc, ); } @@ -1108,15 +1276,17 @@ export class Interpreter { case "slice": { ensureFunArity(1, ast.args, ast.loc); - const str = ensureString( + const str = ensureSimplifiedString( this.interpretExpression(ast.args[0]!), - ast.args[0]!.loc, ); try { - return Cell.fromBase64(str).asSlice(); + return this.util.makeSliceLiteral( + Cell.fromBase64(str.value).asSlice(), + ast.loc, + ); } catch (_) { throwErrorConstEval( - `invalid base64 encoding for a cell: ${str}`, + `invalid base64 encoding for a cell: ${showValue(str)}`, ast.loc, ); } @@ -1125,20 +1295,19 @@ export class Interpreter { case "rawSlice": { ensureFunArity(1, ast.args, ast.loc); - const str = ensureString( + const str = ensureSimplifiedString( this.interpretExpression(ast.args[0]!), - ast.args[0]!.loc, ); - if (!/^[0-9a-fA-F]*_?$/.test(str)) { + if (!/^[0-9a-fA-F]*_?$/.test(str.value)) { throwErrorConstEval( - `invalid hex string: ${str}`, + `invalid hex string: ${showValue(str)}`, ast.loc, ); } // Remove underscores from the hex string - const hex = str.replace("_", ""); + const hex = str.value.replace("_", ""); const paddedHex = hex.length % 2 === 0 ? hex : "0" + hex; const buffer = Buffer.from(paddedHex, "hex"); @@ -1150,7 +1319,7 @@ export class Interpreter { ); // Handle the case where the string ends with an underscore - if (str.endsWith("_")) { + if (str.value.endsWith("_")) { const paddedBits = paddedBufferToBits(buffer); // Ensure there's enough length to apply the offset @@ -1174,17 +1343,19 @@ export class Interpreter { } // Return the constructed slice - return beginCell().storeBits(bits).endCell().asSlice(); + return this.util.makeSliceLiteral( + beginCell().storeBits(bits).endCell().asSlice(), + ast.loc, + ); } break; case "ascii": { ensureFunArity(1, ast.args, ast.loc); - const str = ensureString( + const str = ensureSimplifiedString( this.interpretExpression(ast.args[0]!), - ast.args[0]!.loc, ); - const hex = Buffer.from(str).toString("hex"); + const hex = Buffer.from(str.value).toString("hex"); if (hex.length > 64) { throwErrorConstEval( `ascii string is too long, expected up to 32 bytes, got ${Math.floor(hex.length / 2)}`, @@ -1197,41 +1368,45 @@ export class Interpreter { ast.loc, ); } - return BigInt("0x" + hex); + return this.util.makeNumberLiteral( + BigInt("0x" + hex), + ast.loc, + ); } break; case "crc32": { ensureFunArity(1, ast.args, ast.loc); - const str = ensureString( + const str = ensureSimplifiedString( this.interpretExpression(ast.args[0]!), - ast.args[0]!.loc, ); - return BigInt(crc32.str(str) >>> 0); // >>> 0 converts to unsigned + return this.util.makeNumberLiteral( + BigInt(crc32.str(str.value) >>> 0), + ast.loc, + ); // >>> 0 converts to unsigned } break; case "address": { ensureFunArity(1, ast.args, ast.loc); - const str = ensureString( + const str = ensureSimplifiedString( this.interpretExpression(ast.args[0]!), - ast.args[0]!.loc, ); try { - const address = Address.parse(str); + const address = Address.parse(str.value); if ( address.workChain !== 0 && address.workChain !== -1 ) { throwErrorConstEval( - `${str} is invalid address`, + `${showValue(str)} is invalid address`, ast.loc, ); } - return address; + return this.util.makeAddressLiteral(address, ast.loc); } catch (_) { throwErrorConstEval( - `invalid address encoding: ${str}`, + `invalid address encoding: ${showValue(str)}`, ast.loc, ); } @@ -1241,14 +1416,10 @@ export class Interpreter { ensureFunArity(2, ast.args, ast.loc); const wc = ensureInt( this.interpretExpression(ast.args[0]!), - ast.args[0]!.loc, - ); + ).value; const addr = Buffer.from( - ensureInt( - this.interpretExpression(ast.args[1]!), - ast.args[1]!.loc, - ) - .toString(16) + ensureInt(this.interpretExpression(ast.args[1]!)) + .value.toString(16) .padStart(64, "0"), "hex", ); @@ -1258,7 +1429,10 @@ export class Interpreter { ast.loc, ); } - return new Address(Number(wc), addr); + return this.util.makeAddressLiteral( + new Address(Number(wc), addr), + ast.loc, + ); } default: if (hasStaticFunction(this.context, idText(ast.function))) { @@ -1313,7 +1487,7 @@ export class Interpreter { functionCode: AstFunctionDef, args: AstExpression[], returns: TypeRef, - ): Value { + ): AstLiteral { // Evaluate the arguments in the current environment const argValues = args.map(this.interpretExpression, this); // Extract the parameter names @@ -1367,7 +1541,7 @@ export class Interpreter { // The function does not return a value. // We rely on the typechecker so that the function is called as a statement. // Hence, we can return a dummy null, since the null will be discarded anyway. - return null; + return this.util.makeNullLiteral(dummySrcInfo); } }, { names: paramNames, values: argValues }, @@ -1441,11 +1615,7 @@ export class Interpreter { } } const val = this.interpretExpression(ast.expression); - if ( - val === null || - typeof val !== "object" || - !("$tactStruct" in val) - ) { + if (val.kind !== "struct_value") { throwErrorConstEval( `destructuring assignment expected a struct, but got ${showValue( val, @@ -1454,13 +1624,16 @@ export class Interpreter { ); } + // Keep a map of the fields in val for lookup + const valAsMap: Map = new Map(); + val.args.forEach((f) => valAsMap.set(idText(f.field), f.initializer)); + for (const [field, name] of ast.identifiers.values()) { if (name.text === "_") { continue; } - const v = val[idText(field)]; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (v === undefined) { + const v = valAsMap.get(idText(field)); + if (typeof v === "undefined") { throwErrorConstEval( `destructuring assignment expected field ${idTextErr( field, @@ -1498,9 +1671,8 @@ export class Interpreter { ast.op, currentPathValue, updateVal, - ast.path.loc, - ast.expression.loc, ast.loc, + this.util, ); this.envStack.updateBinding(idText(ast.path), newVal); } else { @@ -1514,9 +1686,8 @@ export class Interpreter { public interpretConditionStatement(ast: AstCondition) { const condition = ensureBoolean( this.interpretExpression(ast.condition), - ast.condition.loc, ); - if (condition) { + if (condition.value) { this.envStack.executeInNewEnvironment(() => { ast.trueStatements.forEach(this.interpretStatement, this); }); @@ -1538,9 +1709,8 @@ export class Interpreter { public interpretRepeatStatement(ast: AstStatementRepeat) { const iterations = ensureRepeatInt( this.interpretExpression(ast.iterations), - ast.iterations.loc, ); - if (iterations > 0) { + if (iterations.value > 0) { // We can create a single environment for all the iterations in the loop // (instead of a fresh environment for each iteration) // because the typechecker ensures that variables do not leak outside @@ -1548,7 +1718,7 @@ export class Interpreter { // loop be initialized, which means that we can overwrite its value in the environment // in each iteration. this.envStack.executeInNewEnvironment(() => { - for (let i = 1; i <= iterations; i++) { + for (let i = 1; i <= iterations.value; i++) { ast.statements.forEach(this.interpretStatement, this); } }); @@ -1602,9 +1772,8 @@ export class Interpreter { // variables declared inside the loop. condition = ensureBoolean( this.interpretExpression(ast.condition), - ast.condition.loc, ); - } while (!condition); + } while (!condition.value); }); } @@ -1623,9 +1792,8 @@ export class Interpreter { // variables declared inside the loop. condition = ensureBoolean( this.interpretExpression(ast.condition), - ast.condition.loc, ); - if (condition) { + if (condition.value) { ast.statements.forEach(this.interpretStatement, this); iterCount++; @@ -1636,7 +1804,7 @@ export class Interpreter { ); } } - } while (condition); + } while (condition.value); }); } } diff --git a/src/node.ts b/src/node.ts index 170330913..218533d09 100644 --- a/src/node.ts +++ b/src/node.ts @@ -144,3 +144,5 @@ export async function run(args: { export { createNodeFileSystem } from "./vfs/createNodeFileSystem"; export { parseAndEvalExpression } from "./interpreter"; + +export { showValue } from "./types/types"; diff --git a/src/optimizer/algebraic.ts b/src/optimizer/algebraic.ts index bfde705b7..47a15adb5 100644 --- a/src/optimizer/algebraic.ts +++ b/src/optimizer/algebraic.ts @@ -4,7 +4,7 @@ import { AstOpBinary, AstOpUnary, eqExpressions, - isValue, + isLiteral, } from "../grammar/ast"; import { ExpressionTransformer, Rule } from "./types"; import { @@ -26,7 +26,7 @@ export class AddZero extends Rule { const topLevelNode = ast as AstOpBinary; if (this.additiveOperators.includes(topLevelNode.op)) { if ( - !isValue(topLevelNode.left) && + !isLiteral(topLevelNode.left) && checkIsNumber(topLevelNode.right, 0n) ) { // The tree has this form: @@ -37,7 +37,7 @@ export class AddZero extends Rule { return x; } else if ( checkIsNumber(topLevelNode.left, 0n) && - !isValue(topLevelNode.right) + !isLiteral(topLevelNode.right) ) { // The tree has this form: // 0 op x @@ -75,7 +75,7 @@ export class MultiplyZero extends Rule { // The tree has this form: // x * 0, where x is an identifier - return util.makeValueExpression(0n); + return util.makeNumberLiteral(0n, ast.loc); } else if ( checkIsNumber(topLevelNode.left, 0n) && checkIsName(topLevelNode.right) @@ -83,7 +83,7 @@ export class MultiplyZero extends Rule { // The tree has this form: // 0 * x, where x is an identifier - return util.makeValueExpression(0n); + return util.makeNumberLiteral(0n, ast.loc); } } } @@ -103,7 +103,7 @@ export class MultiplyOne extends Rule { const topLevelNode = ast as AstOpBinary; if (topLevelNode.op === "*") { if ( - !isValue(topLevelNode.left) && + !isLiteral(topLevelNode.left) && checkIsNumber(topLevelNode.right, 1n) ) { // The tree has this form: @@ -114,7 +114,7 @@ export class MultiplyOne extends Rule { return x; } else if ( checkIsNumber(topLevelNode.left, 1n) && - !isValue(topLevelNode.right) + !isLiteral(topLevelNode.right) ) { // The tree has this form: // 1 * x @@ -152,7 +152,7 @@ export class SubtractSelf extends Rule { const y = topLevelNode.right; if (eqExpressions(x, y)) { - return util.makeValueExpression(0n); + return util.makeNumberLiteral(0n, ast.loc); } } } @@ -173,8 +173,8 @@ export class AddSelf extends Rule { const topLevelNode = ast as AstOpBinary; if (topLevelNode.op === "+") { if ( - !isValue(topLevelNode.left) && - !isValue(topLevelNode.right) + !isLiteral(topLevelNode.left) && + !isLiteral(topLevelNode.right) ) { // The tree has this form: // x + y @@ -187,7 +187,7 @@ export class AddSelf extends Rule { const res = util.makeBinaryExpression( "*", x, - util.makeValueExpression(2n), + util.makeNumberLiteral(2n, ast.loc), ); // Since we joined the tree, there is further opportunity // for simplification @@ -213,18 +213,18 @@ export class OrTrue extends Rule { if (topLevelNode.op === "||") { if ( (checkIsName(topLevelNode.left) || - isValue(topLevelNode.left)) && + isLiteral(topLevelNode.left)) && checkIsBoolean(topLevelNode.right, true) ) { // The tree has this form: // x || true, where x is an identifier or a value - return util.makeValueExpression(true); + return util.makeBooleanLiteral(true, ast.loc); } else if (checkIsBoolean(topLevelNode.left, true)) { // The tree has this form: // true || x - return util.makeValueExpression(true); + return util.makeBooleanLiteral(true, ast.loc); } } } @@ -245,18 +245,18 @@ export class AndFalse extends Rule { if (topLevelNode.op === "&&") { if ( (checkIsName(topLevelNode.left) || - isValue(topLevelNode.left)) && + isLiteral(topLevelNode.left)) && checkIsBoolean(topLevelNode.right, false) ) { // The tree has this form: // x && false, where x is an identifier or a value - return util.makeValueExpression(false); + return util.makeBooleanLiteral(false, ast.loc); } else if (checkIsBoolean(topLevelNode.left, false)) { // The tree has this form: // false && x - return util.makeValueExpression(false); + return util.makeBooleanLiteral(false, ast.loc); } } } @@ -405,10 +405,10 @@ export class ExcludedMiddle extends Rule { const y = rightNode.operand; if ( - (checkIsName(x) || isValue(x)) && + (checkIsName(x) || isLiteral(x)) && eqExpressions(x, y) ) { - return util.makeValueExpression(true); + return util.makeBooleanLiteral(true, ast.loc); } } } else if (checkIsUnaryOpNode(topLevelNode.left)) { @@ -423,10 +423,10 @@ export class ExcludedMiddle extends Rule { const y = topLevelNode.right; if ( - (checkIsName(x) || isValue(x)) && + (checkIsName(x) || isLiteral(x)) && eqExpressions(x, y) ) { - return util.makeValueExpression(true); + return util.makeBooleanLiteral(true, ast.loc); } } } @@ -459,10 +459,10 @@ export class Contradiction extends Rule { const y = rightNode.operand; if ( - (checkIsName(x) || isValue(x)) && + (checkIsName(x) || isLiteral(x)) && eqExpressions(x, y) ) { - return util.makeValueExpression(false); + return util.makeBooleanLiteral(false, ast.loc); } } } else if (checkIsUnaryOpNode(topLevelNode.left)) { @@ -477,10 +477,10 @@ export class Contradiction extends Rule { const y = topLevelNode.right; if ( - (checkIsName(x) || isValue(x)) && + (checkIsName(x) || isLiteral(x)) && eqExpressions(x, y) ) { - return util.makeValueExpression(false); + return util.makeBooleanLiteral(false, ast.loc); } } } @@ -533,7 +533,7 @@ export class NegateTrue extends Rule { // The tree has this form // !true - return util.makeValueExpression(false); + return util.makeBooleanLiteral(false, ast.loc); } } } @@ -556,7 +556,7 @@ export class NegateFalse extends Rule { // The tree has this form // !false - return util.makeValueExpression(true); + return util.makeBooleanLiteral(true, ast.loc); } } } diff --git a/src/optimizer/associative.ts b/src/optimizer/associative.ts index bdb26e4d9..bb8da62dd 100644 --- a/src/optimizer/associative.ts +++ b/src/optimizer/associative.ts @@ -1,21 +1,20 @@ // This module includes rules involving associative rewrites of expressions +import { SrcInfo } from "../grammar"; import { AstBinaryOperation, AstExpression, + AstLiteral, AstOpBinary, - AstValue, - isValue, + isLiteral, } from "../grammar/ast"; -import * as interpreterModule from "../interpreter"; -import { Value } from "../types/types"; +import * as iM from "../interpreter"; import { ExpressionTransformer, Rule } from "./types"; import { abs, checkIsBinaryOpNode, checkIsBinaryOp_With_RightValue, checkIsBinaryOp_With_LeftValue, - extractValue, sign, AstUtil, } from "./util"; @@ -27,18 +26,12 @@ type TransformData = { type Transform = ( x1: AstExpression, - c1: Value, - c2: Value, + c1: AstLiteral, + c2: AstLiteral, util: AstUtil, + s: SrcInfo, ) => TransformData; -/* A simple wrapper function to transform the right value in a binary operator to a continuation - so that we can call the evaluation function in the interpreter module -*/ -function evalBinaryOp(op: AstBinaryOperation, valL: Value, valR: Value): Value { - return interpreterModule.evalBinaryOp(op, valL, () => valR); -} - 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) @@ -140,11 +133,11 @@ export class AssociativeRule1 extends AllowableOpRule { const rightTree = topLevelNode.right as AstOpBinary; const x1 = leftTree.left; - const c1 = leftTree.right as AstValue; + const c1 = leftTree.right as AstLiteral; const op1 = leftTree.op; const x2 = rightTree.left; - const c2 = rightTree.right as AstValue; + const c2 = rightTree.right as AstLiteral; const op2 = rightTree.op; const op = topLevelNode.op; @@ -163,10 +156,12 @@ 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( + const val = iM.evalBinaryOp( op2, - extractValue(c1), - extractValue(c2), + c1, + () => c2, + topLevelNode.loc, + util, ); // The final expression is @@ -178,7 +173,7 @@ export class AssociativeRule1 extends AllowableOpRule { const newLeft = applyRules( util.makeBinaryExpression(op1, x1, x2), ); - const newRight = util.makeValueExpression(val); + const newRight = val; return util.makeBinaryExpression(op, newLeft, newRight); } catch (e) { // Do nothing: will exit rule without modifying tree @@ -194,11 +189,11 @@ export class AssociativeRule1 extends AllowableOpRule { const rightTree = topLevelNode.right as AstOpBinary; const x1 = leftTree.left; - const c1 = leftTree.right as AstValue; + const c1 = leftTree.right as AstLiteral; const op1 = leftTree.op; const x2 = rightTree.right; - const c2 = rightTree.left as AstValue; + const c2 = rightTree.left as AstLiteral; const op2 = rightTree.op; const op = topLevelNode.op; @@ -215,10 +210,12 @@ 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( + const val = iM.evalBinaryOp( op, - extractValue(c1), - extractValue(c2), + c1, + () => c2, + topLevelNode.loc, + util, ); // The current expression could be either @@ -229,7 +226,7 @@ export class AssociativeRule1 extends AllowableOpRule { // Because we are joining x1 and val, // there is further opportunity of simplification, // So, we ask the evaluator to apply all the rules in the subtree. - const newValNode = util.makeValueExpression(val); + const newValNode = val; const newLeft = applyRules( util.makeBinaryExpression(op1, x1, newValNode), ); @@ -248,11 +245,11 @@ export class AssociativeRule1 extends AllowableOpRule { const rightTree = topLevelNode.right as AstOpBinary; const x1 = leftTree.right; - const c1 = leftTree.left as AstValue; + const c1 = leftTree.left as AstLiteral; const op1 = leftTree.op; const x2 = rightTree.left; - const c2 = rightTree.right as AstValue; + const c2 = rightTree.right as AstLiteral; const op2 = rightTree.op; const op = topLevelNode.op; @@ -271,10 +268,12 @@ 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( + const val = iM.evalBinaryOp( op, - extractValue(c2), - extractValue(c1), + c2, + () => c1, + topLevelNode.loc, + util, ); // The current expression could be either @@ -285,7 +284,7 @@ export class AssociativeRule1 extends AllowableOpRule { // Because we are joining x2 and val, // there is further opportunity of simplification, // So, we ask the evaluator to apply all the rules in the subtree. - const newValNode = util.makeValueExpression(val); + const newValNode = val; const newLeft = applyRules( util.makeBinaryExpression(op2, x2, newValNode), ); @@ -304,11 +303,11 @@ export class AssociativeRule1 extends AllowableOpRule { const rightTree = topLevelNode.right as AstOpBinary; const x1 = leftTree.right; - const c1 = leftTree.left as AstValue; + const c1 = leftTree.left as AstLiteral; const op1 = leftTree.op; const x2 = rightTree.right; - const c2 = rightTree.left as AstValue; + const c2 = rightTree.left as AstLiteral; const op2 = rightTree.op; const op = topLevelNode.op; @@ -327,10 +326,12 @@ 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( + const val = iM.evalBinaryOp( op1, - extractValue(c1), - extractValue(c2), + c1, + () => c2, + topLevelNode.loc, + util, ); // The final expression is @@ -342,7 +343,7 @@ export class AssociativeRule1 extends AllowableOpRule { const newRight = applyRules( util.makeBinaryExpression(op2, x1, x2), ); - const newLeft = util.makeValueExpression(val); + const newLeft = val; return util.makeBinaryExpression(op, newLeft, newRight); } catch (e) { // Do nothing: will exit rule without modifying tree @@ -370,7 +371,7 @@ export class AssociativeRule2 extends AllowableOpRule { const topLevelNode = ast as AstOpBinary; if ( checkIsBinaryOp_With_RightValue(topLevelNode.left) && - !isValue(topLevelNode.right) + !isLiteral(topLevelNode.right) ) { // The tree has this form: // (x1 op1 c1) op x2 @@ -378,7 +379,7 @@ export class AssociativeRule2 extends AllowableOpRule { const rightTree = topLevelNode.right; const x1 = leftTree.left; - const c1 = leftTree.right as AstValue; + const c1 = leftTree.right as AstLiteral; const op1 = leftTree.op; const x2 = rightTree; @@ -407,7 +408,7 @@ export class AssociativeRule2 extends AllowableOpRule { } } else if ( checkIsBinaryOp_With_LeftValue(topLevelNode.left) && - !isValue(topLevelNode.right) + !isLiteral(topLevelNode.right) ) { // The tree has this form: // (c1 op1 x1) op x2 @@ -415,7 +416,7 @@ export class AssociativeRule2 extends AllowableOpRule { const rightTree = topLevelNode.right; const x1 = leftTree.right; - const c1 = leftTree.left as AstValue; + const c1 = leftTree.left as AstLiteral; const op1 = leftTree.op; const x2 = rightTree; @@ -441,7 +442,7 @@ export class AssociativeRule2 extends AllowableOpRule { return util.makeBinaryExpression(op1, c1, newRight); } } else if ( - !isValue(topLevelNode.left) && + !isLiteral(topLevelNode.left) && checkIsBinaryOp_With_RightValue(topLevelNode.right) ) { // The tree has this form: @@ -450,7 +451,7 @@ export class AssociativeRule2 extends AllowableOpRule { const rightTree = topLevelNode.right as AstOpBinary; const x1 = rightTree.left; - const c1 = rightTree.right as AstValue; + const c1 = rightTree.right as AstLiteral; const op1 = rightTree.op; const x2 = leftTree; @@ -476,7 +477,7 @@ export class AssociativeRule2 extends AllowableOpRule { return util.makeBinaryExpression(op1, newLeft, c1); } } else if ( - !isValue(topLevelNode.left) && + !isLiteral(topLevelNode.left) && checkIsBinaryOp_With_LeftValue(topLevelNode.right) ) { // The tree has this form: @@ -485,7 +486,7 @@ export class AssociativeRule2 extends AllowableOpRule { const rightTree = topLevelNode.right as AstOpBinary; const x1 = rightTree.right; - const c1 = rightTree.left as AstValue; + const c1 = rightTree.left as AstLiteral; const op1 = rightTree.op; const x2 = leftTree; @@ -626,19 +627,21 @@ export class AssociativeRule3 extends Rule { [ "+", // original expression: (x1 + c1) + c2 - (x1, c1, c2, util) => { + (x1, c1, c2, util, s) => { // final expression: x1 + (c1 + c2) - const val_ = evalBinaryOp("+", c1, c2); - const val_node = util.makeValueExpression(val_); + const val_ = iM.ensureInt( + iM.evalBinaryOp("+", c1, () => c2, s, util), + ); + const c1_ = iM.ensureInt(c1); return { simplifiedExpression: util.makeBinaryExpression( "+", x1, - val_node, + val_, ), safetyCondition: this.standardAdditiveCondition( - c1 as bigint, - val_ as bigint, + c1_.value, + val_.value, 0n, ), }; @@ -648,19 +651,21 @@ export class AssociativeRule3 extends Rule { [ "-", // original expression: (x1 + c1) - c2 - (x1, c1, c2, util) => { + (x1, c1, c2, util, s) => { // final expression: x1 + (c1 - c2) - const val_ = evalBinaryOp("-", c1, c2); - const val_node = util.makeValueExpression(val_); + const val_ = iM.ensureInt( + iM.evalBinaryOp("-", c1, () => c2, s, util), + ); + const c1_ = iM.ensureInt(c1); return { simplifiedExpression: util.makeBinaryExpression( "+", x1, - val_node, + val_, ), safetyCondition: this.standardAdditiveCondition( - c1 as bigint, - val_ as bigint, + c1_.value, + val_.value, 0n, ), }; @@ -675,19 +680,21 @@ export class AssociativeRule3 extends Rule { [ "+", // original expression: (x1 - c1) + c2 - (x1, c1, c2, util) => { + (x1, c1, c2, util, s) => { // final expression x1 - (c1 - c2) - const val_ = evalBinaryOp("-", c1, c2); - const val_node = util.makeValueExpression(val_); + const val_ = iM.ensureInt( + iM.evalBinaryOp("-", c1, () => c2, s, util), + ); + const c1_ = iM.ensureInt(c1); return { simplifiedExpression: util.makeBinaryExpression( "-", x1, - val_node, + val_, ), safetyCondition: this.standardAdditiveCondition( - c1 as bigint, - val_ as bigint, + c1_.value, + val_.value, 0n, ), }; @@ -697,19 +704,21 @@ export class AssociativeRule3 extends Rule { [ "-", // original expression: (x1 - c1) - c2 - (x1, c1, c2, util) => { + (x1, c1, c2, util, s) => { // final expression x1 - (c1 + c2) - const val_ = evalBinaryOp("+", c1, c2); - const val_node = util.makeValueExpression(val_); + const val_ = iM.ensureInt( + iM.evalBinaryOp("+", c1, () => c2, s, util), + ); + const c1_ = iM.ensureInt(c1); return { simplifiedExpression: util.makeBinaryExpression( "-", x1, - val_node, + val_, ), safetyCondition: this.standardAdditiveCondition( - c1 as bigint, - val_ as bigint, + c1_.value, + val_.value, 0n, ), }; @@ -724,20 +733,22 @@ export class AssociativeRule3 extends Rule { [ "*", // original expression: (x1 * c1) * c2 - (x1, c1, c2, util) => { + (x1, c1, c2, util, s) => { // final expression x1 * (c1 * c2) - const val_ = evalBinaryOp("*", c1, c2); - const val_node = util.makeValueExpression(val_); + const val_ = iM.ensureInt( + iM.evalBinaryOp("*", c1, () => c2, s, util), + ); + const c1_ = iM.ensureInt(c1); return { simplifiedExpression: util.makeBinaryExpression( "*", x1, - val_node, + val_, ), safetyCondition: this.standardMultiplicativeCondition( - c1 as bigint, - val_ as bigint, + c1_.value, + val_.value, ), }; }, @@ -751,15 +762,20 @@ export class AssociativeRule3 extends Rule { [ "&&", // original expression: (x1 && c1) && c2 - (x1, c1, c2, util) => { + (x1, c1, c2, util, s) => { // final expression x1 && (c1 && c2) - const val_ = evalBinaryOp("&&", c1, c2); - const val_node = util.makeValueExpression(val_); + const val_ = iM.evalBinaryOp( + "&&", + c1, + () => c2, + s, + util, + ); return { simplifiedExpression: util.makeBinaryExpression( "&&", x1, - val_node, + val_, ), safetyCondition: true, }; @@ -774,15 +790,20 @@ export class AssociativeRule3 extends Rule { [ "||", // original expression: (x1 || c1) || c2 - (x1, c1, c2, util) => { + (x1, c1, c2, util, s) => { // final expression x1 || (c1 || c2) - const val_ = evalBinaryOp("||", c1, c2); - const val_node = util.makeValueExpression(val_); + const val_ = iM.evalBinaryOp( + "||", + c1, + () => c2, + s, + util, + ); return { simplifiedExpression: util.makeBinaryExpression( "||", x1, - val_node, + val_, ), safetyCondition: true, }; @@ -811,19 +832,21 @@ export class AssociativeRule3 extends Rule { [ "+", // original expression: c2 + (c1 + x1) - (x1, c1, c2, util) => { + (x1, c1, c2, util, s) => { // final expression (c2 + c1) + x1 - const val_ = evalBinaryOp("+", c2, c1); - const val_node = util.makeValueExpression(val_); + const val_ = iM.ensureInt( + iM.evalBinaryOp("+", c2, () => c1, s, util), + ); + const c1_ = iM.ensureInt(c1); return { simplifiedExpression: util.makeBinaryExpression( "+", - val_node, + val_, x1, ), safetyCondition: this.standardAdditiveCondition( - c1 as bigint, - val_ as bigint, + c1_.value, + val_.value, 0n, ), }; @@ -833,19 +856,21 @@ export class AssociativeRule3 extends Rule { [ "-", // original expression: c2 + (c1 - x1) - (x1, c1, c2, util) => { + (x1, c1, c2, util, s) => { // final expression (c2 + c1) - x1 - const val_ = evalBinaryOp("+", c2, c1); - const val_node = util.makeValueExpression(val_); + const val_ = iM.ensureInt( + iM.evalBinaryOp("+", c2, () => c1, s, util), + ); + const c1_ = iM.ensureInt(c1); return { simplifiedExpression: util.makeBinaryExpression( "-", - val_node, + val_, x1, ), safetyCondition: this.standardAdditiveCondition( - c1 as bigint, - val_ as bigint, + c1_.value, + val_.value, -1n, ), }; @@ -860,19 +885,21 @@ export class AssociativeRule3 extends Rule { [ "+", // original expression: c2 - (c1 + x1) - (x1, c1, c2, util) => { + (x1, c1, c2, util, s) => { // final expression (c2 - c1) - x1 - const val_ = evalBinaryOp("-", c2, c1); - const val_node = util.makeValueExpression(val_); + const val_ = iM.ensureInt( + iM.evalBinaryOp("-", c2, () => c1, s, util), + ); + const c1_ = iM.ensureInt(c1); return { simplifiedExpression: util.makeBinaryExpression( "-", - val_node, + val_, x1, ), safetyCondition: this.oppositeAdditiveCondition( - c1 as bigint, - val_ as bigint, + c1_.value, + val_.value, 0n, ), }; @@ -882,19 +909,21 @@ export class AssociativeRule3 extends Rule { [ "-", // original expression: c2 - (c1 - x1) - (x1, c1, c2, util) => { + (x1, c1, c2, util, s) => { // final expression (c2 - c1) + x1 - const val_ = evalBinaryOp("-", c2, c1); - const val_node = util.makeValueExpression(val_); + const val_ = iM.ensureInt( + iM.evalBinaryOp("-", c2, () => c1, s, util), + ); + const c1_ = iM.ensureInt(c1); return { simplifiedExpression: util.makeBinaryExpression( "+", - val_node, + val_, x1, ), safetyCondition: this.oppositeAdditiveCondition( - c1 as bigint, - val_ as bigint, + c1_.value, + val_.value, -1n, ), }; @@ -910,20 +939,22 @@ export class AssociativeRule3 extends Rule { "*", // original expression: c2 * (c1 * x1) - (x1, c1, c2, util) => { + (x1, c1, c2, util, s) => { // final expression (c2 * c1) * x1 - const val_ = evalBinaryOp("*", c2, c1); - const val_node = util.makeValueExpression(val_); + const val_ = iM.ensureInt( + iM.evalBinaryOp("*", c2, () => c1, s, util), + ); + const c1_ = iM.ensureInt(c1); return { simplifiedExpression: util.makeBinaryExpression( "*", - val_node, + val_, x1, ), safetyCondition: this.standardMultiplicativeCondition( - c1 as bigint, - val_ as bigint, + c1_.value, + val_.value, ), }; }, @@ -938,14 +969,19 @@ export class AssociativeRule3 extends Rule { "&&", // original expression: c2 && (c1 && x1) - (x1, c1, c2, util) => { + (x1, c1, c2, util, s) => { // final expression (c2 && c1) && x1 - const val_ = evalBinaryOp("&&", c2, c1); - const val_node = util.makeValueExpression(val_); + const val_ = iM.evalBinaryOp( + "&&", + c2, + () => c1, + s, + util, + ); return { simplifiedExpression: util.makeBinaryExpression( "&&", - val_node, + val_, x1, ), safetyCondition: true, @@ -962,14 +998,19 @@ export class AssociativeRule3 extends Rule { "||", // original expression: c2 || (c1 || x1) - (x1, c1, c2, util) => { + (x1, c1, c2, util, s) => { // final expression (c2 || c1) || x1 - const val_ = evalBinaryOp("||", c2, c1); - const val_node = util.makeValueExpression(val_); + const val_ = iM.evalBinaryOp( + "||", + c2, + () => c1, + s, + util, + ); return { simplifiedExpression: util.makeBinaryExpression( "||", - val_node, + val_, x1, ), safetyCondition: true, @@ -999,19 +1040,21 @@ export class AssociativeRule3 extends Rule { [ "+", // original expression: c2 + (x1 + c1) - (x1, c1, c2, util) => { + (x1, c1, c2, util, s) => { // final expression x1 + (c2 + c1) - const val_ = evalBinaryOp("+", c2, c1); - const val_node = util.makeValueExpression(val_); + const val_ = iM.ensureInt( + iM.evalBinaryOp("+", c2, () => c1, s, util), + ); + const c1_ = iM.ensureInt(c1); return { simplifiedExpression: util.makeBinaryExpression( "+", x1, - val_node, + val_, ), safetyCondition: this.standardAdditiveCondition( - c1 as bigint, - val_ as bigint, + c1_.value, + val_.value, 0n, ), }; @@ -1021,19 +1064,21 @@ export class AssociativeRule3 extends Rule { [ "-", // original expression: c2 + (x1 - c1) - (x1, c1, c2, util) => { + (x1, c1, c2, util, s) => { // final expression x1 - (c1 - c2) - const val_ = evalBinaryOp("-", c1, c2); - const val_node = util.makeValueExpression(val_); + const val_ = iM.ensureInt( + iM.evalBinaryOp("-", c1, () => c2, s, util), + ); + const c1_ = iM.ensureInt(c1); return { simplifiedExpression: util.makeBinaryExpression( "-", x1, - val_node, + val_, ), safetyCondition: this.standardAdditiveCondition( - c1 as bigint, - val_ as bigint, + c1_.value, + val_.value, 0n, ), }; @@ -1048,19 +1093,21 @@ export class AssociativeRule3 extends Rule { [ "+", // original expression: c2 - (x1 + c1) - (x1, c1, c2, util) => { + (x1, c1, c2, util, s) => { // final expression (c2 - c1) - x1 - const val_ = evalBinaryOp("-", c2, c1); - const val_node = util.makeValueExpression(val_); + const val_ = iM.ensureInt( + iM.evalBinaryOp("-", c2, () => c1, s, util), + ); + const c1_ = iM.ensureInt(c1); return { simplifiedExpression: util.makeBinaryExpression( "-", - val_node, + val_, x1, ), safetyCondition: this.oppositeAdditiveCondition( - c1 as bigint, - val_ as bigint, + c1_.value, + val_.value, 0n, ), }; @@ -1070,19 +1117,21 @@ export class AssociativeRule3 extends Rule { [ "-", // original expression: c2 - (x1 - c1) - (x1, c1, c2, util) => { + (x1, c1, c2, util, s) => { // final expression (c2 + c1) - x1 - const val_ = evalBinaryOp("+", c2, c1); - const val_node = util.makeValueExpression(val_); + const val_ = iM.ensureInt( + iM.evalBinaryOp("+", c2, () => c1, s, util), + ); + const c1_ = iM.ensureInt(c1); return { simplifiedExpression: util.makeBinaryExpression( "-", - val_node, + val_, x1, ), safetyCondition: this.shiftedAdditiveCondition( - c1 as bigint, - val_ as bigint, + c1_.value, + val_.value, ), }; }, @@ -1098,19 +1147,21 @@ export class AssociativeRule3 extends Rule { [ "*", // original expression: c2 * (x1 * c1) - (x1, c1, c2, util) => { + (x1, c1, c2, util, s) => { // Final expression x1 * (c2 * c1) - const val_ = evalBinaryOp("*", c2, c1); - const val_node = util.makeValueExpression(val_); + const val_ = iM.ensureInt( + iM.evalBinaryOp("*", c2, () => c1, s, util), + ); + const c1_ = iM.ensureInt(c1); return { simplifiedExpression: util.makeBinaryExpression( "*", x1, - val_node, + val_, ), safetyCondition: this.standardMultiplicativeCondition( - c1 as bigint, - val_ as bigint, + c1_.value, + val_.value, ), }; }, @@ -1124,16 +1175,23 @@ export class AssociativeRule3 extends Rule { [ "&&", // original expression: c2 && (x1 && c1) - (x1, c1, c2, util) => { - const val_ = evalBinaryOp("&&", c2, c1); - const val_node = util.makeValueExpression(val_); + (x1, c1, c2, util, s) => { + const val_ = iM.evalBinaryOp( + "&&", + c2, + () => c1, + s, + util, + ); + const c1_ = iM.ensureBoolean(c1); + const c2_ = iM.ensureBoolean(c2); let final_expr; - if (c2 === true) { + if (c2_.value) { // Final expression x1 && (c2 && c1) final_expr = util.makeBinaryExpression( "&&", x1, - val_node, + val_, ); } else { // Final expression (c2 && c1) && x1 @@ -1142,13 +1200,13 @@ export class AssociativeRule3 extends Rule { // at this point c1 = true. final_expr = util.makeBinaryExpression( "&&", - val_node, + val_, x1, ); } return { simplifiedExpression: final_expr, - safetyCondition: c1 === true || c2 === true, + safetyCondition: c1_.value || c2_.value, }; }, ], @@ -1161,16 +1219,23 @@ export class AssociativeRule3 extends Rule { [ "||", // original expression: c2 || (x1 || c1) - (x1, c1, c2, util) => { - const val_ = evalBinaryOp("||", c2, c1); - const val_node = util.makeValueExpression(val_); + (x1, c1, c2, util, s) => { + const val_ = iM.evalBinaryOp( + "||", + c2, + () => c1, + s, + util, + ); + const c1_ = iM.ensureBoolean(c1); + const c2_ = iM.ensureBoolean(c2); let final_expr; - if (c2 === false) { + if (!c2_.value) { // Final expression x1 || (c2 || c1) final_expr = util.makeBinaryExpression( "||", x1, - val_node, + val_, ); } else { // Final expression (c2 || c1) || x1 @@ -1179,13 +1244,13 @@ export class AssociativeRule3 extends Rule { // at this point c1 = false. final_expr = util.makeBinaryExpression( "||", - val_node, + val_, x1, ); } return { simplifiedExpression: final_expr, - safetyCondition: c1 === false || c2 === false, + safetyCondition: !c1_.value || !c2_.value, }; }, ], @@ -1211,19 +1276,21 @@ export class AssociativeRule3 extends Rule { [ "+", // original expression: (c1 + x1) + c2 - (x1, c1, c2, util) => { + (x1, c1, c2, util, s) => { // Final expression (c1 + c2) + x1 - const val_ = evalBinaryOp("+", c1, c2); - const val_node = util.makeValueExpression(val_); + const val_ = iM.ensureInt( + iM.evalBinaryOp("+", c1, () => c2, s, util), + ); + const c1_ = iM.ensureInt(c1); return { simplifiedExpression: util.makeBinaryExpression( "+", - val_node, + val_, x1, ), safetyCondition: this.standardAdditiveCondition( - c1 as bigint, - val_ as bigint, + c1_.value, + val_.value, 0n, ), }; @@ -1233,19 +1300,21 @@ export class AssociativeRule3 extends Rule { [ "-", // original expression: (c1 + x1) - c2 - (x1, c1, c2, util) => { + (x1, c1, c2, util, s) => { // Final expression (c1 - c2) + x1 - const val_ = evalBinaryOp("-", c1, c2); - const val_node = util.makeValueExpression(val_); + const val_ = iM.ensureInt( + iM.evalBinaryOp("-", c1, () => c2, s, util), + ); + const c1_ = iM.ensureInt(c1); return { simplifiedExpression: util.makeBinaryExpression( "+", - val_node, + val_, x1, ), safetyCondition: this.standardAdditiveCondition( - c1 as bigint, - val_ as bigint, + c1_.value, + val_.value, 0n, ), }; @@ -1260,19 +1329,21 @@ export class AssociativeRule3 extends Rule { [ "+", // original expression: (c1 - x1) + c2 - (x1, c1, c2, util) => { + (x1, c1, c2, util, s) => { // Final expression (c1 + c2) - x1 - const val_ = evalBinaryOp("+", c1, c2); - const val_node = util.makeValueExpression(val_); + const val_ = iM.ensureInt( + iM.evalBinaryOp("+", c1, () => c2, s, util), + ); + const c1_ = iM.ensureInt(c1); return { simplifiedExpression: util.makeBinaryExpression( "-", - val_node, + val_, x1, ), safetyCondition: this.standardAdditiveCondition( - c1 as bigint, - val_ as bigint, + c1_.value, + val_.value, -1n, ), }; @@ -1282,19 +1353,21 @@ export class AssociativeRule3 extends Rule { [ "-", // original expression: (c1 - x1) - c2 - (x1, c1, c2, util) => { + (x1, c1, c2, util, s) => { // Final expression (c1 - c2) - x1 - const val_ = evalBinaryOp("-", c1, c2); - const val_node = util.makeValueExpression(val_); + const val_ = iM.ensureInt( + iM.evalBinaryOp("-", c1, () => c2, s, util), + ); + const c1_ = iM.ensureInt(c1); return { simplifiedExpression: util.makeBinaryExpression( "-", - val_node, + val_, x1, ), safetyCondition: this.standardAdditiveCondition( - c1 as bigint, - val_ as bigint, + c1_.value, + val_.value, -1n, ), }; @@ -1309,20 +1382,22 @@ export class AssociativeRule3 extends Rule { [ "*", // original expression: (c1 * x1) * c2 - (x1, c1, c2, util) => { + (x1, c1, c2, util, s) => { // Final expression (c1 * c2) * x1 - const val_ = evalBinaryOp("*", c1, c2); - const val_node = util.makeValueExpression(val_); + const val_ = iM.ensureInt( + iM.evalBinaryOp("*", c1, () => c2, s, util), + ); + const c1_ = iM.ensureInt(c1); return { simplifiedExpression: util.makeBinaryExpression( "*", - val_node, + val_, x1, ), safetyCondition: this.standardMultiplicativeCondition( - c1 as bigint, - val_ as bigint, + c1_.value, + val_.value, ), }; }, @@ -1336,15 +1411,22 @@ export class AssociativeRule3 extends Rule { [ "&&", // original expression: (c1 && x1) && c2 - (x1, c1, c2, util) => { - const val_ = evalBinaryOp("&&", c1, c2); - const val_node = util.makeValueExpression(val_); + (x1, c1, c2, util, s) => { + const val_ = iM.evalBinaryOp( + "&&", + c1, + () => c2, + s, + util, + ); + const c1_ = iM.ensureBoolean(c1); + const c2_ = iM.ensureBoolean(c2); let final_expr; - if (c2 === true) { + if (c2_.value) { // Final expression (c1 && c2) && x1 final_expr = util.makeBinaryExpression( "&&", - val_node, + val_, x1, ); } else { @@ -1355,12 +1437,12 @@ export class AssociativeRule3 extends Rule { final_expr = util.makeBinaryExpression( "&&", x1, - val_node, + val_, ); } return { simplifiedExpression: final_expr, - safetyCondition: c1 === true || c2 === true, + safetyCondition: c1_.value || c2_.value, }; }, ], @@ -1373,15 +1455,22 @@ export class AssociativeRule3 extends Rule { [ "||", // original expression: (c1 || x1) || c2 - (x1, c1, c2, util) => { - const val_ = evalBinaryOp("||", c1, c2); - const val_node = util.makeValueExpression(val_); + (x1, c1, c2, util, s) => { + const val_ = iM.evalBinaryOp( + "||", + c1, + () => c2, + s, + util, + ); + const c1_ = iM.ensureBoolean(c1); + const c2_ = iM.ensureBoolean(c2); let final_expr; - if (c2 === false) { + if (!c2_.value) { // Final expression (c1 || c2) || x1 final_expr = util.makeBinaryExpression( "||", - val_node, + val_, x1, ); } else { @@ -1392,12 +1481,12 @@ export class AssociativeRule3 extends Rule { final_expr = util.makeBinaryExpression( "||", x1, - val_node, + val_, ); } return { simplifiedExpression: final_expr, - safetyCondition: c1 === false || c2 === false, + safetyCondition: !c1_.value || !c2_.value, }; }, ], @@ -1466,19 +1555,19 @@ export class AssociativeRule3 extends Rule { const topLevelNode = ast as AstOpBinary; if ( checkIsBinaryOp_With_RightValue(topLevelNode.left) && - isValue(topLevelNode.right) + isLiteral(topLevelNode.right) ) { // The tree has this form: // (x1 op1 c1) op c2 const leftTree = topLevelNode.left as AstOpBinary; - const rightTree = topLevelNode.right as AstValue; + const rightTree = topLevelNode.right as AstLiteral; const x1 = leftTree.left; - const c1 = extractValue(leftTree.right as AstValue); + const c1 = leftTree.right as AstLiteral; const op1 = leftTree.op; - const c2 = extractValue(rightTree); + const c2 = rightTree; const op = topLevelNode.op; @@ -1488,6 +1577,7 @@ export class AssociativeRule3 extends Rule { c1, c2, util, + topLevelNode.loc, ); if (data.safetyCondition) { // Since the tree is simpler now, there is further @@ -1500,19 +1590,19 @@ export class AssociativeRule3 extends Rule { } } else if ( checkIsBinaryOp_With_LeftValue(topLevelNode.left) && - isValue(topLevelNode.right) + isLiteral(topLevelNode.right) ) { // The tree has this form: // (c1 op1 x1) op c2 const leftTree = topLevelNode.left as AstOpBinary; - const rightTree = topLevelNode.right as AstValue; + const rightTree = topLevelNode.right as AstLiteral; const x1 = leftTree.right; - const c1 = extractValue(leftTree.left as AstValue); + const c1 = leftTree.left as AstLiteral; const op1 = leftTree.op; - const c2 = extractValue(rightTree); + const c2 = rightTree; const op = topLevelNode.op; @@ -1522,6 +1612,7 @@ export class AssociativeRule3 extends Rule { c1, c2, util, + topLevelNode.loc, ); if (data.safetyCondition) { // Since the tree is simpler now, there is further @@ -1533,20 +1624,20 @@ export class AssociativeRule3 extends Rule { // Do nothing: will exit rule without modifying tree } } else if ( - isValue(topLevelNode.left) && + isLiteral(topLevelNode.left) && checkIsBinaryOp_With_RightValue(topLevelNode.right) ) { // The tree has this form: // c2 op (x1 op1 c1) - const leftTree = topLevelNode.left as AstValue; + const leftTree = topLevelNode.left as AstLiteral; const rightTree = topLevelNode.right as AstOpBinary; const x1 = rightTree.left; - const c1 = extractValue(rightTree.right as AstValue); + const c1 = rightTree.right as AstLiteral; const op1 = rightTree.op; - const c2 = extractValue(leftTree); + const c2 = leftTree; const op = topLevelNode.op; @@ -1556,6 +1647,7 @@ export class AssociativeRule3 extends Rule { c1, c2, util, + topLevelNode.loc, ); if (data.safetyCondition) { // Since the tree is simpler now, there is further @@ -1567,20 +1659,20 @@ export class AssociativeRule3 extends Rule { // Do nothing: will exit rule without modifying tree } } else if ( - isValue(topLevelNode.left) && + isLiteral(topLevelNode.left) && checkIsBinaryOp_With_LeftValue(topLevelNode.right) ) { // The tree has this form: // c2 op (c1 op1 x1) - const leftTree = topLevelNode.left as AstValue; + const leftTree = topLevelNode.left as AstLiteral; const rightTree = topLevelNode.right as AstOpBinary; const x1 = rightTree.right; - const c1 = extractValue(rightTree.left as AstValue); + const c1 = rightTree.left as AstLiteral; const op1 = rightTree.op; - const c2 = extractValue(leftTree); + const c2 = leftTree; const op = topLevelNode.op; @@ -1590,6 +1682,7 @@ export class AssociativeRule3 extends Rule { c1, c2, util, + topLevelNode.loc, ); if (data.safetyCondition) { // Since the tree is simpler now, there is further diff --git a/src/optimizer/test/partial-eval.spec.ts b/src/optimizer/test/partial-eval.spec.ts index 8a23f4f0a..34cb418b5 100644 --- a/src/optimizer/test/partial-eval.spec.ts +++ b/src/optimizer/test/partial-eval.spec.ts @@ -1,12 +1,11 @@ import { AstExpression, FactoryAst, - AstValue, eqExpressions, getAstFactory, - isValue, + isLiteral, } from "../../grammar/ast"; -import { AstUtil, extractValue, getAstUtil } from "../util"; +import { AstUtil, getAstUtil } from "../util"; import { getOptimizer } from "../../constEval"; import { CompilerContext } from "../../context"; import { ExpressionTransformer, Rule } from "../types"; @@ -14,6 +13,7 @@ import { AssociativeRule3 } from "../associative"; import { evalBinaryOp, evalUnaryOp } from "../../interpreter"; import { getParser } from "../../grammar"; import { defaultParser } from "../../grammar/grammar"; +import { throwInternalCompilerError } from "../../errors"; const MAX: string = "115792089237316195423570985008687907853269984665640564039457584007913129639935"; @@ -324,11 +324,7 @@ function testExpression(original: string, simplified: string) { parseExpression(original), new CompilerContext(), ); - const simplifiedValue = dummyEval( - parseExpression(simplified), - ast, - util, - ); + const simplifiedValue = dummyEval(parseExpression(simplified), ast); const areMatching = eqExpressions(originalValue, simplifiedValue); expect(areMatching).toBe(true); }); @@ -342,15 +338,10 @@ function testExpressionWithOptimizer( it(`should simplify ${original} to ${simplified}`, () => { const ast = getAstFactory(); const { parseExpression } = getParser(ast, defaultParser); - const util = getAstUtil(ast); const originalValue = optimizer.applyRules( - dummyEval(parseExpression(original), ast, util), - ); - const simplifiedValue = dummyEval( - parseExpression(simplified), - ast, - util, + dummyEval(parseExpression(original), ast), ); + const simplifiedValue = dummyEval(parseExpression(simplified), ast); const areMatching = eqExpressions(originalValue, simplifiedValue); expect(areMatching).toBe(true); }); @@ -361,11 +352,9 @@ function testExpressionWithOptimizer( // The reason for doing this is that the partial evaluator will actually simplify constant // expressions. So, when comparing for equality of expressions, we also need to simplify // constant expressions. -function dummyEval( - ast: AstExpression, - { cloneNode }: FactoryAst, - { makeValueExpression }: AstUtil, -): AstExpression { +function dummyEval(ast: AstExpression, astFactory: FactoryAst): AstExpression { + const cloneNode = astFactory.cloneNode; + const util = getAstUtil(astFactory); const recurse = (ast: AstExpression): AstExpression => { switch (ast.kind) { case "null": @@ -378,6 +367,18 @@ function dummyEval( return ast; case "id": return ast; + case "address": + return ast; + case "cell": + return ast; + case "comment_value": + return ast; + case "simplified_string": + return ast; + case "slice": + return ast; + case "struct_value": + return ast; // No need to simplify: fields already simplified case "method_call": { const newNode = cloneNode(ast); newNode.args = ast.args.map(recurse); @@ -392,13 +393,8 @@ function dummyEval( case "op_unary": { const newNode = cloneNode(ast); newNode.operand = recurse(ast.operand); - if (isValue(newNode.operand)) { - return makeValueExpression( - evalUnaryOp( - ast.op, - extractValue(newNode.operand as AstValue), - ), - ); + if (isLiteral(newNode.operand)) { + return evalUnaryOp(ast.op, newNode.operand, ast.loc, util); } return newNode; } @@ -406,14 +402,14 @@ function dummyEval( const newNode = cloneNode(ast); newNode.left = recurse(ast.left); newNode.right = recurse(ast.right); - if (isValue(newNode.left) && isValue(newNode.right)) { - const valR = extractValue(newNode.right as AstValue); - return makeValueExpression( - evalBinaryOp( - ast.op, - extractValue(newNode.left as AstValue), - () => valR, - ), + if (isLiteral(newNode.left) && isLiteral(newNode.right)) { + const valR = newNode.right; + return evalBinaryOp( + ast.op, + newNode.left, + () => valR, + ast.loc, + util, ); } return newNode; @@ -443,9 +439,10 @@ function dummyEval( newNode.args = ast.args.map(recurse); return newNode; } + default: + throwInternalCompilerError("Unrecognized expression kind"); } }; - return recurse(ast); } diff --git a/src/optimizer/util.ts b/src/optimizer/util.ts index dc6ff85bd..4033b1243 100644 --- a/src/optimizer/util.ts +++ b/src/optimizer/util.ts @@ -1,69 +1,26 @@ +import { Address, Cell, Slice } from "@ton/core"; import { AstExpression, AstUnaryOperation, AstBinaryOperation, - AstValue, - isValue, + isLiteral, + AstNumber, + AstBoolean, + AstSimplifiedString, + AstNull, + AstCell, + AstSlice, + AstAddress, + AstLiteral, + AstStructValue, + AstStructFieldValue, + AstId, + AstCommentValue, FactoryAst, } from "../grammar/ast"; -import { dummySrcInfo } from "../grammar"; -import { throwInternalCompilerError } from "../errors"; -import { Value } from "../types/types"; - -export function extractValue(ast: AstValue): 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; - } -} +import { dummySrcInfo, SrcInfo } from "../grammar"; export const getAstUtil = ({ createNode }: FactoryAst) => { - function makeValueExpression(value: Value): AstValue { - if (value === null) { - const result = createNode({ - kind: "null", - loc: dummySrcInfo, - }); - return result as AstValue; - } - if (typeof value === "string") { - const result = createNode({ - kind: "string", - value: value, - loc: dummySrcInfo, - }); - return result as AstValue; - } - if (typeof value === "bigint") { - const result = createNode({ - kind: "number", - base: 10, - value: value, - loc: dummySrcInfo, - }); - return result as AstValue; - } - if (typeof value === "boolean") { - const result = createNode({ - kind: "boolean", - value: value, - loc: dummySrcInfo, - }); - return result as AstValue; - } - throwInternalCompilerError( - `structs, addresses, cells, and comment values are not supported at the moment.`, - ); - } - function makeUnaryExpression( op: AstUnaryOperation, operand: AstExpression, @@ -92,10 +49,126 @@ export const getAstUtil = ({ createNode }: FactoryAst) => { return result as AstExpression; } + function makeNumberLiteral(n: bigint, loc: SrcInfo): AstNumber { + const result = createNode({ + kind: "number", + base: 10, + value: n, + loc: loc, + }); + return result as AstNumber; + } + + function makeBooleanLiteral(b: boolean, loc: SrcInfo): AstBoolean { + const result = createNode({ + kind: "boolean", + value: b, + loc: loc, + }); + return result as AstBoolean; + } + + function makeSimplifiedStringLiteral( + s: string, + loc: SrcInfo, + ): AstSimplifiedString { + const result = createNode({ + kind: "simplified_string", + value: s, + loc: loc, + }); + return result as AstSimplifiedString; + } + + function makeCommentLiteral(s: string, loc: SrcInfo): AstCommentValue { + const result = createNode({ + kind: "comment_value", + value: s, + loc: loc, + }); + return result as AstCommentValue; + } + + function makeNullLiteral(loc: SrcInfo): AstNull { + const result = createNode({ + kind: "null", + loc: loc, + }); + return result as AstNull; + } + + function makeCellLiteral(c: Cell, loc: SrcInfo): AstCell { + const result = createNode({ + kind: "cell", + value: c, + loc: loc, + }); + return result as AstCell; + } + + function makeSliceLiteral(s: Slice, loc: SrcInfo): AstSlice { + const result = createNode({ + kind: "slice", + value: s, + loc: loc, + }); + return result as AstSlice; + } + + function makeAddressLiteral(a: Address, loc: SrcInfo): AstAddress { + const result = createNode({ + kind: "address", + value: a, + loc: loc, + }); + return result as AstAddress; + } + + function makeStructFieldValue( + fieldName: string, + val: AstLiteral, + loc: SrcInfo, + ): AstStructFieldValue { + const result = createNode({ + kind: "struct_field_value", + field: createNode({ + kind: "id", + text: fieldName, + loc: loc, + }) as AstId, + initializer: val, + loc: loc, + }); + return result as AstStructFieldValue; + } + + function makeStructValue( + fields: AstStructFieldValue[], + type: AstId, + loc: SrcInfo, + ): AstStructValue { + const result = createNode({ + kind: "struct_value", + args: fields, + loc: loc, + type: type, + }); + return result as AstStructValue; + } + return { - makeValueExpression, makeUnaryExpression, makeBinaryExpression, + makeNumberLiteral, + makeBooleanLiteral, + makeSimplifiedStringLiteral, + makeCommentLiteral, + makeNullLiteral, + makeCellLiteral, + makeSliceLiteral, + makeAddressLiteral, + makeStructFieldValue, + makeStructValue, }; }; @@ -114,13 +187,13 @@ export function checkIsBinaryOpNode(ast: AstExpression): boolean { // Checks if top level node is a binary op node // with a value node on the right export function checkIsBinaryOp_With_RightValue(ast: AstExpression): boolean { - return ast.kind === "op_binary" ? isValue(ast.right) : false; + return ast.kind === "op_binary" ? isLiteral(ast.right) : false; } // Checks if top level node is a binary op node // with a value node on the left export function checkIsBinaryOp_With_LeftValue(ast: AstExpression): boolean { - return ast.kind === "op_binary" ? isValue(ast.left) : false; + return ast.kind === "op_binary" ? isLiteral(ast.left) : false; } // Checks if the top level node is the specified number diff --git a/src/pipeline/precompile.ts b/src/pipeline/precompile.ts index 8695f5c14..7c233f9c6 100644 --- a/src/pipeline/precompile.ts +++ b/src/pipeline/precompile.ts @@ -30,16 +30,16 @@ export function precompile( ctx = resolveDescriptors(ctx, ast); // This creates TLB-style type definitions - ctx = resolveSignatures(ctx); + ctx = resolveSignatures(ctx, ast); // This creates allocations for all defined types ctx = resolveAllocations(ctx); // This checks and resolves all statements - ctx = resolveStatements(ctx); + ctx = resolveStatements(ctx, ast); // This extracts error messages - ctx = resolveErrors(ctx); + ctx = resolveErrors(ctx, ast); // Prepared context return ctx; diff --git a/src/prettyPrinter.ts b/src/prettyPrinter.ts index 04f9a441f..6724fbf76 100644 --- a/src/prettyPrinter.ts +++ b/src/prettyPrinter.ts @@ -170,9 +170,13 @@ export const ppExprArgs = (args: A.AstExpression[]) => export const ppAstStructFieldInit = ( param: A.AstStructFieldInitializer, ): string => `${ppAstId(param.field)}: ${ppAstExpression(param.initializer)}`; +export const ppAstStructFieldValue = (param: A.AstStructFieldValue): string => + `${ppAstId(param.field)}: ${ppAstExpression(param.initializer)}`; export const ppAstStructInstance = ({ type, args }: A.AstStructInstance) => `${ppAstId(type)}{${args.map((x) => ppAstStructFieldInit(x)).join(", ")}}`; +export const ppAstStructValue = ({ type, args }: A.AstStructValue) => + `${ppAstId(type)}{${args.map((x) => ppAstStructFieldValue(x)).join(", ")}}`; export const ppAstInitOf = ({ contract, args }: A.AstInitOf) => `initOf ${ppAstId(contract)}(${ppExprArgs(args)})`; @@ -182,6 +186,16 @@ export const ppAstBoolean = ({ value }: A.AstBoolean) => value.toString(); export const ppAstId = ({ text }: A.AstId) => text; export const ppAstNull = (_expr: A.AstNull) => "null"; export const ppAstString = ({ value }: A.AstString) => `"${value}"`; +export const ppAstCommentValue = ({ value }: A.AstCommentValue) => + JSON.stringify(value); +export const ppAstSimplifiedString = ({ value }: A.AstSimplifiedString) => + JSON.stringify(value); +export const ppAstAddress = ({ value }: A.AstAddress) => + `addr("${value.toRawString()}")`; +export const ppAstCell = ({ value }: A.AstCell) => + `cell("${value.toString()}")`; +export const ppAstSlice = ({ value }: A.AstSlice) => + `slice("${value.toString()}")`; export const ppAstStaticCall = ({ function: func, args }: A.AstStaticCall) => { return `${ppAstId(func)}(${ppExprArgs(args)})`; @@ -239,6 +253,7 @@ export const ppAstConditional: ExprPrinter = export const ppAstExpressionNested = makeVisitor()({ struct_instance: ppLeaf(ppAstStructInstance), + struct_value: ppLeaf(ppAstStructValue), number: ppLeaf(ppAstNumber), boolean: ppLeaf(ppAstBoolean), id: ppLeaf(ppAstId), @@ -246,6 +261,11 @@ export const ppAstExpressionNested = makeVisitor()({ init_of: ppLeaf(ppAstInitOf), string: ppLeaf(ppAstString), static_call: ppLeaf(ppAstStaticCall), + comment_value: ppLeaf(ppAstCommentValue), + simplified_string: ppLeaf(ppAstSimplifiedString), + address: ppLeaf(ppAstAddress), + cell: ppLeaf(ppAstCell), + slice: ppLeaf(ppAstSlice), method_call: ppAstMethodCall, field_access: ppAstFieldAccess, @@ -829,18 +849,25 @@ export const ppAstNode: Printer = makeVisitor()({ method_call: exprNode(ppAstExpression), static_call: exprNode(ppAstExpression), struct_instance: exprNode(ppAstExpression), + struct_value: exprNode(ppAstStructValue), init_of: exprNode(ppAstExpression), conditional: exprNode(ppAstExpression), number: exprNode(ppAstExpression), id: exprNode(ppAstExpression), boolean: exprNode(ppAstExpression), string: exprNode(ppAstExpression), + comment_value: exprNode(ppAstExpression), + simplified_string: exprNode(ppAstExpression), null: exprNode(ppAstExpression), + address: exprNode(ppAstExpression), + cell: exprNode(ppAstExpression), + slice: exprNode(ppAstExpression), type_id: exprNode(ppAstType), optional_type: exprNode(ppAstType), map_type: exprNode(ppAstType), bounced_message_type: exprNode(ppAstType), struct_field_initializer: exprNode(ppAstStructFieldInit), + struct_field_value: exprNode(ppAstStructFieldValue), destruct_mapping: () => { throw new Error("Not implemented"); }, diff --git a/src/storage/__snapshots__/resolveAllocation.spec.ts.snap b/src/storage/__snapshots__/resolveAllocation.spec.ts.snap index 18186e26f..8e263504f 100644 --- a/src/storage/__snapshots__/resolveAllocation.spec.ts.snap +++ b/src/storage/__snapshots__/resolveAllocation.spec.ts.snap @@ -2189,7 +2189,13 @@ exports[`resolveAllocation should write program 1`] = ` "text": "Int", }, }, - "default": 0n, + "default": { + "base": 10, + "id": 90, + "kind": "number", + "loc": {}, + "value": 0n, + }, "index": 0, "loc": {}, "name": "v", diff --git a/src/storage/resolveAllocation.spec.ts b/src/storage/resolveAllocation.spec.ts index e784bb90c..1f01614e6 100644 --- a/src/storage/resolveAllocation.spec.ts +++ b/src/storage/resolveAllocation.spec.ts @@ -75,8 +75,8 @@ describe("resolveAllocation", () => { getParser(ast, defaultParser), ); ctx = resolveDescriptors(ctx, ast); - ctx = resolveSignatures(ctx); - ctx = resolveStatements(ctx); + ctx = resolveSignatures(ctx, ast); + ctx = resolveStatements(ctx, ast); ctx = resolveAllocations(ctx); expect(getAllocations(ctx)).toMatchSnapshot(); }); diff --git a/src/test/compilation-failed/const-eval-failed.spec.ts b/src/test/compilation-failed/const-eval-failed.spec.ts index 61f60ab21..6d073e539 100644 --- a/src/test/compilation-failed/const-eval-failed.spec.ts +++ b/src/test/compilation-failed/const-eval-failed.spec.ts @@ -156,7 +156,7 @@ describe("fail-const-eval", () => { itShouldNotCompile({ testName: "const-eval-repeat-upper-bound", errorMessage: - "Cannot evaluate expression to a constant: repeat argument must be a number between -2^256 (inclusive) and 2^31 - 1 (inclusive)", + "Cannot evaluate expression to a constant: repeat argument '2147483648' must be a number between -2^256 (inclusive) and 2^31 - 1 (inclusive)", }); itShouldNotCompile({ testName: "const-eval-ascii-overflow", diff --git a/src/types/__snapshots__/resolveDescriptors.spec.ts.snap b/src/types/__snapshots__/resolveDescriptors.spec.ts.snap index 976d617ec..adcfecf6e 100644 --- a/src/types/__snapshots__/resolveDescriptors.spec.ts.snap +++ b/src/types/__snapshots__/resolveDescriptors.spec.ts.snap @@ -1739,7 +1739,13 @@ exports[`resolveDescriptors should resolve descriptors for const-decl-struct-wit "text": "Int", }, }, - "default": 42n, + "default": { + "base": 10, + "id": 8, + "kind": "number", + "loc": 42, + "value": 42n, + }, "index": 0, "loc": s: Int = 42, "name": "s", @@ -1908,7 +1914,11 @@ exports[`resolveDescriptors should resolve descriptors for const-decl-struct-wit }, }, }, - "default": null, + "default": { + "id": 24, + "kind": "null", + "loc": s: Int?, + }, "index": 0, "loc": s: Int?, "name": "s", @@ -3307,7 +3317,13 @@ exports[`resolveDescriptors should resolve descriptors for contract-const-overri "name": "Int", "optional": false, }, - "value": 42n, + "value": { + "base": 10, + "id": 14, + "kind": "number", + "loc": 42, + "value": 42n, + }, }, ], "dependsOn": [], @@ -3650,7 +3666,13 @@ exports[`resolveDescriptors should resolve descriptors for contract-const-overri "name": "Int", "optional": false, }, - "value": 41n, + "value": { + "base": 10, + "id": 8, + "kind": "number", + "loc": 41, + "value": 41n, + }, }, ], "dependsOn": [], @@ -3796,7 +3818,13 @@ exports[`resolveDescriptors should resolve descriptors for contract-const-overri "name": "Int", "optional": false, }, - "value": 42n, + "value": { + "base": 10, + "id": 15, + "kind": "number", + "loc": 42, + "value": 42n, + }, }, ], "dependsOn": [], @@ -3943,7 +3971,13 @@ exports[`resolveDescriptors should resolve descriptors for contract-const-overri "name": "Int", "optional": false, }, - "value": 41n, + "value": { + "base": 10, + "id": 8, + "kind": "number", + "loc": 41, + "value": 41n, + }, }, ], "dependsOn": [], @@ -12816,7 +12850,13 @@ exports[`resolveDescriptors should resolve descriptors for struct-decl-default-f "text": "Int", }, }, - "default": 42n, + "default": { + "base": 10, + "id": 8, + "kind": "number", + "loc": 42, + "value": 42n, + }, "index": 0, "loc": a: Int = 42, "name": "a", @@ -13390,7 +13430,11 @@ exports[`resolveDescriptors should resolve descriptors for struct-decl-non-rec-t }, }, }, - "default": null, + "default": { + "id": 20, + "kind": "null", + "loc": value: SomeStruct?, + }, "index": 1, "loc": value: SomeStruct?, "name": "value", diff --git a/src/types/resolveDescriptors.spec.ts b/src/types/resolveDescriptors.spec.ts index c662b9cf7..2c5dc177e 100644 --- a/src/types/resolveDescriptors.spec.ts +++ b/src/types/resolveDescriptors.spec.ts @@ -30,7 +30,7 @@ describe("resolveDescriptors", () => { ); ctx = featureEnable(ctx, "external"); ctx = resolveDescriptors(ctx, Ast); - ctx = resolveSignatures(ctx); + ctx = resolveSignatures(ctx, Ast); expect(getAllTypes(ctx)).toMatchSnapshot(); expect(getAllStaticFunctions(ctx)).toMatchSnapshot(); }); @@ -47,7 +47,7 @@ describe("resolveDescriptors", () => { ctx = featureEnable(ctx, "external"); expect(() => { ctx = resolveDescriptors(ctx, Ast); - ctx = resolveSignatures(ctx); + ctx = resolveSignatures(ctx, Ast); }).toThrowErrorMatchingSnapshot(); }); } diff --git a/src/types/resolveDescriptors.ts b/src/types/resolveDescriptors.ts index 2f01f4c9e..bd82e7ade 100644 --- a/src/types/resolveDescriptors.ts +++ b/src/types/resolveDescriptors.ts @@ -57,6 +57,7 @@ import { ItemOrigin } from "../grammar"; import { getExpType, resolveExpression } from "./resolveExpression"; import { emptyContext } from "./resolveStatements"; import { isAssignable } from "./subtyping"; +import { AstUtil, getAstUtil } from "../optimizer/util"; const store = createContextStore(); const staticFunctionsStore = createContextStore(); @@ -290,6 +291,7 @@ export function resolveDescriptors(ctx: CompilerContext, Ast: FactoryAst) { const staticFunctions: Map = new Map(); const staticConstants: Map = new Map(); const ast = getRawAST(ctx); + const util = getAstUtil(Ast); // // Register types @@ -1970,7 +1972,7 @@ export function resolveDescriptors(ctx: CompilerContext, Ast: FactoryAst) { } // A pass that initializes constants and default field values - ctx = initializeConstantsAndDefaultContractAndStructFields(ctx); + ctx = initializeConstantsAndDefaultContractAndStructFields(ctx, util); // detect self-referencing or mutually-recursive types checkRecursiveTypes(ctx); @@ -2118,6 +2120,7 @@ function checkInitializerType( function initializeConstants( constants: ConstantDescription[], ctx: CompilerContext, + util: AstUtil, ): CompilerContext { for (const constant of constants) { if (constant.ast.kind === "constant_def") { @@ -2131,6 +2134,7 @@ function initializeConstants( constant.value = evalConstantExpression( constant.ast.initializer, ctx, + util, ); } } @@ -2139,6 +2143,7 @@ function initializeConstants( function initializeConstantsAndDefaultContractAndStructFields( ctx: CompilerContext, + util: AstUtil, ): CompilerContext { for (const aggregateTy of getAllTypes(ctx)) { switch (aggregateTy.kind) { @@ -2160,6 +2165,7 @@ function initializeConstantsAndDefaultContractAndStructFields( field.default = evalConstantExpression( field.ast.initializer, ctx, + util, ); } else { // if a field has optional type and it is missing an explicit initializer @@ -2167,14 +2173,14 @@ function initializeConstantsAndDefaultContractAndStructFields( field.default = field.type.kind === "ref" && field.type.optional - ? null + ? util.makeNullLiteral(field.ast.loc) : undefined; } } // constants need to be processed after structs because // see more detail below - ctx = initializeConstants(aggregateTy.constants, ctx); + ctx = initializeConstants(aggregateTy.constants, ctx, util); } break; } @@ -2184,7 +2190,7 @@ function initializeConstantsAndDefaultContractAndStructFields( // constants need to be processed after structs because // constants might use default field values: `const x: Int = S{}.f`, where `struct S {f: Int = 42}` // and the default field values are filled in during struct field initializers processing - ctx = initializeConstants(getAllStaticConstants(ctx), ctx); + ctx = initializeConstants(getAllStaticConstants(ctx), ctx, util); return ctx; } diff --git a/src/types/resolveErrors.ts b/src/types/resolveErrors.ts index 29e3e00d3..4a4bf9986 100644 --- a/src/types/resolveErrors.ts +++ b/src/types/resolveErrors.ts @@ -1,6 +1,6 @@ import { sha256_sync } from "@ton/crypto"; import { CompilerContext, createContextStore } from "../context"; -import { AstNode, isRequire } from "../grammar/ast"; +import { AstNode, FactoryAst, isRequire } from "../grammar/ast"; import { traverse } from "../grammar/iterators"; import { evalConstantExpression } from "../constEval"; import { throwInternalCompilerError } from "../errors"; @@ -9,6 +9,8 @@ import { getAllTypes, getAllStaticConstants, } from "./resolveDescriptors"; +import { ensureSimplifiedString } from "../interpreter"; +import { AstUtil, getAstUtil } from "../optimizer/util"; type Exception = { value: string; id: number }; @@ -22,16 +24,19 @@ function exceptionId(src: string): number { return (stringId(src) % 63000) + 1000; } -function resolveStringsInAST(ast: AstNode, ctx: CompilerContext) { +function resolveStringsInAST( + ast: AstNode, + ctx: CompilerContext, + util: AstUtil, +) { traverse(ast, (node) => { if (node.kind === "static_call" && isRequire(node.function)) { if (node.args.length !== 2) { return; } - const resolved = evalConstantExpression( - node.args[1]!, - ctx, - ) as string; + const resolved = ensureSimplifiedString( + evalConstantExpression(node.args[1]!, ctx, util), + ).value; if (!exceptions.get(ctx, resolved)) { const id = exceptionId(resolved); if ( @@ -50,42 +55,44 @@ function resolveStringsInAST(ast: AstNode, ctx: CompilerContext) { return ctx; } -export function resolveErrors(ctx: CompilerContext) { +export function resolveErrors(ctx: CompilerContext, Ast: FactoryAst) { + const util = getAstUtil(Ast); + // Process all static functions for (const f of getAllStaticFunctions(ctx)) { - ctx = resolveStringsInAST(f.ast, ctx); + ctx = resolveStringsInAST(f.ast, ctx, util); } // Process all static constants for (const f of getAllStaticConstants(ctx)) { - ctx = resolveStringsInAST(f.ast, ctx); + ctx = resolveStringsInAST(f.ast, ctx, util); } // Process all types for (const t of getAllTypes(ctx)) { // Process fields for (const f of t.fields) { - ctx = resolveStringsInAST(f.ast, ctx); + ctx = resolveStringsInAST(f.ast, ctx, util); } // Process constants for (const f of t.constants) { - ctx = resolveStringsInAST(f.ast, ctx); + ctx = resolveStringsInAST(f.ast, ctx, util); } // Process init if (t.init) { - ctx = resolveStringsInAST(t.init.ast, ctx); + ctx = resolveStringsInAST(t.init.ast, ctx, util); } // Process receivers for (const f of t.receivers) { - ctx = resolveStringsInAST(f.ast, ctx); + ctx = resolveStringsInAST(f.ast, ctx, util); } // Process functions for (const f of t.functions.values()) { - ctx = resolveStringsInAST(f.ast, ctx); + ctx = resolveStringsInAST(f.ast, ctx, util); } } diff --git a/src/types/resolveExpression.ts b/src/types/resolveExpression.ts index f3d3e9c6e..f4ba9df24 100644 --- a/src/types/resolveExpression.ts +++ b/src/types/resolveExpression.ts @@ -15,6 +15,12 @@ import { eqNames, idText, isWildcard, + AstAddress, + AstCell, + AstSlice, + AstSimplifiedString, + AstCommentValue, + AstStructValue, } from "../grammar/ast"; import { idTextErr, throwCompilationError } from "../errors"; import { CompilerContext, createContextStore } from "../context"; @@ -98,8 +104,44 @@ function resolveNullLiteral( return registerExpType(ctx, exp, { kind: "null" }); } +function resolveAddressLiteral( + exp: AstAddress, + sctx: StatementContext, + ctx: CompilerContext, +): CompilerContext { + return registerExpType(ctx, exp, { + kind: "ref", + name: "Address", + optional: false, + }); +} + +function resolveCellLiteral( + exp: AstCell, + sctx: StatementContext, + ctx: CompilerContext, +): CompilerContext { + return registerExpType(ctx, exp, { + kind: "ref", + name: "Cell", + optional: false, + }); +} + +function resolveSliceLiteral( + exp: AstSlice, + sctx: StatementContext, + ctx: CompilerContext, +): CompilerContext { + return registerExpType(ctx, exp, { + kind: "ref", + name: "Slice", + optional: false, + }); +} + function resolveStringLiteral( - exp: AstString, + exp: AstString | AstSimplifiedString | AstCommentValue, sctx: StatementContext, ctx: CompilerContext, ): CompilerContext { @@ -111,7 +153,7 @@ function resolveStringLiteral( } function resolveStructNew( - exp: AstStructInstance, + exp: AstStructInstance | AstStructValue, sctx: StatementContext, ctx: CompilerContext, ): CompilerContext { @@ -765,6 +807,27 @@ export function resolveExpression( case "string": { return resolveStringLiteral(exp, sctx, ctx); } + case "address": { + return resolveAddressLiteral(exp, sctx, ctx); + } + case "cell": { + return resolveCellLiteral(exp, sctx, ctx); + } + case "slice": { + return resolveSliceLiteral(exp, sctx, ctx); + } + case "simplified_string": { + // A simplified string is resolved as a string + return resolveStringLiteral(exp, sctx, ctx); + } + case "comment_value": { + // A comment value is resolved as a string + return resolveStringLiteral(exp, sctx, ctx); + } + case "struct_value": { + // A struct value is resolved as a struct instance + return resolveStructNew(exp, sctx, ctx); + } case "struct_instance": { return resolveStructNew(exp, sctx, ctx); } diff --git a/src/types/resolveSignatures.ts b/src/types/resolveSignatures.ts index e940bfe47..b5f5ec72f 100644 --- a/src/types/resolveSignatures.ts +++ b/src/types/resolveSignatures.ts @@ -14,14 +14,17 @@ import { ReceiverDescription, } from "./types"; import { throwCompilationError } from "../errors"; -import { AstNumber, AstReceiver } from "../grammar/ast"; +import { AstNumber, AstReceiver, FactoryAst } from "../grammar/ast"; import { commentPseudoOpcode } from "../generator/writers/writeRouter"; import { sha256_sync } from "@ton/crypto"; import { dummySrcInfo } from "../grammar"; import { ensureInt } from "../interpreter"; import { evalConstantExpression } from "../constEval"; +import { getAstUtil } from "../optimizer/util"; + +export function resolveSignatures(ctx: CompilerContext, Ast: FactoryAst) { + const util = getAstUtil(Ast); -export function resolveSignatures(ctx: CompilerContext) { const signatures: Map< string, { signature: string; tlb: string; id: AstNumber | null } @@ -203,9 +206,8 @@ export function resolveSignatures(ctx: CompilerContext) { // ``` // WILL NOT result in error const opCode = ensureInt( - evalConstantExpression(t.ast.opcode, ctx), - t.ast.opcode.loc, - ); + evalConstantExpression(t.ast.opcode, ctx, util), + ).value; if (opCode === 0n) { throwConstEvalError( `Opcode of message ${idTextErr(t.ast.name)} is zero: those are reserved for text comments and cannot be used for message structs`, diff --git a/src/types/resolveStatements.spec.ts b/src/types/resolveStatements.spec.ts index 90c2c0d81..1cb1e39a8 100644 --- a/src/types/resolveStatements.spec.ts +++ b/src/types/resolveStatements.spec.ts @@ -21,7 +21,7 @@ describe("resolveStatements", () => { ); ctx = featureEnable(ctx, "external"); ctx = resolveDescriptors(ctx, Ast); - ctx = resolveStatements(ctx); + ctx = resolveStatements(ctx, Ast); expect(getAllExpressionTypes(ctx)).toMatchSnapshot(); }); } @@ -36,7 +36,9 @@ describe("resolveStatements", () => { ); ctx = featureEnable(ctx, "external"); ctx = resolveDescriptors(ctx, Ast); - expect(() => resolveStatements(ctx)).toThrowErrorMatchingSnapshot(); + expect(() => + resolveStatements(ctx, Ast), + ).toThrowErrorMatchingSnapshot(); }); } }); diff --git a/src/types/resolveStatements.ts b/src/types/resolveStatements.ts index e5ca22bbf..39d01d67c 100644 --- a/src/types/resolveStatements.ts +++ b/src/types/resolveStatements.ts @@ -9,6 +9,7 @@ import { selfId, isSelfId, eqNames, + FactoryAst, } from "../grammar/ast"; import { isAssignable } from "./subtyping"; import { @@ -31,6 +32,7 @@ import { evalConstantExpression } from "../constEval"; import { ensureInt } from "../interpreter"; import { crc16 } from "../utils/crc16"; import { SrcInfo } from "../grammar"; +import { AstUtil, getAstUtil } from "../optimizer/util"; export type StatementContext = { root: SrcInfo; @@ -798,7 +800,9 @@ function processFunctionBody( return res.ctx; } -export function resolveStatements(ctx: CompilerContext) { +export function resolveStatements(ctx: CompilerContext, Ast: FactoryAst) { + const util = getAstUtil(Ast); + // Process all static functions for (const f of getAllStaticFunctions(ctx)) { if (f.ast.kind === "function_def") { @@ -953,7 +957,7 @@ export function resolveStatements(ctx: CompilerContext) { // Check for collisions in getter method IDs if (f.isGetter) { - const methodId = getMethodId(f, ctx, sctx); + const methodId = getMethodId(f, ctx, sctx, util); const existing = methodIds.get(methodId); if (existing) { throwCompilationError( @@ -1016,6 +1020,7 @@ function getMethodId( funcDescr: FunctionDescription, ctx: CompilerContext, sctx: StatementContext, + util: AstUtil, ): number { const optMethodId = funcDescr.ast.attributes.find( (attr) => attr.type === "get", @@ -1032,9 +1037,8 @@ function getMethodId( } const methodId = ensureInt( - evalConstantExpression(optMethodId, ctx), - optMethodId.loc, - ); + evalConstantExpression(optMethodId, ctx, util), + ).value; checkMethodId(methodId, optMethodId.loc); return Number(methodId); } else { diff --git a/src/types/types.ts b/src/types/types.ts index 4bcdd65fa..de49c2e56 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -1,4 +1,4 @@ -import { ABIField, Address, Cell, Slice } from "@ton/core"; +import { ABIField } from "@ton/core"; import { throwInternalCompilerError } from "../errors"; import { AstConstantDef, @@ -13,8 +13,10 @@ import { AstFieldDecl, AstAsmFunctionDef, AstNumber, + AstLiteral, + idText, } from "../grammar/ast"; -import { dummySrcInfo, ItemOrigin, SrcInfo } from "../grammar"; +import { ItemOrigin, SrcInfo } from "../grammar"; export type TypeDescription = { kind: "struct" | "primitive_type_decl" | "contract" | "trait"; @@ -63,47 +65,32 @@ export type TypeRef = // https://github.com/microsoft/TypeScript/issues/35164 and // https://github.com/microsoft/TypeScript/pull/57293 // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style -export type StructValue = { - [key: string]: Value; -}; - -export class CommentValue { - constructor(public readonly comment: string) {} -} -export type Value = - | bigint - | boolean - | string - | Address - | Cell - | Slice - | null - | CommentValue - | StructValue; - -export function showValue(val: Value): string { - if (typeof val === "bigint") { - return val.toString(10); - } else if (typeof val === "string") { - return val; - } else if (typeof val === "boolean") { - return val ? "true" : "false"; - } else if (Address.isAddress(val)) { - return val.toRawString(); - } else if (val instanceof Cell || val instanceof Slice) { - return val.toString(); - } else if (val === null) { - return "null"; - } else if (val instanceof CommentValue) { - return val.comment; - } else if (typeof val === "object" && "$tactStruct" in val) { - const assocList = Object.entries(val).map(([key, value]) => { - return `${key}: ${showValue(value)}`; - }); - return `{${assocList.join(",")}}`; - } else { - throwInternalCompilerError("Invalid value", dummySrcInfo); +export function showValue(val: AstLiteral): string { + switch (val.kind) { + case "number": + return val.value.toString(val.base); + case "simplified_string": + case "comment_value": + return val.value; + case "boolean": + return val.value ? "true" : "false"; + case "address": + return val.value.toRawString(); + case "cell": + case "slice": + return val.value.toString(); + case "null": + return "null"; + case "struct_value": { + const assocList = val.args.map( + (field) => + `${idText(field.field)}: ${showValue(field.initializer)}`, + ); + return `{${assocList.join(",")}}`; + } + default: + throwInternalCompilerError("Invalid value"); } } @@ -112,7 +99,7 @@ export type FieldDescription = { index: number; type: TypeRef; as: string | null; - default: Value | undefined; + default: AstLiteral | undefined; loc: SrcInfo; ast: AstFieldDecl; abi: ABIField; @@ -121,7 +108,7 @@ export type FieldDescription = { export type ConstantDescription = { name: string; type: TypeRef; - value: Value | undefined; + value: AstLiteral | undefined; loc: SrcInfo; ast: AstConstantDef | AstConstantDecl; };