From da9e50888a3f1a247354a47cd881914a50efbb09 Mon Sep 17 00:00:00 2001 From: Anton Trunov Date: Sat, 4 Jan 2025 22:19:05 +0500 Subject: [PATCH] fix(codegen): `self` argument in optional struct methods (#1284) --- CHANGELOG.md | 1 + src/generator/writers/writeExpression.ts | 3 +- src/generator/writers/writeFunction.ts | 12 ++++--- .../contracts/non-mutating-methods.tact | 18 ++++++++++ .../e2e-emulated/non-mutating-methods.spec.ts | 35 +++++++++++++++++++ tact.config.json | 5 +++ 6 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 src/test/e2e-emulated/contracts/non-mutating-methods.tact create mode 100644 src/test/e2e-emulated/non-mutating-methods.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 53788537f..5c8708e0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - TypeScript wrappers generation for messages with single quote: PR [#1106](https://github.com/tact-lang/tact/pull/1106) - `foreach` loops now properly handle `as coins` map value serialization type: PR [#1186](https://github.com/tact-lang/tact/pull/1186) - The typechecker now rejects integer map key types with variable width (`coins`, `varint16`, `varint32`, `varuint16`, `varuint32`): PR [#1276](https://github.com/tact-lang/tact/pull/1276) +- Code generation for `self` argument in optional struct methods: PR [#1284](https://github.com/tact-lang/tact/pull/1284) ### Docs diff --git a/src/generator/writers/writeExpression.ts b/src/generator/writers/writeExpression.ts index cc5340e3d..398eba4c4 100644 --- a/src/generator/writers/writeExpression.ts +++ b/src/generator/writers/writeExpression.ts @@ -624,8 +624,7 @@ export function writeExpression(f: AstExpression, wCtx: WriterContext): string { } } - // Render - const s = writeExpression(f.self, wCtx); + const s = writeCastedExpression(f.self, methodDescr.self!, wCtx); if (methodDescr.isMutating) { // check if it's an l-value const path = tryExtractPath(f.self); diff --git a/src/generator/writers/writeFunction.ts b/src/generator/writers/writeFunction.ts index 5c24f63ed..f897bd258 100644 --- a/src/generator/writers/writeFunction.ts +++ b/src/generator/writers/writeFunction.ts @@ -527,8 +527,10 @@ function writeCondition( } export function writeFunction(f: FunctionDescription, ctx: WriterContext) { - // Resolve self - const self = f.self?.kind === "ref" ? getType(ctx.ctx, f.self.name) : null; + const [self, isSelfOpt] = + f.self?.kind === "ref" + ? [getType(ctx.ctx, f.self.name), f.self.optional] + : [null, false]; // Write function header let returns: string = resolveFuncType(f.returns, ctx); @@ -546,7 +548,9 @@ export function writeFunction(f: FunctionDescription, ctx: WriterContext) { // Resolve function descriptor const params: string[] = []; if (self) { - params.push(resolveFuncType(self, ctx) + " " + funcIdOf("self")); + params.push( + resolveFuncType(self, ctx, isSelfOpt) + " " + funcIdOf("self"), + ); } for (const a of f.params) { params.push(resolveFuncType(a.type, ctx) + " " + funcIdOf(a.name)); @@ -619,7 +623,7 @@ export function writeFunction(f: FunctionDescription, ctx: WriterContext) { } ctx.body(() => { // Unpack self - if (self) { + if (self && !isSelfOpt) { ctx.append( `var (${resolveFuncTypeUnpack(self, funcIdOf("self"), ctx)}) = ${funcIdOf("self")};`, ); diff --git a/src/test/e2e-emulated/contracts/non-mutating-methods.tact b/src/test/e2e-emulated/contracts/non-mutating-methods.tact new file mode 100644 index 000000000..ae0dcb789 --- /dev/null +++ b/src/test/e2e-emulated/contracts/non-mutating-methods.tact @@ -0,0 +1,18 @@ +struct SomeStruct { i: Int; b: Bool } + +extends fun equal(self: SomeStruct?, other: SomeStruct?): Bool { + if (self == null && other == null) { return true } + if (self == null || other == null) { return false } + return self!!.i == other!!.i && self!!.b == other!!.b; +} + +contract Tester { + receive() { } + + get fun test1(): Bool { + let s1 = SomeStruct {i: 42, b: true}; + let s2 = SomeStruct {i: 42, b: false}; + let s3: SomeStruct? = null; + return s1.equal(s1) && !s1.equal(s2) && !s3.equal(s2); + } +} diff --git a/src/test/e2e-emulated/non-mutating-methods.spec.ts b/src/test/e2e-emulated/non-mutating-methods.spec.ts new file mode 100644 index 000000000..63de40567 --- /dev/null +++ b/src/test/e2e-emulated/non-mutating-methods.spec.ts @@ -0,0 +1,35 @@ +import { toNano } from "@ton/core"; +import { Blockchain, SandboxContract, TreasuryContract } from "@ton/sandbox"; +import { Tester } from "./contracts/output/non-mutating-methods_Tester"; +import "@ton/test-utils"; + +describe("bugs", () => { + let blockchain: Blockchain; + let treasure: SandboxContract; + let contract: SandboxContract; + + beforeEach(async () => { + blockchain = await Blockchain.create(); + blockchain.verbosity.print = false; + treasure = await blockchain.treasury("treasure"); + + contract = blockchain.openContract(await Tester.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 non-mutating method chaining correctly", async () => { + expect(await contract.getTest1()).toStrictEqual(true); + }); +}); diff --git a/tact.config.json b/tact.config.json index 07bd8e4e2..3453dd029 100644 --- a/tact.config.json +++ b/tact.config.json @@ -104,6 +104,11 @@ "path": "./src/test/e2e-emulated/contracts/mutating-methods.tact", "output": "./src/test/e2e-emulated/contracts/output" }, + { + "name": "non-mutating-methods", + "path": "./src/test/e2e-emulated/contracts/non-mutating-methods.tact", + "output": "./src/test/e2e-emulated/contracts/output" + }, { "name": "underscore-variable", "path": "./src/test/e2e-emulated/contracts/underscore-variable.tact",