From 4b82e91ec0678c1f6b181902552224ddf91b6256 Mon Sep 17 00:00:00 2001 From: Anton Trunov Date: Sat, 5 Oct 2024 00:03:49 +0400 Subject: [PATCH 1/6] feat: specify methodId for getters --- CHANGELOG.md | 1 + docs/src/content/docs/book/functions.mdx | 31 ++++ knip.json | 6 +- src/bindings/writeTypescript.ts | 16 +- src/generator/createABI.ts | 1 + src/generator/writers/writeFunction.ts | 35 +++-- .../__snapshots__/grammar.spec.ts.snap | 138 ++++++++++++++++++ src/grammar/ast.ts | 2 +- src/grammar/grammar.ohm | 3 +- src/grammar/grammar.ts | 8 +- src/grammar/rename.ts | 26 +++- .../contract-getter-parens-no-method-id.tact | 5 + .../test/contract-getter-with-method-id.tact | 5 + src/prettyPrinter.ts | 12 +- .../resolveAllocation.spec.ts.snap | 1 + .../getter-with-method-id-const.tact | 7 + src/test/contracts/getter-with-method-id.tact | 6 + .../contracts/renamer-expected/case-4.tact | 4 +- .../getter-with-method-id-const.tact | 7 + .../getter-with-method-id.tact | 5 + .../local-type-inference.spec.ts.snap | 19 +++ src/test/e2e-emulated/contracts/getters.tact | 17 +++ src/test/e2e-emulated/getters.spec.ts | 5 + .../resolveDescriptors.spec.ts.snap | 83 ++++++----- .../resolveStatements.spec.ts.snap | 100 +++++++++++++ src/types/resolveDescriptors.ts | 21 +-- src/types/resolveStatements.ts | 84 ++++++++++- .../contract-getter-with-method-id-1.tact | 8 + .../contract-getter-with-method-id-2.tact | 8 + .../getter-collision-trait.tact | 0 .../getter-collision-with-trait.tact | 0 .../getter-collision.tact | 0 .../contract-getter-with-method-id-1.tact | 8 + .../contract-getter-with-method-id-2.tact | 10 ++ .../contract-getter-with-method-id-3.tact | 9 ++ src/types/types.ts | 1 + src/utils/utils.ts | 5 - 37 files changed, 603 insertions(+), 94 deletions(-) create mode 100644 src/grammar/test-failed/contract-getter-parens-no-method-id.tact create mode 100644 src/grammar/test/contract-getter-with-method-id.tact create mode 100644 src/test/contracts/getter-with-method-id-const.tact create mode 100644 src/test/contracts/getter-with-method-id.tact create mode 100644 src/test/contracts/renamer-expected/getter-with-method-id-const.tact create mode 100644 src/test/contracts/renamer-expected/getter-with-method-id.tact create mode 100644 src/types/stmts-failed/contract-getter-with-method-id-1.tact create mode 100644 src/types/stmts-failed/contract-getter-with-method-id-2.tact rename src/types/{test-failed => stmts-failed}/getter-collision-trait.tact (100%) rename src/types/{test-failed => stmts-failed}/getter-collision-with-trait.tact (100%) rename src/types/{test-failed => stmts-failed}/getter-collision.tact (100%) create mode 100644 src/types/stmts/contract-getter-with-method-id-1.tact create mode 100644 src/types/stmts/contract-getter-with-method-id-2.tact create mode 100644 src/types/stmts/contract-getter-with-method-id-3.tact diff --git a/CHANGELOG.md b/CHANGELOG.md index 882f22067..8be64ff73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `&&=`, `||=`, `>>=` and `<<=` augmented assignment operators: PR [#853](https://github.com/tact-lang/tact/pull/853) - New CSpell dictionaries: TVM instructions and adjusted list of Fift words: PR [#881](https://github.com/tact-lang/tact/pull/881) - Docs: the `description` property to the frontmatter of the each page for better SEO: PR [#916](https://github.com/tact-lang/tact/pull/916) +- Ability to specify a compile-time method ID expression for getters: PR [#922](https://github.com/tact-lang/tact/pull/922) ### Changed diff --git a/docs/src/content/docs/book/functions.mdx b/docs/src/content/docs/book/functions.mdx index ab96a9a98..c3eb46eec 100644 --- a/docs/src/content/docs/book/functions.mdx +++ b/docs/src/content/docs/book/functions.mdx @@ -128,6 +128,7 @@ contract Treasure { ## Getter Functions Getter functions define getters on smart contracts and can be defined only within a contract or trait. +Getter functions cannot be used to read some other contract's state: if you need to obtain some data you need to do that by sending a message with a request and define a receiver which would process the request answer. ```tact contract Treasure { @@ -136,3 +137,33 @@ contract Treasure { } } ``` + +As other functions in TVM contracts, getters have their *unique* associated function selectors which are some integers ids (called *method IDs*). +Some of those integers are reserved for internal puproses, e.g. -4, -3, -2, -1, 0 are reserved IDs and +regular functions (internal to a contract and not callable from outside) are usually numbered by subsequent (small) integers starting from 1. +By default, getters have associated method IDs that are derived from their names using the [CRC16](https://en.wikipedia.org/wiki/Cyclic_redundancy_check) algorithm as follows: +`crc16() & 0xffff) | 0x10000`. +Sometimes this can get you the same method ID for getters with different names. +If this happens, you can either rename some of the contract's getters or +specify the getter's method ID manually as a compile-time expression like so: + +```tact +contract ManualMethodId { + const methodId: Int = 16384 + 42; + + get(self.methodId) fun methodId1(): Int { + return self.methodId; + } + + get(crc32("crc32") + 42 & 0x3ffff | 0x4000) + fun methodId2(): Int { + return 0; + } +} +``` + +Note that you *cannot* use method IDs that are reserved by TVM and you cannot use some initial positive integers because those will be used as function selectors by the compiler. + +User-specified method IDs are 19-bit signed integers, so you can use integers from $2^{-18}$ to $-5$ and from $2^14$ to $2^18 - 1$. + +Also, a few method IDs are reserved for the usage by the getters the Tact compiler can insert during compilation, those are 113617, 115390, 121275. diff --git a/knip.json b/knip.json index d268afa29..b6828108d 100644 --- a/knip.json +++ b/knip.json @@ -7,9 +7,5 @@ "src/prettyPrinter.ts", ".github/workflows/tact*.yml" ], - "ignoreDependencies": [ - "@tact-lang/ton-abi", - "@tact-lang/ton-jest", - "@types/jest" - ] + "ignoreDependencies": ["@tact-lang/ton-abi"] } diff --git a/src/bindings/writeTypescript.ts b/src/bindings/writeTypescript.ts index e2b5c6f7c..4f06b19f6 100644 --- a/src/bindings/writeTypescript.ts +++ b/src/bindings/writeTypescript.ts @@ -600,9 +600,19 @@ export function writeTypescript( writeArgumentToStack(a.name, a.type, w); } } - w.append( - `let source = (await provider.get('${g.name}', builder.build())).stack;`, - ); + if (g.methodId) { + // 'as any' is used because Sandbox contracts's getters can be called + // using the function name or the method id number + // but the ContractProvider's interface get methods can only + // take strings (function names) + w.append( + `let source = (await provider.get(${g.methodId} as any, builder.build())).stack;`, + ); + } else { + w.append( + `let source = (await provider.get('${g.name}', builder.build())).stack;`, + ); + } if (g.returnType) { writeGetParser("result", g.returnType, w); w.append(`return result;`); diff --git a/src/generator/createABI.ts b/src/generator/createABI.ts index 45f6ae6e1..503edfa1c 100644 --- a/src/generator/createABI.ts +++ b/src/generator/createABI.ts @@ -123,6 +123,7 @@ export function createABI(ctx: CompilerContext, name: string): ContractABI { if (f.isGetter) { getters.push({ name: f.name, + methodId: f.methodId, arguments: f.params.map((v) => ({ name: idText(v.name), type: createABITypeRefFromTypeRef(ctx, v.type, v.loc), diff --git a/src/generator/writers/writeFunction.ts b/src/generator/writers/writeFunction.ts index 79cccaa43..980a57f9c 100644 --- a/src/generator/writers/writeFunction.ts +++ b/src/generator/writers/writeFunction.ts @@ -12,7 +12,6 @@ import { import { getType, resolveTypeRef } from "../../types/resolveDescriptors"; import { getExpType } from "../../types/resolveExpression"; import { FunctionDescription, TypeRef } from "../../types/types"; -import { getMethodId } from "../../utils/utils"; import { WriterContext } from "../Writer"; import { resolveFuncPrimitive } from "./resolveFuncPrimitive"; import { resolveFuncType } from "./resolveFuncType"; @@ -681,45 +680,45 @@ function writeNonMutatingFunction( }); } -export function writeGetter(f: FunctionDescription, ctx: WriterContext) { +export function writeGetter(f: FunctionDescription, wCtx: WriterContext) { // Render tensors - const self = f.self?.kind === "ref" ? getType(ctx.ctx, f.self.name) : null; + const self = f.self?.kind === "ref" ? getType(wCtx.ctx, f.self.name) : null; if (!self) { throw new Error(`No self type for getter ${idTextErr(f.name)}`); // Impossible } - ctx.append( - `_ %${f.name}(${f.params.map((v) => resolveFuncTupleType(v.type, ctx) + " " + funcIdOf(v.name)).join(", ")}) method_id(${getMethodId(f.name)}) {`, + wCtx.append( + `_ %${f.name}(${f.params.map((v) => resolveFuncTupleType(v.type, wCtx) + " " + funcIdOf(v.name)).join(", ")}) method_id(${f.methodId!}) {`, ); - ctx.inIndent(() => { + wCtx.inIndent(() => { // Unpack parameters for (const param of f.params) { unwrapExternal( funcIdOf(param.name), funcIdOf(param.name), param.type, - ctx, + wCtx, ); } // Load contract state - ctx.append(`var self = ${ops.contractLoad(self.name, ctx)}();`); + wCtx.append(`var self = ${ops.contractLoad(self.name, wCtx)}();`); // Execute get method - ctx.append( - `var res = self~${ctx.used(ops.extension(self.name, f.name))}(${f.params.map((v) => funcIdOf(v.name)).join(", ")});`, + wCtx.append( + `var res = self~${wCtx.used(ops.extension(self.name, f.name))}(${f.params.map((v) => funcIdOf(v.name)).join(", ")});`, ); // Pack if needed if (f.returns.kind === "ref") { - const t = getType(ctx.ctx, f.returns.name); + const t = getType(wCtx.ctx, f.returns.name); if (t.kind === "struct" || t.kind === "contract") { if (f.returns.optional) { - ctx.append( - `return ${ops.typeToOptExternal(t.name, ctx)}(res);`, + wCtx.append( + `return ${ops.typeToOptExternal(t.name, wCtx)}(res);`, ); } else { - ctx.append( - `return ${ops.typeToExternal(t.name, ctx)}(res);`, + wCtx.append( + `return ${ops.typeToExternal(t.name, wCtx)}(res);`, ); } return; @@ -727,8 +726,8 @@ export function writeGetter(f: FunctionDescription, ctx: WriterContext) { } // Return result - ctx.append(`return res;`); + wCtx.append(`return res;`); }); - ctx.append(`}`); - ctx.append(); + wCtx.append(`}`); + wCtx.append(); } diff --git a/src/grammar/__snapshots__/grammar.spec.ts.snap b/src/grammar/__snapshots__/grammar.spec.ts.snap index f3d715854..5bfd1e508 100644 --- a/src/grammar/__snapshots__/grammar.spec.ts.snap +++ b/src/grammar/__snapshots__/grammar.spec.ts.snap @@ -141,6 +141,17 @@ Line 1, col 20: " `; +exports[`grammar should fail contract-getter-parens-no-method-id 1`] = ` +":2:9: Parse error: expected "\\"", "initOf", "null", "_", "A".."Z", "a".."z", "false", "true", "0", "1".."9", "0O", "0o", "0B", "0b", "0X", "0x", "(", "~", "!", "+", or "-" + +Line 2, col 9: + 1 | contract Test { +> 2 | get() fun test(): Int { + ^ + 3 | return 0 +" +`; + exports[`grammar should fail contract-init-trailing-comma-empty-params 1`] = ` "Syntax error: :2:10: Empty parameter list should not have a dangling comma. Line 2, col 10: @@ -934,6 +945,133 @@ exports[`grammar should parse case-35 1`] = ` } `; +exports[`grammar should parse contract-getter-with-method-id 1`] = ` +{ + "id": 17, + "imports": [], + "items": [ + { + "attributes": [], + "declarations": [ + { + "attributes": [ + { + "loc": get(crc32("crc32") + 42 & 0x3ffff | 0x4000), + "methodId": { + "id": 10, + "kind": "op_binary", + "left": { + "id": 8, + "kind": "op_binary", + "left": { + "id": 6, + "kind": "op_binary", + "left": { + "args": [ + { + "id": 3, + "kind": "string", + "loc": "crc32", + "value": "crc32", + }, + ], + "function": { + "id": 2, + "kind": "id", + "loc": crc32, + "text": "crc32", + }, + "id": 4, + "kind": "static_call", + "loc": crc32("crc32"), + }, + "loc": crc32("crc32") + 42, + "op": "+", + "right": { + "base": 10, + "id": 5, + "kind": "number", + "loc": 42, + "value": 42n, + }, + }, + "loc": crc32("crc32") + 42 & 0x3ffff, + "op": "&", + "right": { + "base": 16, + "id": 7, + "kind": "number", + "loc": 0x3ffff, + "value": 262143n, + }, + }, + "loc": crc32("crc32") + 42 & 0x3ffff | 0x4000, + "op": "|", + "right": { + "base": 16, + "id": 9, + "kind": "number", + "loc": 0x4000, + "value": 16384n, + }, + }, + "type": "get", + }, + ], + "id": 15, + "kind": "function_def", + "loc": get(crc32("crc32") + 42 & 0x3ffff | 0x4000) fun test(): Int { + return 0 + }, + "name": { + "id": 11, + "kind": "id", + "loc": test, + "text": "test", + }, + "params": [], + "return": { + "id": 12, + "kind": "type_id", + "loc": Int, + "text": "Int", + }, + "statements": [ + { + "expression": { + "base": 10, + "id": 13, + "kind": "number", + "loc": 0, + "value": 0n, + }, + "id": 14, + "kind": "statement_return", + "loc": return 0, + }, + ], + }, + ], + "id": 16, + "kind": "contract", + "loc": contract Test { + get(crc32("crc32") + 42 & 0x3ffff | 0x4000) fun test(): Int { + return 0 + } +}, + "name": { + "id": 1, + "kind": "id", + "loc": Test, + "text": "Test", + }, + "traits": [], + }, + ], + "kind": "module", +} +`; + exports[`grammar should parse contract-optional-semicolon-for-last-const-def 1`] = ` { "id": 7, diff --git a/src/grammar/ast.ts b/src/grammar/ast.ts index c8cdfa6df..b9267b6d1 100644 --- a/src/grammar/ast.ts +++ b/src/grammar/ast.ts @@ -633,7 +633,7 @@ export type AstContractAttribute = { }; export type AstFunctionAttribute = - | { type: "get"; loc: SrcInfo } + | { type: "get"; methodId: AstExpression | null; loc: SrcInfo } | { type: "mutates"; loc: SrcInfo } | { type: "extends"; loc: SrcInfo } | { type: "virtual"; loc: SrcInfo } diff --git a/src/grammar/grammar.ohm b/src/grammar/grammar.ohm index 3b8dec037..d4f38897e 100644 --- a/src/grammar/grammar.ohm +++ b/src/grammar/grammar.ohm @@ -74,7 +74,8 @@ Tact { ContractAttribute = "@interface" "(" stringLiteral ")" --interface - FunctionAttribute = "get" --getter // 'get' cannot be a reserved word because there is the map '.get' method + // 'get' cannot be a reserved word because there is the map '.get' method + FunctionAttribute = "get" ("(" Expression ")")? --getter | mutates --mutates | extends --extends | virtual --virtual diff --git a/src/grammar/grammar.ts b/src/grammar/grammar.ts index ef702b3c3..760d58b13 100644 --- a/src/grammar/grammar.ts +++ b/src/grammar/grammar.ts @@ -583,8 +583,12 @@ semantics.addOperation("astOfAsmInstruction", { }); semantics.addOperation("astOfFunctionAttributes", { - FunctionAttribute_getter(_) { - return { type: "get", loc: createRef(this) }; + FunctionAttribute_getter(_getKwd, _optLparen, optMethodId, _optRparen) { + return { + type: "get", + methodId: unwrapOptNode(optMethodId, (e) => e.astOfExpression()), + loc: createRef(this), + }; }, FunctionAttribute_extends(_) { return { type: "extends", loc: createRef(this) }; diff --git a/src/grammar/rename.ts b/src/grammar/rename.ts index 6d46105b5..7ec7523d0 100644 --- a/src/grammar/rename.ts +++ b/src/grammar/rename.ts @@ -15,6 +15,7 @@ import { AstFunctionDecl, AstConstantDecl, AstNode, + AstFunctionAttribute, } from "./ast"; import { dummySrcInfo } from "./grammar"; import { AstSorter } from "./sort"; @@ -266,8 +267,29 @@ export class AstRenamer { private renameFunctionContents( functionDef: AstFunctionDef, ): AstFunctionDef { + const attributes = this.renameFunctionAttributes( + functionDef.attributes, + ); const statements = this.renameStatements(functionDef.statements); - return { ...functionDef, statements }; + return { ...functionDef, attributes, statements }; + } + + /** + * Renames getter's methodId expression. + */ + private renameFunctionAttributes( + functionAttrs: AstFunctionAttribute[], + ): AstFunctionAttribute[] { + return functionAttrs.map((attr) => { + if (attr.type === "get" && attr.methodId !== null) { + return { + ...attr, + methodId: this.renameExpression(attr.methodId), + }; + } else { + return attr; + } + }); } /** @@ -403,7 +425,7 @@ export class AstRenamer { case "id": return { ...expr, - text: this.renamed.get(expr.text) ?? expr.text, + text: this.givenNames.get(expr.text) ?? expr.text, }; case "op_binary": return { diff --git a/src/grammar/test-failed/contract-getter-parens-no-method-id.tact b/src/grammar/test-failed/contract-getter-parens-no-method-id.tact new file mode 100644 index 000000000..f3de3915f --- /dev/null +++ b/src/grammar/test-failed/contract-getter-parens-no-method-id.tact @@ -0,0 +1,5 @@ +contract Test { + get() fun test(): Int { + return 0 + } +} diff --git a/src/grammar/test/contract-getter-with-method-id.tact b/src/grammar/test/contract-getter-with-method-id.tact new file mode 100644 index 000000000..2026cdaf4 --- /dev/null +++ b/src/grammar/test/contract-getter-with-method-id.tact @@ -0,0 +1,5 @@ +contract Test { + get(crc32("crc32") + 42 & 0x3ffff | 0x4000) fun test(): Int { + return 0 + } +} diff --git a/src/prettyPrinter.ts b/src/prettyPrinter.ts index b70e69e01..0ac67d4d9 100644 --- a/src/prettyPrinter.ts +++ b/src/prettyPrinter.ts @@ -464,12 +464,22 @@ export class PrettyPrinter { `${this.ppAstId(arg.name)}: ${this.ppAstType(arg.type)}`, ) .join(", "); - const attrsRaw = attributes.map((attr) => attr.type).join(" "); + const attrsRaw = attributes + .map((attr) => this.ppAstFunctionAttribute(attr)) + .join(" "); const attrsFormatted = attrsRaw ? `${attrsRaw} ` : ""; const returnType = retTy ? `: ${this.ppAstType(retTy)}` : ""; return `${attrsFormatted}fun ${this.ppAstId(name)}(${argsFormatted})${returnType}`; } + ppAstFunctionAttribute(attr: AstFunctionAttribute): string { + if (attr.type === "get" && attr.methodId !== null) { + return `get(${this.ppAstExpression(attr.methodId)})`; + } else { + return attr.type; + } + } + ppAstReceiver(receive: AstReceiver): string { const header = this.ppAstReceiverHeader(receive); const stmtsFormatted = this.ppStatementBlock(receive.statements); diff --git a/src/storage/__snapshots__/resolveAllocation.spec.ts.snap b/src/storage/__snapshots__/resolveAllocation.spec.ts.snap index 274c24bff..79708c492 100644 --- a/src/storage/__snapshots__/resolveAllocation.spec.ts.snap +++ b/src/storage/__snapshots__/resolveAllocation.spec.ts.snap @@ -2258,6 +2258,7 @@ exports[`resolveAllocation should write program 1`] = ` "isMutating": true, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "main", "origin": "user", "params": [ diff --git a/src/test/contracts/getter-with-method-id-const.tact b/src/test/contracts/getter-with-method-id-const.tact new file mode 100644 index 000000000..6780dc499 --- /dev/null +++ b/src/test/contracts/getter-with-method-id-const.tact @@ -0,0 +1,7 @@ +const FOO: Int = crc32("crc32") + 42; + +contract Test { + get(FOO) fun test(): Int { + return FOO; + } +} diff --git a/src/test/contracts/getter-with-method-id.tact b/src/test/contracts/getter-with-method-id.tact new file mode 100644 index 000000000..9857e558e --- /dev/null +++ b/src/test/contracts/getter-with-method-id.tact @@ -0,0 +1,6 @@ +contract Test { + get(crc32("crc32") + 42 & 0x3ffff | 0x4000) fun test(): Int { + return 0; + } +} + diff --git a/src/test/contracts/renamer-expected/case-4.tact b/src/test/contracts/renamer-expected/case-4.tact index a6f007b01..28dbc9e1e 100644 --- a/src/test/contracts/renamer-expected/case-4.tact +++ b/src/test/contracts/renamer-expected/case-4.tact @@ -37,10 +37,10 @@ contract contract_6 { fun function_def_8() { let d: Int? = null; self.a = 10; - d = a > 0 ? self.a : 0; + d = constant_def_2 > 0 ? self.a : 0; let res: Bool = isZero(1, 2, false, self.c); let e = 42; - self.b = a; + self.b = constant_def_2; self.c = Source{a: 10, b: 20}; } diff --git a/src/test/contracts/renamer-expected/getter-with-method-id-const.tact b/src/test/contracts/renamer-expected/getter-with-method-id-const.tact new file mode 100644 index 000000000..e842470d9 --- /dev/null +++ b/src/test/contracts/renamer-expected/getter-with-method-id-const.tact @@ -0,0 +1,7 @@ +const constant_def_0: Int = crc32("crc32") + 42; + +contract contract_1 { + get(constant_def_0) fun function_def_2(): Int { + return constant_def_0; + } +} diff --git a/src/test/contracts/renamer-expected/getter-with-method-id.tact b/src/test/contracts/renamer-expected/getter-with-method-id.tact new file mode 100644 index 000000000..2d8e63ff3 --- /dev/null +++ b/src/test/contracts/renamer-expected/getter-with-method-id.tact @@ -0,0 +1,5 @@ +contract contract_0 { + get(crc32("crc32") + 42 & 0x3ffff | 0x4000) fun function_def_1(): Int { + return 0; + } +} diff --git a/src/test/e2e-emulated/__snapshots__/local-type-inference.spec.ts.snap b/src/test/e2e-emulated/__snapshots__/local-type-inference.spec.ts.snap index 9d3c1e7d9..33a4b617f 100644 --- a/src/test/e2e-emulated/__snapshots__/local-type-inference.spec.ts.snap +++ b/src/test/e2e-emulated/__snapshots__/local-type-inference.spec.ts.snap @@ -115,6 +115,7 @@ exports[`local-type-inference should automatically set types for let statements "getters": [ { "arguments": [], + "methodId": 70304, "name": "test1", "returnType": { "format": 257, @@ -125,6 +126,7 @@ exports[`local-type-inference should automatically set types for let statements }, { "arguments": [], + "methodId": 74435, "name": "test2", "returnType": { "format": 257, @@ -135,6 +137,7 @@ exports[`local-type-inference should automatically set types for let statements }, { "arguments": [], + "methodId": 78562, "name": "test3", "returnType": { "kind": "simple", @@ -144,6 +147,7 @@ exports[`local-type-inference should automatically set types for let statements }, { "arguments": [], + "methodId": 82437, "name": "test4", "returnType": { "kind": "simple", @@ -153,6 +157,7 @@ exports[`local-type-inference should automatically set types for let statements }, { "arguments": [], + "methodId": 86564, "name": "test5", "returnType": { "kind": "simple", @@ -162,6 +167,7 @@ exports[`local-type-inference should automatically set types for let statements }, { "arguments": [], + "methodId": 90695, "name": "test6", "returnType": { "kind": "simple", @@ -171,6 +177,7 @@ exports[`local-type-inference should automatically set types for let statements }, { "arguments": [], + "methodId": 94822, "name": "test7", "returnType": { "kind": "simple", @@ -180,6 +187,7 @@ exports[`local-type-inference should automatically set types for let statements }, { "arguments": [], + "methodId": 99209, "name": "test8", "returnType": { "kind": "simple", @@ -189,6 +197,7 @@ exports[`local-type-inference should automatically set types for let statements }, { "arguments": [], + "methodId": 103336, "name": "test9", "returnType": { "kind": "simple", @@ -198,6 +207,7 @@ exports[`local-type-inference should automatically set types for let statements }, { "arguments": [], + "methodId": 107552, "name": "test10", "returnType": { "kind": "simple", @@ -207,6 +217,7 @@ exports[`local-type-inference should automatically set types for let statements }, { "arguments": [], + "methodId": 111617, "name": "test11", "returnType": { "kind": "simple", @@ -216,6 +227,7 @@ exports[`local-type-inference should automatically set types for let statements }, { "arguments": [], + "methodId": 99426, "name": "test12", "returnType": { "key": "int", @@ -225,6 +237,7 @@ exports[`local-type-inference should automatically set types for let statements }, { "arguments": [], + "methodId": 103491, "name": "test13", "returnType": { "key": "int", @@ -235,6 +248,7 @@ exports[`local-type-inference should automatically set types for let statements }, { "arguments": [], + "methodId": 124068, "name": "test14", "returnType": { "kind": "simple", @@ -244,6 +258,7 @@ exports[`local-type-inference should automatically set types for let statements }, { "arguments": [], + "methodId": 128133, "name": "test15", "returnType": { "kind": "simple", @@ -253,6 +268,7 @@ exports[`local-type-inference should automatically set types for let statements }, { "arguments": [], + "methodId": 115942, "name": "test16", "returnType": { "format": 257, @@ -263,6 +279,7 @@ exports[`local-type-inference should automatically set types for let statements }, { "arguments": [], + "methodId": 120007, "name": "test17", "returnType": { "format": 257, @@ -273,6 +290,7 @@ exports[`local-type-inference should automatically set types for let statements }, { "arguments": [], + "methodId": 75048, "name": "test18", "returnType": { "format": 257, @@ -283,6 +301,7 @@ exports[`local-type-inference should automatically set types for let statements }, { "arguments": [], + "methodId": 79113, "name": "test19", "returnType": { "format": 257, diff --git a/src/test/e2e-emulated/contracts/getters.tact b/src/test/e2e-emulated/contracts/getters.tact index bbc89b550..18e5d5026 100644 --- a/src/test/e2e-emulated/contracts/getters.tact +++ b/src/test/e2e-emulated/contracts/getters.tact @@ -10,6 +10,8 @@ message SetIdAndData { data: Cell; } +const METHOD_ID: Int = 16384; + contract Test with Deployable { id: Int as uint32 = 0; anotherData: Cell; @@ -54,4 +56,19 @@ contract Test with Deployable { get fun contractAsInput(test: Test): Test { return test; } + + // method ids are 19 bit signed integers + // so we truncate the result with a (18-bit) bit mask + get((crc32("methodId") + 42) & 0b111111111111111111) fun methodIdExpr(): Bool { + return true; + } + get(METHOD_ID) fun methodIdConst(): Int { + return METHOD_ID; + } + get(-pow2(18)) fun methodIdMin(): Bool { + return true; + } + get(pow2(18) - 1) fun methodIdMax(): Bool { + return true; + } } \ No newline at end of file diff --git a/src/test/e2e-emulated/getters.spec.ts b/src/test/e2e-emulated/getters.spec.ts index 8719f19cf..341307ed4 100644 --- a/src/test/e2e-emulated/getters.spec.ts +++ b/src/test/e2e-emulated/getters.spec.ts @@ -78,5 +78,10 @@ describe("getters", () => { anotherData: beginCell().storeUint(123, 64).endCell(), }), ).toMatchSnapshot(); + + expect(await contract.getMethodIdExpr()).toBe(true); + expect(await contract.getMethodIdConst()).toBe(2n ** 14n); + expect(await contract.getMethodIdMin()).toBe(true); + expect(await contract.getMethodIdMax()).toBe(true); }); }); diff --git a/src/types/__snapshots__/resolveDescriptors.spec.ts.snap b/src/types/__snapshots__/resolveDescriptors.spec.ts.snap index dd0d9ca5d..8437d6927 100644 --- a/src/types/__snapshots__/resolveDescriptors.spec.ts.snap +++ b/src/types/__snapshots__/resolveDescriptors.spec.ts.snap @@ -328,39 +328,6 @@ Line 4, col 17: " `; -exports[`resolveDescriptors should fail descriptors for getter-collision 1`] = ` -":8:13: Method ID collision: getter 'pko' has the same method ID 103057 as getter 'getter1' -Pick a different getter name to avoid collisions -Line 8, col 13: - 7 | -> 8 | get fun pko() { - ^~~ - 9 | return; -" -`; - -exports[`resolveDescriptors should fail descriptors for getter-collision-trait 1`] = ` -":8:13: Method ID collision: getter 'pko' has the same method ID 103057 as getter 'getter1' -Pick a different getter name to avoid collisions -Line 8, col 13: - 7 | -> 8 | get fun pko() { - ^~~ - 9 | return; -" -`; - -exports[`resolveDescriptors should fail descriptors for getter-collision-with-trait 1`] = ` -":4:13: Method ID collision: getter 'pko' has the same method ID 103057 as getter 'getter1' -Pick a different getter name to avoid collisions -Line 4, col 13: - 3 | trait T { -> 4 | get fun pko() { - ^~~ - 5 | return; -" -`; - exports[`resolveDescriptors should fail descriptors for getter-outside-contract 1`] = ` ":8:1: Getters must be defined within a contract Line 8, col 1: @@ -761,6 +728,7 @@ exports[`resolveDescriptors should resolve descriptors for asm-extends-fun 1`] = "isMutating": true, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "loadRef", "origin": "user", "params": [], @@ -920,6 +888,7 @@ exports[`resolveDescriptors should resolve descriptors for asm-fun-no-arg-shuffl "isMutating": false, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "foo", "origin": "user", "params": [ @@ -1064,6 +1033,7 @@ exports[`resolveDescriptors should resolve descriptors for asm-fun-no-ret-shuffl "isMutating": false, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "foo", "origin": "user", "params": [ @@ -1188,6 +1158,7 @@ exports[`resolveDescriptors should resolve descriptors for asm-fun-no-shuffle 2` "isMutating": false, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "foo", "origin": "user", "params": [ @@ -1485,6 +1456,7 @@ exports[`resolveDescriptors should resolve descriptors for asm-fun-shuffle-pair "isMutating": false, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "foo", "origin": "user", "params": [ @@ -4428,6 +4400,7 @@ exports[`resolveDescriptors should resolve descriptors for contract-getter-overr }, { "loc": get, + "methodId": null, "type": "get", }, ], @@ -4475,6 +4448,7 @@ exports[`resolveDescriptors should resolve descriptors for contract-getter-overr }, { "loc": get, + "methodId": null, "type": "get", }, ], @@ -4501,6 +4475,7 @@ exports[`resolveDescriptors should resolve descriptors for contract-getter-overr "isMutating": true, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "getter", "origin": "user", "params": [], @@ -4574,6 +4549,7 @@ exports[`resolveDescriptors should resolve descriptors for contract-getter-overr }, { "loc": get, + "methodId": null, "type": "get", }, ], @@ -4642,6 +4618,7 @@ exports[`resolveDescriptors should resolve descriptors for contract-getter-overr }, { "loc": get, + "methodId": null, "type": "get", }, ], @@ -4682,6 +4659,7 @@ exports[`resolveDescriptors should resolve descriptors for contract-getter-overr "isMutating": true, "isOverride": true, "isVirtual": false, + "methodId": null, "name": "getter", "origin": "user", "params": [], @@ -4763,6 +4741,7 @@ exports[`resolveDescriptors should resolve descriptors for contract-getter-overr }, { "loc": get, + "methodId": null, "type": "get", }, ], @@ -4810,6 +4789,7 @@ exports[`resolveDescriptors should resolve descriptors for contract-getter-overr }, { "loc": get, + "methodId": null, "type": "get", }, ], @@ -4836,6 +4816,7 @@ exports[`resolveDescriptors should resolve descriptors for contract-getter-overr "isMutating": true, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "getter", "origin": "user", "params": [], @@ -4980,6 +4961,7 @@ exports[`resolveDescriptors should resolve descriptors for contract-getter-overr }, { "loc": get, + "methodId": null, "type": "get", }, ], @@ -5041,6 +5023,7 @@ exports[`resolveDescriptors should resolve descriptors for contract-getter-overr }, { "loc": get, + "methodId": null, "type": "get", }, ], @@ -5081,6 +5064,7 @@ exports[`resolveDescriptors should resolve descriptors for contract-getter-overr "isMutating": true, "isOverride": false, "isVirtual": true, + "methodId": null, "name": "getter", "origin": "user", "params": [], @@ -5154,6 +5138,7 @@ exports[`resolveDescriptors should resolve descriptors for contract-getter-overr }, { "loc": get, + "methodId": null, "type": "get", }, ], @@ -5222,6 +5207,7 @@ exports[`resolveDescriptors should resolve descriptors for contract-getter-overr }, { "loc": get, + "methodId": null, "type": "get", }, ], @@ -5262,6 +5248,7 @@ exports[`resolveDescriptors should resolve descriptors for contract-getter-overr "isMutating": true, "isOverride": true, "isVirtual": false, + "methodId": null, "name": "getter", "origin": "user", "params": [], @@ -5343,6 +5330,7 @@ exports[`resolveDescriptors should resolve descriptors for contract-getter-overr }, { "loc": get, + "methodId": null, "type": "get", }, ], @@ -5404,6 +5392,7 @@ exports[`resolveDescriptors should resolve descriptors for contract-getter-overr }, { "loc": get, + "methodId": null, "type": "get", }, ], @@ -5444,6 +5433,7 @@ exports[`resolveDescriptors should resolve descriptors for contract-getter-overr "isMutating": true, "isOverride": false, "isVirtual": true, + "methodId": null, "name": "getter", "origin": "user", "params": [], @@ -5618,6 +5608,7 @@ exports[`resolveDescriptors should resolve descriptors for fun-extends-opt-self "isMutating": false, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "test_extends", "origin": "user", "params": [ @@ -5734,6 +5725,7 @@ exports[`resolveDescriptors should resolve descriptors for fun-extends-opt-self "isMutating": false, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "test_extends_self", "origin": "user", "params": [ @@ -5854,6 +5846,7 @@ exports[`resolveDescriptors should resolve descriptors for fun-extends-opt-self "isMutating": true, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "test_mutates", "origin": "user", "params": [ @@ -5974,6 +5967,7 @@ exports[`resolveDescriptors should resolve descriptors for fun-extends-opt-self "isMutating": true, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "test_mutates_self", "origin": "user", "params": [ @@ -6097,6 +6091,7 @@ exports[`resolveDescriptors should resolve descriptors for fun-extends-opt-self "isMutating": true, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "test_mutates_self_opt", "origin": "user", "params": [ @@ -6227,6 +6222,7 @@ exports[`resolveDescriptors should resolve descriptors for fun-extends-opt-self "isMutating": false, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "test", "origin": "user", "params": [ @@ -6583,6 +6579,7 @@ exports[`resolveDescriptors should resolve descriptors for init-vars-analysis-un "isMutating": true, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "hello", "origin": "user", "params": [], @@ -6619,6 +6616,7 @@ exports[`resolveDescriptors should resolve descriptors for init-vars-analysis-un "isMutating": true, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "hello2", "origin": "user", "params": [], @@ -6837,6 +6835,7 @@ exports[`resolveDescriptors should resolve descriptors for item-funs-with-errors "isMutating": false, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "testFunc", "origin": "user", "params": [], @@ -6884,6 +6883,7 @@ exports[`resolveDescriptors should resolve descriptors for item-funs-with-errors "isMutating": false, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "testFunc2", "origin": "user", "params": [], @@ -6964,6 +6964,7 @@ exports[`resolveDescriptors should resolve descriptors for item-funs-with-errors "isMutating": false, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "testFunc3", "origin": "user", "params": [ @@ -7274,6 +7275,7 @@ exports[`resolveDescriptors should resolve descriptors for item-funs-with-errors "attributes": [ { "loc": get, + "methodId": null, "type": "get", }, ], @@ -7314,6 +7316,7 @@ exports[`resolveDescriptors should resolve descriptors for item-funs-with-errors "attributes": [ { "loc": get, + "methodId": null, "type": "get", }, ], @@ -7424,6 +7427,7 @@ exports[`resolveDescriptors should resolve descriptors for item-funs-with-errors "attributes": [ { "loc": get, + "methodId": null, "type": "get", }, ], @@ -7466,6 +7470,7 @@ exports[`resolveDescriptors should resolve descriptors for item-funs-with-errors "isMutating": true, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "hello", "origin": "user", "params": [], @@ -7485,6 +7490,7 @@ exports[`resolveDescriptors should resolve descriptors for item-funs-with-errors "attributes": [ { "loc": get, + "methodId": null, "type": "get", }, ], @@ -7527,6 +7533,7 @@ exports[`resolveDescriptors should resolve descriptors for item-funs-with-errors "isMutating": true, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "hello2", "origin": "user", "params": [], @@ -7764,6 +7771,7 @@ exports[`resolveDescriptors should resolve descriptors for item-method 1`] = ` "isMutating": false, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "inc", "origin": "user", "params": [], @@ -8010,6 +8018,7 @@ native sample(a: Int): Int;, "isMutating": false, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "sample", "origin": "user", "params": [ @@ -8116,6 +8125,7 @@ mutates extends native inc(self: Int): Int;, "isMutating": true, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "inc", "origin": "user", "params": [], @@ -8322,6 +8332,7 @@ exports[`resolveDescriptors should resolve descriptors for map-value-as-coins 1` "attributes": [ { "loc": get, + "methodId": null, "type": "get", }, ], @@ -8649,6 +8660,7 @@ exports[`resolveDescriptors should resolve descriptors for map-value-as-coins 1` "attributes": [ { "loc": get, + "methodId": null, "type": "get", }, ], @@ -8890,6 +8902,7 @@ exports[`resolveDescriptors should resolve descriptors for map-value-as-coins 1` "isMutating": true, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "test", "origin": "user", "params": [], @@ -9070,6 +9083,7 @@ exports[`resolveDescriptors should resolve descriptors for scope-loops 2`] = ` "isMutating": false, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "scopeUntil", "origin": "user", "params": [], @@ -9169,6 +9183,7 @@ exports[`resolveDescriptors should resolve descriptors for scope-loops 2`] = ` "isMutating": false, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "scopeRepeat", "origin": "user", "params": [], @@ -9267,6 +9282,7 @@ exports[`resolveDescriptors should resolve descriptors for scope-loops 2`] = ` "isMutating": false, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "scopeWhile", "origin": "user", "params": [], @@ -9367,6 +9383,7 @@ exports[`resolveDescriptors should resolve descriptors for scope-loops 2`] = ` "isMutating": false, "isOverride": false, "isVirtual": false, + "methodId": null, "name": "scopeIf", "origin": "user", "params": [], diff --git a/src/types/__snapshots__/resolveStatements.spec.ts.snap b/src/types/__snapshots__/resolveStatements.spec.ts.snap index 6e92c641e..f83f3b84b 100644 --- a/src/types/__snapshots__/resolveStatements.spec.ts.snap +++ b/src/types/__snapshots__/resolveStatements.spec.ts.snap @@ -90,6 +90,26 @@ Line 23, col 22: " `; +exports[`resolveStatements should fail statements for contract-getter-with-method-id-1 1`] = ` +":5:9: Invalid type "String" for binary operator "+" +Line 5, col 9: + 4 | contract Test { +> 5 | get(crc32("crc32") + "42") fun test(): Int { + ^~~~~~~~~~~~~~~~~~~~~ + 6 | return 0 +" +`; + +exports[`resolveStatements should fail statements for contract-getter-with-method-id-2 1`] = ` +":5:9: Getter's method id expression must be of type "Int" but it has type "Bool" +Line 5, col 9: + 4 | contract Test { +> 5 | get(true) fun test(): Int { + ^~~~ + 6 | return 0 +" +`; + exports[`resolveStatements should fail statements for contract-initof-wrong-arg 1`] = ` ":26:25: Invalid type "String" for argument "owner" Line 26, col 25: @@ -535,6 +555,39 @@ Line 13, col 12: " `; +exports[`resolveStatements should fail statements for getter-collision 1`] = ` +":8:13: Method ID collision: getter 'pko' has the same method ID 103057 as getter 'getter1' +Pick a different getter name or explicit method ID to avoid collisions +Line 8, col 13: + 7 | +> 8 | get fun pko() { + ^~~ + 9 | return; +" +`; + +exports[`resolveStatements should fail statements for getter-collision-trait 1`] = ` +":8:13: Method ID collision: getter 'pko' has the same method ID 103057 as getter 'getter1' +Pick a different getter name or explicit method ID to avoid collisions +Line 8, col 13: + 7 | +> 8 | get fun pko() { + ^~~ + 9 | return; +" +`; + +exports[`resolveStatements should fail statements for getter-collision-with-trait 1`] = ` +":4:13: Method ID collision: getter 'pko' has the same method ID 103057 as getter 'getter1' +Pick a different getter name or explicit method ID to avoid collisions +Line 4, col 13: + 3 | trait T { +> 4 | get fun pko() { + ^~~ + 5 | return; +" +`; + exports[`resolveStatements should fail statements for init-vars-analysis-if 1`] = ` ":10:5: Field "value" is not set Line 10, col 5: @@ -1580,6 +1633,53 @@ exports[`resolveStatements should resolve statements for assign-self-mutating-me ] `; +exports[`resolveStatements should resolve statements for contract-getter-with-method-id-1 1`] = ` +[ + [ + "0", + "Int", + ], +] +`; + +exports[`resolveStatements should resolve statements for contract-getter-with-method-id-2 1`] = ` +[ + [ + "16384", + "Int", + ], + [ + "MethodId", + "Int", + ], +] +`; + +exports[`resolveStatements should resolve statements for contract-getter-with-method-id-3 1`] = ` +[ + [ + "16384", + "Int", + ], + [ + "41", + "Int", + ], + [ + "16384 + 41", + "Int", + ], + [ + "self", + "ManualMethodId", + ], + [ + "self.methodId", + "Int", + ], +] +`; + exports[`resolveStatements should resolve statements for contract-receiver-bounced 1`] = ` [ [ diff --git a/src/types/resolveDescriptors.ts b/src/types/resolveDescriptors.ts index 1247ba6be..f6eaafa3e 100644 --- a/src/types/resolveDescriptors.ts +++ b/src/types/resolveDescriptors.ts @@ -52,7 +52,6 @@ import { ItemOrigin } from "../grammar/grammar"; import { getExpType, resolveExpression } from "./resolveExpression"; import { emptyContext } from "./resolveStatements"; import { isAssignable } from "./subtyping"; -import { getMethodId } from "../utils/utils"; const store = createContextStore(); const staticFunctionsStore = createContextStore(); @@ -1003,6 +1002,7 @@ export function resolveDescriptors(ctx: CompilerContext) { isOverride: !!isOverride, isInline: !!isInline, isAbstract: !!isAbstract, + methodId: null, }; } @@ -1947,25 +1947,6 @@ export function resolveDescriptors(ctx: CompilerContext) { staticConstants.set(idText(a.name), buildConstantDescription(a)); } - // Check for collisions in getter method IDs - for (const t of types.values()) { - const methodIds: Map = new Map(); - for (const f of t.functions.values()) { - if (f.isGetter) { - const methodId = getMethodId(f.name); - const existing = methodIds.get(methodId); - if (existing) { - throwCompilationError( - `Method ID collision: getter '${f.name}' has the same method ID ${methodId} as getter '${existing}'\nPick a different getter name to avoid collisions`, - f.ast.name.loc, - ); - } else { - methodIds.set(methodId, f.name); - } - } - } - } - // // Register types and functions in context // diff --git a/src/types/resolveStatements.ts b/src/types/resolveStatements.ts index 91ff414dd..81ce982e9 100644 --- a/src/types/resolveStatements.ts +++ b/src/types/resolveStatements.ts @@ -15,6 +15,7 @@ import { isAssignable } from "./subtyping"; import { idTextErr, throwCompilationError, + throwConstEvalError, throwInternalCompilerError, } from "../errors"; import { @@ -26,7 +27,10 @@ import { getAllTypes, } from "./resolveDescriptors"; import { getExpType, resolveExpression } from "./resolveExpression"; -import { printTypeRef, TypeRef } from "./types"; +import { FunctionDescription, printTypeRef, TypeRef } from "./types"; +import { evalConstantExpression } from "../constEval"; +import { ensureInt } from "../interpreter"; +import { crc16 } from "../utils/crc16"; export type StatementContext = { root: SrcInfo; @@ -856,6 +860,7 @@ export function resolveStatements(ctx: CompilerContext) { } // Process functions + const methodIds: Map = new Map(); for (const f of t.functions.values()) { if ( f.ast.kind !== "native_function_decl" && @@ -864,12 +869,29 @@ export function resolveStatements(ctx: CompilerContext) { ) { // Build statement context let sctx = emptyContext(f.ast.loc, f.name, f.returns); + if (f.self === null) { throwInternalCompilerError( "Self is null where it should not be", ); } sctx = addVariable(selfId, f.self, ctx, sctx); + + // Check for collisions in getter method IDs + if (f.isGetter) { + const methodId = getMethodId(f, ctx, sctx); + const existing = methodIds.get(methodId); + if (existing) { + throwCompilationError( + `Method ID collision: getter '${f.name}' has the same method ID ${methodId} as getter '${existing}'\nPick a different getter name or explicit method ID to avoid collisions`, + f.ast.name.loc, + ); + } else { + f.methodId = methodId; + methodIds.set(methodId, f.name); + } + } + for (const a of f.params) { sctx = addVariable(a.name, a.type, ctx, sctx); } @@ -881,3 +903,63 @@ export function resolveStatements(ctx: CompilerContext) { return ctx; } + +function getMethodId( + funcDescr: FunctionDescription, + ctx: CompilerContext, + sctx: StatementContext, +): number { + const optMethodId = funcDescr.ast.attributes.find( + (attr) => attr.type === "get", + )?.methodId; + + if (optMethodId) { + ctx = resolveExpression(optMethodId, sctx, ctx); + const ty = getExpType(ctx, optMethodId); + if (!(ty.kind === "ref" && ty.name === "Int")) { + throwCompilationError( + `Getter's method id expression must be of type "Int" but it has type "${printTypeRef(ty)}"`, + optMethodId.loc, + ); + } + + const methodId = ensureInt( + evalConstantExpression(optMethodId, ctx), + optMethodId.loc, + ); + // method ids are 19-bit signed integers + if (methodId < -(2n ** 18n) || methodId >= 2n ** 18n) { + throwConstEvalError( + "method ids must fit 19-bit signed integer range", + true, + optMethodId!.loc, + ); + } + // method ids -4, -3, -2, -1, 0 ... 2^14 - 1 (inclusive) are kind of reserved by TVM + // for the upper bound see F12_n (CALL) TVM instruction + // and many small ids will be taken by internal procedures + // + // also, some ids are taken by the getters generated by Tact: + // supported_interfaces -> 113617 + // lazy_deployment_completed -> 115390 + // get_abi_ipfs -> 121275 + if (-4n <= methodId && methodId < 2n ** 14n) { + throwConstEvalError( + "method ids cannot overlap with the TVM reserved ids: -4, -3, -2, -1, 0 ... 2^14 - 1", + true, + optMethodId!.loc, + ); + } + const tactGeneratedGetterMethodIds = [113617n, 115390n, 121275n]; + if (tactGeneratedGetterMethodIds.includes(methodId)) { + throwConstEvalError( + `method ids cannot overlap with Tact reserved method ids: ${tactGeneratedGetterMethodIds.map((n) => n.toString()).join(", ")}`, + true, + optMethodId!.loc, + ); + } + return Number(methodId); + } else { + return (crc16(funcDescr.name) & 0xffff) | 0x10000; + } +} diff --git a/src/types/stmts-failed/contract-getter-with-method-id-1.tact b/src/types/stmts-failed/contract-getter-with-method-id-1.tact new file mode 100644 index 000000000..6a91191b2 --- /dev/null +++ b/src/types/stmts-failed/contract-getter-with-method-id-1.tact @@ -0,0 +1,8 @@ +primitive Int; +trait BaseTrait { } + +contract Test { + get(crc32("crc32") + "42") fun test(): Int { + return 0 + } +} diff --git a/src/types/stmts-failed/contract-getter-with-method-id-2.tact b/src/types/stmts-failed/contract-getter-with-method-id-2.tact new file mode 100644 index 000000000..0d9de630c --- /dev/null +++ b/src/types/stmts-failed/contract-getter-with-method-id-2.tact @@ -0,0 +1,8 @@ +primitive Int; +trait BaseTrait { } + +contract Test { + get(true) fun test(): Int { + return 0 + } +} diff --git a/src/types/test-failed/getter-collision-trait.tact b/src/types/stmts-failed/getter-collision-trait.tact similarity index 100% rename from src/types/test-failed/getter-collision-trait.tact rename to src/types/stmts-failed/getter-collision-trait.tact diff --git a/src/types/test-failed/getter-collision-with-trait.tact b/src/types/stmts-failed/getter-collision-with-trait.tact similarity index 100% rename from src/types/test-failed/getter-collision-with-trait.tact rename to src/types/stmts-failed/getter-collision-with-trait.tact diff --git a/src/types/test-failed/getter-collision.tact b/src/types/stmts-failed/getter-collision.tact similarity index 100% rename from src/types/test-failed/getter-collision.tact rename to src/types/stmts-failed/getter-collision.tact diff --git a/src/types/stmts/contract-getter-with-method-id-1.tact b/src/types/stmts/contract-getter-with-method-id-1.tact new file mode 100644 index 000000000..d66b5bdc7 --- /dev/null +++ b/src/types/stmts/contract-getter-with-method-id-1.tact @@ -0,0 +1,8 @@ +primitive Int; +trait BaseTrait { } + +contract Test { + get((crc32("crc32") + 42) & 0x3FFFF | 0x4000) fun test(): Int { + return 0 + } +} diff --git a/src/types/stmts/contract-getter-with-method-id-2.tact b/src/types/stmts/contract-getter-with-method-id-2.tact new file mode 100644 index 000000000..af00be486 --- /dev/null +++ b/src/types/stmts/contract-getter-with-method-id-2.tact @@ -0,0 +1,10 @@ +primitive Int; +trait BaseTrait { } + +const MethodId: Int = 16384; + +contract Test { + get(MethodId) fun test(): Int { + return MethodId + } +} diff --git a/src/types/stmts/contract-getter-with-method-id-3.tact b/src/types/stmts/contract-getter-with-method-id-3.tact new file mode 100644 index 000000000..9b3c54d1a --- /dev/null +++ b/src/types/stmts/contract-getter-with-method-id-3.tact @@ -0,0 +1,9 @@ +primitive Int; +trait BaseTrait { } + +contract ManualMethodId { + const methodId: Int = 16384 + 41; + get(self.methodId + 1) fun methodId(): Int { + return self.methodId; + } +} diff --git a/src/types/types.ts b/src/types/types.ts index f82fbc70e..8b34e74cf 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -144,6 +144,7 @@ export type FunctionDescription = { name: string; origin: ItemOrigin; isGetter: boolean; + methodId: number | null; isMutating: boolean; isOverride: boolean; isVirtual: boolean; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index d88294dc8..8d3a45ec9 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,4 +1,3 @@ -import { crc16 } from "./crc16"; import { throwInternalCompilerError } from "../errors"; export function topologicalSort(src: T[], references: (src: T) => T[]) { @@ -24,7 +23,3 @@ export function topologicalSort(src: T[], references: (src: T) => T[]) { } return result; } - -export function getMethodId(name: string) { - return (crc16(name) & 0xffff) | 0x10000; -} From bb8032783e1833d13e1ebf5e14ab651517343302 Mon Sep 17 00:00:00 2001 From: Anton Trunov Date: Mon, 7 Oct 2024 17:32:41 +0400 Subject: [PATCH 2/6] docs: typo --- docs/src/content/docs/book/functions.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/content/docs/book/functions.mdx b/docs/src/content/docs/book/functions.mdx index c3eb46eec..18ef76943 100644 --- a/docs/src/content/docs/book/functions.mdx +++ b/docs/src/content/docs/book/functions.mdx @@ -139,7 +139,7 @@ contract Treasure { ``` As other functions in TVM contracts, getters have their *unique* associated function selectors which are some integers ids (called *method IDs*). -Some of those integers are reserved for internal puproses, e.g. -4, -3, -2, -1, 0 are reserved IDs and +Some of those integers are reserved for internal purposes, e.g. -4, -3, -2, -1, 0 are reserved IDs and regular functions (internal to a contract and not callable from outside) are usually numbered by subsequent (small) integers starting from 1. By default, getters have associated method IDs that are derived from their names using the [CRC16](https://en.wikipedia.org/wiki/Cyclic_redundancy_check) algorithm as follows: `crc16() & 0xffff) | 0x10000`. From 8b9dd690b42f6dfdd8a5b5a50450dfc0f0e0ec96 Mon Sep 17 00:00:00 2001 From: Anton Trunov Date: Mon, 7 Oct 2024 17:39:42 +0400 Subject: [PATCH 3/6] doc: fixes --- docs/src/content/docs/book/functions.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/content/docs/book/functions.mdx b/docs/src/content/docs/book/functions.mdx index 18ef76943..cb4a1db76 100644 --- a/docs/src/content/docs/book/functions.mdx +++ b/docs/src/content/docs/book/functions.mdx @@ -164,6 +164,6 @@ contract ManualMethodId { Note that you *cannot* use method IDs that are reserved by TVM and you cannot use some initial positive integers because those will be used as function selectors by the compiler. -User-specified method IDs are 19-bit signed integers, so you can use integers from $2^{-18}$ to $-5$ and from $2^14$ to $2^18 - 1$. +User-specified method IDs are 19-bit signed integers, so you can use integers from $-2^{-18}$ to $-5$ and from $2^{14}$ to $2^{18} - 1$. Also, a few method IDs are reserved for the usage by the getters the Tact compiler can insert during compilation, those are 113617, 115390, 121275. From 8a278866761b5d0a09c0d3f4ec1c953b69e1a99e Mon Sep 17 00:00:00 2001 From: Anton Trunov Date: Mon, 7 Oct 2024 17:59:42 +0400 Subject: [PATCH 4/6] fix docs --- docs/src/content/docs/book/functions.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/content/docs/book/functions.mdx b/docs/src/content/docs/book/functions.mdx index cb4a1db76..4f702b099 100644 --- a/docs/src/content/docs/book/functions.mdx +++ b/docs/src/content/docs/book/functions.mdx @@ -164,6 +164,6 @@ contract ManualMethodId { Note that you *cannot* use method IDs that are reserved by TVM and you cannot use some initial positive integers because those will be used as function selectors by the compiler. -User-specified method IDs are 19-bit signed integers, so you can use integers from $-2^{-18}$ to $-5$ and from $2^{14}$ to $2^{18} - 1$. +User-specified method IDs are 19-bit signed integers, so you can use integers from $-2^{18}$ to $-5$ and from $2^{14}$ to $2^{18} - 1$. Also, a few method IDs are reserved for the usage by the getters the Tact compiler can insert during compilation, those are 113617, 115390, 121275. From d9b2ba0f8530e4ab84aee34d8325af4bd6a7b2a1 Mon Sep 17 00:00:00 2001 From: Anton Trunov Date: Mon, 7 Oct 2024 18:39:09 +0400 Subject: [PATCH 5/6] add "available since tact 1.6" --- docs/src/content/docs/book/functions.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/src/content/docs/book/functions.mdx b/docs/src/content/docs/book/functions.mdx index 4f702b099..f193eb168 100644 --- a/docs/src/content/docs/book/functions.mdx +++ b/docs/src/content/docs/book/functions.mdx @@ -138,6 +138,10 @@ contract Treasure { } ``` +### Explicit resolution of method ID collisions + + + As other functions in TVM contracts, getters have their *unique* associated function selectors which are some integers ids (called *method IDs*). Some of those integers are reserved for internal purposes, e.g. -4, -3, -2, -1, 0 are reserved IDs and regular functions (internal to a contract and not callable from outside) are usually numbered by subsequent (small) integers starting from 1. From 91768efd1b3cb654c1a69c3a693def4106117f14 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:02:07 +0200 Subject: [PATCH 6/6] chore: import Badge --- docs/src/content/docs/book/functions.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/src/content/docs/book/functions.mdx b/docs/src/content/docs/book/functions.mdx index f193eb168..1a29b2be9 100644 --- a/docs/src/content/docs/book/functions.mdx +++ b/docs/src/content/docs/book/functions.mdx @@ -3,6 +3,8 @@ title: Functions description: "Global, asm, native functions, as well as receivers, getters and storage functions, plus the many attributes that allow for great flexibility and expressivity of Tact language" --- +import { Badge } from '@astrojs/starlight/components'; + Functions in Tact could be defined in different ways: * Global static function @@ -140,7 +142,7 @@ contract Treasure { ### Explicit resolution of method ID collisions - +

As other functions in TVM contracts, getters have their *unique* associated function selectors which are some integers ids (called *method IDs*). Some of those integers are reserved for internal purposes, e.g. -4, -3, -2, -1, 0 are reserved IDs and