diff --git a/CHANGELOG.md b/CHANGELOG.md index 77ca2704f..ad9b46b55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - CLI flag `--with-decompilation` to turn on decompilation of BoC files at the end of the compilation pipeline: PR [#417](https://github.com/tact-lang/tact/pull/417) - Support more Tact expressions in the constant evaluator: condition expressions, struct instances, struct field access, `emptyMap()`: PR [#432](https://github.com/tact-lang/tact/pull/432) - The `fromCell` and `fromSlice` methods for struct parsing: PR [#418](https://github.com/tact-lang/tact/pull/418) +- Constants can now be used in receivers: PR [#401](https://github.com/tact-lang/tact/pull/401) ### Changed diff --git a/cspell.json b/cspell.json index 700e7c0a1..cef42341c 100644 --- a/cspell.json +++ b/cspell.json @@ -88,6 +88,7 @@ "src/test/e2e-emulated/__snapshots__/masterchain.spec.ts.snap", "src/test/e2e-emulated/__snapshots__/sample-jetton.spec.ts.snap", "src/test/e2e-emulated/__snapshots__/try-catch.spec.ts.snap", + "src/test/e2e-emulated/__snapshots__/const-string-receiver.spec.ts.snap", "src/test/e2e-emulated/address.spec.ts", "src/test/e2e-emulated/intrinsics.spec.ts", "src/test/e2e-emulated/optionals.spec.ts", diff --git a/src/grammar/ast.ts b/src/grammar/ast.ts index 68561a76c..8669edeaa 100644 --- a/src/grammar/ast.ts +++ b/src/grammar/ast.ts @@ -365,6 +365,10 @@ export type ASTReceiveType = kind: "internal-comment"; comment: ASTString; } + | { + kind: "internal-const-comment"; + comment: ASTLvalueRef[]; + } | { kind: "bounce"; arg: ASTArgument; @@ -379,6 +383,10 @@ export type ASTReceiveType = | { kind: "external-comment"; comment: ASTString; + } + | { + kind: "external-const-comment"; + comment: ASTLvalueRef[]; }; export type ASTReceive = { diff --git a/src/grammar/grammar.ohm b/src/grammar/grammar.ohm index ba54c33b7..282ba5234 100644 --- a/src/grammar/grammar.ohm +++ b/src/grammar/grammar.ohm @@ -85,9 +85,11 @@ Tact { Receiver = receive "(" Parameter? ")" "{" Statement* "}" --regular | receive "(" stringLiteral ")" "{" Statement* "}" --comment + | receive "(" LValue ")" "{" Statement* "}" --constComment | "bounced" "(" Parameter ")" "{" Statement* "}" --bounced // cannot be a reserved word because there a 'bounced' field in stdlib's 'Context' structure | external "(" Parameter? ")" "{" Statement* "}" --externalRegular | external "(" stringLiteral ")" "{" Statement* "}" --externalComment + | external "(" LValue ")" "{" Statement* "}" --constExternalComment Statement = StatementLet | StatementBlock diff --git a/src/grammar/grammar.ts b/src/grammar/grammar.ts index f5173f619..68dfa8a47 100644 --- a/src/grammar/grammar.ts +++ b/src/grammar/grammar.ts @@ -340,6 +340,25 @@ semantics.addOperation("astOfItem", { ref: createRef(this), }); }, + Receiver_constComment( + _receiveKwd, + _lparen, + comment, + _rparen, + _lbrace, + receiverBody, + _rbrace, + ) { + return createNode({ + kind: "def_receive", + selector: { + kind: "internal-const-comment", + comment: comment.astOfLValue(), + }, + statements: receiverBody.children.map((s) => s.astOfStatement()), + ref: createRef(this), + }); + }, Receiver_bounced( _bouncedKwd, _lparen, @@ -398,6 +417,25 @@ semantics.addOperation("astOfItem", { ref: createRef(this), }); }, + Receiver_constExternalComment( + _externalKwd, + _lparen, + comment, + _rparen, + _lbrace, + receiverBody, + _rbrace, + ) { + return createNode({ + kind: "def_receive", + selector: { + kind: "external-const-comment", + comment: comment.astOfLValue(), + }, + statements: receiverBody.children.map((s) => s.astOfStatement()), + ref: createRef(this), + }); + }, }); semantics.addOperation("astOfFunctionAttributes", { diff --git a/src/test/e2e-emulated/__snapshots__/const-string-receiver.spec.ts.snap b/src/test/e2e-emulated/__snapshots__/const-string-receiver.spec.ts.snap new file mode 100644 index 000000000..ae2fb0596 --- /dev/null +++ b/src/test/e2e-emulated/__snapshots__/const-string-receiver.spec.ts.snap @@ -0,0 +1,525 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`constant string receiver should implement const string receiver correctly 1`] = ` +{ + "errors": { + "10": { + "message": "Dictionary error", + }, + "128": { + "message": "Null reference exception", + }, + "129": { + "message": "Invalid serialization prefix", + }, + "13": { + "message": "Out of gas error", + }, + "130": { + "message": "Invalid incoming message", + }, + "131": { + "message": "Constraints error", + }, + "132": { + "message": "Access denied", + }, + "133": { + "message": "Contract stopped", + }, + "134": { + "message": "Invalid argument", + }, + "135": { + "message": "Code of a contract was not found", + }, + "136": { + "message": "Invalid address", + }, + "137": { + "message": "Masterchain support is not enabled for this contract", + }, + "2": { + "message": "Stack underflow", + }, + "3": { + "message": "Stack overflow", + }, + "32": { + "message": "Method ID not found", + }, + "34": { + "message": "Action is invalid or not supported", + }, + "37": { + "message": "Not enough TON", + }, + "38": { + "message": "Not enough extra-currencies", + }, + "4": { + "message": "Integer overflow", + }, + "5": { + "message": "Integer out of expected range", + }, + "6": { + "message": "Invalid opcode", + }, + "7": { + "message": "Type check error", + }, + "8": { + "message": "Cell overflow", + }, + "9": { + "message": "Cell underflow", + }, + }, + "getters": [], + "receivers": [ + { + "message": { + "kind": "empty", + }, + "receiver": "internal", + }, + { + "message": { + "kind": "text", + "text": "string 1", + }, + "receiver": "internal", + }, + { + "message": { + "kind": "text", + "text": "string 2", + }, + "receiver": "internal", + }, + { + "message": { + "kind": "text", + "text": "string 3", + }, + "receiver": "internal", + }, + { + "message": { + "kind": "text", + "text": "string 4", + }, + "receiver": "internal", + }, + { + "message": { + "kind": "text", + "text": "string 5", + }, + "receiver": "internal", + }, + { + "message": { + "kind": "text", + "text": "string 6", + }, + "receiver": "internal", + }, + ], + "types": [ + { + "fields": [ + { + "name": "code", + "type": { + "kind": "simple", + "optional": false, + "type": "cell", + }, + }, + { + "name": "data", + "type": { + "kind": "simple", + "optional": false, + "type": "cell", + }, + }, + ], + "header": null, + "name": "StateInit", + }, + { + "fields": [ + { + "name": "bounced", + "type": { + "kind": "simple", + "optional": false, + "type": "bool", + }, + }, + { + "name": "sender", + "type": { + "kind": "simple", + "optional": false, + "type": "address", + }, + }, + { + "name": "value", + "type": { + "format": 257, + "kind": "simple", + "optional": false, + "type": "int", + }, + }, + { + "name": "raw", + "type": { + "kind": "simple", + "optional": false, + "type": "slice", + }, + }, + ], + "header": null, + "name": "Context", + }, + { + "fields": [ + { + "name": "bounce", + "type": { + "kind": "simple", + "optional": false, + "type": "bool", + }, + }, + { + "name": "to", + "type": { + "kind": "simple", + "optional": false, + "type": "address", + }, + }, + { + "name": "value", + "type": { + "format": 257, + "kind": "simple", + "optional": false, + "type": "int", + }, + }, + { + "name": "mode", + "type": { + "format": 257, + "kind": "simple", + "optional": false, + "type": "int", + }, + }, + { + "name": "body", + "type": { + "kind": "simple", + "optional": true, + "type": "cell", + }, + }, + { + "name": "code", + "type": { + "kind": "simple", + "optional": true, + "type": "cell", + }, + }, + { + "name": "data", + "type": { + "kind": "simple", + "optional": true, + "type": "cell", + }, + }, + ], + "header": null, + "name": "SendParameters", + }, + { + "fields": [ + { + "name": "s", + "type": { + "kind": "simple", + "optional": false, + "type": "string", + }, + }, + ], + "header": null, + "name": "MyStruct", + }, + { + "fields": [ + { + "name": "struct", + "type": { + "kind": "simple", + "optional": false, + "type": "MyStruct", + }, + }, + ], + "header": null, + "name": "MyNestedStruct", + }, + ], +} +`; + +exports[`constant string receiver should implement const string receiver correctly 2`] = ` +[ + { + "$seq": 1, + "events": [ + { + "$type": "received", + "message": { + "body": { + "text": "string 1", + "type": "text", + }, + "bounce": true, + "from": "@treasure(treasure)", + "to": "kQBO2__AL4wOwtXplvVV7psQ7PAJKpbbqzS36LyZWcW01Pl7", + "type": "internal", + "value": "10", + }, + }, + { + "$type": "processed", + "gasUsed": 6857n, + }, + { + "$type": "sent", + "messages": [ + { + "body": { + "text": "string 1", + "type": "text", + }, + "bounce": true, + "from": "kQBO2__AL4wOwtXplvVV7psQ7PAJKpbbqzS36LyZWcW01Pl7", + "to": "@treasure(treasure)", + "type": "internal", + "value": "9.991947", + }, + ], + }, + ], + }, + { + "$seq": 2, + "events": [ + { + "$type": "received", + "message": { + "body": { + "text": "string 2", + "type": "text", + }, + "bounce": true, + "from": "@treasure(treasure)", + "to": "kQBO2__AL4wOwtXplvVV7psQ7PAJKpbbqzS36LyZWcW01Pl7", + "type": "internal", + "value": "10", + }, + }, + { + "$type": "processed", + "gasUsed": 9468n, + }, + { + "$type": "sent", + "messages": [ + { + "body": { + "text": "string 2", + "type": "text", + }, + "bounce": true, + "from": "kQBO2__AL4wOwtXplvVV7psQ7PAJKpbbqzS36LyZWcW01Pl7", + "to": "@treasure(treasure)", + "type": "internal", + "value": "9.989336", + }, + ], + }, + ], + }, + { + "$seq": 3, + "events": [ + { + "$type": "received", + "message": { + "body": { + "text": "string 3", + "type": "text", + }, + "bounce": true, + "from": "@treasure(treasure)", + "to": "kQBO2__AL4wOwtXplvVV7psQ7PAJKpbbqzS36LyZWcW01Pl7", + "type": "internal", + "value": "10", + }, + }, + { + "$type": "processed", + "gasUsed": 9581n, + }, + { + "$type": "sent", + "messages": [ + { + "body": { + "text": "string 3", + "type": "text", + }, + "bounce": true, + "from": "kQBO2__AL4wOwtXplvVV7psQ7PAJKpbbqzS36LyZWcW01Pl7", + "to": "@treasure(treasure)", + "type": "internal", + "value": "9.989223", + }, + ], + }, + ], + }, + { + "$seq": 4, + "events": [ + { + "$type": "received", + "message": { + "body": { + "text": "string 4", + "type": "text", + }, + "bounce": true, + "from": "@treasure(treasure)", + "to": "kQBO2__AL4wOwtXplvVV7psQ7PAJKpbbqzS36LyZWcW01Pl7", + "type": "internal", + "value": "10", + }, + }, + { + "$type": "processed", + "gasUsed": 9766n, + }, + { + "$type": "sent", + "messages": [ + { + "body": { + "text": "string 4", + "type": "text", + }, + "bounce": true, + "from": "kQBO2__AL4wOwtXplvVV7psQ7PAJKpbbqzS36LyZWcW01Pl7", + "to": "@treasure(treasure)", + "type": "internal", + "value": "9.989038", + }, + ], + }, + ], + }, + { + "$seq": 5, + "events": [ + { + "$type": "received", + "message": { + "body": { + "text": "string 5", + "type": "text", + }, + "bounce": true, + "from": "@treasure(treasure)", + "to": "kQBO2__AL4wOwtXplvVV7psQ7PAJKpbbqzS36LyZWcW01Pl7", + "type": "internal", + "value": "10", + }, + }, + { + "$type": "processed", + "gasUsed": 7453n, + }, + { + "$type": "sent", + "messages": [ + { + "body": { + "text": "string 5", + "type": "text", + }, + "bounce": true, + "from": "kQBO2__AL4wOwtXplvVV7psQ7PAJKpbbqzS36LyZWcW01Pl7", + "to": "@treasure(treasure)", + "type": "internal", + "value": "9.991351", + }, + ], + }, + ], + }, + { + "$seq": 6, + "events": [ + { + "$type": "received", + "message": { + "body": { + "text": "string 6", + "type": "text", + }, + "bounce": true, + "from": "@treasure(treasure)", + "to": "kQBO2__AL4wOwtXplvVV7psQ7PAJKpbbqzS36LyZWcW01Pl7", + "type": "internal", + "value": "10", + }, + }, + { + "$type": "processed", + "gasUsed": 7602n, + }, + { + "$type": "sent", + "messages": [ + { + "body": { + "text": "string 6", + "type": "text", + }, + "bounce": true, + "from": "kQBO2__AL4wOwtXplvVV7psQ7PAJKpbbqzS36LyZWcW01Pl7", + "to": "@treasure(treasure)", + "type": "internal", + "value": "9.991202", + }, + ], + }, + ], + }, +] +`; diff --git a/src/test/e2e-emulated/const-string-receiver.spec.ts b/src/test/e2e-emulated/const-string-receiver.spec.ts new file mode 100644 index 000000000..f119bb0a3 --- /dev/null +++ b/src/test/e2e-emulated/const-string-receiver.spec.ts @@ -0,0 +1,36 @@ +import { toNano } from "@ton/core"; +import { ContractSystem } from "@tact-lang/emulator"; +import { __DANGER_resetNodeId } from "../../grammar/ast"; +import { ConstStringReceiverTester } from "../e2e-emulated/contracts/output/const-string-receiver_ConstStringReceiverTester"; + +describe("constant string receiver", () => { + beforeEach(() => { + __DANGER_resetNodeId(); + }); + it("should implement const string receiver correctly", async () => { + // Init + const system = await ContractSystem.create(); + const treasure = system.treasure("treasure"); + const contract = system.open( + await ConstStringReceiverTester.fromInit(), + ); + + expect(contract.abi).toMatchSnapshot(); + + // Deploy + await contract.send(treasure, { value: toNano("10") }, "string 1"); + await system.run(); + + const tracker = system.track(contract); + + await contract.send(treasure, { value: toNano("10") }, "string 1"); + await contract.send(treasure, { value: toNano("10") }, "string 2"); + await contract.send(treasure, { value: toNano("10") }, "string 3"); + await contract.send(treasure, { value: toNano("10") }, "string 4"); + await contract.send(treasure, { value: toNano("10") }, "string 5"); + await contract.send(treasure, { value: toNano("10") }, "string 6"); + await system.run(); + + expect(tracker.collect()).toMatchSnapshot(); + }); +}); diff --git a/src/test/e2e-emulated/contracts/const-string-receiver.tact b/src/test/e2e-emulated/contracts/const-string-receiver.tact new file mode 100644 index 000000000..b514a5c27 --- /dev/null +++ b/src/test/e2e-emulated/contracts/const-string-receiver.tact @@ -0,0 +1,49 @@ +const string1: String = "string 1"; +const string2: String = "string 2"; +const string3: String = "string 3"; + +struct MyStruct { + s: String; +} + +struct MyNestedStruct { + struct: MyStruct; +} + +contract ConstStringReceiverTester { + const string4: String = "string 4"; + const struct: MyStruct = MyStruct { + s: "string 5" + }; + const nestedStruct: MyNestedStruct = MyNestedStruct { + struct: MyStruct { + s: "string 6" + } + }; + + receive() { } + + receive("string 1") { + self.reply("string 1".asComment()); + } + + receive(string2) { + self.reply(string2.asComment()); + } + + receive(string3) { + self.reply(string3.asComment()); + } + + receive(self.string4) { + self.reply(self.string4.asComment()); + } + + receive(self.struct.s) { + self.reply("string 5".asComment()); // until const struct fields are fixed + } + + receive(self.nestedStruct.struct.s) { + self.reply("string 6".asComment()); // until const struct fields are fixed + } +} \ No newline at end of file diff --git a/src/types/__snapshots__/resolveDescriptors.spec.ts.snap b/src/types/__snapshots__/resolveDescriptors.spec.ts.snap index 3cdb50b2f..4e42d50f4 100644 --- a/src/types/__snapshots__/resolveDescriptors.spec.ts.snap +++ b/src/types/__snapshots__/resolveDescriptors.spec.ts.snap @@ -30,6 +30,36 @@ Line 15, col 3: " `; +exports[`resolveDescriptors should fail descriptors for contract-const-string-receiver-not-found 1`] = ` +":21:5: Constant "self.struct.s2" not found +Line 21, col 5: + 20 | +> 21 | receive(self.struct.s2) { + ^~~~~~~~~~~~~~~~~~~~~~~~~ + 22 | +" +`; + +exports[`resolveDescriptors should fail descriptors for contract-const-string-receiver-not-found2 1`] = ` +":21:5: Constant "self.struct2.s" not found +Line 21, col 5: + 20 | +> 21 | receive(self.struct2.s) { + ^~~~~~~~~~~~~~~~~~~~~~~~~ + 22 | +" +`; + +exports[`resolveDescriptors should fail descriptors for contract-const-string-receiver-not-found3 1`] = ` +":25:5: Constant "self.struct.s" not found +Line 25, col 5: + 24 | +> 25 | receive(self.struct.s) { + ^~~~~~~~~~~~~~~~~~~~~~~~ + 26 | +" +`; + exports[`resolveDescriptors should fail descriptors for contract-duplicate-bounced-fallback-receiver 1`] = ` ":24:3: Fallback bounce receive function already exists Line 24, col 3: @@ -50,6 +80,56 @@ Line 24, col 3: " `; +exports[`resolveDescriptors should fail descriptors for contract-duplicate-const-string-receiver 1`] = ` +":20:5: Receive function for string "string 1" already exists +Line 20, col 5: + 19 | +> 20 | receive(string1) { + ^~~~~~~~~~~~~~~~~~ + 21 | +" +`; + +exports[`resolveDescriptors should fail descriptors for contract-duplicate-const-string-receiver2 1`] = ` +":15:5: Receive function for "string 1" already exists +Line 15, col 5: + 14 | +> 15 | receive("string 1") { + ^~~~~~~~~~~~~~~~~~~~~ + 16 | +" +`; + +exports[`resolveDescriptors should fail descriptors for contract-duplicate-const-string-receiver3 1`] = ` +":20:5: Constant "string3" not found +Line 20, col 5: + 19 | +> 20 | receive(string3) { + ^~~~~~~~~~~~~~~~~~ + 21 | +" +`; + +exports[`resolveDescriptors should fail descriptors for contract-duplicate-const-string-receiver4 1`] = ` +":15:5: Receive function for string "string 1" already exists +Line 15, col 5: + 14 | +> 15 | receive(self.string1) { + ^~~~~~~~~~~~~~~~~~~~~~~ + 16 | +" +`; + +exports[`resolveDescriptors should fail descriptors for contract-duplicate-const-string-receiver5 1`] = ` +":21:5: Receive function for string "string 1" already exists +Line 21, col 5: + 20 | +> 21 | receive(self.struct.s) { + ^~~~~~~~~~~~~~~~~~~~~~~~ + 22 | +" +`; + exports[`resolveDescriptors should fail descriptors for contract-duplicate-external-fallback-receiver 1`] = ` ":20:5: Empty receive function already exists Line 20, col 5: @@ -1170,6 +1250,678 @@ exports[`resolveDescriptors should resolve descriptors for contract-bounced-too- exports[`resolveDescriptors should resolve descriptors for contract-bounced-too-small-not-detected 2`] = `{}`; +exports[`resolveDescriptors should resolve descriptors for contract-const-string-receiver 1`] = ` +{ + "BaseTrait": { + "ast": { + "attributes": [], + "declarations": [], + "id": 3, + "kind": "def_trait", + "name": "BaseTrait", + "origin": "user", + "ref": trait BaseTrait { + +}, + "traits": [], + }, + "constants": [], + "dependsOn": [], + "fields": [], + "functions": Map {}, + "header": null, + "init": null, + "interfaces": [], + "kind": "trait", + "name": "BaseTrait", + "origin": "user", + "partialFieldCount": 0, + "receivers": [], + "signature": null, + "tlb": null, + "traits": [], + "uid": 1020, + }, + "Int": { + "ast": { + "id": 1, + "kind": "primitive", + "name": "Int", + "origin": "user", + "ref": primitive Int;, + }, + "constants": [], + "dependsOn": [], + "fields": [], + "functions": Map {}, + "header": null, + "init": null, + "interfaces": [], + "kind": "primitive", + "name": "Int", + "origin": "user", + "partialFieldCount": 0, + "receivers": [], + "signature": null, + "tlb": null, + "traits": [], + "uid": 38154, + }, + "Main": { + "ast": { + "attributes": [], + "declarations": [ + { + "attributes": [], + "id": 11, + "kind": "def_constant", + "name": "struct", + "ref": const struct: MyStruct = MyStruct { + s: "string 1" + };, + "type": { + "id": 7, + "kind": "type_ref_simple", + "name": "MyStruct", + "optional": false, + "ref": MyStruct, + }, + "value": { + "args": [ + { + "exp": { + "id": 8, + "kind": "string", + "ref": "string 1", + "value": "string 1", + }, + "id": 9, + "kind": "new_parameter", + "name": "s", + "ref": s: "string 1", + }, + ], + "id": 10, + "kind": "op_new", + "ref": MyStruct { + s: "string 1" + }, + "type": "MyStruct", + }, + }, + { + "id": 15, + "kind": "def_receive", + "ref": receive(self.struct.s) { + + }, + "selector": { + "comment": [ + { + "id": 12, + "kind": "lvalue_ref", + "name": "self", + "ref": self., + }, + { + "id": 13, + "kind": "lvalue_ref", + "name": "struct", + "ref": struct., + }, + { + "id": 14, + "kind": "lvalue_ref", + "name": "s", + "ref": s, + }, + ], + "kind": "internal-const-comment", + }, + "statements": [], + }, + ], + "id": 16, + "kind": "def_contract", + "name": "Main", + "origin": "user", + "ref": contract Main { + const struct: MyStruct = MyStruct { + s: "string 1" + }; + + receive(self.struct.s) { + + } +}, + "traits": [], + }, + "constants": [ + { + "ast": { + "attributes": [], + "id": 11, + "kind": "def_constant", + "name": "struct", + "ref": const struct: MyStruct = MyStruct { + s: "string 1" + };, + "type": { + "id": 7, + "kind": "type_ref_simple", + "name": "MyStruct", + "optional": false, + "ref": MyStruct, + }, + "value": { + "args": [ + { + "exp": { + "id": 8, + "kind": "string", + "ref": "string 1", + "value": "string 1", + }, + "id": 9, + "kind": "new_parameter", + "name": "s", + "ref": s: "string 1", + }, + ], + "id": 10, + "kind": "op_new", + "ref": MyStruct { + s: "string 1" + }, + "type": "MyStruct", + }, + }, + "name": "struct", + "ref": const struct: MyStruct = MyStruct { + s: "string 1" + };, + "type": { + "kind": "ref", + "name": "MyStruct", + "optional": false, + }, + "value": { + "$tactStruct": "MyStruct", + "s": "string 1", + }, + }, + ], + "dependsOn": [], + "fields": [], + "functions": Map {}, + "header": null, + "init": { + "args": [], + "ast": { + "args": [], + "id": 18, + "kind": "def_init_function", + "ref": contract Main { + const struct: MyStruct = MyStruct { + s: "string 1" + }; + + receive(self.struct.s) { + + } +}, + "statements": [], + }, + }, + "interfaces": [], + "kind": "contract", + "name": "Main", + "origin": "user", + "partialFieldCount": 0, + "receivers": [ + { + "ast": { + "id": 15, + "kind": "def_receive", + "ref": receive(self.struct.s) { + + }, + "selector": { + "comment": [ + { + "id": 12, + "kind": "lvalue_ref", + "name": "self", + "ref": self., + }, + { + "id": 13, + "kind": "lvalue_ref", + "name": "struct", + "ref": struct., + }, + { + "id": 14, + "kind": "lvalue_ref", + "name": "s", + "ref": s, + }, + ], + "kind": "internal-const-comment", + }, + "statements": [], + }, + "selector": { + "comment": "string 1", + "kind": "internal-comment", + }, + }, + ], + "signature": null, + "tlb": null, + "traits": [ + { + "ast": { + "attributes": [], + "declarations": [], + "id": 3, + "kind": "def_trait", + "name": "BaseTrait", + "origin": "user", + "ref": trait BaseTrait { + +}, + "traits": [], + }, + "constants": [], + "dependsOn": [], + "fields": [], + "functions": Map {}, + "header": null, + "init": null, + "interfaces": [], + "kind": "trait", + "name": "BaseTrait", + "origin": "user", + "partialFieldCount": 0, + "receivers": [], + "signature": null, + "tlb": null, + "traits": [], + "uid": 1020, + }, + ], + "uid": 51099, + }, + "MyStruct": { + "ast": { + "fields": [ + { + "as": null, + "id": 5, + "init": null, + "kind": "def_field", + "name": "s", + "ref": s: String, + "type": { + "id": 4, + "kind": "type_ref_simple", + "name": "String", + "optional": false, + "ref": String, + }, + }, + ], + "id": 6, + "kind": "def_struct", + "message": false, + "name": "MyStruct", + "origin": "user", + "prefix": null, + "ref": struct MyStruct { + s: String; +}, + }, + "constants": [], + "dependsOn": [], + "fields": [ + { + "abi": { + "name": "s", + "type": { + "kind": "simple", + "optional": false, + "type": "string", + }, + }, + "as": null, + "ast": { + "as": null, + "id": 5, + "init": null, + "kind": "def_field", + "name": "s", + "ref": s: String, + "type": { + "id": 4, + "kind": "type_ref_simple", + "name": "String", + "optional": false, + "ref": String, + }, + }, + "default": undefined, + "index": 0, + "name": "s", + "ref": s: String, + "type": { + "kind": "ref", + "name": "String", + "optional": false, + }, + }, + ], + "functions": Map {}, + "header": null, + "init": null, + "interfaces": [], + "kind": "struct", + "name": "MyStruct", + "origin": "user", + "partialFieldCount": 0, + "receivers": [], + "signature": "MyStruct{s:^string}", + "tlb": "_ s:^string = MyStruct", + "traits": [], + "uid": 55650, + }, + "String": { + "ast": { + "id": 2, + "kind": "primitive", + "name": "String", + "origin": "user", + "ref": primitive String;, + }, + "constants": [], + "dependsOn": [], + "fields": [], + "functions": Map {}, + "header": null, + "init": null, + "interfaces": [], + "kind": "primitive", + "name": "String", + "origin": "user", + "partialFieldCount": 0, + "receivers": [], + "signature": null, + "tlb": null, + "traits": [], + "uid": 46199, + }, +} +`; + +exports[`resolveDescriptors should resolve descriptors for contract-const-string-receiver 2`] = `{}`; + +exports[`resolveDescriptors should resolve descriptors for contract-const-string-receiver2 1`] = ` +{ + "BaseTrait": { + "ast": { + "attributes": [], + "declarations": [], + "id": 6, + "kind": "def_trait", + "name": "BaseTrait", + "origin": "user", + "ref": trait BaseTrait { + +}, + "traits": [], + }, + "constants": [], + "dependsOn": [], + "fields": [], + "functions": Map {}, + "header": null, + "init": null, + "interfaces": [], + "kind": "trait", + "name": "BaseTrait", + "origin": "user", + "partialFieldCount": 0, + "receivers": [], + "signature": null, + "tlb": null, + "traits": [], + "uid": 1020, + }, + "Int": { + "ast": { + "id": 1, + "kind": "primitive", + "name": "Int", + "origin": "user", + "ref": primitive Int;, + }, + "constants": [], + "dependsOn": [], + "fields": [], + "functions": Map {}, + "header": null, + "init": null, + "interfaces": [], + "kind": "primitive", + "name": "Int", + "origin": "user", + "partialFieldCount": 0, + "receivers": [], + "signature": null, + "tlb": null, + "traits": [], + "uid": 38154, + }, + "Main": { + "ast": { + "attributes": [], + "declarations": [ + { + "id": 8, + "kind": "def_receive", + "ref": receive("string 1") { + + }, + "selector": { + "comment": { + "id": 7, + "kind": "string", + "ref": "string 1", + "value": "string 1", + }, + "kind": "internal-comment", + }, + "statements": [], + }, + { + "id": 10, + "kind": "def_receive", + "ref": receive(string2) { + + }, + "selector": { + "comment": [ + { + "id": 9, + "kind": "lvalue_ref", + "name": "string2", + "ref": string2, + }, + ], + "kind": "internal-const-comment", + }, + "statements": [], + }, + ], + "id": 11, + "kind": "def_contract", + "name": "Main", + "origin": "user", + "ref": contract Main { + receive("string 1") { + + } + + receive(string2) { + + } +}, + "traits": [], + }, + "constants": [], + "dependsOn": [], + "fields": [], + "functions": Map {}, + "header": null, + "init": { + "args": [], + "ast": { + "args": [], + "id": 13, + "kind": "def_init_function", + "ref": contract Main { + receive("string 1") { + + } + + receive(string2) { + + } +}, + "statements": [], + }, + }, + "interfaces": [], + "kind": "contract", + "name": "Main", + "origin": "user", + "partialFieldCount": 0, + "receivers": [ + { + "ast": { + "id": 8, + "kind": "def_receive", + "ref": receive("string 1") { + + }, + "selector": { + "comment": { + "id": 7, + "kind": "string", + "ref": "string 1", + "value": "string 1", + }, + "kind": "internal-comment", + }, + "statements": [], + }, + "selector": { + "comment": "string 1", + "kind": "internal-comment", + }, + }, + { + "ast": { + "id": 10, + "kind": "def_receive", + "ref": receive(string2) { + + }, + "selector": { + "comment": [ + { + "id": 9, + "kind": "lvalue_ref", + "name": "string2", + "ref": string2, + }, + ], + "kind": "internal-const-comment", + }, + "statements": [], + }, + "selector": { + "comment": "string 2", + "kind": "internal-comment", + }, + }, + ], + "signature": null, + "tlb": null, + "traits": [ + { + "ast": { + "attributes": [], + "declarations": [], + "id": 6, + "kind": "def_trait", + "name": "BaseTrait", + "origin": "user", + "ref": trait BaseTrait { + +}, + "traits": [], + }, + "constants": [], + "dependsOn": [], + "fields": [], + "functions": Map {}, + "header": null, + "init": null, + "interfaces": [], + "kind": "trait", + "name": "BaseTrait", + "origin": "user", + "partialFieldCount": 0, + "receivers": [], + "signature": null, + "tlb": null, + "traits": [], + "uid": 1020, + }, + ], + "uid": 51099, + }, + "String": { + "ast": { + "id": 2, + "kind": "primitive", + "name": "String", + "origin": "user", + "ref": primitive String;, + }, + "constants": [], + "dependsOn": [], + "fields": [], + "functions": Map {}, + "header": null, + "init": null, + "interfaces": [], + "kind": "primitive", + "name": "String", + "origin": "user", + "partialFieldCount": 0, + "receivers": [], + "signature": null, + "tlb": null, + "traits": [], + "uid": 46199, + }, +} +`; + +exports[`resolveDescriptors should resolve descriptors for contract-const-string-receiver2 2`] = `{}`; + exports[`resolveDescriptors should resolve descriptors for contract-external-fallback-receiver 1`] = ` { "A": { diff --git a/src/types/resolveDescriptors.ts b/src/types/resolveDescriptors.ts index c84c1d13a..f00203316 100644 --- a/src/types/resolveDescriptors.ts +++ b/src/types/resolveDescriptors.ts @@ -25,6 +25,7 @@ import { TypeOrigin, TypeRef, typeRefEquals, + Value, } from "./types"; import { getRawAST } from "../grammar/store"; import { cloneNode } from "../grammar/clone"; @@ -792,6 +793,26 @@ export function resolveDescriptors(ctx: CompilerContext) { }; } + // + // Resolve static constants + // + + for (const a of ast.constants) { + if (staticConstants.has(a.name)) { + throwSyntaxError( + `Static constant "${a.name}" already exists`, + a.ref, + ); + } + if (staticFunctions.has(a.name) || GlobalFunctions.has(a.name)) { + throwSyntaxError( + `Static function "${a.name}" already exists`, + a.ref, + ); + } + staticConstants.set(a.name, buildConstantDescription(a)); + } + for (const a of ast.types) { if (a.kind === "def_contract" || a.kind === "def_trait") { const s = types.get(a.name)!; @@ -1006,6 +1027,95 @@ export function resolveDescriptors(ctx: CompilerContext) { }, ast: d, }); + } else if ( + d.selector.kind === "internal-const-comment" || + d.selector.kind === "external-const-comment" + ) { + const internal = + d.selector.kind === "internal-const-comment"; + + let path = d.selector.comment; + + const pathString = path.map((v) => v.name).join("."); + + const isSelf = + path.length >= 2 && path[0].name === "self"; + + let commentConstant: ConstantDescription | undefined = + undefined; + if (isSelf) { + commentConstant = s.constants.find( + (v) => v.name === path[1].name, + ); + path = path.slice(2); + } else { + commentConstant = staticConstants.get(path[0].name); + path = path.slice(1); + } + + let commentValue: Value | undefined = + commentConstant?.value; + + while (commentValue && path.length > 0) { + if ( + commentValue == null || + typeof commentValue !== "object" || + !("$tactStruct" in commentValue) + ) { + throwSyntaxError( + `Cannot find constant "${path[0].name}" in ${pathString}`, + d.ref, + ); + } + commentValue = commentValue[path[0].name]; + path = path.slice(1); + } + + if (!commentValue) { + throwSyntaxError( + `Constant "${pathString}" not found`, + d.ref, + ); + } + + if (typeof commentValue !== "string") { + throwSyntaxError( + `Constant "${pathString}" must be of type String`, + d.ref, + ); + } + + if (commentValue === "") { + throwSyntaxError( + "To use empty comment receiver, just remove argument instead of passing empty string", + d.ref, + ); + } + + if ( + s.receivers.find( + (v) => + v.selector.kind === + (internal + ? "internal-comment" + : "external-comment") && + v.selector.comment === commentValue, + ) + ) { + throwSyntaxError( + `Receive function for string "${commentValue}" already exists`, + d.ref, + ); + } + s.receivers.push({ + selector: { + kind: internal + ? "internal-comment" + : "external-comment", + comment: commentValue, + }, + ast: d, + }); } else if ( d.selector.kind === "internal-fallback" || d.selector.kind === "external-fallback" @@ -1595,26 +1705,6 @@ export function resolveDescriptors(ctx: CompilerContext) { } } - // - // Resolve static constants - // - - for (const a of ast.constants) { - if (staticConstants.has(a.name)) { - throwSyntaxError( - `Static constant "${a.name}" already exists`, - a.ref, - ); - } - if (staticFunctions.has(a.name) || GlobalFunctions.has(a.name)) { - throwSyntaxError( - `Static function "${a.name}" already exists`, - a.ref, - ); - } - staticConstants.set(a.name, buildConstantDescription(a)); - } - // // Register types and functions in context // diff --git a/src/types/test-failed/contract-const-string-receiver-not-found.tact b/src/types/test-failed/contract-const-string-receiver-not-found.tact new file mode 100644 index 000000000..1eb75e2aa --- /dev/null +++ b/src/types/test-failed/contract-const-string-receiver-not-found.tact @@ -0,0 +1,24 @@ +primitive Int; +primitive String; + +trait BaseTrait { + +} + +struct MyStruct { + s: String; +} + +contract Main { + const struct: MyStruct = MyStruct { + s: "string 1" + }; + + receive("string 1") { + + } + + receive(self.struct.s2) { + + } +} \ No newline at end of file diff --git a/src/types/test-failed/contract-const-string-receiver-not-found2.tact b/src/types/test-failed/contract-const-string-receiver-not-found2.tact new file mode 100644 index 000000000..8daa49efd --- /dev/null +++ b/src/types/test-failed/contract-const-string-receiver-not-found2.tact @@ -0,0 +1,24 @@ +primitive Int; +primitive String; + +trait BaseTrait { + +} + +struct MyStruct { + s: String; +} + +contract Main { + const struct: MyStruct = MyStruct { + s: "string 1" + }; + + receive("string 1") { + + } + + receive(self.struct2.s) { + + } +} \ No newline at end of file diff --git a/src/types/test-failed/contract-const-string-receiver-not-found3.tact b/src/types/test-failed/contract-const-string-receiver-not-found3.tact new file mode 100644 index 000000000..dd4ead50c --- /dev/null +++ b/src/types/test-failed/contract-const-string-receiver-not-found3.tact @@ -0,0 +1,28 @@ +primitive Int; +primitive String; + +trait BaseTrait { + +} + +struct MyStruct { + s: String; +} + +contract Main { + struct: MyStruct; + + init () { + self.struct = MyStruct { + s: "string 1" + }; + } + + receive("string 1") { + + } + + receive(self.struct.s) { + + } +} \ No newline at end of file diff --git a/src/types/test-failed/contract-duplicate-const-string-receiver.tact b/src/types/test-failed/contract-duplicate-const-string-receiver.tact new file mode 100644 index 000000000..67db131f7 --- /dev/null +++ b/src/types/test-failed/contract-duplicate-const-string-receiver.tact @@ -0,0 +1,23 @@ +primitive Int; +primitive String; + +const string1: String = "string 1"; +const string2: String = "string 2"; + +trait BaseTrait { + +} + +contract Main { + receive("string 1") { + + } + + receive("string 2") { + + } + + receive(string1) { + + } +} \ No newline at end of file diff --git a/src/types/test-failed/contract-duplicate-const-string-receiver2.tact b/src/types/test-failed/contract-duplicate-const-string-receiver2.tact new file mode 100644 index 000000000..d65d0cae0 --- /dev/null +++ b/src/types/test-failed/contract-duplicate-const-string-receiver2.tact @@ -0,0 +1,18 @@ +primitive Int; +primitive String; + +const string1: String = "string 1"; + +trait BaseTrait { + +} + +contract Main { + receive(string1) { + + } + + receive("string 1") { + + } +} \ No newline at end of file diff --git a/src/types/test-failed/contract-duplicate-const-string-receiver3.tact b/src/types/test-failed/contract-duplicate-const-string-receiver3.tact new file mode 100644 index 000000000..3d94d9a59 --- /dev/null +++ b/src/types/test-failed/contract-duplicate-const-string-receiver3.tact @@ -0,0 +1,23 @@ +primitive Int; +primitive String; + +const string1: String = "string 1"; +const string2: String = "string 2"; + +trait BaseTrait { + +} + +contract Main { + receive("string 1") { + + } + + receive("string 2") { + + } + + receive(string3) { + + } +} \ No newline at end of file diff --git a/src/types/test-failed/contract-duplicate-const-string-receiver4.tact b/src/types/test-failed/contract-duplicate-const-string-receiver4.tact new file mode 100644 index 000000000..045c7562c --- /dev/null +++ b/src/types/test-failed/contract-duplicate-const-string-receiver4.tact @@ -0,0 +1,18 @@ +primitive Int; +primitive String; + +trait BaseTrait { + +} + +contract Main { + const string1: String = "string 1"; + + receive("string 1") { + + } + + receive(self.string1) { + + } +} \ No newline at end of file diff --git a/src/types/test-failed/contract-duplicate-const-string-receiver5.tact b/src/types/test-failed/contract-duplicate-const-string-receiver5.tact new file mode 100644 index 000000000..315f1618a --- /dev/null +++ b/src/types/test-failed/contract-duplicate-const-string-receiver5.tact @@ -0,0 +1,24 @@ +primitive Int; +primitive String; + +trait BaseTrait { + +} + +struct MyStruct { + s: String; +} + +contract Main { + const struct: MyStruct = MyStruct { + s: "string 1" + }; + + receive("string 1") { + + } + + receive(self.struct.s) { + + } +} \ No newline at end of file diff --git a/src/types/test/contract-const-string-receiver.tact b/src/types/test/contract-const-string-receiver.tact new file mode 100644 index 000000000..cd0404fc3 --- /dev/null +++ b/src/types/test/contract-const-string-receiver.tact @@ -0,0 +1,20 @@ +primitive Int; +primitive String; + +trait BaseTrait { + +} + +struct MyStruct { + s: String; +} + +contract Main { + const struct: MyStruct = MyStruct { + s: "string 1" + }; + + receive(self.struct.s) { + + } +} \ No newline at end of file diff --git a/src/types/test/contract-const-string-receiver2.tact b/src/types/test/contract-const-string-receiver2.tact new file mode 100644 index 000000000..01729115e --- /dev/null +++ b/src/types/test/contract-const-string-receiver2.tact @@ -0,0 +1,18 @@ +primitive Int; +primitive String; + +const string2: String = "string 2"; + +trait BaseTrait { + +} + +contract Main { + receive("string 1") { + + } + + receive(string2) { + + } +} \ No newline at end of file diff --git a/tact.config.json b/tact.config.json index 4a98e5963..fb8d3d7d1 100644 --- a/tact.config.json +++ b/tact.config.json @@ -220,6 +220,11 @@ "external": true } }, + { + "name": "const-string-receiver", + "path": "./src/test/e2e-emulated/contracts/const-string-receiver.tact", + "output": "./src/test/e2e-emulated/contracts/output" + }, { "name": "debug", "path": "./src/test/e2e-emulated/contracts/debug.tact",