Skip to content

Functions

Tact offers a diverse set of function kinds and attributes that provide great flexibility and expressivity. While some functions stand out, many of their parts and behaviors are common.

Receiver functions

Receiver functions are special functions responsible for receiving messages in contracts and can be defined only within a contract or trait.

contract Counter(counter: Int) {
// This means that this contract can receive the Increment message body,
// and this function would be called to handle such messages.
receive(msg: Increment) {
self.counter += 1;
}
}
message Increment {}

There are three kinds of receiver functions in Tact:

  • receive(), which receives internal messages from other contracts.
  • bounced(), which is processed when an outgoing message from this contract bounces back.
  • external(), which doesn’t have a sender and can be sent by anyone in the world.

receive — internal message receivers

Contract scope

The most common receiver functions, receive(), handle incoming messages from other contracts.

// This contract defines various kinds of receivers in their
// order of handling the corresponding incoming messages.
contract OrderOfReceivers() {
// Empty receiver
receive() {
inMsg().bits; // 0
}
// Text receiver
receive("yeehaw!") {
inMsg().asString(); // "yeehaw!"
}
// Catch-all String receiver
receive(str: String) {
// ...
}
// Binary message receiver
receive(msg: FancyMessage) {
// ...
}
// Catch-all Slice receiver
receive(rawMsg: Slice) {
// ...
}
}
message FancyMessage {}

Read more about them on their dedicated page: Receive messages.

bounced — bounced internal message receivers

Contract scope

The bounced() is a special kind of receivers which handle outgoing messages that were sent from this contract and bounced back to it.

contract Bouncy() {
// Handles empty message bodies
receive() {
// Sending a message...
message(MessageParameters{
to: sender(),
value: ton("0.1"),
body: BB {}.toCell(), // ...with a BB message body
});
}
// If the BB message body wasn't successfully processed by the recipient,
// it can bounce back to our contract, in which case the following receiver
// will handle it.
bounced(msg: bounced<BB>) {
// ...
}
}
message BB {}

Read more about them on their dedicated page: Bounced messages.

external — external message receivers

Contract scope

The external() is a special kind of receivers which handle external messages — they are sent from the off-chain world and do not have a sender address on the blockchain. Such messages are often sent to wallet contracts to process specific messages or simply to send funds to another wallet contract.

contract FeaturelessWallet(publicKey: Int as uint256) {
external(msg: MessageWithSignedData) {
// Can't be replied to as there's no sender!
// Thus, many checks are required.
throwUnless(35, msg.bundle.verifySignature(self.publicKey));
}
}
message MessageWithSignedData {
bundle: SignedBundle;
walletId: Int as int32;
seqno: Int as uint32;
}

Read more about them on their dedicated page: External messages.

fun — regular functions

Any scope

Regular functions are defined using the fun keyword.

fun add(a: Int, b: Int): Int {
return a + b;
}

Read about common aspects of functions: Commonalities.

Global functions

Global scope

Regular functions that can be defined only at the top (module) level are called global. They differ from internal functions not only in the scope, but also in available attributes.

fun reply(msgBody: Cell) {
message(MessageParameters {
to: sender(),
value: 0,
mode: SendRemainingValue | SendIgnoreErrors,
body: msgBody,
});
}

The following attributes can be specified for global functions:

These attributes cannot be specified:

Internal functions

Contract scope

These functions behave similarly to private methods in popular object-oriented languages — they are internal to contracts and can be called by prefixing their names with a special identifier self. That is why internal (or member) functions are sometimes referred to as “contract methods”.

Internal functions can access the contract’s persistent state variables and constants.

They can only be called from receivers, getters, and other internal functions, but not from other contracts.

contract InternalFun(stopped: Bool) {
// Let's define an internal function to ensure that the contract was not stopped.
fun requireNotStopped() {
throwUnless(TactExitCodeContractStopped, !self.stopped);
}
// And use that internal function within a getter.
get fun veryImportantComputation(): Int {
// Internal function calls are prefixed with `self`
self.requireNotStopped();
return 2 + 2;
}
}

The following attributes can be specified for internal functions with no prior attributes:

These attributes cannot be specified:

Extensions

Global scope

Extension functions provide a clean way to organize and extend code by allowing you to add new behaviors to existing types. The extension mutation functions also allow you to modify the value they operate on. Both kinds of extension functions can be called methods and both use special attributes: extends and mutates, respectively.

extends — extension functions

Global scope

The extends attribute can be applied to all top level functions, transforming them into statically dispatched methods on the extended type. These methods are called extension functions.

To define an extension function, add an extends attribute to any global function and name its first parameter as self. The type of self is the type that is being extended.

// Extension function that compares two instances of the `StdAddress` type.
extends fun equals(self: StdAddress, other: StdAddress): Bool {
if (self.workchain == other.workchain && self.address == other.address) {
return true;
}
return false;
}

The special self identifier inside an extension function is a copy of the value of the extended type. In other words, it is the argument value passed to the self parameter.

// Let's define a variable to call the `equals()` extension function on.
let addr = parseStdAddress(
address("EQAFmjUoZUqKFEBGYFEMbv-m61sFStgAfUR8J6hJDwUU09iT").asSlice(),
);
// You can call extension functions on instances of the extended type,
// such as variables...
addr.equals(addr); // true
// ...literals and structure instances...
StdAddress {
workchain: addr.workchain,
address: addr.address,
}.equals(addr); // true
// ...or any other values of that type.
parseStdAddress(
address("EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N").asSlice()
).equals(addr); // false

Most of the standard library functions that work with values of the Builder type are extension functions on that type. As such, they do not modify the original value they are applied to.

To preserve newly created values with extension functions, use intermediary assignments or make a long chained call in a single assignment.

let builder = beginCell();
let cell = builder
.storeUint(42, 7)
.storeInt(42, 7)
.storeBool(true)
.storeSlice(slice)
.storeCoins(42)
.storeAddress(address)
.storeRef(cell)
.endCell();
// Notice that the chained extension function call did not change
// the `builder` variable itself, which is still holding an empty Builder.
builder.refs(); // 0
builder.bits(); // 0

The first parameter of extension functions must be named self. Failure to do so leads to a compilation error.

Additionally, naming the first parameter of any non-extension function as self leads to a compilation error too.

// COMPILATION ERROR! Extend function must have first parameter named "self"
extends fun add(this: Int, other: Int): Int {
// ~~~~~~~~~
return this + other;
}
// COMPILATION ERROR! Parameter name "self" is reserved for functions with "extends"
fun add(self: Int, other: Int): Int {
// ~~~~~~~~~
return self + other;
}

The following additional attributes can be specified:

These attributes cannot be specified:

mutates — extension mutation functions

Global scope

The mutates attribute can be applied to extension functions only, which enables the persistence of the modifications made to the value of the extended type through the self identifier. They allow mutations, and so they are called extension mutation functions.

The extension mutation functions replace the value of the extended type with the new one at the end of its execution. That value would change only upon the assignment to self within the function body.

To define an extension mutation function, add a mutates attribute to the existing extension function.

// Like the `skipBool` extension mutation function, but usable in chain calls.
extends mutates fun chainSkipBool(self: Slice): Slice {
self.skipBool();
// Notice that returning the `self` value gives a copy of the slice,
// but not a mutable reference to it. Therefore, chaining of mutation functions
// is not very useful in most cases, and may lead to seemingly weird errors.
return self;
}

For extension mutation functions, the special self identifier refers to the value of the extended type, and changes made to it will persist after the function call.

// It is most useful to call extension mutation functions on instances
// of the extended type that can keep the change stored either temporarily, i.e.,
// within the current transaction, or permanently — in the contract's state.
contract Mut(val: Slice) {
receive() {
let valTmp = beginCell().storeBool(true).storeInt(42, 7).asSlice();
valTmp.chainSkipBool().loadInt(7); // 42
self.val.chainSkipBool().loadInt(7); // 42
// Now, `valTmp` holds a slice without one bit, i.e.,
// only the value 42 stored using 7 bits. The `self.val` holds its original
// slice but without one bit from its front.
}
}

Extension mutation functions cannot modify constants because they are immutable.

// Constant of the Int type
const VAL: Int = 10;
// Extension mutation function for the Int type
extends mutates fun divideInHalf(self: Int) { self /= 2 }
fun noEffect() {
VAL; // 10
VAL.divideInHalf();
VAL; // still 10
}

The following additional attributes can be specified:

  • inline — embeds the function contents at the call site.

These attributes cannot be specified:

  • abstract, virtual and override — extension mutation functions cannot be defined within a contract or a trait.
  • get — extension mutation functions cannot become getters.

Static extension functions

There is no such attribute yet, so the special static extension functions in the standard library of Tact are introduced directly by the compiler:

get fun — off-chain getter functions

Contract scope

The special get attribute cannot be combined with any other attribute. It is applied to internal functions, transforming them into so-called getter functions (or getters for short). These functions are externally accessible off-chain, allowing direct reading of contract state without regular message passing.

contract StaleCounter(val: Int as uint32) {
// Getter function that simply returns the current value of the state variable
get fun value(): Int {
return self.val;
}
// Getter function that performs basic calculations
get fun valuePlus(another: Int): Int {
return self.val + another;
}
}

Despite the restriction on attributes, getter functions can still be considered internal or contract methods — calling them from within another contract function is possible. In those cases, their behavior would be identical to calling an internal function through self, including any state modifications those functions can perform.

However, most of the time, getter functions are there to be called from the off-chain world. And when they are called that way and not via self, their state modifications no longer persist between transactions (changes are discarded).

contract WillNotBudge(val: Int) {
get fun changeVal() {
self.val = randomInt();
self.val; // ?, it is random!
// However, after this function is done,
// the self.val will be reset to its value prior to this function call.
}
}

Furthermore, when not used as internal functions, getters do not pay the compute fees and have their gas limits to prevent infinite loops or endless transactions. That is, getters can perform arbitrary computations if their internal gas limit is not surpassed in a single transaction.

Those gas limits depend on the API provider used to call a certain getter, but they are usually around 100 million gas units per getter call.

With that in mind, getters are most commonly used for fetching the current state of contract’s persistent state variables. To do so in a convenient matter, Tact allows you to view the contract itself as a struct of contract variables, i.e., to use self to refer to the contract itself and return all of its state variable values at once in a single getter.

contract ManyFields(
f1: Int,
f2: Int,
// ...
f42: Int,
) {
// Notice that the return type of this get function coincides
// with the name of the contract, which makes that return type
// view the contract as a struct of its variables.
get fun state(): ManyFields {
return self; // you do not have to list all fields of a contract for this
}
}

Method IDs

Like other functions in TON contracts, getters have their unique associated function selectors, which are 19-bit signed integer identifiers commonly called method IDs.

Method IDs of getters are derived from their names using the CRC16 algorithm as follows: (crc16(<function_name>) & 0xffff) | 0x10000. In addition, the Tact compiler conditionally reserves some method IDs for use in getters of supported interfaces, namely: 113617 for supported_interfaces, 115390 for lazy_deployment_completed, and 121275 for get_abi_ipfs.

Thus, getters and their method IDs are an important part of the contract interfaces, to the point where explorers tend to recognize contracts based not on their entire code, but on the set of getters they expose to the off-chain world.

Explicit resolution of method ID collisions

Available since Tact 1.6

Sometimes, getters with different names end up with the same method ID. If this happens, you can either rename some of the getters or manually specify the method ID as a compile-time expression like so:

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 crc32("crc32") + 42 & 0x3ffff | 0x4000;
}
}

Unlike getters, method IDs for internal functions and some special functions are obtained sequentially: integers in the inclusive range from -4 to 0 are given to certain message handlers, while internal functions are numbered with method IDs starting at 11 and going up to 21412^{14} - 1 inclusive.

Since method IDs are 19-bit signed integers and some of them are reserved, only the inclusive ranges from 218-2^{18} to 5-5 and from 2142^{14} to 21812^{18} - 1 are free to be used by users. It is recommended to specify method IDs only in these ranges to avoid collisions.

Furthermore, as the algorithm for generating method IDs only produces positive values and IDs -4 to 0 are reserved, by manually specifying negative IDs from 218-2^{18} up to 5-5, you can guarantee that there would be no collisions with any other getters.

contract AntiPositive() {
// This getter's manually specified method ID
// will not collide with any autogenerated IDs
get(-42) fun negAns(): Int {
return -42;
}
get fun posAns(): Int {
return 42;
}
}

init — constructor function

Contract scope

The special constructor function init() is run upon deployment of the contract. Unlike contract parameters, it performs a delayed initialization of the contract data, overriding the values of persistent state variables on-chain.

contract Example {
// Persistent state variables
var1: Int = 0; // initialized with default value 0
var2: Int; // must be initialized in the init() function
var3: Int = 7; // initialized with default value 7
// Constructor function, which is run only once
init() {
self.var2 = 42;
self.var3 = 32; // overrides the default to 32
}
}

For every receiver that doesn’t alter the contract’s variables, Tact optimizes away unnecessary storage overrides. However, contracts cannot benefit from such optimizations if the init() function is present. That is because for contracts with init() every receiver has to check whether the init() function has run already and did that only once, and to do so, a special flag is implicitly stored in the contract’s persistent state.

Effectively, the behavior or init() function can be simulated when using contract parameters instead — through adding a special Bool field and a function that would be used in place of the init(). That function would be called at the beginning of every receiver, while the boolean field would be used as a flag.

contract CounterBuiltinInit {
// Persistent state variables
counter: Int as uint32;
// Constructor function, which is run only once
init() {
self.counter = 0;
}
receive() {
cashback(sender());
}
receive(_: Increment) {
self.counter += 1;
}
get fun counter(): Int {
return self.counter;
}
}
contract CounterDIYInit(
// Persistent state variables
initialized: Bool, // set this field to `false` during deployment
counter: Int as uint32, // value of this field will be overridden in the `customInit()`
) {
// Internal function, which will be used in place of a pseudo-constructor function
fun customInit() {
// Stop further actions if this customInit() function was called before
if (self.initialized) {
return;
}
// Initialize
self.initialized = true;
self.counter = 0;
}
receive() {
// Don't forget to add this call at the start of every receiver
// that can be used for deployments.
self.customInit();
cashback(sender());
}
receive(_: Increment) {
// Don't forget to add this call at the start of every receiver
// that can be used for deployments.
self.customInit();
self.counter += 1;
}
get fun counter(): Int {
return self.counter;
}
}
message Increment {}

None of the attributes can be specified for the init() function.

Read more about the init() on its dedicated section: Constructor function init().

native — foreign function interfaces

Global scope

Native functions are Tact’s FFI to FunC — they allow direct bindings to imported FunC functions. In order to declare them, you have to import a .fc or .func file first. Granted, the FunC’s standard library file, stdlib.fc, is always imported for you.

Consider the following FunC file:

import_me.fc
;; Notice the tensor return type (int, int),
;; which means we would have to define at least one
;; Tact structure to bind to this function.
(int, int) keccak512(slice s) impure asm "ONE HASHEXT_KECCAK512 UNPAIR";

In Tact code, one could access that function as follows:

// 1. Import the FunC file that contains the function.
import "./import_me.fc";
// 2. Declare the native function binding.
@name(keccak512) // name of the target FunC function
native keccak512(s: Slice): HashPair;
// ^^^^^^^^^ name of the Tact function bind,
// which can be the identical to its FunC counterpart
// 3. Define the necessary structs for the bind.
// In our case, it is the HashPair structure to capture multiple return values.
struct HashPair {
h1: Int;
h2: Int;
}
// 4. Use the function whenever you would like.
// It has the same properties as regular global functions.
fun example() {
let res = keccak512(emptySlice());
res.h1; // 663...lots of digits...092
res.h2; // 868...lots of digits...990
}

The following attributes can be specified for native functions:

These attributes cannot be specified:

asm — assembly functions

Global scope Available since Tact 1.5

Assembly functions are top-level functions that allow you to write Tact assembly. Unlike all other functions, their bodies consist only of TVM instructions and some other primitives that serve as arguments to the instructions.

Furthermore, asm functions are nearly devoid of all abstractions and give direct access to the underlying stack and register contents on which TVM operates.

// Simple assembly function that creates a new `Builder`.
asm fun beginCell(): Builder { NEWC }
// Like nativeReserve(), but also allows reserving extra currencies.
asm fun extraReserve(
// Toncoin amount
nativeAmount: Int,
// A map of 32-bit extra currency IDs to VarUInt32 amounts
extraAmountMap: map<Int as uint32, Int as varuint32>,
// Reservation mode
mode: Int,
) { RAWRESERVEX }
// Assembly function as an extension function for the `Builder` type.
asm(value self) extends fun storeBool(self: Builder, value: Bool): Builder { 1 STI }

Read more about them on their dedicated page: Assembly functions.

Inheritance

Contract scope

The with keyword allows a contract to inherit a trait with all its constants, fields, and functions, including those transitively inherited from other traits associated with the specified trait.

In particular, all functions of the trait become accessible in the contract, regardless of any inheritance-related attributes, such as abstract, virtual, or override.

You can allow a contract inheriting a trait to modify a internal function marked with the virtual keyword by using override. A function can also be marked as abstract, in which case the inheriting contract must define its implementation.

trait FilterTrait with Ownable {
// Virtual functions can be overridden by users of this trait
virtual fun filterMessage(): Bool {
return sender() != self.owner;
}
abstract fun specialFilter(): Bool;
// Receivers can be inherited, but cannot be overridden
receive() { cashback(sender()) }
}
contract Filter() with FilterTrait {
// Overriding the default behavior of the FilterTrait
override fun filterMessage(): Bool {
return true;
}
override fun specialFilter(): Bool {
return true;
}
}

abstract

Contract scope

The abstract attribute allows declaring a internal function with no body given at declaration. Functions with this attribute are meant to be overridden later in the inheritance hierarchy by either an intermediate trait or the contract inheriting it.

trait DelayedOwnable {
owner: Address;
abstract fun requireOwner();
abstract fun requireOwnerDelayed(bigBadName: Int);
}
contract Ownership(owner: Address) with DelayedOwnable {
// All functions marked as abstract must be defined
// in the contract that inherits them.
override fun requireOwner() {
throwUnless(TactExitCodeAccessDenied, sender() == self.owner);
}
// The signature of the overridden function must match its inherited abstract
// counterpart on everything except parameter names. Notice the rename
// of the `bigBadName` parameter to `timestamp` in the function below.
override fun requireOwnerDelayed(timestamp: Int) {
throwUnless(TactExitCodeAccessDenied, now() >= timestamp);
throwUnless(TactExitCodeAccessDenied, sender() == self.owner);
}
}

virtual

Contract scope

The virtual attribute allows a internal function to be overridden later in the inheritance hierarchy by either an intermediate trait or the contract inheriting it.

This attribute is similar to the abstract attribute, except that you do not have to override bodies of functions with the virtual attribute. But if you decide to override the function body, you may do it in any trait that depends on this trait and not only in the resulting contract.

import "@stdlib/ownable";
trait DeployableFilterV1 with Ownable {
// Virtual functions can be optionally overridden by users of this trait.
virtual fun filter() {
// Not an owner
throwUnless(TactExitCodeAccessDenied, sender() == self.owner);
}
// Whereas internal functions with an abstract attribute must be overridden
// by the contract that will import this trait or any of the traits that depend on this trait.
abstract fun specialFilter();
// Receivers are inherited too,
// but they cannot defined be virtual or abstract
receive() { cashback(sender()) }
}
trait DeployableFilterV2 with DeployableFilterV1 {
override fun filter() {
// Not an owner
throwUnless(TactExitCodeAccessDenied, sender() == self.owner);
// Message carries too little Toncoin for our tastes
throwUnless(TactExitCodeAccessDenied, context().value < ton("1"));
}
}
contract Auth(owner: Address) with DeployableFilterV2 {
override fun specialFilter() {
if (randomInt() < 10) {
throw(TactExitCodeAccessDenied);
}
}
receive(_: TopSecretRequest) {
self.filter();
self.specialFilter();
// ...subsequent logic...
}
}
message TopSecretRequest {}

override

Contract scope

The override attribute is used to override an inherited internal function that has either a virtual or abstract attribute specified in the declaration of that function in one of the parent traits.

Commonalities

Functions commonly share the same aspects, parts, or behavior. For example, due to the nature of TVM, Tact uses the call by value parameter-passing and binding strategy, which applies to all function kinds. That is, the evaluated value of any variable passed in a function call or assigned in the let or assignment statement is copied.

Therefore, there are no modifications of the original values in different scopes, except for the extension mutation functions. And even so, those functions do not mutate the original value denoted by the self keyword — instead, they discard the old value and replace it with the new one obtained at the end of the function body.

The above applies to all functions except for receivers, because they cannot be called directly and are only executed upon receiving specific messages.

Scope

All kinds of functions vary in scope: some can only be defined at the top level, such as global functions, while others can only be defined within contracts and traits, such as internal functions or receivers.

Functions are hoisted upon declaration, such that the ones declared earlier might call those declared later and vice versa.

// Global function first() is defined prior to the second() function,
// but we can call the second() one just fine.
fun first(): Bool {
return second(true); // always true
}
// Global function second() is defined after the first() function,
// and call to the first() function works as expected.
fun second(flag: Bool): Bool {
if (flag) {
return true;
}
return first(); // always true, because of the prior condition
}

Attributes

Function attributes modify a function’s behavior and typically restrict it to a specific scope.

AttributeScopeCan be applied to
extendsglobalAll global functions
mutatesglobalFunctions with extends attribute
virtualcontractInternal functions
abstractcontractInternal functions
overridecontractInternal functions that were previously marked with virtual or abstract
inlineanyAll functions except for receivers and getters
getcontractInternal functions — turns them into getters

Notice that the “contract” scope also includes the traits.

Naming and namespaces

Function names follow the standard Tact identifier naming conventions.

abstract fun azAZ09_();

Internal functions and getters exist in a namespace separate from the contract’s fields. For example, a persistent state variable named value can co-exist with a getter function named value(), which is very convenient in many cases.

However, identifiers of internal functions within a single contract must be distinct, as are identifiers of all top-level functions. The only exceptions are extensions, which can have the same identifiers, provided that the extended types of their self parameter differ.

// COMPILATION ERROR! Static function "globName" already exists
fun globName() {}
fun globName() {}
// ~~~~~~~~~~~~~
// No errors, because despite the same identifiers,
// the types of the `self` parameter are different
extends fun doWith(self: Int) {}
extends fun doWith(self: Slice) {}

Return values

All functions return a value. If the function does not have an explicit return type specified, it has an implicit return type of void — a pseudo-value that represents “nothing” in the Tact compiler. This also applies to the functions that do not allow specifying a return type, such as receivers or init().

Moreover, execution of any function can be ended prematurely with the return statement. This is also true for the receiver functions and [init()], where empty return statements are allowed.

contract GreedyCashier(owner: Address) {
receive() {
// Stop the execution if the message is not from an `owner`.
if (sender() != self.owner) {
return;
}
// Otherwise, forward excesses back to the sender.
cashback(sender());
}
}

To return multiple values in a function, aggregate the values in a struct.

fun minmax(a: Int, b: Int): MinMax {
if (a < b) {
return MinMax { min: a, max: b };
}
return MinMax { min: b, max: a };
}
struct MinMax {
min: Int;
max: Int;
}

Reachability of return statements should always be evident to the compiler. Otherwise, a compilation error will occur.

Recursion

Recursive functions are allowed, including the mutually recursive functions. There is no upper limit on the recursion depth or the number of calls — computations will continue until there is no more gas to spend.

fun produceOneFrom(a: Int): Int {
if (a <= 1) { return 1 }
return alsoProduceOneFrom(a - 1);
}
fun alsoProduceOneFrom(b: Int): Int {
if (b <= 1) { return 1 }
return produceOneFrom(b - 1);
}
fun example(someA: Int, someB: Int) {
// This sum will always be 2, regardless of values of `someA` and `someB`.
// The only exception is when their values are too big and there is not
// enough gas to finish the computations, in which case the transaction
// will fail with an exit code -14: Out of gas error.
produceOneFrom(someA) + alsoProduceOneFrom(someB); // 2
}

Code embedding with inline

Any scope

The inline attribute embeds the code of the function to its call sites, which eliminates the overhead of the calls but potentially increases the code size if the function is called from multiple places.

This attribute can improve performance of large infrequently called functions or small, alias-like functions. It can be combined with any other attribute except for get.

inline fun add(a: Int, b: Int) {
// The following addition will be performed in-place,
// at all the `add()` call sites.
return a + b;
}

Trailing comma

All functions, except for receiver functions, can have a trailing comma in their definitions (parameter lists) and calls (argument lists):

fun foo(
a: Int, // trailing comma in parameter lists is allowed
) {}
fun bar() {
foo(
5, // trailing comma in argument lists is allowed too!
);
}

Trailing semicolon

The last statement inside the function body has a trailing semicolon — it can be freely omitted, which is useful for writing one-liners.

inline fun getCoinsStringOf(val: Int) { return val.toFloatString(9) }

When declaring an internal function with abstract or virtual attributes, Tact allows you to omit a semicolon if it is the last function in the trait’s body.

trait CompositionVII {
abstract fun paintedWithOil(preliminary: Bool): Bool // no semicolon needed
}
contract Abstractionism() with CompositionVII {
override fun paintedWithOil(preliminary: Bool): Bool {
if (preliminary) {
return false; // perhaps, watercolors were used...
}
return true;
}
}

Wildcard parameters

Available since Tact 1.6.1

For all kinds of functions, naming a parameter with an underscore _ causes its value to be considered unused and discarded. This is useful when you do not access the parameter but want to include it in the signature for possible overrides. Note that such a wildcard parameter name _ cannot be accessed.

trait WildThing {
// Using wildcards for parameter names
virtual fun assure(_: Int, _: Int): Bool {
return true;
}
}
contract YouMakeMyHeartSing() with WildThing {
// And then overriding them with concrete names
override fun assure(a: Int, b: Int): Bool {
return a + b == b + a;
}
// Wildcards are also useful when you do not intend to
// use the contents of the message and only handle the proper one
receive(_: FieldsDoNotLie) {
// ...
}
}
message FieldsDoNotLie {}

Low-level representation

On TVM level, contracts and their functions are often represented as a dictionary (or “hashmap”) of methods, from their numeric method IDs to their respective bodies composed of TVM instructions. Each function then is a method that transforms the stack and TVM registers.

A function call is, essentially, a call into the dictionary using the function’s ID, after which:

  1. Function arguments are pushed onto the stack
  2. The function executes, manipulating the stack
  3. Return values are left on top of the stack, provoking another dictionary call or resulting in the end of the transaction

When a so-called “selector hack” optimization is enabled, if the number of defined receivers is small, they are stored outside of that map as plain instruction sequences. That saves gas but can cause the contract to be incorrectly recognized and misparsed by some explorers and user wallets, which expect the root of the contract’s code (the root cell) to point at the method dictionary.