diff --git a/docs/src/content/docs/book/structs-and-messages.mdx b/docs/src/content/docs/book/structs-and-messages.mdx index 130af4ffa..0723104e4 100644 --- a/docs/src/content/docs/book/structs-and-messages.mdx +++ b/docs/src/content/docs/book/structs-and-messages.mdx @@ -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: @@ -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:\ diff --git a/src/grammar/__snapshots__/grammar.spec.ts.snap b/src/grammar/__snapshots__/grammar.spec.ts.snap index 060ae17e9..fe2cecf11 100644 --- a/src/grammar/__snapshots__/grammar.spec.ts.snap +++ b/src/grammar/__snapshots__/grammar.spec.ts.snap @@ -734,16 +734,6 @@ Line 2, col 13: " `; -exports[`grammar should fail message-negative-opcode 1`] = ` -":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`] = ` ":2:19: Parse error: expected "}" diff --git a/src/grammar/ast.ts b/src/grammar/ast.ts index 536d31346..7d95641e8 100644 --- a/src/grammar/ast.ts +++ b/src/grammar/ast.ts @@ -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; diff --git a/src/grammar/grammar.ohm b/src/grammar/grammar.ohm index 2f17501e4..e21fc697a 100644 --- a/src/grammar/grammar.ohm +++ b/src/grammar/grammar.ohm @@ -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 ";"? diff --git a/src/grammar/hash.ts b/src/grammar/hash.ts index 3a1582f13..a4ac5c616 100644 --- a/src/grammar/hash.ts +++ b/src/grammar/hash.ts @@ -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 { diff --git a/src/grammar/iterators.ts b/src/grammar/iterators.ts index bc473a918..97454ccd5 100644 --- a/src/grammar/iterators.ts +++ b/src/grammar/iterators.ts @@ -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); }); diff --git a/src/grammar/rename.ts b/src/grammar/rename.ts index 3ce5be75c..c77210210 100644 --- a/src/grammar/rename.ts +++ b/src/grammar/rename.ts @@ -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); diff --git a/src/prettyPrinter.ts b/src/prettyPrinter.ts index a825ec5f0..04f9a441f 100644 --- a/src/prettyPrinter.ts +++ b/src/prettyPrinter.ts @@ -537,7 +537,7 @@ export const ppAstMessage: Printer = ({ name, opcode, fields }) => (c) => { const prefixCode = - opcode !== null ? `(${A.astNumToString(opcode)})` : ""; + opcode !== null ? `(${ppAstExpression(opcode)})` : ""; return c.concat([ c.row(`message${prefixCode} ${ppAstId(name)} `), diff --git a/src/test/compilation-failed/contracts/contract-duplicate-receiver-opcode.tact b/src/test/compilation-failed/contracts/contract-duplicate-receiver-opcode.tact index b4ce60d36..3b4936504 100644 --- a/src/test/compilation-failed/contracts/contract-duplicate-receiver-opcode.tact +++ b/src/test/compilation-failed/contracts/contract-duplicate-receiver-opcode.tact @@ -1,5 +1,5 @@ message(1) Msg1 {} -message(1) Msg2 {} +message(1 + 0) Msg2 {} contract Test { receive(msg: Msg1) { } diff --git a/src/test/contracts/case-message-opcode.tact b/src/test/contracts/case-message-opcode.tact index 1afce71c0..ea696de45 100644 --- a/src/test/contracts/case-message-opcode.tact +++ b/src/test/contracts/case-message-opcode.tact @@ -24,5 +24,11 @@ message MyMessageAuto { value: Int; } +const DEADBEEF: Int = 0xdeadbeef; + +message(DEADBEEF + 1) MyMessageWithExprOpcode { + a: Int; +} + contract TestContract { } diff --git a/src/test/contracts/renamer-expected/case-message-opcode.tact b/src/test/contracts/renamer-expected/case-message-opcode.tact index c2d4d93f0..683a72ceb 100644 --- a/src/test/contracts/renamer-expected/case-message-opcode.tact +++ b/src/test/contracts/renamer-expected/case-message-opcode.tact @@ -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 { } diff --git a/src/test/e2e-emulated/contracts/structs.tact b/src/test/e2e-emulated/contracts/structs.tact index cea1d5f8e..af10237e5 100644 --- a/src/test/e2e-emulated/contracts/structs.tact +++ b/src/test/e2e-emulated/contracts/structs.tact @@ -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; diff --git a/src/types/__snapshots__/resolveDescriptors.spec.ts.snap b/src/types/__snapshots__/resolveDescriptors.spec.ts.snap index e4173d1ff..dc4bb74f7 100644 --- a/src/types/__snapshots__/resolveDescriptors.spec.ts.snap +++ b/src/types/__snapshots__/resolveDescriptors.spec.ts.snap @@ -338,8 +338,36 @@ Line 8, col 1: " `; -exports[`resolveDescriptors should fail descriptors for message-opcode-too-large 1`] = ` -":1:9: Opcode of message "Foo" is too large: it must fit into 32 bits +exports[`resolveDescriptors should fail descriptors for message-negative-opcode-1 1`] = ` +":1:9: Opcode of message "Foo" is negative ('-1') which is not allowed +Line 1, col 9: +> 1 | message(-1) Foo { } + ^~ + 2 | +" +`; + +exports[`resolveDescriptors should fail descriptors for message-negative-opcode-2 1`] = ` +":1:9: Opcode of message "Foo" is negative ('-1') which is not allowed +Line 1, col 9: +> 1 | message(42 - 43) Foo { } + ^~~~~~~ + 2 | +" +`; + +exports[`resolveDescriptors should fail descriptors for message-opcode-div-by-zero 1`] = ` +":3:14: Cannot evaluate expression to a constant: divisor expression must be non-zero +Line 3, col 14: + 2 | +> 3 | message(42 / 0) DivByZeroOpcode { } + ^ + 4 | +" +`; + +exports[`resolveDescriptors should fail descriptors for message-opcode-too-large-1 1`] = ` +":1:9: Opcode of message "Foo" is too large ('137438953471'): it must fit into 32 bits Line 1, col 9: > 1 | message(0x1_FFFFF_FFFF) Foo { } ^~~~~~~~~~~~~~ @@ -347,8 +375,17 @@ Line 1, col 9: " `; -exports[`resolveDescriptors should fail descriptors for message-opcode-zero 1`] = ` -":4:9: Zero opcodes are reserved for text comments and cannot be used for message structs +exports[`resolveDescriptors should fail descriptors for message-opcode-too-large-2 1`] = ` +":1:9: Opcode of message "Foo" is too large ('8589737985'): it must fit into 32 bits +Line 1, col 9: +> 1 | message(0xFFFF * 0x1FFFF) Foo { } + ^~~~~~~~~~~~~~~~ + 2 | +" +`; + +exports[`resolveDescriptors should fail descriptors for message-opcode-zero-1 1`] = ` +":4:9: Opcode of message "ZeroOpcode" is zero: those are reserved for text comments and cannot be used for message structs Line 4, col 9: 3 | // zero opcodes are reserved for string comments > 4 | message(0) ZeroOpcode { } @@ -357,6 +394,16 @@ Line 4, col 9: " `; +exports[`resolveDescriptors should fail descriptors for message-opcode-zero-2 1`] = ` +":4:9: Opcode of message "ZeroOpcode" is zero: those are reserved for text comments and cannot be used for message structs +Line 4, col 9: + 3 | // zero opcodes are reserved for string comments +> 4 | message(pow(2, 4) - 16) ZeroOpcode { } + ^~~~~~~~~~~~~~ + 5 | +" +`; + exports[`resolveDescriptors should fail descriptors for method-first-param-not-self1 1`] = ` ":8:17: Extend function must have first parameter named "self" Line 8, col 17: @@ -9032,6 +9079,372 @@ exports[`resolveDescriptors should resolve descriptors for map-value-as-coins 1` exports[`resolveDescriptors should resolve descriptors for map-value-as-coins 2`] = `[]`; +exports[`resolveDescriptors should resolve descriptors for message-opcode-expr 1`] = ` +[ + { + "ast": { + "id": 2, + "kind": "primitive_type_decl", + "loc": primitive Int;, + "name": { + "id": 1, + "kind": "type_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": { + "attributes": [], + "declarations": [], + "id": 4, + "kind": "trait", + "loc": trait BaseTrait { }, + "name": { + "id": 3, + "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": { + "fields": [ + { + "as": { + "id": 8, + "kind": "id", + "loc": uint4, + "text": "uint4", + }, + "id": 9, + "initializer": null, + "kind": "field_decl", + "loc": field: Int as uint4, + "name": { + "id": 6, + "kind": "id", + "loc": field, + "text": "field", + }, + "type": { + "id": 7, + "kind": "type_id", + "loc": Int, + "text": "Int", + }, + }, + ], + "id": 17, + "kind": "message_decl", + "loc": message((crc32("Tact") + 42) & 0xFFFF_FFFF) MsgWithExprOpcode { + field: Int as uint4; +}, + "name": { + "id": 5, + "kind": "type_id", + "loc": MsgWithExprOpcode, + "text": "MsgWithExprOpcode", + }, + "opcode": { + "id": 16, + "kind": "op_binary", + "left": { + "id": 14, + "kind": "op_binary", + "left": { + "args": [ + { + "id": 11, + "kind": "string", + "loc": "Tact", + "value": "Tact", + }, + ], + "function": { + "id": 10, + "kind": "id", + "loc": crc32, + "text": "crc32", + }, + "id": 12, + "kind": "static_call", + "loc": crc32("Tact"), + }, + "loc": crc32("Tact") + 42, + "op": "+", + "right": { + "base": 10, + "id": 13, + "kind": "number", + "loc": 42, + "value": 42n, + }, + }, + "loc": (crc32("Tact") + 42) & 0xFFFF_FFFF, + "op": "&", + "right": { + "base": 16, + "id": 15, + "kind": "number", + "loc": 0xFFFF_FFFF, + "value": 4294967295n, + }, + }, + }, + "constants": [], + "dependsOn": [], + "fields": [ + { + "abi": { + "name": "field", + "type": { + "format": 4, + "kind": "simple", + "optional": false, + "type": "uint", + }, + }, + "as": "uint4", + "ast": { + "as": { + "id": 8, + "kind": "id", + "loc": uint4, + "text": "uint4", + }, + "id": 9, + "initializer": null, + "kind": "field_decl", + "loc": field: Int as uint4, + "name": { + "id": 6, + "kind": "id", + "loc": field, + "text": "field", + }, + "type": { + "id": 7, + "kind": "type_id", + "loc": Int, + "text": "Int", + }, + }, + "default": undefined, + "index": 0, + "loc": field: Int as uint4, + "name": "field", + "type": { + "kind": "ref", + "name": "Int", + "optional": false, + }, + }, + ], + "functions": Map {}, + "header": { + "base": 10, + "id": 0, + "kind": "number", + "loc": , + "value": 898001897n, + }, + "init": null, + "interfaces": [], + "kind": "struct", + "name": "MsgWithExprOpcode", + "origin": "user", + "partialFieldCount": 1, + "receivers": [], + "signature": "MsgWithExprOpcode{field:uint4}", + "tlb": "msg_with_expr_opcode#35866be9 field:uint4 = MsgWithExprOpcode", + "traits": [], + "uid": 23017, + }, + { + "ast": { + "attributes": [], + "declarations": [ + { + "id": 22, + "kind": "receiver", + "loc": receive(msg: MsgWithExprOpcode) { }, + "selector": { + "kind": "internal-simple", + "param": { + "id": 21, + "kind": "typed_parameter", + "loc": msg: MsgWithExprOpcode, + "name": { + "id": 19, + "kind": "id", + "loc": msg, + "text": "msg", + }, + "type": { + "id": 20, + "kind": "type_id", + "loc": MsgWithExprOpcode, + "text": "MsgWithExprOpcode", + }, + }, + }, + "statements": [], + }, + ], + "id": 23, + "kind": "contract", + "loc": contract Foo { + receive(msg: MsgWithExprOpcode) { } +}, + "name": { + "id": 18, + "kind": "id", + "loc": Foo, + "text": "Foo", + }, + "traits": [], + }, + "constants": [], + "dependsOn": [], + "fields": [], + "functions": Map {}, + "header": null, + "init": { + "ast": { + "id": 25, + "kind": "contract_init", + "loc": contract Foo { + receive(msg: MsgWithExprOpcode) { } +}, + "params": [], + "statements": [], + }, + "params": [], + }, + "interfaces": [], + "kind": "contract", + "name": "Foo", + "origin": "user", + "partialFieldCount": 0, + "receivers": [ + { + "ast": { + "id": 22, + "kind": "receiver", + "loc": receive(msg: MsgWithExprOpcode) { }, + "selector": { + "kind": "internal-simple", + "param": { + "id": 21, + "kind": "typed_parameter", + "loc": msg: MsgWithExprOpcode, + "name": { + "id": 19, + "kind": "id", + "loc": msg, + "text": "msg", + }, + "type": { + "id": 20, + "kind": "type_id", + "loc": MsgWithExprOpcode, + "text": "MsgWithExprOpcode", + }, + }, + }, + "statements": [], + }, + "selector": { + "kind": "internal-binary", + "name": { + "id": 19, + "kind": "id", + "loc": msg, + "text": "msg", + }, + "type": "MsgWithExprOpcode", + }, + }, + ], + "signature": null, + "tlb": null, + "traits": [ + { + "ast": { + "attributes": [], + "declarations": [], + "id": 4, + "kind": "trait", + "loc": trait BaseTrait { }, + "name": { + "id": 3, + "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": 10576, + }, +] +`; + +exports[`resolveDescriptors should resolve descriptors for message-opcode-expr 2`] = `[]`; + exports[`resolveDescriptors should resolve descriptors for scope-loops 1`] = `[]`; exports[`resolveDescriptors should resolve descriptors for scope-loops 2`] = ` diff --git a/src/types/resolveDescriptors.ts b/src/types/resolveDescriptors.ts index 557c83fb5..a334b5cd5 100644 --- a/src/types/resolveDescriptors.ts +++ b/src/types/resolveDescriptors.ts @@ -521,20 +521,6 @@ export function resolveDescriptors(ctx: CompilerContext) { a.loc, ); } - if (a.kind === "message_decl" && a.opcode) { - if (a.opcode.value === 0n) { - throwCompilationError( - `Zero opcodes are reserved for text comments and cannot be used for message structs`, - a.opcode.loc, - ); - } - if (a.opcode.value > 0xffff_ffff) { - throwCompilationError( - `Opcode of message ${idTextErr(a.name)} is too large: it must fit into 32 bits`, - a.opcode.loc, - ); - } - } } // Trait diff --git a/src/types/resolveSignatures.ts b/src/types/resolveSignatures.ts index 284fa2a7e..aedd0f9b0 100644 --- a/src/types/resolveSignatures.ts +++ b/src/types/resolveSignatures.ts @@ -2,7 +2,11 @@ import * as changeCase from "change-case"; import { ABIField, beginCell } from "@ton/core"; import { CompilerContext } from "../context"; import { idToHex } from "../utils/idToHex"; -import { idTextErr, throwInternalCompilerError } from "../errors"; +import { + idTextErr, + throwConstEvalError, + throwInternalCompilerError, +} from "../errors"; import { getType, getAllTypes } from "./resolveDescriptors"; import { BinaryReceiverSelector, @@ -14,6 +18,8 @@ import { AstNumber, AstReceiver } from "../grammar/ast"; import { commentPseudoOpcode } from "../generator/writers/writeRouter"; import { sha256_sync } from "@ton/crypto"; import { dummySrcInfo } from "../grammar/grammar"; +import { ensureInt } from "../interpreter"; +import { evalConstantExpression } from "../constEval"; export function resolveSignatures(ctx: CompilerContext) { const signatures: Map< @@ -187,7 +193,46 @@ export function resolveSignatures(ctx: CompilerContext) { let id: AstNumber | null = null; if (t.ast.kind === "message_decl") { if (t.ast.opcode !== null) { - id = t.ast.opcode; + // Currently, message opcode expressions do not get typechecked, so + // ``` + // message(true ? 42 : false) TypeError { } + // ``` + // WILL NOT result in error + const opCode = ensureInt( + evalConstantExpression(t.ast.opcode, ctx), + t.ast.opcode.loc, + ); + if (opCode === 0n) { + throwConstEvalError( + `Opcode of message ${idTextErr(t.ast.name)} is zero: those are reserved for text comments and cannot be used for message structs`, + true, + t.ast.opcode.loc, + ); + } + if (opCode < 0) { + throwConstEvalError( + `Opcode of message ${idTextErr(t.ast.name)} is negative ('${opCode}') which is not allowed`, + true, + t.ast.opcode.loc, + ); + } + if (opCode > 0xffff_ffff) { + throwConstEvalError( + `Opcode of message ${idTextErr(t.ast.name)} is too large ('${opCode}'): it must fit into 32 bits`, + true, + t.ast.opcode.loc, + ); + } + id = + t.ast.opcode.kind === "number" + ? t.ast.opcode + : { + kind: "number", + base: 10, + value: opCode, + id: 0, + loc: dummySrcInfo, + }; } else { id = newMessageOpcode(signature); if (id.value === 0n) { diff --git a/src/grammar/test-failed/message-negative-opcode.tact b/src/types/test-failed/message-negative-opcode-1.tact similarity index 100% rename from src/grammar/test-failed/message-negative-opcode.tact rename to src/types/test-failed/message-negative-opcode-1.tact diff --git a/src/types/test-failed/message-negative-opcode-2.tact b/src/types/test-failed/message-negative-opcode-2.tact new file mode 100644 index 000000000..2ecbe3b8c --- /dev/null +++ b/src/types/test-failed/message-negative-opcode-2.tact @@ -0,0 +1 @@ +message(42 - 43) Foo { } diff --git a/src/types/test-failed/message-opcode-div-by-zero.tact b/src/types/test-failed/message-opcode-div-by-zero.tact new file mode 100644 index 000000000..b7256ff7d --- /dev/null +++ b/src/types/test-failed/message-opcode-div-by-zero.tact @@ -0,0 +1,8 @@ +trait BaseTrait { } + +message(42 / 0) DivByZeroOpcode { } + +contract Test { + receive(msg: DivByZeroOpcode) { } +} + diff --git a/src/types/test-failed/message-opcode-too-large.tact b/src/types/test-failed/message-opcode-too-large-1.tact similarity index 100% rename from src/types/test-failed/message-opcode-too-large.tact rename to src/types/test-failed/message-opcode-too-large-1.tact diff --git a/src/types/test-failed/message-opcode-too-large-2.tact b/src/types/test-failed/message-opcode-too-large-2.tact new file mode 100644 index 000000000..703c42d97 --- /dev/null +++ b/src/types/test-failed/message-opcode-too-large-2.tact @@ -0,0 +1 @@ +message(0xFFFF * 0x1FFFF) Foo { } diff --git a/src/types/test-failed/message-opcode-zero.tact b/src/types/test-failed/message-opcode-zero-1.tact similarity index 100% rename from src/types/test-failed/message-opcode-zero.tact rename to src/types/test-failed/message-opcode-zero-1.tact diff --git a/src/types/test-failed/message-opcode-zero-2.tact b/src/types/test-failed/message-opcode-zero-2.tact new file mode 100644 index 000000000..e77c218e5 --- /dev/null +++ b/src/types/test-failed/message-opcode-zero-2.tact @@ -0,0 +1,9 @@ +trait BaseTrait { } + +// zero opcodes are reserved for string comments +message(pow(2, 4) - 16) ZeroOpcode { } + +contract Test { + receive(msg: ZeroOpcode) { } +} + diff --git a/src/types/test/message-opcode-expr.tact b/src/types/test/message-opcode-expr.tact new file mode 100644 index 000000000..da6a9d707 --- /dev/null +++ b/src/types/test/message-opcode-expr.tact @@ -0,0 +1,10 @@ +primitive Int; +trait BaseTrait { } + +message((crc32("Tact") + 42) & 0xFFFF_FFFF) MsgWithExprOpcode { + field: Int as uint4; +} + +contract Foo { + receive(msg: MsgWithExprOpcode) { } +}