Skip to content

Commit

Permalink
fix: chaining for mutable extension functions (tact-lang#384)
Browse files Browse the repository at this point in the history
Whenever FunC needs an L-value we provide it by creating a temporary binding
the form of a function parameter
  • Loading branch information
Gusarich authored Jun 8, 2024
1 parent ef90392 commit 19aa043
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 9 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions src/generator/writers/ops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
6 changes: 5 additions & 1 deletion src/generator/writers/writeExpression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(", ")})`;
}
Expand Down
74 changes: 68 additions & 6 deletions src/generator/writers/writeFunction.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -422,17 +427,14 @@ 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
const self = f.self ? getType(ctx.ctx, f.self) : null;

// 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") {
Expand All @@ -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"));
Expand All @@ -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(", ")})`);
Expand Down Expand Up @@ -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) {
Expand Down
32 changes: 31 additions & 1 deletion src/test/__snapshots__/bugs.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
21 changes: 20 additions & 1 deletion src/test/bugs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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);
});
});
1 change: 1 addition & 0 deletions src/test/bugs/bugs.tact
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import "./issue43.tact";
import "./issue53.tact";
import "./issue74.tact";
import "./issue117.tact";
import "./issue211.tact";
import "./large-contract.tact";
43 changes: 43 additions & 0 deletions src/test/bugs/issue211.tact
Original file line number Diff line number Diff line change
@@ -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);
}
}

0 comments on commit 19aa043

Please sign in to comment.