diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a8b0955b..6d2bb2581 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `__tact_load_address_opt` code generation: PR [#373](https://github.com/tact-lang/tact/pull/373) - Empty messages are now correctly converted into cells: PR [#380](https://github.com/tact-lang/tact/pull/380) - All integer and boolean expressions are now being attempted to be evaluated as constants. Additionally, compile-time errors are thrown for errors encountered during the evaluation of actual constants: PR [#352](https://github.com/tact-lang/tact/pull/352) +- Chaining mutable functions now does not throw compilation errors: PR [#384](https://github.com/tact-lang/tact/pull/384) ## [1.3.0] - 2024-05-03 diff --git a/src/generator/writers/ops.ts b/src/generator/writers/ops.ts index 09ef56873..f89b15bcd 100644 --- a/src/generator/writers/ops.ts +++ b/src/generator/writers/ops.ts @@ -73,6 +73,7 @@ export const ops = { // Functions extension: (type: string, name: string) => `$${type}$_fun_${name}`, global: (name: string) => `$global_${name}`, + nonModifying: (name: string) => `${name}$not_mut`, // Constants str: (id: string, ctx: WriterContext) => used(`__gen_str_${id}`, ctx), diff --git a/src/generator/writers/writeExpression.ts b/src/generator/writers/writeExpression.ts index 2b302a1c7..77c35a0d3 100644 --- a/src/generator/writers/writeExpression.ts +++ b/src/generator/writers/writeExpression.ts @@ -684,7 +684,11 @@ export function writeExpression(f: ASTExpression, ctx: WriterContext): string { // Render const s = writeExpression(f.src, ctx); if (ff.isMutating) { - return `${s}~${name}(${renderedArguments.join(", ")})`; + if (f.src.kind === "id") { + return `${s}~${name}(${renderedArguments.join(", ")})`; + } else { + return `${ctx.used(ops.nonModifying(name))}(${[s, ...renderedArguments].join(", ")})`; + } } else { return `${name}(${[s, ...renderedArguments].join(", ")})`; } diff --git a/src/generator/writers/writeFunction.ts b/src/generator/writers/writeFunction.ts index b68147c6a..6280db902 100644 --- a/src/generator/writers/writeFunction.ts +++ b/src/generator/writers/writeFunction.ts @@ -1,5 +1,10 @@ import { enabledInline } from "../../config/features"; -import { ASTCondition, ASTExpression, ASTStatement } from "../../grammar/ast"; +import { + ASTCondition, + ASTExpression, + ASTNativeFunction, + ASTStatement, +} from "../../grammar/ast"; import { getType, resolveTypeRef } from "../../types/resolveDescriptors"; import { getExpType } from "../../types/resolveExpression"; import { FunctionDescription, TypeRef } from "../../types/types"; @@ -422,10 +427,6 @@ function writeCondition( } export function writeFunction(f: FunctionDescription, ctx: WriterContext) { - // Do not write native functions - if (f.ast.kind === "def_native_function") { - return; - } const fd = f.ast; // Resolve self @@ -433,6 +434,7 @@ export function writeFunction(f: FunctionDescription, ctx: WriterContext) { // Write function header let returns: string = resolveFuncType(f.returns, ctx); + const returnsOriginal = returns; let returnsStr: string | null; if (self && f.isMutating) { if (f.returns.kind !== "void") { @@ -444,7 +446,6 @@ export function writeFunction(f: FunctionDescription, ctx: WriterContext) { } // Resolve function descriptor - const name = self ? ops.extension(self.name, f.name) : ops.global(f.name); const args: string[] = []; if (self) { args.push(resolveFuncType(self, ctx) + " " + id("self")); @@ -453,6 +454,42 @@ export function writeFunction(f: FunctionDescription, ctx: WriterContext) { args.push(resolveFuncType(a.type, ctx) + " " + id(a.name)); } + // Do not write native functions + if (f.ast.kind === "def_native_function") { + if (f.isMutating) { + // Write same function in non-mutating form + const nonMutName = ops.nonModifying(f.ast.nativeName); + ctx.fun(nonMutName, () => { + ctx.signature( + `${returnsOriginal} ${nonMutName}(${args.join(", ")})`, + ); + ctx.flag("impure"); + if (enabledInline(ctx.ctx) || f.isInline) { + ctx.flag("inline"); + } + if (f.origin === "stdlib") { + ctx.context("stdlib"); + } + ctx.body(() => { + ctx.append( + `return ${id("self")}~${(f.ast as ASTNativeFunction).nativeName}(${fd.args + .slice(1) + .map((arg) => id(arg.name)) + .join(", ")});`, + ); + }); + }); + } + return; + } + + if (fd.kind !== "def_function") { + // should never happen, just to satisfy typescript + throw new Error("Unknown function kind"); + } + + const name = self ? ops.extension(self.name, f.name) : ops.global(f.name); + // Write function body ctx.fun(name, () => { ctx.signature(`${returns} ${name}(${args.join(", ")})`); @@ -497,6 +534,31 @@ export function writeFunction(f: FunctionDescription, ctx: WriterContext) { } }); }); + + if (f.isMutating) { + // Write same function in non-mutating form + const nonMutName = ops.nonModifying(name); + ctx.fun(nonMutName, () => { + ctx.signature( + `${returnsOriginal} ${nonMutName}(${args.join(", ")})`, + ); + ctx.flag("impure"); + if (enabledInline(ctx.ctx) || f.isInline) { + ctx.flag("inline"); + } + if (f.origin === "stdlib") { + ctx.context("stdlib"); + } + ctx.body(() => { + ctx.append( + `return ${id("self")}~${ctx.used(name)}(${fd.args + .slice(1) + .map((arg) => id(arg.name)) + .join(", ")});`, + ); + }); + }); + } } export function writeGetter(f: FunctionDescription, ctx: WriterContext) { diff --git a/src/test/__snapshots__/bugs.spec.ts.snap b/src/test/__snapshots__/bugs.spec.ts.snap index 8f04b35f0..827792e60 100644 --- a/src/test/__snapshots__/bugs.spec.ts.snap +++ b/src/test/__snapshots__/bugs.spec.ts.snap @@ -1,6 +1,36 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`bugs should deploy contract correctly 1`] = ` +exports[`bugs should deploy issue211 correctly 1`] = ` +[ + { + "$seq": 0, + "events": [ + { + "$type": "deploy", + }, + { + "$type": "received", + "message": { + "body": { + "type": "empty", + }, + "bounce": true, + "from": "@treasure(treasure)", + "to": "kQAOtHcanapEEsV6te9yBrhA-zKSOvCV0nAyQv6uSTwnekxO", + "type": "internal", + "value": "10", + }, + }, + { + "$type": "processed", + "gasUsed": 3049n, + }, + ], + }, +] +`; + +exports[`bugs should deploy sample jetton correctly 1`] = ` [ { "$seq": 0, diff --git a/src/test/bugs.spec.ts b/src/test/bugs.spec.ts index 685ec3159..779f2abe7 100644 --- a/src/test/bugs.spec.ts +++ b/src/test/bugs.spec.ts @@ -3,12 +3,13 @@ import { ContractSystem } from "@tact-lang/emulator"; import { __DANGER_resetNodeId } from "../grammar/ast"; import { SampleJetton } from "./bugs/output/bugs_SampleJetton"; import { JettonDefaultWallet } from "./bugs/output/bugs_JettonDefaultWallet"; +import { Issue211 } from "./bugs/output/bugs_Issue211"; describe("bugs", () => { beforeEach(() => { __DANGER_resetNodeId(); }); - it("should deploy contract correctly", async () => { + it("should deploy sample jetton correctly", async () => { // Init const system = await ContractSystem.create(); const treasure = system.treasure("treasure"); @@ -43,4 +44,22 @@ describe("bugs", () => { expect(tracker.collect()).toMatchSnapshot(); }); + it("should deploy issue211 correctly", async () => { + // Init + const system = await ContractSystem.create(); + const treasure = system.treasure("treasure"); + const contract = system.open(await Issue211.fromInit()); + const tracker = system.track(contract.address); + await contract.send(treasure, { value: toNano("10") }, null); + await system.run(); + + expect(tracker.collect()).toMatchSnapshot(); + + expect(await contract.getTest1()).toBe(0n); + expect(await contract.getTest2()).toBe(0n); + expect(await contract.getTest3()).toBe(6n); + expect(await contract.getTest4()).toBe(24n); + expect(await contract.getTest5()).toBe(97n); + expect(await contract.getTest7()).toBe(42n); + }); }); diff --git a/src/test/bugs/bugs.tact b/src/test/bugs/bugs.tact index 49cd9c007..51a0110e2 100644 --- a/src/test/bugs/bugs.tact +++ b/src/test/bugs/bugs.tact @@ -3,4 +3,5 @@ import "./issue43.tact"; import "./issue53.tact"; import "./issue74.tact"; import "./issue117.tact"; +import "./issue211.tact"; import "./large-contract.tact"; \ No newline at end of file diff --git a/src/test/bugs/issue211.tact b/src/test/bugs/issue211.tact new file mode 100644 index 000000000..3846e1be1 --- /dev/null +++ b/src/test/bugs/issue211.tact @@ -0,0 +1,43 @@ +extends mutates fun multiply(self: Int, x: Int): Int { + self *= x; + return self; +} + +contract Issue211 { + init() {} + receive() {} + + get fun test1(): Int { + let x: Int = beginCell().storeUint(0, 1).endCell().beginParse().loadUint(1); + return x; + } + + get fun test2(): Int { + let y: Cell = beginCell().storeUint(0, 1).endCell(); + let x: Slice = beginCell().storeUint(y.beginParse().loadUint(1), 1).endCell().beginParse(); + return x.loadUint(1); + } + + get fun test3(): Int { + let x: Int = 3; + x.multiply(2); + return x; + } + + get fun test4(): Int { + let x: Int = 3; + return x.multiply(2).multiply(4); + } + + get fun test5(): Int { + return "abc".asSlice().loadUint(8); + } + + get fun test6() { + emptySlice().loadRef(); + } + + get fun test7(): Int { + return beginCell().storeInt(42, 7).asSlice().loadInt(7); + } +} \ No newline at end of file