From f5ded91b1241db8e7a44cebb98579bb5656e40b9 Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Mon, 13 Jan 2025 23:36:39 +0400 Subject: [PATCH 1/4] feat(types): add 'The "remainder" field can only be the last field' inspection for contracts Fixes #765 --- .../resolveDescriptors.spec.ts.snap | 535 ++++++++++++++++++ src/types/resolveSignatures.ts | 63 ++- ...contract-decl-remainder-in-the-middle.tact | 14 + src/types/test/contract-decl-remainder.tact | 14 + 4 files changed, 604 insertions(+), 22 deletions(-) create mode 100644 src/types/test-failed/contract-decl-remainder-in-the-middle.tact create mode 100644 src/types/test/contract-decl-remainder.tact diff --git a/src/types/__snapshots__/resolveDescriptors.spec.ts.snap b/src/types/__snapshots__/resolveDescriptors.spec.ts.snap index c8091958f..e642bc735 100644 --- a/src/types/__snapshots__/resolveDescriptors.spec.ts.snap +++ b/src/types/__snapshots__/resolveDescriptors.spec.ts.snap @@ -150,6 +150,16 @@ Line 9, col 3: " `; +exports[`resolveDescriptors should fail descriptors for contract-decl-remainder-in-the-middle 1`] = ` +":8:5: The "remainder" field can only be the last field of the contract +Line 8, col 5: + 7 | a: Int = 0; +> 8 | s: Cell as remaining; + ^~~~~~~~~~~~~~~~~~~~ + 9 | b: Int = 0; +" +`; + exports[`resolveDescriptors should fail descriptors for contract-does-not-override-abstract-const 1`] = ` ":8:1: Trait "T" requires constant "Foo" Line 8, col 1: @@ -4055,6 +4065,531 @@ exports[`resolveDescriptors should resolve descriptors for contract-const-overri exports[`resolveDescriptors should resolve descriptors for contract-const-override-virtual 2`] = `[]`; +exports[`resolveDescriptors should resolve descriptors for contract-decl-remainder 1`] = ` +[ + { + "ast": { + "attributes": [], + "declarations": [], + "id": 2, + "kind": "trait", + "loc": trait BaseTrait { }, + "name": { + "id": 1, + "kind": "id", + "loc": BaseTrait, + "text": "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, + }, + { + "ast": { + "id": 4, + "kind": "primitive_type_decl", + "loc": primitive Int;, + "name": { + "id": 3, + "kind": "id", + "loc": Int, + "text": "Int", + }, + }, + "constants": [], + "dependsOn": [], + "fields": [], + "functions": Map {}, + "header": null, + "init": null, + "interfaces": [], + "kind": "primitive_type_decl", + "name": "Int", + "origin": "user", + "partialFieldCount": 0, + "receivers": [], + "signature": null, + "tlb": null, + "traits": [], + "uid": 38154, + }, + { + "ast": { + "id": 6, + "kind": "primitive_type_decl", + "loc": primitive Cell;, + "name": { + "id": 5, + "kind": "id", + "loc": Cell, + "text": "Cell", + }, + }, + "constants": [], + "dependsOn": [], + "fields": [], + "functions": Map {}, + "header": null, + "init": null, + "interfaces": [], + "kind": "primitive_type_decl", + "name": "Cell", + "origin": "user", + "partialFieldCount": 0, + "receivers": [], + "signature": null, + "tlb": null, + "traits": [], + "uid": 26294, + }, + { + "ast": { + "attributes": [], + "declarations": [ + { + "as": null, + "id": 11, + "initializer": { + "base": 10, + "id": 10, + "kind": "number", + "loc": 0, + "value": 0n, + }, + "kind": "field_decl", + "loc": a: Int = 0, + "name": { + "id": 8, + "kind": "id", + "loc": a, + "text": "a", + }, + "type": { + "id": 9, + "kind": "type_id", + "loc": Int, + "text": "Int", + }, + }, + { + "as": null, + "id": 15, + "initializer": { + "base": 10, + "id": 14, + "kind": "number", + "loc": 0, + "value": 0n, + }, + "kind": "field_decl", + "loc": b: Int = 0, + "name": { + "id": 12, + "kind": "id", + "loc": b, + "text": "b", + }, + "type": { + "id": 13, + "kind": "type_id", + "loc": Int, + "text": "Int", + }, + }, + { + "as": { + "id": 18, + "kind": "id", + "loc": remaining, + "text": "remaining", + }, + "id": 19, + "initializer": null, + "kind": "field_decl", + "loc": s: Cell as remaining, + "name": { + "id": 16, + "kind": "id", + "loc": s, + "text": "s", + }, + "type": { + "id": 17, + "kind": "type_id", + "loc": Cell, + "text": "Cell", + }, + }, + { + "id": 28, + "kind": "contract_init", + "loc": init(cell: Cell) { + self.s = cell; + }, + "params": [ + { + "id": 22, + "kind": "typed_parameter", + "loc": cell: Cell, + "name": { + "id": 20, + "kind": "id", + "loc": cell, + "text": "cell", + }, + "type": { + "id": 21, + "kind": "type_id", + "loc": Cell, + "text": "Cell", + }, + }, + ], + "statements": [ + { + "expression": { + "id": 26, + "kind": "id", + "loc": cell, + "text": "cell", + }, + "id": 27, + "kind": "statement_assign", + "loc": self.s = cell;, + "path": { + "aggregate": { + "id": 23, + "kind": "id", + "loc": self, + "text": "self", + }, + "field": { + "id": 24, + "kind": "id", + "loc": s, + "text": "s", + }, + "id": 25, + "kind": "field_access", + "loc": self.s, + }, + }, + ], + }, + ], + "id": 29, + "kind": "contract", + "loc": contract Test { + a: Int = 0; + b: Int = 0; + s: Cell as remaining; + + init(cell: Cell) { + self.s = cell; + } +}, + "name": { + "id": 7, + "kind": "id", + "loc": Test, + "text": "Test", + }, + "traits": [], + }, + "constants": [], + "dependsOn": [], + "fields": [ + { + "abi": { + "name": "a", + "type": { + "format": 257, + "kind": "simple", + "optional": false, + "type": "int", + }, + }, + "as": null, + "ast": { + "as": null, + "id": 11, + "initializer": { + "base": 10, + "id": 10, + "kind": "number", + "loc": 0, + "value": 0n, + }, + "kind": "field_decl", + "loc": a: Int = 0, + "name": { + "id": 8, + "kind": "id", + "loc": a, + "text": "a", + }, + "type": { + "id": 9, + "kind": "type_id", + "loc": Int, + "text": "Int", + }, + }, + "default": { + "base": 10, + "id": 10, + "kind": "number", + "loc": 0, + "value": 0n, + }, + "index": 0, + "loc": a: Int = 0, + "name": "a", + "type": { + "kind": "ref", + "name": "Int", + "optional": false, + }, + }, + { + "abi": { + "name": "b", + "type": { + "format": 257, + "kind": "simple", + "optional": false, + "type": "int", + }, + }, + "as": null, + "ast": { + "as": null, + "id": 15, + "initializer": { + "base": 10, + "id": 14, + "kind": "number", + "loc": 0, + "value": 0n, + }, + "kind": "field_decl", + "loc": b: Int = 0, + "name": { + "id": 12, + "kind": "id", + "loc": b, + "text": "b", + }, + "type": { + "id": 13, + "kind": "type_id", + "loc": Int, + "text": "Int", + }, + }, + "default": { + "base": 10, + "id": 14, + "kind": "number", + "loc": 0, + "value": 0n, + }, + "index": 1, + "loc": b: Int = 0, + "name": "b", + "type": { + "kind": "ref", + "name": "Int", + "optional": false, + }, + }, + { + "abi": { + "name": "s", + "type": { + "format": "remainder", + "kind": "simple", + "optional": false, + "type": "cell", + }, + }, + "as": "remaining", + "ast": { + "as": { + "id": 18, + "kind": "id", + "loc": remaining, + "text": "remaining", + }, + "id": 19, + "initializer": null, + "kind": "field_decl", + "loc": s: Cell as remaining, + "name": { + "id": 16, + "kind": "id", + "loc": s, + "text": "s", + }, + "type": { + "id": 17, + "kind": "type_id", + "loc": Cell, + "text": "Cell", + }, + }, + "default": undefined, + "index": 2, + "loc": s: Cell as remaining, + "name": "s", + "type": { + "kind": "ref", + "name": "Cell", + "optional": false, + }, + }, + ], + "functions": Map {}, + "header": null, + "init": { + "ast": { + "id": 28, + "kind": "contract_init", + "loc": init(cell: Cell) { + self.s = cell; + }, + "params": [ + { + "id": 22, + "kind": "typed_parameter", + "loc": cell: Cell, + "name": { + "id": 20, + "kind": "id", + "loc": cell, + "text": "cell", + }, + "type": { + "id": 21, + "kind": "type_id", + "loc": Cell, + "text": "Cell", + }, + }, + ], + "statements": [ + { + "expression": { + "id": 26, + "kind": "id", + "loc": cell, + "text": "cell", + }, + "id": 27, + "kind": "statement_assign", + "loc": self.s = cell;, + "path": { + "aggregate": { + "id": 23, + "kind": "id", + "loc": self, + "text": "self", + }, + "field": { + "id": 24, + "kind": "id", + "loc": s, + "text": "s", + }, + "id": 25, + "kind": "field_access", + "loc": self.s, + }, + }, + ], + }, + "params": [ + { + "as": null, + "loc": cell: Cell, + "name": { + "id": 20, + "kind": "id", + "loc": cell, + "text": "cell", + }, + "type": { + "kind": "ref", + "name": "Cell", + "optional": false, + }, + }, + ], + }, + "interfaces": [], + "kind": "contract", + "name": "Test", + "origin": "user", + "partialFieldCount": 0, + "receivers": [], + "signature": null, + "tlb": null, + "traits": [ + { + "ast": { + "attributes": [], + "declarations": [], + "id": 2, + "kind": "trait", + "loc": trait BaseTrait { }, + "name": { + "id": 1, + "kind": "id", + "loc": BaseTrait, + "text": "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": 44104, + }, +] +`; + +exports[`resolveDescriptors should resolve descriptors for contract-decl-remainder 2`] = `[]`; + exports[`resolveDescriptors should resolve descriptors for contract-external-fallback-receiver 1`] = ` [ { diff --git a/src/types/resolveSignatures.ts b/src/types/resolveSignatures.ts index 11167f14e..9ebbfc40e 100644 --- a/src/types/resolveSignatures.ts +++ b/src/types/resolveSignatures.ts @@ -12,6 +12,7 @@ import { BinaryReceiverSelector, CommentReceiverSelector, ReceiverDescription, + TypeDescription, } from "./types"; import { throwCompilationError } from "../errors"; import { AstNumber, AstReceiver, FactoryAst } from "../grammar/ast"; @@ -264,16 +265,38 @@ export function resolveSignatures(ctx: CompilerContext, Ast: FactoryAst) { return { signature, id, tlb }; } - getAllTypes(ctx).forEach((t) => { - if (t.kind === "struct") { - const r = createTupleSignature(t.name); - t.tlb = r.tlb; - t.signature = r.signature; - t.header = r.id; - } - }); + function checkAggregateTypes(ctx: CompilerContext) { + getAllTypes(ctx).forEach((aggregate) => { + switch (aggregate.kind) { + case "struct": { + const r = createTupleSignature(aggregate.name); + aggregate.tlb = r.tlb; + aggregate.signature = r.signature; + aggregate.header = r.id; + break; + } + case "contract": { + checkMessageOpcodesUniqueInContractOrTrait( + aggregate.receivers, + ctx, + ); + checkContractFields(aggregate); + break; + } + case "trait": { + checkMessageOpcodesUniqueInContractOrTrait( + aggregate.receivers, + ctx, + ); + break; + } + default: + break; + } + }); + } - checkMessageOpcodesUnique(ctx); + checkAggregateTypes(ctx); return ctx; } @@ -393,18 +416,14 @@ function checkMessageOpcodesUniqueInContractOrTrait( } } -function checkMessageOpcodesUnique(ctx: CompilerContext) { - getAllTypes(ctx).forEach((aggregate) => { - switch (aggregate.kind) { - case "contract": - case "trait": - checkMessageOpcodesUniqueInContractOrTrait( - aggregate.receivers, - ctx, - ); - break; - default: - break; +function checkContractFields(t: TypeDescription) { + // Check if "as remaining" is only used for the last field of contract + for (const field of t.fields.slice(0, -1)) { + if (field.as === "remaining") { + throwCompilationError( + `The "remainder" field can only be the last field of the contract`, + field.ast.loc, + ); } - }); + } } diff --git a/src/types/test-failed/contract-decl-remainder-in-the-middle.tact b/src/types/test-failed/contract-decl-remainder-in-the-middle.tact new file mode 100644 index 000000000..ec3c11d59 --- /dev/null +++ b/src/types/test-failed/contract-decl-remainder-in-the-middle.tact @@ -0,0 +1,14 @@ +trait BaseTrait { } + +primitive Int; +primitive Cell; + +contract Test { + a: Int = 0; + s: Cell as remaining; + b: Int = 0; + + init(cell: Cell) { + self.s = cell; + } +} diff --git a/src/types/test/contract-decl-remainder.tact b/src/types/test/contract-decl-remainder.tact new file mode 100644 index 000000000..bdfa32b8d --- /dev/null +++ b/src/types/test/contract-decl-remainder.tact @@ -0,0 +1,14 @@ +trait BaseTrait { } + +primitive Int; +primitive Cell; + +contract Test { + a: Int = 0; + b: Int = 0; + s: Cell as remaining; + + init(cell: Cell) { + self.s = cell; + } +} From 8e0ee2113fffde1d193d6aaefc13e63fa7b358ba Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Mon, 13 Jan 2025 23:56:02 +0400 Subject: [PATCH 2/4] process structs first --- src/types/resolveSignatures.ts | 60 +++++++++++++++++----------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/types/resolveSignatures.ts b/src/types/resolveSignatures.ts index 9ebbfc40e..5e1559660 100644 --- a/src/types/resolveSignatures.ts +++ b/src/types/resolveSignatures.ts @@ -265,36 +265,14 @@ export function resolveSignatures(ctx: CompilerContext, Ast: FactoryAst) { return { signature, id, tlb }; } - function checkAggregateTypes(ctx: CompilerContext) { - getAllTypes(ctx).forEach((aggregate) => { - switch (aggregate.kind) { - case "struct": { - const r = createTupleSignature(aggregate.name); - aggregate.tlb = r.tlb; - aggregate.signature = r.signature; - aggregate.header = r.id; - break; - } - case "contract": { - checkMessageOpcodesUniqueInContractOrTrait( - aggregate.receivers, - ctx, - ); - checkContractFields(aggregate); - break; - } - case "trait": { - checkMessageOpcodesUniqueInContractOrTrait( - aggregate.receivers, - ctx, - ); - break; - } - default: - break; - } - }); - } + getAllTypes(ctx).forEach((t) => { + if (t.kind === "struct") { + const r = createTupleSignature(t.name); + t.tlb = r.tlb; + t.signature = r.signature; + t.header = r.id; + } + }); checkAggregateTypes(ctx); @@ -416,6 +394,28 @@ function checkMessageOpcodesUniqueInContractOrTrait( } } +function checkAggregateTypes(ctx: CompilerContext) { + getAllTypes(ctx).forEach((aggregate) => { + switch (aggregate.kind) { + case "contract": + checkMessageOpcodesUniqueInContractOrTrait( + aggregate.receivers, + ctx, + ); + checkContractFields(aggregate); + break; + case "trait": + checkMessageOpcodesUniqueInContractOrTrait( + aggregate.receivers, + ctx, + ); + break; + default: + break; + } + }); +} + function checkContractFields(t: TypeDescription) { // Check if "as remaining" is only used for the last field of contract for (const field of t.fields.slice(0, -1)) { From c4dbccb8ccb2b142172bf60574323a924e3994d7 Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Tue, 14 Jan 2025 01:58:26 +0400 Subject: [PATCH 3/4] add CHANGELOG.md entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eaf53e5e6..e063cb701 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The `VarInt16`, `VarInt32`, `VarUint16`, `VarUint32` integer serialization types: PR [#1186](https://github.com/tact-lang/tact/pull/1186) - `unboc`: a standalone CLI utility to expose Tact's TVM disassembler: PR [#1259](https://github.com/tact-lang/tact/pull/1259) - Added alternative parser: PR [#1258](https://github.com/tact-lang/tact/pull/1258) +- 'The "remainder" field can only be the last field' inspection for contracts: PR [#1301](https://github.com/tact-lang/tact/pull/1301) ### Changed From 9832fcf41ffa529f22afab66e2a1a0877bd19549 Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Tue, 14 Jan 2025 10:27:26 +0400 Subject: [PATCH 4/4] moved and reworded CHANGELOG.md entry to Fixed --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e063cb701..bb08c23da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The `VarInt16`, `VarInt32`, `VarUint16`, `VarUint32` integer serialization types: PR [#1186](https://github.com/tact-lang/tact/pull/1186) - `unboc`: a standalone CLI utility to expose Tact's TVM disassembler: PR [#1259](https://github.com/tact-lang/tact/pull/1259) - Added alternative parser: PR [#1258](https://github.com/tact-lang/tact/pull/1258) -- 'The "remainder" field can only be the last field' inspection for contracts: PR [#1301](https://github.com/tact-lang/tact/pull/1301) ### Changed @@ -47,6 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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) - 'The "remainder" field can only be the last field:' inspection now shows location: PR [#1300](https://github.com/tact-lang/tact/pull/1300) +- Forbid "remainder" field at the middle of a contract storage: PR [#1301](https://github.com/tact-lang/tact/pull/1301) ### Docs