Skip to content

Commit

Permalink
feat: specify method ID for getters (#922)
Browse files Browse the repository at this point in the history
  • Loading branch information
anton-trunov authored Oct 7, 2024
1 parent ffe202a commit 247b255
Show file tree
Hide file tree
Showing 37 changed files with 609 additions and 94 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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)
- Docs: Google Analytics tags per every page: PR [#921](https://github.com/tact-lang/tact/pull/921)
- Ability to specify a compile-time method ID expression for getters: PR [#922](https://github.com/tact-lang/tact/pull/922)

### Changed

Expand Down
37 changes: 37 additions & 0 deletions docs/src/content/docs/book/functions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -128,6 +130,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 {
Expand All @@ -136,3 +139,37 @@ contract Treasure {
}
}
```

### Explicit resolution of method ID collisions

<Badge text="Available since Tact 1.6" variant="tip" size="medium"/><p/>

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.
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(<function_name>) & 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.
6 changes: 1 addition & 5 deletions knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
16 changes: 13 additions & 3 deletions src/bindings/writeTypescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;`);
Expand Down
1 change: 1 addition & 0 deletions src/generator/createABI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
35 changes: 17 additions & 18 deletions src/generator/writers/writeFunction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -681,54 +680,54 @@ 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;
}
}

// Return result
ctx.append(`return res;`);
wCtx.append(`return res;`);
});
ctx.append(`}`);
ctx.append();
wCtx.append(`}`);
wCtx.append();
}
138 changes: 138 additions & 0 deletions src/grammar/__snapshots__/grammar.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,17 @@ Line 1, col 20:
"
`;
exports[`grammar should fail contract-getter-parens-no-method-id 1`] = `
"<unknown>: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: <unknown>:2:10: Empty parameter list should not have a dangling comma.
Line 2, col 10:
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/grammar/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
3 changes: 2 additions & 1 deletion src/grammar/grammar.ohm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions src/grammar/grammar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,8 +583,12 @@ semantics.addOperation<string>("astOfAsmInstruction", {
});

semantics.addOperation<AstFunctionAttribute>("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) };
Expand Down
Loading

0 comments on commit 247b255

Please sign in to comment.