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 scopeRegular 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:
inline
— embeds the function contents at the call site.extends
— makes it an extension function.mutates
, along withextends
— makes it an extension mutation function.
These attributes cannot be specified:
abstract
,virtual
andoverride
— global functions cannot be defined within a contract or a trait.get
— global functions cannot be getters.
Internal functions
Contract scopeThese 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:
inline
— embeds the function contents at the call site.abstract
,virtual
andoverride
— function inheritance.get
— internal functions can become getters.
These attributes cannot be specified:
extends
— internal function cannot become an extension function.mutates
— internal function cannot become an extension mutation function.
Extensions
Global scopeExtension 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(); // 0builder.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:
inline
— embeds the function contents at the call site.mutates
— makes it into an extension mutation function.
These attributes cannot be specified:
abstract
,virtual
andoverride
— extension functions cannot be defined within a contract or a trait.get
— extension functions cannot become getters.
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 typeconst VAL: Int = 10;
// Extension mutation function for the Int typeextends 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
andoverride
— 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.6Sometimes, 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 and going up to inclusive.
Since method IDs are 19-bit signed integers and some of them are reserved, only the inclusive ranges from to and from to 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 up to , 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:
;; 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 functionnative 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:
inline
— embeds the function contents at the call site.extends
— makes it an extension function.mutates
, along withextends
— makes it an extension mutation function.
These attributes cannot be specified:
abstract
,virtual
andoverride
— native functions cannot be defined within a contract or a trait.get
— native functions cannot be getters.
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 scopeThe 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.
Attribute | Scope | Can be applied to |
---|---|---|
extends | global | All global functions |
mutates | global | Functions with extends attribute |
virtual | contract | Internal functions |
abstract | contract | Internal functions |
override | contract | Internal functions that were previously marked with virtual or abstract |
inline | any | All functions except for receivers and getters |
get | contract | Internal 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 existsfun globName() {}fun globName() {}// ~~~~~~~~~~~~~
// No errors, because despite the same identifiers,// the types of the `self` parameter are differentextends 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.1For 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:
- Function arguments are pushed onto the stack
- The function executes, manipulating the stack
- 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.