Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 19 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
anton-trunov marked this conversation as resolved.
Show resolved Hide resolved
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]",
anton-trunov marked this conversation as resolved.
Show resolved Hide resolved
});
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
Loading