Skip to content

Commit

Permalink
feat: comptime expressions for message opcodes (#1188)
Browse files Browse the repository at this point in the history
  • Loading branch information
anton-trunov authored Dec 16, 2024
1 parent 903a911 commit 5eb0fb8
Show file tree
Hide file tree
Showing 25 changed files with 535 additions and 39 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The `SendDefaultMode` send mode constant to the standard library: PR [#1010](https://github.com/tact-lang/tact/pull/1010)
- The `replace` and `replaceGet` methods for the `Map` type: PR [#941](https://github.com/tact-lang/tact/pull/941)
- Utility for logging errors in code that was supposed to be unreachable: PR [#991](https://github.com/tact-lang/tact/pull/991)
- Ability to specify a compile-time message opcode expression: PR [#1188](https://github.com/tact-lang/tact/pull/1188)

### Changed

Expand Down
10 changes: 10 additions & 0 deletions docs/src/content/docs/book/structs-and-messages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ message Add {
}
```

### Message opcodes

Messages are almost the same thing as [Structs](#structs) with the only difference that Messages have a 32-bit integer header in their serialization containing their unique numeric id, commonly referred to as an _opcode_ (operation code). This allows Messages to be used with [receivers](/book/receive) since the contract can tell different types of messages apart based on this id.

Tact automatically generates those unique ids (opcodes) for every received Message, but this can be manually overwritten:
Expand All @@ -114,6 +116,14 @@ message(0x7362d09c) TokenNotification {

This is useful for cases where you want to handle certain opcodes of a given smart contract, such as [Jetton standard](https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md). The short-list of opcodes this contract is able to process is [given here in FunC](https://github.com/ton-blockchain/token-contract/blob/main/ft/op-codes.fc). They serve as an interface to the smart contract.

A message opcode can be any compile-time (constant) expression which evaluates to a strictly positive integer that fits into 32-bits, so the following is also a valid message declaration:

```tact
message((crc32("Tact") + 42) & 0xFFFF_FFFF) MsgWithExprOpcode {
field: Int as uint4;
}
```

:::note

For more in-depth information on this see:\
Expand Down
10 changes: 0 additions & 10 deletions src/grammar/__snapshots__/grammar.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -734,16 +734,6 @@ Line 2, col 13:
"
`;

exports[`grammar should fail message-negative-opcode 1`] = `
"<unknown>:1:9: Parse error: expected "0", "1".."9", "0O", "0o", "0B", "0b", "0X", or "0x"

Line 1, col 9:
> 1 | message(-1) Foo { }
^
2 |
"
`;

exports[`grammar should fail struct-double-semicolon 1`] = `
"<unknown>:2:19: Parse error: expected "}"

Expand Down
2 changes: 1 addition & 1 deletion src/grammar/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export type AstStructDecl = {
export type AstMessageDecl = {
kind: "message_decl";
name: AstId;
opcode: AstNumber | null;
opcode: AstExpression | null;
fields: AstFieldDecl[];
id: number;
loc: SrcInfo;
Expand Down
2 changes: 1 addition & 1 deletion src/grammar/grammar.ohm
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Tact {
ConstantDeclaration = ConstantAttribute* const id ":" Type (";" | &"}")

StructDecl = "struct" typeId "{" StructFields "}" --regular
| "message" ("(" integerLiteral ")")? typeId "{" StructFields "}" --message
| "message" ("(" Expression ")")? typeId "{" StructFields "}" --message

StructField = FieldDecl
StructFields = ListOf<StructField, ";"> ";"?
Expand Down
4 changes: 2 additions & 2 deletions src/grammar/grammar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,8 @@ semantics.addOperation<AstNode>("astOfModuleItem", {
kind: "message_decl",
name: typeId.astOfType(),
fields: fields.astsOfList(),
opcode: unwrapOptNode(optIntMsgId, (number) =>
number.astOfExpression(),
opcode: unwrapOptNode(optIntMsgId, (msgId) =>
msgId.astOfExpression(),
),
loc: createRef(this),
});
Expand Down
2 changes: 1 addition & 1 deletion src/grammar/hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ export class AstHasher {

private hashMessageDecl(node: AstMessageDecl): string {
const fieldsHash = this.hashFields(node.fields);
return `message|${fieldsHash}|${node.opcode?.value}`;
return `message|${fieldsHash}|${node.opcode ? this.hash(node.opcode) : "null"}`;
}

private hashFunctionDef(node: AstFunctionDef): string {
Expand Down
3 changes: 3 additions & 0 deletions src/grammar/iterators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ export function traverse(node: AstNode, callback: (node: AstNode) => void) {
case "struct_decl":
case "message_decl":
traverse(node.name, callback);
if (node.kind === "message_decl" && node.opcode !== null) {
traverse(node.opcode, callback);
}
node.fields.forEach((e) => {
traverse(e, callback);
});
Expand Down
7 changes: 7 additions & 0 deletions src/grammar/rename.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,14 @@ export class AstRenamer {
private renameModuleItemContents(item: AstModuleItem): AstModuleItem {
switch (item.kind) {
case "struct_decl":
return item;
case "message_decl":
if (item.opcode !== null) {
return {
...item,
opcode: this.renameExpression(item.opcode),
};
}
return item;
case "function_def":
return this.renameFunctionContents(item as AstFunctionDef);
Expand Down
2 changes: 1 addition & 1 deletion src/prettyPrinter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ export const ppAstMessage: Printer<A.AstMessageDecl> =
({ name, opcode, fields }) =>
(c) => {
const prefixCode =
opcode !== null ? `(${A.astNumToString(opcode)})` : "";
opcode !== null ? `(${ppAstExpression(opcode)})` : "";

return c.concat([
c.row(`message${prefixCode} ${ppAstId(name)} `),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
message(1) Msg1 {}
message(1) Msg2 {}
message(1 + 0) Msg2 {}

contract Test {
receive(msg: Msg1) { }
Expand Down
6 changes: 6 additions & 0 deletions src/test/contracts/case-message-opcode.tact
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,11 @@ message MyMessageAuto {
value: Int;
}

const DEADBEEF: Int = 0xdeadbeef;

message(DEADBEEF + 1) MyMessageWithExprOpcode {
a: Int;
}

contract TestContract {
}
8 changes: 7 additions & 1 deletion src/test/contracts/renamer-expected/case-message-opcode.tact
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,11 @@ message message_decl_4 {
value: Int;
}

contract contract_5 {
message(constant_def_5 + 1) message_decl_6 {
a: Int;
}

const constant_def_5: Int = 0xdeadbeef;

contract contract_7 {
}
2 changes: 1 addition & 1 deletion src/test/e2e-emulated/contracts/structs.tact
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ struct IntFields {
i257: Int as int257;
}

message(0xea01f46a) UintFields {
message(0xea01f469 + 1) UintFields {
u1: Int as uint1;
u2: Int as uint2;
u3: Int as uint3;
Expand Down
Loading

0 comments on commit 5eb0fb8

Please sign in to comment.