Skip to content

Commit

Permalink
fix: support use of constant in other constant and for struct field d…
Browse files Browse the repository at this point in the history
…efault value before declaration (#1478)

* fix: support use of constant in other constant and for struct field default value before declaration

Fixes #1477
Fixes #879

* added CHANGELOG.md entry

* fixed review issues and added more positive tests

* use `??=`

* fix

* merge

* fixed review issues

* fixed review issue

* fixed review issue

* fixed review issue
  • Loading branch information
i582 authored Jan 27, 2025
1 parent 6f27a9a commit b7add10
Show file tree
Hide file tree
Showing 16 changed files with 305 additions and 26 deletions.
1 change: 1 addition & 0 deletions dev-docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Incorrect arithmetic bit shift operations optimizations: PR [#1501](https://github.com/tact-lang/tact/pull/1501)
- Throwing from functions with non-trivial branching in the `try` statement: PR [#1501](https://github.com/tact-lang/tact/pull/1501)
- Forbid read and write to self in contract init function: PR [#1482](https://github.com/tact-lang/tact/pull/1482)
- Support for using a constant within another constant and for the default value of a struct field before constant declaration: PR [#1478](https://github.com/tact-lang/tact/pull/1478)

### Docs

Expand Down
93 changes: 78 additions & 15 deletions src/optimizer/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import * as A from "../ast/ast";
import { evalConstantExpression } from "./constEval";
import { CompilerContext } from "../context/context";
import {
idTextErr,
TactCompilationError,
TactConstEvalError,
idTextErr,
throwConstEvalError,
throwInternalCompilerError,
} from "../error/errors";
Expand All @@ -20,7 +20,7 @@ import {
hasStaticFunction,
} from "../types/resolveDescriptors";
import { getExpType } from "../types/resolveExpression";
import { TypeRef, showValue } from "../types/types";
import { showValue, TypeRef } from "../types/types";
import { sha256_sync } from "@ton/crypto";
import { defaultParser, getParser, Parser } from "../grammar/grammar";
import { dummySrcInfo, SrcInfo } from "../grammar";
Expand Down Expand Up @@ -69,6 +69,7 @@ function throwErrorConstEval(msg: string, source: SrcInfo): never {
source,
);
}

type EvalResult =
| { kind: "ok"; value: A.AstLiteral }
| { kind: "error"; message: string };
Expand Down Expand Up @@ -719,6 +720,18 @@ that binds a variable name to its corresponding value.
export class Interpreter {
private envStack: EnvironmentStack;
private context: CompilerContext;

/**
* Stores all visited constants during the current computation.
*/
private visitedConstants: Set<string> = new Set();

/**
* Stores all constants that were calculated during the computation of some constant,
* and the functions that were called for this process.
* Used only in case of circular dependencies to return a clear error.
*/
private constantComputationPath: string[] = [];
private config: InterpreterConfig;
private util: AstUtil;

Expand Down Expand Up @@ -870,18 +883,41 @@ export class Interpreter {
}

public interpretName(ast: A.AstId): A.AstLiteral {
if (hasStaticConstant(this.context, idText(ast))) {
const constant = getStaticConstant(this.context, idText(ast));
const name = idText(ast);

if (hasStaticConstant(this.context, name)) {
const constant = getStaticConstant(this.context, name);
if (constant.value !== undefined) {
return constant.value;
} else {
}

// Since we call `interpretExpression` on a constant value below, we don't want
// infinite recursion due to circular dependencies. To prevent this, let's collect
// all the constants we process in this iteration. That way, any circular dependencies
// will result in a second occurrence here and thus an early (before stack overflow)
// exception being thrown here.
if (this.visitedConstants.has(name)) {
throwErrorConstEval(
`cannot evaluate declared constant ${idTextErr(ast)} as it does not have a body`,
`cannot evaluate ${name} as it has circular dependencies: [${this.formatComputationPath(name)}]`,
ast.loc,
);
}
this.visitedConstants.add(name);

const astNode = constant.ast;
if (astNode.kind === "constant_def") {
constant.value = this.inComputationPath(name, () =>
this.interpretExpression(astNode.initializer),
);
return constant.value;
}

throwErrorConstEval(
`cannot evaluate declared constant ${idTextErr(ast)} as it does not have a body`,
ast.loc,
);
}
const variableBinding = this.envStack.getBinding(idText(ast));
const variableBinding = this.envStack.getBinding(name);
if (variableBinding !== undefined) {
return variableBinding;
}
Expand Down Expand Up @@ -1401,21 +1437,26 @@ export class Interpreter {
this.context,
idText(ast.function),
);
switch (functionDescription.ast.kind) {
case "function_def":
const functionNode = functionDescription.ast;
switch (functionNode.kind) {
case "function_def": {
// Currently, no attribute is supported
if (functionDescription.ast.attributes.length > 0) {
if (functionNode.attributes.length > 0) {
throwNonFatalErrorConstEval(
"calls to functions with attributes are currently not supported",
ast.loc,
);
}
return this.evalStaticFunction(
functionDescription.ast,
ast.args,
functionDescription.returns,
return this.inComputationPath(
`${functionDescription.name}()`,
() =>
this.evalStaticFunction(
functionNode,
ast.args,
functionDescription.returns,
),
);

}
case "asm_function_def":
throwNonFatalErrorConstEval(
`${idTextErr(ast.function)} cannot be interpreted because it's an asm-function`,
Expand Down Expand Up @@ -1771,4 +1812,26 @@ export class Interpreter {
ast.statements.forEach(this.interpretStatement, this);
});
}

private inComputationPath<T>(path: string, cb: () => T) {
this.constantComputationPath.push(path);
const res = cb();
this.constantComputationPath.pop();
return res;
}

private formatComputationPath(name: string): string {
const start = this.constantComputationPath.indexOf(name);
const path =
start !== -1
? this.constantComputationPath.slice(start)
: this.constantComputationPath;

const shortPath =
path.length > 10
? [...path.slice(0, 5), "...", ...path.slice(path.length - 4)]
: path;

return `${shortPath.join(" -> ")} -> ${name}`;
}
}
37 changes: 37 additions & 0 deletions src/test/compilation-failed/const-eval-failed.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,41 @@ describe("fail-const-eval", () => {
errorMessage:
"Cannot evaluate expression to a constant: ascii string cannot be empty",
});
itShouldNotCompile({
testName: "const-eval-constant-circular-dependency",
errorMessage:
"Cannot evaluate expression to a constant: cannot evaluate C as it has circular dependencies: [C -> A -> C]",
});
itShouldNotCompile({
testName: "const-eval-constant-deep-circular-dependency",
errorMessage:
"Cannot evaluate expression to a constant: cannot evaluate E as it has circular dependencies: [E -> D -> C -> B -> A -> E]",
});
itShouldNotCompile({
testName: "const-eval-constant-circular-dependency-with-function",
errorMessage:
"Cannot evaluate expression to a constant: cannot evaluate C as it has circular dependencies: [C -> A -> foo() -> C]",
});
itShouldNotCompile({
testName: "const-eval-constant-circular-dependency-with-functions",
errorMessage:
"Cannot evaluate expression to a constant: cannot evaluate C as it has circular dependencies: [C -> A -> foo() -> bar() -> baz() -> C]",
});
itShouldNotCompile({
testName:
"const-eval-constant-circular-dependency-with-recursive-function",
errorMessage:
"Cannot evaluate expression to a constant: cannot evaluate C as it has circular dependencies: [C -> A -> foo() -> foo() -> foo() -> C]",
});
itShouldNotCompile({
testName:
"const-eval-constant-circular-dependency-with-deep-recursive-function",
errorMessage:
"Cannot evaluate expression to a constant: cannot evaluate C as it has circular dependencies: [C -> A -> foo() -> foo() -> foo() -> ... -> foo() -> foo() -> foo() -> foo() -> C]",
});
itShouldNotCompile({
testName: "const-eval-constant-circular-dependency-self-assignment",
errorMessage:
"Cannot evaluate expression to a constant: cannot evaluate A as it has circular dependencies: [A -> A]",
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const A: Int = A;

contract Test {
get fun getConstant(): Int {
return A;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const A: Int = foo(20);
const C: Int = A;

fun foo(value: Int): Int {
if (value > 1) {
return foo(value - 1)
}
return C;
}

contract Test {
get fun getConstant(): Int {
return C;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const A: Int = foo();
const C: Int = A;

fun foo(): Int {
return C;
}

contract Test {
get fun getConstant(): Int {
return C;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const A: Int = foo();
const C: Int = A;

fun foo(): Int {
return bar()
}

fun bar(): Int {
return baz()
}

fun baz(): Int {
return C
}

contract Test {
get fun getConstant(): Int {
return C;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const A: Int = foo(3);
const C: Int = A;

fun foo(value: Int): Int {
if (value > 1) {
return foo(value - 1)
}
return C;
}

contract Test {
get fun getConstant(): Int {
return C;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const A: Int = C;
const C: Int = A;

contract Test {
get fun getConstant(): Int {
return C;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const A: Int = E;
const B: Int = A;
const C: Int = B;
const D: Int = C;
const E: Int = D;

contract Test {
get fun getConstant(): Int {
return E;
}
}
5 changes: 5 additions & 0 deletions src/test/e2e-emulated/constants.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,5 +129,10 @@ describe("constants", () => {
expect(await contract.getGlobalConst11()).toEqual(24n);
expect(await contract.getGlobalConst12()).toEqual(8n);
expect(await contract.getGlobalConst13()).toEqual(8n);

expect(await contract.getBeforeDefinedA()).toEqual(10n);
expect(await contract.getBeforeDefinedC()).toEqual(20n);
expect(await contract.getDefaultFieldB()).toEqual(20n);
expect(await contract.getNoCircularA()).toEqual(200n);
});
});
28 changes: 27 additions & 1 deletion src/test/e2e-emulated/contracts/constants.tact
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ const globalConst11: Int = factorial_iterative(globalConst3); // 4! = 24
const globalConst12: Int = fibonacci_recursive(globalConst3 + 2); // fibonacci(6) = 8
const globalConst13: Int = fibonacci_iterative(globalConst3 + 2); // fibonacci(6) = 8

const beforeDefinedC: Int = beforeDefinedA + beforeDefinedB;
const beforeDefinedA: Int = beforeDefinedB;
const beforeDefinedB: Int = 10;

struct A {
b: Int = beforeDefinedC;
}

struct S {
a: Bool;
b: Int;
Expand All @@ -24,6 +32,17 @@ struct T {
s: S;
}

const NoCircularA: Int = NoCircularB;
const NoCircularB: Int = useAConditionally(1);

fun useAConditionally(v: Int): Int {
if (v == 1) {
return 100;
} else {
return NoCircularA; // The else never executes, so no circular dependence at compile-time
}
}

// Global functions

// Test assignments
Expand Down Expand Up @@ -327,6 +346,13 @@ contract ConstantTester {
get fun globalConst12(): Int { return globalConst12; }
get fun globalConst13(): Int { return globalConst13; }

get fun beforeDefinedA(): Int { return beforeDefinedA; }
get fun beforeDefinedC(): Int { return beforeDefinedC; }

get fun defaultFieldB(): Int { return A {}.b; }

get fun noCircularA(): Int { return NoCircularA + NoCircularB; }

get fun minInt1(): Int {
return -115792089237316195423570985008687907853269984665640564039457584007913129639936;
}
Expand All @@ -342,4 +368,4 @@ contract ConstantTester {
get fun globalConst(): Int {
return someGlobalConst;
}
}
}
26 changes: 26 additions & 0 deletions src/types/__snapshots__/resolveStatements.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1789,6 +1789,32 @@ exports[`resolveStatements should resolve statements for assign-self-mutating-me
]
`;

exports[`resolveStatements should resolve statements for constant-as-default-value-of-struct-field 1`] = `
[
[
"100",
"Int",
],
[
"C",
"Int",
],
]
`;

exports[`resolveStatements should resolve statements for constant-as-default-value-of-struct-field-2 1`] = `
[
[
"100",
"Int",
],
[
"C",
"Int",
],
]
`;

exports[`resolveStatements should resolve statements for contract-getter-with-method-id-1 1`] = `
[
[
Expand Down
Loading

0 comments on commit b7add10

Please sign in to comment.