Skip to content

Commit

Permalink
fix: allow constant/trait constants depend on each other (#1622)
Browse files Browse the repository at this point in the history
  • Loading branch information
i582 authored Jan 31, 2025
1 parent dd48c25 commit 3d6616c
Show file tree
Hide file tree
Showing 15 changed files with 222 additions and 9 deletions.
1 change: 1 addition & 0 deletions dev-docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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)
- Incorrect call generation to a mutation function: PR [#1608](https://github.com/tact-lang/tact/pull/1608)
- Allow constant/trait constants depend on each other: PR [#1622](https://github.com/tact-lang/tact/pull/1622)

### Docs

Expand Down
27 changes: 24 additions & 3 deletions src/optimizer/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1163,14 +1163,35 @@ export class Interpreter {
ast.aggregate.loc,
);
}

if (foundContractConst.value !== undefined) {
return foundContractConst.value;
} else {
}

const name = `self.${idText(ast.field)}`;

// see comment in `interpretName`
if (this.visitedConstants.has(name)) {
throwErrorConstEval(
`cannot evaluate declared contract/trait constant ${idTextErr(ast.field)} as it does not have a body`,
ast.field.loc,
`cannot evaluate ${name} as it has circular dependencies: [${this.formatComputationPath(name)}]`,
ast.loc,
);
}
this.visitedConstants.add(name);

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

throwErrorConstEval(
`cannot evaluate declared contract/trait constant ${idTextErr(ast.field)} as it does not have a body`,
ast.field.loc,
);
}
}
const valStruct = this.interpretExpressionInternal(ast.aggregate);
Expand Down
27 changes: 27 additions & 0 deletions src/test/compilation-failed/const-eval-failed.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,4 +230,31 @@ describe("fail-const-eval", () => {
errorMessage:
"Cannot evaluate expression to a constant: cannot evaluate A as it has circular dependencies: [A -> A]",
});

itShouldNotCompile({
testName: "const-eval-self-constant-circular-dependency",
errorMessage:
"Cannot evaluate expression to a constant: cannot evaluate self.C as it has circular dependencies: [self.C -> self.A -> self.C]",
});
itShouldNotCompile({
testName: "const-eval-self-constant-deep-circular-dependency",
errorMessage:
"Cannot evaluate expression to a constant: cannot evaluate self.E as it has circular dependencies: [self.E -> self.D -> self.C -> self.B -> self.A -> self.E]",
});
itShouldNotCompile({
testName:
"const-eval-self-constant-circular-dependency-self-assignment",
errorMessage:
"Cannot evaluate expression to a constant: cannot evaluate self.A as it has circular dependencies: [self.A -> self.A]",
});
itShouldNotCompile({
testName: "const-eval-self-constant-assign-field",
errorMessage:
"Cannot evaluate expression to a constant: cannot evaluate non-constant self field access",
});
itShouldNotCompile({
testName: "const-eval-self-constant-with-method-call-in-value",
errorMessage:
'Cannot evaluate expression to a constant: calls of "test" are not supported at this moment',
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
contract Test {
const A: Int = self.value;

value: Int = 10;

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

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

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

get fun getConstant(): Int {
return self.E;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
contract Test {
a: Int = 1;

const B: Int = 10;
const A: Int = self.B + self.test();

fun test(): Int {
return self.a;
}
}
41 changes: 41 additions & 0 deletions src/test/e2e-emulated/contracts/self-constants.tact
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
trait WithConstant {
const TraitA: Int = 41;
const TraitB: Int = self.TraitA + 1;

const TraitA2: Int = 41;
const TraitC2: Int = self.TraitA2 + 1 + self.TraitB2;
const TraitB2: Int = 9;
}

contract ConstantTester with WithConstant {
const A: Int = 41;
const B: Int = self.A + 1;

const A2: Int = 41;
const C2: Int = self.A2 + 1 + self.B2;
const B2: Int = 9;

value: Int = self.B2 + self.C2 + self.TraitB2;

receive() {}

get fun b(): Int {
return self.B;
}

get fun c2(): Int {
return self.C2;
}

get fun value(): Int {
return self.value;
}

get fun traitB(): Int {
return self.TraitB;
}

get fun traitC2(): Int {
return self.TraitC2;
}
}
38 changes: 38 additions & 0 deletions src/test/e2e-emulated/self-constants.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { toNano } from "@ton/core";
import { Blockchain, SandboxContract, TreasuryContract } from "@ton/sandbox";
import { ConstantTester } from "./contracts/output/self-constants_ConstantTester";
import "@ton/test-utils";

describe("self-constants", () => {
let blockchain: Blockchain;
let treasure: SandboxContract<TreasuryContract>;
let contract: SandboxContract<ConstantTester>;

beforeEach(async () => {
blockchain = await Blockchain.create();
blockchain.verbosity.print = false;
treasure = await blockchain.treasury("treasure");

contract = blockchain.openContract(await ConstantTester.fromInit());

const deployResult = await contract.send(
treasure.getSender(),
{ value: toNano("10") },
null,
);
expect(deployResult.transactions).toHaveTransaction({
from: treasure.address,
to: contract.address,
success: true,
deploy: true,
});
});

it("should implement self constants correctly", async () => {
expect(await contract.getB()).toEqual(42n);
expect(await contract.getC2()).toEqual(51n);
expect(await contract.getValue()).toEqual(69n);
expect(await contract.getTraitB()).toEqual(42n);
expect(await contract.getTraitC2()).toEqual(51n);
});
});
9 changes: 9 additions & 0 deletions src/types/__snapshots__/resolveDescriptors.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@ exports[`resolveDescriptors should fail descriptors for const-eval-overflow 1`]
"
`;

exports[`resolveDescriptors should fail descriptors for constant-does-not-exist 1`] = `
"<unknown>:7:20: Cannot find 'A', did you mean 'self.A'?
6 | const A: Int = 100;
> 7 | const B: Int = A;
^
8 | }
"
`;

exports[`resolveDescriptors should fail descriptors for contract-bounced-receiver-int 1`] = `
"<unknown>:17:3: Bounce receive function can only accept bounced message, message or Slice
16 |
Expand Down
25 changes: 21 additions & 4 deletions src/types/resolveDescriptors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
idText,
isSelfId,
isSlice,
selfId,
} from "../ast/ast-helpers";
import { traverse, traverseAndCheck } from "../ast/iterators";
import {
Expand Down Expand Up @@ -42,7 +43,7 @@ import { isRuntimeType } from "./isRuntimeType";
import { GlobalFunctions } from "../abi/global";
import { ItemOrigin } from "../grammar";
import { getExpType, resolveExpression } from "./resolveExpression";
import { emptyContext } from "./resolveStatements";
import { addVariable, emptyContext } from "./resolveStatements";
import { isAssignable } from "./subtyping";
import { AstUtil, getAstUtil } from "../ast/util";

Expand Down Expand Up @@ -2157,8 +2158,12 @@ function checkInitializerType(
declTy: TypeRef,
initializer: A.AstExpression,
ctx: CompilerContext,
selfTypeRef: TypeRef | undefined,
): CompilerContext {
const stmtCtx = emptyContext(initializer.loc, null, declTy);
let stmtCtx = emptyContext(initializer.loc, null, declTy);
if (selfTypeRef) {
stmtCtx = addVariable(selfId, selfTypeRef, ctx, stmtCtx);
}
ctx = resolveExpression(initializer, stmtCtx, ctx);
const initTy = getExpType(ctx, initializer);
if (!isAssignable(initTy, declTy)) {
Expand Down Expand Up @@ -2190,6 +2195,7 @@ function initializeConstants(
function checkConstants(
constants: ConstantDescription[],
ctx: CompilerContext,
typeRef: TypeRef | undefined,
): CompilerContext {
for (const constant of constants) {
if (constant.ast.kind === "constant_def") {
Expand All @@ -2199,6 +2205,7 @@ function checkConstants(
constant.type,
constant.ast.initializer,
ctx,
typeRef,
);
}
}
Expand All @@ -2213,7 +2220,7 @@ function initializeConstantsAndDefaultContractAndStructFields(

// we split the handling of constants into two steps:
// first we check all constants to make sure the types of initializers are correct
ctx = checkConstants(staticConstants, ctx);
ctx = checkConstants(staticConstants, ctx, undefined);

for (const aggregateTy of getAllTypes(ctx)) {
switch (aggregateTy.kind) {
Expand All @@ -2223,7 +2230,16 @@ function initializeConstantsAndDefaultContractAndStructFields(
case "contract":
case "struct": {
{
ctx = checkConstants(aggregateTy.constants, ctx);
const selfTypeRef: TypeRef = {
kind: "ref",
name: aggregateTy.name,
optional: false,
};
ctx = checkConstants(
aggregateTy.constants,
ctx,
selfTypeRef,
);

for (const field of aggregateTy.fields) {
if (field.ast.initializer !== null) {
Expand All @@ -2233,6 +2249,7 @@ function initializeConstantsAndDefaultContractAndStructFields(
field.type,
field.ast.initializer,
ctx,
selfTypeRef,
);
field.default = evalConstantExpression(
field.ast.initializer,
Expand Down
8 changes: 7 additions & 1 deletion src/types/resolveExpression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -915,7 +915,13 @@ export function resolveExpression(
const field = t.fields.find(
(f) => f.name == exp.text,
);
if (field) {
const constant = t.constants.find(
(c) => c.name == exp.text,
);
if (
typeof field !== "undefined" ||
typeof constant !== "undefined"
) {
throwCompilationError(
`Cannot find '${exp.text}', did you mean 'self.${exp.text}'?`,
exp.loc,
Expand Down
2 changes: 1 addition & 1 deletion src/types/resolveStatements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ function removeRequiredVariable(
};
}

function addVariable(
export function addVariable(
name: A.AstId,
ref: TypeRef,
ctx: CompilerContext,
Expand Down
8 changes: 8 additions & 0 deletions src/types/test-failed/constant-does-not-exist.tact
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
primitive Int;

trait BaseTrait { }

trait TestTrait {
const A: Int = 100;
const B: Int = A;
}

0 comments on commit 3d6616c

Please sign in to comment.