diff --git a/dev-docs/CHANGELOG.md b/dev-docs/CHANGELOG.md index 5375e05ad..a22e5d5b0 100644 --- a/dev-docs/CHANGELOG.md +++ b/dev-docs/CHANGELOG.md @@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Allow serialization specifiers for trait fields PR: [#1303](https://github.com/tact-lang/tact/pull/1303) - Remove unused typechecker wrapper with the file `check.ts` it is contained in: PR [#1313](https://github.com/tact-lang/tact/pull/1313) - Unified `StatementTry` and `StatementTryCatch` AST nodes: PR [#1418](https://github.com/tact-lang/tact/pull/1418) +- Calling methods on `null` when `self` is of an optional type is now allowed: PR [#1567](https://github.com/tact-lang/tact/pull/1567) - Make `msg_bounced` last parameter of `*_contract_router_internal` for better code generation: PR [#1585](https://github.com/tact-lang/tact/pull/1585) - Inline `*_contract_init` function: PR [#1589](https://github.com/tact-lang/tact/pull/1589) - Better error message for `unresolved name` error: PR [#1595](https://github.com/tact-lang/tact/pull/1595) @@ -69,6 +70,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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) +- Incorrect call generation to a mutation function: PR [#1608](https://github.com/tact-lang/tact/pull/1608) ### Docs diff --git a/src/generator/writers/writeFunction.ts b/src/generator/writers/writeFunction.ts index c19379b85..2eb76bc38 100644 --- a/src/generator/writers/writeFunction.ts +++ b/src/generator/writers/writeFunction.ts @@ -702,9 +702,13 @@ function writeNonMutatingFunction( ctx.context("stdlib"); } ctx.body(() => { + const params = f.ast.params; + const withoutSelfParams = + params.length > 0 && params.at(0)?.name.text === "self" + ? params.slice(1) + : params; ctx.append( - `return ${funcIdOf("self")}~${markUsedName ? ctx.used(name) : name}(${f.ast.params - .slice(1) + `return ${funcIdOf("self")}~${markUsedName ? ctx.used(name) : name}(${withoutSelfParams .map((arg) => funcIdOf(arg.name)) .join(", ")});`, ); diff --git a/src/test/e2e-emulated/contracts/mutating-methods.tact b/src/test/e2e-emulated/contracts/mutating-methods.tact index aac3bf439..ba437eefa 100644 --- a/src/test/e2e-emulated/contracts/mutating-methods.tact +++ b/src/test/e2e-emulated/contracts/mutating-methods.tact @@ -16,6 +16,20 @@ struct Foo { s: Slice; } +struct MyStruct { + age: Int; +} + +fun returnMyStruct(): MyStruct { + return MyStruct{ age: 10 }; +} + +extends mutates fun setAge(self: MyStruct, age: Int): Int { + let old = self.age; + self.age = age; + return old + self.age; +} + contract Tester { s: Slice; @@ -84,4 +98,8 @@ contract Tester { x.multiplyExtends(2).multiplyExtends(3); return x.multiplyExtends(2).multiplyExtends(3); } -} \ No newline at end of file + + get fun test12(): Int { + return returnMyStruct().setAge(20); + } +} diff --git a/src/test/e2e-emulated/mutating-methods.spec.ts b/src/test/e2e-emulated/mutating-methods.spec.ts index b1e929510..d9c615671 100644 --- a/src/test/e2e-emulated/mutating-methods.spec.ts +++ b/src/test/e2e-emulated/mutating-methods.spec.ts @@ -88,5 +88,6 @@ describe("bugs", () => { expect(await contract.getTest11(1n)).toBe(6n); expect(await contract.getTest11(2n)).toBe(12n); + expect(await contract.getTest12()).toBe(30n); }); }); diff --git a/src/types/__snapshots__/resolveStatements.spec.ts.snap b/src/types/__snapshots__/resolveStatements.spec.ts.snap index 4eee0c95d..d78dc86d7 100644 --- a/src/types/__snapshots__/resolveStatements.spec.ts.snap +++ b/src/types/__snapshots__/resolveStatements.spec.ts.snap @@ -420,6 +420,24 @@ exports[`resolveStatements should fail statements for expr-map-exists-method-on- " `; +exports[`resolveStatements should fail statements for expr-method-call-null-ambiguous 1`] = ` +":19:16: Ambiguous method call "foo" + 18 | get fun test(): Bool { +> 19 | return null.foo(); + ^~~~~~~~~~ + 20 | } +" +`; + +exports[`resolveStatements should fail statements for expr-method-call-null-not-existing 1`] = ` +":14:16: Invalid type "" for function call + 13 | get fun test(): Bool { +> 14 | return null.bar(); + ^~~~~~~~~~ + 15 | } +" +`; + exports[`resolveStatements should fail statements for expr-method-does-not-exist-but-field-does 1`] = ` ":13:5: Type "S" does not have a function named "x()", did you mean field "x" instead? 12 | let s: S = S{ x: 1 }; @@ -2530,6 +2548,217 @@ exports[`resolveStatements should resolve statements for expr-maps-exists-method ] `; +exports[`resolveStatements should resolve statements for expr-method-call-null 1`] = ` +[ + [ + "self", + "Int?", + ], + [ + "null", + "", + ], + [ + "self == null", + "Bool", + ], + [ + "false", + "Bool", + ], + [ + "self", + "Int?", + ], + [ + "self!!", + "Int", + ], + [ + "42", + "Int", + ], + [ + "self!! == 42", + "Bool", + ], + [ + "null", + "", + ], + [ + "null.foo()", + "Bool", + ], +] +`; + +exports[`resolveStatements should resolve statements for expr-method-call-null2 1`] = ` +[ + [ + "self", + "Int?", + ], + [ + "null", + "", + ], + [ + "self == null", + "Bool", + ], + [ + "false", + "Bool", + ], + [ + "self", + "Int?", + ], + [ + "self!!", + "Int", + ], + [ + "42", + "Int", + ], + [ + "self!! == 42", + "Bool", + ], + [ + "self", + "Int?", + ], + [ + "null", + "", + ], + [ + "self == null", + "Bool", + ], + [ + "false", + "Bool", + ], + [ + "self", + "Int?", + ], + [ + "self!!", + "Int", + ], + [ + "69", + "Int", + ], + [ + "self!! == 69", + "Bool", + ], + [ + "null", + "", + ], + [ + "null.foo()", + "Bool", + ], + [ + "null", + "", + ], + [ + "null.bar()", + "Bool", + ], +] +`; + +exports[`resolveStatements should resolve statements for expr-method-call-null3 1`] = ` +[ + [ + "self", + "Int?", + ], + [ + "null", + "", + ], + [ + "self == null", + "Bool", + ], + [ + "false", + "Bool", + ], + [ + "self", + "Int?", + ], + [ + "self!!", + "Int", + ], + [ + "42", + "Int", + ], + [ + "self!! == 42", + "Bool", + ], + [ + "self", + "String?", + ], + [ + "null", + "", + ], + [ + "self == null", + "Bool", + ], + [ + "false", + "Bool", + ], + [ + "self", + "String?", + ], + [ + "self!!", + "String", + ], + [ + ""hello"", + "String", + ], + [ + "self!! == "hello"", + "Bool", + ], + [ + "null", + "", + ], + [ + "x", + "Int?", + ], + [ + "x.foo()", + "Bool", + ], +] +`; + exports[`resolveStatements should resolve statements for expr-struct-construction 1`] = ` [ [ diff --git a/src/types/resolveExpression.ts b/src/types/resolveExpression.ts index fb9600008..ddf6ada39 100644 --- a/src/types/resolveExpression.ts +++ b/src/types/resolveExpression.ts @@ -703,6 +703,43 @@ function resolveCall( throwCompilationError(`Cannot call function on bounced value`, exp.loc); } + if (src.kind === "null") { + // e.g. null.foo() + // we need to try to find a method foo that accepts nullable type as self + + const types = getAllTypes(ctx); + const candidates = []; + for (const t of types) { + const f = t.functions.get(idText(exp.method)); + if (f) { + if (f.self?.kind === "ref" && f.self.optional) { + candidates.push({ type: t, f }); + } + } + } + + const candidate = candidates[0]; + + // No candidates found + if (typeof candidate === "undefined") { + throwCompilationError( + `Invalid type "${printTypeRef(src)}" for function call`, + exp.loc, + ); + } + + // Too many candidates found + if (candidates.length > 1) { + throwCompilationError( + `Ambiguous method call ${idTextErr(exp.method)}`, + exp.loc, + ); + } + + // Return the only candidate + return registerExpType(ctx, exp, candidate.f.returns); + } + throwCompilationError( `Invalid type "${printTypeRef(src)}" for function call`, exp.loc, diff --git a/src/types/stmts-failed/expr-method-call-null-ambiguous.tact b/src/types/stmts-failed/expr-method-call-null-ambiguous.tact new file mode 100644 index 000000000..25fa60216 --- /dev/null +++ b/src/types/stmts-failed/expr-method-call-null-ambiguous.tact @@ -0,0 +1,21 @@ +primitive Bool; +primitive Int; +primitive String; + +trait BaseTrait { } + +extends fun foo(self: Int?): Bool { + if (self == null) { return false } + else { return self!! == 42 } +} + +extends fun foo(self: String?): Bool { + if (self == null) { return false } + else { return self!! == "hello" } +} + +contract Test { + get fun test(): Bool { + return null.foo(); + } +} \ No newline at end of file diff --git a/src/types/stmts-failed/expr-method-call-null-not-existing.tact b/src/types/stmts-failed/expr-method-call-null-not-existing.tact new file mode 100644 index 000000000..bc39e9c8d --- /dev/null +++ b/src/types/stmts-failed/expr-method-call-null-not-existing.tact @@ -0,0 +1,16 @@ +primitive Bool; +primitive Int; +primitive String; + +trait BaseTrait { } + +extends fun foo(self: Int?): Bool { + if (self == null) { return false } + else { return self!! == 42 } +} + +contract Test { + get fun test(): Bool { + return null.bar(); + } +} \ No newline at end of file diff --git a/src/types/stmts/expr-method-call-null.tact b/src/types/stmts/expr-method-call-null.tact new file mode 100644 index 000000000..29947d00b --- /dev/null +++ b/src/types/stmts/expr-method-call-null.tact @@ -0,0 +1,15 @@ +primitive Bool; +primitive Int; + +trait BaseTrait { } + +extends fun foo(self: Int?): Bool { + if (self == null) { return false } + else { return self!! == 42 } +} + +contract Test { + get fun test(): Bool { + return null.foo(); + } +} \ No newline at end of file diff --git a/src/types/stmts/expr-method-call-null2.tact b/src/types/stmts/expr-method-call-null2.tact new file mode 100644 index 000000000..f8866070f --- /dev/null +++ b/src/types/stmts/expr-method-call-null2.tact @@ -0,0 +1,24 @@ +primitive Bool; +primitive Int; + +trait BaseTrait { } + +extends fun foo(self: Int?): Bool { + if (self == null) { return false } + else { return self!! == 42 } +} + +extends fun bar(self: Int?): Bool { + if (self == null) { return false } + else { return self!! == 69 } +} + +contract Test { + get fun test(): Bool { + return null.foo(); + } + + get fun test2(): Bool { + return null.bar(); + } +} diff --git a/src/types/stmts/expr-method-call-null3.tact b/src/types/stmts/expr-method-call-null3.tact new file mode 100644 index 000000000..4b8490fcd --- /dev/null +++ b/src/types/stmts/expr-method-call-null3.tact @@ -0,0 +1,22 @@ +primitive Bool; +primitive Int; +primitive String; + +trait BaseTrait { } + +extends fun foo(self: Int?): Bool { + if (self == null) { return false } + else { return self!! == 42 } +} + +extends fun foo(self: String?): Bool { + if (self == null) { return false } + else { return self!! == "hello" } +} + +contract Test { + get fun test(): Bool { + let x: Int? = null; + return x.foo(); + } +} \ No newline at end of file