From 4d865629e5bf69a4a21a0ed6e3e45db463363be4 Mon Sep 17 00:00:00 2001 From: tolk-vm Date: Mon, 10 Nov 2025 12:00:44 +0300 Subject: [PATCH 01/11] Fix some typos in existing Tolk docs --- docs.json | 2 +- languages/tolk/counter-smart-contract.mdx | 6 +++--- languages/tolk/from-func/create-message.mdx | 6 +++--- languages/tolk/from-func/in-detail.mdx | 8 +++----- languages/tolk/from-func/in-short.mdx | 19 +++++++++--------- languages/tolk/from-func/lazy-loading.mdx | 22 ++++++++++----------- languages/tolk/from-func/mutability.mdx | 18 ++++++++--------- languages/tolk/from-func/pack.mdx | 12 +++++------ languages/tolk/from-func/stdlib.mdx | 18 ++++++++--------- languages/tolk/overview.mdx | 12 +++++------ 10 files changed, 60 insertions(+), 63 deletions(-) diff --git a/docs.json b/docs.json index 2b2a15983..36581be18 100644 --- a/docs.json +++ b/docs.json @@ -396,7 +396,7 @@ "pages": [ "languages/tolk/overview", { - "group": "From FunC", + "group": "Migrating from FunC", "pages": [ "languages/tolk/from-func/in-short", "languages/tolk/from-func/in-detail", diff --git a/languages/tolk/counter-smart-contract.mdx b/languages/tolk/counter-smart-contract.mdx index a332b8e5e..2c0d583d9 100644 --- a/languages/tolk/counter-smart-contract.mdx +++ b/languages/tolk/counter-smart-contract.mdx @@ -46,7 +46,7 @@ get fun currentCounter() ... get fun initialId() ... ``` -Let’s explore what’s happening here. +Let's explore what's happening here. ## Smart contract data @@ -105,7 +105,7 @@ struct (0x7e8764ef) IncreaseCounter { ## Sending messages -This contract doesn’t send any messages. +This contract doesn't send any messages. To learn how message construction works in Tolk, see the [Create message](/languages/tolk/from-func/create-message) article. ## Handling internal bounced messages @@ -117,7 +117,7 @@ fun onBouncedMessage(in: InMessageBounced) { } ``` -Since this contract only accepts messages like `IncreaseCounter` and `ResetCounter` and doesn’t send any, you can leave this function empty or remove it entirely. +Since this contract only accepts messages like `IncreaseCounter` and `ResetCounter` and doesn't send any, you can leave this function empty or remove it entirely. ## GET methods diff --git a/languages/tolk/from-func/create-message.mdx b/languages/tolk/from-func/create-message.mdx index 167c33963..1d7f89a8f 100644 --- a/languages/tolk/from-func/create-message.mdx +++ b/languages/tolk/from-func/create-message.mdx @@ -216,7 +216,7 @@ The rules are the following: Why not make a ref always? Because creating cells is expensive. Avoiding cells for small bodies is crucial for gas consumption. -Interestingly, whether the body is **small** is determined **AT COMPILE TIME — no runtime checks are needed**. How? Thanks to generics! Here’s how `createMessage` is declared: +Interestingly, whether the body is **small** is determined **AT COMPILE TIME — no runtime checks are needed**. How? Thanks to generics! Here's how `createMessage` is declared: ```tolk fun createMessage(options: CreateMessageOptions): OutMessage; @@ -281,7 +281,7 @@ val excessesMsg = createMessage({ excessesMsg.send(SEND_MODE_IGNORE_ERRORS); ``` -This works perfectly, as expected. But an interesting fact—this also works: +This works perfectly, as expected. But an interesting fact — this also works: ```tolk // just an example, that even this would work @@ -371,7 +371,7 @@ excessesMsg.send(mode); excessesMsg.sendAndEstimateFee(mode); ``` -This strategy makes the code **easier to read** later. You see — okay, this is about excesses, this one is about burn notification, etc. As opposed to a potential `send(...)` function, you have to dig into what body is actually being sent to understand what’s going on. +This strategy makes the code **easier to read** later. You see — okay, this is about excesses, this one is about burn notification, etc. As opposed to a potential `send(...)` function, you have to dig into what body is actually being sent to understand what's going on. ## Why not provide a separate deploy function? diff --git a/languages/tolk/from-func/in-detail.mdx b/languages/tolk/from-func/in-detail.mdx index 9e4f709e7..83bde757c 100644 --- a/languages/tolk/from-func/in-detail.mdx +++ b/languages/tolk/from-func/in-detail.mdx @@ -41,8 +41,7 @@ It is similar to how `number` is a valid identifier in TypeScript. In FunC, almost any character can be part of an identifier. For example, `2+2` without spaces is treated as a single identifier, and a variable can be declared with such a name. -In Tolk, spaces are not required. `2+2` is `4`, not an identifier. Identifiers can only contain alphanumeric characters. -`2+2` evaluates to `4`, and `3+~x` is interpreted as `3 + (~x)`, and so on. +In Tolk, spaces are not required. `2+2` evaluates to `4`, and `3+~x` is interpreted as `3 + (~x)`, and so on. | FunC | Tolk | | :--------------------------------------: | :-----------------: | @@ -54,7 +53,6 @@ This feature is intended primarily for code generation, where keywords may need | FunC | Tolk | | :------------------------------------: | :--------------------------------------: | | `const op::increase = 0x1234;` | `const OP_INCREASE = 0x1234` | -| `int 2+2 = 5; ;; even 2%&!2 is valid ` | ` var '2+2' = 5; // don't do like this` | ### Impure by default, no function call elimination @@ -281,7 +279,7 @@ if (smth) { | `const ONE_TON = 1000000000;` | `const ONE_TON = ton("1")` | The function `ton()` only accepts constant values. For example, `ton(some_var)` is invalid. -Its type is `coins`, not `int`, although it’s treated as a regular `int` by the TVM. +Its type is `coins`, not `int`, although it's treated as a regular `int` by the TVM. Arithmetic operations on `coins` degrade to `int` — for example, `cost << 1` or `cost + ton("0.02")` are both valid. ### Type system changes @@ -2205,7 +2203,7 @@ fun onBouncedMessage(in: InMessageBounced) { ### Next steps -Explore the [Tolk vs FunC benchmarks](https://github.com/ton-blockchain/tolk-bench) —real Jetton, NFT, and Wallet contracts migrated from FunC with the same logic. +Explore the [Tolk vs FunC benchmarks](https://github.com/ton-blockchain/tolk-bench) — real Jetton, NFT, and Wallet contracts migrated from FunC with the same logic. Use the [FunC-to-Tolk converter](https://github.com/ton-blockchain/convert-func-to-tolk) for incremental migration. diff --git a/languages/tolk/from-func/in-short.mdx b/languages/tolk/from-func/in-short.mdx index 449ed7146..97abbb7e5 100644 --- a/languages/tolk/from-func/in-short.mdx +++ b/languages/tolk/from-func/in-short.mdx @@ -6,7 +6,7 @@ sidebarTitle: "In short" Tolk is more similar to TypeScript and Kotlin than to C or Lisp. It provides complete control over the TVM assembler, as it includes a FunC kernel within. -### Declarations +## Declarations - `fun` declares a function. - `get fun` declares a get method. @@ -36,18 +36,18 @@ fun sum(a: int, b: int) { // auto inferred int get fun currentCounter(): int { ... } ``` -### Syntax and semantics +## Syntax and semantics - No `impure`. All functions are treated as `impure` by default; the compiler does not remove user function calls. - Use `onInternalMessage`, `onExternalMessage`, and `onBouncedMessage` instead of `recv_internal` and `recv_external`. -- `2+2` is 4, not an identifier. Identifiers are **alphanumeric**. Use `const OP_INCREASE` instead of `const op::increase`. `cell` and `slice` are valid identifiers; not keywords. +- `2+2` is 4, not an identifier. Identifiers are **alphanumeric**. Use `const OP_INCREASE` instead of `const op::increase`. `cell` and `slice` are valid identifiers, not keywords. - Logical operators AND `&&`, OR `||`, NOT `!` are supported. #### Syntax improvements - `;; comment` → `// comment` - `{- comment -}` → `/* comment */` -- `#include` → `import`; strict rule: **import only what is used** +- `#include` → `import`; strict rule: import what you use - `~ found` → `!found`; used for boolean values only (true is -1, as in FunC) - `v = null()` → `v = null` - `null?(v)` → `v == null`, similar for `builder_null?` and others @@ -63,13 +63,12 @@ get fun currentCounter(): int { ... } - `ifnot (cond)` → `if (!cond)` - `"..."c` → `stringCrc32("...")` and other postfixes -### Compilation model +## Compilation model -- Functions can be called before declaration. - Forward declarations are not required. - The compiler first parses and then resolves symbols, producing an AST representation of the source code. -### Standard library +## Standard library - Standard library functions use clear, camelCase names. - The stdlib is embedded and split into multiple files. @@ -77,7 +76,7 @@ get fun currentCounter(): int { ... } - IDEs provide import suggestions. - See [the FunC-Tolk stdlib mapping](/languages/tolk/from-func/stdlib). -### Language features +## Language features - No `~` tilde methods. See [mutability details](/languages/tolk/from-func/mutability). - Type mismatch errors are clear and readable. @@ -100,13 +99,13 @@ get fun currentCounter(): int { ... } - The compiler automatically detects and inlines functions. - Includes gas optimization improvements. -### Tooling around +## Tooling around - [JetBrains IDE plugin](https://github.com/ton-blockchain/intellij-ton) - [VS Code extension](https://github.com/ton-blockchain/ton-language-server) - [FunC-to-Tolk converter](https://github.com/ton-blockchain/convert-func-to-tolk) - [tolk-js](https://github.com/ton-blockchain/tolk-js) WASM wrapper for blueprint -### Gas benchmarks +## Gas benchmarks The [Tolk-bench repository](https://github.com/ton-blockchain/tolk-bench) contains contracts migrated from FunC to Tolk, with identical logic and passing the same tests. diff --git a/languages/tolk/from-func/lazy-loading.mdx b/languages/tolk/from-func/lazy-loading.mdx index b86c3f696..bf21b9d93 100644 --- a/languages/tolk/from-func/lazy-loading.mdx +++ b/languages/tolk/from-func/lazy-loading.mdx @@ -30,7 +30,7 @@ get fun getPublicKey() { } ``` -That’s it! With a single `lazy` keyword, loading is deferred until the data is accessed. The compiler tracks all control flow paths, inserts loading points as needed, groups unused fields to skip, and performs other optimizations as necessary. Best of all, this works with any type and any combination of fields used anywhere in your code — the compiler tracks everything. +That's it! With a single `lazy` keyword, loading is deferred until the data is accessed. The compiler tracks all control flow paths, inserts loading points as needed, groups unused fields to skip, and performs other optimizations as necessary. Best of all, this works with any type and any combination of fields used anywhere in your code — the compiler tracks everything. ## Even deeper than you might think @@ -51,7 +51,7 @@ struct CollectionContent { } ``` -Now, imagine you want to access just the `content` field from the storage—and then extract `commonKey` from it: +Now, imagine you want to access just the `content` field from the storage — and then extract `commonKey` from it: ```tolk val storage = lazy NftCollectionStorage.load(); @@ -85,7 +85,7 @@ struct (0x12345678) CounterReset { ... } ... type MyMessage = CounterReset | CounterIncrement | ... -val msg = lazy MyMessage.fromSlice(msgBody); +val msg = lazy MyMessage.fromSlice(msgBody); match (msg) { CounterReset => { assert(senderAddress == storage.owner) throw 403; @@ -114,9 +114,9 @@ if (msgBody.isEmpty()) { } val op = msgBody.loadUint(32); // because this would throw excno 9 -if (op == OP_RESET) { +if (op == OP_RESET) { ... - return; + return; } throw 0xFFFF; // "invalid opcode" @@ -127,7 +127,7 @@ The only reason to handle empty messages upfront was to avoid throwing a _cell u With lazy `match`, you no longer need to pay gas upfront for these checks. You can handle all cases in the `else` branch: ```tolk -val msg = lazy MyMessage.fromSlice(msgBody); +val msg = lazy MyMessage.fromSlice(msgBody); match (msg) { CounterReset => { ... } ... // handle all types of a union @@ -147,7 +147,7 @@ Note that `else` in `match` by type is only allowed with `lazy` because it match ## Partial updating -The magic doesn’t stop at reading. The `lazy` keyword also works seamlessly when **writing data back**. +The magic doesn't stop at reading. The `lazy` keyword also works seamlessly when **writing data back**. Imagine you load a storage, use its fields for assertions, modify one field, and save it back: @@ -178,13 +178,13 @@ storage.toCell(); // - store immutable tail ``` -No more manual optimizations using intermediate slices—the compiler handles everything for you. It can even group unmodified fields in the middle, load them as a slice, and preserve that slice on write-back. +No more manual optimizations using intermediate slices — the compiler handles everything for you. It can even group unmodified fields in the middle, load them as a slice, and preserve that slice on write-back. This optimization is only effective when the compiler can statically resolve control flow within a local function scope. If you use globals, split logic into non-inlined functions, this optimization will break, and `lazy` will fall back to regular loading. ## Q: What are the disadvantages of lazy? -In terms of gas usage, `lazy fromSlice` is always equal to or cheaper than regular `fromSlice` because, in the worst case—when you access all fields—it loads everything one by one, just like the regular method. +In terms of gas usage, `lazy fromSlice` is always equal to or cheaper than regular `fromSlice` because, in the worst case — when you access all fields—it loads everything one by one, just like the regular method. However, there is another difference unrelated to gas consumption: @@ -198,6 +198,6 @@ struct Point { x: int8, y: int8 } If you use `lazy Point` and access only `p.x`, then an input of `FF` (8 bits) is acceptable even though `y` is missing. Similarly, `FFFF0000`, which includes 16 bits of extra data, is also fine, as `lazy` ignores any data that is not requested. -In most cases, this isn’t an issue. For storage, you have guarantees regarding the data shape, as your contract controls it. For incoming messages, you typically use all fields (otherwise, why include them in the struct?). If there is extra data in the input—who cares? The message can still be deserialized correctly, and I don’t see any problem here. +In most cases, this isn't an issue. For storage, you have guarantees regarding the data shape, as your contract controls it. For incoming messages, you typically use all fields (otherwise, why include them in the struct?). If there is extra data in the input — who cares? The message can still be deserialized correctly, and I don't see any problem here. -Perhaps someday, `lazy` will become the default. For now, it remains a distinct keyword highlighting the lazy-loading capability—a killer feature of Tolk. +Perhaps someday, `lazy` will become the default. For now, it remains a distinct keyword highlighting the lazy-loading capability — a killer feature of Tolk. diff --git a/languages/tolk/from-func/mutability.mdx b/languages/tolk/from-func/mutability.mdx index 5a9a45911..905e80f89 100644 --- a/languages/tolk/from-func/mutability.mdx +++ b/languages/tolk/from-func/mutability.mdx @@ -36,7 +36,7 @@ Tolk method calls are designed to behave similarly to JavaScript: | `b = b.store_int(x, 32);` | `b.storeInt(x, 32);` // also works `b = b.storeUint(32);` | | `b = b.store_int(x, 32).store_int(y, 32);` | `b.storeInt(x, 32).storeInt(y, 32);` // also works `b = ...;` | -### Value semantics +## Value semantics By default, function arguments in Tolk are **copied by value**. Function calls **do not** modify the original data. @@ -61,7 +61,7 @@ var flags = readFlags(msgBody); // msgBody is not modified // msgBody.loadInt(32) reads the same flags ``` -### Mutating function parameters +## Mutating function parameters Adding the `mutate` keyword makes a parameter mutable. To prevent unintended modifications, `mutate` must also be specified when calling the function. @@ -105,10 +105,10 @@ incrementXY(mutate origX, mutate origY, 10); // both += 10 ``` -### Instance methods and self +## Instance methods and self Methods — unlike global functions `fun f()` — are declared as `fun receiver_type.f()`. If a method accepts `self`, it is an instance method; otherwise, it is static. @@ -136,7 +136,7 @@ fun slice.preloadInt32(self) { } ``` -### Mutating methods with self +## Mutating methods with self Combining `mutate` with `self` defines a method that modifies the object and is called using the dot syntax. @@ -166,7 +166,7 @@ fun int.incrementWithY(mutate self, mutate y: int, byValue: int) { origX.incrementWithY(mutate origY, 10); // both += 10 ``` -The standard library includes many mutate **mutate self**, such as in tuples and dictionaries. +The standard library includes many **mutate self**, such as in tuples and dictionaries. In FunC, equivalent mutating methods use the tilde `~`. ```tolk @@ -177,7 +177,7 @@ fun tuple.push(mutate self, value: X): void t.push(1); ``` -### Returning self for chaining +## Returning self for chaining Returning `self` works as `return self` in Python or `return this` in JavaScript. It makes methods such as `storeInt()` chainable. @@ -191,13 +191,13 @@ fun builder.storeInt32(mutate self, x: int): self { } var b = beginCell().storeInt(1, 32).storeInt32(2).storeInt(3, 32); -b.storeInt32(4); // // works without assignment because it mutates b directly +b.storeInt32(4); // works without assignment because it mutates b directly b = b.storeInt32(5); // works with assignment, since also returns ``` The return type must be explicitly declared as `self`. Omitting it causes a compilation error. -### Mutate self in asm functions +## Mutate self in asm functions The same behavior can also be implemented in `asm` functions. A mutation in the compiler works as an implicit return and reassignment of `mutate` parameters. diff --git a/languages/tolk/from-func/pack.mdx b/languages/tolk/from-func/pack.mdx index 53636fa9b..aa58a2713 100644 --- a/languages/tolk/from-func/pack.mdx +++ b/languages/tolk/from-func/pack.mdx @@ -357,25 +357,25 @@ data: (bits len) // 0..255 bits of data To express this, you can create your own type and **define a custom serializer**: ```tolk -type VariadicString = slice +type ShortString = slice -fun VariadicString.packToBuilder(self, mutate b: builder) { +fun ShortString.packToBuilder(self, mutate b: builder) { val nBits = self.remainingBitsCount(); b.storeUint(nBits, 8); b.storeSlice(self); } -fun VariadicString.unpackFromSlice(mutate s: slice) { +fun ShortString.unpackFromSlice(mutate s: slice) { val nBits = s.loadUint(8); return s.loadBits(nBits); } ``` -And just use `VariadicString` as a regular type — everywhere: +And just use `ShortString` as a regular type — everywhere: ```tolk -tokenName: VariadicString -fullDomain: Cell +tokenName: ShortString +fullDomain: Cell ``` The compiler inlines your functions every time packing/unpacking takes place. diff --git a/languages/tolk/from-func/stdlib.mdx b/languages/tolk/from-func/stdlib.mdx index c7b3b21ad..5dfe9470d 100644 --- a/languages/tolk/from-func/stdlib.mdx +++ b/languages/tolk/from-func/stdlib.mdx @@ -7,7 +7,7 @@ import { Aside } from '/snippets/aside.jsx'; FunC includes a low-level [standard library](/languages/tolk/from-func/stdlib) in the `stdlib.fc` file, containing `asm` functions closely tied to [TVM instructions](/tvm/instructions). -Tolk provides a standard library based on FunC’s with three main differences. +Tolk provides a standard library based on FunC's with three main differences. 1. It is split into multiple files: `common.tolk`, `tvm-dicts.tolk`, and others. Functions from `common.tolk` are available, while functions from other files require an import: @@ -77,7 +77,7 @@ The table follows the order in which functions appear in `stdlib.fc`. | `t~tpush` | `t.push(v)` | | | `first(t)` or `t.first()` | `t.first()` | | | `at(t,i)` or `t.at(i)` | `t.get(i)`, `t.0`, etc. | | -| `touch(v)` | `v.stackMoveToTop()` | TVM low-level | +| `touch(v)` | `v.stackMoveToTop()` | tvm-lowlevel | | `impure_touch` | _(deleted)_ | | | `single` | _(deleted)_ | | | `unsingle` | _(deleted)_ | | @@ -115,9 +115,9 @@ The table follows the order in which functions appear in `stdlib.fc`. | `dump_stack` | `debug.dumpStack` | | | `get_data` | `contract.getData` | | | `set_data` | `contract.setData` | | -| `get_c3` | `getTvmRegisterC3` | TVM low-level | -| `set_c3` | `setTvmRegisterC3` | TVM low-level | -| `bless` | `transformSliceToContinuation` | TVM low-level | +| `get_c3` | `getTvmRegisterC3` | tvm-lowlevel | +| `set_c3` | `setTvmRegisterC3` | tvm-lowlevel | +| `bless` | `transformSliceToContinuation` | tvm-lowlevel | | `accept_message` | `acceptExternalMessage` | | | `set_gas_limit` | `setGasLimit` | | | `buy_gas` | _(deleted)_ | | @@ -198,9 +198,9 @@ The table follows the order in which functions appear in `stdlib.fc`. | `cdr` | `listGetTail` | lisp-lists | | `new_dict` | `createEmptyMap` | | | `dict_empty?(d)` | `m.isEmpty` | | -| `pfxdict_get?` | `prefixDictGet` | TVM dicts | -| `pfxdict_set?` | `prefixDictSet` | TVM dicts | -| `pfxdict_delete?` | `prefixDictDelete` | TVM dicts | +| `pfxdict_get?` | `prefixDictGet` | tvm-dicts | +| `pfxdict_set?` | `prefixDictSet` | tvm-dicts | +| `pfxdict_delete?` | `prefixDictDelete` | tvm-dicts | | `idict_set_ref` | use native maps | | | `udict_set_ref` | use native maps | | | `idict_get_ref` | use native maps | | @@ -281,7 +281,7 @@ Many FunC functions that used the `~` tilde now mutate the object in Tolk rather All standard library functions are available out of the box. Non-common functions require an `import`, but no external downloads are needed. -Here’s how it works: +Here's how it works: 1. The Tolk compiler locates the stdlib folder by searching predefined paths relative to an executable binary. diff --git a/languages/tolk/overview.mdx b/languages/tolk/overview.mdx index 223688a0c..d2b4f5bc9 100644 --- a/languages/tolk/overview.mdx +++ b/languages/tolk/overview.mdx @@ -5,7 +5,7 @@ sidebarTitle: "Overview" **Tolk** is a next-generation language for developing smart contracts on TON. It replaces FunC with an expressive syntax, a robust type system, and built-in serialization — while generating highly optimized assembly code. -This page introduces Tolk’s _core features, explains how it compares to FunC, and outlines how to get started with the language_ — whether you’re migrating from FunC or writing smart contracts from scratch. +This page introduces Tolk's _core features, explains how it compares to FunC, and outlines how to get started with the language_ — whether you're migrating from FunC or writing smart contracts from scratch. ## Why choose Tolk @@ -27,20 +27,20 @@ If you're starting with Tolk, follow these steps: ``` 1. Follow the [step-by-step guide](/languages/tolk/counter-smart-contract) to build a counter contract. 1. Read the [Tolk language guide](/languages/tolk/language-guide) for a high-level overview of the language. -1. Continue exploring the documentation. When you come across FunC or Tact code, compare it to its Tolk equivalent — you’ll often find it’s cleaner and more declarative. +1. Continue exploring the documentation. When you come across FunC or Tact code, compare it to its Tolk equivalent — you'll often find it's cleaner and more declarative. The TON documentation will continue evolving to support Tolk as the primary smart contract language. ## How to migrate from FunC -If you’ve written contracts in FunC, migrating to Tolk is straightforward — and often results in simpler, more maintainable code. +If you've written contracts in FunC, migrating to Tolk is straightforward — and often results in simpler, more maintainable code. -1. Try building a basic contract in Tolk — for example, a counter using `npm create ton@latest`. You’ll quickly notice the shift from stack logic to expressive constructs with structured types, unions, pattern matching, `toCell()`, and more. +1. Try building a basic contract in Tolk — for example, a counter using `npm create ton@latest`. You'll quickly notice the shift from stack logic to expressive constructs with structured types, unions, pattern matching, `toCell()`, and more. 1. Explore the [Tolk vs FunC benchmarks](https://github.com/ton-blockchain/tolk-bench). These are real-world contracts (Jetton, NFT, Wallet, etc.) migrated from FunC — same logic, but expressed in a cleaner, more expressive style. 1. Read [Tolk vs FunC: in short](/languages/tolk/from-func/in-short) for a quick comparison of syntax and concepts. 1. Use the [FunC-to-Tolk converter](https://github.com/ton-blockchain/convert-func-to-tolk) to migrate existing codebases incrementally. -### Example of a basic Tolk smart contract: +## Example of a basic Tolk smart contract: ```tolk import "utils" @@ -99,7 +99,7 @@ get fun currentCounter(): int { A set of tools is available to support development with Tolk — including IDE support, language services, and migration utilities. - [JetBrains IDE plugin](https://github.com/ton-blockchain/intellij-ton). Adds support for Tolk, FunC, Fift, and TL-B. - \[VS Code extension]\([https://marketplace.visualstudio.com/items?itemName=ton-core.-](https://marketplace.visualstudio.com/items?itemName=ton-core.-) vscode-ton). Provides Tolk support and language features. +- [VS Code extension](https://marketplace.visualstudio.com/items?itemName=ton-core.vscode-ton). Provides Tolk support and language features. - [Language server](https://github.com/ton-blockchain/ton-language-server). Works with any LSP-compatible editor. - [FunC-to-Tolk converter](https://github.com/ton-blockchain/convert-func-to-tolk). Migrates entire projects with a single `npx` command. - [`tolk-js`](https://github.com/ton-blockchain/tolk-js). WASM wrapper for the Tolk compiler is integrated into Blueprint. From df0525adc49ad3127de073bb1fc3883b8d962ddb Mon Sep 17 00:00:00 2001 From: tolk-vm Date: Mon, 10 Nov 2025 14:05:31 +0300 Subject: [PATCH 02/11] [Tolk] Created page structure for the future Tolk documentation --- docs.json | 73 +- languages/tolk/basic-syntax.mdx | 17 + languages/tolk/counter-smart-contract.mdx | 133 ---- languages/tolk/environment-setup.mdx | 64 -- languages/tolk/features/asm-functions.mdx | 4 + .../auto-serialization.mdx} | 4 +- .../tolk/features/compiler-optimizations.mdx | 4 + languages/tolk/features/contract-getters.mdx | 3 + languages/tolk/features/contract-storage.mdx | 4 + .../{from-func => features}/lazy-loading.mdx | 2 +- languages/tolk/features/message-handling.mdx | 4 + .../message-sending.mdx} | 21 +- languages/tolk/features/standard-library.mdx | 5 + languages/tolk/idioms-conventions.mdx | 5 + languages/tolk/language-guide.mdx | 654 ------------------ languages/tolk/overview.mdx | 20 +- languages/tolk/syntax/conditions-loops.mdx | 4 + languages/tolk/syntax/exceptions.mdx | 4 + languages/tolk/syntax/functions-methods.mdx | 4 + languages/tolk/syntax/imports.mdx | 4 + languages/tolk/syntax/mutability.mdx | 4 + languages/tolk/syntax/operators.mdx | 4 + languages/tolk/syntax/pattern-matching.mdx | 4 + languages/tolk/syntax/structures-fields.mdx | 4 + languages/tolk/syntax/variables.mdx | 4 + languages/tolk/types/address.mdx | 4 + languages/tolk/types/aliases.mdx | 4 + languages/tolk/types/booleans.mdx | 4 + languages/tolk/types/callables.mdx | 4 + languages/tolk/types/cells.mdx | 4 + languages/tolk/types/enums.mdx | 4 + languages/tolk/types/generics.mdx | 5 + languages/tolk/types/list-of-types.mdx | 5 + languages/tolk/types/maps.mdx | 5 + languages/tolk/types/nullable.mdx | 5 + languages/tolk/types/numbers.mdx | 4 + languages/tolk/types/strings.mdx | 5 + languages/tolk/types/structures.mdx | 4 + languages/tolk/types/tensors.mdx | 5 + languages/tolk/types/tuples.mdx | 5 + .../tolk/types/type-checks-and-casts.mdx | 4 + languages/tolk/types/unions.mdx | 4 + languages/tolk/types/void-never.mdx | 4 + 43 files changed, 233 insertions(+), 899 deletions(-) create mode 100644 languages/tolk/basic-syntax.mdx delete mode 100644 languages/tolk/counter-smart-contract.mdx delete mode 100644 languages/tolk/environment-setup.mdx create mode 100644 languages/tolk/features/asm-functions.mdx rename languages/tolk/{from-func/pack.mdx => features/auto-serialization.mdx} (99%) create mode 100644 languages/tolk/features/compiler-optimizations.mdx create mode 100644 languages/tolk/features/contract-getters.mdx create mode 100644 languages/tolk/features/contract-storage.mdx rename languages/tolk/{from-func => features}/lazy-loading.mdx (99%) create mode 100644 languages/tolk/features/message-handling.mdx rename languages/tolk/{from-func/create-message.mdx => features/message-sending.mdx} (96%) create mode 100644 languages/tolk/features/standard-library.mdx create mode 100644 languages/tolk/idioms-conventions.mdx delete mode 100644 languages/tolk/language-guide.mdx create mode 100644 languages/tolk/syntax/conditions-loops.mdx create mode 100644 languages/tolk/syntax/exceptions.mdx create mode 100644 languages/tolk/syntax/functions-methods.mdx create mode 100644 languages/tolk/syntax/imports.mdx create mode 100644 languages/tolk/syntax/mutability.mdx create mode 100644 languages/tolk/syntax/operators.mdx create mode 100644 languages/tolk/syntax/pattern-matching.mdx create mode 100644 languages/tolk/syntax/structures-fields.mdx create mode 100644 languages/tolk/syntax/variables.mdx create mode 100644 languages/tolk/types/address.mdx create mode 100644 languages/tolk/types/aliases.mdx create mode 100644 languages/tolk/types/booleans.mdx create mode 100644 languages/tolk/types/callables.mdx create mode 100644 languages/tolk/types/cells.mdx create mode 100644 languages/tolk/types/enums.mdx create mode 100644 languages/tolk/types/generics.mdx create mode 100644 languages/tolk/types/list-of-types.mdx create mode 100644 languages/tolk/types/maps.mdx create mode 100644 languages/tolk/types/nullable.mdx create mode 100644 languages/tolk/types/numbers.mdx create mode 100644 languages/tolk/types/strings.mdx create mode 100644 languages/tolk/types/structures.mdx create mode 100644 languages/tolk/types/tensors.mdx create mode 100644 languages/tolk/types/tuples.mdx create mode 100644 languages/tolk/types/type-checks-and-casts.mdx create mode 100644 languages/tolk/types/unions.mdx create mode 100644 languages/tolk/types/void-never.mdx diff --git a/docs.json b/docs.json index 36581be18..6cb0182ab 100644 --- a/docs.json +++ b/docs.json @@ -395,21 +395,68 @@ "tag": "recommended", "pages": [ "languages/tolk/overview", + "languages/tolk/basic-syntax", + "languages/tolk/idioms-conventions", + { + "group": "Type system", + "pages": [ + "languages/tolk/types/list-of-types", + "languages/tolk/types/numbers", + "languages/tolk/types/booleans", + "languages/tolk/types/address", + "languages/tolk/types/cells", + "languages/tolk/types/strings", + "languages/tolk/types/structures", + "languages/tolk/types/aliases", + "languages/tolk/types/generics", + "languages/tolk/types/enums", + "languages/tolk/types/nullable", + "languages/tolk/types/unions", + "languages/tolk/types/tensors", + "languages/tolk/types/tuples", + "languages/tolk/types/maps", + "languages/tolk/types/callables", + "languages/tolk/types/void-never", + "languages/tolk/types/type-checks-and-casts" + ] + }, + { + "group": "Syntax details", + "pages": [ + "languages/tolk/syntax/variables", + "languages/tolk/syntax/conditions-loops", + "languages/tolk/syntax/exceptions", + "languages/tolk/syntax/functions-methods", + "languages/tolk/syntax/structures-fields", + "languages/tolk/syntax/pattern-matching", + "languages/tolk/syntax/mutability", + "languages/tolk/syntax/operators", + "languages/tolk/syntax/imports" + ] + }, + { + "group": "Language features", + "pages": [ + "languages/tolk/features/message-handling", + "languages/tolk/features/contract-storage", + "languages/tolk/features/contract-getters", + "languages/tolk/features/message-sending", + "languages/tolk/features/auto-serialization", + "languages/tolk/features/lazy-loading", + "languages/tolk/features/standard-library", + "languages/tolk/features/asm-functions", + "languages/tolk/features/compiler-optimizations" + ] + }, { "group": "Migrating from FunC", "pages": [ "languages/tolk/from-func/in-short", "languages/tolk/from-func/in-detail", "languages/tolk/from-func/mutability", - "languages/tolk/from-func/stdlib", - "languages/tolk/from-func/create-message", - "languages/tolk/from-func/lazy-loading", - "languages/tolk/from-func/pack" + "languages/tolk/from-func/stdlib" ] }, - "languages/tolk/environment-setup", - "languages/tolk/counter-smart-contract", - "languages/tolk/language-guide", "languages/tolk/changelog" ] }, @@ -916,17 +963,17 @@ }, { "source": "/v3/documentation/smart-contracts/tolk/environment-setup", - "destination": "/languages/tolk/environment-setup", + "destination": "contract-dev/first-smart-contract", "permanent": true }, { "source": "/v3/documentation/smart-contracts/tolk/counter-smart-contract", - "destination": "languages/tolk/counter-smart-contract", + "destination": "contract-dev/first-smart-contract", "permanent": true }, { "source": "/v3/documentation/smart-contracts/tolk/language-guide", - "destination": "languages/tolk/language-guide", + "destination": "languages/tolk/basic-syntax", "permanent": true }, { @@ -951,17 +998,17 @@ }, { "source": "/v3/documentation/smart-contracts/tolk/tolk-vs-func/pack-to-from-cells", - "destination": "languages/tolk/from-func/pack", + "destination": "languages/tolk/features/auto-serialization", "permanent": true }, { "source": "/v3/documentation/smart-contracts/tolk/tolk-vs-func/create-message", - "destination": "languages/tolk/from-func/create-message", + "destination": "languages/tolk/features/message-sending", "permanent": true }, { "source": "/v3/documentation/smart-contracts/tolk/tolk-vs-func/lazy-loading", - "destination": "languages/tolk/from-func/lazy-loading", + "destination": "languages/tolk/features/lazy-loading", "permanent": true }, { diff --git a/languages/tolk/basic-syntax.mdx b/languages/tolk/basic-syntax.mdx new file mode 100644 index 000000000..f2903dc27 --- /dev/null +++ b/languages/tolk/basic-syntax.mdx @@ -0,0 +1,17 @@ +--- +title: "Tolk basic syntax" +sidebarTitle: "Basic syntax" +--- + +semicolons + +variables + +function + +struct + +method + +contract getter + diff --git a/languages/tolk/counter-smart-contract.mdx b/languages/tolk/counter-smart-contract.mdx deleted file mode 100644 index 2c0d583d9..000000000 --- a/languages/tolk/counter-smart-contract.mdx +++ /dev/null @@ -1,133 +0,0 @@ ---- -title: "Simple contract" ---- - -When you create a counter contract using `npm create ton@latest`, it generates the following file: - -```tolk title="counter.tolk" -tolk 1.0 - -struct Storage { - ... -} - -fun Storage.load() ... -fun Storage.save(self) ... - -struct (0x7e8764ef) IncreaseCounter { - ... -} -struct (0x3a752f06) ResetCounter { - ... -} - -type AllowedMessage = IncreaseCounter | ResetCounter - -fun onInternalMessage(in: InMessage) { - val msg = lazy AllowedMessage.fromSlice(in.body); - - match (msg) { - IncreaseCounter => { - ... - } - ResetCounter => { - ... - } - else => { - ... - } - } -} - -fun onBouncedMessage(in: InMessageBounced) { -} - -get fun currentCounter() ... -get fun initialId() ... -``` - -Let's explore what's happening here. - -## Smart contract data - -First, we declare the storage structure. It corresponds exactly to the format used when deploying from the TypeScript client `wrappers/Counter.ts`. - -```tolk -// In `Counter.ts`, the function `counterConfigToCell` -// manually constructs two uint32 values — which correspond here: -struct Storage { - id: uint32 - counter: uint32 -} - -// Methods to load from and store to persistent storage - -fun Storage.load() { - return Storage.fromCell(contract.getData()) -} -fun Storage.save(self) { - contract.setData(self.toCell()) -} -``` - -## Handling incoming messages - -When a client sends a message — for example, using the `sendIncrease` function in `wrappers/Counter.ts`— the contract processes it using this function: - -```tolk -fun onInternalMessage(in: InMessage) { - val msg = lazy AllowedMessage.fromSlice(in.body); - - match (msg) { - IncreaseCounter => { - // ... - } - ResetCounter => { - // ... - } - else => { - // ... unrecognized messages - } - } -} -``` - -Take a look at the `sendIncrease` function in TypeScript, which constructs a message containing `OP_INCREASE`, `queryId`, and `increaseBy`. - -The corresponding struct in `counter.tolk` defines the format of this incoming message: - -```tolk -struct (0x7e8764ef) IncreaseCounter { - queryId: uint64 - increaseBy: uint32 -} -``` - -## Sending messages - -This contract doesn't send any messages. -To learn how message construction works in Tolk, see the [Create message](/languages/tolk/from-func/create-message) article. - -## Handling internal bounced messages - -**Bounced messages** are messages that you _send_, but the receiver fails to process — so they are returned to you. - -```tolk -fun onBouncedMessage(in: InMessageBounced) { -} -``` - -Since this contract only accepts messages like `IncreaseCounter` and `ResetCounter` and doesn't send any, you can leave this function empty or remove it entirely. - -## GET methods - -GET methods, also called _contract getters_, run **off-chain** and typically return data from the storage. - -```tolk -get fun currentCounter(): int { - val storage = lazy Storage.load(); - return storage.counter; -} -``` - -The `getCounter` function in `wrappers/Counter.ts` calls this exact getter from `counter.tolk`. diff --git a/languages/tolk/environment-setup.mdx b/languages/tolk/environment-setup.mdx deleted file mode 100644 index 4e5e9ef0c..000000000 --- a/languages/tolk/environment-setup.mdx +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: "Environment setup" ---- - -To start working with Tolk, you need a development environment. - -We recommend using **Blueprint**, a tool for writing, testing, and deploying smart contracts on TON. - -## Create a new TON project - -> **Prerequisite:** Node.js v16 or higher - -1. Create a new TON project: - ```bash - npm create ton@latest - ``` -1. Enter any project name and select **"A simple counter contract (Tolk)"** when prompted by Blueprint. - -## Project structure - -Here's a quick look at the typical project layout: - -``` -my-first-contract/ -├── build/ # Compiled smart contract bytecode -├── contracts/ # Smart contract sources (e.g., counter.tolk) -├── scripts/ # TypeScript deployment scripts -├── tests/ # TypeScript tests -├── wrappers/ # TypeScript wrappers for contracts -``` - -## Build the project - -```bash -npx blueprint build -# or -yarn blueprint build -``` - -Blueprint compiles the contract and displays the resulting bytecode, along with its hash. The output is saved in the `build/` directory. - -## Run tests - -```bash -npx blueprint test -# or -yarn blueprint test -``` - -This runs unit tests from the `tests/` directory. - -## Deploy or run a script - -```bash -npx blueprint run -# or -yarn blueprint run -``` - -Use this command to deploy your contract to Mainnet, Testnet, or MyLocalTon. - -## IDE support - -Tolk is supported in IntelliJ-based IDEs using the [TON plugin](https://github.com/ton-blockchain/intellij-ton) and in Visual Studio Code through the [TON extension](https://github.com/ton-blockchain/ton-language-server). diff --git a/languages/tolk/features/asm-functions.mdx b/languages/tolk/features/asm-functions.mdx new file mode 100644 index 000000000..49c82dfd9 --- /dev/null +++ b/languages/tolk/features/asm-functions.mdx @@ -0,0 +1,4 @@ +--- +title: "Assembler functions" +--- + diff --git a/languages/tolk/from-func/pack.mdx b/languages/tolk/features/auto-serialization.mdx similarity index 99% rename from languages/tolk/from-func/pack.mdx rename to languages/tolk/features/auto-serialization.mdx index aa58a2713..c0fddc10b 100644 --- a/languages/tolk/from-func/pack.mdx +++ b/languages/tolk/features/auto-serialization.mdx @@ -653,7 +653,7 @@ val reply = createMessage({ reply.send(SEND_MODE_REGULAR); ``` -Continue reading here: [Universal createMessage](/languages/tolk/from-func/create-message). +Continue reading here: [Universal createMessage](/languages/tolk/features/message-sending). ## Not "fromCell" but "lazy fromCell" @@ -675,4 +675,4 @@ get fun getPublicKey() { } ``` -Continue reading here: [lazy loading, partial loading](/languages/tolk/from-func/lazy-loading). +Continue reading here: [lazy loading, partial loading](/languages/tolk/features/lazy-loading). diff --git a/languages/tolk/features/compiler-optimizations.mdx b/languages/tolk/features/compiler-optimizations.mdx new file mode 100644 index 000000000..07e541796 --- /dev/null +++ b/languages/tolk/features/compiler-optimizations.mdx @@ -0,0 +1,4 @@ +--- +title: "Compiler optimizations" +--- + diff --git a/languages/tolk/features/contract-getters.mdx b/languages/tolk/features/contract-getters.mdx new file mode 100644 index 000000000..1f89f595e --- /dev/null +++ b/languages/tolk/features/contract-getters.mdx @@ -0,0 +1,3 @@ +--- +title: "Contract getters" +--- diff --git a/languages/tolk/features/contract-storage.mdx b/languages/tolk/features/contract-storage.mdx new file mode 100644 index 000000000..d1ebc8f5e --- /dev/null +++ b/languages/tolk/features/contract-storage.mdx @@ -0,0 +1,4 @@ +--- +title: "Contract storage" +--- + diff --git a/languages/tolk/from-func/lazy-loading.mdx b/languages/tolk/features/lazy-loading.mdx similarity index 99% rename from languages/tolk/from-func/lazy-loading.mdx rename to languages/tolk/features/lazy-loading.mdx index bf21b9d93..242d4aead 100644 --- a/languages/tolk/from-func/lazy-loading.mdx +++ b/languages/tolk/features/lazy-loading.mdx @@ -108,7 +108,7 @@ Since lazy `match` for a union is done by inspecting the prefix (opcode), you ca In FunC contracts, a common pattern was to **ignore empty messages**: ```tolk -// FunC-style +// old style, before Tolk with `lazy` and `match` if (msgBody.isEmpty()) { return; // ignore empty messages } diff --git a/languages/tolk/features/message-handling.mdx b/languages/tolk/features/message-handling.mdx new file mode 100644 index 000000000..e33cb193f --- /dev/null +++ b/languages/tolk/features/message-handling.mdx @@ -0,0 +1,4 @@ +--- +title: "Handling messages" +--- + diff --git a/languages/tolk/from-func/create-message.mdx b/languages/tolk/features/message-sending.mdx similarity index 96% rename from languages/tolk/from-func/create-message.mdx rename to languages/tolk/features/message-sending.mdx index 1d7f89a8f..8d252e11c 100644 --- a/languages/tolk/from-func/create-message.mdx +++ b/languages/tolk/features/message-sending.mdx @@ -1,23 +1,8 @@ --- -title: "Constructing messages" +title: "Sending messages" --- -In FunC, message cells were composed manually: - -```func -cell m = begin_cell() - .store_uint(0x10, 6) - .store_slice(sender_address) - .store_coins(50000000) - .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) - .store_uint(0x178d4519, 32) - .store_uint(query_id, 64) - ... - .end_cell(); -send_raw_message(m, 0); -``` - -In Tolk, there is a high-level function: +Tolk has a high-level function `createMessage`. In practice, you'll immediately `send` its result: ```tolk val reply = createMessage({ @@ -41,7 +26,7 @@ reply.send(SEND_MODE_REGULAR); ## The concept is based on union types -There is a variety of interacting between contracts. When you explore FunC implementations, you notice that: +There is a variety of interacting between contracts. - sometimes, you "send to an address (slice)" - ... but sometimes, you "build the address (builder) manually" diff --git a/languages/tolk/features/standard-library.mdx b/languages/tolk/features/standard-library.mdx new file mode 100644 index 000000000..8adbff8c7 --- /dev/null +++ b/languages/tolk/features/standard-library.mdx @@ -0,0 +1,5 @@ +--- +title: "Standard library of Tolk" +sidebarTitle: "Standard library" +--- + diff --git a/languages/tolk/idioms-conventions.mdx b/languages/tolk/idioms-conventions.mdx new file mode 100644 index 000000000..c7201feb9 --- /dev/null +++ b/languages/tolk/idioms-conventions.mdx @@ -0,0 +1,5 @@ +--- +title: "Idioms and conventions" +sidebarTitle: "Idioms and conventions" +--- + diff --git a/languages/tolk/language-guide.mdx b/languages/tolk/language-guide.mdx deleted file mode 100644 index f67291934..000000000 --- a/languages/tolk/language-guide.mdx +++ /dev/null @@ -1,654 +0,0 @@ ---- -title: "Language guide" ---- - -## Basic syntax - -### Comments - -Tolk uses traditional C-style syntax for comments: - -```tolk -// Single-line comment - -/* - Multi-line comment - can span multiple lines -*/ -``` - -### Identifiers - -Identifiers must start with `[a-zA-Z$_]` — that is, a letter, underscore, or dollar sign — and may continue with characters from `[a-zA-Z0-9$_]`. - -As in most programming languages, **camelCase** is the preferred naming convention. - -```tolk -var justSomeVariable = 123; -``` - -## Functions - -### Function declaration - -Use the `fun` keyword with TypeScript-like syntax: - -```tolk -fun functionName(param1: type1, param2: type2): returnType { - // function body -} -``` - -Examples: - -```tolk -fun parseData(cs: slice): cell { } -fun loadStorage(): (cell, int) { } -fun main() { ... } -``` - -If the return type is omitted, it's auto-inferred. - -### Generic functions - -Tolk supports generic functions: - -```tolk -fun swap(a: T1, b: T2): (T2, T1) { - return (b, a); -} -``` - -### Default parameters - -Function parameters can have default values: - -```tolk -fun increment(x: int, by: int = 1): int { - return x + by; -} -``` - -### GET methods - -Contract getters use the `get fun` syntax: - -```tolk -get fun seqno(): int { - return 1; -} -``` - -## Variables and types - -### Variable declaration - -Declare variables with `var` for mutable variables and `val` for immutable ones: - -```tolk -var mutableVar: int = 10; -val immutableVar: int = 20; -``` - -Type annotations are optional for local variables: - -```tolk -var i = 10; // int inferred -var b = beginCell(); // builder inferred -``` - -### Variable scope - -Variables cannot be redeclared within the same scope: - -```tolk -var a = 10; -var a = 20; // Error! Use: a = 20; - -if (true) { - var a = 30; // OK, different scope -} -``` - -## Control flow - -### Conditional statements - -```tolk -if (condition) { - // code -} else if (anotherCondition) { - // code -} else { - // code -} -``` - -### Loops - -#### While loop - -```tolk -while (condition) { - // code -} -``` - -#### Do-while loop - -```tolk -do { - // code -} while (condition); -``` - -#### Repeat loop - -```tolk -repeat (10) { - // body -} -``` - -## Type system - -### Basic types - -- `int` - integer; fixed-width variants like `int32` and `uint64` are also supported -- `bool` - boolean (`true`/`false`). **Note:** in TVM, `true` is represented as `-1`, **not** `1` -- `cell` - a TVM cell -- `slice` - a TVM slice; you can also use `bitsN`. (e.g. `bits512` for storing a signature) -- `builder` - a TVM builder -- `address` - internal (standard) address -- `any_address` — internal/external/none address -- `coins` - Toncoin or coin amounts -- `void` - no return value -- `never` - function never returns (always throws an exception) - -### Fixed-width integers - -```tolk -var smallInt: int32 = 42; -var bigInt: uint64 = 1000000; -``` - -### Boolean type - -The Boolean type is distinct from integers: - -```tolk -var valid: bool = true; -var result: bool = (x > 0); - -if (valid) { // accepts bool - // code -} - -// Cast to int if needed -var intValue = valid as int; // -1 for true, 0 for false -``` - -### Nullable types - -Nullable types are denoted by `?`: - -```tolk -var maybeInt: int? = null; -var maybeCell: cell? = someCell; - -if (maybeInt != null) { - // Smart cast: maybeInt is now int - var result = maybeInt + 5; -} -``` - -### Union types - -Union types allow a value to have multiple possible types. Typically, you handle them using `match` by type: - -```tolk -fun processValue(value: int | slice) { - match (value) { - int => { - // Got integer - } - slice => { - // Got slice - } - } -} -``` - -Alternatively, you can test a union value using the `is` or `!is` operators: - -```tolk -fun processValue(value: int | slice) { - if (value is slice) { - // call methods for slice - return; - } - // value is int - return value * 2; -} -``` - -### Tuple types - -Tuples with explicit types: - -```tolk -var data: [int, slice, bool] = [42, mySlice, true]; -``` - -### Tensor types - -Tensors are similar to tuples but stored on the stack: - -```tolk -var coords: (int, int) = (10, 20); -var x = coords.0; // Access first element -var y = coords.1; // Access second element -``` - -### Type aliases - -Create aliases to improve code clarity: - -```tolk -type UserId = int32 -type MaybeOwnerHash = bits256? - -fun calcHash(id: UserId): MaybeOwnerHash { ... } -``` - -### Address type - -A dedicated type for blockchain addresses: - -```tolk -val addr = address("EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF"); -val workchain = addr.getWorkchain(); -``` - -## Tensors - -### Indexed access - -Access elements using dot `.` notation: - -```tolk -var t = (5, someSlice, someBuilder); -t.0 = 10; // Modify first element -t.1; // Access second element -``` - -### Tuples - -```tolk -var t = [5, someSlice, someBuilder]; -t.0 = 10; // asm "SETINDEX" -var first = t.0; // asm "INDEX" -``` - -## Error handling - -### Throw and assert - -Simplified error handling: - -```tolk -throw 404; // Throw exception with code -throw (404, "Not found"); // Throw with data - -assert (condition) throw 404; -assert (!condition) throw 404; -``` - -### Try-catch - -```tolk -try { - riskyOperation(); -} catch (excNo, arg) { - // Handle exception -} -``` - -## Structures - -### Struct declaration - -```tolk -struct Point { - x: int - y: int -} -``` - -### Creating objects - -```tolk -var p: Point = { x: 10, y: 20 }; -var p2 = Point { x: 5, y: 15 }; -``` - -### Default values - -```tolk -struct Config { - timeout: int = 3600 - enabled: bool = true -} - -var config: Config = {}; // Uses defaults -``` - -### Generic structures - -```tolk -struct Container { - value: T - isEmpty: bool -} - -var intContainer: Container = { value: 42, isEmpty: false }; -``` - -### Field modifiers - -- `private` field — accessible only within methods -- `readonly` field — immutable after object creation - -```tolk -struct PosInTuple { - private readonly t: tuple - curIndex: int -} - -fun PosInTuple.last(mutate self) { - // `t` is visible only in methods - // and can not be modified - self.curIndex = self.t.size() - 1; -} -``` - -## Methods - -### Instance methods - -Methods are implemented as extension functions that accept `self`: - -```tolk -fun Point.distanceFromOrigin(self): int { - return sqrt(self.x * self.x + self.y * self.y); -} -``` - -### Mutating methods - -Use the `mutate` keyword for methods that modify the receiver: - -```tolk -fun Point.moveBy(mutate self, dx: int, dy: int) { - self.x += dx; - self.y += dy; -} -``` - -### Methods for any type - -```tolk -fun int.isZero(self) { - return self == 0; -} - -fun T.copy(self): T { - return self; -} -``` - -### Chaining methods - -Methods can return `self` to enable method chaining: - -```tolk -fun builder.storeInt32(mutate self, value: int32): self { - return self.storeInt(value, 32); -} -``` - -## Enums - -```tolk -// will be 0 1 2 -enum Color { - Red - Green - Blue -} -``` - -They are: - -- similar to TypeScript/C++ enums -- distinct type, not just `int` -- checked on deserialization -- allowed in `throw` and `assert` - -### Enums syntax - -Enum members can be separated by `,` or by `;` or by a newline — like struct fields. - -Like in TypeScript and C++, you can manually specify a value, the following will be auto-calculated. - -```tolk -enum Mode { - Foo = 256, - Bar, // implicitly 257 -} -``` - -### Enums usage: they are distinct types - -Since enums are types, you can - -- declare variables and parameters -- declare methods for an enum -- use them in struct fields, in unions, in generics, etc. - -```tolk -struct Gradient { - from: Color - to: Color? = null -} - -fun Color.isRed(self) { - return self == Color.Red -} - -var g: Gradient = { from: Color.Blue }; -g.from.isRed(); // false -``` - -Enums are integers under the hood. They can be cast to `int` and back with `as` operator. - -### Serialization of enums - -You can manually specify `:intN`. Otherwise, the compiler will auto-calculate it. - -```tolk -// will be (un)packed as `int8` -enum Role1: int8 { Admin, User, Guest } - -// will (un)packed as `uint2` (auto-calculated to fit all values) -enum Color { Red, Green, Blue } -``` - -On deserialization, an input value is checked for correctness. E.g., for `Role`, if input\<0 or input>2, an exception "5 (integer out of range)" will be thrown. - -## Imports and modules - -### Import syntax - -```tolk -import "another" -import "@stdlib/gas-payments" -``` - -## Advanced features - -### TVM assembler functions - -You can implement functions directly in TVM assembler: - -```tolk -@pure -fun third(t: tuple): X - asm "THIRD" -``` - -### Function attributes - -Functions can have attributes using the `@` syntax: - -```tolk -@inline -fun fastFunction() {} - -@inline_ref -fun load_data() {} - -@deprecated -fun oldFunction() {} - -@method_id(1666) -fun afterCodeUpgrade(oldCode: continuation) {} -``` - -### Trailing commas - -Tolk supports trailing commas in tensors, tuples, function calls, and parameters: - -```tolk -var items = ( - totalSupply, - verifiedCode, - validatorsList, -); -``` - -### Optional semicolons - -Semicolons are optional for the last statement in a block: - -```tolk -fun f() { - doSomething(); - return result // Valid without semicolon -} -``` - -Semicolons are also optional for top-level declarations such as constants, type aliases, and struct fields. - -### Compile-time functions - -String-processing functions execute at compile time: - -```tolk -const BASIC_ADDR = address("EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF"); -const HASH = stringSha256_32("transfer(slice, int)"); -``` - -Available compile-time functions include: `stringCrc32`, `stringCrc16`, `stringSha256`, `stringSha256_32`, `stringHexToSlice`, and `stringToBase256`. - -### Toncoin amounts - -Use human-readable Toncoin amounts: - -```tolk -val cost = ton("0.05"); // 50,000,000 nanotons -const ONE_TON = ton("1"); -``` - -### Smart casts - -Automatic type narrowing: - -```tolk -if (value != null) { - // value is automatically cast from T? to T - value.someMethod(); -} -``` - -### Non-null assertion - -Use `!` when you are sure a value is not null: - -```tolk -fun processCell(maybeCell: cell?) { - if (hasCell) { - processCell(maybeCell!); // bypass nullability check - } -} -``` - -### Auto-packing - -Structures can be automatically packed to and unpacked from cells: - -```tolk -struct Point { - x: int8 - y: int8 -} - -var point: Point = { x: 10, y: 20 }; -var cell = point.toCell(); // Auto-pack -var restored = Point.fromCell(cell); // Auto-unpack -``` - -Deep dive: [Auto-packing](/languages/tolk/from-func/pack). - -### "Lazy loading" from cells: unpack only requested fields - -```tolk -val st = lazy Storage.load(); -// the compiler skips everything and loads only what you access -return st.publicKey; -``` - -Deep dive: [Lazy loading](/languages/tolk/from-func/lazy-loading). - -### Universal message composition - -Create messages using a high-level syntax: - -```tolk -val reply = createMessage({ - bounce: BounceMode.NoBounce, - value: ton("0.05"), - dest: senderAddress, - body: RequestedInfo { ... } -}); -reply.send(SEND_MODE_REGULAR); -``` - -Deep dive: [Sending messages](/languages/tolk/from-func/create-message). - -## Standard library - -Common functions are available by default: - -```tolk -// Common functions always available -var time = blockchain.logicalTime(); -``` - -While more specific ones require importing (IDE suggests you): - -```tolk -import "@stdlib/gas-payments" - -var fee = calculateStorageFee(...); -``` diff --git a/languages/tolk/overview.mdx b/languages/tolk/overview.mdx index d2b4f5bc9..c29669a4f 100644 --- a/languages/tolk/overview.mdx +++ b/languages/tolk/overview.mdx @@ -10,26 +10,18 @@ This page introduces Tolk's _core features, explains how it compares to FunC, an ## Why choose Tolk 1. Clean, expressive syntax inspired by TypeScript and Rust. -1. Built-in primitives for TON, including [_auto‑packed structs_](/languages/tolk/language-guide#auto-packing) [(to/from cells)](/languages/tolk/from-func/pack), pattern matching, and first-class message handling. +1. Built-in primitives for TON, including structures, auto‑packing to/from cells, pattern matching, and first-class message handling. 1. Strong static type system with _null safety, type aliases, union types, and generics_ for safer, clearer code. -1. Gas‑efficient by design — see [benchmarks](https://github.com/ton-blockchain/tolk-bench) and [comparison](/languages/tolk/from-func/in-short); features like **[Lazy loading](/languages/tolk/from-func/lazy-loading)** can reduce costs. +1. Gas‑efficient by design — see [benchmarks](https://github.com/ton-blockchain/tolk-bench) and [comparison](/languages/tolk/from-func/in-short); features like **[Lazy loading](/languages/tolk/features/lazy-loading)** can reduce costs. 1. Low-level control is available when needed, with direct access to the TVM stack and instructions. ## Getting started with Tolk -If you're starting with Tolk, follow these steps: +To get familiar with Tolk, follow the ["Your first smart contract"](/contract-dev/first-smart-contract) article. +It contains required steps for setting up an environment and creating a counter contract from a template, as well as explanation of every step. -1. Explore the [TON smart contract documentation](/contract-dev/first-smart-contract) to learn the fundamentals. -1. [Set up](/languages/tolk/environment-setup) your first Tolk counter project: - - ```bash - npm create ton@latest - ``` -1. Follow the [step-by-step guide](/languages/tolk/counter-smart-contract) to build a counter contract. -1. Read the [Tolk language guide](/languages/tolk/language-guide) for a high-level overview of the language. -1. Continue exploring the documentation. When you come across FunC or Tact code, compare it to its Tolk equivalent — you'll often find it's cleaner and more declarative. - -The TON documentation will continue evolving to support Tolk as the primary smart contract language. +Later on, return to this section and start exploring [Basic syntax](/languages/tolk/basic-syntax), [Type system](/languages/tolk/types/list-of-types), +and other points from the left menu. ## How to migrate from FunC diff --git a/languages/tolk/syntax/conditions-loops.mdx b/languages/tolk/syntax/conditions-loops.mdx new file mode 100644 index 000000000..dd446ecc9 --- /dev/null +++ b/languages/tolk/syntax/conditions-loops.mdx @@ -0,0 +1,4 @@ +--- +title: "Conditions and loops" +--- + diff --git a/languages/tolk/syntax/exceptions.mdx b/languages/tolk/syntax/exceptions.mdx new file mode 100644 index 000000000..7d5ef9a02 --- /dev/null +++ b/languages/tolk/syntax/exceptions.mdx @@ -0,0 +1,4 @@ +--- +title: "Exceptions, throw, assert" +--- + diff --git a/languages/tolk/syntax/functions-methods.mdx b/languages/tolk/syntax/functions-methods.mdx new file mode 100644 index 000000000..38c41d748 --- /dev/null +++ b/languages/tolk/syntax/functions-methods.mdx @@ -0,0 +1,4 @@ +--- +title: "Functions and methods" +--- + diff --git a/languages/tolk/syntax/imports.mdx b/languages/tolk/syntax/imports.mdx new file mode 100644 index 000000000..e0115e070 --- /dev/null +++ b/languages/tolk/syntax/imports.mdx @@ -0,0 +1,4 @@ +--- +title: "Imports and name resolution" +--- + diff --git a/languages/tolk/syntax/mutability.mdx b/languages/tolk/syntax/mutability.mdx new file mode 100644 index 000000000..787b6e6e0 --- /dev/null +++ b/languages/tolk/syntax/mutability.mdx @@ -0,0 +1,4 @@ +--- +title: "Mutability" +--- + diff --git a/languages/tolk/syntax/operators.mdx b/languages/tolk/syntax/operators.mdx new file mode 100644 index 000000000..f6610860d --- /dev/null +++ b/languages/tolk/syntax/operators.mdx @@ -0,0 +1,4 @@ +--- +title: "Operators" +--- + diff --git a/languages/tolk/syntax/pattern-matching.mdx b/languages/tolk/syntax/pattern-matching.mdx new file mode 100644 index 000000000..e9a1fbb41 --- /dev/null +++ b/languages/tolk/syntax/pattern-matching.mdx @@ -0,0 +1,4 @@ +--- +title: "Pattern matching" +--- + diff --git a/languages/tolk/syntax/structures-fields.mdx b/languages/tolk/syntax/structures-fields.mdx new file mode 100644 index 000000000..47597fae6 --- /dev/null +++ b/languages/tolk/syntax/structures-fields.mdx @@ -0,0 +1,4 @@ +--- +title: "Structures and fields" +--- + diff --git a/languages/tolk/syntax/variables.mdx b/languages/tolk/syntax/variables.mdx new file mode 100644 index 000000000..422121d30 --- /dev/null +++ b/languages/tolk/syntax/variables.mdx @@ -0,0 +1,4 @@ +--- +title: "Variables" +--- + diff --git a/languages/tolk/types/address.mdx b/languages/tolk/types/address.mdx new file mode 100644 index 000000000..626502c00 --- /dev/null +++ b/languages/tolk/types/address.mdx @@ -0,0 +1,4 @@ +--- +title: "Address" +--- + diff --git a/languages/tolk/types/aliases.mdx b/languages/tolk/types/aliases.mdx new file mode 100644 index 000000000..ce13c93a5 --- /dev/null +++ b/languages/tolk/types/aliases.mdx @@ -0,0 +1,4 @@ +--- +title: "Type aliases" +--- + diff --git a/languages/tolk/types/booleans.mdx b/languages/tolk/types/booleans.mdx new file mode 100644 index 000000000..cfeb3756e --- /dev/null +++ b/languages/tolk/types/booleans.mdx @@ -0,0 +1,4 @@ +--- +title: "Booleans" +--- + diff --git a/languages/tolk/types/callables.mdx b/languages/tolk/types/callables.mdx new file mode 100644 index 000000000..a7ff0f488 --- /dev/null +++ b/languages/tolk/types/callables.mdx @@ -0,0 +1,4 @@ +--- +title: "Callables" +--- + diff --git a/languages/tolk/types/cells.mdx b/languages/tolk/types/cells.mdx new file mode 100644 index 000000000..c4a424917 --- /dev/null +++ b/languages/tolk/types/cells.mdx @@ -0,0 +1,4 @@ +--- +title: "Cells, slices, builders" +--- + diff --git a/languages/tolk/types/enums.mdx b/languages/tolk/types/enums.mdx new file mode 100644 index 000000000..41261580d --- /dev/null +++ b/languages/tolk/types/enums.mdx @@ -0,0 +1,4 @@ +--- +title: "Enums" +--- + diff --git a/languages/tolk/types/generics.mdx b/languages/tolk/types/generics.mdx new file mode 100644 index 000000000..21582ea51 --- /dev/null +++ b/languages/tolk/types/generics.mdx @@ -0,0 +1,5 @@ +--- +title: "Generic structs and aliases" +sidebarTitle: "Generics" +--- + diff --git a/languages/tolk/types/list-of-types.mdx b/languages/tolk/types/list-of-types.mdx new file mode 100644 index 000000000..6dca357ed --- /dev/null +++ b/languages/tolk/types/list-of-types.mdx @@ -0,0 +1,5 @@ +--- +title: "Type system overview" +sidebarTitle: "List of types" +--- + diff --git a/languages/tolk/types/maps.mdx b/languages/tolk/types/maps.mdx new file mode 100644 index 000000000..53df0f1b2 --- /dev/null +++ b/languages/tolk/types/maps.mdx @@ -0,0 +1,5 @@ +--- +title: "Maps (key-value)" +sidebarTitle: "Maps" +--- + diff --git a/languages/tolk/types/nullable.mdx b/languages/tolk/types/nullable.mdx new file mode 100644 index 000000000..ec2298a98 --- /dev/null +++ b/languages/tolk/types/nullable.mdx @@ -0,0 +1,5 @@ +--- +title: "Nullable types, null safety" +sidebarTitle: "Nullable types" +--- + diff --git a/languages/tolk/types/numbers.mdx b/languages/tolk/types/numbers.mdx new file mode 100644 index 000000000..bdb2871f2 --- /dev/null +++ b/languages/tolk/types/numbers.mdx @@ -0,0 +1,4 @@ +--- +title: "Numbers" +--- + diff --git a/languages/tolk/types/strings.mdx b/languages/tolk/types/strings.mdx new file mode 100644 index 000000000..b4c04b399 --- /dev/null +++ b/languages/tolk/types/strings.mdx @@ -0,0 +1,5 @@ +--- +title: "Strings (actually, slices)" +sidebarTitle: "Strings" +--- + diff --git a/languages/tolk/types/structures.mdx b/languages/tolk/types/structures.mdx new file mode 100644 index 000000000..e922f2e9f --- /dev/null +++ b/languages/tolk/types/structures.mdx @@ -0,0 +1,4 @@ +--- +title: "Structures" +--- + diff --git a/languages/tolk/types/tensors.mdx b/languages/tolk/types/tensors.mdx new file mode 100644 index 000000000..cf89f6762 --- /dev/null +++ b/languages/tolk/types/tensors.mdx @@ -0,0 +1,5 @@ +--- +title: "Tensors (not tuples!)" +sidebarTitle: "Tensors" +--- + diff --git a/languages/tolk/types/tuples.mdx b/languages/tolk/types/tuples.mdx new file mode 100644 index 000000000..601ecbcc1 --- /dev/null +++ b/languages/tolk/types/tuples.mdx @@ -0,0 +1,5 @@ +--- +title: "Tuples (not tensors!)" +sidebarTitle: "Tuples" +--- + diff --git a/languages/tolk/types/type-checks-and-casts.mdx b/languages/tolk/types/type-checks-and-casts.mdx new file mode 100644 index 000000000..84bb84835 --- /dev/null +++ b/languages/tolk/types/type-checks-and-casts.mdx @@ -0,0 +1,4 @@ +--- +title: "Type checks and casts" +--- + diff --git a/languages/tolk/types/unions.mdx b/languages/tolk/types/unions.mdx new file mode 100644 index 000000000..2958501a5 --- /dev/null +++ b/languages/tolk/types/unions.mdx @@ -0,0 +1,4 @@ +--- +title: "Union types" +--- + diff --git a/languages/tolk/types/void-never.mdx b/languages/tolk/types/void-never.mdx new file mode 100644 index 000000000..a84b11544 --- /dev/null +++ b/languages/tolk/types/void-never.mdx @@ -0,0 +1,4 @@ +--- +title: "Void and never types" +sidebarTitle: "Void and never" +--- From 610b1e8458126ee0a2d9d0bf85f9d9e2dfa3d42d Mon Sep 17 00:00:00 2001 From: tolk-vm Date: Thu, 13 Nov 2025 13:41:46 +0300 Subject: [PATCH 03/11] [Tolk] New docs: type system - all articles about Tolk type system - get rid of 'you' and 'we' in existing pages --- docs.json | 4 +- languages/tolk/basic-syntax.mdx | 329 ++++++++++++++- .../tolk/features/auto-serialization.mdx | 372 +++++------------ languages/tolk/features/lazy-loading.mdx | 50 ++- languages/tolk/features/message-sending.mdx | 70 ++-- languages/tolk/idioms-conventions.mdx | 26 ++ languages/tolk/syntax/pattern-matching.mdx | 22 + languages/tolk/types/address.mdx | 128 ++++++ languages/tolk/types/aliases.mdx | 95 +++++ languages/tolk/types/booleans.mdx | 87 ++++ languages/tolk/types/callables.mdx | 126 ++++++ languages/tolk/types/cells.mdx | 284 +++++++++++++ languages/tolk/types/enums.mdx | 139 +++++++ languages/tolk/types/generics.mdx | 107 +++++ languages/tolk/types/list-of-types.mdx | 35 ++ languages/tolk/types/maps.mdx | 362 +++++++++++++++++ languages/tolk/types/nullable.mdx | 155 +++++++ languages/tolk/types/numbers.mdx | 202 ++++++++++ .../tolk/types/overall-serialization.mdx | 380 ++++++++++++++++++ languages/tolk/types/overall-tvm-stack.mdx | 254 ++++++++++++ languages/tolk/types/strings.mdx | 165 ++++++++ languages/tolk/types/structures.mdx | 144 +++++++ languages/tolk/types/tensors.mdx | 125 ++++++ languages/tolk/types/tuples.mdx | 149 +++++++ .../tolk/types/type-checks-and-casts.mdx | 122 ++++++ languages/tolk/types/unions.mdx | 163 ++++++++ languages/tolk/types/void-never.mdx | 121 ++++++ 27 files changed, 3886 insertions(+), 330 deletions(-) create mode 100644 languages/tolk/types/overall-serialization.mdx create mode 100644 languages/tolk/types/overall-tvm-stack.mdx diff --git a/docs.json b/docs.json index 6cb0182ab..cdca94cd6 100644 --- a/docs.json +++ b/docs.json @@ -417,7 +417,9 @@ "languages/tolk/types/maps", "languages/tolk/types/callables", "languages/tolk/types/void-never", - "languages/tolk/types/type-checks-and-casts" + "languages/tolk/types/type-checks-and-casts", + "languages/tolk/types/overall-tvm-stack", + "languages/tolk/types/overall-serialization" ] }, { diff --git a/languages/tolk/basic-syntax.mdx b/languages/tolk/basic-syntax.mdx index f2903dc27..2dc74f69e 100644 --- a/languages/tolk/basic-syntax.mdx +++ b/languages/tolk/basic-syntax.mdx @@ -3,15 +3,332 @@ title: "Tolk basic syntax" sidebarTitle: "Basic syntax" --- -semicolons +import { Aside } from '/snippets/aside.jsx'; -variables +Syntax of Tolk is similar to TypeScript, Rust, and Kotlin. It is designed to be straightforward to read and write. -function +Below is a list of basic syntax elements with examples. Most sections end with a link to a detailed topic. -struct +## Imports -method +Imports exist at the top of the file: -contract getter +```tolk +import "another-file" +// symbols from `another-file.tolk` can now be used +``` + +In typical workflows, an IDE inserts imports automatically (for example, when selecting an element from auto‑completion). + + + +See: [imports](/languages/tolk/syntax/imports). + +## Structures + +A struct `Point` holding two 8-bit integers: + +```tolk +struct Point { + x: int8 + y: int8 +} + +fun demo() { + // create an object + val p1: Point = { x: 10, y: 20 }; + + // the same, type of p2 is auto-inferred + val p2 = Point { x: 10, y: 20 }; +} +``` + +- methods are declared like `fun Point.method(self)`, read below +- fields can be of any types: numeric, cell, union, etc. (see [type system](/languages/tolk/types/list-of-types)) +- fields can have default values: `x: int8 = 0` +- fields can be `private` and `readonly` +- structs can be generic: `struct Wrapper { ... }` + +If all fields are serializable, a struct can be [automatically serialized](/languages/tolk/features/auto-serialization): + +```tolk +// makes a cell containing hex "0A14" +val c = p1.toCell(); +// back to { x: 10, y: 20 } +val p3 = Point.fromCell(c); +``` + +See: [structures](/languages/tolk/syntax/structures-fields). + +## Functions + +A function that calculates the sum of two integers: + +```tolk +fun sum(a: int, b: int): int { + return a + b; +} +``` + +- parameter types are mandatory +- the return type can be omitted: it will be auto-inferred, like in TypeScript +- parameters can have a default value: `fun f(b: int = 0)` +- statements inside a block are separated by semicolons `;` +- generic functions: `fun f(value: T) { ... }` +- assembler functions: `fun f(...): int asm "..."` + +See: [functions and methods](/languages/tolk/syntax/functions-methods). + +## Methods + +A function declared as `fun .name(...)` is a method. + +- if the first parameter is `self`, it's an **instance method** +- if not `self`, it's a **static method** + +```tolk +// `self` — instance method (invoked on a value) +fun Point.sumCoords(self) { + return sum(self.x, self.y); +} + +// not `self` — static method +fun Point.createZero(): Point { + return { x: 0, y: 0 }; +} + +fun demo() { + val p = Point.createZero(); // { 0, 0 } + return p.sumCoords(); // 0 +} +``` + +- by default, `self` is immutable, but `mutate self` allows modifying an object +- methods may be declared not only for a struct, but for any type, even a primitive: + +```tolk +fun int.isNegative(self) { + return self < 0 +} +``` + +See: [functions and methods](/languages/tolk/syntax/functions-methods). + +## Variables + +Inside functions, variables are declared with `val` or `var` keywords. + +The `val` keyword declares a variable that is assigned exactly once (immutable): + +```tolk +val coeff = 5; +// cannot change its value, `coeff += 1` is an error +``` + +The `var` keyword declares a variable that may be reassigned: + +```tolk +var x = 5; +x += 1; // now 6 +``` + +Variable's type can be specified after its name: + +```tolk +var x: int8 = 5; +``` + +Declaring variables at the top-level (not inside functions) is supported via `global` keyword. + +See: [variables](/languages/tolk/syntax/variables). + +## Constants + +Declaring constants is allowed at the top-level (not inside functions): + +```tolk +const ONE = 1 +const MAX_AMOUNT = ton("0.05") +const ADMIN_ADDRESS = address("EQ...") +``` + +To group integer constants, [enums](/languages/tolk/types/enums) are also useful. + +## Value semantics + +Tolk follows value semantics: assignments create independent copies, and function calls do not mutate arguments unless explicitly specified. + +```tolk +var a = Point { x: 1, y: 2 }; +var b = a; // `b` is a copy +b.x = 99; // `a.x` remains 1 +someFn(a); // pass a copy; `a` will not change + +// but there can be mutating functions, called this way: +anotherFn(mutate a); +``` + +See: [mutability](/languages/tolk/syntax/mutability). + +## Semicolons + +- semicolons are **optional at the top-level** (after imports, aliases, etc.) +- **required between statements in a function** +- after the last statement in a block, it's also optional + +```tolk +// optional at the top-level +const ONE = 1 +type UserId = int + +// required inside functions +fun demo() { + val x = 5; + val y = 6; + return x + y // optional after the last statement +} +``` + +## Comments + +Like most modern languages, Tolk supports single-line (or end-of-line) and multi-line (block) comments: + +```tolk +// This is a single-line comment + +/* This is a block comment + across multiple lines. */ + +const TWO = 1 /* + 100 */ + 1 // 2 +``` + +## Conditional operators + +```tolk +fun sortNumbers(a: int, b: int) { + if (a > b) { + return (b, a) + } else { + return (a, b) + } +} +``` + +In Tolk, `if` is a statement, with `else if` and `else` optional blocks. + +A ternary operator is also available: + +```tolk +val sign = a > 0 ? 1 : a < 0 ? -1 : 0; +``` + +See: [conditions and loops](/languages/tolk/syntax/conditions-loops). + +## Union types and matching + +Union types allow a variable to hold "one of possible types". They are typically handled by `match`: + +```tolk +fun processValue(value: int | slice) { + match (value) { + int => { + value * 2 + } + slice => { + value.loadUint(8) + } + } +} +``` + +Alternatively, test a union with `is` or `!is` operators: + +```tolk +fun processValue(value: int | slice) { + if (value is slice) { + // call methods for `slice` + return; + } + // value is `int` + return value * 2; +} +``` + +Unions types are commonly used when [handling incoming messages](/languages/tolk/features/message-handling). + +See: [union types](/languages/tolk/types/unions). + +## While loop + +```tolk +while (i > 0) { + // ... + i -= 1; +} +``` + +The `for` loop does not exist. + +See: [conditions and loops](/languages/tolk/syntax/conditions-loops). + +## Assert and throw + +```tolk +const ERROR_NO_BALANCE = 403; + +// in some function +throw ERROR_NO_BALANCE; + +// or conditional throw +assert (balance > 0) throw ERROR_NO_BALANCE; +``` + +A try-catch statement is also supported, although it is not commonly used in contracts. + +See: [exceptions](/languages/tolk/syntax/exceptions). + +## Iterate over a map + +```tolk +fun iterateOverMap(m: map) { + var r = m.findFirst(); + while (r.isFound) { + // ... + r = m.iterateNext(r); + } +} +``` + +See: [maps](/languages/tolk/types/maps). + +## Send a message to another contract + +An outgoing message body is typically represented by a structure (for example, `RequestedInfo`). + +```tolk +val reply = createMessage({ + bounce: BounceMode.NoBounce, + value: ton("0.05"), + dest: someAddress, + body: RequestedInfo { ... } +}); +reply.send(SEND_MODE_REGULAR); +``` + +See: [constructing and sending messages](/languages/tolk/features/message-sending). + +## Contract getters + +Contract getters (or "get methods") are declared with `get fun`: + +```tolk +get fun currentOwner() { + val storage = lazy Storage.load(); + return storage.ownerAddress; +} +``` + +See: [contract getters](/languages/tolk/features/contract-getters). diff --git a/languages/tolk/features/auto-serialization.mdx b/languages/tolk/features/auto-serialization.mdx index c0fddc10b..641a216da 100644 --- a/languages/tolk/features/auto-serialization.mdx +++ b/languages/tolk/features/auto-serialization.mdx @@ -2,7 +2,7 @@ title: "Auto packing to/from cells" --- -A short demo of how it looks: +A short demo of auto serialization: ```tolk struct Point { @@ -10,20 +10,22 @@ struct Point { y: int8 } -var value: Point = { x: 10, y: 20 }; +fun demo() { + var value: Point = { x: 10, y: 20 }; -// makes a cell containing "0A14" -var c = value.toCell(); -// back to { x: 10, y: 20 } -var p = Point.fromCell(c); + // makes a cell containing "0A14" + var c = value.toCell(); + // back to { x: 10, y: 20 } + var p = Point.fromCell(c); +} ``` ## Key features of auto-serialization - Supports all types: unions, tensors, nullables, generics, atomics, ... -- Allows you to specify serialization prefixes (particularly, opcodes) -- Allows you to manage cell references and when to load them -- Lets you control error codes and other behavior +- Allows to specify serialization prefixes (particularly, opcodes) +- Allows to manage cell references and when to load them +- Granular control over error codes - Unpacks data from a cell or a slice, mutate it or not - Packs data to a cell or a builder - Warns if data potentially exceeds 1023 bits @@ -31,63 +33,18 @@ var p = Point.fromCell(c); ## List of supported types and how they are serialized -A small reminder: Tolk has `intN` types (`int8`, `uint64`, etc.). Of course, they can be nested, like nullable `int32?` or a tensor `(uint5, int128)`. -They are just integers at the TVM level, they can hold any value at runtime: **overflow only happens at serialization**. -For example, if you assign 256 to uint8, asm command "8 STU" will fail with code 5 (integer out of range). - -| Type | TL-B equivalent | Serialization notes | -| ------------------------ | ---------------------------------------- | ----------------------------------- | -| `int8`, `uint55`, etc. | same as TL-B | `N STI` / `N STU` | -| `coins` | VarUInteger 16 | `STGRAMS` | -| `varint16`, 32, etc. | Var(U)Integer 16/32 | `STVARINT16` / `STVARUINT32` / etc. | -| `bits8`, `bits123`, etc. | just N bits | runtime check + `STSLICE` (1) | -| `address` | addr\_std (internal, 267 bits) | `STSTDADDR` | -| `address?` | addr\_none or addr\_std (2/267 bits) | `STOPTSTDADDR` | -| `any_address` | MsgAddress (internal/external/none) | `STSLICE` | -| `bool` | one bit | `1 STI` | -| `cell` | untyped reference, TL-B `^Cell` | `STREF` | -| `cell?` | maybe reference, TL-B `(Maybe ^Cell)` | `STOPTREF` | -| `Cell` | typed reference, TL-B `^T` | `STREF` | -| `Cell?` | maybe typed reference, TL-B `(Maybe ^T)` | `STOPTREF` | -| `RemainingBitsAndRefs` | rest of slice | `STSLICE` | -| `builder` | only for writing, not for reading (4) | `STB` | -| `slice` | only for writing, not for reading (4) | `STSLICE` | -| `T?` | TL-B `(Maybe T)` | `1 STI` + IF ... | -| `T1 \| T2` | TL-B `(Either T1 T2)` | `1 STI` + IF ... + ELSE ... (2) | -| `T1 \| T2 \| ...` | TL-B multiple constructors | IF ... + ELSE IF ... + ELSE ... (3) | -| `(T1, T2)` | TL-B `(Pair T1 T2)` = one by one | pack T1 + pack T2 | -| `(T1, T2, ...)` | nested pairs = one by one | pack T1 + pack T2 + ... | -| `SomeStruct` | fields one by one | like a tensor | -| `enum SomeEnum` | intN or uintN, where N manual or auto | `enum SomeEnum: int8` for manual | - -- (1) By analogy with `intN`, there are `bytesN` types. It's just a `slice` under the hood: the type shows how to serialize this slice. - By default, before `STSLICE`, the compiler inserts runtime checks (get bits/refs count + compare with N + compare with 0). - These checks ensure that serialized binary data will be correct, but they cost gas. - However, if you guarantee that a slice is valid (for example, it comes from trusted sources), pass an option `skipBitsNValidation` to disable runtime checks. - -- (2) TL-B Either is expressed with a union `T1 | T2`. For example, `int32 | int64` is packed as ("0" + 32-bit int OR "1" + 64-bit int). - However, if T1 and T2 are both structures with manual serialization prefixes, those prefixes are used instead of a 0/1 bit. - -- (3) To pack or unpack a union, say, `Msg1 | Msg2 | Msg3`, we need serialization prefixes. For structures, you can specify them manually - (or the compiler will generate them right here). For primitives, like `int32 | int64 | int128 | int256`, the compiler generates a prefix tree - (00/01/10/11 in this case). Read **auto-generating serialization prefixes** below. - -- (4) Using raw `builder` and `slice` for writing is supported but not recommended, as they do not reveal any information about their internal structure. - Auto-generated TypeScript wrappers will be available in the future. However, for a raw `slice`, a wrapper cannot be generated, as there is no way to predict how to read such a field back. - This feature will likely be removed in the future. - -## Some examples of valid types +Follow [Overall: serialization](/languages/tolk/types/overall-serialization). + +Some examples: ```tolk struct A { f1: int8 // just int8 f2: int8? // maybe int8 - f3: address // internal address (267 bits) - f4: address? // internal or none (`null` <-> '00', `address` <-> 267 bits) - f5: bool // TRUE (-1) serialized as bit '1' - f6: B // embed fields of struct B - f7: B? // maybe B - f8: coins // used for money amounts + f3: address // internal address + f4: B // embed fields of struct B + f5: B? // maybe B + f6: coins // used for money amounts r1: cell // always-existing untyped ref r2: Cell // typed ref @@ -108,33 +65,19 @@ struct A { ## Serialization prefixes and opcodes -Declaring a struct, there is a special syntax to provide pack prefixes. - -Typically, you'll use 32-bit prefixes for messages opcodes, or arbitrary prefixes is case you'd like to express TL-B multiple constructors. +Structures provide special syntax for declare "prefixes". +32-bit ones are typically called **opcodes** and used for messages (incoming and outgoing): ```tolk struct (0x7362d09c) TransferNotification { queryId: uint64 - ... + // ... } ``` -Prefixes **can be of any width**, they are not restricted to be 32 bit. - -- `0x000F` — 16-bit prefix -- `0x0F` — 8-bit prefix -- `0b010` — 3-bit prefix -- `0b00001111` — 8-bit prefix +But prefixes are not restricted to be 32-bit: `0x000F` is a 16-bit prefix, `0b010` is 3-bit (binary). -Declaring messages with 32-bit opcodes does not differ from declaring any other structs. Say, you the following TL-B scheme: - -```tlb -asset_simple$001 workchain:int8 ptr:bits32 = Asset; -asset_booking$1000 order_id:uint64 = Asset; -... -``` - -You can express the same with structures and union types: +Example of structures combined to a union, with comments: ```tolk struct (0b001) AssetSimple { @@ -146,69 +89,66 @@ struct (0b1000) AssetBooking { orderId: uint64 } -type Asset = AssetSimple | AssetBooking | ... +type Asset = AssetSimple | AssetBooking // | ... struct ProvideAssetMessage { - ... + // ... asset: Asset } -``` - -When deserializing, `Asset` will follow manually provided prefixes: -```tolk -// msg.asset is parsed as '001' + int8 + bits32 OR ... -val msg = ProvideAssetMessage.fromSlice(s); - -// now, msg.asset is just a union -// you can match it -match (msg.asset) { - AssetSimple => { // smart cast - msg.asset.workchain - msg.asset.ptr +fun demo() { + // msg.asset is parsed as '001' + int8 + bits32 OR ... + val msg = ProvideAssetMessage.fromSlice(s); + + // now, msg.asset is just a union + match (msg.asset) { + AssetSimple => { // smart cast + msg.asset.workchain; + msg.asset.ptr; + } + // ... other variants + } + // or test with `is` operator + if (msg.asset is AssetBooking) { + // ... } - ... -} -// or test with `is` operator -if (msg.asset is AssetBooking) { - ... } -// or do any other things with a union: -// prefixes play their role only in deserialization process ``` When serializing, everything also works as expected: ```tolk val out: ProvideAssetMessage = { - ..., - asset: AssetSimple { // will be serialized as - workchain: ..., // '001' + int8 + bits32 - ptr: ... + // will be serialized as: '001' + '00000011' + bits32 + asset: AssetSimple { + workchain: 3, + ptr: SOME_32_BITS } } ``` -Note that if a struct has a manual pack prefix, it does not matter whether this struct is inside any union or not. +Note that if a struct has a prefix, it does not matter whether it's inside any union or not. ```tolk -struct (0x1234) MyData { - ... +struct (0x00FF) MyData { + // ... } -MyData.fromSlice(s) // expected to be "1234..." (hex) -data.toCell() // "1234..." +fun demo() { + MyData.fromSlice(s); // expected to be '00FF...' (hex) + data.toCell(); // '00FF...' +} ``` -That's why, when you declare outgoing messages with 32-bit opcodes and just serialize them, that opcodes are included in binary data. +That's why, structs for outgoing messages (with 32-bit opcodes), being serialized, include opcodes in binary data. ## What can NOT be serialized - `int` can't be serialized, it does not define binary width; use `int32`, `uint64`, etc. - `slice`, for the same reason; use `address` or `bitsN` - tuples, not implemented -- `A | B` (and `A|B|C|...` in general) if A has manual serialization prefix, B not (because it seems like a bug in your code) -- `int32 | A` (and `primitives|...|structs` in general) if A has manual serialization prefix (because it's not definite what prefixes to use for primitives) +- `A | B` if A has manual serialization prefix, B not (because it seems like a bug in code) +- `primitives | ... | structs` if structs have serialization prefixes (because it's not definite what prefixes to use for primitives) Example of invalid: @@ -224,20 +164,22 @@ fun invalidDemo(obj: A | B) { ## Error messages if serialization unavailable -If you, by mistake, use unsupported types, Tolk compiler will fire a meaningful error. Example: +If someone, by mistake, uses unsupported types, Tolk compiler will fire a meaningful error. Example: ```tolk struct ExtraData { owner: address - lastTime: int + lastTime: int // mistake is here } struct Storage { - ... + // ... more: Cell } -Storage.fromSlice(""); +fun errDemo() { + Storage.fromSlice(""); +} ``` fires an error: @@ -253,9 +195,10 @@ hint: replace `int` with `int32` / `uint64` / `coins` / etc. ## Controlling cell references. Typed cells -Tolk gives you full control over how your data is placed in cells and how cells reference each other. -When you declare fields in a struct, there is no compiler magic of reordering fields, making any implicit references, etc. -As follows, whenever you need to place data in a ref, you do it manually. As well as you manually control, when contents of that ref is loaded. +Fields of a struct are serialized one be one. +The compiler does not reorder fields, create implicit references, etc. +Hence, when data should be placed in a ref, it's done explicitly. +Similarly, a developer controls, when exactly contents of that ref is loaded. There are two types of references: typed and untyped. @@ -275,13 +218,13 @@ struct RoyaltyParams { } ``` -When you call `NftCollectionStorage.fromSlice` (or fromCell), the process is as follows: +A call `NftCollectionStorage.fromSlice` (or fromCell) is processed as follows: -1. read address (slice.loadAddress) -1. read uint64 (slice.loadUint 64) -1. read three refs (slice.loadRef); do not unpack them: we just have pointers to cells +1. read address +1. read uint64 +1. read three refs; do not unpack them: just load pointers to cells -Note, that `royaltyParams` is `Cell`, not `T` itself. You can NOT access `numerator`, etc. To load those fields, you should manually unpack that ref: +Note, that `royaltyParams` is `Cell`, not `T` itself. To access fields (`numerator` and others), manually unpack that ref: ```tolk // error: field does not exist in type `Cell` @@ -295,7 +238,7 @@ rp.numerator val rp = RoyaltyParams.fromCell(st.royaltyParams); ``` -And vice versa: when composing such a struct, you should assign a typed cell to a field: +And vice versa: when composing such a struct, a cell should be assigned there, not an object: ```tolk val st: NftCollectionStorage = { @@ -307,24 +250,15 @@ val st: NftCollectionStorage = { } ``` -Probably, you've guessed that `T.toCell()` makes `Cell`, actually. That's true: +A method `T.toCell()` returns `Cell`: ```tolk val c = p.toCell(); // Point to Cell val p2 = c.load(); // Cell to Point ``` -With types cells, you can express snake data: - -```tolk -struct Snake { - data: bits1023 - next: Cell? -} -``` - So, typed cells are a powerful mechanism to express the contents of referenced cells. -Note that `Cell
` or even `Cell` is also okay, you are not restricted to structures. +Note that `Cell
` or even `Cell` is also okay, `T` is not restricted to structures. When it comes to untyped cells — just `cell` — they also denote references, but don't denote their inner contents, don't have the `.load()` method. @@ -332,12 +266,19 @@ It's just _some cell_, like code/data of a contract or an untyped nft content. ## Remaining data after reading -Suppose you have `struct Point (x int8, y int8)`, and read from a slice with contents `0102FF`. Byte `01` for x, byte `02` for y, and the remaining `FF` — is it correct? +What happens if after `fromSlice` remaining data is left? + +```tolk +// input is "0102FF" +Point.fromSlice(input); +``` + +Byte "01" for x, byte "02" for y, and the remaining "FF" — is it correct? **By default, this is incorrect**. By default, functions `fromCell` and `fromSlice` ensure the slice end after reading. In this case, exception 9 ("cell underflow") is thrown. -But you can override this behavior with an option: +This behavior can be turned off with an option: ```tolk Point.fromSlice(s, { @@ -347,44 +288,25 @@ Point.fromSlice(s, { ## Custom serializers for custom types -Imagine that you have your own "variable-length string": you encode its len, and then data, like - -``` -len: (## 8) // 8 bits of len -data: (bits len) // 0..255 bits of data -``` - -To express this, you can create your own type and **define a custom serializer**: +Using type aliases, it's possible to override serialization behavior in case it can not be expresses in existing types: ```tolk -type ShortString = slice +type MyString = slice -fun ShortString.packToBuilder(self, mutate b: builder) { - val nBits = self.remainingBitsCount(); - b.storeUint(nBits, 8); - b.storeSlice(self); +fun MyString.packToBuilder(self, mutate b: builder) { + // custom logic } -fun ShortString.unpackFromSlice(mutate s: slice) { - val nBits = s.loadUint(8); - return s.loadBits(nBits); +fun MyString.unpackFromSlice(mutate s: slice) { + // custom logic } ``` -And just use `ShortString` as a regular type — everywhere: - -```tolk -tokenName: ShortString -fullDomain: Cell -``` - -The compiler inlines your functions every time packing/unpacking takes place. - -Method names `packToBuilder` and `unpackFromSlice` are reserved for this purpose, their prototype must be exactly as showed. +For examples, proceed to [Serialization of type aliases](/languages/tolk/types/overall-serialization#type-aliases). ## UnpackOptions and PackOptions -They allow you to control behavior of `fromCell`, `toCell`, and similar functions: +These structs control behavior of `fromCell`, `toCell`, and similar functions: ```tolk MyMsg.fromSlice(s, { @@ -398,7 +320,7 @@ Serialization functions have the second optional parameter, actually: fun T.fromSlice(rawSlice: slice, options: UnpackOptions = {}): T; ``` -When you don't pass it, default options are used. But you can overload some of the options. +When omitted, default options are used, which can be overridden as shown above. For deserialization (`fromCell` and similar), there are now two available options: @@ -452,7 +374,7 @@ Internally, a cell is unpacked to a slice, and that slice is parsed (c.beginPars var msg = CounterIncrement.fromSlice(cs); ``` -All fields are read from a slice immediately. If a slice is corrupted, an exception is thrown (most likely, `excode` 9 "cell underflow"). Note, that a passed slice is NOT mutated; its internal pointer is NOT shifted. If you need to mutate it, like `cs.loadInt()`, consider calling `cs.loadAny()`. +All fields are read from a slice immediately. If a slice is corrupted, an exception is thrown (most likely, `excode` 9 "cell underflow"). Note, that a passed slice is NOT mutated; its internal pointer is NOT shifted. To mutate it, like `cs.loadInt()`, consider calling `cs.loadAny()`. 3. `slice.loadAny` — parse anything from a slice, shifting its internal pointer. Similar to `slice.loadUint()` and others, but allows loading structures. Example: @@ -482,107 +404,27 @@ struct JettonMessage { } ``` -When you deserialize JettonMessage, forwardPayload contains _everything left after reading fields above_. Essentially, it's an alias to a slice which is handled specially while unpacking: +Then, after `JettonMessage.fromCell`, forwardPayload contains _everything left after reading fields above_. Essentially, it's an alias to a slice which is handled specially while unpacking: ```tolk type RemainingBitsAndRefs = slice ``` -## Auto-generating prefixes for unions - -We've mentioned multiple times, that `T1 | T2` is encoded as TL-B Either: bit '0' + T1 OR bit '1' + T2. But what about wider unions? Say, - -```tolk -struct WithUnion { - f: int8 | int16 | int32 -} -``` - -In this case, **the compiler auto-generates a prefix tree**. This field will be packed as: '00' + int8 OR '01' + int16 OR '10' + int32. -On deserialization, the same format is expected (prefix '11' will throw an exception). - -The rules: - -- if `null` exists, it's 0, all others are 1+tree: A|B|C|D|null => 0 | 100+A | 101+B | 110+C | 111+D -- if no `null`, just distributed sequentially: A|B|C => 00+A | 01+B | 10+C - -Same for structs without a manually specified prefix: - -```tolk -struct A { ... } // 0x... prefixes not specified -struct B { ... } -struct C { ... } - -struct WithUnion { - // simple either ('0' + A OR '1' + B) - e: A | B - // auto-generated prefix tree: 00/01/10 - f: A | B | C - // with null, like Maybe: 0/10/11 - g: A | B | null - // even this works, why not - h: A | int32 | C | bits128 -} -``` - -When declaring a struct, you can manually specify serialization prefix (32-bit prefixes for messages are called opcodes, but prefixes in general can be of any length): - -```tolk -struct (0x01) WithPrefixLen8 { ... } -struct (0x00FF) WithPrefixLen16 { ... } -struct (0b1100) WithPrefixLen4 { ... } - -struct WithUnion { - // manual prefixes will be used, not 0/1 - e: WithPrefixLen8 | WithPrefixLen16 - // also, no auto-generation - f: WithPrefixLen8 | WithPrefixLen16 | WithPrefixLen4 -} -``` - -**If you specify prefixes manually, they will be used in (de)serialization**. -Moreover: since a prefix exists for a struct, when deserializing a struct itself (not inside a union), a prefix is expected to be contained in binary data: - -```tolk -// s should be "00FF..." -WithPrefixLen16.fromSlice(s) -// c will be "00FF..." -WithPrefixLen16{...}.toCell() -``` - -So, the rules are quite simple: - -- if you specify prefixes manually, they will be used (no matter within a union or not) -- if you don't specify any prefixes, the compiler auto-generates a prefix tree -- if you specify prefix for A, but forgot prefix for B, `A | B` can't be serialized -- either bit (0/1) is just a prefix tree for two cases - -**How can I specify a serialization prefix for non-struct?** Currently, there is no way to write something like `Prefixed`. -But you can just create a struct with a single field: - -```tolk -struct (0b0011) MyPrefixedInt { - value: int32 -} -``` - -It will have no overhead against just `int32`, the same slot on a stack, just adding a prefix for (de)serialization. - ## What if data exceeds 1023 bits -Struct fields are serialized one-by-one. So, if you have a large structure, its content may not fit into a cell. -Tolk compiler calculates the maximum size of every serialized struct, and if it potentially exceeds 1023 bits, fires an error. Your choice is +Tolk compiler calculates maximum size of every serializable type and warns if it potentially exceeds 1023 bits. +The developer should perform one of the actions: 1. either to suppress the error by placing an annotation above a struct; it means "okay, I understand" -1. or repack your data by splitting into multiple cells +1. or reorganize a struct by splitting into multiple cells -Why do we say "potentially"? Because for many types, their size can vary: +Why "potentially exceeds"? Because for many types, their size can vary: - `int8?` is either one or nine bits - `coins` is variadic: from 4 bits (small values) up to 124 bits -- `any_address` is internal (267 bits), or external (up to 521 bits), or none (2 bits) +- etc. -So, suppose you have: +So, given a struct: ```tolk struct MoneyInfo { @@ -592,16 +434,16 @@ struct MoneyInfo { } ``` -When you try to (de)serialize it, the compiler calculates its size, and prints an error: +And trying to serialize it, the compiler prints an error: ``` struct `MoneyInfo` can exceed 1023 bits in serialization (estimated size: 808..1048 bits) -... (and some instructions, what you should do) +... (and some instructions) ``` -Actually, you have two choices: +Actually, two choices are available: -1. you definitely know, that `coins` fields will be relatively small, and this struct will 100% fit in reality; then, suppress the error using an annotation: +1. if `coins` values are expected to be relatively small, and this struct will 100% fit in reality; then, suppress the error using an annotation: ```tolk @overflow1023_policy("suppress") @@ -610,10 +452,10 @@ struct MoneyInfo { } ``` -2. or you really expect billions of billions in `coins`, so data really can exceed; in this case, you should extract some fields into a separate cell; for example, store 800 bits as a ref; or extract other 2 fields and ref them: +2. or `coins` are expected to be billions of billions, so data really can exceed; in this case, extract some fields into a separate cell; for example, store 800 bits as a ref; or extract other 2 fields and ref them: ```tolk -// you can extract the first field +// either extract the first field struct MoneyInfo { fixed: Cell wallet1: coins @@ -631,15 +473,15 @@ struct MoneyInfo { } ``` -Generally, you leave more frequently used fields directly and place less-frequent fields to another ref. -All in all, the compiler indicates on potential cell overflow, and it's your choice how to overcome this. +A general advice: leave more frequently used fields directly and place less-frequent fields to another ref. +All in all, the compiler indicates on potential cell overflow, and it's a developer's choice how to overcome this. Probably, in the future, there will be more policies (besides "suppress") — for example, to auto-repack fields. For now, it's absolutely straightforward. ## Integration with message sending Auto-serialization is natively integrated with sending messages to other contracts. -You just "send some object," and the compiler automatically serializes it, detects whether it fits into a message cell, etc. +The compiler automatically serializes body, detects whether it fits into a message cell, etc. ```tolk val reply = createMessage({ diff --git a/languages/tolk/features/lazy-loading.mdx b/languages/tolk/features/lazy-loading.mdx index 242d4aead..feb580496 100644 --- a/languages/tolk/features/lazy-loading.mdx +++ b/languages/tolk/features/lazy-loading.mdx @@ -20,7 +20,7 @@ fun Storage.load() { What does `Storage.load()` do? It unpacks a cell, populates all struct fields, checks consistency, and so on. -**The magic of `lazy Storage.load()` is that it does not load the entire cell upfront**. Instead, the compiler tracks exactly which fields you access and automatically loads only those, skipping the rest. +**The magic of `lazy Storage.load()` is that it does not load the entire cell upfront**. Instead, the compiler tracks exactly which fields are accessed, and automatically loads only those, skipping the rest. ```tolk get fun getPublicKey() { @@ -30,11 +30,11 @@ get fun getPublicKey() { } ``` -That's it! With a single `lazy` keyword, loading is deferred until the data is accessed. The compiler tracks all control flow paths, inserts loading points as needed, groups unused fields to skip, and performs other optimizations as necessary. Best of all, this works with any type and any combination of fields used anywhere in your code — the compiler tracks everything. +That's it! With a single `lazy` keyword, loading is deferred until the data is accessed. The compiler tracks all control flow paths, inserts loading points as needed, groups unused fields to be skipped, etc. Best of all, this works with any type and any combination of fields. ## Even deeper than you might think -Suppose you have an NFT collection: +Take a look at NFT collection: ```tolk struct NftCollectionStorage { @@ -51,7 +51,7 @@ struct CollectionContent { } ``` -Now, imagine you want to access just the `content` field from the storage — and then extract `commonKey` from it: +Imagine: a developer needs just the `content` field from the storage — and then extract `commonKey` from it: ```tolk val storage = lazy NftCollectionStorage.load(); @@ -59,26 +59,26 @@ val storage = lazy NftCollectionStorage.load(); val contentCell = storage.content; ``` -**First trick:** no need to **skip address or skip uint64**. To get a reference field, the compiler knows exactly where it is and doesn't require skipping preceding data. +**First trick:** no need to **skip address and uint64**. To access a ref, it's not required to skipping preceding data. -**Second trick:** we have `contentCell`. How do we get `commonKey` from it? Since `content` is a cell, you need to load it… _lazily_: +**Second trick:** having `contentCell`, how to get `commonKey` from it? The answer: since `content` is a cell, load it… _lazily_: ```tolk val storage = lazy NftCollectionStorage.load(); // <-- "preload ref" inserted — to get `content` -// Cell.load() unpacks a cell and gives you T +// Cell.load() unpacks a cell and returns T val content = lazy storage.content.load(); // <-- "skip 32 bits, preload uint256" - to get commonKey return content.commonKey; ``` -A quick reminder about `Cell`: these typed cells are commonly used to represent nested references. When you have `p: Cell`, you can't directly access `p.x` — you need to load the cell first, either with `Point.fromCell(p)` or, preferably, `p.load()`. Both can be used with `lazy`. +A quick reminder: `Cell` is commonly used to represent nested references. Having `p: Cell`, it's not allowed to access `p.x` — the cell (reference) needs to be loaded first, either with `Point.fromCell(p)` or, preferably, `p.load()`. Both can be used with `lazy`. ## Lazy matching -Similarly, when reading a union type such as an incoming message, you use `lazy`: +Similarly, when reading a union type such as an incoming message, use `lazy`: ```tolk struct (0x12345678) CounterReset { ... } @@ -103,7 +103,7 @@ This makes **lazy matching highly efficient**, outperforming patterns like `if ( ## Lazy matching and else -Since lazy `match` for a union is done by inspecting the prefix (opcode), you can handle unmatched cases using an `else` branch. +Since lazy `match` for a union is done by inspecting the prefix (opcode), unmatched cases fall through to the `else` branch. In FunC contracts, a common pattern was to **ignore empty messages**: @@ -124,7 +124,7 @@ throw 0xFFFF; // "invalid opcode" The only reason to handle empty messages upfront was to avoid throwing a _cell underflow_ error when calling `loadUint`. -With lazy `match`, you no longer need to pay gas upfront for these checks. You can handle all cases in the `else` branch: +With lazy `match`, these upfront checks no longer needed. Handle all cases in `else`: ```tolk val msg = lazy MyMessage.fromSlice(msgBody); @@ -141,7 +141,7 @@ match (msg) { } ``` -Without an explicit `else`, unpacking throws `error 63` by default, which is controlled by the `throwIfOpcodeDoesNotMatch` option in fromCell/fromSlice. Adding `else` allows you to override this behavior. +Without an explicit `else`, unpacking throws `error 63` by default, which is controlled by the `throwIfOpcodeDoesNotMatch` option in fromCell/fromSlice. The `else` branch allows to override this behavior. Note that `else` in `match` by type is only allowed with `lazy` because it matches on prefixes. Without `lazy`, it's just a regular union, matched by a union tag (`typeid`) on a stack. @@ -149,7 +149,7 @@ Note that `else` in `match` by type is only allowed with `lazy` because it match The magic doesn't stop at reading. The `lazy` keyword also works seamlessly when **writing data back**. -Imagine you load a storage, use its fields for assertions, modify one field, and save it back: +Example: load a storage, use its fields for assertions, modify one field, and save it back: ```tolk val storage = lazy Storage.load(); @@ -178,26 +178,32 @@ storage.toCell(); // - store immutable tail ``` -No more manual optimizations using intermediate slices — the compiler handles everything for you. It can even group unmodified fields in the middle, load them as a slice, and preserve that slice on write-back. - -This optimization is only effective when the compiler can statically resolve control flow within a local function scope. If you use globals, split logic into non-inlined functions, this optimization will break, and `lazy` will fall back to regular loading. +No more manual optimizations using intermediate slices — the compiler handles everything automatically. It can even group unmodified fields in the middle, load them as a slice, and preserve that slice on write-back. ## Q: What are the disadvantages of lazy? -In terms of gas usage, `lazy fromSlice` is always equal to or cheaper than regular `fromSlice` because, in the worst case — when you access all fields—it loads everything one by one, just like the regular method. +In terms of gas usage, `lazy fromSlice` is always equal to or cheaper than regular `fromSlice` because, in the worst case — when all fields are accessed — it loads everything one by one, just like non-lazy. However, there is another difference unrelated to gas consumption: -- When you do `T.fromSlice(s)`, it unpacks all fields of `T` and then inserts`s.assertEnd()`, which can be turned off using an option. So, if the slice is corrupted or contains extra data, `fromSlice` will throw an error. +- `T.fromSlice(s)` unpacks all fields of `T` and then inserts `s.assertEnd()`, which can be turned off using an option. So, if the slice is corrupted or contains extra data, `fromSlice` will throw an error. - The `lazy` keyword, of course, selectively _picks_ only the requested fields and handles partially invalid input gracefully. For example, given: -``` -struct Point { x: int8, y: int8 } +```tolk +struct Point { + x: int8 + y: int8 +} + +fun demo(s: slice) { + val p = lazy Point.fromSlice(s); + return p.x; +} ``` -If you use `lazy Point` and access only `p.x`, then an input of `FF` (8 bits) is acceptable even though `y` is missing. Similarly, `FFFF0000`, which includes 16 bits of extra data, is also fine, as `lazy` ignores any data that is not requested. +Since only `p.x` is accessed, an input of `FF` (8 bits) is acceptable even though `y` is missing. Similarly, `FFFF0000`, which includes 16 bits of extra data, is also fine, as `lazy` ignores any data that is not requested. -In most cases, this isn't an issue. For storage, you have guarantees regarding the data shape, as your contract controls it. For incoming messages, you typically use all fields (otherwise, why include them in the struct?). If there is extra data in the input — who cares? The message can still be deserialized correctly, and I don't see any problem here. +In most cases, this isn't an issue. For incoming messages, typically all fields are used (otherwise, why include them in the struct?). If there is extra data in the input — who cares? The message can still be deserialized correctly without any problem. Perhaps someday, `lazy` will become the default. For now, it remains a distinct keyword highlighting the lazy-loading capability — a killer feature of Tolk. diff --git a/languages/tolk/features/message-sending.mdx b/languages/tolk/features/message-sending.mdx index 8d252e11c..799102a7b 100644 --- a/languages/tolk/features/message-sending.mdx +++ b/languages/tolk/features/message-sending.mdx @@ -2,7 +2,7 @@ title: "Sending messages" --- -Tolk has a high-level function `createMessage`. In practice, you'll immediately `send` its result: +Tolk has a high-level function `createMessage`. In practice, it's immediately followed by `send`: ```tolk val reply = createMessage({ @@ -28,29 +28,29 @@ reply.send(SEND_MODE_REGULAR); There is a variety of interacting between contracts. -- sometimes, you "send to an address (slice)" +- sometimes, you "send to an address" - ... but sometimes, you "build the address (builder) manually" - sometimes, you compose `StateInit` from code+data - ... but sometimes, you already have `StateInit` as a ready cell - sometimes, you send a message to basechain -- ... but sometimes, you have the `MY_workchain` constant and use it everywhere -- sometimes, you just attach tons (**msg value**) +- ... but sometimes, you use a `MY_WORKCHAIN` constant everywhere +- sometimes, you just attach tons (msg value) - ... but sometimes, you also need extra currencies - etc. -**How can we describe such a vast variety of options? The solution is union types!** +**How to describe such a vast variety of options? The solution is union types!** Let's start exploring this idea by looking at how extra currencies are supported. ## Extra currencies: union -When you don't need them, you just attach **msg value** as tons: +In most cases, just attach msg value as tons: ```tolk value: someTonAmount ``` -When you need them, you attach tons AND extra currencies dict: +But when extra currencies are needed, attach tons AND a dict: ```tolk value: (someTonAmount, extraDict) @@ -60,7 +60,7 @@ How does it work? Because the field `value` is a union: ```tolk // how it is declared in stdlib -type ExtraCurrenciesDict = dict; +type ExtraCurrenciesDict = dict struct CreateMessageOptions { ... @@ -68,7 +68,7 @@ struct CreateMessageOptions { value: coins | (coins, ExtraCurrenciesDict) ``` -That's it! You just attach tons OR tons with extra, and the compiler takes care of composing this into a cell. +That's it! Just attach tons OR tons with extra, and the compiler takes care of composing this into a cell. ## Destination: union @@ -95,7 +95,7 @@ struct CreateMessageOptions { ## `StateInit` and workchains -Let's start from an example. From a jetton minter, you are deploying a jetton wallet. You know wallet's code and initial data: +Let's start from an example. A contract "jetton minter" deploys a "jetton wallet". Wallet's code and initial data are known: ```tolk val walletInitialState: ContractState = { @@ -104,7 +104,7 @@ val walletInitialState: ContractState = { }; ``` -Now, from a minter, you want to send a message to a wallet. But since you are not sure whether the wallet already exists onchain, you attach its code+data: if a wallet doesn't exist, it's immediately initialized with that code. So, where should you send a message to? What is **destination**? The answer is: **destination is the wallet's `StateInit`**. You need to send a message to a `walletInitialState` because, in TON, the address of a contract is — by definition — a hash of its initial state: +A minter needs to send a message to a wallet. But since it's unknown whether the wallet already exists on-chain, a message needs wallet's code+data attached: if a wallet doesn't exist, it's immediately initialized with that code. So, where a message should be sent to? What is **destination**? The answer is: **destination is the wallet's StateInit**. In TON, the address of a contract is — by definition — a hash of its initial state: ```tolk // address auto-calculated, code+data auto-attached @@ -113,7 +113,7 @@ dest: { } ``` -In more complex tasks, you can configure additional fields: +To serve more complex tasks, configure additional fields: ```tolk dest: { @@ -136,9 +136,9 @@ struct AutoDeployAddress { ## Sharding: deploying "close to" another contract -The `createMessage` interface also supports initializing contracts in specific shards. Say you're writing sharded jettons, and you want every jetton wallet to be in the same shard as the owner's wallet. +The `createMessage` interface also supports initializing contracts in specific shards. Take, for example, sharded jettons — a jetton wallet should be deployed to the same shard as the owner's wallet. -In other words, your intention is: +In other words, the intention is: - a jetton wallet must be **close to** the owner's wallet - this _closeness_ is determined by a shard depth (syn. _fixed prefix length_, syn. _split depth_) @@ -152,7 +152,7 @@ Let's illustrate it with numbers for `shard depth` = 8: | stateInitHash | `yyyyyyyy...yyy` | calculated by code+data | | result (JW addr) | `01010101...yyy` | jetton wallet in same shard as owner | -That's how you do it: +That's how to do it: ```tolk dest: { @@ -185,7 +185,7 @@ struct AddressShardingOptions { In TON Blockchain, according to the specification, a message is a cell (flags, dest address, stateInit, etc.), and its _body_ can be either inlined into the same cell or can be placed into its own cell (and be a ref). -In FunC, you had to manually calculate whether it's safe to embed body (you did it _on paper_ or dynamically). In Tolk, you just pass `body`, and the compiler does all calculations for you: +Fortunately, a developer shouldn't keep this in mind. Just pass `body`, and the compiler does all calculations: ```tolk createMessage({ @@ -201,7 +201,7 @@ The rules are the following: Why not make a ref always? Because creating cells is expensive. Avoiding cells for small bodies is crucial for gas consumption. -Interestingly, whether the body is **small** is determined **AT COMPILE TIME — no runtime checks are needed**. How? Thanks to generics! Here's how `createMessage` is declared: +Interestingly, whether the body is **small** is determined **at compile time** — no runtime checks are needed. How? Thanks to generics: ```tolk fun createMessage(options: CreateMessageOptions): OutMessage; @@ -212,13 +212,13 @@ struct CreateMessageOptions { } ``` -Hence, when you pass `body: RequestedInfo {...}`, then `TBody = RequestedInfo`, and the compiler estimates its size: +Hence, when called as `body: RequestedInfo {...}`, then `TBody = RequestedInfo`, and the compiler estimates its size: - it's **small** if its maximum size is less than 500 bits and 2 refs — then **no ref** - it's **large** if >= 500 bits or >= 2 refs — then "ref" - it's **unpredictable** if contains `builder` or `slice` inside — then **ref** -**Even if body is large/unpredictable, you can force it to be inlined** by wrapping into a special type: +**Even if body is large/unpredictable, it can be force-inlined** by wrapping into a special type: ```tolk // potentialy 620 bits (if all coins are billions of billions) @@ -249,8 +249,7 @@ That's why, don't pass `body: someObj.toCell()`, pass just `body: someObj`, let ## Body is not restricted to structures -In practice, you use `createMessage` to send a message (sic!) to another contract — in the exact format as the receiver expects. -You declare a struct with 32-bit opcode and some data in it. +In practice, `createMessage` is used to send a message (sic!) to another contract — in the exact format as the receiver expects. ```tolk struct (0xd53276db) Excesses { @@ -258,7 +257,7 @@ struct (0xd53276db) Excesses { } val excessesMsg = createMessage({ - ... + // ... body: Excesses { queryId: input.queryId, } @@ -283,7 +282,7 @@ The example above just illustrates the power of the type system, no more. ## Body can be empty -If you don't need any `body` at all, just leave it out: +Don't need any `body` at all? Just leave it out: ```tolk createMessage({ @@ -305,8 +304,7 @@ struct CreateMessageOptions { } ``` -Hence, omitting `body` in `createMessage` defaults `TBody` to `void`. -And by convention, fields having `void` type are not required in a literal. +Hence, if `body` is omitted, it leads to the default `TBody = never`. And by convention, fields having `never` type are not required in a literal. It's not that obvious, but it is definitely beautiful. ## Don't confuse `StateInit` and code+data, they are different @@ -331,11 +329,11 @@ struct ContractState { And that's why a field `stateInit: ContractState | cell` is named **stateInit**, emphasizing that `StateInit` can be initialized automatically from `ContractState` (or can be a well-formed **rich** cell). -## Why not send, but createMessage? +## Q: Why not send, but createMessage? -You might ask: "why do we follow the pattern `val msg = createMessage(...); msg.send(mode)` instead of just `send(... + mode)` ?" +In other words: why the pattern `val msg = createMessage(...); msg.send(mode)`, why not `send(... + mode)`? -Typically, yes — you send a message immediately after composing it. But there are also advanced use cases: +Typically, yes — a message is immediately send after composing it. But there are also advanced use cases: - not just send, but send and estimate fees, - or estimate fees without sending, @@ -345,7 +343,7 @@ Typically, yes — you send a message immediately after composing it. But there So, composing a message cell and THEN doing some action with it is a more flexible pattern. -Moreover, following this pattern requires you to give **a name** to a variable. Advice is not to name it "m" or "msg," but to give a descriptive name like "excessesMsg" or "transferMsg": +Moreover, following this pattern requires to give **a name** to a variable. Advice is not to name it "m" or "msg," but to give a descriptive name like "excessesMsg" or "transferMsg": ```tolk val excessesMsg = createMessage({ @@ -356,15 +354,15 @@ excessesMsg.send(mode); excessesMsg.sendAndEstimateFee(mode); ``` -This strategy makes the code **easier to read** later. You see — okay, this is about excesses, this one is about burn notification, etc. As opposed to a potential `send(...)` function, you have to dig into what body is actually being sent to understand what's going on. +This strategy makes the code **easier to read** later. While scanning the code, a reader sees: this is about excesses, this one is about burn notification, etc. As opposed to a potential `send(...)` function, hard to identify what _meaning_ is intended by the exact call. -## Why not provide a separate deploy function? +## Q: Why not provide a separate deploy function? -You might also ask: why do we join `stateInit` as a **destination** for other use cases? Why not make a `deploy()` function that accepts code+data and drops stateInit from a **regular** createMessage? +In other words: why `stateInit` is a **destination**? Why not make a `deploy()` function that accepts code+data, and drop stateInit from a regular createMessage? -The answer lies in terminology. Yes, **attaching stateInit** is often referred to as **deployment**, but it's an inaccurate term. **TON Blockchain doesn't have a dedicated deployment mechanism.** You send a message to some _void_ — and if this _void_ doesn't exist, but you've attached a way to initialize it (code+data) — it's initialized immediately and accepts your message. +The answer lies in terminology. Yes, **attaching stateInit** is often referred to as **deployment**, but it's an inaccurate term. **TON Blockchain doesn't have a dedicated deployment mechanism.** A message is sent to some _void_ — and if this _void_ doesn't exist, but a way to initialize it (code+data) is provided — it's initialized immediately and accepts the message. -If you wish to emphasize the deployment, give it _a name_: +To emphasize the deployment, give it _a name_: ```tolk val deployMsg = createMessage({ @@ -375,7 +373,7 @@ deployMsg.send(mode); ## Universal createExternalLogMessage -The philosophy is similar to `createMessage`. But **external outs** don't have **bounce**; you don't attach tons, etc. So, options for creating are different. +The philosophy is similar to `createMessage`. But **external outs** don't have **bounce**, no attached tons, etc. So, options for creating are different. **Currently, external messages are used only for emitting logs (for viewing them in indexers).** But theoretically, they can be extended to **send messages to the offchain**. @@ -434,4 +432,4 @@ createExternalLogMessage({ }); ``` -`ExtOutLogBucket` is a variant of a custom external address for emitting logs **to the outer world.** It includes some **topic** (arbitrary number), that determines the format of the message body. In the example above, you emit **deposit event** (reserving topic `deposit` = 123) — and external indexers will index your emitted logs by destination address without parsing body. +`ExtOutLogBucket` is a variant of a custom external address for emitting logs **to the outer world.** It includes some **topic** (arbitrary number), that determines the format of the message body. In the example above, a **deposit event** is emitted (reserving topic `deposit` = 123) — and external indexers will index emitted logs by destination address without parsing body. diff --git a/languages/tolk/idioms-conventions.mdx b/languages/tolk/idioms-conventions.mdx index c7201feb9..1cd750188 100644 --- a/languages/tolk/idioms-conventions.mdx +++ b/languages/tolk/idioms-conventions.mdx @@ -3,3 +3,29 @@ title: "Idioms and conventions" sidebarTitle: "Idioms and conventions" --- +todo: note that it's not for beginners + + +storage + +typed cells + +handle message + +send message + +deploy is send message + +use lazy + +no manual builders, use types + +custom serializers + +stringHexToSlice() + +address() + +opcodes + +copy by value, e.g. slices diff --git a/languages/tolk/syntax/pattern-matching.mdx b/languages/tolk/syntax/pattern-matching.mdx index e9a1fbb41..5d2a7e7ff 100644 --- a/languages/tolk/syntax/pattern-matching.mdx +++ b/languages/tolk/syntax/pattern-matching.mdx @@ -2,3 +2,25 @@ title: "Pattern matching" --- + + +## Variable declaration inside `match` + +```tolk +type Pair2 = (int, int) +type Pair3 = (int, int, int) + +fun getPair2Or3(): Pair2 | Pair3 { /* ... */ } + +fun demo() { + match (val v = getPair2Or3()) { + Pair2 => return v.0 + v.1, + Pair3 => throw v.0 + v.1 + v.2, + } +} +``` + + +Familiar with Rust's `enum` and `match`? +You may notice that match is very similar, but `enum` in Tolk hold only integers. Union types are more general and powerful. + diff --git a/languages/tolk/types/address.mdx b/languages/tolk/types/address.mdx index 626502c00..c479d9f64 100644 --- a/languages/tolk/types/address.mdx +++ b/languages/tolk/types/address.mdx @@ -2,3 +2,131 @@ title: "Address" --- +import { Aside } from '/snippets/aside.jsx'; + +Every smart contract has an address that is used for all on-chain interactions. +When a client sends a message to a contract, the message is effectively sent to its address. + +Tolk provides the following types for working with addresses: + +- `address` — a standard address; also called "internal address" +- `address?` (nullable) — a pattern to say "potentially missing"; also called "internal or none", sometimes "maybe address" +- `any_address` — allows storing external addresses as well (for emitting logs to the off-chain, for example) + +## Address = workchain + hash + +A standard (internal) `address` consists of: + +- workchain (int8) — currently masterchain (-1) or basechain (0) +- hash (aka "account ID", uint256) — 256 bits + +`address` has methods to retrieve these properties: + +```tolk +fun checkAddress(addr: address, expectHash: uint256) { + val (wc, hash) = addr.getWorkchainAndHash(); + assert (wc == BASECHAIN) throw 123; + assert (hash == expectHash) throw 456; +} +``` + +At the binary level, `address` occupies 267 bits: 0b100 (standard address no anycast) + workchain + hash. +For a detailed description, see [Addresses overview in TON](/foundations/addresses/overview). + +During deserialization (for example, when a message contains an `address` field), the value is automatically validated. +If parsing succeeds, the resulting address is guaranteed to be valid. + +## Operator `==` works for addresses + +Compare addresses using `==` or `!=`. Internally, it is represented as a binary slice without references, that's why `==` just tests for bits equality. + +```tolk +struct Storage { + owner: address + // ... +} + +fun onInternalMessage(in: InMessage) { + var st = Storage.load(); + // process a message only from owner + if (in.senderAddress == st.owner) { + // ... + } +} +``` + +## Embedding a const address into a contract + +A constant address can be embedded using the `address()` function: + +```tolk +const REFUND_ADDR = address("EQCRDM9h4k3UJdOePPuyX40mCgA4vxge5Dc5vjBR8djbEKC5") +``` + +## Nullable address as "missing" + +A nullable address is often used to represent a potentially absent value. Examples: + +- `adminAddress` in a storage, but it may be unset +- `sendExcessesTo` in a message — if exists, send money there; if not, keep on contract's balance + +**Such a missing value is called "none address" in TON**. So, `address?` means "internal or none". + +From the type system perspective, it either holds an address or `null`. So, it must be checked for `null` before usage. + +```tolk +fun send(adminAddress: address?) { // may have no admin + if (adminAddress == null) { + return; + } + // send a message to adminAddress, it's not null +} +``` + + + +## `any_address` — internal/external/none + +TON also defines external addresses, although they are rarely used. To represent "any address valid for TON", use `any_address`. + +```tolk +struct CrossBridgeMessage { + destination: any_address +} +``` + +A compatible "none" address can be created using `createAddressNone()`. + +## Casting to a `slice` and vice versa + +To manually operate on the bits of an address, cast it to a slice: + +```tolk +val s = someAddr as slice; +s.loadUint(3); // 0b100 — internal address no anycast +s.loadInt(8); // workchain +``` + +Conversely, since an address is represented as a slice at the TVM level, this cast is permitted. +For example, after constructing a builder with its binary representation: + +```tolk +var b = beginCell() + .storeUint(0b01) // addr_extern + ...; +var s = b.endCell().beginParse(); +return s as any_address; +``` + +Such a transformation is **unsafe**, because no validation is performed at runtime. It may lead to an invalid address, and further `getWorkchain()` or serialization will fail. + +## Stack layout and serialization + +An address is backed by TVM `SLICE`, just binary data. "Internal" is 267 bits: 0b100 + workchain + hash. "None" is '00' (two zero bits). + +For details, follow [TVM representation](/languages/tolk/types/overall-tvm-stack) and [Serialization](/languages/tolk/types/overall-serialization). diff --git a/languages/tolk/types/aliases.mdx b/languages/tolk/types/aliases.mdx index ce13c93a5..ef8ef676a 100644 --- a/languages/tolk/types/aliases.mdx +++ b/languages/tolk/types/aliases.mdx @@ -2,3 +2,98 @@ title: "Type aliases" --- +Tolk supports type aliases, similar to TypeScript and Rust. +An alias creates a new name for an existing type and remains fully interchangeable with it. + +```tolk +type UserId = int32 +type MaybeOwnerHash = bytes32? + +fun calcHash(id: UserId): MaybeOwnerHash { + // ... +} +``` + +## Aliases are interchangeable with underlying types + +`UserId` and `int32` from the above are fully equivalent: + +- `id + 1` is okay, will be `int` +- `someF(id)` is okay if `someF` accepts `int32` or `int` +- methods for `int32` can be called having `UserId` and vice versa (and for `int` also, because `int32` is assignable to `int`) +- a union `UserId | int32` makes no sense, it is simply `int32` + +```tolk +fun demo() { + var id: UserId = 1; // ok + var num: int = id; // ok + var h = calcHash(id); + if (h != null) { + h as slice; // bytes32 as slice + } +} +``` + +**To obtain a "strict alias"**, which defines a distinct type, use a struct with one field: + +```tolk +struct UniqueId { + value: int32 +} +``` + +Such a struct has no overhead over `int32`, but it becomes a distinct type with its own methods and semantics. + +## Two equal aliases are considered different + +If two aliases share the same underlying type, + +```tolk +type AssetsDict = dict +type BalanceDict = dict +``` + +Then they are not assignable to each other. It allows them to have identical methods: + +```tolk +fun AssetsDict.validate(self) { /* ... */ } +fun BalanceDict.validate(self) { /* ... */ } + +fun demo(a: AssetsDict, b: BalanceDict) { + a.validate(); // ok, method 1 + b.validate(); // ok, method 2 + a = b; // error, can not assign +} +``` + +This reminds `intN` types: `int32` is assignable to `int` and back, `int64` also, but assigning `int32` to `int64` is something strange. +Assignment can be done with explicit casting: `b as AssetsDict`, see [operator `as`](/languages/tolk/types/type-checks-and-casts). + +## Type aliases can be generic + +```tolk +type Wrapper = Nothing | Container +``` + +Read about [generic structs and aliases](/languages/tolk/types/generics). + +## Stack layout and serialization + +An alias is identical to its underlying type. + +Serialization can be overloaded with custom serializers. +This is useful in tricky cases where binary encoding cannot be expressed using existing types. + +```tolk +type MyString = slice + +fun MyString.packToBuilder(self, mutate b: builder) { + // custom logic +} + +fun MyString.unpackFromSlice(mutate s: slice) { + // custom logic +} +``` + +For details, follow [TVM representation](/languages/tolk/types/overall-tvm-stack) and [Serialization](/languages/tolk/types/overall-serialization). diff --git a/languages/tolk/types/booleans.mdx b/languages/tolk/types/booleans.mdx index cfeb3756e..1aebd464c 100644 --- a/languages/tolk/types/booleans.mdx +++ b/languages/tolk/types/booleans.mdx @@ -2,3 +2,90 @@ title: "Booleans" --- +import { Aside } from '/snippets/aside.jsx'; + +Tolk has a `bool` type which can have two values: `true` or `false`. + + + +```tolk +fun isGreater10(v: int): bool { + return v > 10 +} +``` + +Operators `== >= && || ...` return `bool`. +Many standard library functions also return `bool` values: + +```tolk +var valid = isSignatureValid(...); // bool +var end = someSlice.isEmpty(); // bool +``` + +## Logical operators accept both `int` and `bool` + +- operator `!x` supports both `bool` and `int` +- the condition of `if` and similar statements accepts both `bool` and `int` (!= 0) +- logical `&& ||` accept both `bool` and `int`; for example, `a && b` evaluates to true when both operands are `true`, and for integers when both operands are non-zero + +Arithmetic operators are restricted to integers. + +```tolk +valid && end; // ok +valid & end; // ok, bitwise & | ^ also work if both are bools +if (!end) // ok + +valid + end; // error +8 & valid; // error, int & bool not allowed +``` + +## Logical vs bitwise operators + +Tolk has both bitwise `& ^ |` and logical `&& ||` operators. Both can be used for booleans and integers. + +The main difference is that logical are short-circuit: the right operand is evaluated only if required to. + +| Expression | Behavior | +| :------------------: | :--------------------------------------------: | +| `condition & f()` | `f()` is called always | +| `condition && f()` | `f()` is called only if `condition` is `true` | +| `condition \| f()` | `f()` is called always | +| `condition \|\| f()` | `f()` is called only if `condition` is `false` | + +The compiler does some optimizations for booleans. +Example: `!x` for `int` results in asm `0 EQINT`, but `!x` for `bool` results in asm `NOT`. + +Bitwise operators may sometimes be used instead of logical operators to avoid generating conditional branches at runtime. +For example, `(a > 0) && (a < 10)`, being replaced with bitwise, consumes less gas. +Future versions of the compiler may perform such transformations automatically, although this is non-trivial. + +## Casting to `int` via `as` operator + +```tolk +var i = boolValue as int; // -1 / 0 +``` + +There are no runtime transformations: `bool` is guaranteed to be -1/0 at the TVM level. + +## Q: Why are booleans -1, not 1? + +In TVM, there are only integers. +When all bits in a signed integer are set to 1, it equals -1 in a decimal representation. +This makes bitwise operations more intuitive — for example, `NOT 0` naturally becomes `-1`. + +- `true` = `-1` — all bits set to 1 +- `false` = `0` — all bits set to 0 + +It is consistent across TVM instructions. For example, operator `a > b` is asm `GREATER`, which returns -1 or 0. + +## Stack layout and serialization + +A boolean is backed by TVM `INT` (-1 or 0). Serialized as a single bit. + +For details, follow [TVM representation](/languages/tolk/types/overall-tvm-stack) and [Serialization](/languages/tolk/types/overall-serialization). diff --git a/languages/tolk/types/callables.mdx b/languages/tolk/types/callables.mdx index a7ff0f488..131140322 100644 --- a/languages/tolk/types/callables.mdx +++ b/languages/tolk/types/callables.mdx @@ -2,3 +2,129 @@ title: "Callables" --- +import { Aside } from '/snippets/aside.jsx'; + +Tolk has first-class functions: they can be passed as callbacks. Such values have callable types: `(...ArgsT) -> ReturnT`. + +## First-class functions + +Pass a function as a callback: + +```tolk +fun invokeMath(fn: (int, int) -> int) { + return fn(10, 20); +} + +fun myPlus(a: int, b: int) { + return a + b; +} + +fun demo() { + invokeMath(myPlus); // 30 +} +``` + +A function `myPlus` has the type `(int, int) -> int`. A function `demo`, for example, is `() -> void`. + +Assigning functions to variables also works: + +```tolk +fun demo() { + val callback = myPlus; + callback(5, 5); // 10 + + // or, with explicit type: + val fn: (int, int) -> int = myPlus; +} +``` + + + +## Lambda functions + +Tolk supports lambda functions: + +```tolk +fun demo() { + invokeMath(fun(a, b) { + return a * b + }); // 200 +} +``` + +Lambda parameter types may be omitted when they can be inferred. +In the example above, both `a` and `b` are `int`, inferred from `invokeMath` declaration. +If they cannot be inferred, the parameter types must be specified: + +```tolk +// error: param's type cannot be inferred here: +val doubleFn = fun(param) { return param * 2 }; + +// correct is: +val doubleFn = fun(param: int) { return param * 2 }; +``` + +As first-class functions, lambdas can even be returned: + +```tolk +fun createFinalizer() { + return fun(b: builder) { + b.storeUint(0xFFFFFFFF, 32); + return b.toSlice(); + } +} + +fun demo() { + val f = createFinalizer(); // (builder) -> slice + f(beginCell()); // slice with 32 bits +} +``` + +While lambdas are uncommon in smart contracts, they are useful in general‑purpose tooling. +They can be combined with generics of any level and may be nested without restrictions. + +```tolk +struct HasCallback { + arg: int + fn: (int -> TResult)? +} + +fun HasCallback.invoke(self): TResult { + assert (self.fn != null) throw ERR_CALLBACK_UNSET; + return self.fn(self.arg) +} +``` + +Note that lambdas are not closures: capturing outer variables is not supported. + +```tolk +fun outer(x: int) { + return fun(y: int) { + return x + y; // error: undefined symbol `x` + } +} +``` + +## Low-level TVM continuations + +Continuations are executable cells representing TVM bytecode. +A callable is effectively a typed continuation. + +Tolk has a type `continuation` type for low-level purposes. +For example, TVM register `c3` contains current smart contract code. +Some functions are available in stdlib: + +```tolk +import "@stdlib/tvm-lowlevel" +``` + +## Stack layout and serialization + +A callable is backed by TVM `CONT`. It cannot be serialized. + +For details, follow [TVM representation](/languages/tolk/types/overall-tvm-stack) and [Serialization](/languages/tolk/types/overall-serialization). diff --git a/languages/tolk/types/cells.mdx b/languages/tolk/types/cells.mdx index c4a424917..2252ee17e 100644 --- a/languages/tolk/types/cells.mdx +++ b/languages/tolk/types/cells.mdx @@ -2,3 +2,287 @@ title: "Cells, slices, builders" --- +import { Aside } from '/snippets/aside.jsx'; + +In TON, all data is stored in **cells**. +Cells opened for reading are called **slices**. +Cells being constructed are called **builders**. +Having a builder, only writing is possible. Having a slice, only reading is possible. + +Tolk provides low-level capabilities to construct and parse cells manually, as well as automatic packing structures to/from cells. + +## Cells + +A cell is the fundamental data structure in TON. It's a container that holds **up to 1023 bits** of data and **up to 4 references** to other cells. + +Everything in TON (contracts, messages, storage) is represented using cells. +They are read-only and immutable once created. +[Read more about cells](/foundations/serialization/cells). + +In Tolk, the basic type `cell` describes "some cell". + +```tolk +struct SomeMessage { + // ... + customPayload: cell +} +``` + +## Typed cells: `Cell` + +Besides "some cell", Tolk has a "cell with known shape" `Cell`. +Since one cell can store only 1023 bits, when storage exceeds this limit, the solution is to split it +into multiple cells, so they become referencing each other. + +```tolk +struct A { + ref1: cell // untyped ref + ref2: Cell // typed ref + ref3: Cell? // maybe ref +} +``` + + + +A typed cell can be assigned to `cell` implicitly. + +## Slices: cells opened for reading + +To manually read data from a cell, use `beginParse()` to get a slice: + +```tolk +var s = cell.beginParse(); +``` + +Then load data incrementally: integers, coins, sub-slices, references, etc. + +```tolk +val mode = s.loadUint(8); +val dest = s.loadAddress(); +val firstRef = s.loadRef(); // cell + +if (s.remainingBitsCount()) { + // ... +} +``` + +An IDE will suggest applicable methods after a dot. + +## Builders: cells at the moment of writing + +To manually construct a cell, create a builder, write some data, and finalize this builder: + +```tolk +var b = beginCell(); +b.storeUint(123, 8); +b.storeAddress(dest); +val c = b.endCell(); +``` + +Since methods `storeXXX` return `self`, these calls can be chained: + +```tolk +val c = beginCell() + .storeUint(123, 8) + .storeAddress(dest) + .endCell(); +``` + +## How to read from a builder + +The only way to access already written bits is to convert a builder into a slice: + +```tolk +var s = b.asSlice(); +// or (absolutely the same) +var s = b.endCell().beginParse(); +``` + +Constructing a cell is generally expensive in terms of gas, but `b.endCell().beginParse()` is optimized to a cheap asm instruction `BTOS` without intermediate cell creation. + +## Auto packing to/from cells + +Tolk type system is designed to avoid cumbersome manual work with slices and builders. +Almost every practical use case can be represented with an auto-serializable structure. + +```tolk +struct Something { + // ... +} + +fun parseAndModify(c: cell): cell { + var smth = Something.fromCell(c); + // ... + return smth.toCell(); +} +``` + +Having `Cell`, just call `load()` to get `T`: + +```tolk +fun parsePoint(c: Cell) { + // same as `Point.fromCell(c)` + var p = c.load(); +} +``` + +Read a detailed article [automatic serialization](/languages/tolk/features/auto-serialization). + +Internally, `fromCell()` does `beginParse()` and reads data from a slice. + +## Auto packing to/from builders/slices + +A struct can be parsed not only from a cell but also from a slice: + +```tolk +val smth = Something.fromSlice(s); +``` + +Auto-serialization works at low-level also: by analogy with `loadUint()` and others, there is a `loadAny()` method: + +```tolk +val smth = s.loadAny(); +// or even +val smth: Something = s.loadAny(); // T=Something deduced +``` + + + +Similarly, `storeAny()` for a builder accepts any serializable value: + +```tolk +beginCell() + .storeAddress(dest) + .storeAny(smth) // T=Something deduced + .storeUint(123, 8); +``` + +Furthermore, it works not only with structures but also with arbitrary types. + +```tolk +s.loadAny(); // same as loadInt(32) +s.loadAny<(coins, bool?)>(); // read a pair (a tensor) + +b.storeAny(someAddress); // same as storeAddress +b.storeAny(0xFF as uint8); // same as storeUint(0xFF, 8) +``` + +This approach allows both low-level and high-level intentions to be expressed uniformly. + +## Builders and slices can NOT be serialized + +Builders and slices are low-level primitives used for constructing and parsing cells. They contain raw binary data. +For this reason, attempting to read an arbitrary slice from another slice is impossible: how many bits should be read? + +```tolk +struct CantBeRead { + before: int8 + s: slice + after: int8 +} +``` + +An attempt to call `CantBeRead.fromCell(c)` will fire an error _"Can not be deserialized, because `CantBeRead.s` is `slice`"_. + +Express shape of data using the type system to make serialization distinct. For example, `s: bits100` if it's exactly 100 bits. + +## Type `bitsN`: fixed-size slices + +By analogy: `int` can not be serialized, but `int32` and `int64` can. +The same: `slice` can not be serialized, but `bits32` and `bytes8` can. +At runtime, `bitsN` is a TVM `SLICE`, like `int32` is a TVM `INT`. + +```tolk +struct OkayToRead { + before: int8 + s: bits100 + after: int8 +} + +fun read(c: cell) { + // a cell `c` is expected to be 116 bits + val r = OkayToRead.fromCell(c); + // on the stack: INT, SLICE, INT +} +``` + +**To cast `slice` to `bitsN`, use the unsafe `as` operator**. It's intentional, because slices may have refs, so explicit casting forces a programmer to think whether this transformation is valid. At runtime, it's no-op. + +```tolk +fun calcHash(raw: bits512) { + // ... +} + +fun demo() { + calcHash(someSlice); // error + calcHash(someSlice as bits512); // ok + + someBytes.loadAddress(); // error + (someBytes as slice).loadAddress(); // ok +} +``` + +## "The remaining" slice when reading + +A common pattern is to read a portion of data and then retrieve the remainder. With manual parsing, it happens naturally: + +```tolk +val ownerId = s.loadUint(32); +val dest = s.loadAddress(); +// `s` contains all bits/refs still unread +val payload = s; +``` + +To express the same with the type system use **a special type `RemainingBitsAndRefs`**: + +```tolk +struct WithPayload { + ownerId: uint32 + dest: address + payload: RemainingBitsAndRefs +} +``` + +Then, `obj = WithPayload.fromSlice(s)` will return an object, where `obj.payload` contains "all bits/refs left". +This is a special type: + +```tolk +// declared in stdlib, handled specially by the compiler +type RemainingBitsAndRefs = slice +``` + +Naturally, such a field must appear last in a struct: no more data exists after reading it. + +## Embedding constant slices into a contract + +A string literal is represented as a slice: + +```tolk +// `slice` with 4 bytes: 97,98,99,100 (0x61626364) +const SLICE1 = "abcd" +``` + +Also, use `stringHexToSlice("...")` to embed hexadecimal binary data: + +```tolk +// `slice` with 2 bytes: 16,32 (0x1020) +const SLICE2 = stringHexToSlice("1020") +``` + +TVM does not have string types; it operates solely on slices. [Read about emulating strings](/languages/tolk/types/strings). + +## Stack layout and serialization + +Both `cell` and `Cell` are backed by TVM `CELL`. Serialized as a reference; nullable are "maybe reference". + +The primitive types `builder` and `slice` cannot be serialized. Use `bitsN` and `RemainingBitsAndRefs`. + +For details, follow [TVM representation](/languages/tolk/types/overall-tvm-stack) and [Serialization](/languages/tolk/types/overall-serialization). diff --git a/languages/tolk/types/enums.mdx b/languages/tolk/types/enums.mdx index 41261580d..86daa8952 100644 --- a/languages/tolk/types/enums.mdx +++ b/languages/tolk/types/enums.mdx @@ -2,3 +2,142 @@ title: "Enums" --- +import { Aside } from '/snippets/aside.jsx'; + +Tolk supports enums, similar to TypeScript and C++ enums. + +In the TVM (virtual machine) all enums are integers. +From the compiler's point of view, it's a distinct type. + +```tolk +// will be 0 1 2 +enum Color { + Red + Green + Blue +} +``` + +## Enum members + +Values can be specified manually. Otherwise, auto-calculated as `+1`. + +```tolk +enum Mode { + Foo = 256, + Bar, // implicitly 257 +} +``` + +## Enums are distinct types, not integers + +`Color.Red` is `Color`, not `int`, although it holds the value `0` at runtime. + +```tolk +fun isRed(c: Color) { + return c == Color.Red +} + +fun demo() { + isRed(Color.Blue); // ok + isRed(1); // error, pass `int` to `Color` +} +``` + +Since enums are types, they can be: + +- used as variable and parameters, +- extended with methods for an enum, +- used in struct fields, unions, generics, and other type contexts: + +```tolk +struct Gradient { + from: Color + to: Color? = null +} + +fun Color.isRed(self) { + return self == Color.Red +} + +var g: Gradient = { from: Color.Blue }; +g.from.isRed(); // false +Color.Red.isRed(); // true + +match (g.to) { + null => ... + Color => ... +} +``` + +## `match` for enums is exhaustive + +Pattern matching on enums requires coverage of all cases: + +```tolk +match (someColor) { + Color.Red => {} + Color.Green => {} + // error: Color.Blue is missing +} +``` + +Alternatively, use `else` to handle remaining values: + +```tolk +match (someColor) { + Color.Red => {} + else => {} +} +``` + +Operator `==` compares values directly: + +```tolk +if (someColor == Color.Red) {} +else {} +``` + +## Enums are integers under the hood + +At the TVM level, every enum is represented as `int`. Casting between the enum and `int` is allowed: + +- `Color.Blue as int` evaluates to `2` +- `2 as Color` evaluates to `Color.Blue` + + + +During deserialization with `fromCell()`, the compiler performs checks to ensure that encoded integers correspond to valid enum values. + + + +## Enums are allowed in `throw` and `assert` + +```tolk +enum Err { + InvalidId = 0x100 + TooHighId +} + +fun validate(id: int) { + assert (id < 1000) throw Err.TooHighId; // excno = 257 +} +``` + +## Stack layout and serialization + +Every enum is backed by TVM `INT`. Serialized as `(u)intN` where `N` is: + +- specified manually: `enum Role: int8 { ... }` +- or calculated automatically to fit all values + +For details, follow [TVM representation](/languages/tolk/types/overall-tvm-stack) and [Serialization](/languages/tolk/types/overall-serialization). diff --git a/languages/tolk/types/generics.mdx b/languages/tolk/types/generics.mdx index 21582ea51..4349ddc13 100644 --- a/languages/tolk/types/generics.mdx +++ b/languages/tolk/types/generics.mdx @@ -3,3 +3,110 @@ title: "Generic structs and aliases" sidebarTitle: "Generics" --- +Generic structs and type aliases exist only at the type level and incur no runtime cost. + +```tolk +struct Container { + element: T? +} + +struct Nothing + +type Wrapper = Nothing | Container +``` + +Example usage: + +```tolk +fun checkElement(c: Container) { + return c.element != null; +} + +fun demo() { + var c: Container = { element: null }; + + var n: Wrapper = Nothing {}; + var i: Wrapper = Container { element: 0 }; +} +``` + +## Type arguments must be specified + +When referencing a generic `Container`, `T` must be provided explicitly: + +```tolk +fun getItem(c: Container) // error +fun getItem(c: Container) // ok +fun getItem(c: Container) // ok +``` + +For generic functions, type arguments can be inferred from the call site: + +```tolk +fun doSmth(value: Container) { + // ... +} + +fun demo() { + doSmth({ element: 123 }); // T = int + doSmth({ element: cellOrNull }); // T = cell? +} +``` + +## Default type arguments + +A type parameter can have a default type: ``. Such type parameters may be omitted at use sites. + +```tolk +struct StrangeUnion { + item: T1 | T2 +} + +fun demo() { + var m1: StrangeUnion = { item: 10 }; + var m2: StrangeUnion = { item: 20 }; + // m1.item is `int?` + // m2.item is `int | bool` +} +``` + +Type arguments cannot currently reference one another; expressions such as `T2 = T1` are invalid. + +## Not only structs, but also type aliases + +The following example demonstrates a generic type alias `Response`: + +```tolk +struct Ok { result: TResult } +struct Err { err: TError } + +type Response = Ok | Err + +fun loadNextRef(mutate s: slice): Response { + return s.remainingRefsCount() + ? Ok { result: s.loadRef() } + : Err { err: ERR_NO_MORE_REFS } +} + +fun demo() { + match (val r = loadNextRef(mutate parsedSlice)) { + Ok => { r.result } // cell + Err => { r.err } // int32 + } +} +``` + +## Methods for generic types + +Methods for generics are declared exactly as for regular structures. +In this form, the compiler treats `T` (unknown symbol) as a type parameter: + +```tolk +fun Container.getElement(self) { + return self.element +} +``` + +Notice the first `self` parameter. Without it, a method will be static. + +See [Functions and methods](/languages/tolk/syntax/functions-methods) for examples. diff --git a/languages/tolk/types/list-of-types.mdx b/languages/tolk/types/list-of-types.mdx index 6dca357ed..72a6fba60 100644 --- a/languages/tolk/types/list-of-types.mdx +++ b/languages/tolk/types/list-of-types.mdx @@ -3,3 +3,38 @@ title: "Type system overview" sidebarTitle: "List of types" --- +The Tolk type system is designed with the specifics of TON in mind. + +Since on-chain data and communication rely entirely on cells, +the system focuses on binary serialization and clear data relationships. + +Programs run on a stack-based virtual machine ([TVM](/tvm/overview)), +which imposes specific rules on how values are represented at runtime. + +This section describes all the types in Tolk language: + +- [numbers](/languages/tolk/types/numbers) — int, int32, uint64, coins, etc. +- [boolean](/languages/tolk/types/booleans) — true/false and logical operators +- [address](/languages/tolk/types/address) — internal, external, and none addresses +- [cells](/languages/tolk/types/cells) — and also slices, builders, and raw bits +- [strings](/languages/tolk/types/strings) — not a native type, emulated using slices +- [structures](/languages/tolk/types/structures) — group several fields into one entity +- [type aliases](/languages/tolk/types/aliases) — similar to TypeScript and Rust +- [generics](/languages/tolk/types/generics) — any struct can be generic \ +- [enums](/languages/tolk/types/enums) — a distinct type containing integer variants +- [nullable types](/languages/tolk/types/nullable) — with null safety and smart casts +- [union types](/languages/tolk/types/unions) — a variable holds one of possible values +- [tensors](/languages/tolk/types/tensors) — multiple values placed sequentially on the stack +- [tuples](/languages/tolk/types/tuples) — multiple values stored in a single TVM tuple +- [maps](/languages/tolk/types/maps) — key-value dictionaries +- [callables](/languages/tolk/types/callables) — first-class functions +- [void and never](/languages/tolk/types/void-never) — both mean "absence of a value" + +An article ["Type checks and casts"](/languages/tolk/types/type-checks-and-casts) +describes casting types with unsafe `as` operator. + +For a summary of how types are represented on a TVM stack, +follow [Overall: TVM stack representation](/languages/tolk/types/overall-tvm-stack). + +For a summary of how types are serialized and their relation to TL/B, +follow [Overall: serialization](/languages/tolk/types/overall-serialization). diff --git a/languages/tolk/types/maps.mdx b/languages/tolk/types/maps.mdx index 53df0f1b2..ef18ab6bf 100644 --- a/languages/tolk/types/maps.mdx +++ b/languages/tolk/types/maps.mdx @@ -3,3 +3,365 @@ title: "Maps (key-value)" sidebarTitle: "Maps" --- +import { Aside } from '/snippets/aside.jsx'; + +Tolk supports `map` — a high‑level type that encapsulates TVM dictionaries: + +- Any serializable keys and values. +- Natural syntax for iterating forwards, backwards, or starting from a specified key. +- Zero overhead compared to low-level approach. + +## Create an empty map + +```tolk +var m: map = createEmptyMap(); +// or +var m = createEmptyMap(); +``` + +A map is a dedicated type, it may be used in parameters, fields, etc.: + +```tolk +struct Demo { + m: map +} + +fun create(): Demo { + return { + m: createEmptyMap() + } +} +``` + +## Add values to a map + +Use `m.set(k, v)` and other methods suggested by an IDE after a dot (a full list is available below): + +```tolk +var m: map = createEmptyMap(); +m.set(1, 10); +m.addIfNotExists(2, -20); +m.replaceIfExists(2, 20); +m.delete(2); // now: [ 1 => 10 ] + +m.exists(1); // true +m.exists(2); // false +``` + +## Get a value by key + +`m.get(key)` returns `isFound + loadValue()`: + +```tolk +var r = m.get(1); +if (r.isFound) { + val v = r.loadValue(); // 10 +} +``` + +Note: check **not `r == null`, but `r.isFound`**. In other words, `map.get(key)` returns not `V?`, but a special result. + +Also use `m.mustGet(key)` that returns `V` and throws if the key is missing: + +```tolk +m.mustGet(1); // 10 +m.mustGet(100500); // runtime error +``` + +## Iterate forward and backward + +There is no dedicated `foreach` syntax. Iteration follows this pattern: + +- define the starting key: `r = m.findFirst()` or `r = m.findLast()` +- while `r.isFound`: + - use `r.getKey()` and `r.loadValue()` + - move the cursor: `r = m.iterateNext(r)` or `r = m.iteratePrev(r)` + +Example: iterate all keys forward + +```tolk +// suppose there is a map [ 1 => 10, 2 => 20, 3 => 30 ] +// this function will print "1 10 2 20 3 30" +fun iterateAndPrint(m: map) { + var r = m.findFirst(); + while (r.isFound) { + debug.print(r.getKey()); + debug.print(r.loadValue()); + r = m.iterateNext(r); + } +} +``` + +Example: iterate backwards from keys ≤ 2 + +```tolk +// suppose `m` is `[ int => address ]` and already populated +// for every key<=2, print addr.workchain +fun printWorkchainsBackwards(m: map) { + var r = m.findKeyLessOrEqual(2); + while (r.isFound) { + val a = r.loadValue(); // it's `address` + debug.print(a.getWorkchain()); + r = m.iteratePrev(r); + } +} +``` + +## Check if a map is empty + +```tolk +m.isEmpty() // not `m == null` +``` + + + +## Allowed types for K and V + +All the following key and value types are valid: + +```tolk +map +map +map> +map> +map +``` + +Some types are not allowed. General rules: + +- Keys must be fixed-width and contain zero references + - Valid: `int32`, `address`, `bits256`, `Point` + - Invalid: `int`, `coins`, `cell` +- Values must be serializable + - Valid: `coins`, `AnyStruct`, `Cell` + - Invalid: `int`, `builder` + +In practice, keys are most commonly `intN`, `uintN`, or `address`. Values can be any serializable type. + +## Available methods for maps + +An IDE suggests available methods after a dot. Most methods are self-explanatory. + +- `createEmptyMap(): map` + +Returns an empty typed map. Equivalent to `PUSHNULL` since TVM `NULL` represents an empty map. + +- `createMapFromLowLevelDict(d: dict): map` + +Converts a low-level TVM dictionary to a typed map. Accepts an optional cell and returns the same optional cell. +Mismatched key or value types result in failures when calling `map.get` or related methods. + +- `m.toLowLevelDict(): dict` + +Converts a high-level map to a low-level TVM dictionary. Returns the same optional cell. + +- `m.isEmpty(): bool` + +Checks whether a map is empty. Use `m.isEmpty()` instead of `m == null`. + +- `m.exists(key: K): bool` + +Checks whether a key exists in a map. + +- `m.get(key: K): MapLookupResult` + +Gets an element by key. Returns `isFound = false` if key does not exist. + +- `m.mustGet(key: K, throwIfNotFound: int = 9): V` + +Gets an element by key and throws if it does not exist. + +- `m.set(key: K, value: V): self` + +Sets an element by key. Since it returns `self`, calls may be chained. + +- `m.setAndGetPrevious(key: K, value: V): MapLookupResult` + +Sets an element and returns the previous element. If no previous element, `isFound = false`. + +- `m.replaceIfExists(key: K, value: V): bool` + +Sets an element only if the key exists. Returns whether an element was replaced. + +- `m.replaceAndGetPrevious(key: K, value: V): MapLookupResult` + +Sets an element only if the key exists and returns the previous element. + +- `m.addIfNotExists(key: K, value: V): bool` + +Sets an element only if the key does not exist. Returns true if added. + +- `m.addOrGetExisting(key: K, value: V): MapLookupResult` + +Sets an element only if the key does not exist. If exists, returns an old value. + +- `m.delete(key: K): bool` + +Deletes an element by key. Returns true if deleted. + +- `m.deleteAndGetDeleted(key: K): MapLookupResult` + +Deletes an element by key and returns the deleted element. If not found, `isFound = false`. + +- `m.findFirst(): MapEntry` +- `m.findLast(): MapEntry` + +Finds the first (minimal) or last (maximal) element. +For integer keys, returns minimal (maximal) integer. +For addresses or complex keys (represented as slices), returns lexicographically smallest (largest) key. +Returns `isFound = false` when the map is empty. + +- `m.findKeyGreater(pivotKey: K): MapEntry` +- `m.findKeyGreaterOrEqual(pivotKey: K): MapEntry` +- `m.findKeyLess(pivotKey: K): MapEntry` +- `m.findKeyLessOrEqual(pivotKey: K): MapEntry` + +Finds an element with key compared to pivotKey. + +- `m.iterateNext(current: MapEntry): MapEntry` +- `m.iteratePrev(current: MapEntry): MapEntry` + +Iterates over a map in ascending (descending) order. + +## Augmented hashmaps and prefix dictionaries + +These structures are rarely used and are not part of the Tolk type system. + +- Prefix dictionaries: `import @stdlib/tvm-dicts` and use assembly functions. +- Augmented hashmaps and Merkle proofs: implement interaction manually. + +## Keys are auto-serialized + +At the TVM level, keys can be numbers or slices. Complex keys, such as `Point`, are automatically serialized and deserialized by the compiler. + +```tolk +struct Point { + x: int8 + y: int8 +} + +fun demo(m: map) { + // a key is automatically packed to a 16-bit slice + m.set({x: 10, y: 20}, 123); + // and unpacked back to `Point` + return m.findFirst().key; +} +``` + +If a key is a struct with a single intN field, it behaves like a number. + +```tolk +struct UserId { + v: int32 +} + +struct Demo { + // equal to K=int32 without extra serialization + m: map +} +``` + +## How to emulate `Set` with maps + +Use an "empty tensor" as a type for `V`: + +```tolk +type Set = map +``` + +It will work, a bit noisy. +Lots of methods for maps are just inapplicable to sets, so its "public interface" is wrong. +Sets have a much simpler API, literally 4 functions. +It's better to create a simple wrapper: + +```tolk +struct Set { + private m: map +} + +fun Set.add(self, value: T) { /* ... */ } +// etc. +``` + +## Low-level: why "isFound" but not "optional value"? + +There are two reasons for this design: + +- Gas consumption (zero overhead) +- Nullable values can be supported, like `map` or `map`. Returning `V?`, makes it impossible to distinguish between "key exists but value is null" and "key does not exist". + + + +TVM dictionaries store binary data. Having a `map` and doing `m.set(k, 10)`, this "10" is actually 0x0000000A (automatically packed by the compiler). +All TVM instructions for reading return slices, so at some point, those bits should be decoded back to "10". + +TVM instructions put two values on a stack: `(slice -1)` or `(null 0)`. If a choice is to return `V?`, the compiler needs to do something like + +```ansi +IF stack[0] == -1: + decode stack[1] to V + transform V to V? +ELSE: + drop stack[1] + transform null to V? +``` + +Then, at usage, it's compared null: + +```tolk +val v = m.get(k); // internally, IF ELSE: for decoding +if (v != null) { // one more IF: for checking + ... +} +``` + +So, it results in two runtime checks and three TVM continuations. + +That's why instead of `V?`, a special struct is returned: + +```tolk +fun map.get(self, key: K): MapLookupResult; + +struct MapLookupResult { + private readonly rawSlice: slice? + isFound: bool +} + +fun MapLookupResult.loadValue(self): TValue { + return TValue.fromSlice(self.rawSlice!) +} +``` + +This struct directly maps onto the TVM stack: `(slice -1)` or `(null 0)`. +The condition `if (r.isFound)` naturally checks the top element (automatically popped). +Followed by auto-deserialization at `r.loadValue()` when `rawSlice` is left on the top. + +Moreover, some other functions return the same struct. For example, `m.setAndGetPrevious`: + +```tolk +val prev = m.setAndGetPrevious(1, 100500); +// NOT `if (prev != null)`, but +if (prev.isFound) { + prev.loadValue() // 10 +} +``` + +Overall, this provides zero overhead compared to plain dictionaries. + +## Stack layout and serialization + +An empty map is backed by TVM `NULL`, serialized as '0'. A non-empty map is TVM `CELL`, serialized as '1'+ref. + +For details, follow [TVM representation](/languages/tolk/types/overall-tvm-stack) and [Serialization](/languages/tolk/types/overall-serialization). diff --git a/languages/tolk/types/nullable.mdx b/languages/tolk/types/nullable.mdx index ec2298a98..5abe64d72 100644 --- a/languages/tolk/types/nullable.mdx +++ b/languages/tolk/types/nullable.mdx @@ -3,3 +3,158 @@ title: "Nullable types, null safety" sidebarTitle: "Nullable types" --- +Tolk supports nullable types `T?`, a shorthand for `T | null`. +Any type can be made nullable: primitive types, structures, and other composites. +`null` cannot be assigned to a non-nullable type. + +## Null safety + +The compiler enforces null safety: nullable values cannot be accessed without an explicit null check. + +```tolk +var value = x > 0 ? 1 : null; // int? + +value + 5; // error +b.storeInt(value); // error +``` + +A check for `value != null` is required before: + +```tolk +if (value != null) { // value is `int` inside + value + 5; // ok + b.storeInt(value); // ok +} +``` + +When a variable has no explicit type, its type is inferred from the initial assignment and does not change. +Therefore, a variable must be declared explicitly as nullable when required: + +```tolk +var i = 0; +i = null; // error, can't assign `null` to `int` +i = maybeInt; // error, can't assign `int?` to `int` + +// correct form: +var i: int? = 0; +i = null; // ok +``` + +Similarly, when the initial value is `null`, its type must be specified: + +```tolk +var i: int? = null; +// or +var i = null as int?; +``` + +## Smart casts + +After a null check, the compiler automatically narrows the type. +This feature is known as "smart casts". It exists in TypeScript and Kotlin, and makes working with nullable types more natural. +Examples: + +```tolk +if (lastCell != null) { + // here lastCell is `cell`, not `cell?` +} +``` + +```tolk +if (lastCell == null || prevCell == null) { + return; +} +// both lastCell and prevCell are `cell` +``` + +```tolk +var x: int? = ...; +if (x == null) { + x = random(); +} +// here x is `int` +``` + +```tolk +while (lastCell != null) { + lastCell = lastCell.beginParse().loadMaybeRef(); +} +// here lastCell is definitely null +``` + +Smart casts apply to local variables, structure fields, and tensor/tuple indices. + +```tolk +struct A { + optionalId: int? +} + +fun demo(obj: A) { + if (obj.optionalId != null) { + // obj.optionalId is `int` here + } +} +``` + +Smart casts also work for initial values. +Even if a variable declared as `int?` but initialized with a number, it's a safe non-null integer: + +```tolk +var idx: int? = -1; +// idx is `int` +``` + +When a variable is definitely null, its type becomes `null`, meaning it can be safely passed to any nullable type: + +```tolk +fun takeOptSlice(s: slice?) {} + +fun demo() { + var i: int? = getOptInt(); + if (i == null) { + takeOptSlice(i); // ok: pass `null` to `slice|null` + } +} +``` + +## Operator `!` (non-null assertion) + +The `!` operator in Tolk is similar to `!` in TypeScript and `!!` in Kotlin. +It allows bypassing the compiler's check: + +```tolk +fun doSmth(c: cell) {} + +fun analyzeStorage(nCells: int, lastCell: cell?) { + if (nCells > 0) { // then lastCell is 100% not null + doSmth(lastCell!); // use ! for this fact + } +} +``` + +Sometimes "a developer knows better than the compiler". For example: + +```tolk +// this key 100% exists, so force `cell` instead of `cell?` +val mainValidators = blockchain.configParam(16)!; +``` + +Unlike local variables, global variables cannot be smart-cast. The `!` operator is the only way to drop nullability from globals: + +```tolk +global gLastCell: cell? + +fun demo() { + // we are sure that a global is set, convert to `cell` + doSmth(gLastCell!); +} +``` + +Therefore, `!` is useful when non-nullability is guaranteed by conditions outside the code itself. + +## Stack layout and serialization + +Atomics like `int?` or `cell?` / etc. are backed by either TVM `NULL` or a value. Non-atomics are tagged unions. +Serialized as: null -> '0', otherwise -> '1'+T, but `address?` is serialized specially. + +For details, follow [TVM representation](/languages/tolk/types/overall-tvm-stack) and [Serialization](/languages/tolk/types/overall-serialization). diff --git a/languages/tolk/types/numbers.mdx b/languages/tolk/types/numbers.mdx index bdb2871f2..a3e5f50c3 100644 --- a/languages/tolk/types/numbers.mdx +++ b/languages/tolk/types/numbers.mdx @@ -2,3 +2,205 @@ title: "Numbers" --- +import { Aside } from '/snippets/aside.jsx'; + +Tolk has several types for integers: + +- general `int` +- signed `int32`, `int256`, etc. — any `intN` (0 \< N ≤ 257) +- unsigned `uint64`, `uint119`, etc. — any `uintN` (0 \< N ≤ 256) +- `coins` (representing nanotoncoins, 1 TON = 10^9) +- (rarely used) `varintN` and `varuintN` (N = 16/32) + + + +## Syntax: decimal, hex, and binary literals + +All the constants below are just `int`: + +```tolk +const TEN = 0b1010; // binary +const MAX_UINT8 = 0xFF; // hex + +// integers up to 2^256-1 are allowed +const MAX_INT = 115792089237316195423570985008687907853269984665640564039457584007913129639935; +``` + +## `intN` describes serialization, `int` does not + +To automatically parse binary data, the compiler must correctly load and store integers. +When a contract schema is designed, it is described in terms such as "queryID is unsigned 64-bit, counterValue is 32-bit", and similar. +This is expressed directly in Tolk: + +```tolk +struct IncMessage { + queryID: uint64 + counterValue: int32 +} +``` + +As a result, `IncMessage` can be serialized to a cell and decoded back. + +A general-purpose type `int` is "an integer, but no information how to serialize it". Consider this struct: + +```tolk +struct Point { + x: int + y: int +} +``` + +Is it valid? Of course, it is! Creating a `Point` variable is pretty fine. +But a call `p.toCell()` gives an error: + +``` +error: auto-serialization via toCell() is not available for type `Point` + because field `Point.x` of type `int` can't be serialized + because type `int` is not serializable, it doesn't define binary width + hint: replace `int` with `int32` / `uint64` / `coins` / etc. +``` + +To make it serializable, replace `int` with a specific integer type. For example: + +```tolk +struct Point { + x: int8 + y: int8 +} +``` + +## Overflow happens only at serialization + +A natural question is: "What about overflow?" + +```tolk +var v: uint8 = 255; +v += 1; // ??? +``` + +The answer — **no runtime overflow or clamping**: + +- arithmetic works as usual – `v` becomes 256 +- no extra gas cost – no runtime bounds checks +- overflow only happens at serialization + +```tolk +struct Resp { + outValue: uint8 +} + +fun demo(resp: Resp) { + resp.outValue = v; // 256 (no error yet) + resp.toCell(); // a runtime "overflow" error +} +``` + + + +Analogy: Tolk has `mulDivFloor(x,y,z)`, which uses 513-bit precision internally to prevent rounding errors. +Similarly, overflow only occurs _at the boundary between the contract and the outside world_. + +## Generic `int` implicitly cast to and from any `intN` + +- arithmetic operations on `intN` degrade to `int` +- numeric literals (like 0, 100) are just `int` +- direct assignment between `intN` and `intM` is disallowed (as a probable error) + +```tolk +fun takeAnyInt(a: int) { /* ... */ } +fun getAnyInt(): int { /* ... */ } + +fun f(op: int32, qid: uint64) { + op = qid; // error + op = qid as int32; // ok + + op + query_id; // ok, int + if (op == qid) // ok, not assignment + + takeAnyInt(op); // ok + op = getAnyInt(); // ok + + var amount: int32 = 1000; + var percent: uint8 = 50; + var new = amount * percent / 100; // ok, int + amount = new; // ok, int auto-casted to int32 +} +``` + +## Type `coins` and function `ton("0.05")` + +Similar to `int32`, Tolk has a dedicated `coins` type representing nanoton values. + +**The purpose of `coins` is to have special serialization rules**. It's serialized as "variadic integer": +small values consume a few bits, large values consume more. + +- arithmetic with `coins` degrades to `int`, similar to `intN` (except addition/subtraction) +- coins can be cast back from `int`, following the same rules as `intN` + +**The `ton("0.05")` built-in function** calculates "nanotoncoins" at compile-time. +It only accepts constant values (e.g., `ton(some_var)` is invalid). + +```tolk +const ONE_TON = ton("1"); // `coins`, value: 1000000000 + +fun calcCost() { + val cost = ton("0.05"); // `coins`, value: 50000000 + return ONE_TON + cost; +} +``` + +## All of them are first-class types + +At runtime, all integers are 257-bit. But they are different from the type system's perspective. + +For example, they can be nullable, combined within a union, and so on: + +```tolk +struct Demo { + f1: int32? // nullable + f2: int32 | uint64 // union (yes, it's correct) + pair: (int8, coins) +} + +fun demo(d: Demo) { + if (d.f1 != null) { + d.f1 // smart cast to `int32` + } + d.pair.1 // `coins` +} +``` + +## No floating-point numbers + +Note that only integer types (257-bit) are supported by the virtual machine. There are no floating-point numbers. + +For example, monetary values are represented as nanotoncoins: + +```tolk +const MIN_BALANCE = ton("1.23") // 1230000000 +``` + +## Stack layout and serialization + +All numeric types are backed by TVM `INT`. Serialization happens as follows: + +- `int` — not serializable, use `intN` and other types +- `intN` — a fixed N-bit signed integer +- `uintN` — a fixed N-bit unsigned integer +- `coins` — alias to `varuint16` +- `varintN` — variadic N 16/32: 4/5 bits for len + `8*len`-bit number +- `varuintN` — the same, but unsigned + +For details, follow [TVM representation](/languages/tolk/types/overall-tvm-stack) and [Serialization](/languages/tolk/types/overall-serialization). diff --git a/languages/tolk/types/overall-serialization.mdx b/languages/tolk/types/overall-serialization.mdx new file mode 100644 index 000000000..86a406573 --- /dev/null +++ b/languages/tolk/types/overall-serialization.mdx @@ -0,0 +1,380 @@ +--- +title: "Overall: serialization" +sidebarTitle: "Overall: serialization" +--- + +import { Aside } from '/snippets/aside.jsx'; + +A consolidated summary of how each Tolk type is serialized into TL-B–compatible binary data. + + + +## `int` + +Not serializable; use `intN` or other numeric types instead. + +## `intN` + +- fixed N-bit signed integer +- TL-B `intN` +- stored via `{N} STI` +- loaded via `{N} LDI` + +## `uintN` + +- fixed N-bit unsigned integer +- TL-B `uintN` +- stored via `{N} STU` +- loaded via `{N} LDU` + +## `coins` + +- alias to `varuint16` +- TL-B `VarUInteger 16` +- stored via `STGRAMS` +- loaded via `LDGRAMS` + +## `varintN` for N = 16 or N = 32 + +- variadic signed integer: 4/5 bits for len + 8\*len bit number +- TL-B `VarInteger {N}` +- stored via `STVARINT{N}` +- loaded via `LDVARINT{N}` + +## `varuintN` for N = 16 or N = 32 + +- variadic unsigned integer: 4/5 bits for len + 8\*len bit number +- TL-B `VarUInteger {N}` +- stored via `STVARUINT{N}` +- loaded via `LDVARUINT{N}` + +## `bool` + +- one bit: '0' or '1' +- TL-B `Bool` +- stored via `1 STI` +- loaded via `1 LDI` resulting in 0 or -1 + +## `address` + +- standard (internal) address; 267 bits: 0b100 + workchain + hash +- TL-B `addr_std` +- stored via `STSTDADDR` +- loaded via `LDSTDADDR` + +## `address?` (nullable) + +- internal or none address; 2 or 267 bits: null -> '00', otherwise -> address +- TL-B `addr_none` or `addr_std` +- stored via `STOPTSTDADDR` +- loaded via `LDOPTSTDADDR` + +## `any_address` + +- any valid TL-B address, from 2 to 523 bits +- TL-B `MsgAddress` +- stored via `STSLICE` +- loaded via `LDMSGADDR` + +## `cell` and `Cell` + +- a reference +- TL-B `^Cell` / `^T` +- stored via `STREF` +- loaded via `LDREF` + +## `cell?` and `Cell?` (nullable) + +- maybe reference ('0' or '1'+ref) +- TL-B `Maybe ^Cell` / `Maybe ^T` +- stored via `STOPTREF` +- loaded via `LDOPTREF` + +## `bitsN` + +- just N bits +- TL-B `bitsN` +- stored via `STSLICE`, preceded by a runtime check that the slice contains exactly N bits and zero references; this check can be disabled using `skipBitsNValidation = false`. +- loaded via `LDSLICE` / `LDSLICEX` (for N > 256) + +## `RemainingBitsAndRefs` + +- the remainder of a slice when reading, and a raw slice when writing +- TL-B `Cell` +- stored via `STSLICE` +- loaded by copying current slice and assigning current to an empty one + +## `builder` and `slice` + +Can be used for writing, not for reading. +Not recommended, because they do not reveal internal structure and have unpredictable size. +Auto-generated TypeScript wrappers are not able to parse them. + +## Structures + +If a struct has a prefix, it's written first. Then its fields are serialized sequentially. + +```tolk +struct (0x12345678) A { + a: int8 + b: cell? +} + +fun demo() { + val a: A = { + a: 123, + b: createEmptyCell(), + }; + // 41 bits and 1 ref: opcode + int8 + '1' + empty ref + a.toCell() +} +``` + +### 32-bit prefixes (opcodes) + +By convention, all messages (incoming and outgoing) use 32-bit prefixes: + +```tolk +struct (0x7362d09c) TransferNotification { + queryId: uint64 + // ... +} +``` + +### Not only 32-bit prefixes + +Declaring messages with opcodes does not differ from declaring any other structs. Prefixes can be of any width: + +- `0x000F` — 16-bit prefix +- `0x0F` — 8-bit prefix +- `0b010` — 3-bit prefix +- `0b00001111` — 8-bit prefix + +Example. Let's express the following TL-B scheme: + +```tl-b +asset_simple$001 workchain:int8 ptr:bits32 = Asset; +asset_booking$1000 order_id:uint64 = Asset; +// ... +``` + +In Tolk, use structures and union types: + +```tolk +struct (0b001) AssetSimple { + workchain: int8 + ptr: bits32 +} + +struct (0b1000) AssetBooking { + orderId: uint64 +} + +type Asset = AssetSimple | AssetBooking // | ... +``` + +When deserializing, `Asset` will follow manually provided prefixes, see "union types" below. + +If a structure has a prefix, it is used consistently in all contexts (both standalone and within unions): + +```tolk +AssetBooking.fromSlice(s) // expecting '1000...' (binary) +AssetBooking{...}.toCell() // '1000...' +``` + +## Type aliases + +A type alias is identical to its underlying type unless a custom serializer is defined. + +Example. Need to implement a "variadic string" encoded as "len + data": + +``` +len: (## 8) // 8 bits of len +data: (bits len) // 0..255 bits of data +``` + +To express this, create a `type` and **define a custom serializer**: + +```tolk +type ShortString = slice + +fun ShortString.packToBuilder(self, mutate b: builder) { + val nBits = self.remainingBitsCount(); + b.storeUint(nBits, 8); + b.storeSlice(self); +} + +fun ShortString.unpackFromSlice(mutate s: slice) { + val nBits = s.loadUint(8); + return s.loadBits(nBits); +} +``` + +And just use `ShortString` as a regular type — everywhere: + +```tolk +tokenName: ShortString +fullDomain: Cell +``` + +Method names `packToBuilder` and `unpackFromSlice` are reserved for this purpose, their signatures must match exactly as shown. + +## Enums + +The serialization type can be specified manually: + +```tolk +// `Role` will be (un)packed as `int8` +enum Role: int8 { + Admin, + User, + Guest, +} + +struct ChangeRoleMsg { + ownerAddress: address + newRole: Role // int8: -128 <= V <= 127 +} +``` + +Otherwise, it is calculated automatically. +For `Role` above, `uint2` is sufficient to fit values `0, 1, 2`: + +```tolk +// `Role` will (un)packed as `uint2` +enum Role { + Admin, + User, + Guest, +} +``` + +Input values are validated during deserialization. +For `enum Role: int8` any (input\<0 || input>2) triggers exception 5 (integer out of range). + +Non-range values are also validated: + +```tolk +enum OwnerHashes: uint256 { + id1 = 0x1234, + id2 = 0x2345, + ... +} + +// on serialization, just "store uint256" +// on deserialization, "load uint256" + throw 5 if v not in [0x1234, 0x2345, ...] +``` + +## Nullable types `T?` (except `address?`) + +- often called "Maybe"; '0' or '1'+T +- TL-B `(Maybe T)` +- asm `1 STI` + IF ... +- asm `1 LDI` + IF ... + +The exception: `address?` is serialized as "internal or none" (2/267 bits): null -> '00', otherwise -> address. + +## Union types `T1 | T2 | ...` + +Rules for union type serialization: + +- `T | null` is TL/B `Maybe T` ('0' or '1'+T) +- if all `T_i` have prefixes `struct (0x1234) A`, they are used +- otherwise, a compiler auto-generates a prefix tree + +### Manual serialization prefixes + +If all `T_i` have manual prefixes, they are used: + +```tolk +struct (0b001) AssetSimple { /* body1 */ } +struct (0b1000) AssetBooking { /* body2 */ } +struct (0b01) AssetNothing {} + +struct Demo { + // '001'+body1 OR '1000'+body2 + e: AssetSimple | AssetBooking + // '001'+body1 OR '1000'+body2 OR '01' + f: AssetSimple | AssetBooking | AssetNothing +} +``` + +If a prefix exists for `A` but not for `B`, the union `A | B` cannot be serialized: it seems like a bug in code. + +### Auto-generated prefix tree + +If `T_i` don't have manual prefixes, the compiler generates a prefix tree. + +A two-component union `T1 | T2` is TL/B `Either` (prefixes 0/1). +For example, `int32 | int64` becomes ('0'+int32 or '1'+int64). + +Multi-component unions have longer prefixes. +For example `int32 | int64 | int128 | int256` forms a tree 00/01/10/11. +General rules: + +- if `null` exists, it's 0, all others are 1+tree ("maybe others") + - example: `A|B|C|D|null` => 0 | 100+A | 101+B | 110+C | 111+D +- if no `null`, just distributed sequentially + - example: `A|B|C` => 00+A | 01+B | 10+C + +```tolk +struct WithUnion { + f: int8 | int16 | int32 +} +``` + +This field will be packed as: '00'+int8 OR '01'+int16 OR '10'+int32. +On deserialization, the same format is expected (prefix '11' will throw an exception). + +Same for structs without a manual prefix: + +```tolk +struct A { ... } // 0x... prefixes not specified +struct B { ... } +struct C { ... } + +struct WithUnion { + // auto-generated prefix tree: 00/01/10 + f: A | B | C + // with null, like Maybe: 0/10/11 + g: A | B | null + // even this works; when '11', a ref exists + h: A | int32 | C | cell +} +``` + +## Tensors `(T1, T2, ...)` + +Tensor components are serialized sequentially, in the same manner as structure fields. + +## `tuple` and typed tuples + +Tuples cannot be serialized; serialization is not implemented for tuples. + +But tuples can be returned from get methods, since contract getters work via the stack, not serialization. + +## `map` + +- maybe reference: '0' (empty) or '1'+ref (dict contents) +- TL-B `HashmapE n X` (follow [hashmaps in TL-B](/languages/tl-b/complex-and-non-trivial-examples#hashmap)) +- stored via `STDICT` +- loaded via `LDDICT` + +## Callables `(...ArgsT) -> ResultT` + +Callables cannot be serialized. + +Lambdas may be used within contract logic but cannot be serialized for off‑chain responses. + +## See also + +- [Overall: TVM stack representation](/languages/tolk/types/overall-tvm-stack) +- [Type system overview](/languages/tolk/types/list-of-types) +- [Automatic serialization](/languages/tolk/features/auto-serialization) diff --git a/languages/tolk/types/overall-tvm-stack.mdx b/languages/tolk/types/overall-tvm-stack.mdx new file mode 100644 index 000000000..059e8bee3 --- /dev/null +++ b/languages/tolk/types/overall-tvm-stack.mdx @@ -0,0 +1,254 @@ +--- +title: "Overall: TVM stack representation" +sidebarTitle: "Overall: TVM stack" +--- + +import { Aside } from '/snippets/aside.jsx'; + +A consolidated summary of how each Tolk type is represented on the stack. + + + +## `int`, `intN`, `coins` + +All numeric types are backed by TVM `INT`. + +A reminder: `intN` uses full 257‑bit precision, so any integer value fits into it. +Overflow happens only at serialization. + +## `bool` + +Type `bool` is backed by TVM `INT` with value `-1` or `0` at runtime. + +The unsafe cast `someBool as int` is valid and produces `-1` or `0`. + +## `address` and `any_address` + +Addresses are backed by TVM `SLICE` values containing raw binary data. + +A nullable `address?` is either TVM `NULL` or `SLICE`. + +The unsafe cast `someAddr as slice` and back is valid. + +## `cell` + +Type `cell` is backed by TVM `CELL`. + +The unsafe cast `someCell as Cell` is valid. + +## `Cell` + +Type `Cell` is also backed by TVM `CELL`. The type parameter `T` is purely compile‑time metadata. + +## `slice` + +Type `slice` is backed by TVM `SLICE`. + +## `bitsN` + +Type `bitsN` is backed by TVM `SLICE`. + +The unsafe cast `someSlice as bitsN` and back is valid. + +## `RemainingBitsAndRefs` + +Type "remaining" is backed by TVM `SLICE`. It's actually an alias for `slice`, handled specially at deserialization. + +## `builder` + +Type `builder` is backed by TVM `BUILDER`. Note that already written bits cannot be read. The only possible way to access builder's data is converting it to a slice. + +## Structures + +Fields of a structure are placed sequentially on the stack. For example, `Point` occupies two stack slots, and `Line` — four: + +```tolk +struct Point { + x: int + y: int +} + +struct Line { + start: Point + end: Point +} +``` + +When constructing a `Line` value, four integers are placed onto the stack: + +```tolk +fun generateLine() { + val l: Line = { + start: { x: 10, y: 20 }, + end: { x: 30, y: 40 }, + }; + // on a stack: 10 20 30 40 + return l; +} +``` + +Therefore, single‑field structures have no overhead compared to plain values. + +## Enums + +Every enum is backed by TVM `INT`. Tolk supports integer enums only (not addresses, for example). + +## Nullable types `T?` + +Atomics like `int?` / `address?` / `cell?` / etc. occupy a single stack slot: it holds either TVM `NULL` or a value. + +```tolk +fun demo(maybeAddr: address?) { + // maybeAddr is one stack slot: `NULL` or `SLICE` +} +``` + +A nullable structure with one primitive non-nullable field can be also optimized this way: + +```tolk +struct MyId { + value: int32 +} + +fun demo(maybeId: MyId?) { + // maybeId is one stack slot: `NULL` or `INT` +} +``` + +Nullable values of multi‑slot types (e.g., `Point` or a tensor `(bool, cell)`) occupy N+1 slots: the last is used for _typeid_. + +```tolk +struct Point { + x: int + y: int +} + +fun demo(maybeP: Point?) { + // maybeP is 3 stack slots: + // when null: "NULL, NULL, INT (0)" + // when not: "INT (x), INT (y), INT (4567)" +} +``` + +For every nullable type, the compiler assigns a unique _typeid_ (e.g., 4567 for `Point`). +That _typeid_ is stored in an extra slot. Typeid for `null` is `0`. +Expressions such as `p == null` or `p is Point` check that _typeid_ slot. + +A tricky example. A struct below, being nullable, requires an extra stack slot: + +```tolk +struct Tricky { + opt: int? +} + +fun demo(v: Tricky?) { + // v occupies 2 stack slots, + // because either `v == null` or `v.opt == null`: + // when v == null: "NULL, INT (0)" + // when v != null: "INT/NULL, INT (2345)" +} +``` + +## Union types `T1 | T2 | ...` + +Unions are represented as "tagged unions": + +- each alternative type is assigned a unique _typeid_ (e.g., 1234 for `int`) +- the union occupies N+1 stack slots, where N is the maximum size of `T_i` +- (N+1)-th slot contains _typeid_ of the current value + +Thus, `match` is implemented as a comparison of the (N+1)-th slot, and passing/assigning a value is a bit of stack juggling. + +```tolk +fun complex(v: int | slice | (int, int)) { + // `v` is 3 stack slots: + // - int: (NULL, 100, 1234) + // - slice: (NULL, CS{...}, 2345) + // - (int, int): (200, 300, 3456) +} + +fun demo(someOf: int | slice) { + // `someOf` is 2 stack slots: value and type-id + // - int: (100, 1234) + // - slice: (CS{...}, 2345) + match (someOf) { + int => { // IF TOP == 1234 + // slot1 is TVM `INT`, can be used in arithmetics + } + slice => { // ELSE + // slot1 is TVM `SLICE`, can be used to loadInt() + } + } + + complex(v); // passes (NULL, v.slot1, v.typeid) + complex(5); // passes (NULL, 5, 1234) +} +``` + + + +`T | null` is called "nullable" and optimized for atomics: `int?` use a single slot. Non-atomics are handled generally, with _typeid_=0. + +## Tensors `(T1, T2, ...)` + +Tensor components are placed sequentially, identical to struct fields. + +For example, `(coins, Point, int?)` occupies 4 slots: "INT (coins), INT (p.x), INT (p.y), INT/NULL". + +```tolk +type MyTensor = (coins, Point, int?) + +fun demo(t: MyTensor) { + // t is 4 stack slots + val p = t.1; + // p is 2 stack slots +} +``` + +## `tuple` + +Type `tuple` is backed by TVM `TUPLE` — one stack slot regardless of the number of elements in it (up to 255). + +## Typed tuple `[T1, T2, ...]` + +A typed tuple is also TVM `TUPLE`. Its shape is known at compile-time, but at runtime it's the same `tuple`. + +```tolk +fun demo(t: [int, [int, int]]) { + // t is one stack slot (TVM `TUPLE`) + // t.0 is TVM `INT` + // t.1 is TVM `TUPLE` + return t.1.0; // asm "1 INDEX" + "0 INDEX" +} +``` + +## `map` + +Every map is one stack slot: either TVM `NULL` or `CELL`. + +Non‑empty maps (cells) have a non‑trivial bit‑level layout (follow [hashmaps in TL/B](/languages/tl-b/complex-and-non-trivial-examples#hashmap)). + +## Callables `(...ArgsT) -> ResultT` + +A callable and `continuation` is backed by TVM `CONT`. + +## `void` and `never` + +Both represent the absence of a value and occupy zero stack slots. + +For example, a `void` function does not place any value onto the stack. + +## See also + +- [Overall: serialization to binary data](/languages/tolk/types/overall-serialization) +- [Type system overview](/languages/tolk/types/list-of-types) diff --git a/languages/tolk/types/strings.mdx b/languages/tolk/types/strings.mdx index b4c04b399..d9d62e2b4 100644 --- a/languages/tolk/types/strings.mdx +++ b/languages/tolk/types/strings.mdx @@ -3,3 +3,168 @@ title: "Strings (actually, slices)" sidebarTitle: "Strings" --- +The TVM (virtual machine) has only `SLICE` (binary data). +**There are no usual "strings"** (likewise, no floating-point numbers). +However, several techniques exist to store and return string-like data on-chain. + +## Embedding addresses as strings + +Use `address("...")` to insert a constant standard address: + +```tolk +const REFUND_ADDR = address("EQCRDM9h4k3UJdOePPuyX40mCgA4vxge5Dc5vjBR8djbEKC5") +``` + +Its type is `address`, it has workchain and hash, but internally it's a TVM `SLICE`, with 267 bits and 0 refs. + +To return a human‑readable address, just `address`. +Client‑side libraries reconstruct and display addresses according to their own formatting rules. + +## Raw string literals + +The syntax `val s = "abcd"` is valid, but it does not produce a string; it produces a binary slice: each character is encoded with its _ASCII char no_. + +```tolk +// `slice` with 4 bytes: 97,98,99,100 (0x61626364) +const SLICE1 = "abcd" +``` + +Literals cannot exceed 127 ASCII characters, because a cell can contain up to 1023 bits. + +## Hex string literals + +Use `stringHexToSlice("...")` to embed hexadecimal binary data: + +```tolk +// `slice` with 2 bytes: 16,32 (0x1020) +const SLICE2 = stringHexToSlice("1020") +``` + +## Concatenating string literals + +An example for `"ab"` and `"cd"`. Because they are slices, concatenation is performed by via a builder: + +```tolk +val concat = beginCell() + .storeSlice("ab") + .storeSlice("cd") + .asSlice(); // builder-to-slice +``` + +## How to return a string from a contract + +A plain `slice` cannot be deserialized directly. +Like `int` — use `int32` / `coins` / etc. to specify encoding. +Similarly, string‑like data must use an explicit encoding to be stored on‑chain. + +### Way #1: fixed-size strings via `bitsN` + +If a response always contains 4 bytes of text, fixed‑size encodings may be used: `bits32` or `bytes4`. + +```tolk +struct Response1 { + someData: uint8 + str: bits32 +} + +fun generateResponse(): Response1 { + return { + someData: 0xFF, + str: "abcd" as bits32, + } +} +``` + +### Way #2: "snake string", or "tail string" + +"Snake strings" (also called "tail strings") are a standard encoding for arbitrary‑length strings, including those exceeding 127 characters. + +Snake encoding is: **store portion of data → store rest in a ref cell**. Here is a string `xxxxyyyyzzzz` split into 3 parts: + +```tolk +"xxxx".ref("yyyy".ref("zzzz")) +``` + +Such a chaining can easily be implemented: + +```tolk +struct Response2 { + someData: int8 + str: MyTailString +} + +fun slice.ref(self, other: slice): slice { + return beginCell() + .storeSlice(self).storeSlice(other).asSlice(); +} + +fun generateResponse(): Response2 { + return { + someData: 0xFF, + str: "xxxx".ref("yyyy".ref("zzzz")), + } +} +``` + +Then a cell will be `"FFxxxx".ref(...)`. So, it's named "a tail string" because it goes after existing data up to the end. + +`MyTailString` can be described in the simplest form as the remainder of a slice — `RemainingBitsAndRefs` (mentioned [in an article about cells](/languages/tolk/types/cells#%E2%80%9Dthe-remaining%E2%80%9D-slice-when-reading)). + +```tolk +type MyTailString = RemainingBitsAndRefs +``` + +When decoding on‑chain, no reference iteration is required. +Client‑side tooling can reconstruct and display the full string. + +### Way #3: variable-length encoding + +Variable‑length encodings may also be implemented manually. For example, short strings like "abcd" can be stored like + +- 8 bits for N = string length +- N bits of data + +This can be implemented using custom serializers: + +```tolk +type ShortString = slice + +fun ShortString.packToBuilder(self, mutate b: builder) { + val nBits = self.remainingBitsCount(); + b.storeUint(nBits, 8); + b.storeSlice(self); +} + +fun ShortString.unpackFromSlice(mutate s: slice) { + val nBits = s.loadUint(8); + return s.loadBits(nBits); +} +``` + +Then use it like a regular type: + +```tolk +struct Response3 { + someData: int8 + str: ShortString +} + +fun generateResponse(): Response3 { + return { + someData: 0xFF, + str: "abcd", + } +} +``` + +## Calculate hex / crc32 / etc. at compile-time + +There are several functions operating on strings: + +- `stringCrc32("some_str")` +- `stringCrc16("some_str")` +- `stringSha256("some_crypto_key")` +- `stringSha256_32("some_crypto_key")` +- `stringToBase256("AB")` + +For examples, see [standard library](/languages/tolk/features/standard-library), they are at the top. diff --git a/languages/tolk/types/structures.mdx b/languages/tolk/types/structures.mdx index e922f2e9f..dd0b9d4ef 100644 --- a/languages/tolk/types/structures.mdx +++ b/languages/tolk/types/structures.mdx @@ -2,3 +2,147 @@ title: "Structures" --- +Tolk supports structures — similar to TypeScript classes, but designed to operate on TVM. + +```tolk +struct Point { + x: int + y: int +} + +fun calcMaxCoord(p: Point) { + return p.x > p.y ? p.x : p.y +} + +fun demo() { + // declared using object-literal syntax + var p: Point = { x: 10, y: 20 }; + calcMaxCoord(p); + + // constructed using object-literal syntax + calcMaxCoord({ x: 10, y: 20 }); +} +``` + +## Two identical structures are not assignable + +```tolk +struct A { v: int } +struct B { v: int } + +fun acceptA(a: A) {} + +fun demo(a: A, b: B) { + b = a; // error, can not assign `A` to `B` + acceptA(b); // error, can not pass `B` to `A` +} +``` + +Even though `A` and `B` have identical body, they represent distinct types. +Analogy: **a struct behaves like a TypeScript `class`, not `interface`**. Typing is nominal, not structural. + +## The compiler infers the type from context + +In a snippet below, the compiler understands that `{ ... }` is `StoredInfo` because of parameter's type: + +```tolk +fun store(info: StoredInfo) { + // ... +} + +fun demo() { + store({ + counterValue: ..., + ownerAddress: ..., + }); +} +``` + +The same applies to return values and assignments: + +```tolk +fun loadData(): StoredInfo { + return { + counterValue: ..., + ownerAddress: ..., + } +} + +fun demo() { + var s: StoredInfo = { counterValue, ... }; + var s: (int, StoredInfo) = (0, { counterValue, ... }); +} +``` + +## Explicit type hints are also available + +Besides the plain `{ ... }` syntax, the form `StructName { ... }` may be used, similar to Rust. +The snippet below is equivalent to the above: + +```tolk +fun loadData() { + return StoredInfo { + counterValue: ..., + ownerAddress: ..., + } +} + +fun demo() { + var s = StoredInfo { counterValue, ... }; + var s = (0, StoredInfo { counterValue, ... }); +} +``` + +When neither contextual information nor an explicit type hint is available, the type cannot be inferred and an error is produced. + +```tolk +val o = { x: 10, y: 20 }; // error, what type is it? +``` + +## Methods for structures + +Methods are declared as extension functions, similar to Kotlin: + +```tolk +fun Point.calcSquare(self) { + return self.x * self.y +} +``` + +Notice the first `self` parameter. Without it, a method will be static. + +By default, `self` is immutable. The form `mutate self` enables mutation. + +Read [Functions and methods](/languages/tolk/syntax/functions-methods). + +## Prefixes do not affect typing or layout + +Syntax `struct (PREFIX) Name { ... }` specifies **a serialization prefix**. +It affects binary representation only, nothing else changes: + +```tolk +struct (0x12345678) CounterIncrement { + byValue: uint32 +} + +fun demo(inc: CounterIncrement) { + // `inc` has one field; the prefix is not a property + inc.byValue + // `inc` is still one TVM `INT` on the stack +} +``` + +## Syntax of structures + +- Shorthand syntax `{ x, y }` is available +- Default values for fields: `a: int32 = 10` +- `private` and `readonly` fields +- Serialization prefixes (opcodes) + +Read [Syntax of structures and fields](/languages/tolk/syntax/structures-fields). + +## Stack layout and serialization + +Fields are placed on the stack sequentially and are serialized in the same order. If a struct has a prefix, it is written first. + +For details, follow [TVM representation](/languages/tolk/types/overall-tvm-stack) and [Serialization](/languages/tolk/types/overall-serialization). diff --git a/languages/tolk/types/tensors.mdx b/languages/tolk/types/tensors.mdx index cf89f6762..9937487f7 100644 --- a/languages/tolk/types/tensors.mdx +++ b/languages/tolk/types/tensors.mdx @@ -3,3 +3,128 @@ title: "Tensors (not tuples!)" sidebarTitle: "Tensors" --- +import { Aside } from '/snippets/aside.jsx'; + +In Tolk, **a tensor** is `(T1, T2, ...)`. +For example, `(int, slice)` — two values one by one. +Large tensors are generally impractical; structures are typically more suitable. +Essentially, structures are named tensors. + + + +## Syntax `tensorVar.{i}` + +Access tensor components for reading and writing: + +```tolk +// v is `(int, int, builder)` +var v = (1, 2, beginCell()); + +// read +v.0; // 1 + +// modify +v.1 = 123; +v.2.storeInt(v.0, 16); +// v is now (1, 123, builder "0x0001") + +v.100500 // compilation error +``` + +It also works for nested tensors: + +```tolk +fun getNested(): (int, (bool, coins)) { + // ... +} + +fun demo() { + val v = getNested(); + v.1.0 // bool +} +``` + +## Structures as named tensors + +Struct `User` below and a tensor `(int, slice)` have identical stack layouts and serialization rules: + +```tolk +struct User { + id: int + name: slice +} +``` + +Furthermore, `obj.{field}` is equivalent to `tensorVar.{i}`: + +```tolk +struct Storage { + lastUpdated: int + owner: User +} + +fun demo(s: Storage) { + s.lastUpdated; // s.0 + s.owner.id; // s.1.0 +} +``` + +## Destructuring at assignment + +The following syntax is valid: + +```tolk +var (i, j) = (10, 20); +``` + +Actually, it's a 2-component tensor `(10, 20)` assigned to two variables: + +```tolk +var tensor = (10, 20); +var (i, j) = tensor; + +// more complex example +var (str, (i, j)) = ("abcd", (10, 20)); +``` + +A special placeholder `_` can be used on the left side: + +```tolk +var (_, j) = (10, 20); +// j = 20 +``` + +## Empty tensors are also valid values + +```tolk +val empty = (); +``` + +This is analogous to creating an instance of an empty struct. + + + +## Tensors vs tuples + +**A tuple** is a special TVM primitive: it is a dynamic container capable of holding multiple values within a single stack slot. + +For example, a tensor `(int, int, int)` is "3 integers on the stack", whereas a tuple with 3 integers is "one tuple on the stack". + +Tolk supports the `tuple` type. [Read about tuples](/languages/tolk/types/tuples). + +## Stack layout and serialization + +Values are placed on the stack sequentially and serialized in the same order. + +For details, follow [TVM representation](/languages/tolk/types/overall-tvm-stack) and [Serialization](/languages/tolk/types/overall-serialization). diff --git a/languages/tolk/types/tuples.mdx b/languages/tolk/types/tuples.mdx index 601ecbcc1..80991b786 100644 --- a/languages/tolk/types/tuples.mdx +++ b/languages/tolk/types/tuples.mdx @@ -3,3 +3,152 @@ title: "Tuples (not tensors!)" sidebarTitle: "Tuples" --- +import { Aside } from '/snippets/aside.jsx'; + +In TVM (the virtual machine), a **tuple** is a dynamic container that stores from 0 to 255 elements in a single stack slot. +Tolk supports two tuple forms: + +- `tuple` — a dynamic tuple with unknown shape +- `[T1, T2, ...]` — TVM tuple with known shape + + + +## `tuple`: dynamic tuples + +A TVM tuple allows storing from 0 to 255 elements in one stack slot. Push elements into a tuple: + +```tolk +fun demo(): tuple { + var t = createEmptyTuple(); + t.push(10); + t.push(beginCell()); + t.push(null); + + t.size(); // 3 + return t; +} +``` + +When calling `t.get(idx)`, the type must be provided explicitly or inferred: + +```tolk +val first = t.get(0); +// or +val first: int = t.get(0); // T=int auto-inferred +``` + +The syntax `tupleVar.{i}` is also permitted when the type is evident: + +```tolk +val first: int = t.0; +``` + +The method `set()` does not create new elements; it only updates existing ones. +If `idx` is out of bounds in `get()` or `set()`, an exception is thrown. + +```tolk +t.set(value, 100500); // throws errCode=5 +t.100500 = value; // throws errCode=5 +``` + +The following methods are available — `last()`, `set()`, `pop()`, etc. IDE will suggest them after a dot. + +## Only primitives can be placed into a tuple + +An attempt to call `t.push(somePoint)` will raise an error: only single‑slot values can be added and retrieved. + +```tolk +t.push(somePoint); // error +t.push(somePoint.x); // ok (int) +``` + +## Convert an object to a tuple and back + +Tolk provides built-in generic methods `T.toTuple()` and `T.fromTuple()`. +If a value occupies N stack slots, the resulting tuple has size N. + +```tolk +struct Point3d { + x: int + y: int + z: int +} + +fun demo(p: Point3d) { + val t = p.toTuple(); // a tuple with 3 elements + t.get(2); // z + p = Point3d.fromTuple(t); // back to a struct +} +``` + +## Typed tuples `[T1, T2, ...]` + +Use this syntax to describe a tuple with a known shape: + +```tolk +// its type is `[int, int, builder]` +val t = [1, 2, beginCell()]; + +// read +t.0; // 1 + +// modify +t.1 = 123; +t.2.storeInt(t.0, 16); +// t is now (1, 123, builder "0x0001") + +t.100500 // compilation error +``` + +This syntax is similar to a tensor `(T1, T2, ...)`, but all values are stored in a single stack slot. +Non-primitive values are also not permitted. + +A typed tuple may be destructured into multiple variables: + +```tolk +fun sumFirstTwo(t: [int, int, builder]) { + val [first, second, _] = t; + return first + second; +} +``` + +## Lisp-style lists + +Lisp-style lists are nested two-element tuples: `[1, [2, [3, null]]]` represents the list `[1, 2, 3]`. +An empty list is conventionally represented as `null`. +From the type‑system perspective, it's `tuple?` with dynamic contents, not a special type. + +It was a common pattern in old contracts, but now it's almost unused. +Its primary remaining use case is to return more than 255 elements. + +Lisp‑style lists can be processed using the following stdlib module: + +```tolk +import "@stdlib/lisp-lists" +``` + +## Tuples vs tensors + +**A tensor** is `(T1, T2, ...)` — multiple values in parentheses. +The primary distinction is the stack layout: a tuple is a single stack slot, +whereas tensor components are placed one by one. + +For example, `(coins, builder, int?)` occupies 3 slots, but `[coins, builder, int?]` — only one. + +Accessing `tupleVar.{i}` produces TVM asm `{i} INDEX`, but `tensorVar.{i}` is just stack shuffling. + +[Read about tensors](/languages/tolk/types/tensors). + +## Stack layout and serialization + +Tuples are backed by TVM `TUPLE`. +Serialization is not implemented, but tuples may be returned from get methods, because contract getters operate via the stack rather than serialization. + +For details, follow [TVM representation](/languages/tolk/types/overall-tvm-stack) and [Serialization](/languages/tolk/types/overall-serialization). diff --git a/languages/tolk/types/type-checks-and-casts.mdx b/languages/tolk/types/type-checks-and-casts.mdx index 84bb84835..1853f67d8 100644 --- a/languages/tolk/types/type-checks-and-casts.mdx +++ b/languages/tolk/types/type-checks-and-casts.mdx @@ -2,3 +2,125 @@ title: "Type checks and casts" --- +import { Aside } from '/snippets/aside.jsx'; + +Different types may be converted to one another — either automatically or using the unsafe `as` operator. + +## Non-assignable types give a compilation error + +Assignable types may be passed as arguments, assigned to fields, etc. This behavior follows conventional type-system rules. + +```tolk +fun takeOptInt(v: int?) {} + +fun main() { + takeOptInt(100); // ok, `int` to `int?` + takeOptInt(null); // ok, `null` to `int?` +} +``` + +Some conversions are performed automatically: `int` to `intN`, `AliasForT` and `T`, `Cell` to `cell`. + +```tolk +fun demo() { + var number: int32 = 100; // auto cast `int` to `int32` +} +``` + +Non-assignable types give a type checker error: + +```tolk +fun main() { + var number: int = true; +} +``` + +```ansi +file.tolk:2:23: error: can not assign `bool` to variable of type `int` + hint: use `as` operator for UNSAFE casting: ` as int` + + // in function `main` + 2 | var number: int = true; + | ^^^^ +``` + +In cases where non-trivial type transformations are disallowed, the unsafe operator `as` may be used to override the restriction. + +## Unsafe operator `as` + +For example, `bool` is not assignable to `int`, but a boolean is backed by TVM `INT` (-1 or 0), making the cast valid: + +```tolk +var number: int = true as int; // -1 +``` + +Similarly, operator `as` can be used to convert types that have equal TVM representation. +Using `as` explicitly forces a developer to verify that the transformation is correct. +No additional runtime instructions are inserted — it is purely a type cast. + +Some examples: + +- `address` is TVM `SLICE`, so `someAddr as slice` is valid +- conversely, `someSlice as address` +- `bits123` is TVM `SLICE`, so `someSlice as bits123` is valid +- `intN` is TVM `INT`, so `someInt64 as int32` is valid +- `bool` is TVM `INT`, so `someBool as int` and `someBool as int32` are valid, resulting in -1 or 0 +- an enum is TVM `INT`, so `someColor as int` is valid +- and similar conversions apply. + +When a cast using `as` is appropriate, the compiler indicates this in diagnostic messages, as shown above. +Not all transformations are possible. +For example, `someInt as cell` is invalid. + + + +## Operator `!` (non-null assertion) + +The `!` operator is similar to `!` in TypeScript and `!!` in Kotlin. It allows bypassing the compiler's nullability check: + +```tolk +fun doSmth(c: cell) {} + +fun analyzeStorage(nCells: int, lastCell: cell?) { + if (nCells) { // then lastCell 100% not null + doSmth(lastCell!); // use ! for this fact + } +} +``` + +## Smart casts + +Once a variable is checked, the compiler automatically narrows its type. + +```tolk +if (lastCell != null) { + // here lastCell is `cell`, not `cell?` +} +``` + +Smart casts operate similarly to those in TypeScript and Kotlin. +They are described in [nullables and null safety](/languages/tolk/types/nullable). + +## Operator `is` for union types + +Given a union type `T1 | T2 | ...`, the operator `is` performs a runtime type test. + +```tolk +fun demo(v: int | Point | cell) { + if (v is Point) { + return v.x + v.y; + } + // v is `int | cell` here +} +``` + +The `as` operator does not apply to unions: `v as Point` is incorrect. Use `is` and smart casts. + +Operators `is`, `!is`, and `match` are described in [union types](/languages/tolk/types/unions). diff --git a/languages/tolk/types/unions.mdx b/languages/tolk/types/unions.mdx index 2958501a5..3214ec852 100644 --- a/languages/tolk/types/unions.mdx +++ b/languages/tolk/types/unions.mdx @@ -2,3 +2,166 @@ title: "Union types" --- +Tolk supports union types `T1 | T2 | ...` similar to TypeScript. +They allow a value to belong to one of several possible types. +Pattern matching over unions is essential for message handling. +A special case `T | null` is written as `T?` and called "[nullable](/languages/tolk/types/nullable)". + +```tolk +struct (0x12345678) Increment { /* ... */ } +struct (0x23456789) Reset { /* ... */ } + +type IncomingMsg = Increment | Reset + +fun handle(m: IncomingMsg) { + match (m) { + Increment => { /* here m is `Increment` */ } + Reset => { /* here m is `Reset` */ } + } +} +``` + +## Not only structures: arbitrary types + +All these types are valid: + +- `int | slice` +- `address | Point | null` +- `Increment | Reset | coins` +- `int8 | int16 | int32 | int64` + +Union types are automatically flattened: + +```tolk +type Int8Or16 = int8 | int16 + +struct Demo { + t1: Int8Or16 | int32? // int8 | int16 | int32 | null + t2: int | int // int +} +``` + +Union types support assignment based on subtype relations. For instance, `B | C` can be passed/assigned to `A | B | C | D`: + +```tolk +fun take(v: bits2 | bits4 | bits8 | bits16) {} + +fun demo() { + take(someSlice as bits4); // ok + take(anotherV); // ok for `bits2 | bits16` +} +``` + +## `match` must cover all cases + +In other words, it must be **exhaustive**. + +```tolk +fun errDemo(v: int | slice | Point) { + match (v) { + slice => { v.loadAddress() } + int => { v * 2 } + // error: missing `Point` + } +} +``` + +`match` can be used for nullable types, since `T?` is `T | null`. It may also be used as an expression: + +```tolk +fun replaceNullWith0(maybeInt: int?): int { + return match (maybeInt) { + null => 0, + int => maybeInt, + } +} +``` + +See [pattern matching](/languages/tolk/syntax/pattern-matching) for syntax details. + +## Auto-inference of a union results in an error + +What if match arms result in different types, what is the resulting type? + +```tolk +var a = match (...) { + ... => beginCell(), + ... => 123, +}; +``` + +Formally, the type of `a` is inferred as `builder | int`, but this is most likely not what is intended and typically indicates an error in the code. +In such situations, the compiler emits a message: + +```ansi +error: type of `match` was inferred as `builder | int`; probably, it's not what you expected +assign it to a variable `var a: = match (...) { ... }` manually +``` + +So, either explicitly declare `a` as a union, or fix contract's code if it's a misprint. The same applies to other situations: + +```tolk +fun f() { + if (...) { return someInt64 } + else { return someInt32 } +} +``` + +The result is inferred as `int32 | int64`, which is valid, but in most cases a single integer type is expected. +The compiler shows an error, just explicitly declare a return type: + +```tolk +fun f(): int { + if (...) { return someInt64 } + else { return someInt32 } +} +``` + +Anyway, declaring return types is good practice, and following it resolves any ambiguity. + +## Operators `is` and `!is` + +Besides `match`, unions can also be tested using `is`: + +```tolk +fun demo(v: A | B) { + if (v is A) { + v.aMethod(); + } else { + v.bMethod(); + } +} +``` + +## Lazy match for unions + +In all examples of message handling, unions are parsed with `lazy`: + +```tolk +fun onInternalMessage(in: InMessage) { + val msg = lazy MyUnion.fromSlice(in.body); + match (msg) { + // ... + } +} +``` + +This pattern is called "lazy match": + +1. No union is allocated on the stack upfront; loading is deferred until needed. +1. `match` operates by inspecting the slice prefix (opcode), not by _typeid_ on the stack. + +This approach is significantly more efficient, although unions continue to function correctly without `lazy` and comply with all type-system rules. + +Read about [lazy loading](/languages/tolk/features/lazy-loading). + +## Stack layout and serialization + +Unions have a complex stack layout, commonly named as "tagged unions". Enums in Rust work the same way. + +Serialization depends on whether the it's a union of structures with manual serialization prefixes: + +- if yes (`struct (0x1234) A`), those prefixes are used +- if no, the compiler auto-generates a prefix tree; for instance, `T1 | T2` is called "Either": '0'+T1 or '1'+T2 + +For details, follow [TVM representation](/languages/tolk/types/overall-tvm-stack) and [Serialization](/languages/tolk/types/overall-serialization). diff --git a/languages/tolk/types/void-never.mdx b/languages/tolk/types/void-never.mdx index a84b11544..777c0a120 100644 --- a/languages/tolk/types/void-never.mdx +++ b/languages/tolk/types/void-never.mdx @@ -2,3 +2,124 @@ title: "Void and never types" sidebarTitle: "Void and never" --- + +import { Aside } from '/snippets/aside.jsx'; + +Tolk supports the types `void` and `never`, similar to TypeScript. Both represent the absence of a value, but in different ways. + +## A function that returns nothing is `void` + +Both functions below return `void`: + +```tolk +fun demo1() { + debug.print(globalVar); +} + +fun demo2() { + if (random.uint256() == 0) { + return; + } + globalVar = 123; +} +``` + +When the return type of a function is not specified, it is inferred from the `return` statements. +A `return` without a value means "void". + +Explicitly specifying `fun(...): void` behaves identically: + +```tolk +fun demo1(): void { + // ... +} +``` + + + +### Special fields with `void` type + +If a structure field is `void`, it indicates that the field does not exist at all. +This technique is used in combination with generics ``, allowing structures to express optional fields. + +```tolk +struct WithOptionalField { + a: int + b: T + c: slice +} + +type OnlyAC = WithOptionalField + +fun demo() { + // field `b` may be omitted + val v: OnlyAC = { a: 10, c: "" }; +} +``` + +## A function that never returns is `never` + +An always-throwing function is an example: + +```tolk +fun accessDenied(): never { + throw 403 +} + +fun validate(userId: int) { + if (userId > 0) { + return userId; + } + accessDenied(); + // no `return` statement is needed +} +``` + +In a `match` expression, branches that never return are also of type `never`: + +```tolk +val result = match (ownerKey) { + ADMIN_KEY => true, // `bool` + else => throw 403, // `never` +}; +// result is `bool` +``` + +## Implicit `never` in unreachable conditions + +The `never` type occurs implicitly when a condition is impossible to satisfy. +More precisely, it means that no value of such a type can exist; no possible data can produce such a value: + +```tolk +fun demo(v: int | slice) { + if (v is builder) { + // v is `never` + } +} +``` + +Another case arises when a non-nullable type is checked against `null`: + +```tolk +fun demo(v: int) { + if (v == null) { + // v is `never` + v + 10; // error, can not apply `+` `never` and `int` + } + // v is `int` again +} +``` + +Encountering `never` in a compilation error typically indicates an issue in the preceding code. + +## Stack layout and serialization + +Both mean "absence of a value" and occupy zero stack slots and zero bits. + +For example, a `void` function does not place any value onto the stack. + +For details, follow [TVM representation](/languages/tolk/types/overall-tvm-stack) and [Serialization](/languages/tolk/types/overall-serialization). From 08c2887683e41c29398d1b81d06c6f19b14df175 Mon Sep 17 00:00:00 2001 From: tolk-vm Date: Mon, 17 Nov 2025 21:23:17 +0300 Subject: [PATCH 04/11] [Tolk] New docs: syntax details --- languages/tolk/syntax/conditions-loops.mdx | 174 ++++++++ languages/tolk/syntax/exceptions.mdx | 136 ++++++ languages/tolk/syntax/functions-methods.mdx | 444 ++++++++++++++++++++ languages/tolk/syntax/imports.mdx | 147 +++++++ languages/tolk/syntax/mutability.mdx | 203 +++++++++ languages/tolk/syntax/operators.mdx | 75 ++++ languages/tolk/syntax/pattern-matching.mdx | 155 ++++++- languages/tolk/syntax/structures-fields.mdx | 184 ++++++++ languages/tolk/syntax/variables.mdx | 205 +++++++++ 9 files changed, 1719 insertions(+), 4 deletions(-) diff --git a/languages/tolk/syntax/conditions-loops.mdx b/languages/tolk/syntax/conditions-loops.mdx index dd446ecc9..0318c203e 100644 --- a/languages/tolk/syntax/conditions-loops.mdx +++ b/languages/tolk/syntax/conditions-loops.mdx @@ -2,3 +2,177 @@ title: "Conditions and loops" --- +Tolk provides flexible constructs for controlling contract flow. +Use `if`, `assert`, and loops to express conditional logic. +The `match` expression is a powerful "pattern-matching" feature. + +## `if` statement + +Similar to common languages, optionally followed by `else if` or `else`. + +```tolk +if (condition) { + // ... +} else { + // ... +} +``` + +A condition must be a boolean or an integer (will be true if not equals 0): + +```tolk +if (someInt) { // means "someInt != 0" + // ... +} +``` + +The body of `if` and `else` must be enclosed in `{ ... }`: + +```tolk +// invalid +if (condition) + someFn(); + +// valid +if (condition) { + someFn(); +} +``` + +## `assert` statement + +`assert` throws an exception if a condition is false. + +```tolk +assert (condition) throw ERR_CODE; +``` + +It is equivalent to the following form: + +```tolk +if (!condition) { + throw ERR_CODE; +} +``` + +See [exceptions](/languages/tolk/syntax/exceptions). + +## `match` expression + +`match` is used to perform different actions for different values of a variable. +One common use case is routing values of a union type: + +```tolk +fun demo(v: A | B | C) { + match (v) { + A => { + // use `v.aField` etc. + } + B => { /* ... */ } + C => { /* ... */ } + } +} +``` + +The `match` is equivalent to a series of `if-else` checks: + +```tolk +fun demo(v: A | B | C) { + if (v is A) { + // use `v.aField` etc. + } + else if (v is B) { /* ... */ } + else { /* ... */ } +} +``` + +The `match` can also be used for expressions (switch-like behavior): + +```tolk +fun invertNumber(curValue: int) { + return match (curValue) { + 1 => 0, + 0 => 1, + else => throw ERR_UNEXPECTED, + }; +} +``` + +See [union types](/languages/tolk/types/unions) and [pattern matching](/languages/tolk/syntax/pattern-matching). + +## Ternary operator + +A ternary `condition ? when_true : when_false` is also available: + +```tolk +fun myAbs(v: int) { + return v < 0 ? -v : v +} +``` + +Note that if types of `when_true` and `when_false` are different, a resulting type is a union. +However, this is typically not the intended result, so the compiler reports an error. + +```tolk +fun demo(a: int32, b: int64) { + // instead of inferring result1 as `int32 | int64`, + // an error "probably it's not what you expected" + val result1 = a > b ? a : b; + + // correct: specify the type manually + val result2: int = a > b ? a : b; + + // also correct, types are compatible + val result3 = a > b ? a as int64 : b; +} +``` + +## `while` loops + +`while` and `do-while` loops repeatedly execute their bodies while the condition remains true. +`while` checks the condition first and may not execute the body at all, whereas `do-while` runs the body first. + +```tolk +fun demoWhile(i: int) { + while (i > 0) { + debug.print(i); + i -= 1; + } +} + +fun demoDoWhile() { + var rand: int; + do { + rand = random.uint256(); + } while (rand % 2 == 0); + return rand; // an odd number +} +``` + +For instance, `while` is used to iterate over maps: + +```tolk +fun iterateOverMap(m: map) { + var r = m.findFirst(); + while (r.isFound) { + // ... + r = m.iterateNext(r); + } +} +``` + +## `repeat` loop + +The `repeat (N)` statement executes a block N times: + +```tolk +repeat (5) { + // executed 5 times +} +``` + +`N` does not have to be a constant; it may also be a variable. + +## Break and continue in loops + +The keywords `break` and `continue` are not implemented yet, and a `for` loop may be added in future versions. diff --git a/languages/tolk/syntax/exceptions.mdx b/languages/tolk/syntax/exceptions.mdx index 7d5ef9a02..f5a9092ce 100644 --- a/languages/tolk/syntax/exceptions.mdx +++ b/languages/tolk/syntax/exceptions.mdx @@ -2,3 +2,139 @@ title: "Exceptions, throw, assert" --- +import { Aside } from '/snippets/aside.jsx'; + +Tolk allows throwing exceptions and catching them via `try catch`. +If an exception is not caught, the program terminates with `errCode`. +This is the standard mechanism for reacting on invalid input. + + + +## Error codes + +A suggested pattern is to introduce a constant for every exception that a contract may produce. +For better navigation, keep them in a separate file, e.g., `errors.tolk`: + +```tolk +const ERR_LOW_BALANCE = 200 +const ERR_SIGNATURE_MISMATCH = 201 +``` + +Alternatively, introduce an `enum`: + +```tolk +enum ErrCode { + LowBalance = 200, + SignatureMismatch, // implicitly 201 +} +``` + +These constants are then used in `throw` and related statements. + + + +## `throw` statement + +To throw an exception unconditionally: + +```tolk +throw ERR_CODE; +``` + +Non-constant expressions such as `throw someVariable` are supported but not recommended. +It works, but the compiler can't resolve possible codes and provide a correct ABI for external usage. + +An exception may additionally carry an **argument**: + +```tolk +throw (ERR_CODE, errArg); +``` + +The argument will then be available inside `catch`. It must be a TVM primitive (a single-slot value). + +## `assert` statement + +A useful shorthand to "throw if an expectation is not satisfied". Commonly used when parsing user input: + +```tolk +assert (msg.seqno == storage.seqno) throw E_INVALID_SEQNO; +assert (msg.validUntil > blockchain.now()) throw E_EXPIRED; +``` + +The long form `assert (condition) throw ERR_CODE` is preferred. +A short form `assert (condition, ERR_CODE)` exists but is not recommended. + +A condition must be a boolean or an integer (will be true if not equals 0). This works the same way as: + +```tolk +if (!condition) { + throw ERR_CODE; +} +``` + +## Implicit throws by TVM internals + +While a contract is running, it may throw other runtime exceptions. For example: + +- `slice.loadInt(8)`, but a slice is empty +- `builder.storeRef(cell)`, but a builder already has 4 refs +- `tuple.push(value)`, but a tuple already has 255 elements +- an "out of gas" exception may occur at any moment + +## What happens if execution interrupts + +As seen above, exceptions might occur literally everywhere. +For example, an incoming message is parsed with `Msg.fromSlice()`, but an input is corrupted, and data cannot be successfully deserialized. +Or some `assert` statement triggers a user-scope exception. +What happens next? + +Then execution of the current request (_compute phase_) interrupts. +Even if `createMessage` was called, a message won't be sent, because actual sending is done after successful execution, in the _action phase_. +Instead, a _bounce phase_ starts: all changes are rolled back, and typically the current request is **bounced** back to the caller. +The caller may handle this bounce in a custom manner. + +Of course, after bouncing the current message, the contract remains on-chain and is ready to serve the next request. + +Read about [computation phases](/foundations/phases). + +## `try catch` statement + +The `catch` statement allows reacting to runtime errors. The syntax resembles other languages: + +```tolk +try { + // ... +} catch (errCode) { + // errCode is `int` +} +``` + +Use a short form `catch { ... }` if `errCode` is not needed. +A long form `catch (errCode, arg)` provides data from `throw (errCode, arg)`. +For just `throw errCode`, the argument is `null`. + +```tolk +try { + throw (ERR_LOW_BALANCE, 555); +} catch (errCode, arg) { + val data = arg as int; // 555 +} +``` + + diff --git a/languages/tolk/syntax/functions-methods.mdx b/languages/tolk/syntax/functions-methods.mdx index 38c41d748..2ce3f7e13 100644 --- a/languages/tolk/syntax/functions-methods.mdx +++ b/languages/tolk/syntax/functions-methods.mdx @@ -2,3 +2,447 @@ title: "Functions and methods" --- +import { Aside } from '/snippets/aside.jsx'; + +Tolk allows declaring functions and methods: + +- `fun f()` — a function +- `fun .f()` — a method: + - `fun .f(self, ...)` — an instance method + - `fun .f(...)` — a static method (no `self`) + +All features are identical except for the receiver. +Most examples below use functions, but all concepts equally apply to methods. + +## Types for parameters are required + +```tolk +// invalid: +fun sum(a, b) {} + +// valid: +fun sum(a: int, b: bool) {} +``` + +## The return type is auto-inferred if omitted + +Either specify return type manually: + +```tolk +fun demo(): int { + // ... +} +``` + +or it is inferred from the `return` statements, similar to TypeScript: + +```tolk +fun demo() { // auto-infer `int` + // ... + return 10; +} +``` + +If various returns have different types, the compiler will fire an error instead of inferring a union type, because this typically indicates a bug. + +## Default values for parameters + +Default values are placed after the type, allowing constant expressions: + +```tolk +fun plus(value: int, delta: int = 1) { + return value + delta +} + +fun demo() { + plus(10); // 11 + plus(10, 5); // 15 +} +``` + +## Methods: for any type, including structures + +A method is declared as an extension function, similar to Kotlin. +If the first parameter is `self`, it's an instance method. Otherwise, it's a static method. + +```tolk +struct Point { + x: int + y: int +} + +// no `self` — static method +fun Point.createZero(): Point { + return { x: 0, y: 0 } +} + +// has `self` — instance method +fun Point.sumCoords(self) { + return self.x + self.y +} +``` + +Instance methods are invoked via `obj.method()`, whereas static methods — on a type: + +```tolk +fun demo() { + val p = Point.createZero(); + return p.sumCoords(); // 0 +} +``` + +Methods are not limited to structures. They may exist for any receiver, including unions, aliases, and primitives: + +```tolk +fun int.one() { + return 1 +} + +fun int.negate(self) { + return -self +} + +fun demo() { + return int.one().negate() // -1 +} +``` + +All standard methods are declared this way: `fun cell.hash(self)`, etc. + +The type of `self` is determined by the receiver. +All parameters after should have their types set. +The return type is inferred if omitted. + +```tolk +fun Point.equalTo(self, r: Point) { // auto-infer `bool` + return self.x == r.x && self.y == r.y +} +``` + +## `self` is immutable by default + +To allow modifications, declare `mutate self` explicitly. +For details, see [mutability](/languages/tolk/syntax/mutability). + +```tolk +// mind `mutate` — to allow modifications +fun Point.reset(mutate self) { + self.x = 0; + self.y = 0; +} +``` + +## `return self` makes a chainable method + +To make a method chainable, declare it as `fun (...): self`. +For instance, all methods for `builder` return `self`, +which allows `b.storeXXX().storeXXX()`: + +```tolk +fun builder.myStoreInt32(mutate self, v: int): self { + self.storeInt(v, 32); + return self; +} + +fun demo() { + return beginCell() + .storeAddress(SOME_ADDR) + .myStoreInt32(123) + .endCell(); +} +``` + +## Small functions are automatically inlined + +Create one-liner methods without any worries. +The compiler automatically inlines them in-place, targeting zero overhead. For example, this program + +```tolk +fun int.zero() { + return 0 +} + +fun int.inc(mutate self, byValue: int = 1): self { + self += byValue; + return self; +} + +fun main() { + return int.zero().inc().inc() +} +``` + +is reduced to "return 2" in assembler: + +```fift +main() PROC:<{ + 2 PUSHINT +}> +``` + +For details, see [compiler optimizations](/languages/tolk/features/compiler-optimizations). + +## @attributes + +A function or method may be preceded by one or several attributes: + +```tolk +@noinline +@custom("any contents here") +fun slowSum(a: int, b: int) { + return a + b +} +``` + +The following attributes are allowed: + +- `@inline` — Forces a function to be inlined in-place. Typically unnecessary, as the compiler performs inlining automatically. +- `@inline_ref` — Turns on a special version of inlining: its body will be embedded as a child cell reference in the resulting bytecode. +- `@noinline` — Disables inlining for a function. For example, if it's a "slow path". +- `@method_id()` — Low-level annotation to manually override TVM method\_id. +- `@pure` — Indicates that a function does not modify global state (including TVM one). If its result is unused, a call could be deleted. Typically used in assembler functions. +- `@deprecated` — A function is not recommended for use and exists only for backwards compatibility. +- `@custom()` — A custom expression, not analyzed by the compiler. + +## Assembler functions + +Tolk supports declaring low-level assembler functions with embedded Fift code: + +```tolk +@pure +fun minMax(x: int, y: int): (int, int) + asm "MINMAX" +``` + +It's a low-level feature and requires deep knowledge of [Fift](/languages/fift/overview) and [TVM](/tvm/overview). +See [assembler functions](/languages/tolk/features/asm-functions). + +## Anonymous functions (lambdas) + +Tolk supports first-class functions: they can be passed as callbacks. +Both named functions and function expressions may be referenced: + +```tolk +fun customRead(reader: (slice) -> int) { + // ... +} + +fun demo() { + customRead(fun(s) { + return s.loadUint(32) + }) +} +``` + +See [callables](/languages/tolk/types/callables). + +## Generic functions + +A function declared as `fun f(...)` is a generic one. `T` is called a "type parameter". + +```tolk +fun duplicate(value: T): (T, T) { + var copy: T = value; + return (value, copy); +} +``` + +When calling a generic function, the compiler automatically infers type arguments: + +```tolk +fun demo() { + duplicate(1); // duplicate + duplicate(somePoint); // duplicate + duplicate((1, 2)); // duplicate<(int, int)> +} +``` + +Type arguments may also be specified explicitly using `f<...>(args)`: + +```tolk +fun demo() { + duplicate(1); + duplicate(null); // two nullable points +} +``` + +Functions may declare multiple type parameters: + +```tolk +// returns `(tensor.0 || defA, tensor.1 || defB)` +fun replaceNulls(tensor: (T1?, T2?), defA: T1, defB: T2): (T1, T2) { + var (a, b) = tensor; + return (a == null ? defA : a, b == null ? defB : b); +} +``` + +Since Tolk supports first-class functions, various custom invokers for general-purpose frameworks can be expressed: + +```tolk +fun customInvoke(f: TArg -> R, arg: TArg) { + return f(arg); +} +``` + +Default type parameters are supported, like `fun f`, but they cannot reference one another currently. + +Although type parameters are usually inferred from the arguments, there are edge cases where `T` cannot be inferred because it does not depend on them. +For example, tuples. A tuple may have anything inside it, that's why invoking `tuple.get` lacks without `T`: + +```tolk +// a method `tuple.get` is declared this way in stdlib: +fun tuple.get(self, index: int): T; + +fun demo(t: tuple) { + var mid = t.get(1); // error, can not deduce T + + // correct is: + var mid = t.get(1); + // or + var mid: int = t.get(1); +} +``` + +A generic function may be assigned to a variable, but `T` must be specified explicitly because this is not a call: + +```tolk +fun genericFn(v: T) { + // ... +} + +fun demo() { + var callable = genericFn; + callable(beginCell()); +} +``` + +## Generic methods + +Declaring a method for a generic type does not differ from declaring any other method. +The compiler treats unknown symbols as type parameters while parsing the receiver: + +```tolk +struct Pair { + first: T1 + second: T2 +} + +// both , , etc. work: any unknown symbols +fun Pair.create(f: A, s: B): Pair { + return { + first: f, + second: s, + } +} + +// instance method with `self` +fun Pair.compareFirst(self, rhs: A) { + return self.first <=> rhs +} +``` + +Generic methods can be also used as first-class functions: + +```tolk +var callable = Pair.compareFirst; +callable(somePair, 123); // pass somePair as self +``` + +Methods for generic structures can themselves be generic: + +```tolk +fun Pair.createFrom(f: U, s: V): Pair { + return { + first: f as A, + second: s as B, + } +} + +fun demo() { + return Pair.createFrom(1, 2); +} +``` + +## Methods for "any receiver" + +Any unknown symbol (typically `T`) may be used to define a method applicable to any type: + +```tolk +// any receiver +fun T.copy(self): T { + return self +} + +// any nullable receiver +fun T?.isNull(self): bool { + return self == null +} +``` + +Note that "any receivers" do not conflict with specific ones. + +```tolk +fun T.someMethod(self) { ... } +fun int.someMethod(self) { ... } + +fun demo() { + 42.someMethod(); // (2) + address("...").someMethod(); // (1) with T=address +} +``` + +This is known as "overloading" or "partial specialization". + +## Partial specialization of generic methods + +Specializing generic methods allows some logic to work differently for predefined types or patterns. +Consider the following example. +Suppose, there is an iterator over a tuple of slices, each slice contains encoded `T`. + +```tolk +struct TupleIterator { + data: tuple // [slice, slice, ...] + nextIndex: int +} + +fun TupleIterator.next(self): T { + val v = self.data.get(self.nextIndex); + self.nextIndex += 1; + return T.fromSlice(v); +} +``` + +Thus, given `TupleIterator` or `TupleIterator`, `next()` will decode the next slice and return it. +But additional requirements are: + +- `TupleIterator` should just return `data[i]` without calling `fromSlice()` +- `TupleIterator>` should return not `Cell` but unpack a cell and return `T` + +Tolk allows **overloading methods for more specific type patterns**: + +```tolk +fun TupleIterator.next(self): T { ... } +fun TupleIterator>.next(self): T { ... } +fun TupleIterator.next(self): slice { ... } +``` + +Another example. Declare an extension method for `map`, but + +- if `V = K`, provide a different implementation +- if `V` is another map, modify the behavior + +```tolk +fun map.method(self) { ... } +fun map.method(self) { ... } +fun map>.method(self) { ... } + +// (1) called for map / map> +// (2) called for map / map +// (3) called for map> +``` + + diff --git a/languages/tolk/syntax/imports.mdx b/languages/tolk/syntax/imports.mdx index e0115e070..8168de0ab 100644 --- a/languages/tolk/syntax/imports.mdx +++ b/languages/tolk/syntax/imports.mdx @@ -2,3 +2,150 @@ title: "Imports and name resolution" --- +import { Aside } from '/snippets/aside.jsx'; + +In practice, contract sources are split into multiple files. +A project containing multiple contracts shares the same codebase (messages, storage definitions, and related logic). +To use symbols in another file, that file must be imported. +Upon import, all its symbols become available. + +## Import a file to access its symbols + +For example, error codes are placed in `errors.tolk`: + +```tolk +const ERR_ZERO_BALANCE = 123 +// ... +``` + +To use it from `parse.tolk`, an explicit import is required: + +```tolk +import "errors" + +fun validate(balance: coins) { + assert (balance > 0) throw ERR_ZERO_BALANCE; +} +``` + + + +## All top‑level symbols must have unique names + +Notice that no `export const ...` or `export fun ...` declarations needed. +All constants, functions, etc. are visible. +No module‑private symbols exist. + +It means that **all symbols must have unique names**. + +Having `fun log()` in multiple files results in a "duplicate declaration" error if both files are imported. + +## Techniques to avoid name collisions + +1. Use long, descriptive names for top-level symbols, especially in multi‑contract projects + - Good: `ReturnExcessesBack`, `WalletStorage` + - Bad: `Excesses`, `Storage` +1. Group integer constants to enums +1. Prefer methods to global-scope functions + +## Prefer methods to functions + +Methods do not collide because they are attached to distinct types: + +```tolk +fun Struct1.validate(self) { /* ... */ } +fun Struct2.validate(self) { /* ... */ } +``` + +Methods are generally more convenient: `obj.someMethod()` looks nicer than `someFunction(obj)`: + +```tolk +struct AuctionConfig { + // ... +} + +// NOT +// fun isAuctionConfigInvalid(config: AuctionConfig) +// BUT +fun AuctionConfig.isInvalid(self) { + // ... +} +``` + +Same for static methods: `Auction.createFrom(...)` seems better than `createAuctionFrom(...)`. +A method without `self` is a static one: + +```tolk +fun Auction.createFrom(config: cell, minBid: coins) { + // ... +} +``` + +Static methods may also be used to represent various utility functions. +For example, standard functions `blockchain.now()` and others are essentially static methods of an empty struct. + +```tolk +struct blockchain + +fun blockchain.now(): int /* ... */; +fun blockchain.logicalTime(): int /* ... */; +``` + +In large projects, this technique may be used to emulate namespaces. + +## Symbols that may be repeated across the project + +When developing multiple contracts, each contract has its own file (compilation target). +They do not import one another; therefore, + +- `onInternalMessage` and `onExternalMessage` +- `get fun` +- and other declarations in a contract file don't conflict with each other + +```tolk +// file: a-contract.tolk +import "storage" +import "errors" + +fun onInternalMessage(in: InMessage) { + // ... +} + +get fun name() { + return "a" +} +``` + +```tolk +// file: b-contract.tolk +import "storage" +import "errors" + +fun onInternalMessage(in: InMessage) { + // ... +} + +get fun name() { + return "b" +} +``` + +Get methods conceptually belong to the scope of a specific contract. + +Typically, in a multi-contract project, each contract file contains only + +- its entrypoints, +- probably, a union with allowed messages, +- and little else; the remaining codebase is shared. + +For instance, struct `SomeMessage`, outgoing for contract A, is incoming for contract B. +When deploying one contract from another, a storage struct must be known. And similar. + +As a guideline, group messages / errors / utils / etc. in shared files, +and use only minimal declarations inside each `contract.tolk`. diff --git a/languages/tolk/syntax/mutability.mdx b/languages/tolk/syntax/mutability.mdx index 787b6e6e0..54bd9ba23 100644 --- a/languages/tolk/syntax/mutability.mdx +++ b/languages/tolk/syntax/mutability.mdx @@ -2,3 +2,206 @@ title: "Mutability" --- +import { Aside } from '/snippets/aside.jsx'; + +Tolk follows **value semantics**: when calling a function, arguments are copied by value. +There are no "pointers" or "references to objects". +Nevertheless, the keyword `mutate`, used both at declaration and invocation, allows to modify an argument. + +## Value semantics + +Function arguments are **copied by value**. Function calls do not modify the original data. + +```tolk +fun someFn(x: int) { + x += 1; +} + +fun demo() { + var origX = 0; + someFn(origX); // origX remains 0 +} +``` + +This also applies to slices, cells, and other types: + +```tolk +fun readFlags(cs: slice) { + return cs.loadInt(32); +} + +fun onInternalMessage(in: InMessage) { + var flags = readFlags(in.body); // body is NOT modified + // `in.body.loadInt(32)` reads the same flags +} +``` + +## `mutate` for a parameter + +The `mutate` keyword makes a parameter mutable. +To prevent unintended modifications, `mutate` must also be specified at the call site. + +```tolk +fun increment(mutate x: int) { + x += 1; +} + +fun demo() { + // correct: + var origX = 0; + increment(mutate origX); // origX becomes 1 + + // these are compiler errors + increment(origX); // error, unexpected mutation + increment(10); // error, not lvalue +} +``` + +This also applies to slices and other types: + +```tolk +fun readFlags(mutate cs: slice) { + return cs.loadInt(32); +} + +fun onInternalMessage(in: InMessage) { + var flags = readFlags(mutate in.body); + // `in.body.loadInt(32)` reads the next integer +} +``` + +A function can define multiple mutate parameters: + +```tolk +fun incrementXY(mutate x: int, mutate y: int, delta: int) { + x += delta; + y += delta; +} + +fun demo() { + var (a, b) = (5, 8); + incrementXY(mutate a, mutate b, 10); // a = 15, b = 18 +} +``` + + + +## `self` in methods is immutable by default + +Instance methods are declared as `fun .f(self)`. +By default, `self` is immutable: + +```tolk +fun slice.readFlags(self) { + return self.loadInt(32); // error, a mutating method +} + +fun slice.preloadFlags(self) { + return self.preloadInt(32); // ok, a read-only method +} +``` + +## `mutate self` allows modifying the receiver + +```tolk +fun slice.readFlags(mutate self) { + return self.loadInt(32); +} +``` + +Thus, when calling `someSlice.readFlags()`, the object is mutated. + +Methods for structures are declared in the same way: + +```tolk +struct Point { + x: int + y: int +} + +fun Point.reset(mutate self) { + self.x = self.y = 0 +} +``` + +A mutating method may even modify another variable: + +```tolk +fun Point.resetAndRemember(mutate self, mutate sum: int) { + sum = self.x + self.y; + self.reset(); +} + +fun demo() { + var (p, sumBefore) = (Point { x: 10, y: 20 }, 0); + p.resetAndRemember(mutate sumBefore); + return (p, sumBefore); // { 0, 0 } and 30 +} +``` + +## How `mutate` works under the hood + +Tolk code is executed by [TVM](/tvm/overview) — a stack-based virtual machine. +Mutations work by implicit returning new values via the stack. + +```tolk +// transformed to: "returns (int, void)" +fun increment(mutate x: int): void { + x += 1; + // a hidden "return x" is inserted +} + +fun demo() { + var x = 5; + // transformed to: (newX, _) = increment(x); x = newX + increment(mutate x); +} +``` + +Mutating methods work literally the same: + +```tolk +// transformed to: (newS, flags) = loadInt(s, 32); s = newS +flags = s.loadInt(32); +``` + +For detailed examples of stack ordering, follow [assembler functions](/languages/tolk/features/asm-functions). + +## Note: `T.fromSlice(s)` does NOT modify `s` + +Auto-serialization via `fromSlice` follows absolutely identical rules. +But some developers got stuck on this exact case, let's highlight it specially. + +Given `f(anyVariable)`, the variable remains unchanged. +If a function mutates it, such a call is invalid, a valid is `f(mutate anyVariable)`. + +Same goes for `AnyStruct.fromSlice(s)`: a slice is not mutated, its internal pointer is not shifted. +So, calling `s.assertEnd()` will not actually check "nothing is left after loading AnyStruct". + +```tolk +struct Point { + x: int8 + y: int8 +} + +fun demo(s: slice) { + // want to check that "0102" is ok, "0102FF" is wrong + // but this is NOT correct + var p = Point.fromSlice(s); + s.assertEnd(); // because s is not mutated +} +``` + +To check that a slice does not contain excess data, no special actions required, +because **`fromCell` and `fromSlice` automatically ensure the slice end after reading**. +For input `0102FF`, an exception 9 is thrown. This behavior can be turned off with an option: + +```tolk +Point.fromSlice(s, { + assertEndAfterReading: false // true by default +}) +``` + +For more details and examples, proceed to [automatic serialization](/languages/tolk/features/auto-serialization). diff --git a/languages/tolk/syntax/operators.mdx b/languages/tolk/syntax/operators.mdx index f6610860d..c64780306 100644 --- a/languages/tolk/syntax/operators.mdx +++ b/languages/tolk/syntax/operators.mdx @@ -2,3 +2,78 @@ title: "Operators" --- +Tolk provides standard operators for integers and booleans. +This page lists them briefly, as they closely resemble other common languages. + +## Operators from highest to lowest priority + +**Parenthesis `(` `)`**
+Groups expressions: `(1 + 2) * 3`; or creates [tensors](/languages/tolk/types/tensors): `pair = (1, 2)`. + +**Square brackets `[` `]`**
+Creates typed [tuples](/languages/tolk/types/tuples): `[1, 2]`. + +**Operator `lazy`**
+See [lazy loading](/languages/tolk/features/lazy-loading). + +**Non-null assertion operator `!`**
+Skips the nullability check: `someVar!`. See [nullable types](/languages/tolk/types/nullable). + +**Unary operators `!` `~` `-` `+`**
+Logical negation `!x` (see [booleans](/languages/tolk/types/booleans)), bitwise not `~x`, unary `-x` and `+x`. + +**Operators `as` `is` `!is`**
+[Unsafe `as` cast](/languages/tolk/types/type-checks-and-casts) and check a [union type](/languages/tolk/types/unions): `someVar is int`. + +**Multiplicative `*` `/` `%` `^/` `~/`**
+Integer multiplication, division, modulo, ceiling-division, and rounding-division. +Note that all [integers](/languages/tolk/types/numbers) have 257-bit precision. + +**Additive `+` `-`**
+Standard integer addition and subtraction. + +**Shifts `<<` `>>` `^>>` `~>>`**
+Bitwise shifts, extended by ceiling-right and rounding-right. + +**Comparison `==` `<` `>` `<=` `>=` `!=` `<=>`**
+Comparison operators. The last one is known as the "spaceship" or "sign" operator. +Operators `==` and `!=` work also for several non-numeric types (particularly, [addresses](/languages/tolk/types/address)). + +**Bitwise `&` `|` `^`**
+Standard bitwise operators, applicable to both integers and booleans. + +**Logical `&&` `||`**
+[Short-circuit](/languages/tolk/types/booleans#logical-vs-bitwise-operators): the right operand is evaluated only when necessary. + +**Assignment `=` `+=` `-=` and other**
+Assignment and augmented assignments. + +**Ternary `... ? ... : ...`**
+See [conditions and loops](/languages/tolk/syntax/conditions-loops). + +## Missing operators + +Tolk does not have `i++` and `i--`. Use `i += 1` and `i -= 1` instead. + +## Warnings on potentially unexpected precedence + +A common pitfall across many languages: + +```tolk +if (flags & 0xFF != 0) { + // ... +} +``` + +This does not check the lowest byte: it's parsed as `flags & (0xFF != 0)`, +because `!=` has higher priority (the same in C++ and other languages). + +To prevent such problematic cases, the compiler triggers an error: + +```ansi +error: & has lower precedence than !=, probably this code won't work as you expected. + Use parenthesis: either (... & ...) to evaluate it first, or (... != ...) to suppress this error. + + 2 | if (flags & 0xFF != 0) { + | ^^^^^^^^^^^^^^^^^ +``` diff --git a/languages/tolk/syntax/pattern-matching.mdx b/languages/tolk/syntax/pattern-matching.mdx index 5d2a7e7ff..fa206775c 100644 --- a/languages/tolk/syntax/pattern-matching.mdx +++ b/languages/tolk/syntax/pattern-matching.mdx @@ -2,15 +2,103 @@ title: "Pattern matching" --- +import { Aside } from '/snippets/aside.jsx'; +Tolk supports a `match` expression for pattern matching. +It is a powerful construct that applies both to types and expressions. -## Variable declaration inside `match` +## `match` for union types + +Pattern matching for unions is a key to [message handling](/languages/tolk/features/message-handling). + +```tolk +type IncomingMessage = + | CounterIncBy + | CounterReset + // ... + +// ... after parsing a message +match (msg) { + CounterIncBy => { + newCounter = curCounter + msg.byValue + } + CounterReset => { + newCounter = 0 + } + // ... +} +``` + +Although it's a general mechanism compatible with any union type: + +```tolk +fun processValue(value: int | slice) { + match (value) { + int => { + value * 2 + } + slice => { + value.loadUint(8) + } + } +} +``` + +## `match` for a union must be exhaustive + +In other words, all alternatives must be covered. + +```tolk +fun errDemo(v: int | slice | Point) { + match (v) { + slice => { v.loadAddress() } + int => { v * 2 } + // error: missing `Point` + } +} +``` + +Note that `else` is not allowed for unions but is permitted for a [lazy match](/languages/tolk/features/lazy-loading#lazy-matching). + +## Syntax details + +- After `=>` there is + - either a block: `A => { ... }` + - or an expression: `A => 123` + - or `A => return SOME_EXPR` + - or `A => throw ERR_CODE` +- A comma + - optional after a block: `A => {} B => {}` + - required otherwise: `A => 1, B => 2` +- May be used as a match expression: `return match (v) { ... }` +- Variable declarations are allowed inside: `match (val v = ...)` + +## Using `match` as an expression + +Using `match` in expression position allows: + +- `var smth = match (v) { ... }` +- `return match (v) { ... }` +- and similar ```tolk type Pair2 = (int, int) type Pair3 = (int, int, int) -fun getPair2Or3(): Pair2 | Pair3 { /* ... */ } +fun getLast(tensor: Pair2 | Pair3) { + return match (tensor) { + Pair2 => tensor.1, + Pair3 => tensor.2, + } +} +``` + +## Declaring variables inside `match` + +```tolk +fun getPair2Or3(): Pair2 | Pair3 { + // ... +} fun demo() { match (val v = getPair2Or3()) { @@ -20,7 +108,66 @@ fun demo() { } ``` +## `match` for expressions (not for types) + +`match` can be used with constant expressions, similar to `switch`: + +```tolk +val nextValue = match (curValue) { + 1 => 0, + 0 => 1, + else => -1 +}; +``` + +Rules: + +- only constant expressions may appear before `=>` +- `else` is required for match expressions and optional for statements + +```tolk +// statement form +match (curValue) { + 1 => { nextValue = 0 } + 0 => { nextValue = 1 } + -1 => throw NEGATIVE_NOT_ALLOWED +} + +// expression form, `else` required +val nextValue = match (curValue) { + // ... + else => 1 + 2 +} +``` + + + +## `match` for enums + +Pattern matching on [enums](/languages/tolk/types/enums) requires coverage of all cases (_exhaustive_): + +```tolk +match (someColor) { + Color.Red => {} + Color.Green => {} + // error: Color.Blue is missing +} +``` + +Alternatively, use `else` to handle remaining values: -Familiar with Rust's `enum` and `match`? -You may notice that match is very similar, but `enum` in Tolk hold only integers. Union types are more general and powerful. +```tolk +match (someColor) { + Color.Red => {} + else => {} +} +``` + diff --git a/languages/tolk/syntax/structures-fields.mdx b/languages/tolk/syntax/structures-fields.mdx index 47597fae6..b6cc98247 100644 --- a/languages/tolk/syntax/structures-fields.mdx +++ b/languages/tolk/syntax/structures-fields.mdx @@ -2,3 +2,187 @@ title: "Structures and fields" --- +Tolk supports structures — similar to TypeScript classes. +This page focuses on syntax details only. +See also [structures in the type system](/languages/tolk/types/structures). + +## Syntax of structures + +```tolk +struct Demo { + allowExotic: bool = false + customData: tuple +} +``` + +- every field has a mandatory type and an optional default value +- fields are separated by newlines (preferred), commas, or semicolons +- fields may be `private` and `readonly` + +## Create an object + +Use `{ ... }` when the type is clear, or `StructName { ... }` explicitly. + +```tolk +fun demo() { + // the type is clear from the assignment + var o1: Demo = { customData: someTuple }; + o1.someMethod(); + + // alternatively, `Demo { ... }` + Demo{customData: someTuple}.someMethod(); +} +``` + +## Shorthand object syntax + +Similar to TypeScript, `{ a, b }` means `{ a: a, b: b }`. + +```tolk +fun createDemo(allowExotic: bool, customData: tuple) { + return Demo { allowExotic, customData } +} +``` + +## Methods for structures + +Methods are declared separately as extension functions: + +```tolk +fun Demo.hasData(self): bool { + return self.customData.size() != 0 +} +``` + +See [Functions and methods](/languages/tolk/syntax/functions-methods). + +## Empty structures + +An empty structure without fields serves as a grouping construct for static methods. + +For example, standard functions `blockchain.now()` and others are declared this way: + +```tolk +struct blockchain + +fun blockchain.now(): int + asm "NOW" + +fun blockchain.logicalTime(): int + asm "LTIME" +``` + +These methods are static because they do not accept `self`. + +## Default values for fields + +Fields that have defaults may be missed out of a literal: + +```tolk +struct DefDemo { + f1: int = 0 + f2: int? = null + f3: (int, coins) = (0, ton("0.05")) +} + +fun demo() { + var d: DefDemo = {}; // ok + var d: DefDemo = { f2: 5 }; // ok +} +``` + +## Private and readonly fields + +- `private` — accessible only within methods +- `readonly` — immutable after object creation + +```tolk +struct PositionInTuple { + private readonly t: tuple + currentIndex: int +} + +fun PositionInTuple.create(t: tuple): PositionInTuple { + return { t, currentIndex: 0 } +} + +fun PositionInTuple.next(mutate self) { + // self.t cannot be modified: it is readonly + self.currentIndex += 1; +} + +fun demo() { + var p = PositionInTuple.create(someTuple); + // p.t is unavailable here: it is private +} +``` + +As a consequence, the only way to create an object with a private field is through a static method or an assembler function. + +## Generic structs + +Generics exist only at the type level and incur no runtime cost. + +```tolk +struct Nullable { + value: T? +} +``` + +See [generics](/languages/tolk/types/generics) for structures and type aliases. + +## Methods for generic structs + +When parsing the receiver in `fun .f()`, the compiler treats unknown symbols as type parameters: + +```tolk +fun Nullable.isNull(self) { + return self.value == null +} +``` + +It's a generalization. For example, `fun T.copy()` works for "any receiver". + +Method overloading (also known as partial specialization) is also allowed. + +```tolk +fun Nullable>.isNull(self) { + return self.value.isEmpty() +} +``` + +See examples in [Functions and methods](/languages/tolk/syntax/functions-methods#generic-methods). + +## Serialization prefixes and opcodes + +Syntax `struct (PREFIX) Name { ... }` allows specifying **serialization prefixes**. +Typically, 32-bit prefixes for messages are called **opcodes**. + +```tolk +struct (0x7362d09c) TransferNotification { + queryId: uint64 + // ... +} +``` + +For example, such an outgoing message will start with this hex number: + +```tolk +// message will be '0x7362d09c0000000000000000' + ... +createMessage({ + // ... + body: TransferNotification { + queryId: 0, + // ... + } +}); +``` + +Prefixes are not restricted to 32 bits: + +- `0x000F` — 16-bit prefix +- `0b010` is 3-bit (binary form, notice '0b') + +Non‑32‑bit prefixes are useful for controlling union types when expressing TL‑B's multiple constructors. + +Read about [serialization of structures](/languages/tolk/types/overall-serialization#structures). diff --git a/languages/tolk/syntax/variables.mdx b/languages/tolk/syntax/variables.mdx index 422121d30..0e85ff180 100644 --- a/languages/tolk/syntax/variables.mdx +++ b/languages/tolk/syntax/variables.mdx @@ -2,3 +2,208 @@ title: "Variables" --- +import { Aside } from '/snippets/aside.jsx'; + +Variables are declared with `val` (immutable) or `var` (mutable). + +Outside functions, use `const` or (rarely) `global`. + +## Keywords `val` and `var` + +`val` declares a variable that is **assigned exactly once** (immutable): + +```tolk +fun demo() { + val coeff = 5; + // cannot change its value, `coeff += 1` is an error +} +``` + +`var` declares a variable that **may be reassigned**: + +```tolk +fun demo() { + var x = 5; + x += 1; // now 6 +} +``` + +**Explicit types** can be specified. If not specified, the variable type is inferred from the initial assignment: + +```tolk +fun demo() { + var x = 5; // inferred `int` + x = null; // error, cannot assign + + var y: int? = 5; // specified nullable + y = null; // ok +} +``` + +For instance, explicit types are useful for structures: + +```tolk +fun demo() { + var p1: Point = { x: 10, y: 20 }; + + // without an explicit type, use `Point { ... }` + var p2 = Point { x: 10, y: 20 }; +} +``` + +A variable **may be left unassigned** at declaration. Then it must be definitely assigned before its first use: + +```tolk +fun demo(mode: int) { + var result: int; // not assigned at declaration + + if (mode == MODE_SLOW) { + result = doSlowCalc(); + } else if (mode == MODE_FAST) { + result = doFastCalc(); + } else { + throw ERR_INVALID_MODE; + } + return result; // ok, it's definitely assigned +} +``` + +Creating **multiple variables at once** is actually destructuring of a tensor: + +```tolk +fun demo() { + var (a, b) = (1, ""); + + // with explicit types + var (c: int, d: slice) = (1, ""); +} +``` + +The block `{ ... }` opens **a nested scope**: + +```tolk +fun demo() { + val x = 10; + if (smth) { + val x = 50; // this is a different `x` + } + // x is 10 +} +``` + +## Parameters of a function + +Function parameters work exactly like local variables. They can be reassigned, but changes do not affect the caller's state: + +```tolk +fun analyze(userId: int?) { + if (userId == null) { + userId = DEFAULT_ID; + } + // ... +} + +fun demo() { + var id = null as int?; + analyze(id); + // id remains `null` +} +``` + +To make modifications to `userId` visible inside `demo`, the parameter must be declared as `mutate userId`. +See [mutability](/languages/tolk/syntax/mutability). + +## Constants + +Global-scope constants are declared with `const` **outside functions**: + +```tolk +const SLEEP_TIME_SEC = 5 +``` + +The right side of an assignment must be a constant expression: numbers, const literals, compile-time functions, etc. + +```tolk +// ok +const FLAG_JANUARY = 1 << 10 +const OP_TRANSFER = stringCrc32("transfer") + +// error: not a constant expression +const CUR_TIME = blockchain.now() +``` + +The type is inferred from assignment unless specified manually: + +```tolk +const MODE_NORMAL: uint32 = 0 +``` + +Constants are **not restricted to integers**: + +```tolk +// type `address` +const ADMIN_ADDR = address("UQ...") + +// type `coins` +const MINIMAL_COST = ton("0.05") + +// even objects with constant fields +const ZERO_POINT: Point = { x: 0, y: 0 } +``` + +**To calculate crc32 / etc. at compile-time**, use `stringCrc32("...")` and similar. +See [strings](/languages/tolk/types/strings#calculate-hex-%2F-crc32-%2F-etc-at-compile-time). + +To group integer constants, also use [enums](/languages/tolk/types/enums). + + + +## Global variables + +Tolk has the `global` keyword to declare variables **outside functions**: + +```tolk +global runtimeCalls: tuple +``` + +It must be followed by a type but cannot be initialized at the point of declaration: initialization is done manually at some point of a program. +A contract has several entrypoints (`get fun`, `onInternalMessage`, and more low-level). +So, a particular global must be initialized at some place where its forward usage is expected. + +```tolk +global runtimeCalls: tuple + +fun execute() { + runtimeCalls.push("start execute"); + // ... +} + +get fun devTrace() { + runtimeCalls = createEmptyTuple(); // initialize + val result = execute(); + return (result, runtimeCalls); +} +``` + + + + From 35ada07cf38050c9305a2fc00405e91454ff9a36 Mon Sep 17 00:00:00 2001 From: tolk-vm Date: Thu, 20 Nov 2025 19:00:17 +0300 Subject: [PATCH 05/11] [Tolk] New docs: language features --- languages/tolk/features/asm-functions.mdx | 231 ++++++++ .../tolk/features/auto-serialization.mdx | 471 ++++++---------- .../tolk/features/compiler-optimizations.mdx | 299 ++++++++++ languages/tolk/features/contract-getters.mdx | 110 ++++ languages/tolk/features/contract-storage.mdx | 194 +++++++ languages/tolk/features/lazy-loading.mdx | 185 ++++--- languages/tolk/features/message-handling.mdx | 258 +++++++++ languages/tolk/features/message-sending.mdx | 250 ++++----- languages/tolk/features/standard-library.mdx | 523 ++++++++++++++++++ 9 files changed, 2026 insertions(+), 495 deletions(-) diff --git a/languages/tolk/features/asm-functions.mdx b/languages/tolk/features/asm-functions.mdx index 49c82dfd9..2a7d460b6 100644 --- a/languages/tolk/features/asm-functions.mdx +++ b/languages/tolk/features/asm-functions.mdx @@ -2,3 +2,234 @@ title: "Assembler functions" --- +import { Aside } from '/snippets/aside.jsx'; + +Functions in Tolk may be defined using assembler code. +It's a low-level feature that requires deep understanding of stack layout, [Fift](/languages/fift/overview), and [TVM](/tvm/overview). + +## Standard functions are actually `asm` wrappers + +Many functions from [stdlib](/languages/tolk/features/standard-library) are translated to Fift assembler directly. + +For example, TVM has a `HASHCU` instruction: "calculate hash of a cell". +It pops a cell from the stack and pushes an integer in the range 0 to 2^256-1. +Therefore, the method `cell.hash` is defined this way: + +```tolk +@pure +fun cell.hash(self): uint256 + asm "HASHCU" +``` + +The type system guarantees that when this method is invoked, a TVM `CELL` will be the topmost element (`self`). + +## Custom functions are declared in the same way + +```tolk +@pure +fun incThenNegate(v: int): int + asm "INC" "NEGATE" +``` + +A call `incThenNegate(10)` will be translated into those commands. + +A good practice is to specify `@pure` if the body does not modify TVM state or throw exceptions. + +The return type for `asm` functions is mandatory (for regular functions, it's auto-inferred from `return` statements). + + + +## Multi-line asm + +To embed a multi-line command, use triple quotes: + +```tolk +fun hashStateInit(code: cell, data: cell): uint256 asm """ + DUP2 + HASHCU + ... + ONE HASHEXT_SHA256 +""" +``` + +It is treated as a single string and inserted as-is into Fift output. +In particular, it may contain `//` comments inside (valid comments for Fift). + +## Stack order for multiple slots + +When calling a function, arguments are pushed in a declared order. +The last parameter becomes the topmost stack element. + +If an instruction results in several slots, the resulting type should be a tensor or a struct. + +For example, write a function `abs2` that calculates `abs()` for two values at once: `abs2(-5, -10)` = `(5, 10)`. +Stack layout (the right is the top) is written in comments. + +```tolk +fun abs2(v1: int, v2: int): (int, int) + asm // v1 v2 + "ABS" // v1 v2_abs + "SWAP" // v2_abs v1 + "ABS" // v2_abs v1_abs + "SWAP" // v1_abs v2_abs +``` + +## Rearranging arguments on the stack + +Sometimes a function accepts parameters in an order different from what a TVM instruction expects. +For example, `GETSTORAGEFEE` expects the order "cells bits seconds workchain". +But for more clear API, workchain should be passed first. +Stack positions can be reordered via the `asm(...)` syntax: + +```tolk +fun calculateStorageFee(workchain: int8, seconds: int, bits: int, cells: int): coins + asm(cells bits seconds workchain) "GETSTORAGEFEE" +``` + +Similarly for return values. If multiple slots are returned, and they must be reordered to match typing, +use `asm(-> ...)` syntax: + +```tolk +fun asmLoadCoins(s: slice): (slice, int) + asm(-> 1 0) "LDVARUINT16" +``` + +Both the input and output sides may be combined: `asm(... -> ...)`. +Reordering is mostly used with `mutate` variables. + +## `mutate` and `self` in assembler functions + +The `mutate` keyword (see [mutability](/languages/tolk/syntax/mutability)) works +by implicitly returning new values via the stack — both for regular and `asm` functions. + +For better understanding, let's look at regular functions first. +The compiler does all transformations automatically: + +```tolk +// transformed to: "returns (int, void)" +fun increment(mutate x: int): void { + x += 1; + // a hidden "return x" is inserted +} + +fun demo() { + // transformed to: (newX, _) = increment(x); x = newX + increment(mutate x); +} +``` + +How to implement `increment()` via asm? + +```tolk +fun increment(mutate x: int): void + asm "INC" +``` + +The function still returns `void` (from the type system's perspective it does not return a value), +but `INC` leaves a number on the stack — that's a hidden "return x" from a manual variant. + +Similarly, it works for `mutate self`. +An `asm` function should place `newSelf` onto the stack before the actual result: + +```tolk +// "TPUSH" pops (tuple) and pushes (newTuple); +// so, newSelf = newTuple, and return `void` (syn. "unit") +fun tuple.push(mutate self, value: X): void + asm "TPUSH" + +// "LDU" pops (slice) and pushes (int, newSlice); +// with `asm(-> 1 0)`, we make it (newSlice, int); +// so, newSelf = newSlice, and return `int` +fun slice.loadMessageFlags(mutate self): int + asm(-> 1 0) "4 LDU" +``` + +To return `self` for chaining, just specify a return type: + +```tolk +// "STU" pops (int, builder) and pushes (newBuilder); +// with `asm(op self)`, we put arguments to correct order; +// so, newSelf = newBuilder, and return `void`; +// but to make it chainable, `self` instead of `void` +fun builder.storeMessageOp(mutate self, op: int): self + asm(op self) "32 STU" +``` + +## `asm` is compatible with structures + +Methods for structures may also be declared as assembler ones knowing the layout: fields are placed sequentially. +For instance, a struct with one field is identical to this field. + +```tolk +struct MyCell { + private c: cell +} + +@pure +fun MyCell.hash(self): uint256 + asm "HASHCU" +``` + +Similarly, a structure may be used instead of tensors for returns. +This is widely practiced in `map` methods over TVM dictionaries: + +```tolk +struct MapLookupResult { + private readonly rawSlice: slice? + isFound: bool +} + +@pure +fun map.get(self, key: K): MapLookupResult + builtin +// it produces `DICTGET` and similar, which push +// (slice -1) or (null 0) — the shape of MapLookupResult +``` + +## Generics in `asm` should be single-slot + +Take `tuple.push` as an example. The `TPUSH` instruction pops `(tuple, someVal)` and pushes `(newTuple)`. +It should work with any `T`: int, int8, slice, etc. + +```tolk +fun tuple.push(mutate self, value: T): void + asm "TPUSH" +``` + +A reasonable question: how should `t.push(somePoint)` work? +The stack would be misaligned, because `Point { x, y }` is not a single slot. +The answer: this would not compile. + +```ansi +dev.tolk:6:5: error: can not call `tuple.push` with T=Point, because it occupies 2 stack slots in TVM, not 1 + + // in function `main` + 6 | t.push(somePoint); + | ^^^^^^ +``` + +Only regular and built-in generics may be instantiated with variadic type arguments, `asm` cannot. + +## Do not use `asm` for micro-optimizations + +Introduce assembler functions only for rarely-used TVM instructions that are not covered by stdlib. +For example, when manually parsing merkle proofs or calculating extended hashes. + +However, attempting to micro-optimize with `asm` instead of writing straightforward code is not desired. +The compiler is smart enough to generate optimal bytecode from consistent logic. +For instance, it automatically inlines simple functions, so create one-liner methods without any worries about gas: + +```tolk +fun builder.storeFlags(mutate self, flags: int): self { + return self.storeUint(32, flags); +} +``` + +The function above is better than "manually optimized" as `32 STU`. Because: + +- it is inlined automatically +- for constant `flags`, it's merged with subsequent stores into `STSLICECONST` + +See [compiler optimizations](/languages/tolk/features/compiler-optimizations). diff --git a/languages/tolk/features/auto-serialization.mdx b/languages/tolk/features/auto-serialization.mdx index 641a216da..88a7f1018 100644 --- a/languages/tolk/features/auto-serialization.mdx +++ b/languages/tolk/features/auto-serialization.mdx @@ -1,8 +1,12 @@ --- -title: "Auto packing to/from cells" +title: "Automatic serialization" --- -A short demo of auto serialization: +import { Aside } from '/snippets/aside.jsx'; + +All data in TON (messages, storage, etc.) is represented with **cells**. +Tolk type system is designed to express cell contents, +enabling auto-serialization via `fromCell` and `toCell`: ```tolk struct Point { @@ -13,59 +17,18 @@ struct Point { fun demo() { var value: Point = { x: 10, y: 20 }; - // makes a cell containing "0A14" + // makes a cell containing "0A14" (hex) var c = value.toCell(); // back to { x: 10, y: 20 } var p = Point.fromCell(c); } ``` -## Key features of auto-serialization - -- Supports all types: unions, tensors, nullables, generics, atomics, ... -- Allows to specify serialization prefixes (particularly, opcodes) -- Allows to manage cell references and when to load them -- Granular control over error codes -- Unpacks data from a cell or a slice, mutate it or not -- Packs data to a cell or a builder -- Warns if data potentially exceeds 1023 bits -- More efficient than manual serialization - -## List of supported types and how they are serialized - -Follow [Overall: serialization](/languages/tolk/types/overall-serialization). - -Some examples: +## How each type is serialized -```tolk -struct A { - f1: int8 // just int8 - f2: int8? // maybe int8 - f3: address // internal address - f4: B // embed fields of struct B - f5: B? // maybe B - f6: coins // used for money amounts - - r1: cell // always-existing untyped ref - r2: Cell // typed ref - r3: Cell? // optional ref that stores int32 - - u1: int32 | int64 // Either - u2: B | C // also Either - u3: B | C | D // manual or autogenerated prefixes - u4: bits4 | bits8? // Maybe - - // even this works - e: Point | Cell - - // rest of slice - rest: RemainingBitsAndRefs -} -``` +To dig into binary data, follow [Overall: serialization](/languages/tolk/types/overall-serialization). -## Serialization prefixes and opcodes - -Structures provide special syntax for declare "prefixes". +A `struct` can provide its "serialization prefix". 32-bit ones are typically called **opcodes** and used for messages (incoming and outgoing): ```tolk @@ -77,323 +40,236 @@ struct (0x7362d09c) TransferNotification { But prefixes are not restricted to be 32-bit: `0x000F` is a 16-bit prefix, `0b010` is 3-bit (binary). -Example of structures combined to a union, with comments: +[Serialization of structures](/languages/tolk/types/overall-serialization#structures) is also on the "overall" page. -```tolk -struct (0b001) AssetSimple { - workchain: int8 - ptr: bits32 -} - -struct (0b1000) AssetBooking { - orderId: uint64 -} - -type Asset = AssetSimple | AssetBooking // | ... +## Controlling cell references. Typed cells -struct ProvideAssetMessage { - // ... - asset: Asset -} +Fields of a struct are serialized one by one. +The compiler does not reorder fields, create implicit references, etc. +When data should be stored in a ref, it's done explicitly. +A developer controls exactly when each ref is loaded. -fun demo() { - // msg.asset is parsed as '001' + int8 + bits32 OR ... - val msg = ProvideAssetMessage.fromSlice(s); - - // now, msg.asset is just a union - match (msg.asset) { - AssetSimple => { // smart cast - msg.asset.workchain; - msg.asset.ptr; - } - // ... other variants - } - // or test with `is` operator - if (msg.asset is AssetBooking) { - // ... - } -} -``` +**There are two types of references — typed and untyped**: -When serializing, everything also works as expected: +- `cell` — untyped ref — just "some cell", "arbitrary content" +- `Cell` — typed ref — a cell, which internal structure is known ```tolk -val out: ProvideAssetMessage = { - // will be serialized as: '001' + '00000011' + bits32 - asset: AssetSimple { - workchain: 3, - ptr: SOME_32_BITS - } +struct NftStorage { + ownerAddress: address + nextItemIndex: uint64 + content: cell // untyped ref + royalty: Cell // typed ref } -``` - -Note that if a struct has a prefix, it does not matter whether it's inside any union or not. -```tolk -struct (0x00FF) MyData { +struct RoyaltyParams { + numerator: uint16 // ... } - -fun demo() { - MyData.fromSlice(s); // expected to be '00FF...' (hex) - data.toCell(); // '00FF...' -} ``` -That's why, structs for outgoing messages (with 32-bit opcodes), being serialized, include opcodes in binary data. - -## What can NOT be serialized - -- `int` can't be serialized, it does not define binary width; use `int32`, `uint64`, etc. -- `slice`, for the same reason; use `address` or `bitsN` -- tuples, not implemented -- `A | B` if A has manual serialization prefix, B not (because it seems like a bug in code) -- `primitives | ... | structs` if structs have serialization prefixes (because it's not definite what prefixes to use for primitives) - -Example of invalid: +A call `NftCollectionStorage.fromCell()` is processed as follows: -```tolk -struct (0xFF) A {} -struct B {} // forgot prefix +1. read `address` +1. read `uint64` +1. read two refs without unpacking them: only their pointers are loaded -fun invalidDemo(obj: A | B) { - // (it's better to fire an error than to generate '0'+'FF'+dataA OR '1'+dataB) - obj.toCell(); // error: A has prefix, B not -} -``` +## `Cell` must be loaded to get `T` -## Error messages if serialization unavailable - -If someone, by mistake, uses unsupported types, Tolk compiler will fire a meaningful error. Example: +Let's look at the `royalty` above: ```tolk -struct ExtraData { - owner: address - lastTime: int // mistake is here -} - -struct Storage { +struct NftStorage { // ... - more: Cell + royalty: Cell } - -fun errDemo() { - Storage.fromSlice(""); -} -``` - -fires an error: - -``` -auto-serialization via fromSlice() is not available for type `Storage` -because field `Storage.more` of type `Cell` can't be serialized -because type `ExtraData` can't be serialized -because field `ExtraData.lastTime` of type `int` can't be serialized -because type `int` is not serializable, it doesn't define binary width -hint: replace `int` with `int32` / `uint64` / `coins` / etc. ``` -## Controlling cell references. Typed cells - -Fields of a struct are serialized one be one. -The compiler does not reorder fields, create implicit references, etc. -Hence, when data should be placed in a ref, it's done explicitly. -Similarly, a developer controls, when exactly contents of that ref is loaded. - -There are two types of references: typed and untyped. - -```tolk -struct NftCollectionStorage { - ownerAddress: address - nextItemIndex: uint64 - content: cell // untyped - nftItemCode: cell // untyped - royaltyParams: Cell // typed -} +Since it is a cell, `storage.royalty.xxx` is NOT valid: -struct RoyaltyParams { - numerator: uint16 - denominator: uint16 - royaltyAddress: address -} +```ansi +// error: `Cell` has no field `numerator` +storage.royalty.numerator; + ^^^^^^^^^ ``` -A call `NftCollectionStorage.fromSlice` (or fromCell) is processed as follows: - -1. read address -1. read uint64 -1. read three refs; do not unpack them: just load pointers to cells - -Note, that `royaltyParams` is `Cell`, not `T` itself. To access fields (`numerator` and others), manually unpack that ref: +To access `numerator` and other fields, manually **load that ref**: ```tolk -// error: field does not exist in type `Cell` -st.royaltyParams.numerator +val royalty = storage.royalty.load(); // Cell to T +// or, alternatively +val royalty = RoyaltyParams.fromCell(storage.royalty); -// that's the way -val rp = st.royaltyParams.load(); // Cell -> T -rp.numerator - -// alternatively -val rp = RoyaltyParams.fromCell(st.royaltyParams); +// ok +royalty.numerator; ``` -And vice versa: when composing such a struct, a cell should be assigned there, not an object: +And conversely, when composing an instance, **assign a cell, not an object**: ```tolk -val st: NftCollectionStorage = { - ... +val storage: NftStorage = { // error - royaltyParams: RoyaltyParams{ ... } + royalty: RoyaltyParams{ ... } // correct - royaltyParams: RoyaltyParams{ ... }.toCell() + royalty: RoyaltyParams{ ... }.toCell() } ``` -A method `T.toCell()` returns `Cell`: +The following snippet summarizes the behavior: ```tolk -val c = p.toCell(); // Point to Cell -val p2 = c.load(); // Cell to Point +pCell = point.toCell(); // `Point` to `Cell` +point = pCell.load(); // `Cell` to `Point` ``` -So, typed cells are a powerful mechanism to express the contents of referenced cells. Note that `Cell
` or even `Cell` is also okay, `T` is not restricted to structures. -When it comes to untyped cells — just `cell` — they also denote references, but don't denote their inner contents, don't have the `.load()` method. - -It's just _some cell_, like code/data of a contract or an untyped nft content. - -## Remaining data after reading +## Custom serializers for custom types -What happens if after `fromSlice` remaining data is left? +Using type aliases, it is possible to override serialization behavior when it cannot be expressed using existing types: ```tolk -// input is "0102FF" -Point.fromSlice(input); -``` +type MyString = slice -Byte "01" for x, byte "02" for y, and the remaining "FF" — is it correct? +fun MyString.packToBuilder(self, mutate b: builder) { + // custom logic +} -**By default, this is incorrect**. By default, functions `fromCell` and `fromSlice` ensure the slice end after reading. -In this case, exception 9 ("cell underflow") is thrown. +fun MyString.unpackFromSlice(mutate s: slice) { + // custom logic +} +``` -This behavior can be turned off with an option: + -```tolk -Point.fromSlice(s, { - assertEndAfterReading: false -}) -``` +See [Serialization of type aliases](/languages/tolk/types/overall-serialization#type-aliases) for examples. -## Custom serializers for custom types +## What if input is corrupted -Using type aliases, it's possible to override serialization behavior in case it can not be expresses in existing types: +How will `Point.fromCell(c)` work if `c` is less than 16 bits? ```tolk -type MyString = slice - -fun MyString.packToBuilder(self, mutate b: builder) { - // custom logic +struct Point { + x: int8 + y: int8 } -fun MyString.unpackFromSlice(mutate s: slice) { - // custom logic +fun demo() { + Point.fromCell(createEmptyCell()); } ``` -For examples, proceed to [Serialization of type aliases](/languages/tolk/types/overall-serialization#type-aliases). +The answer: **an exception is thrown**. In multiple cases, actually: -## UnpackOptions and PackOptions +- input is too small — not enough bits or refs, unless `lazy fromCell` +- input is too big — contains extra data (can be turned off) +- `address` has incorrect format +- `enum` has an invalid value +- a struct prefix does not match +- etc. -These structs control behavior of `fromCell`, `toCell`, and similar functions: +An exception code is typically 9 ("cell underflow") or 5 ("out of range"). + +Some aspects of this behavior can be controlled. For example, if "input is too big" is okay, use an option: ```tolk MyMsg.fromSlice(s, { - throwIfOpcodeDoesNotMatch: 0xFFFF + assertEndAfterReading: false }) ``` -Serialization functions have the second optional parameter, actually: +## UnpackOptions and PackOptions + +Behavior of `fromCell` and `toCell` can be controlled by options: ```tolk -fun T.fromSlice(rawSlice: slice, options: UnpackOptions = {}): T; +MyMsg.fromCell(c, { + // options object +}) ``` -When omitted, default options are used, which can be overridden as shown above. +For deserialization (`fromCell` and similar), there are two options: -For deserialization (`fromCell` and similar), there are now two available options: - -| Field of UnpackOptions | Description | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `assertEndAfterReading` | after finished reading all fields from a cell/slice, call `slice.assertEnd` to ensure no remaining data left; it's the default behavior, it ensures that you've fully described data you're reading with a struct; for struct `Point`, input `0102` is ok, `0102FF` will throw `excno` 9; **default: true** | -| `throwIfOpcodeDoesNotMatch` | this excNo is thrown if opcode doesn't match, e.g. for `struct (0x01) A` given input "88..."; similarly, for a union type, this is thrown when none of the opcodes match; **default: 63** | +```tolk +MyMsg.fromCell(c, { + // call `assertEnd` to ensure no remaining data left; + // (in other words, the struct describes all data) + assertEndAfterReading: true, // default: true + + // this errCode is thrown if opcode doesn't match, + // e.g. for `struct (0x01) A` given input "88...", + // or for a union type, none of the prefixes match + throwIfOpcodeDoesNotMatch: 63, // default: 63 +}) +``` -For serialization (`toCell` and similar), there is now one option: +For serialization (`toCell` and similar), there is one option: -| Field of PackOptions | Description | -| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `skipBitsNValidation` | when a struct has a field of type `bits128` and similar (it's a slice under the hood), by default, compiler inserts runtime checks (get bits/refs count + compare with 128 + compare with 0); these checks ensure that serialized binary data will be correct, but they cost gas; however, if you guarantee that a slice is valid (for example, it comes from trusted sources), set this option to true to disable runtime checks; _note: `int32` and other are always validated for overflow without any extra gas, so this flag controls only rarely used `bitsN` types;_ **default: false** | +```tolk +obj.toCell({ + // for `bits128` and similar (a slice under the hood), + // insert the checks (bits == 128 and refs == 0); + // turn off to save gas if you guarantee input is valid; + // `intN` are always validated, it's only for `bitsN` + skipBitsNValidation: false, // default: false +}); +``` -## Full list of serialization functions +## Not only `fromCell`, but `fromSlice` and more -Each of them can be controlled by `PackOptions` described above. +This API is also designed to integrate with low-level features. +Each of these functions can be controlled by `UnpackOptions`. -1. `T.toCell()` — convert anything to a cell. Example: +1. `T.fromCell(c)` — parse a cell: "c.beginParse() + fromSlice": ```tolk -contract.setData(storage.toCell()); +var storage = NftStorage.fromCell(contract.getData()); ``` -Internally, a builder is created, all fields are serialized one by one, and a builder is flushed (beginCell() + serialize fields + endCell()). - -2. `builder.storeAny(v)` — similar to `builder.storeUint()` and others, but allows storing structures. Example: +2. `T.fromSlice(s)` — parse a slice (a slice is **not mutated**): ```tolk -var b = beginCell() - .storeUint(32) - .storeAny(msgBody) // T=MyMsg here - .endCell(); +var msg = CounterIncrement.fromSlice(s); ``` -## Full list of deserialization functions - -Each of them can be controlled by `UnpackOptions` described above. - -1. `T.fromCell(c)` — parse anything from a cell. Example: +3. `slice.loadAny()` — **mutate** the slice: ```tolk -var st = MyStorage.fromCell(contract.getData()); +var storage = s.loadAny(); +var nextNum = s.loadAny(); // also ok ``` -Internally, a cell is unpacked to a slice, and that slice is parsed (c.beginParse() + read from slice). +Note: `options.assertEndAfterReading` is ignored by this function because it is intended to read data from the middle. -2. `T.fromSlice(s)` — parse anything from a slice. Example: +4. `slice.skipAny()` — like `skipBits()` and similar: ```tolk -var msg = CounterIncrement.fromSlice(cs); +s.skipAny(); // skips 16 bits ``` -All fields are read from a slice immediately. If a slice is corrupted, an exception is thrown (most likely, `excode` 9 "cell underflow"). Note, that a passed slice is NOT mutated; its internal pointer is NOT shifted. To mutate it, like `cs.loadInt()`, consider calling `cs.loadAny()`. +**Same for serialization.** +Each of these functions can be controlled by `PackOptions`. -3. `slice.loadAny` — parse anything from a slice, shifting its internal pointer. Similar to `slice.loadUint()` and others, but allows loading structures. Example: +1. `T.toCell()` — works as "beginCell() + serialize + endCell()": ```tolk -var st: MyStorage = cs.loadAny(); // or cs.loadAny() -cs.loadAny(); // = cs.loadInt(32) +contract.setData(storage.toCell()); ``` -Similar to `MyStorage.fromSlice(cs)`, but called as a method and mutates the slice. Note: `options.assertEndAfterReading` is ignored by this function, because it's actually intended to read data from the middle. - -4. `slice.skipAny` — skip anything in a slice, shifting its internal pointer. Similar to `slice.skipBits()` and others, but allows skipping structures. Example: +2. `builder.storeAny(v)` — like `storeUint()` and similar: ```tolk -struct TwoInts { a: int32; b: int32; } -cs.skipAny(); // skips 64 bits -cs.skipAny(); // = cs.skipBits(32) +var b = beginCell() + .storeUint(32) + .storeAny(msgBody) // T=MyMsg here + .endCell(); ``` -## Special type RemainingBitsAndRefs +## Special type: RemainingBitsAndRefs It's a built-in type to get "all the rest" slice tail on reading. Example: @@ -404,7 +280,8 @@ struct JettonMessage { } ``` -Then, after `JettonMessage.fromCell`, forwardPayload contains _everything left after reading fields above_. Essentially, it's an alias to a slice which is handled specially while unpacking: +After `JettonMessage.fromCell`, forwardPayload contains **everything left after reading the fields above**. +Essentially, it's an alias to a slice which is handled specially by the compiler: ```tolk type RemainingBitsAndRefs = slice @@ -412,17 +289,13 @@ type RemainingBitsAndRefs = slice ## What if data exceeds 1023 bits -Tolk compiler calculates maximum size of every serializable type and warns if it potentially exceeds 1023 bits. -The developer should perform one of the actions: +Tolk compiler warns if a serializable struct potentially exceeds 1023 bits. +A developer should **take one of the following actions**: -1. either to suppress the error by placing an annotation above a struct; it means "okay, I understand" +1. to suppress the error; it means "okay, I understand" 1. or reorganize a struct by splitting into multiple cells -Why "potentially exceeds"? Because for many types, their size can vary: - -- `int8?` is either one or nine bits -- `coins` is variadic: from 4 bits (small values) up to 124 bits -- etc. +Why "potentially exceeds"? Because for many types, their size can vary. For example, `int8?` is either one or nine bits, `coins` is 4..124 bits, etc. So, given a struct: @@ -436,7 +309,7 @@ struct MoneyInfo { And trying to serialize it, the compiler prints an error: -``` +```ansi wrap struct `MoneyInfo` can exceed 1023 bits in serialization (estimated size: 808..1048 bits) ... (and some instructions) ``` @@ -452,17 +325,17 @@ struct MoneyInfo { } ``` -2. or `coins` are expected to be billions of billions, so data really can exceed; in this case, extract some fields into a separate cell; for example, store 800 bits as a ref; or extract other 2 fields and ref them: +2. or `coins` are expected to be billions of billions, so data really can exceed; in this case, extract some fields into a separate cell; for example, store 800 bits as a ref or extract the other two fields: ```tolk -// either extract the first field +// extract the first field struct MoneyInfo { fixed: Cell wallet1: coins wallet2: coins } -// or extract other 2 fields +// or extract the other two fields struct WalletsBalances { wallet1: coins wallet2: coins @@ -473,33 +346,53 @@ struct MoneyInfo { } ``` -A general advice: leave more frequently used fields directly and place less-frequent fields to another ref. -All in all, the compiler indicates on potential cell overflow, and it's a developer's choice how to overcome this. +A general guideline: leave frequently used fields directly and place less-frequent fields into a nested ref. +Overall, the compiler reports potential overflow, and it is the developer's responsibility to resolve it. + +## What if serialization is unavailable + +A common mistake: using `int` (it cannot be serialized; use `int32`, `uint64`, etc.; see [numeric types](/languages/tolk/types/numbers)). + +```tolk +struct Storage { + owner: address + lastTime: int // mistake is here +} + +fun errDemo() { + Storage.fromSlice(""); +} +``` + +The compiler reports a reasonable error: -Probably, in the future, there will be more policies (besides "suppress") — for example, to auto-repack fields. For now, it's absolutely straightforward. +```ansi +auto-serialization via fromSlice() is not available for type `Storage` +because field `Storage.lastTime` of type `int` can't be serialized +because type `int` is not serializable, it doesn't define binary width +hint: replace `int` with `int32` / `uint64` / `coins` / etc. +``` ## Integration with message sending -Auto-serialization is natively integrated with sending messages to other contracts. -The compiler automatically serializes body, detects whether it fits into a message cell, etc. +Auto-serialization is integrated natively with message sending to other contracts: ```tolk val reply = createMessage({ - bounce: BounceMode.RichBounce, - value: ton("0.05"), - dest: senderAddress, + // ... body: RequestedInfo { // auto-serialized - ... + // ... } }); reply.send(SEND_MODE_REGULAR); ``` -Continue reading here: [Universal createMessage](/languages/tolk/features/message-sending). +See: [sending messages](/languages/tolk/features/message-sending). ## Not "fromCell" but "lazy fromCell" -Tolk has a special keyword `lazy` that is combined with auto-deserialization. The compiler will load not a whole struct, but only fields requested. +Tolk provides a special keyword `lazy` combined with auto-deserialization. +The compiler loads only the fields requested, rather than the entire struct. ```tolk struct Storage { @@ -507,14 +400,14 @@ struct Storage { seqno: uint32 subwalletId: uint32 publicKey: uint256 - extensions: dict + extensions: cell? } -get fun getPublicKey() { +get fun publicKey() { val st = lazy Storage.fromCell(contract.getData()); // <-- here "skip 65 bits, preload uint256" is inserted return st.publicKey } ``` -Continue reading here: [lazy loading, partial loading](/languages/tolk/features/lazy-loading). +See: [lazy loading](/languages/tolk/features/lazy-loading). diff --git a/languages/tolk/features/compiler-optimizations.mdx b/languages/tolk/features/compiler-optimizations.mdx index 07e541796..5aa31be36 100644 --- a/languages/tolk/features/compiler-optimizations.mdx +++ b/languages/tolk/features/compiler-optimizations.mdx @@ -2,3 +2,302 @@ title: "Compiler optimizations" --- +import { Aside } from '/snippets/aside.jsx'; + +Tolk compiler is smart enough to generate optimal bytecode from a clear, idiomatic code. +The ideal target is "zero overhead": extracting variables and simple methods should not increase gas consumption. + + + +## Constant folding + +Tolk compiler evaluates constant variables and conditions at compile-time: + +```tolk +fun calcSecondsInAYear() { + val days = 365; + val minutes = 60 * 24 * days; + return minutes * 60; +} +``` + +All these computations are done statically, resulting in + +```fift +31536000 PUSHINT +``` + +It works for conditions as well. +For example, when `if`'s condition is guaranteed to be false, only `else` body is left. +If an `assert` is proven statically, only the corresponding `throw` remains. + +```tolk +fun demo(s: slice) { + var flags = s.loadUint(32); // definitely >= 0 + if (flags < 0) { // always false + // ... + } + return s.remainingBitsCount(); +} +``` + +The compiler drops `IF` at all (both body and condition evaluation), because it can never be reached. + +While calculating compile-time values, all mathematical operators are emulated as they would have run at runtime. +Additional flags like "this value is even / non-positive" are also tracked, leading to more aggressive code elimination. +It works not only for plain variables, but also for struct fields, tensor items, across inlining, etc. +(because it happens after transforming a high-level syntax tree to low-level intermediate representation). + +## Merge constant builder.storeInt + +When building cells manually, there is no need to group constant `storeUint` into a single number. + +```tolk +// no need for manual grouping anymore +b.storeUint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1); +``` + +Successive `builder.storeInt` are merged automatically: + +```tolk +b.storeUint(0, 1) // prefix + .storeUint(1, 1) // ihr_disabled + .storeUint(1, 1) // bounce + .storeUint(0, 1) // bounced + .storeUint(0, 2) // addr_none +``` + +is translated to just + +```fift +b{011000} STSLICECONST +``` + +It works together with constant folding — even with variables and conditions, when they turn out to be constant: + +```tolk +fun demo() { + var x = 0; + var b = beginCell(); + b.storeUint(x, 4); + x += 12; + if (x > 0) { + x += x; + } + b.storeUint(x + 2, 8); + return b; +} +``` + +is translated to just + +```fift +NEWC +x{01a} STSLICECONST +``` + +It works even for structures — including their fields: + +```tolk +struct Point { + x: uint32 + y: uint32 +} + +fun demo() { + var p: Point = { x: 10, y: 20 }; + return p.toCell(); +} +``` + +becomes + +```fift +NEWC +x{0000000a00000014} STSLICECONST +ENDC +``` + +_(in the future, Tolk will be able to emit a constant cell here)_ + +That's the reason why [createMessage](/languages/tolk/features/message-sending) for unions is so lightweight. +The compiler really generates all IF-ELSE and STU, but in a later analysis, +they become constant (since types are compile-time known), +and everything flattens into simple PUSHINT / STSLICECONST. + +## Auto-inline functions + +Tolk inlines functions at the compiler level: + +```tolk +fun Point.create(x: int, y: int): Point { + return {x, y} +} + +fun Point.getX(self) { + return self.x +} + +fun sum(a: int, b: int) { + return a + b +} + +fun main() { + var p = Point.create(10, 20); + return sum(p.getX(), p.y); +} +``` + +is compiled just to + +```fift +main PROC:<{ + 30 PUSHINT +}> +``` + +The compiler **automatically determines which functions to inline** and also gives manual control. + +### How does auto-inline work? + +- simple, small functions are always inlined +- functions called only once are always inlined + +For every function, the compiler calculates some "weight" and the usages count. + +- if `weight < THRESHOLD`, the function is always inlined +- if `usages == 1`, the function is always inlined +- otherwise, an empirical formula determines inlining + +Inlining is efficient in terms of stack manipulations. +It works with arguments of any stack width, any functions and methods, except recursive or having "return" in the middle. + +As a conclusion, create utility methods without worrying about gas consumption, they are absolutely zero-cost. + +### How to control inlining manually? + +- `@inline` forces inlining even for large functions +- `@noinline` prevents from being inlined +- `@inline_ref` preserves an inline reference, suitable for rarely executed paths + +### What can NOT be auto-inlined? + +A function is NOT inlined, even if marked with `@inline`, if: + +- contains `return` in the middle; multiple return points are unsupported +- participates in a recursive call chain `f -> g -> f` +- is used as a non-call; e.g., as a reference `val callback = f` + +For example, this function cannot be inlined due to `return` in the middle: + +```tolk +fun executeForPositive(userId: int) { + if (userId <= 0) { + return; + } + // ... +} +``` + +The advice is to check pre-conditions out of the function and keep body linear. + +## Peephole and stack optimizations + +After the code has been analyzed and transformed to IR, the compiler repeatedly replaces some assembler combinations to equal ones, but cheaper. +Some examples are: + +- stack permutations: DUP + DUP => 2DUP, SWAP + OVER => TUCK, etc. +- N LDU + NIP => N PLDU +- SWAP + N STU => N STUR, SWAP + STSLICE => STSLICER, etc. +- SWAP + EQUAL => EQUAL and other symmetric like MUL, OR, etc. +- 0 EQINT + N THROWIF => N THROWIFNOT and vice versa +- N EQINT + NOT => N NEQINT and other xxx + NOT +- ... + +Some others are done semantically in advance when it's safe: + +- replace a ternary operator to `CONDSEL` +- evaluate arguments of `asm` functions in a desired stack order +- evaluate struct fields of a shuffled object literal to fit stack order + +## Lazy loading + +The magic `lazy` keyword loads only required fields from a cell/slice: + +```tolk +struct Storage { + // ... +} + +get fun publicKey() { + val st = lazy Storage.load(); + // <-- fields before are skipped, publicKey preloaded + return st.publicKey +} +``` + +The compiler tracks exactly which fields are accessed, and unpacks only those fields, skipping the rest. + +Read [lazy loading](/languages/tolk/features/lazy-loading). + +## Suggestions for manual optimizations + +Although the compiler performs substantial work in the background, there are still cases when a developer can gain a few gas units. + +The primary aspect is **changing evaluation order** to target fewer stack manipulations. +The compiler does not reorder blocks of code unless they are constant expressions or pure calls. +But a developer knows the context better. Generally, it looks like this: + +```tolk +fun demo() { + // variable initialization, grouped + val v1 = someFormula1(); + val v2 = someFormula2(); + val v3 = someFormula3(); + + // use them in calls, assertions, etc. + someUsage(v1); + anotherUsage(v2); + assert(v3) throw 123; +} +``` + +After the first block, the stack is `(v1 v2 v3)`. +But v1 is used at first, so the stack must be shuffled with `SWAP` / `ROT` / `XCPU` / etc. +If to rearrange assignments or usages — say, move `assert(v3)` upper — it will naturally pop the topmost element. +Of course, automatic reordering is unsafe and prohibited, but in exact cases business logic might be still valid. + +Another option is **using bitwise `& |` instead of logical `&& ||`**. +Logical operators are short-circuit: the right operand is evaluated only if required to. +It's implemented via conditional branches at runtime. +But in some cases, evaluating both operands is less expensive than a dynamic `IF`. + +The last possibility is **using low-level Fift code** for certain independent tasks that cannot be expressed imperatively. +Usage of exotic TVM instructions like `NULLROTRIFNOT` / `IFBITJMP` / etc. +Overriding how top-level Fift dictionary works for routing method\_id. And similar. +Old residents call it "deep fifting". +Anyway, it's applicable only to a very limited set of goals, mostly as exercises, not as real-world usage. + + + +## How to explore Fift assembler + +Tolk compiler outputs Fift assembler. The bytecode (bag of cells) is generated by Fift, actually. +Projects built on blueprint rely on `tolk-js` under the hood, which invokes Tolk and then Fift. + +As a result: + +- for command-line users, fift assembler is the compiler's output +- for blueprint users, it's an intermediate result, but can easily be found + +**To view Fift assembler in blueprint**, run `npm build` or `blueprint build` in a project. +After successful compilation, a directory `build/` is created, and a folder `build/ContractName/` +contains a `.fif` file. diff --git a/languages/tolk/features/contract-getters.mdx b/languages/tolk/features/contract-getters.mdx index 1f89f595e..e5b15d843 100644 --- a/languages/tolk/features/contract-getters.mdx +++ b/languages/tolk/features/contract-getters.mdx @@ -1,3 +1,113 @@ --- title: "Contract getters" --- + +import { Aside } from '/snippets/aside.jsx'; + +Contract getters (or "get methods") are declared using `get fun xxx()`. +They typically return data extracted from [storage](/languages/tolk/features/contract-storage). + +```tolk +get fun currentOwner() { + val storage = lazy Storage.load(); + return storage.ownerAddress; +} +``` + +## The return type is inferred if omitted + +Get methods follow the same behavior as [functions and methods](/languages/tolk/syntax/functions-methods). + +However, specifying the type explicitly is considered good practice: + +```tolk +get fun currentOwner(): address { + // ... +} +``` + +## Prefer camelCase for custom names + +- `get fun currentOwner()` — recommended +- `get fun get_current_owner()` — not recommended + +**Unless it's required to match standard TEPs.** +For example, a jetton wallet, by convention, should expose a method `get_wallet_data` +(because it was named so in early implementations). + +In such cases, `get fun get_wallet_data` is appropriate, even though it may not look very nice. + +## Use structures for complex replies + +When a getter returns several values — introduce a structure and return it. +It's perfectly fine if a structure is used only once. +Field names provide clear metadata for client wrappers and human readers. + +```tolk +struct JettonWalletDataReply { + jettonBalance: coins + ownerAddress: address + minterAddress: address + jettonWalletCode: cell +} + +get fun get_wallet_data(): JettonWalletDataReply { + val storage = lazy WalletStorage.load(); + + return { + jettonBalance: storage.jettonBalance, + ownerAddress: storage.ownerAddress, + minterAddress: storage.minterAddress, + jettonWalletCode: contract.getCode(), + } +} +``` + +## Use `lazy` for storage loading + +Prefer `lazy loadStorage()` over `loadStorage()`. +Unreferenced fields are automatically skipped, resulting in smaller bytecode. + +See [lazy loading](/languages/tolk/features/lazy-loading). + +## Get methods may accept parameters + +The parameter syntax is identical to that of regular functions: + +```tolk +get fun get_wallet_address(ownerAddress: address): address { + // ... +} +``` + +## Get methods are called off-chain + + + +Read the [Coming from Ethereum](/from-ethereum) guide. + +Get methods are evaluated off‑chain by external tooling such as: + +- APIs +- explorers +- lite servers + +## Get methods operate via the stack, not serialization + +Get methods accept parameters and return values via the TVM stack, since they are called off-chain. + +This is why **a getter can return `int`** (although it's not serializable, unlike `intN`, see [numbers](/languages/tolk/types/numbers)). +Returning a structure pushes its fields onto the stack as N separate values. +Client libraries such as Blueprint parse get‑method responses using a tuple reader. + +Get methods do not store their names. They are identified by a _method\_id_ = `crc16(name) | 0x10000` — to avoid storing extra strings on-chain. + +For low-level details, see [get methods in TVM](/tvm/get-method). diff --git a/languages/tolk/features/contract-storage.mdx b/languages/tolk/features/contract-storage.mdx index d1ebc8f5e..3938d241e 100644 --- a/languages/tolk/features/contract-storage.mdx +++ b/languages/tolk/features/contract-storage.mdx @@ -2,3 +2,197 @@ title: "Contract storage" --- +import { Aside } from '/snippets/aside.jsx'; + +Contract storage is not "something special". +It is a regular `struct`, serialized into persistent blockchain data. +Tolk does not impose strict rules, although several common guidelines are helpful in practice. + + + +## Common pattern: Storage, load(), and save() + +A storage a regular structure. It is convenient to add `load` and `store` methods: + +```tolk +struct Storage { + counterValue: int64 +} + +fun Storage.load() { + return Storage.fromCell(contract.getData()) +} + +fun Storage.save(self) { + contract.setData(self.toCell()) +} +``` + +Then, at any point in a program, it can be easily accessed or modified: + +```tolk +get fun currentCounter() { + var storage = lazy Storage.load(); + return storage.counterValue; +} + +fun demoModify() { + var storage = lazy Storage.load(); + storage.counterValue += 100; + storage.save(); +} +``` + +Concepts used: + +- `struct` behaves similarly to a TypeScript class. See [structures](/languages/tolk/types/structures). +- `fun Storage.f(self)` defines an instance method. See [functions](/languages/tolk/syntax/functions-methods). +- `T.fromCell()` deserializes a cell into `T`, and `obj.toCell()` packs it back into a cell. See [automatic serialization](/languages/tolk/features/auto-serialization). +- `lazy` operator does this parsing on demand. See [lazy loading](/languages/tolk/features/lazy-loading). +- `contract.getData()` fetches persistent data. See [standard library](/languages/tolk/features/standard-library). + +## Set default values to fields + +In TON, the contract's address depends on its initial storage, when a contract is created on-chain. +A good practice is to assign default values to fields that must have defined values at deployment: + +```tolk +struct WalletStorage { + // these fields must have these values when deploying + // to make the contact's address predictable + jettonBalance: coins = 0 + isFrozen: bool = false + + // these fields must be manually assigned for deployment + ownerAddress: address + minterAddress: address +} +``` + +Therefore, to calculate an initial address, only two fields are required to be provided. + +## Multiple contracts in a project + +When developing multiple contracts simultaneously (for example, a jetton minter and a jetton wallet), +every contract has its own storage shape described by a `struct`. + +Give these structs reasonable names — for example, `MinterStorage` and `WalletStorage`. +It's better to place them in a single file (`storage.tolk`) together with their methods. + +Contracts often deploy each other, and initial storage must be provided during deployment. +For example, a minter deploys a wallet, so `WalletStorage` becomes accessible via a simple import: + +```tolk +// all symbols from imported files become visible +import "storage" + +fun deploy(ownerAddress: address, minterAddress: address) { + val emptyWalletStorage: WalletStorage = { + ownerAddress, + minterAddress, + // the other two use their defaults + }; + // ... +} +``` + +See [sending messages](/languages/tolk/features/message-sending) for examples of deployment. + +## Storage that changes its shape + +Another pattern for address calculation and for security is: + +- when a contract is deployed, it has fields `a,b,c` (uninitialized storage) +- followed by a message supplying `d,e` — it becomes `a,b,c,d,e` + + + +Such patterns are common in NFTs. +Initially, an NFT has only `itemIndex` and `collectionAddress`, nothing more (an _uninitialized NFT_). +Upon initialization, fields `ownerAddress` and `content` are appended to a storage. +How can such logic be implemented? + +Since arbitrary imperative code is allowed, the suggested approach is: + +- describe two structures: "initialized" and "uninitialized" storage +- start loading `contract.getData()` +- detect whether storage is initialized based on its bits/refs counts +- parse into one or another struct + +A long demo with detailed comments: + +```tolk +// two structures representing different storage states + +struct NftItemStorage { + itemIndex: uint64 + collectionAddress: address + ownerAddress: address + content: cell +} + +struct NftItemStorageNotInitialized { + itemIndex: uint64 + collectionAddress: address +} + +// instead of the usual `load()` method — `startLoading()` + +fun NftItemStorage.startLoading() { + return NftItemStorageLoader.fromCell(contract.getData()) +} + +fun NftItemStorage.save(self) { + contract.setData(self.toCell()) +} + +// this helper detects shape of a storage +struct NftItemStorageLoader { + itemIndex: uint64 + collectionAddress: address + private rest: RemainingBitsAndRefs +} + +// when `rest` is empty, `collectionAddress` is the last field +fun NftItemStorageLoader.isNotInitialized(self) { + return self.rest.isEmpty() +} + +// `endLoading` continues loading when `rest` is not empty +fun NftItemStorageLoader.endLoading(mutate self): NftItemStorage { + return { + itemIndex: self.itemIndex, + collectionAddress: self.collectionAddress, + ownerAddress: self.rest.loadAny(), + content: self.rest.loadAny(), + } +} +``` + +Usage in `onInternalMessage`: + +```tolk +var loadingStorage = NftItemStorage.startLoading(); +if (loadingStorage.isNotInitialized()) { + // ... probably, initialize and save + return; +} + +var storage = loadingStorage.endLoading(); +// and the remaining logic: lazy match, etc. +``` + + diff --git a/languages/tolk/features/lazy-loading.mdx b/languages/tolk/features/lazy-loading.mdx index feb580496..e098ececd 100644 --- a/languages/tolk/features/lazy-loading.mdx +++ b/languages/tolk/features/lazy-loading.mdx @@ -2,6 +2,19 @@ title: "Lazy loading" --- +import { Aside } from '/snippets/aside.jsx'; + +Tolk language has a magic feature — the `lazy` keyword. +The compiler tracks exactly which fields are accessed, and automatically loads only those, skipping the rest. + +In practice, prefer `lazy T.fromCell()` to a regular `T.fromCell()`. + + + +## A short demo of `lazy` + Suppose there is a `Storage` struct in a wallet: ```tolk @@ -10,7 +23,7 @@ struct Storage { seqno: uint32 subwalletId: uint32 publicKey: uint256 - extensions: dict + extensions: cell? } fun Storage.load() { @@ -20,28 +33,28 @@ fun Storage.load() { What does `Storage.load()` do? It unpacks a cell, populates all struct fields, checks consistency, and so on. -**The magic of `lazy Storage.load()` is that it does not load the entire cell upfront**. Instead, the compiler tracks exactly which fields are accessed, and automatically loads only those, skipping the rest. +The magic of `lazy Storage.load()` is that it **does not load the entire cell**. Instead, unused fields are just skipped: ```tolk -get fun getPublicKey() { +get fun publicKey() { val st = lazy Storage.load(); // <-- here "skip 65 bits, preload uint256" is inserted return st.publicKey } ``` -That's it! With a single `lazy` keyword, loading is deferred until the data is accessed. The compiler tracks all control flow paths, inserts loading points as needed, groups unused fields to be skipped, etc. Best of all, this works with any type and any combination of fields. +The compiler tracks all control flow paths, inserts loading points as needed, groups unused fields to be skipped, etc. Best of all, this works with any type and any combination of fields. -## Even deeper than you might think +## Even deeper than it seems -Take a look at NFT collection: +Take a look at the NFT collection: ```tolk struct NftCollectionStorage { adminAddress: address nextItemIndex: uint64 content: Cell - ... + // ... } struct CollectionContent { @@ -51,7 +64,7 @@ struct CollectionContent { } ``` -Imagine: a developer needs just the `content` field from the storage — and then extract `commonKey` from it: +Suppose a developer needs to read `content` and get `commonKey` from it: ```tolk val storage = lazy NftCollectionStorage.load(); @@ -59,9 +72,9 @@ val storage = lazy NftCollectionStorage.load(); val contentCell = storage.content; ``` -**First trick:** no need to **skip address and uint64**. To access a ref, it's not required to skipping preceding data. +**First trick:** no need to **skip address and uint64**. To access a ref, it is not necessary to skip preceding data. -**Second trick:** having `contentCell`, how to get `commonKey` from it? The answer: since `content` is a cell, load it… _lazily_: +**Second trick:** having `content`, how to get `commonKey` from it? The answer: since `content` is a cell, load it… _lazily_: ```tolk val storage = lazy NftCollectionStorage.load(); @@ -74,23 +87,29 @@ val content = lazy storage.content.load(); return content.commonKey; ``` -A quick reminder: `Cell` is commonly used to represent nested references. Having `p: Cell`, it's not allowed to access `p.x` — the cell (reference) needs to be loaded first, either with `Point.fromCell(p)` or, preferably, `p.load()`. Both can be used with `lazy`. +A quick reminder: having `p: Cell`, it is not allowed to access `p.x` — the cell (reference) needs to be loaded first, either with `Point.fromCell(p)` or `p.load()`. Both can be used with `lazy`. ## Lazy matching -Similarly, when reading a union type such as an incoming message, use `lazy`: +Similarly, a union type (an incoming message) can be read with `lazy`: ```tolk -struct (0x12345678) CounterReset { ... } -... -type MyMessage = CounterReset | CounterIncrement | ... - -val msg = lazy MyMessage.fromSlice(msgBody); -match (msg) { - CounterReset => { - assert(senderAddress == storage.owner) throw 403; - // <-- here "load msg.initial" is inserted - storage.counter = msg.initial; +struct (0x12345678) CounterIncrement { /* ... */ } +struct (0x23456789) CounterReset { /* ... */ } + +type MyMessage = CounterIncrement | CounterReset + +fun onInternalMessage(in: InMessage) { + val msg = lazy MyMessage.fromSlice(in.body); + match (msg) { + CounterReset => { + assert (something) throw 403; + // <-- here "load msg.initial" is inserted + storage.counter = msg.initial; + } + // ... + } +} ``` With `lazy` applied to unions: @@ -99,51 +118,37 @@ With `lazy` applied to unions: 1. `match` operates naturally by inspecting the slice prefix (opcode). 1. Within each branch, the compiler inserts loading points and skips unused fields — just like it does for structs. -This makes **lazy matching highly efficient**, outperforming patterns like `if (op == OP_RESET)` commonly used in FunC. From a type system perspective, it aligns perfectly with the TVM execution model, eliminating unnecessary stack operations. +**Lazy matching is highly efficient**, outperforming `if (op == OP_RESET)`. +It aligns perfectly with the TVM execution model, eliminating unnecessary stack operations. -## Lazy matching and else +## Lazy matching and `else` Since lazy `match` for a union is done by inspecting the prefix (opcode), unmatched cases fall through to the `else` branch. -In FunC contracts, a common pattern was to **ignore empty messages**: - -```tolk -// old style, before Tolk with `lazy` and `match` -if (msgBody.isEmpty()) { - return; // ignore empty messages -} -val op = msgBody.loadUint(32); // because this would throw excno 9 - -if (op == OP_RESET) { - ... - return; -} - -throw 0xFFFF; // "invalid opcode" -``` - -The only reason to handle empty messages upfront was to avoid throwing a _cell underflow_ error when calling `loadUint`. - -With lazy `match`, these upfront checks no longer needed. Handle all cases in `else`: - ```tolk -val msg = lazy MyMessage.fromSlice(msgBody); +val msg = lazy MyMessage.fromSlice(in.body); match (msg) { - CounterReset => { ... } - ... // handle all types of a union + CounterReset => { /* ... */ } + // ... handle all variants of the union // else - when nothing matched; - // even corrupted input (less than 32 bits), no "underflow" fired + // even input less than 32 bits, no "underflow" thrown else => { - // ignore empty messages, "wrong opcode" for others - assert (msgBody.isEmpty()) throw 0xFFFF + // for example + throw 0xFFFF } } ``` -Without an explicit `else`, unpacking throws `error 63` by default, which is controlled by the `throwIfOpcodeDoesNotMatch` option in fromCell/fromSlice. The `else` branch allows to override this behavior. +Without an explicit `else`, unpacking throws `error 63` by default, which is controlled by the `throwIfOpcodeDoesNotMatch` option in `fromSlice`. +The `else` branch allows inserting any custom logic. -Note that `else` in `match` by type is only allowed with `lazy` because it matches on prefixes. Without `lazy`, it's just a regular union, matched by a union tag (`typeid`) on a stack. + ## Partial updating @@ -152,25 +157,28 @@ The magic doesn't stop at reading. The `lazy` keyword also works seamlessly when Example: load a storage, use its fields for assertions, modify one field, and save it back: ```tolk -val storage = lazy Storage.load(); +var storage = lazy Storage.load(); assert (storage.validUntil > blockchain.now()) throw 123; assert (storage.seqno == msg.seqno) throw 456; -... +// ... storage.seqno += 1; contract.setData(storage.toCell()); // <-- magic ``` -The compiler is smart: when calling `toCell()`, it **does not save all fields of the storage** since only `seqno` was modified. Instead, during loading, after loading `seqno`, it saved an _immutable tail_ and reuses it when writing back: +The compiler is smart: `toCell()` **does not save all fields of the storage** since only `seqno` was modified. +Instead, after loading `seqno`, an _immutable tail_ was saved — and is reused when writing back: ```tolk -val storage = lazy Storage.load(); +var storage = lazy Storage.load(); // actually, what was done: // - load isSignatureAllowed, seqno // - save immutable tail // - load validUntil, etc. +// ... use all fields for reading + storage.seqno += 1; storage.toCell(); // actually, what was done: @@ -178,17 +186,55 @@ storage.toCell(); // - store immutable tail ``` -No more manual optimizations using intermediate slices — the compiler handles everything automatically. It can even group unmodified fields in the middle, load them as a slice, and preserve that slice on write-back. +The compiler can even group unmodified fields in the middle, load them as a slice, and preserve that slice on write-back. -## Q: What are the disadvantages of lazy? +## Q: How does `lazy` skip unused fields? -In terms of gas usage, `lazy fromSlice` is always equal to or cheaper than regular `fromSlice` because, in the worst case — when all fields are accessed — it loads everything one by one, just like non-lazy. +When several consecutive fields are unused, the compiler tries to group them. +It works perfectly for fixed-size types such as `intN` or `bitsN`: -However, there is another difference unrelated to gas consumption: +```tolk +struct Demo { + isAllowed: bool // always 1 bit + queryId: uint64 // always 64 bits + crc: bits32 // always 32 bits + next: RemainingBitsAndRefs +} -- `T.fromSlice(s)` unpacks all fields of `T` and then inserts `s.assertEnd()`, which can be turned off using an option. So, if the slice is corrupted or contains extra data, `fromSlice` will throw an error. +fun demo() { + val obj = lazy Demo.fromSlice(someSlice); + // <-- skip 1+64+32 = 97 bits + obj.next; +} +``` -- The `lazy` keyword, of course, selectively _picks_ only the requested fields and handles partially invalid input gracefully. For example, given: +In Fift assembler, "skip 97 bits" is generated to + +```fift +97 LDU +NIP +``` + +But **variable-width fields**, like `coins`, cannot be grouped. +And cannot be skipped in a single instruction: TVM has no "skip coins" feature. +The only possible way is to load, but ignore the result. +Similarly, for `address`: despite it's always 267 bits, it should be validated even if unused — +otherwise, binary data could be decoded wrong. + +For such types, `lazy` cannot do anything better than "**load and ignore**". +In practice, `intN` types are very common, so grouping has an evident effect. +A trick "access a ref without skipping any data" also works fine. + +## Q: What are the disadvantages of `lazy`? + +In terms of gas consumption, `lazy fromSlice` is equal to or cheaper than regular `fromSlice`. +In the worst case — when all fields are accessed — it loads everything one by one, just like the non-lazy version. + +However, there is a difference **unrelated to gas consumption**: + +- If a slice is too small or contains extra data, `fromSlice` will throw. + +- The `lazy` keyword selectively _picks_ only the requested fields and **handles partially invalid input gracefully**. For example, given: ```tolk struct Point { @@ -202,8 +248,15 @@ fun demo(s: slice) { } ``` -Since only `p.x` is accessed, an input of `FF` (8 bits) is acceptable even though `y` is missing. Similarly, `FFFF0000`, which includes 16 bits of extra data, is also fine, as `lazy` ignores any data that is not requested. +Since only `p.x` is accessed, an input of `FF` (8 bits) is acceptable even though `y` is missing. +Similarly, `FFFF0000` (16 bits of extra data) is also fine, as `lazy` ignores any data that is not requested. + +In most cases, this isn't an issue. +For incoming messages, typically all fields are used (otherwise, why include them in the struct?). +Extra data in the input is typically harmless. The message can still be deserialized correctly. -In most cases, this isn't an issue. For incoming messages, typically all fields are used (otherwise, why include them in the struct?). If there is extra data in the input — who cares? The message can still be deserialized correctly without any problem. + diff --git a/languages/tolk/features/message-handling.mdx b/languages/tolk/features/message-handling.mdx index e33cb193f..1cf73e37e 100644 --- a/languages/tolk/features/message-handling.mdx +++ b/languages/tolk/features/message-handling.mdx @@ -2,3 +2,261 @@ title: "Handling messages" --- +Each Tolk contract has specials **entrypoints** — reserved functions to handle various types of messages. +From the language perspective, handling an incoming message is just ordinary code. + +## onInternalMessage + +In 99% of cases, a contract handles **internal messages**. +An end user does not interact with a contract directly; instead, interaction occurs through the user's wallet, +which sends an internal message to the contract. + +The entrypoint is declared this way: + +```tolk +fun onInternalMessage(in: InMessage) { + // internal non-bounced messages arrive here +} +``` + +A basic guideline is the following: + +- For each incoming message, declare a `struct` with a unique 32-bit prefix (opcode). +- Declare a union type "all available messages". +- Parse this union from `in.body` and `match` it over structures. + +```tolk +struct (0x12345678) CounterIncrement { + incBy: uint32 +} + +struct (0x23456789) CounterReset { + initialValue: int64 +} + +type AllowedMessage = CounterIncrement | CounterReset + +fun onInternalMessage(in: InMessage) { + val msg = lazy AllowedMessage.fromSlice(in.body); + match (msg) { + CounterIncrement => { + // use `msg.incBy` + } + CounterReset => { + // use `msg.initialValue` + } + else => { + // invalid input; a typical reaction is: + // ignore empty messages, "wrong opcode" if not + assert (in.body.isEmpty()) throw 0xFFFF + } + } +} +``` + +### Brief explanation of the example + +- `struct` declares any business data (particularly, messages and storage). It's like a TypeScript class. See [structures](/languages/tolk/types/structures). +- `(0x12345678)` is called a "message opcode" (32 bit). Unique prefixes help routing `in.body` (binary data). +- `AllowedMessage` is a type alias for a union type, similar to TypeScript (and in some way, to Rust's enums). See [union types](/languages/tolk/types/unions). +- `in: InMessage` provides access to message properties: `in.body`, `in.senderAddress`, and so on. +- `T.fromSlice` parses binary data to `T`. See [auto-serialization](/languages/tolk/features/auto-serialization). Combined with `lazy`, it's done on demand. See [lazy loading](/languages/tolk/features/lazy-loading). +- `match` routes a union type. Inside each branch, type of `msg` is narrowed (called "smart cast"). See [pattern matching](/languages/tolk/syntax/pattern-matching). +- `throw 0xFFFF` is a standard reaction on "unrecognized message". But typically, a contract should ignore empty messages: it's "just top-up balance" (send some Toncoin, body is empty). That's why `throw` is wrapped by `if` or `assert`. See [conditions and loops](/languages/tolk/syntax/conditions-loops). + +Bounced messages do not enter `onInternalMessage`. +Read `onBouncedMessage` below. + +### How to define and modify contract's storage + +A storage is also a regular structure. It's convenient to add `load` and `store` methods accessing blockchain's persistent data: + +```tolk +struct Storage { + counterValue: int64 +} + +fun Storage.load() { + return Storage.fromCell(contract.getData()) +} + +fun Storage.save(self) { + contract.setData(self.toCell()) +} +``` + +Then, in `match` cases, invoke those methods: + +```tolk +match (msg) { + CounterIncrement => { + var storage = lazy Storage.load(); + storage.counterValue += msg.incBy; + storage.save(); + } + // ... +} +``` + +Alternatively, load the storage above `match` instead of doing it in every branch. + +For further reading, consider [contract's storage in details](/languages/tolk/features/contract-storage). + +### Old-fashioned `onInternalMessage` + +In old times, a handler was declared this way in FunC language: + +```func +() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { + ;; manually parse in_msg_full to retrieve sender_address and others +} +``` + +Tolk also allows to use old-style declarations: + +```tolk +fun onInternalMessage(myBalance: coins, msgValue: coins, msgFull: cell, msgBody: slice) { + // manually parse msgFull to retrieve senderAddress and others +} +``` + +For instance, after using [convert-func-to-tolk](https://github.com/ton-blockchain/convert-func-to-tolk), +the result is exactly as above. + +Prefer using a modern solution with `InMessage`: it's not only easier, but also cheaper in gas terms. +Transitioning after auto-conversion is trivial: + +- `myBalance` => `contract.getOriginalBalance()` (it's not a property of a message, it's a state of a contract) +- `msgValue` => `in.valueCoins` +- `msgFull` => use `in.senderAddress` etc., no need for manual parsing +- `msgBody` => `in.body` + +## onBouncedMessage + +A special entrypoint for handling **bounced messages** also exists. +When a contract sends a message to another, but the another fails to handle it, a message is bounced back to the sender. + +```tolk +fun onBouncedMessage(in: InMessageBounced) { + // messages sent with BounceMode != NoBounce arrive here +} +``` + +`InMessageBounced` is very similar to `InMessage`. +The only difference is that `in.bouncedBody` has a special shape depending on how a message was originally sent. + +### BounceMode in createMessage + +When [sending a message](/languages/tolk/features/message-sending), it's required to specify `bounce` behavior: + +```tolk +val msg1 = createMessage({ + bounce: BounceMode.NoBounce, + body: TransferMessage { ... }, + // ... +}); +msg1.send(mode); // will not be bounced on error + +val msg2 = createMessage({ + bounce: BounceMode.RichBounce, + body: TransferMessage { ... }, + // ... +}); +msg2.send(mode); // may be bounced +``` + +`BounceMode` is an enum with these options available: + +- `BounceMode.NoBounce` +- `BounceMode.Only256BitsOfBody` — `in.bouncedBody` will be "0xFFFFFFFF" + first 256 bits _(cheapest, and often sufficient)_ +- `BounceMode.RichBounce` — allows to access the entire `originalBody`; also, `gasUsed`, `exitCode`, and some other properties of a failed request are available _(most expensive)_ +- `BounceMode.RichBounceOnlyRootCell` — the same, but `originalBody` will contain only a root cell + +### How to handle `in.bouncedBody` + +Depending on `BounceMode`, `in.bouncedBody` will look differently. + +If all bounceable messages are sent with a cheap `Only256BitsOfBody`: + +```tolk +fun onBouncedMessage(in: InMessageBounced) { + // in.bouncedBody is 0xFFFFFFFF + 256 bits + in.bouncedBody.skipBouncedPrefix(); + // handle the rest, keep the 256-bit limit in mind +} +``` + +If you use `RichBounce`, that's the way: + +```tolk +fun onBouncedMessage(in: InMessageBounced) { + val rich = lazy RichBounceBody.fromSlice(in.bouncedBody); + // handle rich.originalBody + // use rich.xxx to get exitCode, gasUsed, and so on +} +``` + +Mixing different modes (sending some messages as cheap and others as rich) complicates handling and is discouraged. + +So, the binary body of an outgoing message (`TransferMessage` above) is either `in.bouncedBody` (256 bits) or `rich.originalBody` (the entire slice). +To handle this correctly, + +- create a union "all messages theoretically bounceable" +- handle it with `lazy` in `onBouncedMessage` + +```tolk +struct (0x98765432) TransferMessage { + // ... +} +// ... and other messages + +// some of them are bounceable (send not with NoBounce) +type TheoreticallyBounceable = TransferMessage // | ... + +// example for BounceMode.Only256BitsOfBody +fun onBouncedMessage(in: InMessageBounced) { + in.bouncedBody.skipBouncedPrefix(); // skips 0xFFFFFFFF + + val msg = lazy TheoreticallyBounceable.fromSlice(in.bouncedBody); + match (msg) { + TransferMessage => { + // revert changes using `msg.xxx` + } + // ... + } +} +``` + +## onExternalMessage + +Besides internal messages, a contract may handle **external messages** that arrive from off-chain. +For example, a [wallet contract](/standard/wallets/how-it-works) handles external messages, performing signature validation via a public key. + +```tolk +fun onExternalMessage(inMsg: slice) { + // external messages arrive here +} +``` + +When a contract accepts an external message, it has a very limited gas amount for execution. +Once a request is validated, remember to call `acceptExternalMessage()` to increase this limit. +Also, `commitContractDataAndActions()` might be useful. +Both are standard functions with detailed comments, an IDE provides them. + +## Other reserved entrypoints + +Besides the functions above, several predefined prototypes also exist: + +- `fun onTickTock` — triggers when tick and tock transactions occur +- `fun onSplitPrepare` and `fun onSplitInstall` — prepared for split/install transactions, currently unavailable in the blockchain +- `fun main` is often used for short snippets and demos + +This program is correct: + +```tolk +fun main() { + return 123 +} +``` + +It compiles, runs, and pushes 123 onto the stack. Its TVM _method\_id_ is 0. diff --git a/languages/tolk/features/message-sending.mdx b/languages/tolk/features/message-sending.mdx index 799102a7b..30ff21175 100644 --- a/languages/tolk/features/message-sending.mdx +++ b/languages/tolk/features/message-sending.mdx @@ -2,7 +2,9 @@ title: "Sending messages" --- -Tolk has a high-level function `createMessage`. In practice, it's immediately followed by `send`: +import { Aside } from '/snippets/aside.jsx'; + +Tolk provides a high-level function `createMessage`. In practice, it's immediately followed by `send`: ```tolk val reply = createMessage({ @@ -14,37 +16,40 @@ val reply = createMessage({ reply.send(SEND_MODE_REGULAR); ``` -## Key features of `createMessage` +It's a **universal function for composing messages** across various cases. 1. Supports extra currencies -1. Supports [`StateInit`](/foundations/messages/deploy) with automatic address computation 1. Supports workchains 1. Supports sharding (formerly `splitDepth`) -1. Integrated with auto-serialization of `body` -1. Automatically detects **body ref or not** -1. More efficient than handwritten code +1. Integrated with auto-serialization of the `body` +1. Automatically detects, body ref or not +1. Also used for deployment (passing code+data of a contract) + + ## The concept is based on union types -There is a variety of interacting between contracts. +There are many ways in which contracts interact. - sometimes, you "send to an address" -- ... but sometimes, you "build the address (builder) manually" +-     ... but sometimes, you have workchain + hash - sometimes, you compose `StateInit` from code+data -- ... but sometimes, you already have `StateInit` as a ready cell +-     ... but sometimes, `StateInit` is a ready cell - sometimes, you send a message to basechain -- ... but sometimes, you use a `MY_WORKCHAIN` constant everywhere +-     ... but sometimes, you use a `MY_WORKCHAIN` constant - sometimes, you just attach tons (msg value) -- ... but sometimes, you also need extra currencies -- etc. +-     ... but sometimes, you also need extra currencies -**How to describe such a vast variety of options? The solution is union types!** +How can such a wide variety of options be expressed? With [union types](/languages/tolk/types/unions)! -Let's start exploring this idea by looking at how extra currencies are supported. +Let's illustrate this idea by examining how extra currencies are supported. ## Extra currencies: union -In most cases, just attach msg value as tons: +In most cases, the "message value" is just tonAmount: ```tolk value: someTonAmount @@ -60,12 +65,10 @@ How does it work? Because the field `value` is a union: ```tolk // how it is declared in stdlib -type ExtraCurrenciesDict = dict - struct CreateMessageOptions { - ... - /// message value: attached tons (or tons + extra currencies) - value: coins | (coins, ExtraCurrenciesDict) + // ... + value: coins | (coins, ExtraCurrenciesMap) +} ``` That's it! Just attach tons OR tons with extra, and the compiler takes care of composing this into a cell. @@ -83,28 +86,32 @@ It's either an address, OR (workchain + hash), OR ...: ```tolk struct CreateMessageOptions { - ... - /// destination is either a provided address, or is auto-calculated by stateInit + // ... dest: address | // either just send a message to some address builder | // ... or a manually constructed builder with a valid address (int8, uint256) | // ... or to workchain + hash (also known as accountID) - AutoDeployAddress; // ... or "send to stateInit" aka deploy (address auto-calculated) + AutoDeployAddress // ... or "send to stateInit" aka deploy (address auto-calculated) +} ``` -**That's indeed the TypeScript way** — but it works at compile-time! +**That's indeed the TypeScript way** — but it works at compile-time. -## `StateInit` and workchains +## Deployment, StateInit, and workchains -Let's start from an example. A contract "jetton minter" deploys a "jetton wallet". Wallet's code and initial data are known: +Consider the following example. A contract "jetton minter" deploys a "jetton wallet". The wallet's code and initial data are known: ```tolk val walletInitialState: ContractState = { code: ..., // probably, kept in minter's storage - data: ..., // zero balance, etc. (initial wallet's storage) + data: ..., // initial wallet's storage }; ``` -A minter needs to send a message to a wallet. But since it's unknown whether the wallet already exists on-chain, a message needs wallet's code+data attached: if a wallet doesn't exist, it's immediately initialized with that code. So, where a message should be sent to? What is **destination**? The answer is: **destination is the wallet's StateInit**. In TON, the address of a contract is — by definition — a hash of its initial state: +A minter needs to send a message to a wallet. But since it's unknown whether the wallet already exists on-chain, a message needs wallet's code+data attached. So, where should the message be sent? What is the destination? The answer is: **destination is the wallet's StateInit**. + + ```tolk // address auto-calculated, code+data auto-attached @@ -117,26 +124,15 @@ To serve more complex tasks, configure additional fields: ```tolk dest: { - workchain: ..., // by default, 0 (basechain) + workchain: ..., // default: 0 (basechain) stateInit: ..., // either code+data OR a ready cell - toShard: ..., // by default, null (no sharding) -} -``` - -That's the essence of `AutoDeployAddress`. Here is how it's declared in stdlib: - -```tolk -// declared in stdlib -struct AutoDeployAddress { - workchain: int8 = BASECHAIN; - stateInit: ContractState | cell; - toShard: AddressShardingOptions? = null; + toShard: ..., // default: null (no sharding) } ``` ## Sharding: deploying "close to" another contract -The `createMessage` interface also supports initializing contracts in specific shards. Take, for example, sharded jettons — a jetton wallet should be deployed to the same shard as the owner's wallet. +The `createMessage` interface also supports initializing contracts in specific shards. For example, in sharded jettons, a jetton wallet must be deployed to the same shard as the owner's wallet. In other words, the intention is: @@ -152,7 +148,7 @@ Let's illustrate it with numbers for `shard depth` = 8: | stateInitHash | `yyyyyyyy...yyy` | calculated by code+data | | result (JW addr) | `01010101...yyy` | jetton wallet in same shard as owner | -That's how to do it: +Here is how this is done: ```tolk dest: { @@ -164,22 +160,11 @@ dest: { } ``` -Technically, **shard depth** must be a part of `StateInit` (besides code+data) — for correct initialization inside the blockchain. The compiler automatically embeds it. - -But semantically, **shard depth** alone makes no sense. That's why **shard depth + closeTo** is a single entity: - -```tolk -// how it is declared in stdlib -struct AutoDeployAddress { - ... - toShard: AddressShardingOptions? = null; -} + ## Body ref or not: compile-time calculation @@ -189,100 +174,86 @@ Fortunately, a developer shouldn't keep this in mind. Just pass `body`, and the ```tolk createMessage({ - ... - body: RequestedInfo { ... } // no `toCell`! just pass an object + // ... + body: RequestedInfo { ... } }); ``` The rules are the following: -1. if `body` is small, it's embedded directly into a message cell -1. if `body` is large or unpredictable, it's wrapped into a ref - -Why not make a ref always? Because creating cells is expensive. Avoiding cells for small bodies is crucial for gas consumption. +1. if `body` is small, it's embedded directly into a message cell (cheaper, because creating cells is expensive) +1. if `body` is large or unpredictable, it is wrapped into a ref -Interestingly, whether the body is **small** is determined **at compile time** — no runtime checks are needed. How? Thanks to generics: +Interestingly, the behavior is determined **at compile time** — no runtime checks are needed. How? Thanks to generics: ```tolk -fun createMessage(options: CreateMessageOptions): OutMessage; +fun createMessage( + options: CreateMessageOptions +): OutMessage; struct CreateMessageOptions { - ... + // ... body: TBody; } ``` -Hence, when called as `body: RequestedInfo {...}`, then `TBody = RequestedInfo`, and the compiler estimates its size: +Hence, each `createMessage()` call has its own `TBody`, and the compiler estimates its size: -- it's **small** if its maximum size is less than 500 bits and 2 refs — then **no ref** -- it's **large** if >= 500 bits or >= 2 refs — then "ref" -- it's **unpredictable** if contains `builder` or `slice` inside — then **ref** +- maximum size is less than 500 bits and 2 refs — small, "no ref" +- size is potentially >= 500 bits or >= 2 refs — large, "ref" +- contains `builder` or `slice` inside — unpredictable, "ref" **Even if body is large/unpredictable, it can be force-inlined** by wrapping into a special type: ```tolk -// potentialy 620 bits (if all coins are billions of billions) -// by default, compiler will make a ref +// maximum 620 bits (if all coins are billions of billions) +// by default, the compiler will make a ref struct ProbablyLarge { a: (coins, coins, coins, coins, coins) } -val contents: ProbablyLarge = { ... }; // but you are sure: coins are small -createMessage({ // so, you take the risks - body: UnsafeBodyNoRef { // and force "no ref" - forceInline: contents, - } - -// here TBody = UnsafeBodyNoRef +fun demo(contents: ProbablyLarge) { + // but you are sure: coins are small; + // so, you take the risks and force "no ref" + createMessage({ + body: UnsafeBodyNoRef { + forceInline: contents, + }, + // ... + }); + // btw, here TBody = UnsafeBodyNoRef +} ``` If `body` is already a cell, it will be left as a ref, without any surprise: ```tolk createMessage({ - body: someCell, // ok, just a cell, keep it as a ref - -// here TBody = cell + body: someCell, // ok, just a cell, keep it as a ref + // ... +}); ``` -That's why, don't pass `body: someObj.toCell()`, pass just `body: someObj`, let the compiler take care of everything. +Therefore, do not pass `body: obj.toCell()`, pass just `body: obj`, let the compiler take care of everything. ## Body is not restricted to structures -In practice, `createMessage` is used to send a message (sic!) to another contract — in the exact format as the receiver expects. +An interesting fact — this also works: ```tolk -struct (0xd53276db) Excesses { - queryId: uint64; -} - val excessesMsg = createMessage({ // ... - body: Excesses { - queryId: input.queryId, - } -}); -excessesMsg.send(SEND_MODE_IGNORE_ERRORS); -``` - -This works perfectly, as expected. But an interesting fact — this also works: - -```tolk -// just an example, that even this would work -val excessesMsg = createMessage({ - ... body: (0xd53276db as int32, input.queryId) }); -excessesMsg.send(SEND_MODE_IGNORE_ERRORS); +excessesMsg.send(mode); ``` -Even this is okay, it is inferred as `createMessage<(int32, uint64)>(...)` and encoded correctly. - -The example above just illustrates the power of the type system, no more. +It is inferred as `createMessage<(int32, uint64)>(...)` and encoded correctly. +This simply illustrates the flexibility of the type system. ## Body can be empty -Don't need any `body` at all? Just leave it out: +If no `body` is needed, it can be omitted entirely: ```tolk createMessage({ @@ -292,48 +263,45 @@ createMessage({ }); ``` -A curious reader might ask, "what's the type of `body` here? How is it expressed in the type system?" The answer is: `void`. + -```tolk -// declared in stdlib -struct CreateMessageOptions { - ... - body: TBody; -} -``` +## SendMode -Hence, if `body` is omitted, it leads to the default `TBody = never`. And by convention, fields having `never` type are not required in a literal. It's not that obvious, but it is definitely beautiful. +Typically, `createMessage()` is followed by `msg.send(mode)`. -## Don't confuse `StateInit` and code+data, they are different +[Read about send modes](/foundations/messages/modes#sending-modes). -It's incorrect to say that **`StateInit` is code+data**, because in TON, a full `StateInit` cell contents is richer (consider `block.tlb`): +## Low-level terminology: StateInit != code+data -- it also contains fixed\_prefix\_length (formerly split\_depth), -- it also contains ticktock info -- it also contains a library cell -- code and data are actually optional + -For instance, when sending a message to another shard, fixed\_prefix\_length is automatically set by the compiler (from `toShard.fixedPrefixLength`). +It's incorrect to say that "StateInit is code+data", because in TON, a full **StateInit** cell contents is richer (consider `block.tlb`): +it also contains fixed\_prefix\_length (automatically set by the compiler if `toShard`), ticktock info, a library cell; moreover, code and data are nullable. -That's why a structure **code+data** is named `ContractState`, NOT `StateInit`: +Therefore, the structure **code + data** is named `ContractState`, NOT `StateInit`: ```tolk // in stdlib struct ContractState { - code: cell; - data: cell; + code: cell + data: cell } ``` -And that's why a field `stateInit: ContractState | cell` is named **stateInit**, emphasizing that `StateInit` can be initialized automatically from `ContractState` (or can be a well-formed **rich** cell). +And that's why a field `stateInit: ContractState | cell` is named **stateInit**, emphasizing that `StateInit` can be initialized automatically from `ContractState` (or can be a well-formed rich cell). ## Q: Why not send, but createMessage? -In other words: why the pattern `val msg = createMessage(...); msg.send(mode)`, why not `send(... + mode)`? - -Typically, yes — a message is immediately send after composing it. But there are also advanced use cases: +Typically, yes — a message is sent immediately after being composed. However, certain scenarios require separating composition from sending: - not just send, but send and estimate fees, - or estimate fees without sending, @@ -343,11 +311,11 @@ Typically, yes — a message is immediately send after composing it. But there a So, composing a message cell and THEN doing some action with it is a more flexible pattern. -Moreover, following this pattern requires to give **a name** to a variable. Advice is not to name it "m" or "msg," but to give a descriptive name like "excessesMsg" or "transferMsg": +Moreover, following this pattern requires to give **a name** to a variable. It is advisable not to name it "m" or "msg", but to give a descriptive name like "excessesMsg" or "transferMsg": ```tolk val excessesMsg = createMessage({ - ... + // ... }); excessesMsg.send(mode); // also possible @@ -356,13 +324,13 @@ excessesMsg.sendAndEstimateFee(mode); This strategy makes the code **easier to read** later. While scanning the code, a reader sees: this is about excesses, this one is about burn notification, etc. As opposed to a potential `send(...)` function, hard to identify what _meaning_ is intended by the exact call. -## Q: Why not provide a separate deploy function? +## Q: Why not provide a dedicated deploy function? In other words: why `stateInit` is a **destination**? Why not make a `deploy()` function that accepts code+data, and drop stateInit from a regular createMessage? The answer lies in terminology. Yes, **attaching stateInit** is often referred to as **deployment**, but it's an inaccurate term. **TON Blockchain doesn't have a dedicated deployment mechanism.** A message is sent to some _void_ — and if this _void_ doesn't exist, but a way to initialize it (code+data) is provided — it's initialized immediately and accepts the message. -To emphasize the deployment, give it _a name_: +To emphasize deployment intent, give it _a name_: ```tolk val deployMsg = createMessage({ @@ -373,9 +341,9 @@ deployMsg.send(mode); ## Universal createExternalLogMessage -The philosophy is similar to `createMessage`. But **external outs** don't have **bounce**, no attached tons, etc. So, options for creating are different. +The philosophy mirrors that of `createMessage`. But **external outs** don't have bounce, attached tons, etc. So, the options for creating are different. -**Currently, external messages are used only for emitting logs (for viewing them in indexers).** But theoretically, they can be extended to **send messages to the offchain**. +**Currently, external messages are used only for emitting logs** (for viewing them in indexers). But theoretically, they can be extended to send messages to off-chain. Example: @@ -401,7 +369,7 @@ struct CreateExternalLogMessageOptions { } ``` -So, as for `createMessage` — just pass `someObject`, do NOT call `toCell()`, let the compiler decide whether it fits into the same cell or not. `UnsafeBodyNoRef` is also applicable. +Similarly, the compiler automatically decides whether `body` it fits into the same cell or needs to be a ref. `UnsafeBodyNoRef` is also applicable. **Emitting external logs, example 1**: @@ -412,7 +380,7 @@ struct DepositData { } val emitMsg = createExternalLogMessage({ - dest: ExtOutLogBucket { topic: 123 }, // 123 for indexers + dest: ExtOutLogBucket { topic: 123 }, // for indexers body: DepositData { ... } }); emitMsg.send(SEND_MODE_REGULAR); @@ -432,4 +400,6 @@ createExternalLogMessage({ }); ``` -`ExtOutLogBucket` is a variant of a custom external address for emitting logs **to the outer world.** It includes some **topic** (arbitrary number), that determines the format of the message body. In the example above, a **deposit event** is emitted (reserving topic `deposit` = 123) — and external indexers will index emitted logs by destination address without parsing body. +`ExtOutLogBucket` is a variant of a custom external address for emitting logs **to the outer world.** +It includes some **topic** (arbitrary number), that determines the format of the message body. +In the example above, a deposit event is emitted (reserving topic `deposit = 123`), and the resulting logs will be indexed by destination address without requiring body parsing. diff --git a/languages/tolk/features/standard-library.mdx b/languages/tolk/features/standard-library.mdx index 8adbff8c7..6fcae08b0 100644 --- a/languages/tolk/features/standard-library.mdx +++ b/languages/tolk/features/standard-library.mdx @@ -3,3 +3,526 @@ title: "Standard library of Tolk" sidebarTitle: "Standard library" --- +import { Aside } from '/snippets/aside.jsx'; + +Tolk provides many functions for everyday use, available out of the box. +They are called "standard library", or "stdlib". + + + +## Compile-time calculations and embeds + +Constant data can be embedded into a contract by dedicated functions. + +### address("UQ...") + +A function `address()` embeds a constant address: + +```tolk +const REFUND_ADDR = address("EQCRDM9h4k3UJdOePPuyX40mCgA4vxge5Dc5vjBR8djbEKC5") +``` + +### stringCrc32("some\_str") and other + +Several functions operate on strings. +They all expect a constant literal and are evaluated to `int` at compile-time. + +```tolk +// calculates crc32 of a string +const crc32 = stringCrc32("some_str") // = 4013618352 = 0xEF3AF4B0 + +// calculates crc16 (XMODEM) of a string +const crc16 = stringCrc16("some_str") // = 53407 = 0xD09F + +// calculates sha256 of a string and returns 256-bit integer +const hash = stringSha256("some_crypto_key") + +// calculates sha256 of a string and takes the first 32 bits +const minihash = stringSha256_32("some_crypto_key") + +// interprets an N-chars ascii string as a number in base 256 +const base256 = stringToBase256("AB") // = 16706 (65*256 + 66) +``` + +### ton("0.05") + +Calculates "nanotoncoins" at compile-time. + +```tolk +const ONE_TON = ton("1"); // `coins`, value: 1000000000 + +fun calcCost() { + val cost = ton("0.05"); // `coins`, value: 50000000 + return ONE_TON + cost; +} +``` + +## Common functions, available always + +All the functions in this section are visible everywhere. +Actually, they are placed in `@stdlib/common.tolk`, which is auto-imported. + +### Tuples + +Tuple-related functions allow interacting with [tuple types](/languages/tolk/types/tuples) +(a dynamic container up to 255 elements). + +#### createEmptyTuple + +Creates a tuple with zero elements. + +#### tuple.push, tuple.get, etc. + +An IDE suggests all methods after a dot: + +```tolk +fun demo() { + var t = createEmptyTuple(); + t.push(123); + return t.get(0); // 123 +} +``` + +#### T.toTuple and T.fromTuple + +Packs any object from a stack into a tuple (and back). +If a value occupies N stack slots, the resulting tuple has size N. + +```tolk +struct Point { + x: int + y: int +} + +fun demo() { + var p: Point = { x: 1, y: 2 }; + var t = p.toTuple(); // [ 1 2 ] + p = Point.fromTuple(t); // restored + t.get(0); // 1 +} +``` + +### Mathematical primitives + +These functions accept and return integers unless stated otherwise. +All integers are 257-bit, see [numbers](/languages/tolk/types/numbers). + +#### min(x, y) + +Computes the minimum of two integers. + +#### max(x, y) + +Computes the maximum of two integers. + +#### minMax(x, y) + +Returns `(int, int)` — a tensor `(smallest, largest)`. + +#### abs(x) + +Computes the absolute value of an integer. + +#### sign(x) + +Returns the sign of an integer value: + +- `-1` if x \< 0 +- `0` if x == 0 +- `1` if x > 0 + +#### divMod(x, y) + +Returns `(int, int)` — the quotient and remainder of `x / y`. + +Example: `divMod(112, 3)` = `(37, 1)`. + +#### modDiv(x, y) + +Returns `(int, int)` — the remainder and quotient of `x / y`. + +Example: `modDiv(112, 3)` = `(1, 37)`. + +#### mulDivFloor(x, y, z) + +Computes multiple-then-divide: `floor(x * y / z)`. The intermediate result is stored in a 513-bit integer to prevent precision loss. + +#### mulDivRound(x, y, z) + +Similar to `mulDivFloor`, but rounds the result: `round(x * y / z)`. + +#### mulDivCeil(x, y, z) + +Similar to `mulDivFloor`, but ceils the result: `ceil(x * y / z)`. + +#### mulDivMod(x, y, z) + +Returns `(int, int)` — the quotient and remainder of `(x * y / z)`. + +Example: `mulDivMod(112, 3, 10)` = `(33, 6)`. + +### Global getters and setters of current contract state + +All these functions are actually methods of the empty struct `contract`. + +#### contract.getAddress + +Returns `address` — the internal address of the current smart contract. +It can be parsed further using `address.getWorkchain` and others. + +#### contract.getOriginalBalance + +Returns `coins` — the balance (in nanotoncoins) of the smart contract at the start of Computation Phase. + +#### contract.getOriginalBalanceWithExtraCurrencies + +Returns `[coins, ExtraCurrenciesMap]` — similar to `contract.getOriginalBalance`, but also returns extra currencies. + +#### contract.getData + +Returns `cell` — the persistent contract storage cell. Typically, its result is used as `Storage.fromCell()`. +See [contract storage](/languages/tolk/features/contract-storage). + +#### contract.setData(cell) + +Sets the persistent contract storage. Typically, the argument is `storageObject.toCell()`. +See [contract storage](/languages/tolk/features/contract-storage). + +#### contract.getCode + +Returns `cell` — the code of the smart contract from a TVM register `c7`. + +#### contract.setCodePostponed(newCodeCell) + +Creates an output action that would change this smart contract code after successful termination of the current run. + +### Global getters of the blockchain (environment) state + +Most of these functions are methods of the empty struct `blockchain`. + +#### blockchain.now + +Returns `int` — current Unix timestamp (in seconds). + +#### blockchain.logicalTime + +Returns `int` — the logical time of the current transaction. + +#### blockchain.currentBlockLogicalTime + +Returns `int` — the starting logical time of the current block. + +#### blockchain.configParam(i) + +Returns `cell?` — the value of the global configuration parameter with integer index `i`, or `null` if not exists. + +#### commitContractDataAndActions + +Commits current state of TVM registers `c4` (persistent data) and `c5` (actions), +so that the current execution is considered "successful" with the saved values even if an exception +in Computation Phase is thrown later. + +### Signature checks, hashing, cryptography + +Functions and methods for hashing, verifying, and randomization. + +#### cell.hash + +Returns `uint256` — the representation hash of a cell. Useful for signing and checking signatures of arbitrary entities represented by a tree of cells. + +#### slice.hash + +Returns `uint256` — the hash of data in a slice. The same as `cell.hash` for a cell containing data and references from this slice. + +#### builder.hash + +Returns `uint256` — the hash of data in a builder. The same as "transform builder to cell + calc hash of that cell", but without cell creation. + +#### slice.bitsHash + +Returns `uint256` — sha256 of the data bits of a slice (without refs). If the bit length is not divisible by eight, throws a cell underflow exception. + +#### isSignatureValid(hash, signatureSlice, publicKey) + +Checks the Ed25519 - `signatureSlice` of a `hash` (an integer, usually computed as the hash of some data) using `publicKey` (also `uint256`). The signature must contain at least 512 data bits; only the first 512 bits are used. Returns `bool`. + +#### isSliceSignatureValid(dataSlice, signatureSlice, publicKey) + +Similar to the above, but accepts a `slice` instead of an already-calculated integer hash. If the bit length of `data` is not divisible by eight, throws a cell underflow exception. + +#### random.uint256 + +Returns `uint256` — a new pseudo-random number. + +Ensure you've called `random.initialize` to make it unpredictable! + +#### random.range(limit) + +Returns `int` — a new pseudo-random integer z in the range `0..limit−1` (or `limit..−1` if negative). +More precisely, an unsigned random value `x` is generated, then `z := x * limit / 2^256` is computed. + +Ensure you've called `random.initialize` to make it unpredictable! + +#### random.getSeed + +Returns `uint256` — the current random seed used to generate pseudo-random numbers. + +#### random.setSeed(newSeed) + +Sets the random seed to the provided value. + +#### random.initializeBy(mixSeedWith) + +Initializes (mixes) the random seed with the provided value. + +#### random.initialize + +Initializes the seed with current time to make randomization unpredictable. + +Call this function once before using `random.uint256` or `random.range`. + +### Size computation primitives + +May be useful for computing storage fees of user-provided data. + +#### cell.calculateSize(maxCells) + +Returns `(x, y, z, -1)` or `(null, null, null, 0)`. Recursively computes the count of distinct cells `x`, data bits `y`, and cell references `z` in a tree of cells. The total count of visited cells `x` cannot exceed non-negative `maxCells`; otherwise a zero flag is returned to indicate failure. + +#### slice.calculateSize(maxCells) + +Similar to `cell.calculateSize`, but accepting a slice instead of a cell. +The returned value `x` does count the cell that contains the slice itself. + +#### cell.calculateSizeStrict(maxCells) + +A non-quiet version of `cell.calculateSize` that throws a cell overflow exception on failure. + +#### slice.calculateSizeStrict(maxCells) + +A non-quiet version of `slice.calculateSize` that throws a cell overflow exception on failure. + +#### cell.depth + +Returns `int` — the depth of a cell: 0 if no references, otherwise 1 + maximum of depths of all references. + +#### slice.depth + +Returns `int` — the depth of a slice, like `cell.depth`. + +#### builder.depth + +Returns `int` — the depth of a builder, like `cell.depth`. + +#### sizeof\(anyVariable) + +Returns `int` — the number of stack slots `anyVariable` occupies (works at compile-time). + +### Debug primitives + +Only work for local TVM execution with debug level verbosity. + +#### debug.print\(anyVariable) + +Dump a variable to the debug log. + +#### debug.printString(str) + +Dump a string to the debug log. + +#### debug.dumpStack + +Dump the stack (at most the top 255 values) and show its depth. + +### Slice primitives: parsing cells + +`cell.beginParse()` converts a cell into a slice. + +`slice.loadAddress` and other `loadXXX` methods are automatically suggested after a dot. + +Prefer [automatic serialization](/languages/tolk/features/auto-serialization) to manual working with slices. + +### Builder primitives: constructing cells + +`beginCell()` creates a builder. + +`builder.storeAddress` and other `storeXXX` methods are automatically suggested after a dot. + +`builder.endCell()` composes a cell from the data written. + +Prefer [automatic serialization](/languages/tolk/features/auto-serialization) to manual working with builders. + +### Address manipulation primitives + +Methods for `address` and `any_address` (see [address types](/languages/tolk/types/address)) are suggested after a dot. + +#### address.fromWorkchainAndHash(wc, hash) + +A static method of `address` — constructs an internal address (267 bits) from arguments. + +```tolk +fun demo() { + val a = address.fromWorkchainAndHash(0, somehash); + a.getWorkchain() == BASECHAIN; // true +} +``` + +#### address.getWorkchain + +Returns `int8` — workchain from a standard (internal) address. + +#### address.getWorkchainAndHash + +Returns `(int8, uint256)` — workchain and hash from a standard (internal) address. + +#### address.calculateSameAddressInAnotherShard(options) + +Given an internal address A="aaaa...a" returns "bbaa...a" (D bits from address B, 256-D from A). + +#### createAddressNone + +Returns `any_address` — holding a slice '00' (two zero bits) at runtime. +It means "none address". + +```tolk +fun demo() { + val none = createAddressNone(); // it's `any_address` + none.isInternal(); // false + none.isNone(); // true +} +``` + + + +#### any\_address.isNone / isInternal / isExternal + +Tests which specific blockchain address is contained in "any". + +#### any\_address.castToInternal + +Casts `any_address` to `address` checking that "any" is actually "internal". +To skip the runtime check, use an [unsafe cast](/languages/tolk/types/type-checks-and-casts): `addr = myAny as address`. + +### Reserving Toncoins on contract's balance + +Stdlib contains several constants `RESERVE_MODE_XXX` for [reserve modes](/foundations/actions/reserve). +As well as a couple of functions: + +#### reserveToncoinsOnBalance + +Creates an output action which would reserve Toncoins on balance. + +#### reserveExtraCurrenciesOnBalance + +Similar to `reserveToncoinsOnBalance`, but also accepts a dictionary `extraAmount`. + +### Creating and sending messages + +Stdlib contains several constants `SEND_MODE_XXX` for [sending modes](/foundations/messages/modes). + +Composing message cells is encapsulated with a power function `createMessage`, see [message sending](/languages/tolk/features/message-sending). + +For externals (`createExternalLogMessage`), follow the same link. + +A low-level function `sendRawMessage` also exists, but its use is not recommended. + +--- + +## Modules requiring explicit imports + +Besides "always-available" functions from `common.tolk`, Tolk has separate files that must be imported in order to be used. + +```tolk +import "@stdlib/gas-payments" +``` + +Gas- and payment-related primitives. All these functions have detailed comments, see them in an IDE. + +- `getGasConsumedAtTheMoment` +- `acceptExternalMessage` +- `setGasLimitToMaximum` +- `setGasLimit` +- `calculateGasFee` +- `calculateGasFeeWithoutFlatPrice` +- `calculateStorageFee` +- `calculateForwardFee` +- `calculateForwardFeeWithoutLumpPrice` +- `contract.getStorageDuePayment` +- `contract.getStoragePaidPayment` + +```tolk +import "@stdlib/exotic-cells" +``` + +Functions for loading and parsing [exotic cells](/foundations/serialization/cells) and particularly library references. + +```tolk +import "@stdlib/lisp-lists" +``` + +Lisp-style lists are nested two-element tuples: `[1, [2, [3, null]]]` represents the list `[1, 2, 3]`. +They are used rarely nowadays. + +```tolk +import "@stdlib/tvm-dicts" +``` + +Low-level API for working with TVM dictionaries. Not recommended to use — enjoy [built-in maps](/languages/tolk/types/maps) instead. + +```tolk +import "@stdlib/tvm-lowlevel" +``` + +Functions for working with TVM registers and the stack. + +## Q: Why are some functions `builtin`? + +Many functions in `common.tolk` don't provide `asm` instructions: + +```tolk +@pure +fun slice.loadInt(mutate self, len: int): int + builtin +``` + +This is done for optimization — the compiler generates different assembler code depending on arguments. +For example, if `len` is constant (say, 32), `loadUint` is translated to `32 LDU`, but if it's a variable, +it's taken from a stack, and the `LDUX` instruction is emitted. + +Another example — `builder.storeUint(1, 32).storeUint(2, 64)` is not translated to `32 STU` and `64 STU`. +Instead, these calls are merged into a single `STSLICECONST`. + +See [compiler optimizations](/languages/tolk/features/compiler-optimizations) for details. + +## Appendix: How the embedded stdlib works + +The stdlib is bundled with the compiler — it's a folder `tolk-stdlib/`, automatically discovered. +An import `@stdlib/xxx` actually loads `{STLIB_PATH}/xxx.tolk`. + +Here is how the folder is discovered: + +1. The compiler searches in predefined and relative paths. + + For example, when launched from a system-installed package, e.g., `/usr/bin/tolk`, the stdlib is located in `/usr/share/ton/smartcont`. + + For custom installations, the `TOLK_STDLIB` environment variable can be set. This is standard practice for compilers. + +1. The WASM wrapper [tolk-js](https://github.com/ton-blockchain/tolk-js) also includes the stdlib, so it's already installed when using blueprint. + +1. IDE plugins automatically locate the stdlib to provide auto-completion. + + Blueprint installs tolk-js, creating the `node_modules/@ton/tolk-js/` folder in the project structure. + + It contains `common.tolk`, `tvm-dicts.tolk`, and other files. From 50b60c06d817b825f3069ad21b996bc65d90b613 Mon Sep 17 00:00:00 2001 From: tolk-vm Date: Fri, 21 Nov 2025 20:00:29 +0300 Subject: [PATCH 06/11] [Tolk] New docs: migration from FunC --- docs.json | 66 +- foundations/addresses/derive.mdx | 2 +- languages/tolk/from-func/converter.mdx | 64 + languages/tolk/from-func/in-detail.mdx | 2210 --------------------- languages/tolk/from-func/in-short.mdx | 111 -- languages/tolk/from-func/mutability.mdx | 265 --- languages/tolk/from-func/stdlib-fc.mdx | 189 ++ languages/tolk/from-func/stdlib.mdx | 298 --- languages/tolk/from-func/tolk-vs-func.mdx | 457 +++++ languages/tolk/from-func/tolk-vs-tlb.mdx | 79 + languages/tolk/overview.mdx | 8 +- 11 files changed, 852 insertions(+), 2897 deletions(-) create mode 100644 languages/tolk/from-func/converter.mdx delete mode 100644 languages/tolk/from-func/in-detail.mdx delete mode 100644 languages/tolk/from-func/in-short.mdx delete mode 100644 languages/tolk/from-func/mutability.mdx create mode 100644 languages/tolk/from-func/stdlib-fc.mdx delete mode 100644 languages/tolk/from-func/stdlib.mdx create mode 100644 languages/tolk/from-func/tolk-vs-func.mdx create mode 100644 languages/tolk/from-func/tolk-vs-tlb.mdx diff --git a/docs.json b/docs.json index cdca94cd6..a477ec9c1 100644 --- a/docs.json +++ b/docs.json @@ -453,10 +453,10 @@ { "group": "Migrating from FunC", "pages": [ - "languages/tolk/from-func/in-short", - "languages/tolk/from-func/in-detail", - "languages/tolk/from-func/mutability", - "languages/tolk/from-func/stdlib" + "languages/tolk/from-func/tolk-vs-func", + "languages/tolk/from-func/tolk-vs-tlb", + "languages/tolk/from-func/stdlib-fc", + "languages/tolk/from-func/converter" ] }, "languages/tolk/changelog" @@ -980,22 +980,22 @@ }, { "source": "/v3/documentation/smart-contracts/tolk/tolk-vs-func/in-short", - "destination": "/languages/tolk/from-func/in-short", + "destination": "/languages/tolk/from-func/tolk-vs-func", "permanent": true }, { "source": "/v3/documentation/smart-contracts/tolk/tolk-vs-func/in-detail", - "destination": "/languages/tolk/from-func/in-detail", + "destination": "/languages/tolk/from-func/tolk-vs-func", "permanent": true }, { "source": "/v3/documentation/smart-contracts/tolk/tolk-vs-func/mutability", - "destination": "/languages/tolk/from-func/mutability", + "destination": "/languages/tolk/syntax/mutability", "permanent": true }, { "source": "/v3/documentation/smart-contracts/tolk/tolk-vs-func/stdlib", - "destination": "/languages/tolk/from-func/stdlib", + "destination": "/languages/tolk/from-func/stdlib-fc", "permanent": true }, { @@ -3242,6 +3242,56 @@ "source": "/participate/run-nodes/full-node", "destination": "/v3/guidelines/nodes/running-nodes/full-node", "permanent": true + }, + { + "source": "/languages/tolk/from-func/in-short", + "destination": "/languages/tolk/from-func/tolk-vs-func", + "permanent": true + }, + { + "source": "/languages/tolk/from-func/in-detail", + "destination": "/languages/tolk/from-func/tolk-vs-func", + "permanent": true + }, + { + "source": "/languages/tolk/from-func/mutability", + "destination": "/languages/tolk/syntax/mutability", + "permanent": true + }, + { + "source": "/languages/tolk/from-func/stdlib", + "destination": "/languages/tolk/from-func/stdlib-fc", + "permanent": true + }, + { + "source": "/languages/tolk/from-func/create-message", + "destination": "/languages/tolk/features/message-sending", + "permanent": true + }, + { + "source": "/languages/tolk/from-func/lazy-loading", + "destination": "/languages/tolk/features/lazy-loading", + "permanent": true + }, + { + "source": "/languages/tolk/from-func/pack", + "destination": "/languages/tolk/features/auto-serialization", + "permanent": true + }, + { + "source": "/languages/tolk/environment-setup", + "destination": "/languages/tolk/overview", + "permanent": true + }, + { + "source": "/languages/tolk/counter-smart-contract", + "destination": "/languages/tolk/overview", + "permanent": true + }, + { + "source": "/languages/tolk/language-guide", + "destination": "/languages/tolk/overview", + "permanent": true } ] } diff --git a/foundations/addresses/derive.mdx b/foundations/addresses/derive.mdx index 68b4040e9..cb08bd52f 100644 --- a/foundations/addresses/derive.mdx +++ b/foundations/addresses/derive.mdx @@ -57,7 +57,7 @@ fun main() { The `b5ee9c724101010100020000004cacb9cd` in the `getCode` function is a placeholder for a hardcoded code cell in [BoC](/foundations/serialization/boc) format known at compile-time. The `getData` function is a placeholder for building a data cell, and the actual implementation depends on the storage layout of the target smart contract. Usually, data is composed in a parametrized way, but this does not alter the rest of the logic — only the `getData` function. -The `calculateAddress` function uses the [`AutoDeployAddress`](/languages/tolk/from-func/create-message#stateinit-and-workchains) built-in that handles all the underlying `StateInit` and address composing logic. In the [Tolk stdlib](/languages/tolk/from-func/stdlib), the code and data pair is represented by [`ContractState`](/languages/tolk/from-func/create-message#dont-confuse-stateinit-and-codedata-they-are-different). The `buildAddress` method returns a builder containing the resulting address, which can be cheaply stored in another builder — the most common use case. +The `calculateAddress` function uses the [`AutoDeployAddress`](/languages/tolk/features/message-sending) built-in that handles all the underlying `StateInit` and address composing logic. In the Tolk stdlib, the code and data pair is represented by `ContractState`. The `buildAddress` method returns a builder containing the resulting address, which can be cheaply stored in another builder — the most common use case. ### On-chain — contract sharding diff --git a/languages/tolk/from-func/converter.mdx b/languages/tolk/from-func/converter.mdx new file mode 100644 index 000000000..141b090d6 --- /dev/null +++ b/languages/tolk/from-func/converter.mdx @@ -0,0 +1,64 @@ +--- +title: "FunC-to-Tolk converter" +sidebarTitle: "Converter" +--- + +import { Aside } from '/snippets/aside.jsx'; + +**The converter** may look like magic: any FunC project can be immediately transformed into Tolk with a single command: + +```bash +npx @ton/convert-func-to-tolk contracts +``` + +[GitHub repo](https://github.com/ton-blockchain/convert-func-to-tolk). + +## Key idea: transform and gradually modernize + +1. Take a FunC project and apply the converter
+ \=> get a contract that "looks and feels" like FunC. + +1. Refactor it with modern features, step by step, keeping green tests
+ \=> resulting in an idiomatic Tolk project for further development. + +## Step 1: convert and make it compile + +This is a **syntax-level converter** that assists in migrating contracts to Tolk. +It rewrites FunC code with 1:1 semantics, — emitting a Tolk version of a contract that remains very close to the FunC original. + +Example input: [jetton-minter.fc](https://github.com/ton-blockchain/convert-func-to-tolk/blob/master/tests/inout/jetton-minter.fc).
+Example output: [jetton-minter.tolk](https://github.com/ton-blockchain/convert-func-to-tolk/blob/master/tests/inout/jetton-minter.fc.tolk). + +The converted contract won't use modern Tolk features like structures, auto-serialization, or clean message composition. +But after some manual fixes, it compiles, runs, and passes tests. + + + +**From there, the codebase can be gradually modernized** — step by step, while keeping it functional at every stage. + +## Step 2: gradually refactor + +- use the modern `onInternalMessage`, see [handling messages](/languages/tolk/features/message-handling) +- extract a `Storage` struct, with toCell/fromCell like in [examples](/languages/tolk/features/contract-storage) +- refactor incoming messages into [structs](/languages/tolk/syntax/structures-fields#serialization-prefixes-and-opcodes) with 32-bit opcodes — incrementally, one message at a time +- define a union of possible messages and [lazy](/languages/tolk/features/lazy-loading) match it +- extract outgoing messages into structs and [send](/languages/tolk/features/message-sending) them + + + +## Where to find examples? + +Examples: [Tolk vs FunC benchmarks](https://github.com/ton-blockchain/tolk-bench). +This repository contains several contracts migrated from FunC — preserving original logic and passing the same tests. + +Explore the Git history to see how each contract was gradually rewritten, step by step. diff --git a/languages/tolk/from-func/in-detail.mdx b/languages/tolk/from-func/in-detail.mdx deleted file mode 100644 index 83bde757c..000000000 --- a/languages/tolk/from-func/in-detail.mdx +++ /dev/null @@ -1,2210 +0,0 @@ ---- -title: "Tolk vs FunC: in detail" -sidebarTitle: "In detail" ---- - -import { Aside } from '/snippets/aside.jsx'; - - - -### Comment syntax - -| FunC | Tolk | -| :-----------------------: | :-----------------------: | -| `;; comment` | `// comment` | -| `{- multiline comment -}` | `/* multiline comment */` | - -### Identifiers - -- An identifier starts with {'[a-zA-Z$]'} and continue with {'[a-zA-Z0-9$]'}. - -- Characters such as `?` or `:` are invalid, so names like `found?` or `op::increase` are not allowed. - -- Identifiers such as `cell` or `slice` are valid. - -Example: - -```tolk -var cell = ... -var cell: cell = ... -``` - -It is similar to how `number` is a valid identifier in TypeScript. - -**FunC vs Tolk** - -In FunC, almost any character can be part of an identifier. -For example, `2+2` without spaces is treated as a single identifier, and a variable can be declared with such a name. - -In Tolk, spaces are not required. `2+2` evaluates to `4`, and `3+~x` is interpreted as `3 + (~x)`, and so on. - -| FunC | Tolk | -| :--------------------------------------: | :-----------------: | -| `return 2+2; ;; undefined function 2+2` | `return 2+2; // 4` | - -Backticks can be used to enclose an identifier, allowing any symbols to be included. -This feature is intended primarily for code generation, where keywords may need to appear as identifiers. - -| FunC | Tolk | -| :------------------------------------: | :--------------------------------------: | -| `const op::increase = 0x1234;` | `const OP_INCREASE = 0x1234` | - -### Impure by default, no function call elimination - -FunC has an `impure` function specifier. When absent, a function is treated as pure. If its result is unused, its call is deleted by the compiler. - -For example, functions that do not return a value, such as those that throw an exception on a mismatch, are removed. This issue is spoiled by FunC not validating the function body, allowing impure operations to be executed within pure functions. - -In Tolk, all functions are impure by default. A function can be marked as pure using an annotation. In pure functions, impure operations such as throwing exceptions, modifying globals, or calling non-pure functions are disallowed. - -### Function syntax updates - -- `fun` keyword - -| FunC | Tolk | -| :------------------------------: | :-----------------------------------: | -| `cell parse_data(slice cs) { }` | `fun parse_data(cs: slice): cell { }` | -| `(cell, int) load_storage() { }` | `fun load_storage(): (cell, int) { }` | -| `() main() { ... }` | `fun main() { ... }` | - -- Types of variables — on the right: - -| FunC | Tolk | -| :---------------------------------: | :---------------------------------------: | -| `slice cs = ...;` | `var cs: slice = ...;` | -| `(cell c, int n) = parse_data(cs);` | `var (c: cell, n: int) = parse_data(cs);` | -| `global int stake_at;` | `global stake_at: int` | - -- Modifiers such as `inline` — with `@` annotations: - -| FunC | Tolk | -| :----------------------------------: | :-----------------------------: | -| `int f(cell s) inline {` | `@inline fun f(s: cell): int {` | -| `() load_data() impure inline_ref {` | `@inline_ref fun load_data() {` | -| `global int stake_at;` | `global stake_at: int` | - -- `forall` — this way: - -| FunC | Tolk | -| :------------------------------------------: | :----------------------------------------: | -| `forall X -> tuple cons(X head, tuple tail)` | `fun cons(head: X, tail: tuple): tuple` | - -- `asm` implementation — same as in FunC, but properly aligned: - -```tolk -@pure -fun third(t: tuple): X - asm "THIRD" - -@pure -fun builder.storeSlice(mutate self, s: slice): self - asm(s self) "STSLICE" - -@pure -fun mulDivFloor(x: int, y: int, z: int): int - builtin -``` - -- There is also a `@deprecated` attribute, not affecting compilation but for developers and IDE. - -### get instead of method\_id - -In FunC, `method_id` without arguments declares a get method. -In Tolk, a direct get syntax is used: - -| FunC | Tolk | -| :-----------------------------: | :----------------------------: | -| `int seqno() method_id { ... }` | `get fun seqno(): int { ... }` | - -For `method_id(xxx)` — uncommon in practice but valid — Tolk uses an annotation: - -| FunC | Tolk | -| :-----------------------------------------------------------: | :------------------------------------------------------------: | -| `() after_code_upgrade(cont old_code) impure method_id(1666)` | `@method_id(1666) fun afterCodeUpgrade(oldCode: continuation)` | - -### Parameter types are required, local types are optional - -```tolk -// not allowed -fun do_smth(c, n) -// types are mandatory -fun do_smth(c: cell, n: int) -``` - -Parameter types are mandatory, but the return type is optional when it can be inferred. -If omitted, it's auto-inferred: - -```tolk -fun x() { ... } // auto infer from return statements -``` - -Local variable types are optional: - -```tolk -var i = 10; // ok, int -var b = beginCell(); // ok, builder -var (i, b) = (10, beginCell()); // ok, two variables, int and builder - -// types can be specified manually, of course: -var b: builder = beginCell(); -var (i: int, b: builder) = (10, beginCell()); -``` - -Default values for parameters are supported: - -```tolk -fun increment(x: int, by: int = 1) { - return x + by -} -``` - -### Variables cannot be redeclared in the same scope - -```tolk -var a = 10; -... -var a = 20; // error, correct is `a = 20` -if (1) { - var a = 30; // ok, it's another scope -} -``` - -As a consequence, partial reassignment is not allowed: - -```tolk -var a = 10; -... -var (a, b) = (20, 30); // error, releclaration of a -``` - -This is not an issue for methods like `loadUint()`. - -In FunC, such methods returned a modified object, so a pattern like -`var (cs, int value) = cs.load_int(32)` is common. - -In Tolk, such methods mutate the object: `var value = cs.loadInt(32)`, so redeclaration is rarely needed: - -```tolk -fun send(msg: cell) { - var msg = ...; // error, redeclaration of msg - - // solution 1: intruduce a new variable - var msgWrapped = ...; - // solution 2: use `redef`, though not recommended - var msg redef = ...; -``` - -### String postfixes removed, compile-time functions added - -Tolk removes FunC-style string postfixes like `"..."c` and replaces them with compile-time functions. - -| FunC | Tolk | -| :------: | :-----------------------: | -| `"..."c` | `stringCrc32("...")` | -| `—` | `stringCrc16("...")` | -| `"..."H` | `stringSha256("...")` | -| `"..."h` | `stringSha256_32("...")` | -| `"..."a` | `address("...")` | -| `"..."s` | `stringHexToSlice("...")` | -| `"..."u` | `stringToBase256("...")` | - -These functions are: - -- compile-time only -- for constant strings only -- usable in constant initialization - -```tolk -// type will be `address` -const BASIC_ADDR = address("EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF") - -// return type will be `int` -fun minihashDemo() { - return stringSha256_32("transfer(slice, int)"); -} -``` - -The naming highlights that these functions arrived from string postfixes and operate on string values. -At runtime, there are no strings, only slices. - -### Trailing comma support - -Tolk supports trailing commas in the following contexts: - -- tensors -- tuples -- function calls -- function parameters - -```tolk -var items = ( - totalSupply, - verifiedCode, - validatorsList, -); -``` - -Note that `(5)` is not a tensor. It's the integer `5` in parentheses. -With a trailing comma `(5,)` it's still `(5)`. - -### Optional semicolon for the last statement in a block - -In Tolk, the semicolon after the final statement in a block can be omitted. -While semicolons are still required between statements, the trailing semicolon on the last statement is now optional. - -```tolk -fun f(...) { - doSomething(); - return result // <-- valid without semicolon -} - -// or -if (smth) { - return 1 -} else { - return 2 -} -``` - -### ton("...") function for readable Toncoin amounts - -| FunC | Tolk | -| :---------------------------: | :------------------------: | -| `int cost = 50000000;` | `val cost = ton("0.05");` | -| `const ONE_TON = 1000000000;` | `const ONE_TON = ton("1")` | - -The function `ton()` only accepts constant values. For example, `ton(some_var)` is invalid. -Its type is `coins`, not `int`, although it's treated as a regular `int` by the TVM. -Arithmetic operations on `coins` degrade to `int` — for example, `cost << 1` or `cost + ton("0.02")` are both valid. - -### Type system changes - -In Tolk v0.7, the type system was rewritten from scratch. -To introduce booleans, fixed-width integers, nullability, structures, and generics, Tolk required a static type system similar to TypeScript or Rust. - -The types are: - -- `int`, `bool`, `cell`, `slice`, `builder`, untyped `tuple` -- typed tuple `[T1, T2, ...]` -- tensor `(T1, T2, ...)` -- callables `(TArgs) -> TResult` -- nullable types `T?`, compile-time null safety -- union types `T1 | T2 | ...`, handled with pattern matching -- `coins` and function `ton("0.05")` -- `int32`, `uint64`, and other fixed-width integers — int at TVM — [details](https://github.com/ton-blockchain/ton/pull/1559) -- `bytesN` and `bitsN` — similar to `intN` — backed by slices at TVM -- `address` — internal (standard) address, still a slice at TVM -- `any_address` — internal/external/none -- `void` — more canonical to be named `unit`, but `void` is more reliable -- `self`, to make chainable methods, described below; it's not a type, it can only occur instead of return type of a function -- `never` — an always-throwing function returns `never`, for example; an _impossible type_ is also `never` -- structures and generics - -The type system obeys the following rules: - -- Variable types can be specified manually or are inferred from declarations, and never change after being declared. -- Function parameters must be strictly typed. -- Function return types, if unspecified, are inferred from return statements similar to TypeScript. In the case of recursion, direct or indirect, the return type must be explicitly declared. -- Generic functions are supported. - -### Clear and readable type mismatch errors - -In FunC, type mismatch errors are hard to interpret: - -``` -error: previous function return type (int, int) -cannot be unified with implicit end-of-block return type (int, ()): -cannot unify type () with int -``` - -In Tolk, errors are human-readable: - -``` -1) can not assign `(int, slice)` to variable of type `(int, int)` -2) can not call method for `builder` with object of type `int` -3) can not use `builder` as a boolean condition -4) missing `return` -... -``` - -### bool type, casting boolVar as int - -At the TVM level, `bool` is represented as -1 or 0, but in the type system, `bool` and `int` are distinct types. - -- Comparison operators `== / >= /...` return `bool`. -- Logical operators `&& ||` return `bool`. -- Constants `true` and `false` have the `bool` type. - Many standard library functions now return `bool`, not `int`: - -```tolk -var valid = isSignatureValid(...); // bool -var end = cs.isEnd(); // bool -``` - -- Operator `!x` supports both `int` and `bool`. -- `if` conditions and similar statements accept both `int` values that are not equal to zero and `bool`. -- Logical operators `&&` and `||` accept both `bool` and `int`, preserving compatibility with constructs like `a && b` where `a` and `b` are nonzero integers. -- Arithmetic operators are restricted to integers. Only bitwise and logical operations are allowed for `bool`. - -```tolk -valid && end; // ok -valid & end; // ok, bitwise & | ^ also work if both are bools -if (!end) // ok - -if (~end) // error, use !end -valid + end; // error -8 & valid; // error, int & bool not allowed -``` - -Logical operators `&&` and `||`, which are absent in FunC, use the `if/else` `asm` representation. -In the future, for optimization, they could be automatically replaced with `&` or `|` when safe to do so, for example, `a > 0 && a < 10`. - -To manually optimize gas consumption, `&` and `|` can be used for `bool`, but they are not short-circuited. - -- `bool` can be cast to `int` using `as` operator: - -```tolk -var i = boolValue as int; // -1 / 0 -``` - -There are no runtime transformations. `bool` is guaranteed to be -1 or 0 at the TVM level, so this is a type-only cast. -Such casts are rarely necessary, except for tricky bitwise optimizations. - -### Generic functions and instantiations like f\(...) - -Tolk introduces properly made generic functions. The syntax reminds mainstream languages: - -```tolk -fun replaceNulls(tensor: (T1?, T2?), v1IfNull: T1, v2IfNull: T2): (T1, T2) { - var (a, b) = tensor; - return (a == null ? v1IfNull : a, b == null ? v2IfNull : b); -} -``` - -A generic parameter `T` may represent any type, including complex ones: - -```tolk -fun duplicate(value: T): (T, T) { - var copy: T = value; - return (value, copy); -} - -duplicate(1); // duplicate -duplicate([1, cs]); // duplicate<[int, slice]> -duplicate((1, 2)); // duplicate<(int, int)> -``` - -Function types are also supported: - -```tolk -fun callAnyFn(f: TObj -> TResult, arg: TObj) { - return f(arg); -} - -fun callAnyFn2(f: TCallback, arg: TObj) { - return f(arg); -} -``` - -Although the generic type `T` is usually inferred from the arguments, there are edge cases where `T` cannot be inferred because it does not depend on them. - -```tolk -fun tupleLast(t: tuple): T - asm "LAST" - -var last = tupleLast(t); // error, can not deduce T -``` - -To make this valid, `T` must be specified externally: - -```tolk -var last: int = tupleLast(t); // ok, T=int -var last = tupleLast(t); // ok, T=int -var last = tupleLast(t) as int; // ok, T=int - -someF(tupleLast(t)); // ok, T=(paremeter's declared type) -return tupleLast(t); // ok if function specifies return type -``` - -- For asm functions, `T` must occupy exactly one stack slot. -- For user-defined functions, `T` may represent any structure. -- Otherwise, the `asm` body cannot handle it properly. - -### Anonymous functions (lambdas) - -Use lambdas — function expressions without capturing outer variables. Pass as callbacks, assign to variables, or return them. - -```tolk -fun callAndAssertTrue(callback: () -> bool) { - val result = callback(); - assert (result == true) throw 123; -} - -callAndAssertTrue(fun() { // lambda - // ... - return SOME_BOOL; -}); -``` - -Regular functions require explicit parameter types; lambda parameter types may be omitted when they can be inferred: - -```tolk -fun Result.pushMath(mutate self, loadFn: (int, slice) -> int) { - val ans = mathFn(32, "..."); - self.output.push(ans); -} - -r.pushMath(fun(bits, s) { // bits is `int`, s is `slice` - return s.loadUint(bits) -}); - -// but it's an error: param's type cannot be inferred here: -val doubleFn = fun(param) { return param * 2 }; -// correct is: -val doubleFn = fun(param: int) { return param * 2 }; -``` - -From the type system point of view, a function (and a lambda) has a special type `(...ArgsT) -> ReturnT`. - -```tolk -// a regular function -fun abs(a: int): int { ... } - -val cb1 = abs; // (int) -> int -val cb2 = fun(a: coins): MyData { ... }; // (coins) -> MyData -val cb3 = fun(a: slice) {}; // (slice) -> void -``` - -As first-class functions, lambdas can even be returned: - -```tolk -fun createFinalizer() { - return fun(b: builder) { - b.storeUint(0xFFFFFFFF, 32); - return b.toSlice(); - } -} - -val f = createFinalizer(); // (builder) -> slice -f(beginCell()); // slice with 32 bits -``` - -While lambdas are not common in smart contracts, they become useful in general purpose tools. -They can easily be combined with generics of any level, nested into each other, and so on. - -Note that lambdas are not closures: capturing outer variables not supported. - -```tolk -fun outer(x: int) { - return fun(y: int) { - return x + y; // error: undefined symbol `x` - } -} -``` - -Capturing variables is nearly impossible to implement on a stack machine, just like inheritance (conceptually equivalent). - -### #include → import - -| FunC | Tolk | -| :----------------------: | :----------------: | -| `#include "another.fc";` | `import "another"` | - -In Tolk, symbols from another file cannot be used without explicitly importing it — **import what is used**. - -All standard library functions are available by default. Downloading the stdlib and including it manually `#include "stdlib.fc"` is unnecessary. See [embedded stdlib](/languages/tolk/from-func/in-detail#stdlib-is-now-embedded%2C-not-downloaded-from-github). - -There is a global naming scope. If the same symbol is declared in multiple files, it results in an error. -`import` brings all file-level symbols into scope. The `export` keyword is reserved for future use. - -### #pragma → compiler options - -In FunC, **experimental** features such as `allow-post-modifications` were enabled with `#pragma` directives inside `.fc` files, which caused inconsistencies across files. These flags are compiler options, not file-level pragmas. - -In Tolk, all pragmas were removed. `allow-post-modification` and `compute-asm-ltr` are merged into Tolk sources and behave as if they were always enabled in FunC. Instead of pragmas, experimental behavior is set through compiler options. - -There is one experimental option: `remove-unused-functions`, which excludes unused symbols from the Fift output. - -`#pragma version xxx` is replaced with `tolk xxx`, no `>=`, only strict versioning. If the version does not match, Tolk shows a warning. - -```tolk -tolk 0.12 -``` - -### Late symbols resolution and AST representation - -In FunC, as in C, a function cannot be accessed before its declaration: - -```func -int b() { a(); } ;; error -int a() { ... } ;; since it's declared below -``` - -To avoid an error, a forward declaration is required because symbol resolution occurs during the parsing process. - -Tolk compiler separates parsing and symbol resolution into two distinct steps. The code above is valid, since _symbols are resolved after parsing_. - -This required introducing an intermediate AST representation, which is absent in FunC. The AST enables future language extensions and semantic code analysis. - -### null keyword - -Creating null values and checking variables for null is now straightforward. - -| FunC | Tolk | -| :--------------------: | :--------------: | -| `a = null()` | `a = null` | -| `if (null?(a))` | `if (a == null)` | -| `if (~ null?(b))` | `if (b != null)` | -| `if (~ cell_null?(c))` | `if (c != null)` | - -### throw and assert keywords - -Tolk simplifies exception handling. - -While FunC provides `throw()`, `throw_if()`, `throw_arg_if()`, and the corresponding unless forms, Tolk offers two primitives: `throw` and `assert`: - -| FunC | Tolk | -| :------------------------------: | :-------------------------: | -| `throw(excNo)` | `throw excNo` | -| `throw_arg(arg, excNo)` | `throw (excNo, arg)` | -| `throw_unless(excNo, condition)` | `assert(condition, excNo)` | -| `throw_if(excNo, condition)` | `assert(!condition, excNo)` | - -The `!condition` is valid, as logical NOT is supported. - -A verbose form `assert(condition, excNo)` is also available: - -```tolk -assert(condition) throw excNo; -// with a possibility to include arg to throw -``` - -Tolk swaps `catch` arguments: `catch (excNo, arg)`, both of which are optional since arg is usually empty. - -| FunC | Tolk | -| :------------------------------: | :-----------------------------: | -| `try { } catch (_, _) { }` | `try { } catch { }` | -| `try { } catch (_, excNo) { }` | `try { } catch(excNo) { }` | -| `try { } catch (arg, excNo) { }` | `try { } catch(excNo, arg) { }` | - -### do ... until → do ... while - -| FunC | Tolk | -| :-------------------------------: | :------------------------------: | -| `do { ... } until (~ condition);` | `do { ... } while (condition);` | -| `do { ... } until (condition);` | `do { ... } while (!condition);` | - -The `!condition` is valid, as logical NOT is supported. - -### Operator precedence aligned with C++ and JavaScript - -In FunC, the code `if (slices_equal() & status == 1)` is parsed as `if ((slices_equal() & status) == 1)`. This causes errors in real-world contracts. - -In Tolk, `&` has a lower priority, identical to C++ and JavaScript. - -Tolk generates errors on potentially incorrect operator usage to prevent such mistakes: - -```tolk -if (flags & 0xFF != 0) -``` - -Produces a compilation error: - -``` -& has lower precedence than ==, probably this code won't work as you expected. Use parenthesis: either (... & ...) to evaluate it first, or (... == ...) to suppress this error. -``` - -Code should be rewritten as: - -```tolk -// Evaluate it first (this case) -if ((flags & 0xFF) != 0) -// Or emphasize the behavior (not used here) -if (flags & (0xFF != 0)) -``` - -Tolk detects a common mistake in bitshift operators: `a << 8 + 1` is equivalent to `a << 9`, which may be unexpected. - -``` -int result = a << 8 + low_mask; - -error: << has lower precedence than +, probably this code won't work as you expected. Use parenthesis: either (... << ...) to evaluate it first, or (... + ...) to suppress this error. -``` - -Operators `~% ^% /% ~/= ^/= ~%= ^%= ~>>= ^>>=` are no longer supported. - -### Immutable variables declared with val - -Like in Kotlin, `var` declares mutable variables and `val` declares immutable variables, optionally specifying a type. FunC has no equivalent of `val`. - -```tolk -val flags = msgBody.loadMessageFlags(); -flags &= 1; // error, modifying an immutable variable - -val cs: slice = c.beginParse(); -cs.loadInt(32); // error, since loadInt() mutates an object -cs.preloadInt(32); // ok, it's a read-only method -``` - -Function parameters are mutable within the function, but arguments are passed by value and remain unchanged. This behavior matches FunC. - -```tolk -fun some(x: int) { - x += 1; -} - -val origX = 0; -some(origX); // origX remains 0 - -fun processOpIncrease(msgBody: slice) { - val flags = msgBody.loadInt(32); - ... -} - -processOpIncrease(msgBody); // by value, not modified -``` - -In Tolk, functions can declare `mutate` parameters. It's a generalization of FunC `~` tilde functions. - -### Deprecated command-line options removed - -Command-line flags such as `-A` and `-P` are removed. The default usage: - -``` -/path/to/tolk -``` - -- Use `-v` to print the version and exit. -- Use `-h` to list all available flags. - -Only one input file can be specified. Additional files must be **imported**. - -### stdlib functions renamed to clear names, camelCase style - -All standard library functions now use longer, descriptive names in camelCase style. - -| FunC | Tolk | -| :--------------------------: | :-------------------------------: | -| `cur_lt()` | `blockchain.logicalTime()` | -| `car(l)` | `listGetHead(l)` | -| `get_balance().pair_first()` | `contract.getOriginalBalance()` | -| `raw_reserve(count)` | `reserveToncoinsOnBalance(count)` | -| `dict~idict_add?(...)` | `dict.addIfNotExists(...)` | -| `t~tpush(triple(x, y, z))` | `t.push([x, y, z])` | -| `s.slice_bits()` | `s.remainingBitsCount()` | -| `~dump(x)` | `debug.print(x)` | - -The former `stdlib.fc` was split into multiple files, including `common.tolk` and `tvm-dicts.tolk`. - -See the full comparison: [Tolk vs FunC: standard library](/languages/tolk/from-func/stdlib). - -### stdlib is now embedded, not downloaded from GitHub - -| FunC | Tolk | -| :-------------------------------: | :-----------------------: | -| 1. Download stdlib.fc from GitHub | 1. Use standard functions | -| 2. Save into the project | – | -| 3. \`#include "stdlib.fc";\`\` | – | -| 4.Use standard functions | – | - -In Tolk, the standard library is part of the distribution. It is inseparable, as maintaining the **language, compiler, and standard library** together is required for proper release management. - -The compiler automatically locates the standard library. If Tolk is installed using an apt package, stdlib sources are downloaded and stored on disk, so the compiler locates them by system paths. When using the WASM wrapper, stdlib is provided by `tolk-js`. - -The standard library is split into multiple files: - -- `common.tolk` for most common functions, -- `gas-payments.tolk` for gas calculations, -- `tvm-dicts.tolk`, and others. - -Functions from `common.tolk` are available and implicitly imported by the compiler. Other files must be explicitly imported. - -```tolk -import "@stdlib/gas-payments" // ".tolk" optional - -var fee = calculateStorageFee(...); -``` - -The rule **import what is used** applies to `@stdlib/...` files as well, with the only exception of `common.tolk`. - -IDE plugins automatically detect the stdlib folder and insert required imports while typing. - -### Logical operators && ||, logical not ! - -In FunC, only bitwise operators `~ & | ^` exist. Using them as logical operators leads to errors because their behavior is different: - -| `a & b` | `a && b` | note | -| :-----------: | :------------------: | :-----------------: | -| `0 & X = 0` | `0 & X = 0` | sometimes identical | -| `-1 & X = -1` | `-1 & X = -1` | sometimes identical | -| `1 & 2 = 0` | `1 && 2 = -1 (true)` | generally not | - -| `~ found` | `!found` | note | -| :---------------------: | :-------------: | :-----------------: | -| `true (-1) → false (0)` | `-1 → 0` | sometimes identical | -| `false (0) → true (-1)` | `0 → -1` | sometimes identical | -| `1 → -2` | `1 → 0 (false)` | generally not | - -| `condition & f()` | `condition && f()` | -| :--------------------: | :--------------------------------------------: | -| `f()` is called always | `f()` is called only if `condition` | -| `condition \| f()` | `condition \|\| f()` | -| `f()` is called always | `f()` is called only if `condition` is `false` | - -Tolk supports logical operators. They behave as expected, as shown in the right column.. `&&` and `||` may produce suboptimal Fift code, but the effect is negligible. Use them as in other languages. - -| FunC | Tolk | -| :----------------------------------------------: | :----------------------------------------: | -| `if (~ found?)` | `if (!found)` | -| `if (~ found?) {if (cs~load_int(32) == 0) {...}` | `if (!found && cs.loadInt(32) == 0) {...}` | -| `ifnot (cell_null?(signatures))` | `if (signatures != null)` | -| `elseifnot (eq_checksum)` | `else if (!eqChecksum)` | - -Keywords `ifnot` and `elseifnot` are removed because logical NOT is now available. For optimization, Tolk compiler generates `IFNOTJMP`. The `elseif` keyword is replaced by the standard `else if`. - -A boolean `true` transformed as `int` is -1, not 1. This reflects TVM representation. - -### Indexed access tensorVar.0 and tupleVar.0 - -Use `tensorVar.{i}` to access i-th component of a tensor. Modifying it changes the tensor. - -```tolk -var t = (5, someSlice, someBuilder); // 3 stack slots -t.0 // 5 -t.0 = 10; // t is now (10, ...) -t.0 += 1; // t is now (11, ...) -increment(mutate t.0); // t is now (12, ...) -t.0.increment(); // t is now (13, ...) - -t.1 // slice -t.100500 // compilation error -``` - -Use `tupleVar.{i}` to access the i-th element of a tuple, uses INDEX internally. Modifying it changes the tuple, SETINDEX internally. - -```tolk -var t = [5, someSlice, someBuilder]; // 1 tuple on a stack with 3 items -t.0 // "0 INDEX", reads 5 -t.0 = 10; // "0 SETINDEX", t is now [10, ...] -t.0 += 1; // also works: "0 INDEX" to read 10, "0 SETINDEX" to write 11 -increment(mutate t.0); // also, the same way -t.0.increment(); // also, the same way - -t.1 // "1 INDEX", it's slice -t.100500 // compilation error -``` - -It also works for untyped tuples, though the compiler does not guarantee index correctness. - -```tolk -var t = createEmptyTuple(); -t.tuplePush(5); -t.0 // will head 5 -t.0 = 10 // t will be [10] -t.100500 // will fail at runtime -``` - -- Supports nesting `var.{i}.{j}` -- Supports nested tensors, nested tuples, and tuples inside tensors -- Supports `mutate` and global variables - -```tolk -t.1.2 = 10; // "1 INDEX" + "2 SETINDEX" + "1 SETINDEX" -t.1.2 += 10; // "1 INDEX" + "2 INDEX" + sum + "2 SETINDEX" + "1 SETINDEX" - -globalTuple.1.2 += 10; // "GETGLOB" + ... + "SETGLOB" -``` - -### Type address - -In TVM, all **binary data** is represented as **a slice**. The same applies to addresses: even though TL-B describes the `MsgAddress`, at the TVM level, it's just a slice. -Thus, in FunC's standard library, `loadAddress` returns `slice` and `storeAddress` accepts `slice`. - -Tolk introduces a dedicated `address` type meaning "internal address". It remains a TVM slice at runtime, but differs from an **abstract slice** in terms of the type system: - -1. Integrated with auto-serialization: the compiler knows how to pack and unpack it using `LDSTDADDR` and `STSTDADDR`. -1. Comparable: operators `==` and `!=` supported for addresses. - -```tolk -if (senderAddress == msg.owner) -``` - -3. Introspectable: `address.getWorkchain()` and `address.getWorkchainAndHash()`. - -Passing a slice instead leads to an error: - -```tolk -var a: slice = s.loadAddress(); // error, can not assign `address` to `slice` -``` - -There is also a type `any_address` to store _internal, external, or none_ address. - -**Embedding a const address into a contract** - -Use the built-in `address()` function. In FunC, this was done using the postfix `"..."a`, which returned a slice. - -```tolk -address("EQCRDM9h4k3UJdOePPuyX40mCgA4vxge5Dc5vjBR8djbEKC5") -address("0:527964d55cfa6eb731f4bfc07e9d025098097ef8505519e853986279bd8400d8") -``` - -**Casting `slice` to `address` and vice versa** - -A raw slice that represents an address can be cast using the `as` operator. - -This occurs when an address is manually constructed in a builder using its binary representation: - -```tolk -var b = beginCell() - .storeUint(0b01) // addr_extern - ...; -var s = b.endCell().beginParse(); -return s as address; // `slice` as `address` -``` - -A reversed cast is also valid: `someAddr as slice`. - -**Different types of addresses** - -There are different types of [addresses](/foundations/addresses/overview). The most frequently used is an [internal address](/foundations/addresses/overview#internal-addresses) — the address of a smart contract. But also, there are **external** and **none** addresses. In a binary TL-B representation: - -- `10` (**internal** prefix) + `0` (anycast, always 0) + workchain (8 bits) + hash (256 bits) — that's `EQ...`: it's 267 bits -- `01` (**external** prefix) + len (9 bits) + len bits — external addresses -- `00` (**none** prefix) — address **none**, 2 bits - -**`address` is "internal only"** (90% use cases). -**`address?` (nullable) is "internal/none"** (9% use cases). -**`any_address` is "internal/external/none"** (1% use cases). - -Remember that `address` is "workchain + hash". Validate untrusted input: - -```tolk -val newOwner = msg.nextOwnerAddress; -assert(newOwner.getWorkchain() == BASECHAIN) throw 403; -``` - -### Type aliases type NewName = \ - -Tolk supports type aliases, like in TypeScript and Rust. -An alias creates a new name for an existing type and **remains fully interchangeable with it**. - -```tolk -type UserId = int32 -type MaybeOwnerHash = bytes32? - -fun calcHash(id: UserId): MaybeOwnerHash { ... } - -var id: UserId = 1; // ok -var num: int = id; // ok -var h = calcHash(id); -if (h != null) { - h as slice; // bytes32 as slice -} -``` - -### Nullable types T?, null safety, smart casts, operator ! - -Tolk supports nullable types: `int?`, `cell?`, and `T?` in general, including tensors. -Non-nullable types, such as `int` and `cell`, cannot hold null values. - -The compiler enforces **null safety**: nullable types cannot be accessed without a null check. -Checks are applied through smart casts. Smart casts exist only at compile time and do not affect gas or stack usage. - -```tolk -var value = x > 0 ? 1 : null; // int? - -value + 5; // error -s.storeInt(value); // error - -if (value != null) { - value + 5; // ok, smart cast - s.storeInt(value); // ok, smart cast -} -``` - -When a variable's type is not declared, it is inferred from the initial assignment and never changes: - -```tolk -var i = 0; -i = null; // error, can't assign `null` to `int` -i = maybeInt; // error, can't assign `int?` to `int` -``` - -Variables that may hold null must be **explicitly declared as nullable**: - -```tolk -// incorrect -var i = null; -if (...) { - i = 0; // error -} - -// correct -var i: int? = null; -// or -var i = null as int?; -``` - -Smart casts handle nullable types automatically, enabling code such as: - -```tolk -if (lastCell != null) { - // here lastCell is `cell`, not `cell?` -} -``` - -```tolk -if (lastCell == null || prevCell == null) { - return; -} -// both lastCell and prevCell are `cell` -``` - -```tolk -var x: int? = ...; -if (x == null) { - x = random(); -} -// here x is `int` -``` - -```tolk -while (lastCell != null) { - lastCell = lastCell.beginParse().loadMaybeRef(); -} -// here lastCell is 100% null -``` - -```tolk -// t: (int, int)? -t.0 // error -t!.0 // ok -if (t.0 != null) { - t.0 // ok -} -``` - -Smart casts do not apply to global variables; they operate only on local variables. - -The `!` operator in Tolk provides a compile-time non-null assertion, similar to `!` in TypeScript and `!!`in Kotlin. -It bypasses the compiler's check for variables that are guaranteed to be non-null. - -```tolk -fun doSmth(c: cell); - -fun analyzeStorage(nCells: int, lastCell: cell?) { - if (nCells) { // then lastCell 100% not null - doSmth(lastCell!); // use ! for this fact - } -} -``` - -Functions that always throw can be declared with the return type `never`: - -```tolk -fun alwaysThrows(): never { - throw 123; -} - -fun f(x: int) { - if (x > 0) { - return x; - } - alwaysThrows(); - // no `return` statement needed -} -``` - -The `never` type occurs implicitly when a condition is impossible to satisfy: - -```tolk -var v = 0; -// prints a warning -if (v == null) { - // v is `never` - v + 10; // error, can not apply `+` `never` and `int` -} -// v is `int` again -``` - -Encountering `never` in compilation errors usually indicates a warning in the preceding code. - -Non-atomic nullable types are supported, e.g., `(int, int)?`, `(int?, int?)?`, or `()?`. -A special _value presence_ stack slot is added automatically. - -It stores `0` for null values and `-1` for non-null values. - -```tolk -// t: (int, int)? -t = (1, 2); // 1 2 -1 -t = (3, 4); // 3 4 -1 -t = null; // null null 0 - -// t: ()? -t = (); // -1 -t = null; // 0 -``` - -Nullability improves type safety and reliability. -Nullable types prevent runtime errors by enforcing explicit handling of optional values. - -### Union types T1 | T2 | ..., operators match, is, !is - -Union types allow a variable to hold multiple types, similar to TypeScript. - -```tolk -fun whatFor(a: bits8 | bits256): slice | UserId { ... } - -var result = whatFor(...); // slice | UserId -``` - -Nullable types `T?` are equivalent to `T | null`. -Union types support intersection properties. For example, `B | C` can be passed and assigned to `A | B | C | D`. - -The only way to handle union types in code is through **pattern matching**: - -```tolk -match (result) { - slice => { /* result is smart-casted to slice */ } - UserId => { /* result is smart-casted to UserId */ } -} -``` - -Example: - -```tolk -match (result) { - slice => { - return result.loadInt(32); - } - UserId => { - if (result < 0) { - throw 123; - } - return loadUser(result).parentId; - } -} -``` - -The `match` must cover all union cases and can be used as an expression. - -```tolk -type Pair2 = (int, int) -type Pair3 = (int, int, int) - -fun getLast(tensor: Pair2 | Pair3) { - return match (tensor) { - Pair2 => tensor.1, - Pair3 => tensor.2, - } -} -``` - -**Syntax details:** - -- Commas are optional inside `{}` but required in expressions. -- A trailing comma is allowed. -- No semicolon is required after `match` when used as a statement. -- For match-expressions, an arm that terminates has the type `never`. - -```tolk -return match (msg) { - ... - CounterReset => throw 403, // forbidden -} -``` - -Variable declaration inside `match` is allowed: - -```tolk -match (val v = getPair2Or3()) { - Pair2 => { - // use v.0 and v.1 - } - Pair3 => { - // use v.0, v.1, and v.2 - } -} -``` - - - At the TVM level, union types are stored as tagged unions, similar to enums in Rust: - - - Each type is assigned a unique type ID, stored alongside the value. - - The union occupies N + 1 stack slots, where N is the maximum size of any type in the union. - - A nullable type `T?` is a union with `null` (type ID = 0). Atomic types like `int?` use a single stack slot. - - ```tolk - var v: int | slice; // 2 stack slots: value and typeID - // - int: (100, 0xF831) - // - slice: (CS{...}, 0x29BC) - match (v) { - int => // IF TOP == 0xF831 { ... } - // v.slot1 contains int, can be used in arithmetics - slice => // ELSE { IF TOP == 0x29BC { ... } } - // v.slot1 contains slice, can be used to loadInt() - } - - fun complex(v: int | slice | (int, int)) { - // Stack representation: - // - int: (null, 100, 0xF831) - // - slice: (null, CS{...}, 0x29BC) - // - (int, int): (200, 300, 0xA119) - } - - complex(v); // passes (null, v.slot1, v.typeid) - complex(5); // passes (null, 5, 0xF831) - ``` - - -Union types can also be tested using `is`. Smart casts behave as follows: - -```tolk -fun f(v: cell | slice | builder) { - if (v is cell) { - v.cellHash(); - } else { - // v is `slice | builder` - if (v !is builder) { return } - // v is `slice` - v.sliceHash(); - } - // v is `cell | slice` - if (v is int) { - // v is `never` - // a warning is also printed, condition is always false - } -} -``` - -### Pattern matching for expressions (switch-like behavior) - -`match` can be used with constant expressions, similar to `switch`: - -```tolk -val nextValue = match (curValue) { - 1 => 0, - 0 => 1, - else => -1 -}; -``` - -**Rules:** - -- Only constant expressions are allowed on the left-hand side, e.g.,`1`, `SOME_CONST`, `2 + 3`. -- Branches may include `return` or `throw`. -- `else` is required for expression form and optional for statement form. - -```tolk -// statement form -match (curValue) { - 1 => { nextValue = 0 } - 0 => { nextValue = 1 } - -1 => throw NEGATIVE_NOT_ALLOWED -} - -// expression form, else branch required -val nextValue = match (curValue) { - ... - else => -} -``` - -### Structures - -Similar to TypeScript, but executed at the TVM level. - -```tolk -struct Point { - x: int - y: int -} - -fun calcMaxCoord(p: Point) { - return p.x > p.y ? p.x : p.y; -} - -// declared like a JS object -var p: Point = { x: 10, y: 20 }; -calcMaxCoord(p); - -// called like a JS object -calcMaxCoord({ x: 10, y: 20 }); - -// works with shorthand syntax -fun createPoint(x: int, y: int): Point { - return { x, y } -} -``` - -- A struct is a **named tensor**. -- `Point` is equivalent to `(int, int)` at the TVM level. -- Field access `p.x` corresponds to tensor element access `t.0` for reading and writing. - -There is **no bytecode overhead**; tensors can be replaced with structured types. - -Fields can be separated by newlines, which is recommended, or by `;` or `,,`. Both of which are valid, similar to TypeScript. - -When creating an object, either `StructName { ... }` or `{ ... }` can be used if the type is clear from context, such as return type or assignment: - -```tolk -var s: StoredInfo = { counterValue, ... }; -var s: (int, StoredInfo) = (0, { counterValue, ... }); - -// also valid -var s = StoredInfo { counterValue, ... }; -``` - -Default values for fields are supported: - -```tolk -struct DefDemo { - f1: int = 0 - f2: int? = null - f3: (int, coins) = (0, ton("0.05")) -} - -var d: DefDemo = {}; // ok -var d: DefDemo = { f2: 5 }; // ok -``` - -Structs can include methods as extension functions. - -Fields support the following modifiers: - -- `private` — accessible only within methods. -- `readonly` — immutable after object creation. - -```tolk -struct PositionInTuple { - private readonly t: tuple - currentIndex: int -} - -fun PositionInTuple.create(t: tuple): PositionInTuple { - // the only way to create an object with a private field - // is from a static method (or asm function) - return { t, currentIndex: 0 } -} - -fun PositionInTuple.next(mutate self) { - // self.t can not be modified: it's readonly - self.currentIndex += 1; -} - -var p = PositionInTuple.create(someTuple); -// p.t is unavailable here: it's private -``` - -### Generic structs and aliases - -Generic structs and type aliases exist only at the type level and incur no runtime cost. - -```tolk -struct Container { - isAllowed: bool - element: T? -} - -struct Nothing - -type Wrapper = Nothing | Container -``` - -Example usage: - -```tolk -fun checkElement(c: Container) { - return c.element != null; -} - -var c: Container = { isAllowed: false, element: null }; - -var v: Wrapper = Nothing {}; -var v: Wrapper = Container { value: 0 }; -``` - -For generic types, type arguments must be specified when using them: - -```tolk -fun getItem(c: Container) // error, specify type arguments -fun getItem(c: Container) // ok -fun getItem(c: Container) // ok - -var c: Container = { ... } // error, specify type arguments -var c: Container = { ... } // ok -``` - -For generic functions, the compiler can automatically infer type arguments from a call: - -```tolk -fun doSmth(value: Container) { ... } - -doSmth({ item: 123 }); // T = int -doSmth({ item: cellOrNull }); // T = cell? -``` - -Demo: `Response`: - -```tolk -struct Ok { result: TResult } -struct Err { err: TError } - -type Response = Ok | Err - -fun tryLoadMore(slice: slice): Response { - return ... - ? Ok { result: ... } - : Err { err: ErrorCodes.NO_MORE_REFS } -} - -match (val r = tryLoadMore(inMsg)) { - Ok => { r.result } - Err => { r.err } -} -``` - -### Methods: for any types, including structures - -Methods are declared as extension functions, similar to Kotlin. -A method that accepts the first `self` parameter acts as an instance method; without `self`, it is a static method. - -```tolk -fun Point.getX(self) { - return self.x -} - -fun Point.create(x: int, y: int): Point { - return { x, y } -} -``` - -Methods can be defined **for any type**, including aliases, unions, and built-in types: - -```tolk -fun int.isZero(self) { - return self == 0 -} - -type MyMessage = CounterIncrement | ... - -fun MyMessage.parse(self) { ... } -// this is identical to -// fun (CounterIncrement | ...).parse(self) -``` - -Methods work with `asm`, as `self` is treated like a regular variable: - -```tolk -@pure -fun tuple.size(self): int - asm "TLEN" -``` - -By default, `self` is immutable, preventing modification or calls to mutating methods. -To make `self` mutable, declare `mutate self` explicitly: - -```tolk -fun Point.assignX(mutate self, x: int) { - self.x = x; // without mutate, an error "modifying immutable object" -} - -fun builder.storeInt32(mutate self, v: int32): self { - return self.storeInt(v, 32); -} -``` - -Methods for generic structs can be created without specifying ``. The compiler interprets unknown symbols in the receiver type as generic arguments during the parsing process. - -```tolk -struct Container { - item: T -} - -// compiler treats T (unknown symbol) as a generic parameter -fun Container.getItem(self) { - return self.item; -} - -// and this is a specialization for integer containers -fun Container.getItem(self) { - ... -} -``` - -Example: - -```tolk -struct Pair { - first: T1 - second: T2 -} - -// both , , etc. work: any unknown symbols -fun Pair.create(f: A, s: B): Pair { - return { - first: f, - second: s, - } -} -``` - -Similarly, any unknown symbol, typically `T`, can be used to define a method that accepts any type: - -```tolk -// any receiver -fun T.copy(self): T { - return self; -} - -// any nullable receiver -fun T?.isNull(self): bool { - return self == null; -} -``` - -When multiple methods match a call to `someObj.method()`, the compiler selects the most specific one: - -```tolk -fun int.copy(self) { ... } -fun T.copy(self) { ... } - -6.copy() // int.copy -(6 as int32).copy() // T.copy with T=int32 -(6 as int32?).copy() // T.copy with T=int? - -type MyMessage = CounterIncrement | CounterReset -fun MyMessage.check() { ... } -fun CounterIncrement.check() { ... } - -MyMessage{...}.check() // first -CounterIncrement{...}.check() // second -CounterReset{...}.check() // first -``` - -A generic function can be assigned to a variable, but type arguments must be specified explicitly. - -```tolk -fun genericFn(v: T) { ... } -fun Container.getItem(self) { ... } - -var callable1 = genericFn; -var callable2 = Container.getItem; -callable2(someContainer32); // pass it as self -``` - -### Enums - -```tolk -// will be 0 1 2 -enum Color { - Red - Green - Blue -} -``` - -**Properties:** - -- Similar to TypeScript and C++ enums -- Distinct type, not `int` -- Checked during deserialization -- Exhaustive in `match` - -**Enum syntax** - -Enum members can be separated by `,` , `;`, or a newline, similar to struct fields. -Values can be specified manually; unspecified members are auto-calculated. - -```tolk -enum Mode { - Foo = 256, - Bar, // implicitly 257 -} -``` - -**Enums are distinct types, not integers** - -`Color.Red` is `Color`, not `int`, although it holds the value `0` at runtime. - -```tolk -fun isRed(c: Color) { - return c == Color.Red -} - -isRed(Color.Blue) // ok -isRed(1) // error, can not pass `int` to `Color` -``` - -Since enums are types, they can be: - -- Used as variable and parameters -- Extended with methods an enum -- Used in struct fields, unions, generics, and other type contexts - -```tolk -struct Gradient { - from: Color - to: Color? = null -} - -fun Color.isRed(self) { - return self == Color.Red -} - -var g: Gradient = { from: Color.Blue }; -g.from.isRed(); // false -Color.Red.isRed(); // true - -match (g.to) { - null => ... - Color => ... -} -``` - -**Enums are integers under the hood** - -At the TVM level, an enum such as `Color` is represented as `int`. Casting between the enum and `int` is allowed: - -- `Color.Blue as int` evaluates to `2` -- `2 as Color` evaluates to `Color.Blue` - -Using `as` can produce invalid enum values. This is undefined behavior: for example, `100 as Color` is syntactically valid, but program behavior is unpredictable after this point - -During deserialization using `fromCell()`, the compiler performs checks to ensure that encoded integers correspond to valid enum values. - -Enums in Tolk differ from Rust. In Rust, each enum member can have a distinct structure. In Tolk, union types provide that capability, so enums are **integer constants**. - -**`match` for enums is exhaustive** - -Pattern matching on enums requires coverage of all cases: - -```tolk -match (someColor) { - Color.Red => {} - Color.Green => {} - // error: Color.Blue is missing -} -``` - -All enum cases must be covered, or `else` can be used to handle remaining values: - -```tolk -match (someColor) { - Color.Red => {} - else => {} -} -``` - -The `==` operator can be used to compare integers and addresses: - -```tolk -if (someColor == Color.Red) {} -else {} -``` - -The expression `someColor is Color.Red` is invalid syntax. -The `is` operator is used for type checks. -Given `var union: Color | A`, `u is Color` is valid. -Use `==` to compare enum values. - -**Enums are allowed in `throw` and `assert`** - -```tolk -enum Err { - InvalidId = 0x100 - TooHighId -} - -assert (id < 1000) throw Err.TooHighId; // excno = 257 -``` - -**Enums and serialization** - -Enums can be packed to and unpacked from cells like `intN` or `uintN`, where `N` is: - -- Specified manually, e.g., `enum Role: int8 { ... }` -- Calculated automatically as the minimal N to fit all values - -_The serialization type can be specified manually:_ - -```tolk -// `Role` will be (un)packed as `int8` -enum Role: int8 { - Admin, - User, - Guest, -} - -struct ChangeRoleMsg { - ownerAddress: address - newRole: Role // int8: -128 <= V <= 127 -} -``` - -_Or it will be calculated automatically._ For `Role` above, `uint2` is sufficient to fit values `0, 1, 2`: - -```tolk -// `Role` will (un)packed as `uint2` -enum Role { - Admin, - User, - Guest, -} -``` - -_During deserialization, the input value is checked for correctness._ For `enum Role: int8` with values `0, 1, 2`, any **input\<0** or **input>2** triggers exception 5, integer out of range. - -This check applies to both value ranges and manually specified enum values: - -```tolk -enum OwnerHashes: uint256 { - id1 = 0x1234, - id2 = 0x2345, - ... -} - -// on serialization, just "store uint256" -// on deserialization, "load uint256" + throw 5 if v not in [0x1234, 0x2345, ...] -``` - -### Auto-detect and inline functions - -Tolk can inline functions at the compiler level without using `PROCINLINE` as defined by Fift. - -```tolk -fun Point.create(x: int, y: int): Point { - return {x, y} -} - -fun Point.getX(self) { - return self.x -} - -fun sum(a: int, b: int) { - return a + b; -} - -fun main() { - var p = Point.create(10, 20); - return sum(p.getX(), p.y); -} -``` - -is compiled to: - -```fift -main PROC:<{ - 30 PUSHINT -}> -``` - -The compiler automatically determines which functions to inline. - -- `@inline` attribute forces inlining. -- `@noinline` prevents a function from being inlined. -- `@inline_ref` preserves an inline reference, suitable for rarely executed paths. - -Compiler inlining: - -- Efficient for stack manipulation. -- Supports arguments of any stack width. -- Works with any functions or methods, except: - - Recursive functions - - Functions containing `return` statements in the middle -- Supports `mutate` and `self`. - -Simple getters, such as `fun Point.getX(self) { return self.x }`, do not require stack reordering. -Small functions can be extracted without runtime cost. -The compiler handles inlining; no inlining is deferred to Fift. - -**How does auto-inline work?** - -- Simple, small functions are always inlined -- Functions called only once are always inlined - -For every function, the compiler calculates a weight, a heuristic AST-based metric, and the usages count. - -- If `weight < THRESHOLD`, the function is always inlined -- If `usages == 1`, the function is always inlined -- Otherwise, an empirical formula determines inlining - -The `@inline` annotation can be applied to large functions when all usages correspond to hot paths. -Inlining can also be disabled with `@inline_ref`, even for functions called once. For example, in unlikely execution paths. -For optimization, use gas benchmarks and experiment with inlining and branch reordering. - -**What can NOT be auto-inlined?** - -A function is NOT inlined, even if marked with `@inline`, in the following cases: - -- The function contains `return` in the middle. Multiple return points are unsupported for inlining. -- The function participates in a recursive call chain `f -> g -> f`. -- The function is used as a non-call. For example, when a reference is taken: `val callback = f`. - -### No tilde \~ methods, mutate keyword instead - -In FunC, both `.methods()` and `~methods()` exist. -In Tolk, only the **dot syntax** is used, and methods are called as `.method()`. -Tolk follows expected behavior: - -```tolk -b.storeUint(x, 32); // modifies a builder, can be chainable -s.loadUint(32); // modifies a slice, returns integer -``` - -For details, see [Mutability in Tolk](/languages/tolk/from-func/mutability). - -### Auto-packing to/from cells/builders/slices - -Any struct can be automatically packed into a cell or unpacked from one: - -```tolk -struct Point { - x: int8 - y: int8 -} - -var value: Point = { x: 10, y: 20 } - -// makes a cell containing "0A14" -var c = value.toCell(); -// back to { x: 10, y: 20 } -var p = Point.fromCell(c); -``` - -### Universal createMessage: avoid manual cells composition - -No need for manual `beginCell().storeUint(...).storeRef(...)` boilerplate — describe the message in a literal and the compiler handles packing. - -```tolk -val reply = createMessage({ - bounce: BounceMode.NoBounce, - value: ton("0.05"), - dest: senderAddress, - body: RequestedInfo { ... } -}); -reply.send(SEND_MODE_REGULAR); -``` - -### map\ instead of low-level TVM dictionaries - -Tolk introduces `map`: - -- A generic type `map` — any serializable keys and values. -- The compiler automatically generates `asm` instructions and performs (de)serialization on demand. -- Natural syntax for iterating forwards, backwards, or starting from a specified key. -- Zero overhead compared to low-level approach. - -**Demo: `set`, `exists`, `get`, etc.** - -```tolk -var m: map = createEmptyMap(); -m.set(1, 10); -m.addIfNotExists(2, -20); -m.replaceIfExists(2, 20); -m.delete(2); // now: [ 1 => 10 ] - -m.exists(1); // true -m.exists(2); // false - -val r1 = m.get(1); -if (r1.isFound) { // true - val v = r1.loadValue(); // 10 -} - -val r2 = m.get(2); -if (r2.isFound) { // false - ... -} - -m.mustGet(1); // 10 -m.mustGet(2); // runtime error -``` - -`m.get(key)` returns not an "optional value", but `isFound + loadValue()` - -```tolk -// NOT like this -var v = m.get(key); -if (v != null) { - // "then v is the value" — NO, not like this -} - -// BUT -var r = m.get(key); -if (r.isFound) { - val v = r.loadValue(); // this is the value -} -``` - -- `m.get(key)` returns a struct, NOT `V?`. -- `m.mustGet(key)` returns `V` and throws if the key is missing. - -**Why "isFound" but not "optional value"?** - -- Gas consumption; zero overhead. -- Nullable values can be supported, such as `map` or `map`. -- Returning `V?`, makes it impossible to distinguish between "key exists but value is null" and "key does not exist". - -**Iterating forward and backward** - -There is no syntax like `foreach`. Iteration follows this pattern: - -- define the starting key: `r = m.findFirst()` or `r = m.findLast()` -- while `r.isFound`: - - use `r.getKey()` and `r.loadValue()` - - move the cursor: `r = m.iterateNext(r)` or `r = m.iteratePrev(r)` - -Example: iterate all keys forward - -```tolk -// suppose there is a map [ 1 => 10, 2 => 20, 3 => 30 ] -// this function will print "1 10 2 20 3 30" -fun iterateAndPrint(m: map) { - var r = m.findFirst(); - while (r.isFound) { - debug.print(r.getKey()); - debug.print(r.loadValue()); - r = m.iterateNext(r); - } -} -``` - -Example: iterate from key\<=2 backward - -```tolk -// suppose `m` is `[ int => address ]`, already filled -// for every key<=2, print addr.workchain -fun printWorkchainsBackwards(m: map) { - var r = m.findKeyLessOrEqual(2); - while (r.isFound) { - val a = r.loadValue(); // it's address - debug.print(a.getWorkchain()); - r = m.iteratePrev(r); - } -} -``` - -Iteration over maps uses existing syntax. - -Use `while (r.isFound)`, not `while (r == null)`. -As with `m.get(key)`, existence is checked through `isFound`. - -```tolk -// this is a cursor, it has "isFound" + "getKey()" + "loadValue()" -// (methods are applicable only if isFound) -var r = m.findFirst(); -while (r.isFound) { - // ... use r.getKey() and r.loadValue() - r = m.iterateNext(r); -} - -// similar to map.get() with "isFound" + "loadValue()" -var f = m.get(key); -if (f.isFound) { - // ... use f.loadValue() -} -``` - -The reason is the same — zero overhead and no hidden runtime instructions or stack manipulations. - -Use `m.isEmpty()`, not `m == null`. Since `map` is a dedicated type, it must be checked with `isEmpty()`, because `m == null` does not work. - -Suppose a wrapper over dictionaries is implemented: - -```tolk -struct MyMap { - tvmDict: cell | null -} - -fun MyMap.isEmpty(self) {} -``` - -Given `var m: MyMap`, calling `m.isEmpty()` works. The expression `m == null` is invalid. The compiler issues the following warning: - -```text -variable `m` of type `map` can never be `null`, this condition is always false -``` - -The same rule applies to built-in maps. When transitioning code from low-level dicts to high-level maps, pay attention to compiler warnings in the console. - -**A nullable map** is valid: `var m: map<...>?`. This variable can be null and not null. When not null, it can contain an empty map or a non-empty map. The expression `m == null` only makes sense for nullable maps. - -**Allowed types for K and V** - -All the following key and value types are valid: - -```tolk -// all these types are valid -map -map -map> -map> -map -``` - -Some types are NOT allowed. General rules: - -- Keys must be fixed-width and contain zero references - - Valid: int32, uint64, address, bits256, Point - - Invalid: int, coins, cell -- Values must be serializable - - Valid: int32, coins, AnyStruct, Cell\ - - Invalid: int, builder - -In practice, keys are typically `intN`, `uintN`, or `address`. Values can be any serializable type. - -At the TVM level, keys can be numbers or slices. Complex keys, such as `Point`, are automatically serialized and deserialized by the compiler. - -```tolk -struct Point { - x: int8 - y: int8 -} - -// the compiler automatically packs Point to a 16-bit slice key -var m: map -``` - -If a key is a struct with a single intN field, it behaves like a number. - -```tolk -struct UserId { - v: int32 -} - -// works equally to K=int32 without extra serialization -var m: map -``` - -#### Available methods for maps - -JetBrains IDE and VS Code provide method suggestions. Most methods are self-explanatory. - -- `createEmptyMap(): map` - -Returns an empty typed map. Equivalent to `PUSHNULL` since TVM NULL represents an empty map. - -- `createMapFromLowLevelDict(d: dict): map` - -Converts a low-level TVM dictionary to a typed map. Accepts an optional cell and returns the same optional cell. Incorrect key and value types cause failure at `map.get` or similar methods. - -- `m.toLowLevelDict(): dict` - -Converts a high-level map to a low-level TVM dictionary. Returns the same optional cell. - -- `m.isEmpty(): bool` - -Checks whether a map is empty. Use `m.isEmpty()` instead of `m == null`. - -- `m.exists(key: K): bool` - -Checks whether a key exists in a map. - -- `m.get(key: K): MapLookupResult` - -Gets an element by key. Returns `isFound = false` if key does not exist. - -- `m.mustGet(key: K, throwIfNotFound: int = 9): V` - -Gets an element by key and throws if it does not exist. - -- `m.set(key: K, value: V): self` - -Sets an element by key. Since it returns `self`, calls may be chained. - -- `m.setAndGetPrevious(key: K, value: V): MapLookupResult` - -Sets an element and returns the previous element. If no previous element, `isFound = false`. - -- `m.replaceIfExists(key: K, value: V): bool` - -Sets an element only if the key exists. Returns whether an element was replaced. - -- `m.replaceAndGetPrevious(key: K, value: V): MapLookupResult` - -Sets an element only if the key exists and returns the previous element. - -- `m.addIfNotExists(key: K, value: V): bool` - -Sets an element only if the key does not exist. Returns true if added. - -- `m.addOrGetExisting(key: K, value: V): MapLookupResult` - -Sets an element only if the key does not exist. If exists, returns an old value. - -- `m.delete(key: K): bool` - -Deletes an element by key. Returns true if deleted. - -- `m.deleteAndGetDeleted(key: K): MapLookupResult` - -Deletes an element by key and returns the deleted element. If not found, `isFound = false`. - -- `m.findFirst(): MapEntry` - -Finds the first (minimal) element. For integer keys, returns minimal integer. For addresses or complex keys, represented as slices, returns lexicographically smallest key. Returns `isFound = false` for an empty map. - -- `m.findLast(): MapEntry` - -Finds the last (maximal) element. For integer keys, returns maximal integer. For addresses or complex keys (represented as slices), returns lexicographically largest key. Returns `isFound = false` for an empty map. - -- `m.findKeyGreater(pivotKey: K): MapEntry` - -Finds an element with key greater than `pivotKey`. - -- `m.findKeyGreaterOrEqual(pivotKey: K): MapEntry` - -Finds an element with key greater than or equal to pivotKey. - -- `m.findKeyLess(pivotKey: K): MapEntry` - -Finds an element with key less than pivotKey. - -- `m.findKeyLessOrEqual(pivotKey: K): MapEntry` - -Finds an element with key less than or equal to pivotKey. - -- `m.iterateNext(current: MapEntry): MapEntry` - -Iterates over a map in ascending order. - -- `m.iteratePrev(current: MapEntry): MapEntry` - -Iterates over a map in descending order. - -**Augmented hashmaps and prefix dictionaries** - -These structures are rarely used and are not part of the type system. - -- Prefix dictionaries: `import @stdlib/tvm-dicts` and use assembly functions. -- Augmented hashmaps and Merkle proofs: implement interaction manually. - -### Modern onInternalMessage - -In Tolk, `msg_cell` does not require manual parsing to retrieve `sender_address` or `fwd_fee`. Fields are accessed directly: - -```tolk -fun onInternalMessage(in: InMessage) { - in.senderAddress - in.originalForwardFee - in.valueCoins // typically called "msg value" - - in.| // IDE shows completions -} -``` - -The legacy approach of accepting 4 parameters, as `recv_internal`, works but is less efficient. `InMessage` fields are directly mapped to TVM-11 instructions. - -**Recommended pattern:** - -1. Define each message as a struct, typically including a 32-bit opcode. -1. Define a union of all allowed messages. -1. Use `val msg = lazy MyUnion.fromSlice(in.body)`. -1. Match on `msg`, handling each branch and possibly an `else`. - -Avoid manually extracting `fwd_fee` or other fields at the start of the function. Access them on demand through the `in.smth`. - -```tolk -type AllowedMessageToMinter = - | MintNewJettons - | BurnNotificationForMinter - | RequestWalletAddress - -fun onInternalMessage(in: InMessage) { - val msg = lazy AllowedMessageToMinter.fromSlice(in.body); - - match (msg) { - BurnNotificationForMinter => { - var storage = lazy MinterStorage.load(); - ... - storage.save(); - ... - } - RequestWalletAddress => ... - MintNewJettons => ... - else => { - // for example: - // ignore empty messages, "wrong opcode" for others - assert (in.body.isEmpty()) throw 0xFFFF - } - } -} -``` - -**Separate `onBouncedMessage`** - -In FunC, `msg_cell` required parsing, reading 4-bit flags, and testing `flags & 1` to detect a bounced message. - -In Tolk, bounced messages are handled through a separate entry point: - -```tolk -fun onBouncedMessage(in: InMessageBounced) { -} -``` - -The compiler automatically routes bounced messages: - -```tolk -fun onInternalMessage(in: InMessage) { - // the compiler inserts this automatically: - if (MSG_IS_BOUNCED) { onBouncedMessage(...); return; } - - ... // contract logic -} -``` - -If `onBouncedMessage` is not declared, bounced messages are filtered out: - -```tolk -fun onInternalMessage(in: InMessage) { - // the compiler inserts this automatically: - if (MSG_IS_BOUNCED) { return; } - - ... // contract logic -} -``` - -**Bounced body is either "first 256 bits" or "the entire body"** - -When you use `createMessage`, its parameter `bounce` is an enum: - -- `BounceMode.NoBounce` -- `BounceMode.Only256BitsOfBody` — `in.bouncedBody` will be "0xFFFFFFFF" + first 256 bits of original body (cheapest) -- `BounceMode.RichBounce` — parse `in.bouncedBody` with `RichBounceBody.fromSlice` -- `BounceMode.RichBounceOnlyRootCell` — same, but `originalBody` will contain only a root cell - -```tolk -// demo for "old-fashioned" 256 bits (if you send all with Only256BitsOfBody) -fun onBouncedMessage(in: InMessageBounced) { - in.bouncedBody // 32-bit prefix + 256 bits - - in.bouncedBody.skipBouncedPrefix(); // skips 0xFFFFFFFF - // handle rest of body, probably with lazy match -} -``` - -```tolk -// demo for "rich bounces" to access not 256 bits, but the entire body -// (if you send ALL outgoing messages with BounceMode.RichBounce) -fun onBouncedMessage(in: InMessageBounced) { - val rich = lazy RichBounceBody.fromSlice(in.bouncedBody); - // handle rich.originalBody, probably with lazy match - // use rich.xxx to get exitCode, gasUsed, and so on -} -``` - -### Next steps - -Explore the [Tolk vs FunC benchmarks](https://github.com/ton-blockchain/tolk-bench) — real Jetton, NFT, and Wallet contracts migrated from FunC with the same logic. - -Use the [FunC-to-Tolk converter](https://github.com/ton-blockchain/convert-func-to-tolk) for incremental migration. - -Run `npm create ton@latest` to experiment. diff --git a/languages/tolk/from-func/in-short.mdx b/languages/tolk/from-func/in-short.mdx deleted file mode 100644 index 97abbb7e5..000000000 --- a/languages/tolk/from-func/in-short.mdx +++ /dev/null @@ -1,111 +0,0 @@ ---- -title: "Tolk vs FunC: in short" -sidebarTitle: "In short" ---- - -Tolk is more similar to TypeScript and Kotlin than to C or Lisp. -It provides complete control over the TVM assembler, as it includes a FunC kernel within. - -## Declarations - -- `fun` declares a function. -- `get fun` declares a get method. -- `var` declares a variable. -- `val` declares an immutable variable. - -Types are written on the right. - -Parameter types are required; **return** and **local** variable types are inferred automatically. - -Specifiers such as `inline_ref` are written as `@` attributes. - -```tolk -global storedV: int; - -fun parseData(cs: slice): cell { - var flags: int = cs.loadMessageFlags(); - // … -} - -@inline -fun sum(a: int, b: int) { // auto inferred int - val both = a + b; // same - return both; -} - -get fun currentCounter(): int { ... } -``` - -## Syntax and semantics - -- No `impure`. All functions are treated as `impure` by default; the compiler does not remove user function calls. -- Use `onInternalMessage`, `onExternalMessage`, and `onBouncedMessage` instead of `recv_internal` and `recv_external`. -- `2+2` is 4, not an identifier. Identifiers are **alphanumeric**. Use `const OP_INCREASE` instead of `const op::increase`. `cell` and `slice` are valid identifiers, not keywords. -- Logical operators AND `&&`, OR `||`, NOT `!` are supported. - -#### Syntax improvements - -- `;; comment` → `// comment` -- `{- comment -}` → `/* comment */` -- `#include` → `import`; strict rule: import what you use -- `~ found` → `!found`; used for boolean values only (true is -1, as in FunC) -- `v = null()` → `v = null` -- `null?(v)` → `v == null`, similar for `builder_null?` and others -- `~ null?(v)` → `c != null` -- `throw(excNo)` → `throw excNo` -- `catch(_, _)` → `catch` -- `catch(_, excNo)` → `catch(excNo)` -- `throw_unless(excNo, cond)` → `assert(cond, excNo)` -- `throw_if(excNo, cond)` → `assert(!cond, excNo)` -- `return ()` → `return` -- `do ... until (cond)` → `do ... while (!cond)` -- `elseif` → `else if` -- `ifnot (cond)` → `if (!cond)` -- `"..."c` → `stringCrc32("...")` and other postfixes - -## Compilation model - -- Forward declarations are not required. -- The compiler first parses and then resolves symbols, producing an AST representation of the source code. - -## Standard library - -- Standard library functions use clear, camelCase names. -- The stdlib is embedded and split into multiple files. -- Common functions are available; specialized ones can be imported. For example, `import "@stdlib/tvm-dicts"`. -- IDEs provide import suggestions. -- See [the FunC-Tolk stdlib mapping](/languages/tolk/from-func/stdlib). - -## Language features - -- No `~` tilde methods. See [mutability details](/languages/tolk/from-func/mutability). -- Type mismatch errors are clear and readable. -- The `bool` type is supported. -- Indexed access `tensorVar.0` and `tupleVar.0` is supported. -- Nullable types `T?`, null safety, smart casts, and the operator `!` are supported. -- Union types and pattern matching are supported for types and expressions, with switch-like behavior. -- Type aliases are supported. -- Structures are supported. -- Generics are supported. -- Methods as extension functions are supported. -- Enums are supported. -- Trailing comma is supported. -- A semicolon after the last statement in a block is optional. -- Fift output contains original `.tolk` lines as comments. -- Auto-packing to and from cells is supported for all types. -- Universal createMessage simplifies cell composition. -- `map` instead of low-level TVM dictionaries. -- Anonymous functions (lambdas). -- The compiler automatically detects and inlines functions. -- Includes gas optimization improvements. - -## Tooling around - -- [JetBrains IDE plugin](https://github.com/ton-blockchain/intellij-ton) -- [VS Code extension](https://github.com/ton-blockchain/ton-language-server) -- [FunC-to-Tolk converter](https://github.com/ton-blockchain/convert-func-to-tolk) -- [tolk-js](https://github.com/ton-blockchain/tolk-js) WASM wrapper for blueprint - -## Gas benchmarks - -The [Tolk-bench repository](https://github.com/ton-blockchain/tolk-bench) contains contracts migrated from FunC to Tolk, with identical logic and passing the same tests. diff --git a/languages/tolk/from-func/mutability.mdx b/languages/tolk/from-func/mutability.mdx deleted file mode 100644 index 905e80f89..000000000 --- a/languages/tolk/from-func/mutability.mdx +++ /dev/null @@ -1,265 +0,0 @@ ---- -title: "Mutability in Tolk vs tilde functions in FunC" -sidebarTitle: "Mutability" ---- - -import { Aside } from '/snippets/aside.jsx'; - - - -In FunC, methods can be called as `.method()` or `~method()`. - -In Tolk, all methods use a **dot:** `.method()`. A method _may or may not mutate_ the object. -Tolk defines a mutability model that generalizes the behavior of the `~` tilde in FunC. -Behavior and semantics differ from FunC. - -Tolk method calls are designed to behave similarly to JavaScript: - -| FunC | Tolk | -| ------------------------------------------------------------------------- | -------------------------------------------------------------- | -| `int flags = cs~load_uint(32);` | `var flags = cs.loadUint(32);` | -| `(cs, int flags) = cs.load_uint(32);` | `var flags = cs.loadUint(32);` | -| `(slice cs2, int flags) = cs.load_uint(32);` | `var cs2 = cs;` `var flags = cs2.loadUint(32);` | -| `slice data = get_data().begin_parse();` `int flag = data~load_uint(32);` | `val flag = contract.getData().beginParse().loadUint(32);` | -| `dict~udict_set(...);` | `dict.set(...);` | -| `b~store_uint(x, 32);` | `b.storeInt(x, 32);` | -| `b = b.store_int(x, 32);` | `b.storeInt(x, 32);` // also works `b = b.storeUint(32);` | -| `b = b.store_int(x, 32).store_int(y, 32);` | `b.storeInt(x, 32).storeInt(y, 32);` // also works `b = ...;` | - -## Value semantics - -By default, function arguments in Tolk are **copied by value**. Function calls **do not** modify the original data. - -```tolk -fun someFn(x: int) { - x += 1; -} - -var origX = 0; -someFn(origX); // origX remains 0 -someFn(10); // int -``` - -This also applies to slices, cells, and other types: - -```tolk -fun readFlags(cs: slice) { - return cs.loadInt(32); -} - -var flags = readFlags(msgBody); // msgBody is not modified -// msgBody.loadInt(32) reads the same flags -``` - -## Mutating function parameters - -Adding the `mutate` keyword makes a parameter mutable. To prevent unintended modifications, `mutate` must also be specified when calling the function. - -```tolk -fun increment(mutate x: int) { - x += 1; -} - -// it's correct, simple and straightforward -var origX = 0; -increment(mutate origX); // origX becomes 1 - -// these are compiler errors -increment(origX); // error, unexpected mutation -increment(10); // error, not lvalue -origX.increment(); // error, not a method, unexpected mutation -val constX = getSome(); -increment(mutate constX); // error, it's immutable since `val` -``` - -This also applies to slices and other types: - -```tolk -fun readFlags(mutate cs: slice) { - return cs.loadInt(32); -} - -val flags = readFlags(mutate msgBody); -// msgBody.loadInt(32) will read the next integer -``` - -A function can define multiple mutate parameters: - -```tolk -fun incrementXY(mutate x: int, mutate y: int, byValue: int) { - x += byValue; - y += byValue; -} - -incrementXY(mutate origX, mutate origY, 10); // both += 10 -``` - - - -## Instance methods and self - -Methods — unlike global functions `fun f()` — are declared as `fun receiver_type.f()`. -If a method accepts `self`, it is an instance method; otherwise, it is static. - -```tolk -fun int.assertNotEq(self, throwIfEq: int) { - if (self == throwIfEq) { - throw 100; - } -} - -someN.assertNotEq(10); -10.assertNotEq(10); // also ok, since self is not mutating -``` - -By default, `self` is immutable. The method cannot modify the object. - -```tolk -fun slice.readFlags(self) { - return self.loadInt(32); // error, modifying immutable variable -} - -fun slice.preloadInt32(self) { - return self.preloadInt(32); // ok, it's a read-only method -} -``` - -## Mutating methods with self - -Combining `mutate` with `self` defines a method that modifies the object and is called using the dot syntax. - -Example: - -```tolk -fun slice.readFlags(mutate self) { - return self.loadInt(32); -} - -val flags = msgBody.readFlags(); - -fun int.increment(mutate self) { - self += 1; -} - -var origX = 10; -origX.increment(); // 11 -10.increment(); // error, not lvalue - -// Method can also mutate multiple arguments: -fun int.incrementWithY(mutate self, mutate y: int, byValue: int) { - self += byValue; - y += byValue; -} - -origX.incrementWithY(mutate origY, 10); // both += 10 -``` - -The standard library includes many **mutate self**, such as in tuples and dictionaries. -In FunC, equivalent mutating methods use the tilde `~`. - -```tolk -@pure -fun tuple.push(mutate self, value: X): void - asm "TPUSH" - -t.push(1); -``` - -## Returning self for chaining - -Returning `self` works as `return self` in Python or `return this` in JavaScript. It makes methods such as `storeInt()` chainable. - -```tolk -fun builder.storeInt32(mutate self, x: int): self { - self.storeInt(x, 32); - return self; - - // this also works as expected (the same Fift code) - // return self.storeInt(x, 32); -} - -var b = beginCell().storeInt(1, 32).storeInt32(2).storeInt(3, 32); -b.storeInt32(4); // works without assignment because it mutates b directly -b = b.storeInt32(5); // works with assignment, since also returns -``` - -The return type must be explicitly declared as `self`. Omitting it causes a compilation error. - -## Mutate self in asm functions - -The same behavior can also be implemented in `asm` functions. -A mutation in the compiler works as an implicit return and reassignment of `mutate` parameters. - -Example: - -```tolk -// returns (int, void) -fun increment(mutate x: int): void { ... } - -// does: (x', _) = increment(x); x = x' -increment(mutate x); - -// returns (int, int, (slice, cell)) -fun f2(mutate x: int, mutate y: int): (slice, cell) { ... } - -// does: (x', y', r) = f2(x, y); x = x'; y = y'; someF(r) -someF(f2(mutate x, mutate y)); - -// when `self`, it's the same -// does: (cs', r) = loadInt(cs, 32); cs = cs'; flags = r -flags = cs.loadInt(32); -``` - -Therefore, an `asm` function should place `self'` onto the stack before returning the result: - -```tolk -// "TPUSH" pops (tuple) and pushes (tuple') -// so, self' = tuple', and return an empty tensor -// `void` is a synonym for an empty tensor -fun tuple.push(mutate self, value: X): void - asm "TPUSH" - -// "LDU" pops (slice) and pushes (int, slice') -// with asm(-> 1 0), we make it (slice', int) -// so, self' = slice', and return int -fun slice.loadMessageFlags(mutate self): int - asm(-> 1 0) "4 LDU" -``` - -To return self, specify a return type. -The compiler handles the rest: - -```tolk -// "STU" pops (int, builder) and pushes (builder') -// with asm(op self), we put arguments to correct order -// so, self' = builder', and return an empty tensor -// but to make it chainable, `self` instead of `void` -fun builder.storeMessageOp(mutate self, op: int): self - asm(op self) "32 STU" -``` - -Low-level constructs are rarely needed. Wrappers around existing functions are usually enough. - -```tolk -// just do it like this, without asm; it's the same effective - -fun slice.myLoadMessageFlags(mutate self): int { - return self.loadUint(4); -} - -fun builder.myStoreMessageOp(mutate self, flags: int): self { - return self.storeUint(32, flags); -} -``` diff --git a/languages/tolk/from-func/stdlib-fc.mdx b/languages/tolk/from-func/stdlib-fc.mdx new file mode 100644 index 000000000..bfcde5c6a --- /dev/null +++ b/languages/tolk/from-func/stdlib-fc.mdx @@ -0,0 +1,189 @@ +--- +title: "Mapping: stdlib.fc -> Tolk" +sidebarTitle: "stdlib.fc mapping" +--- + +import { Aside } from '/snippets/aside.jsx'; + +FunC contracts depend on the `stdlib.fc` file, which contains `asm` functions closely tied to [TVM instructions](/tvm/instructions). + +Tolk provides a standard library that gradually evolved from FunC's stdlib. +Therefore, **many functions from `stdlib.fc` can be mapped to Tolk**. + +But since Tolk is much more flexible as a language, most of the functions turned into methods. + +## Functions vs methods + +Tolk supports declaring methods (see [syntax](/languages/tolk/syntax/functions-methods)) — even for primitives. + +That's why lots of FunC's global-scope functions are now methods: `cell.hash()`, `tuple.size()`, etc. + +```tolk +@pure +fun cell.hash(self): uint256 + asm "HASHCU" +``` + + + +## List of renamed and removed functions + +For `load_xxx`, `store_xxx`, and `skip_xxx` — use [automatic serialization](/languages/tolk/features/auto-serialization). +Methods `slice.loadXXX`, `builder.storeXXX`, and `slice.skipXXX` still exist for manual cell parsing, but their use is not recommended. + +For `idict_xxx`, `udict_xxx`, and `dict_xxx` — use [native maps](/languages/tolk/types/maps). +Old-fashioned dictionaries still exist in a file `@stdlib/tvm-dicts`, but their use is not recommended. + +Other functions are listed below. + +- some were renamed to clean, descriptive names +- some became methods and are called via dot +- some were removed, because they are rarely used in practice +- some were removed, because they can be expressed syntactically + - `pair(a, b)` => `[a, b]` + - `fourth(t)` => dot-access `t.3` + - etc. + +| FunC name | Tolk name | +| ----------------------------- | ------------------------------------ | +| `empty_tuple` | `createEmptyTuple` | +| `t~tpush(v)` | `t.push(v)` | +| `first(t)` or `t.first()` | `t.first()` | +| `at(t,i)` or `t.at(i)` | `t.get(i)` or dot-access `t.{i}` | +| `touch(v)` | `v.stackMoveToTop()` | +| `impure_touch` | _(deleted)_ | +| `single` | _(deleted)_ | +| `unsingle` | _(deleted)_ | +| `pair` | _(deleted)_ | +| `unpair` | _(deleted)_ | +| `triple` | _(deleted)_ | +| `untriple` | _(deleted)_ | +| `tuple4` | _(deleted)_ | +| `untuple4` | _(deleted)_ | +| `second` | _(deleted)_ | +| `third` | _(deleted)_ | +| `fourth` | _(deleted)_ | +| `pair_first` | _(deleted)_ | +| `pair_second` | _(deleted)_ | +| `triple_first` | _(deleted)_ | +| `triple_second` | _(deleted)_ | +| `triple_third` | _(deleted)_ | +| `minmax` | `minMax` | +| `now` | `blockchain.now` | +| `my_address` | `contract.getAddress` | +| `get_balance + pair_first` | `contract.getOriginalBalance` | +| `cur_lt` | `blockchain.logicalTime` | +| `block_lt` | `blockchain.currentBlockLogicalTime` | +| `cell_hash(c)` | `c.hash()` | +| `slice_hash(s)` | `s.hash()` | +| `string_hash(s)` | `s.bitsHash()` | +| `check_signature` | `isSignatureValid` | +| `check_data_signature` | `isSliceSignatureValid` | +| `compute_data_size(c)` | `c.calculateSizeStrict()` | +| `slice_compute_data_size(s)` | `s.calculateSizeStrict()` | +| `compute_data_size?(c)` | `c.calculateSize()` | +| `slice_compute_data_size?(s)` | `s.calculateSize()` | +| `~dump` | `debug.print` | +| `~strdump` | `debug.printString` | +| `dump_stack` | `debug.dumpStack` | +| `get_data` | `contract.getData` | +| `set_data` | `contract.setData` | +| `get_c3` | `getTvmRegisterC3` | +| `set_c3` | `setTvmRegisterC3` | +| `bless` | `transformSliceToContinuation` | +| `accept_message` | `acceptExternalMessage` | +| `set_gas_limit` | `setGasLimit` | +| `buy_gas` | _(deleted)_ | +| `commit` | `commitContractDataAndActions` | +| `divmod` | `divMod` | +| `moddiv` | `modDiv` | +| `muldiv` | `mulDivFloor` | +| `muldivr` | `mulDivRound` | +| `muldivc` | `mulDivCeil` | +| `muldivmod` | `mulDivMod` | +| `begin_parse` | `beginParse` | +| `end_parse(s)` | `s.assertEnd()` | +| `first_bits(s)` | `s.getFirstBits()` | +| `skip_last_bits(s)` | `s.removeLastBits()` | +| `slice_last(s)` | `s.getLastBits()` | +| `cell_depth(c)` | `c.depth()` | +| `slice_refs(s)` | `s.remainingRefsCount()` | +| `slice_bits(s)` | `s.remainingBitsCount()` | +| `slice_bits_refs(s)` | `s.remainingBitsAndRefsCount()` | +| `slice_empty?(s)` | `s.isEmpty()` | +| `slice_data_empty?(s)` | `s.isEndOfBits()` | +| `slice_refs_empty?(s)` | `s.isEndOfRefs()` | +| `slice_depth(s)` | `s.depth()` | +| `equal_slice_bits(a,b)` | `a.bitsEqual(b)` | +| `builder_refs(b)` | `b.refsCount()` | +| `builder_bits(b)` | `b.bitsCount()` | +| `builder_depth(b)` | `b.depth()` | +| `begin_cell` | `beginCell` | +| `end_cell` | `endCell` | +| `parse_addr` | _(deleted)_ | +| `parse_std_addr` | use `address` type | +| `parse_var_addr` | _(deleted)_ | +| `config_param` | `blockchain.configParam` | +| `raw_reserve` | `reserveToncoinsOnBalance` | +| `raw_reserve_extra` | `reserveExtraCurrenciesOnBalance` | +| `send_raw_message` | use `createMessage` | +| `set_code` | `contract.setCodePostponed` | +| `random` | `random.uint256` | +| `rand` | `random.range` | +| `get_seed` | `random.getSeed` | +| `set_seed` | `random.setSeed` | +| `randomize` | `random.initializeBy` | +| `randomize_lt` | `random.initialize` | +| `dump` | `debug.print` | +| `strdump` | `debug.printString` | +| `dump_stk` | `debug.dumpStack` | +| `empty_list` | `createEmptyList` | +| `cons` | `listPrepend` | +| `uncons` | `listSplit` | +| `list_next` | `listNext` | +| `car` | `listGetHead` | +| `cdr` | `listGetTail` | +| `new_dict` | `createEmptyMap` | +| `dict_empty?(d)` | `m.isEmpty` | +| `pfxdict_get?` | `prefixDictGet` | +| `pfxdict_set?` | `prefixDictSet` | +| `pfxdict_delete?` | `prefixDictDelete` | + +Lisp-style lists require an explicit import (an IDE inserts it automatically): + +```tolk +import "@stdlib/lisp-lists" +``` + +## Mutating functions + +In FunC, `x~method` mutates, whereas `x.method` returns a copy. +In Tolk, methods are called via dot. A method may (or may not) mutate the object. + +| FunC | Tolk | +| ---------------------------------- | ----------------------------------------- | +| `int n = cs~load_uint(32);` | `var n = cs.loadUint(32);` | +| `var (cs2, n) = cs.load_uint(32);` | `var cs2 = cs; var n = cs2.loadUint(32);` | + +- If `cs~load_uint(…)` was used, `cs.loadUint(…)` behaves identically. +- If `cs.load_uint(…)` was used to obtain a **copy**, a different approach is required. [Read about mutability](/languages/tolk/syntax/mutability). + +## Added functions + +Tolk provides much more capabilities out of the box. See [Tolk stdlib](/languages/tolk/features/standard-library). + +The standard library is split into multiple files. +Functions from `common.tolk` are always available (most of FunC's analogues are, actually), but those from other files require an explicit import. + +```tolk +import "@stdlib/gas-payments" + +// now `calculateGasFee()` and other symbols are visible +``` diff --git a/languages/tolk/from-func/stdlib.mdx b/languages/tolk/from-func/stdlib.mdx deleted file mode 100644 index 5dfe9470d..000000000 --- a/languages/tolk/from-func/stdlib.mdx +++ /dev/null @@ -1,298 +0,0 @@ ---- -title: "Tolk vs FunC: standard library" -sidebarTitle: "Standard library" ---- - -import { Aside } from '/snippets/aside.jsx'; - -FunC includes a low-level [standard library](/languages/tolk/from-func/stdlib) in the `stdlib.fc` file, containing `asm` functions closely tied to [TVM instructions](/tvm/instructions). - -Tolk provides a standard library based on FunC's with three main differences. - -1. It is split into multiple files: `common.tolk`, `tvm-dicts.tolk`, and others. Functions from `common.tolk` are available, while functions from other files require an import: - - ```tolk - import "@stdlib/tvm-dicts" - - beginCell() // always available - createEmptyDict() // available due to import - ``` - -1. No separate download from GitHub; is required; the library is included in the Tolk distribution. - -1. Tolk has functions and methods which are called using the dot operator. Many global FunC functions became methods of builders, slices, and other types, and can no longer be called as functions. - -## Functions vs methods - -In FunC, there are no methods. All functions are globally scoped. -Any function can be called using the dot operator: - -```func -;; FunC -cell config_param(int x) asm "CONFIGOPTPARAM"; - -config_param(16); ;; ok -16.config_param(); ;; also ok -``` - -Calling `b.end_cell()` invokes the global function `end_cell`. -Since all functions are global, there are no "short methods": - -```func -someTuple.tuple_size(); -;; why not someTuple.size()? because it's a global function: -;; int tuple_size(tuple t) -``` - -**Tolk separates functions and methods**, as in most languages: - -1. Functions cannot be called with the dot operator; only methods can. -1. Methods can have short names without conflicts. - -```tolk -// FunC -someCell.cell_hash(); // or cell_hash(someCell) -someSlice.slice_hash(); - -// Tolk -someCell.hash(); // the only possible -someSlice.hash(); -``` - -## Renamed functions - -In the table below, if the **Required import** column is empty, the function is available without imports. - -Some functions were removed because they can be expressed syntactically or are rarely used in practice. - - - -The table follows the order in which functions appear in `stdlib.fc`. - -| FunC name | Tolk name | Required import | -| ---------------------------- | ------------------------------------ | --------------- | -| `empty_tuple` | `createEmptyTuple` | | -| `t~tpush` | `t.push(v)` | | -| `first(t)` or `t.first()` | `t.first()` | | -| `at(t,i)` or `t.at(i)` | `t.get(i)`, `t.0`, etc. | | -| `touch(v)` | `v.stackMoveToTop()` | tvm-lowlevel | -| `impure_touch` | _(deleted)_ | | -| `single` | _(deleted)_ | | -| `unsingle` | _(deleted)_ | | -| `pair` | _(deleted)_ | | -| `unpair` | _(deleted)_ | | -| `triple` | _(deleted)_ | | -| `untriple` | _(deleted)_ | | -| `tuple4` | _(deleted)_ | | -| `untuple4` | _(deleted)_ | | -| `second` | _(deleted)_ | | -| `third` | _(deleted)_ | | -| `fourth` | _(deleted)_ | | -| `pair_first` | _(deleted)_ | | -| `pair_second` | _(deleted)_ | | -| `triple_first` | _(deleted)_ | | -| `triple_second` | _(deleted)_ | | -| `triple_third` | _(deleted)_ | | -| `minmax` | `minMax` | | -| `now` | `blockchain.now` | | -| `my_address` | `contract.getAddress` | | -| `get_balance + pair_first` | `contract.getOriginalBalance` | | -| `cur_lt` | `blockchain.logicalTime` | | -| `block_lt` | `blockchain.currentBlockLogicalTime` | | -| `cell_hash(c)` | `c.hash()` | | -| `slice_hash(s)` | `s.hash()` | | -| `string_hash(s)` | `s.bitsHash()` | | -| `check_signature` | `isSignatureValid` | | -| `check_data_signature` | `isSliceSignatureValid` | | -| `compute_data_size(c)` | `c.calculateSizeStrict()` | | -| `slice_compute_data_size(s)` | `s.calculateSizeStrict()` | | -| `c.compute_data_size?()` | `c.calculateSize()` | | -| `slice_compute_data_size?()` | `s.calculateSize()` | | -| `~dump` | `debug.print` | | -| `~strdump` | `debug.printString` | | -| `dump_stack` | `debug.dumpStack` | | -| `get_data` | `contract.getData` | | -| `set_data` | `contract.setData` | | -| `get_c3` | `getTvmRegisterC3` | tvm-lowlevel | -| `set_c3` | `setTvmRegisterC3` | tvm-lowlevel | -| `bless` | `transformSliceToContinuation` | tvm-lowlevel | -| `accept_message` | `acceptExternalMessage` | | -| `set_gas_limit` | `setGasLimit` | | -| `buy_gas` | _(deleted)_ | | -| `commit` | `commitContractDataAndActions` | | -| `divmod` | `divMod` | | -| `moddiv` | `modDiv` | | -| `muldiv` | `mulDivFloor` | | -| `muldivr` | `mulDivRound` | | -| `muldivc` | `mulDivCeil` | | -| `muldivmod` | `mulDivMod` | | -| `begin_parse` | `beginParse` | | -| `end_parse(s)` | `s.assertEnd()` | | -| `load_ref` | `loadRef` | | -| `preload_ref` | `preloadRef` | | -| `load_int` | `loadInt` | | -| `load_uint` | `loadUint` | | -| `preload_int` | `preloadInt` | | -| `preload_uint` | `preloadUint` | | -| `load_bits` | `loadBits` | | -| `preload_bits` | `preloadBits` | | -| `load_grams` | `loadCoins` | | -| `load_coins` | `loadCoins` | | -| `skip_bits` | `s.skipBits` | | -| `first_bits` | `getFirstBits` | | -| `skip_last_bits` | `removeLastBits` | | -| `slice_last` | `getLastBits` | | -| `load_dict` | `loadDict` | | -| `preload_dict` | `preloadDict` | | -| `skip_dict` | `skipDict` | | -| `load_maybe_ref` | `loadMaybeRef` | | -| `preload_maybe_ref` | `preloadMaybeRef` | | -| `cell_depth(c)` | `c.depth()` | | -| `slice_refs(s)` | `s.remainingRefsCount()` | | -| `slice_bits(s)` | `s.remainingBitsCount()` | | -| `slice_bits_refs(s)` | `s.remainingBitsAndRefsCount()` | | -| `slice_empty?(s)` | `s.isEmpty()` | | -| `slice_data_empty?(s)` | `s.isEndOfBits()` | | -| `slice_refs_empty?(s)` | `s.isEndOfRefs()` | | -| `slice_depth(s)` | `s.depth()` | | -| `equal_slice_bits(a,b)` | `a.bitsEqual(b)` | | -| `builder_refs(b)` | `b.refsCount()` | | -| `builder_bits(b)` | `b.bitsCount()` | | -| `builder_depth(b)` | `b.depth()` | | -| `begin_cell` | `beginCell` | | -| `end_cell` | `endCell` | | -| `store_ref` | `storeRef` | | -| `store_uint` | `storeUint` | | -| `store_int` | `storeInt` | | -| `store_slice` | `storeSlice` | | -| `store_grams` | `storeCoins` | | -| `store_coins` | `storeCoins` | | -| `store_dict` | `storeDict` | | -| `store_maybe_ref` | `storeMaybeRef` | | -| `store_builder` | `storeBuilder` | | -| `load_msg_addr` | `loadAddress` | | -| `parse_addr` | _(deleted)_ | | -| `parse_std_addr` | `parseStandardAddress` | | -| `parse_var_addr` | _(deleted)_ | | -| `config_param` | `blockchain.configParam` | | -| `raw_reserve` | `reserveToncoinsOnBalance` | | -| `raw_reserve_extra` | `reserveExtraCurrenciesOnBalance` | | -| `send_raw_message` | `sendRawMessage` | | -| `set_code` | `contract.setCodePostponed` | | -| `random` | `random.uint256` | | -| `rand` | `random.range` | | -| `get_seed` | `random.getSeed` | | -| `set_seed` | `random.setSeed` | | -| `randomize` | `random.initializeBy` | | -| `randomize_lt` | `random.initialize` | | -| `dump` | `debug.print` | | -| `strdump` | `debug.printString` | | -| `dump_stk` | `debug.dumpStack` | | -| `empty_list` | `createEmptyList` | lisp-lists | -| `cons` | `listPrepend` | lisp-lists | -| `uncons` | `listSplit` | lisp-lists | -| `list_next` | `listNext` | lisp-lists | -| `car` | `listGetHead` | lisp-lists | -| `cdr` | `listGetTail` | lisp-lists | -| `new_dict` | `createEmptyMap` | | -| `dict_empty?(d)` | `m.isEmpty` | | -| `pfxdict_get?` | `prefixDictGet` | tvm-dicts | -| `pfxdict_set?` | `prefixDictSet` | tvm-dicts | -| `pfxdict_delete?` | `prefixDictDelete` | tvm-dicts | -| `idict_set_ref` | use native maps | | -| `udict_set_ref` | use native maps | | -| `idict_get_ref` | use native maps | | -| `idict_get_ref?` | use native maps | | -| `udict_get_ref?` | use native maps | | -| `idict_set_get_ref` | use native maps | | -| `udict_set_get_ref` | use native maps | | -| `idict_delete?` | use native maps | | -| `udict_delete?` | use native maps | | -| `idict_get?` | use native maps | | -| `udict_get?` | use native maps | | -| `idict_delete_get?` | use native maps | | -| `udict_delete_get?` | use native maps | | -| `udict_set` | use native maps | | -| `idict_set` | use native maps | | -| `dict_set` | use native maps | | -| `udict_add?` | use native maps | | -| `udict_replace?` | use native maps | | -| `idict_add?` | use native maps | | -| `idict_replace?` | use native maps | | -| `udict_set_builder` | use native maps | | -| `idict_set_builder` | use native maps | | -| `dict_set_builder` | use native maps | | -| `udict_add_builder?` | use native maps | | -| `udict_replace_builder?` | use native maps | | -| `idict_add_builder?` | use native maps | | -| `idict_replace_builder?` | use native maps | | -| `udict_delete_get_min` | use native maps | | -| `idict_delete_get_min` | use native maps | | -| `dict_delete_get_min` | use native maps | | -| `udict_delete_get_max` | use native maps | | -| `idict_delete_get_max` | use native maps | | -| `dict_delete_get_max` | use native maps | | -| `udict_get_min?` | use native maps | | -| `udict_get_max?` | use native maps | | -| `udict_get_min_ref?` | use native maps | | -| `udict_get_max_ref?` | use native maps | | -| `idict_get_min?` | use native maps | | -| `idict_get_max?` | use native maps | | -| `idict_get_min_ref?` | use native maps | | -| `idict_get_max_ref?` | use native maps | | -| `udict_get_next?` | use native maps | | -| `udict_get_nexteq?` | use native maps | | -| `udict_get_prev?` | use native maps | | -| `udict_get_preveq?` | use native maps | | -| `idict_get_next?` | use native maps | | -| `idict_get_nexteq?` | use native maps | | -| `idict_get_prev?` | use native maps | | -| `idict_get_preveq?` | use native maps | | -| `udict::delete_get_min` | use native maps | | -| `idict::delete_get_min` | use native maps | | -| `dict::delete_get_min` | use native maps | | -| `udict::delete_get_max` | use native maps | | -| `idict::delete_get_max` | use native maps | | -| `dict::delete_get_max` | use native maps | | - -## Added functions - -Some functions from FunC are missing in Tolk's standard library, but it works well for everyday tasks. - -Tolk is evolving, and its standard library is continually updated over time. Check the `tolk-stdlib/` folder in the [source code](https://github.com/ton-blockchain/ton/tree/master/crypto/smartcont/tolk-stdlib). In addition to functions, some constants are added, such as `SEND_MODE_*` and `RESERVE_MODE_*`. - -All of these functions are wrappers over TVM assembler instructions. Missing functionality can be implemented by manually wrapping any TVM instruction. - -## Mutating functions - -Many FunC functions that used the `~` tilde now mutate the object in Tolk rather than returning a copy. - -| FunC | Tolk | -| ------------------------------- | ------------------------------ | -| `int flags = cs~load_uint(32);` | `var flags = cs.loadUint(32);` | -| ... | ... | - -- If `cs~load_uint(…)` was used, use `cs.loadUint(…)` and everything works. -- If `cs.load_uint(…)` was used to get a **copy**, a different approach is needed. [Read about mutability](/languages/tolk/from-func/mutability). - -## How the embedded stdlib works - -All standard library functions are available out of the box. Non-common functions require an `import`, but no external downloads are needed. - -Here's how it works: - -1. The Tolk compiler locates the stdlib folder by searching predefined paths relative to an executable binary. - - For example, when the Tolk compiler is launched from a system-installed package, e.g., `/usr/bin/tolk`, the stdlib is located in `/usr/share/ton/smartcont`. - - For custom installations, the `TOLK_STDLIB` environment variable can be set. This is standard practice for compilers. - -1. The WASM wrapper [tolk-js](https://github.com/ton-blockchain/tolk-js) also includes the stdlib, so all stdlib functions are available out of the box when using tolk-js directly or through blueprint. - -1. JetBrains and VS Code IDE plugins automatically locate the stdlib to provide auto-completion. - - Blueprint installs tolk-js automatically, creating the `node_modules/@ton/tolk-js/` folder in the project structure. - - It contains `common.tolk`, `tvm-dicts.tolk`, and other stdlib files. diff --git a/languages/tolk/from-func/tolk-vs-func.mdx b/languages/tolk/from-func/tolk-vs-func.mdx new file mode 100644 index 000000000..58cb965a8 --- /dev/null +++ b/languages/tolk/from-func/tolk-vs-func.mdx @@ -0,0 +1,457 @@ +--- +title: "Tolk vs FunC" +--- + +import { Aside } from '/snippets/aside.jsx'; + +**FunC** is the first high-level language for writing smart contracts in TON. +For years, it was the only option. +Lots of production code was written in FunC, and it will always be alive on-chain and in developers' hearts. + +**Tolk** replaces FunC with modern syntax, a robust type system, and built-in serialization — while generating efficient assembly code. +Released in 2025, now it is considered the primary language for the TON ecosystem. + +## How to migrate from FunC to Tolk + +1. Scan the list below to get the overall picture. +1. Explore the [tolk-bench repo](https://github.com/ton-blockchain/tolk-bench) as a source of reference contracts. +1. Use the [FunC-to-Tolk converter](/languages/tolk/from-func/converter) to migrate existing projects. + +## Gas benchmarks + +The [tolk-bench repository](https://github.com/ton-blockchain/tolk-bench) compares FunC and Tolk on several TEPs. + +For every metric measured, **gas consumption reduced 30–50%**. Primarily it's a result of the language design. + +## What Tolk and FunC have in common + +Both languages **target into Fift assembler**. +Tolk is not "a wrapper" that transpiles to FunC — it has its own semantic and optimization kernel. + +Both languages **work on TVM** after being compiled to bytecode. +TVM is a stack machine, imposing architectural and runtime restrictions. + +Both languages **have IDE plugins**, although support for Tolk is way better: +JetBrains IDEs, VS Code, Cursor, Windsurf, etc. + +Both languages **are available in blueprint** and other client-side tooling. +Command-line mode is also supported. + +But all _language aspects_ are completely different — a huge list below. + +## List of "Tolk vs FunC" differences + +Tolk and FunC are completely different. +It's even inaccurate to compare them — the difference lies in the design, not in syntax. +Nevertheless, let's try to summarize the details. + +### Tolk reminds TypeScript and Rust + +- FunC: resembles C ("FunC" stands for "functional C") +- Tolk: resembles TypeScript, Rust, and Kotlin + +```tolk +fun sum(a: int, b: int): int { + return a + b; +} +``` + +See: [basic syntax](/languages/tolk/basic-syntax). + +### Tolk has structures + +- FunC: return long unnamed tensors such as `(int, slice, int, int)` +- Tolk: declare a struct, it's the same efficient + +```tolk +struct Demo { + previousValue: int256 + ownerAddress: address + effectsCount: uint32 + totalAmount: coins +} +``` + +See: [structures](/languages/tolk/syntax/structures-fields). + +### Automatic serialization + +- FunC: manual bit-level work with builders and slices +- Tolk: declare a struct and call `fromCell` and `toCell` + +```tolk +struct Point { + x: int8 + y: int8 +} + +fun demo() { + var value: Point = { x: 10, y: 20 }; + + // makes a cell containing "0A14" (hex) + var c = value.toCell(); + // back to { x: 10, y: 20 } + var p = Point.fromCell(c); +} +``` + +Pay attention to the use of `int8`, `uint64`, `coins` — all of them are TVM integers (see [numbers](/languages/tolk/types/numbers)). + +See: [automatic serialization](/languages/tolk/features/auto-serialization). + +### Lazy loading + +- FunC: for optimization, manual juggling with preloads and skips +- Tolk: the `lazy` keyword loads only requested fields skipping the rest + +```tolk +get fun publicKey() { + val st = lazy Storage.load(); + // <-- here "skip 65 bits, preload uint256" is inserted + return st.publicKey +} +``` + +See: [lazy loading](/languages/tolk/features/lazy-loading). + +### The `bool` type + +- FunC: only integers, 'true' is `-1`, 'false' is `0`; `ifnot` +- Tolk: type `bool` and logical operators `&& || !` are supported + +```tolk +if (trustInput || validate(input)) { + // ... +} +``` + +See: [booleans](/languages/tolk/types/booleans). + +### The `address` type + +- FunC: only slices (binary data); parse and compare bits +- Tolk: type `address` with convenient methods and operator `==` + +```tolk +if (in.senderAddress == storage.ownerAddress) { + val workchain = storage.ownerAddress.getWorkchain(); + // ... +} +``` + +See: [address](/languages/tolk/types/address). + +### Null safety + +- FunC: any variable can hold `null`, which may lead to runtime errors +- Tolk: provides nullable types `T?`, null safety, and smart casts + +```tolk +fun checkWithOptional(a: int, b: int?): bool { + if (b == null) { + return checkSingle(a); + } + return b >= 0 && checkDouble(a, b); +} +``` + +See: [nullability](/languages/tolk/types/nullable). + +### Everything else in the type system + +- FunC: several types, the Hindley-Milner type system +- Tolk: a wide range of types, including unions, generics, and enums + +```tolk +struct Container { + element: T? +} + +struct Nothing + +type Wrapper = Nothing | Container +``` + +See: [type system overview](/languages/tolk/types/list-of-types). + +### Methods for any types + +- FunC: global-scope functions only +- Tolk: both functions and methods — for structures and even primitives + +```tolk +// no `self` — static method +fun Point.createZero(): Point { + return { x: 0, y: 0 } +} + +// has `self` — instance method +fun Point.sumCoords(self) { + return self.x + self.y +} + +// even for primitives: cells, integers, tuples, etc. +fun tuple.isEmpty(self) { + return self.size() == 0 +} +``` + +See: [functions and methods](/languages/tolk/syntax/functions-methods). + +### No `impure` keyword + +- FunC: once `impure` is forgotten, a call may be dropped +- Tolk: the compiler does not remove user function calls + +```tolk +fun validate(input: SomeStruct) { + // ... +} +``` + +### No `~tilde` methods + +- FunC: `x~f()` and `x.f()` are different (mutating and not) +- Tolk: only the dot — a single, consistent way to call methods + +```tolk +val delta = someSlice.loadUint(32); // mutates someSlice +val owner = someSlice.loadAddress(); +``` + +See: [mutability](/languages/tolk/syntax/mutability). + +### Native maps over TVM dictionaries + +- FunC: `m~idict_set_builder(1,32,begin_cell().store_uint(10,32))` +- Tolk: `m.set(1, 10)` + +```tolk +var m: map = createEmptyMap(); +m.set(1, 10); +m.addIfNotExists(2, -20); +m.delete(2); // now: [ 1 => 10 ] +``` + +See: [maps](/languages/tolk/types/maps). + +### Modern message handling + +- FunC: `() recv_internal(4 params)` and parse a message cell +- Tolk: `fun onInternalMessage(in)` and use `in.senderAddress`, etc. + +```tolk +fun onInternalMessage(in: InMessage) { + // internal non-bounced messages arrive here + in.senderAddress; + in.originalForwardFee; + // and other fields +} + +fun onBouncedMessage(in: InMessageBounced) { + // bounced messages arrive here +} +``` + +See: [message handling](/languages/tolk/features/message-handling). + +### No `if (op == OP_TRANSFER)` for opcodes + +- FunC: `if-else` to route an incoming message based on `opcode` +- Tolk: use union types and pattern matching + +```tolk +type MyMessage = + | CounterIncBy + | CounterReset + // ... + +fun onInternalMessage(in: InMessage) { + val msg = lazy MyMessage.fromSlice(in.body); + match (msg) { + CounterIncBy => { + // ... + } + CounterReset => { + // ... + } + // ... + } +} +``` + +See: [pattern matching](/languages/tolk/syntax/pattern-matching). + +### No "ignore empty messages" pattern + +- FunC: `recv_internal()` starts with `if (slice_empty?(...))` +- Tolk: just use `else` in match + +```tolk +fun onInternalMessage(in: InMessage) { + val msg = lazy MyMessage.fromSlice(in.body); + match (msg) { + CounterReset => { /* ... */ } + // ... handle all variants of the union + + else => { + // for example: ignore empty messages + if (in.body.isEmpty()) { + return + } + throw 0xFFFF + } + } +} +``` + +See: [lazy matching](/languages/tolk/features/lazy-loading#lazy-matching). + +### Native message composition + +- FunC: `store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)` etc. +- Tolk: `createMessage` that auto-detects body ref or not + +```tolk +val reply = createMessage({ + bounce: BounceMode.NoBounce, + value: ton("0.05"), + dest: senderAddress, + body: RequestedInfo { ... } +}); +reply.send(SEND_MODE_REGULAR); +``` + +See: [message sending](/languages/tolk/features/message-sending). + +### Native deployment and `StateInit` + +- FunC: manually pack contract's code and data according to TL-B +- Tolk: `createMessage` auto-computes destination + +```tolk +val deployMsg = createMessage({ + // address auto-calculated, code+data auto-attached + dest: { + stateInit: { + code: contractCodeCell, + data: emptyStorage.toCell(), + }, + // optionally control workchains and sharding + } +}); +``` + +See: [message sending](/languages/tolk/features/message-sending). + +### op::increase is not a valid identifier + +- FunC: allows any symbols in identifiers, even `var 2+2 = ...` is ok +- Tolk: alphanumeric identifiers, `2+2` is `4`, as expected + +```tolk +const OP_INCREASE = 0x12345678 +``` + +See: [variables](/languages/tolk/syntax/variables). + +### Small functions are inlined automatically + +- FunC: prefer larger functions for reduced gas consumption +- Tolk: the compiler auto-inlines functions with zero overhead + +```tolk +fun int.zero() { + return 0 +} + +fun int.inc(mutate self, byValue: int = 1): self { + self += byValue; + return self; +} + +fun main() { + return int.zero().inc().inc() +} +``` + +is reduced to "return 2" in assembler: + +```fift +main() PROC:<{ + 2 PUSHINT +}> +``` + +Note: `inline` modifier in FunC works at the Fift level, it's sub-optimal due to extra stack permutations. +In Tolk, inlining works at the compiler level and is combined with constant folding. + +See: [compiler optimizations](/languages/tolk/features/compiler-optimizations). + +### Consecutive `builder.storeUint` are merged + +- FunC: manually combine constant stores into `b.storeUint(0x18,6)` +- Tolk: merges `b.storeUint(...).storeUint(...)` if constant + +```tolk +b.storeUint(0, 1) + .storeUint(1, 1) + .storeUint(1, 1) + .storeUint(0, 1) + .storeUint(0, 2) +``` + +is translated to just + +```fift +b{011000} STSLICECONST +``` + +See: [compiler optimizations](/languages/tolk/features/compiler-optimizations). + +### Standard library redesigned + +Functions from `stdlib.fc` now use longer, descriptive naming: + +| FunC | Tolk | +| :------------------: | :-------------------------------: | +| `cur_lt()` | `blockchain.logicalTime()` | +| `car(l)` | `listGetHead(l)` | +| `raw_reserve(coins)` | `reserveToncoinsOnBalance(coins)` | +| `~dump(x)` | `debug.print(x)` | + +Many global-scope functions became methods for primitives: + +| FunC | Tolk | +| :------------------------: | :-----------------: | +| `s.slice_hash()` | `s.hash()` | +| `equal_slices_bits(a, b)` | `a.bitsEqual(b)` | +| `t.tuple_len()` | `t.size()` | +| `t~tpush(triple(x, y, z))` | `t.push([x, y, z])` | + +String postfixes like `"..."c` became built-in functions: + +| FunC | Tolk | +| :------: | :-----------------------: | +| `"..."c` | `stringCrc32("...")` | +| `"..."H` | `stringSha256("...")` | +| `"..."h` | `stringSha256_32("...")` | +| `"..."a` | `address("...")` | +| `"..."s` | `stringHexToSlice("...")` | +| `"..."u` | `stringToBase256("...")` | + +See: [differences in a standard library](/languages/tolk/from-func/stdlib-fc). + +### ... and of course — assembler functions + +Regardless of being a high-level language, Tolk provides all low-level capabilities. +The code can still be written in a "FunC-style" with manual builders and slices, +exotic TVM instructions can still be used. + +```tolk +@pure +fun incThenNegate(v: int): int + asm "INC" "NEGATE" +``` + +See: [assembler functions](/languages/tolk/features/asm-functions). diff --git a/languages/tolk/from-func/tolk-vs-tlb.mdx b/languages/tolk/from-func/tolk-vs-tlb.mdx new file mode 100644 index 000000000..25010c4ba --- /dev/null +++ b/languages/tolk/from-func/tolk-vs-tlb.mdx @@ -0,0 +1,79 @@ +--- +title: "Tolk vs TL-B" +--- + +import { Aside } from '/snippets/aside.jsx'; + +Experienced developers coming from FunC tend to ask questions like: + +- _"What's the analogue of XXX in TL-B?"_ +- _"How to generate a TL-B schema for a Tolk structure?"_ +- etc. + +Such questions lead in the wrong direction — because the Tolk **type system** is designed as a **replacement for TL-B**. + +## Stop thinking in TL-B terms + + + +Why is TL-B so widely used in TON today? + +**Because contracts written in FunC are not declarative.** +They are low-level imperative programs that manually parse cells and slices. +TL-B exists to compensate for this — to provide a declarative description: + +- which inputs are valid, +- which shape is expected, +- how storage is structured, etc. + +**In Tolk, messages and storage are described directly in structs.** +This description _is exactly what TL-B used to provide_ — but serialized automatically. + + + +Moreover, **TL-B and the Tolk type system are not equivalent**, even if they look similar at first glance. + +Similarities include: + +- `intN`, `uintN`, `bitsN` +- `Maybe` (nullable), `Either` (a two-component union) +- multiple constructors (declared structs + prefixes + unions) +- cells and typed cells + +But the differences are essential. + +TL-B supports the following (not expressible in Tolk): + +- `~tilde` +- `{conditions}` +- dynamic `## n` + +Tolk supports the following (not expressible in TL-B): + +- type aliases +- enums +- inline unions (auto-generated prefix trees) +- tensors +- custom `packToBuilder` and `unpackFromSlice` +- `address?` as "internal or none" (not "maybe T") +- further language improvements, such as namespaces or modules + +The conclusion is simple: **all the tooling around Tolk will not involve TL-B**. + +TL-B is excellent for describing blockchain internals like `block.tlb` — but not for contracts APIs or interaction models. + + diff --git a/languages/tolk/overview.mdx b/languages/tolk/overview.mdx index c29669a4f..6e0aeee1d 100644 --- a/languages/tolk/overview.mdx +++ b/languages/tolk/overview.mdx @@ -12,7 +12,7 @@ This page introduces Tolk's _core features, explains how it compares to FunC, an 1. Clean, expressive syntax inspired by TypeScript and Rust. 1. Built-in primitives for TON, including structures, auto‑packing to/from cells, pattern matching, and first-class message handling. 1. Strong static type system with _null safety, type aliases, union types, and generics_ for safer, clearer code. -1. Gas‑efficient by design — see [benchmarks](https://github.com/ton-blockchain/tolk-bench) and [comparison](/languages/tolk/from-func/in-short); features like **[Lazy loading](/languages/tolk/features/lazy-loading)** can reduce costs. +1. Gas‑efficient by design — see [benchmarks](https://github.com/ton-blockchain/tolk-bench) and [comparison](/languages/tolk/from-func/tolk-vs-func); features like **[Lazy loading](/languages/tolk/features/lazy-loading)** can reduce costs. 1. Low-level control is available when needed, with direct access to the TVM stack and instructions. ## Getting started with Tolk @@ -29,8 +29,8 @@ If you've written contracts in FunC, migrating to Tolk is straightforward — an 1. Try building a basic contract in Tolk — for example, a counter using `npm create ton@latest`. You'll quickly notice the shift from stack logic to expressive constructs with structured types, unions, pattern matching, `toCell()`, and more. 1. Explore the [Tolk vs FunC benchmarks](https://github.com/ton-blockchain/tolk-bench). These are real-world contracts (Jetton, NFT, Wallet, etc.) migrated from FunC — same logic, but expressed in a cleaner, more expressive style. -1. Read [Tolk vs FunC: in short](/languages/tolk/from-func/in-short) for a quick comparison of syntax and concepts. -1. Use the [FunC-to-Tolk converter](https://github.com/ton-blockchain/convert-func-to-tolk) to migrate existing codebases incrementally. +1. Read [Tolk vs FunC](/languages/tolk/from-func/tolk-vs-func) for a quick comparison of syntax and concepts. +1. Use the [FunC-to-Tolk converter](/languages/tolk/from-func/converter) to migrate existing codebases incrementally. ## Example of a basic Tolk smart contract: @@ -112,4 +112,4 @@ If you encounter an issue, please connect with the developer community on [TON D ## See also -- [Tolk vs FunC: in short](/languages/tolk/from-func/in-short) +- [Tolk vs FunC](/languages/tolk/from-func/tolk-vs-func) From 4854fdfc0e18bcb4d6ca15697e6a68f45ffd4dcd Mon Sep 17 00:00:00 2001 From: tolk-vm Date: Sun, 23 Nov 2025 18:22:22 +0400 Subject: [PATCH 07/11] [Tolk] New docs: idioms and conventions --- docs.json | 1 + languages/tolk/features/jetton-payload.mdx | 175 ++++++ languages/tolk/idioms-conventions.mdx | 588 ++++++++++++++++++++- languages/tolk/syntax/imports.mdx | 47 +- 4 files changed, 751 insertions(+), 60 deletions(-) create mode 100644 languages/tolk/features/jetton-payload.mdx diff --git a/docs.json b/docs.json index a477ec9c1..579391128 100644 --- a/docs.json +++ b/docs.json @@ -445,6 +445,7 @@ "languages/tolk/features/message-sending", "languages/tolk/features/auto-serialization", "languages/tolk/features/lazy-loading", + "languages/tolk/features/jetton-payload", "languages/tolk/features/standard-library", "languages/tolk/features/asm-functions", "languages/tolk/features/compiler-optimizations" diff --git a/languages/tolk/features/jetton-payload.mdx b/languages/tolk/features/jetton-payload.mdx new file mode 100644 index 000000000..efcf7b2dc --- /dev/null +++ b/languages/tolk/features/jetton-payload.mdx @@ -0,0 +1,175 @@ +--- +title: "Forward payload in jettons" +sidebarTitle: "Payload in jettons" +--- + +import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from "/snippets/tolk-highlight.jsx"; + + + +By convention, a jetton transfer may have some `forwardPayload` attached — to provide custom data for a transaction's recipient. +Below are several approaches to represent a "payload" depending on particular needs. + + + +## The schema of a jetton payload + +By definition, the TL-B format is `(Either Cell ^Cell)`: one bit plus the corresponding data depending on the bit: + +- bit '0' means: payload = "all the next bits/refs" (_inline payload_) +- bit '1' means: payload = "the next ref" (_ref payload_) + +Since it may be inline, it's always positioned in the end of a message. + +However, many **existing jetton implementations do not follow the schema**. + +- Some implementations allow empty data to be passed (no bits at all). + It is invalid, because at least one bit must exist. + An empty payload should actually be encoded as bit '0': empty inline payload. +- Some implementations do not check that no extra data is left after bit '1'. +- Error codes differ between various implementations. + +## Canonical typing of the payload + +TL-B `(Either X Y)` is essentially a union type `X | Y` in Tolk. Hence, this will work: + +```tolk +struct Transfer { + // ... + forwardPayload: RemainingBitsAndRefs | cell +} +``` + +It will be parsed/serialized exactly as it should: either bit '0' + inline data or bit '1' + ref. + +It is convenient for assignment and client metadata, but **has some disadvantages**: + +- if you trust the input and need just to proxy data as-is, this approach consumes more gas due to runtime branching (IF bit '0', etc.) +- it does not check, that no extra data is left after bit '1' + +## Questions to ask yourself + +To choose the correct typing, **answer the following questions**: + +- Do you need validation or just proxy any data as-is? +- Do you need custom error codes while validating? +- Do you need to assign it dynamically or just to carry it forward? + +## Various approaches depending on answers + +To proxy any data **without validation**, shape the "payload" as "all the rest": + +```tolk +struct Transfer { + // ... + forwardPayload: RemainingBitsAndRefs +} +``` + +To add **validation of a canonical union** `RemainingBitsAndRefs | cell`, check that no extra data exists after a _ref payload_: + +```tolk +struct Transfer { + // ... + forwardPayload: RemainingBitsAndRefs | cell + mustBeEmpty: RemainingBitsAndRefs +} + +fun Transfer.validatePayload(self) { + // if extra data exists, throws 9 + self.mustBeEmpty.assertEnd() + // if no bits at all, failed with 9 beforehand, + // because the union could not be loaded +} +``` + +If **gas consumption is critical**, but validation is required — it's cheaper not to allocate unions on the stack, +but to load a slice, validate it, and keep a slice for further serialization: + +```tolk +struct Transfer { + // ... + forwardPayload: ForwardPayload +} + +type ForwardPayload = RemainingBitsAndRefs + +// validate TL/B `(Either Cell ^Cell)` +fun ForwardPayload.checkIsCorrectTLBEither(self) { + var mutableCopy = self; + // throw 9 if no bits at all ("maybe ref" loads one bit) + if (mutableCopy.loadMaybeRef() != null) { + // if ^Cell, throw 9 if other data exists + mutableCopy.assertEnd() + } +} +``` + +To throw **custom error codes** (not errCode 9 "cell underflow"), even calling `loadMaybeRef()` like above is discouraged. +The solution can be: + +```tolk +type ForwardPayload = RemainingBitsAndRefs + +struct (0b0) PayloadInline { + data: RemainingBitsAndRefs +} + +struct (0b1) PayloadRef { + refData: cell + rest: RemainingBitsAndRefs +} + +type PayloadInlineOrRef = PayloadInline | PayloadRef + +// validate TL/B `(Either Cell ^Cell)` +fun ForwardPayload.checkIsCorrectTLBEither(self) { + val p = lazy PayloadInlineOrRef.fromSlice(self); + match (p) { + PayloadInline => { + // okay, valid + } + PayloadRef => { + // valid if nothing besides ref exists + assert (p.rest.isEmpty()) throw ERR_EXTRA_BITS + } + else => { + // both not bit '0' and not bit '1' — empty + throw ERR_EMPTY_PAYLOAD_FIELD + } + } +} +``` + +Keeping a "remainder" is cheaper and allows graceful validation, but it's not convenient +if you need to **assign a payload dynamically**. It's a plain `slice`, holding an encoded union. +For example, creating a _ref payload_ from code having a `cell` requires manual work: + +```tolk +fun createRefPayload(ref: cell) { + // not like this, mismatched types + val err1 = ref; + // not like this, incorrect logic + val err2 = ref.beginParse(); + + // but like this: '1' + ref + val payload = beginCell() + .storeBool(true).storeRef(ref) + .asSlice(); +} +``` + +Of course, `RemainingBitsAndRefs | cell` is **much more convenient for assignment**, but as shown above, +has its own disadvantages. + + diff --git a/languages/tolk/idioms-conventions.mdx b/languages/tolk/idioms-conventions.mdx index 1cd750188..c277c189b 100644 --- a/languages/tolk/idioms-conventions.mdx +++ b/languages/tolk/idioms-conventions.mdx @@ -1,31 +1,591 @@ --- title: "Idioms and conventions" -sidebarTitle: "Idioms and conventions" --- -todo: note that it's not for beginners +import { Aside } from '/snippets/aside.jsx'; +This section summarizes common patterns and conventions used in idiomatic Tolk code. -storage +While [Basic syntax](/languages/tolk/basic-syntax) introduces the language itself, +this page outlines the preferred **ways of expressing ideas** and **best practices** in Tolk. +It may serve as a style reference throughout development. -typed cells +## Auto-serialization instead of slices/builders -handle message +Tolk [type system](/languages/tolk/types/list-of-types) is designed to entirely avoid manual cell parsing. +The presence of `beginCell()` indicates a possibly wrong approach. +All practical use cases in contract interaction are expressed with structures, unions, and references. -send message +```tolk +struct Holder { + owner: address + lastUpdated: uint32 + extra: Cell +} -deploy is send message +fun demo(data: Holder) { + // make a cell with 299 bits and 1 ref + val c = data.toCell(); -use lazy + // unpack it back + val holder = Holder.fromCell(c); +} +``` -no manual builders, use types + -custom serializers +See: [automatic serialization](/languages/tolk/features/auto-serialization). -stringHexToSlice() +## Cell\ — a "cell with known shape" -address() +All data in TON is stored in cells that reference each other. +To express clear data relation, use **typed cells** — `Cell`. -opcodes +Literally, it means: a cell whose contents is `T`: -copy by value, e.g. slices +```tolk +struct Holder { + // ... + extra: Cell +} + +struct ExtraInfo { + someField: int8 + // ... +} + +fun getDeepData(value: Holder) { + // `value.extra` is a reference + // use `load()` to access its contents + val data = value.extra.load(); + return data.someField; +} +``` + +See: [cell references in serialization](/languages/tolk/features/auto-serialization#controlling-cell-references-typed-cells). + +## Not "fromCell", but "lazy fromCell" + +In practice, when reading data from cells, prefer `lazy`: + +- `lazy SomeStruct.fromCell(c)` over `SomeStruct.fromCell(c)` +- `lazy typedCell.load()` over `typedCell.load()` + +The compiler loads only requested fields, skipping the rest. +It reduces gas consumption and bytecode size. + +```tolk +get fun publicKey() { + val st = lazy Storage.load(); + // <-- here "skip 65 bits, preload uint256" is inserted + return st.publicKey +} +``` + +See: [lazy loading](/languages/tolk/features/lazy-loading). + +## Custom serializers — if can't express with types + +Even though the type system is very rich, there still may occur situations where +binary serialization is non-standard. +Tolk allows to declare custom types with arbitrary serialization rules. + +```tolk +type MyString = slice + +fun MyString.packToBuilder(self, mutate b: builder) { + // custom logic +} + +fun MyString.unpackFromSlice(mutate s: slice) { + // custom logic +} +``` + +And just use `MyString` as a regular type — everywhere: + +```tolk +struct Everywhere { + tokenName: MyString + fullDomain: Cell +} +``` + +An interesting example. Imagine a structure which tail is signed: + +```tolk +struct SignedRequest { + signature: uint256 + // hash of all data below is signed + field1: int32 + field2: address? + // ... +} +``` + +The task is to parse it and check signature. +A manual solution is obvious: read uint256, calculate the hash of the remainder, read other fields. + +What about the type system? +Even this complex scenario can be expressed by introducing a synthetic field that is populated on loading: + +```tolk +type HashOfRemainder = uint256 + +struct SignedRequest { + signature: uint256 + restHash: HashOfRemainder // populated on load + field1: int32 + field2: address? + // ... +} + +fun HashOfRemainder.unpackFromSlice(mutate s: slice) { + // `s` is after reading `signature` in our case; + // we don't need to load anything — + // just calculate the hash on the fly + return s.hash() +} + +fun demo(input: slice) { + val req = SignedRequest.fromSlice(input); + assert (req.signature == req.restHash) throw XXX; +} +``` + +See: [serialization of type aliases](/languages/tolk/types/overall-serialization#type-aliases). + +## Contract storage = struct + load + save + +Contract storage is a regular `struct`, serialized into persistent on-chain data. +It is convenient to add `load` and `store` methods: + +```tolk +struct Storage { + counterValue: int64 +} + +fun Storage.load() { + return Storage.fromCell(contract.getData()) +} + +fun Storage.save(self) { + contract.setData(self.toCell()) +} +``` + +See: [contract storage](/languages/tolk/features/contract-storage). + +## Message = struct with a 32-bit prefix + +By convention, every message in TON has an **opcode** — a unique 32-bit number. +In Tolk, every struct can have a "serialization prefix" of arbitrary length. +32-bit prefixes are called opcodes. + +So, every incoming and outgoing message is a struct with a prefix: + +```tolk +struct (0x12345678) CounterIncrement { + // ... +} +``` + + + +See: [structures](/languages/tolk/syntax/structures-fields). + +## Handle a message = structs + union + match + +The suggested pattern to handle messages: + +- each incoming message is a struct with an opcode +- combine these structs into a union +- parse it via `lazy fromSlice` and `match` over variants + +```tolk +struct (0x12345678) CounterIncrement { + incBy: uint32 +} + +struct (0x23456789) CounterReset { + initialValue: int64 +} + +type AllowedMessage = CounterIncrement | CounterReset + +fun onInternalMessage(in: InMessage) { + val msg = lazy AllowedMessage.fromSlice(in.body); + match (msg) { + CounterIncrement => { + // use `msg.incBy` + } + CounterReset => { + // use `msg.initialValue` + } + else => { + // invalid input; a typical reaction is: + // ignore empty messages, "wrong opcode" if not + assert (in.body.isEmpty()) throw 0xFFFF + } + } +} +``` + +Notice `lazy`: it also works with unions and does "lazy match" by a slice prefix. +It's much more efficient than manual parsing an opcode and branching via `if (op == TRANSFER_OP)`. + +See: [handling messages](/languages/tolk/features/message-handling) and [pattern matching](/languages/tolk/syntax/pattern-matching). + +## Send a message = struct + createMessage + +To send a message from contract A to contract B, + +- declare a struct, specify an opcode and fields expected by a receiver +- use `createMessage` + `send` + +```tolk +struct (0x98765432) RequestedInfo { + // ... +} + +fun respond(/* ... */) { + val reply = createMessage({ + bounce: BounceMode.NoBounce, + value: ton("0.05"), + dest: addressOfB, + body: RequestedInfo { + // ... initialize fields + } + }); + reply.send(SEND_MODE_REGULAR); +} +``` + + + +## Deploy another contract = createMessage + +A common case: a minter deploys a jetton wallet, knowing wallet's code and initial state. +This "deployment" is actually sending a message, auto-attaching code+data, and auto-calculating its address: + +```tolk +val deployMsg = createMessage({ + // address auto-calculated, code+data auto-attached + dest: { + stateInit: { + code: jettonWalletCode, + data: emptyWalletStorage.toCell(), + } + } +}); +``` + +A preferred way is to extract generating `stateInit` to a separate function, because +it's used not only to send a message, but also to calculate/validate an address without sending. + +```tolk +fun calcDeployedJettonWallet(/* ... */): AutoDeployAddress { + val emptyWalletStorage: WalletStorage = { + // ... initialize fields from parameters + }; + + return { + stateInit: { + code: jettonWalletCode, + data: emptyWalletStorage.toCell() + } + } +} + +fun demoDeploy() { + val deployMsg = createMessage({ + // address auto-calculated, code+data auto-attached + dest: calcDeployedJettonWallet(...), + // ... + }); + deployMsg.send(mode); +} +``` + +See: [tolk-bench repo](https://github.com/ton-blockchain/tolk-bench) for reference jettons. + +## Shard optimization = createMessage + +"Deploy a contract to a specific shard" is also done with the same technique. +For example, in sharded jettons, a jetton wallet must be deployed to the same shard as the owner's wallet. + +```tolk +val deployMsg = createMessage({ + dest: { + stateInit: { code, data }, + toShard: { + closeTo: ownerAddress, + fixedPrefixLength: 8 + } + } +}); +``` + +Following the guideline above, the task is resolved by adding just a couple lines of code. +Sharding will automatically be supported in `createMessage` and other address calculations. + +```tolk +fun calcDeployedJettonWallet(/* ... */): AutoDeployAddress { + // ... + return { + stateInit: ..., + toShard: { + closeTo: ownerAddress, + fixedPrefixLength: SHARD_DEPTH + } + } +} +``` + +See: [sharding in createMessage](/languages/tolk/features/message-sending#sharding:-deploying-%E2%80%9Cclose-to%E2%80%9D-another-contract). + +## Emitting events/logs to off-chain + +Emitting events and logs "to the outer world" is done via **external messages**. +They are useful for monitoring: being indexed by TON indexers, +they show "a picture of on-chain activity". +These are also messages and also cost gas, but are constructed in a slightly different way. + +1. Create a `struct` to represent the message body +1. Use `createExternalLogMessage` + `send` + +```tolk +struct DepositEvent { + // ... +} + +fun demo() { + val emitMsg = createExternalLogMessage({ + dest: createAddressNone(), + body: DepositEvent { + // ... + } + }); + emitMsg.send(SEND_MODE_REGULAR); +} +``` + +See: [sending external messages](/languages/tolk/features/message-sending#universal-createexternallogmessage). + +## Return a struct from a get method + +When a contract getter (`get fun`) needs to return several values — introduce a structure and return it. +Do not return unnamed tensors like `(int, int, int)`. +Field names provide clear metadata for client wrappers and human readers. + +```tolk +struct JettonWalletDataReply { + jettonBalance: coins + ownerAddress: address + minterAddress: address + jettonWalletCode: cell +} + +get fun get_wallet_data(): JettonWalletDataReply { + return { + jettonBalance: ..., + ownerAddress: ..., + minterAddress: ..., + jettonWalletCode: .., + } +} +``` + +See: [contract getters](/languages/tolk/features/contract-getters). + +## Use assertions to validate user input + +After parsing an incoming message, validate required fields with `assert`: + +```tolk +assert (msg.seqno == storage.seqno) throw E_INVALID_SEQNO; +assert (msg.validUntil > blockchain.now()) throw E_EXPIRED; +``` + +Execution will terminate with some `errCode`, and a contract will be ready to serve the next request. +This is the standard mechanism for reacting on invalid input. + +See: [exceptions](/languages/tolk/syntax/exceptions). + +## Organize a project into several files + +No matter whether a project contains one contract or multiple — split it into files. +Having identical file structure across all projects simplifies navigation: + +- `errors.tolk` with constants or enums +- `storage.tolk` with a storage and helper methods +- `messages.tolk` with incoming/outgoing messages +- `some-contract.tolk` as an entrypoint +- probably, some other + +When several contracts are developed simultaneously, their share the same codebase. +For instance, struct `SomeMessage`, outgoing for contract A, is incoming for contract B. +Or for deployment, contract A should know B's storage to assign `stateInit`. + + + +See: [imports](/languages/tolk/syntax/imports). + +## Prefer methods to functions + +All symbols across different files share the same namespace and must have unique names project-wise. +There are no "modules" or "exports". + +Using methods avoids name collisions: + +```tolk +fun Struct1.validate(self) { /* ... */ } +fun Struct2.validate(self) { /* ... */ } +``` + +Methods are also more convenient: `obj.someMethod()` looks nicer than `someFunction(obj)`: + +```tolk +struct AuctionConfig { + // ... +} + +// NOT +// fun isAuctionConfigInvalid(config: AuctionConfig) +// BUT +fun AuctionConfig.isInvalid(self) { + // ... +} +``` + +Same for static methods: `Auction.createFrom(...)` seems better than `createAuctionFrom(...)`. +A method without `self` is a static one: + +```tolk +fun Auction.createFrom(config: cell, minBid: coins) { + // ... +} +``` + +Static methods may also be used to group various utility functions. +For example, standard functions `blockchain.now()` and others are essentially static methods of an empty struct. + +```tolk +struct blockchain + +fun blockchain.now(): int /* ... */; +fun blockchain.logicalTime(): int /* ... */; +``` + +In large projects, this technique may be used to emulate namespaces. + +See: [functions and methods](/languages/tolk/syntax/functions-methods). + +## How to describe "forward payload" in jettons + +By a standard, a jetton transfer may have `forwardPayload` attached, which TL-B format is `(Either Cell ^Cell)`. +How to describe this in Tolk? + +```tolk +struct Transfer { + // ... + forwardPayload: RemainingBitsAndRefs | cell +} +``` + +The union above is exactly what TL-B's `Either` means. It will work, but has some disadvantages in gas consumption and validation (for a `cell` we also need to check that no extra data exists besides the ref). + +Actually, no universal solution exists — it depends on particular requirements: + +- is the validation needed? +- are custom error codes needed on error? +- should it be convenient to be assigned from code? + +All these cases are described on a separate page. + +See: [forward payload in jettons](/languages/tolk/features/jetton-payload). + +## How to describe "address or none" in a field + +A nullable address — `address?` — is a pattern to say "potentially missing address", sometimes called "maybe address". + +- `null` is "none", serialized as '00' (two zero bits) +- `address` is "internal", serialized as 267 bits: '100' + workchain + hash + +See: [addresses](/languages/tolk/types/address). + +## How to calculate crc32/sha256 at compile-time + +Several built-in functions operate on strings and work at compile-time: + +```tolk +// calculates crc32 of a string +const crc32 = stringCrc32("some_str") + +// calculates sha256 of a string and returns 256-bit integer +const hash = stringSha256("some_crypto_key") + +// and more +``` + +See: [standard library](/languages/tolk/features/standard-library). + +## How to return a string from a contract + +TVM has no strings, it has only slices. +A binary slice must be encoded in a specific way to be parsed and interpreted correctly as a string. + +1. Fixed-size strings via `bitsN` — possible if the size is predefined. +1. Snake strings: portion of data → the rest in a ref cell, recursively. +1. Variable-length encoding via custom serializers. + +See: [strings](/languages/tolk/types/strings). + +## Final suggestion: do not micro-optimize + +Tolk compiler is smart. +It targets "zero overhead": clean, consistent logic must be as efficient as low-level code. +It automatically inlines functions, reduces stack permutations, and does a lot of underlying work +to let a developer focus on business logic. +And it works. +Any attempts to overtrick the compiler result either in negligible or even in negative effect. + +That's why, follow the "**readability-first principle**": + +- use one-line methods without any worries — they are auto-inlined +- use small structures — they are as efficient as raw stack values +- extract constants and variables for clarity +- do not use assembler functions unless being absolutely sure + + + +See: [compiler optimizations](/languages/tolk/features/compiler-optimizations). diff --git a/languages/tolk/syntax/imports.mdx b/languages/tolk/syntax/imports.mdx index 8168de0ab..34f9b55ec 100644 --- a/languages/tolk/syntax/imports.mdx +++ b/languages/tolk/syntax/imports.mdx @@ -52,52 +52,7 @@ Having `fun log()` in multiple files results in a "duplicate declaration" error - Good: `ReturnExcessesBack`, `WalletStorage` - Bad: `Excesses`, `Storage` 1. Group integer constants to enums -1. Prefer methods to global-scope functions - -## Prefer methods to functions - -Methods do not collide because they are attached to distinct types: - -```tolk -fun Struct1.validate(self) { /* ... */ } -fun Struct2.validate(self) { /* ... */ } -``` - -Methods are generally more convenient: `obj.someMethod()` looks nicer than `someFunction(obj)`: - -```tolk -struct AuctionConfig { - // ... -} - -// NOT -// fun isAuctionConfigInvalid(config: AuctionConfig) -// BUT -fun AuctionConfig.isInvalid(self) { - // ... -} -``` - -Same for static methods: `Auction.createFrom(...)` seems better than `createAuctionFrom(...)`. -A method without `self` is a static one: - -```tolk -fun Auction.createFrom(config: cell, minBid: coins) { - // ... -} -``` - -Static methods may also be used to represent various utility functions. -For example, standard functions `blockchain.now()` and others are essentially static methods of an empty struct. - -```tolk -struct blockchain - -fun blockchain.now(): int /* ... */; -fun blockchain.logicalTime(): int /* ... */; -``` - -In large projects, this technique may be used to emulate namespaces. +1. Prefer methods to global-scope functions ([read an idiom](/languages/tolk/idioms-conventions#prefer-methods-to-functions)) ## Symbols that may be repeated across the project From 4c42120f08c2f0d34acefda259a9b22458325326 Mon Sep 17 00:00:00 2001 From: tolk-vm Date: Sun, 23 Nov 2025 18:49:19 +0400 Subject: [PATCH 08/11] [Tolk] New docs: additions to changelog --- languages/tolk/changelog.mdx | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/languages/tolk/changelog.mdx b/languages/tolk/changelog.mdx index d3ed9554d..6b23447ea 100644 --- a/languages/tolk/changelog.mdx +++ b/languages/tolk/changelog.mdx @@ -2,6 +2,8 @@ title: "Changelog" --- +import { Aside } from '/snippets/aside.jsx'; + ## v1.2 1. Breaking change: `address` is now "internal only" ([details and migration](https://github.com/ton-blockchain/ton/pull/1886)) @@ -89,3 +91,26 @@ title: "Changelog" ## v0.6 The first public release. Here are some notes about its origin: + +## Tolk is a fork of FunC, iteratively improved + +For years, FunC was the primary language for TON. +It gave complete control over the TVM — and if you mastered it, it gave you power. +But its Lisp-like syntax and functional style made onboarding difficult for many. + +Initially, the plan was to improve FunC itself. +The goal was simple: to make it more familiar to developers coming from TypeScript, Rust, or Go — without losing any efficiency. + +In 2024, a pull request [FunC v0.5.0](https://github.com/ton-blockchain/ton/pull/1026) was submitted — along with a roadmap for further improvements. + +But instead of merging it, the decision was made: **to fork**. +To leave FunC untouched. As it is. As it always was. +And to create a new language under a completely new name. + +At the TON Gateway in November 2024, Tolk was first announced to the public. The video is available [on YouTube](https://www.youtube.com/watch?v=Frq-HUYGdbI). + +The first released version of Tolk was **v0.6** — a metaphor for the **FunC v0.5** that could have been but never was. + + From 420b8325cb9a9ab4736593dfd342f62b973a34b1 Mon Sep 17 00:00:00 2001 From: tolk-vm Date: Sun, 23 Nov 2025 21:41:40 +0400 Subject: [PATCH 09/11] [Tolk] New docs: overview page (landing) --- contract-dev/first-smart-contract.mdx | 16 +-- languages/func/overview.mdx | 2 +- languages/tolk/overview.mdx | 142 ++++++++++++-------------- 3 files changed, 72 insertions(+), 88 deletions(-) diff --git a/contract-dev/first-smart-contract.mdx b/contract-dev/first-smart-contract.mdx index 773cebc10..1a9cda886 100644 --- a/contract-dev/first-smart-contract.mdx +++ b/contract-dev/first-smart-contract.mdx @@ -118,7 +118,7 @@ First, we need a way to store the counter value. Tolk makes this simple with - TON Blockchain uses Tolk as the official language. Other languages are still available, and legacy codebases exist, but Tolk is the only actively supported language. The FunC compiler, for instance, is no longer maintained. + TON Blockchain uses [Tolk](/languages/tolk) as the official language. Other languages are still available, and legacy codebases exist, but Tolk is the only actively supported language. The FunC compiler, for instance, is no longer maintained. FunC is a domain-specific, statically typed language with C-like syntax designed to write smart contracts on TON. It can be characterized as an intermediate-level language sitting on top of the [Fift](/languages/fift) assembly language. diff --git a/languages/tolk/overview.mdx b/languages/tolk/overview.mdx index 6e0aeee1d..d8bc43fc2 100644 --- a/languages/tolk/overview.mdx +++ b/languages/tolk/overview.mdx @@ -1,115 +1,99 @@ --- -title: "Tolk" +title: "Tolk — the language for TON" sidebarTitle: "Overview" --- -**Tolk** is a next-generation language for developing smart contracts on TON. It replaces FunC with an expressive syntax, a robust type system, and built-in serialization — while generating highly optimized assembly code. +import { Aside } from '/snippets/aside.jsx'; -This page introduces Tolk's _core features, explains how it compares to FunC, and outlines how to get started with the language_ — whether you're migrating from FunC or writing smart contracts from scratch. +**Tolk** is a statically typed language designed for TON smart contracts. -## Why choose Tolk +It provides declarative structures, automatic cell serialization, first-class message handling, and a modern development experience. -1. Clean, expressive syntax inspired by TypeScript and Rust. -1. Built-in primitives for TON, including structures, auto‑packing to/from cells, pattern matching, and first-class message handling. -1. Strong static type system with _null safety, type aliases, union types, and generics_ for safer, clearer code. -1. Gas‑efficient by design — see [benchmarks](https://github.com/ton-blockchain/tolk-bench) and [comparison](/languages/tolk/from-func/tolk-vs-func); features like **[Lazy loading](/languages/tolk/features/lazy-loading)** can reduce costs. -1. Low-level control is available when needed, with direct access to the TVM stack and instructions. - -## Getting started with Tolk - -To get familiar with Tolk, follow the ["Your first smart contract"](/contract-dev/first-smart-contract) article. -It contains required steps for setting up an environment and creating a counter contract from a template, as well as explanation of every step. - -Later on, return to this section and start exploring [Basic syntax](/languages/tolk/basic-syntax), [Type system](/languages/tolk/types/list-of-types), -and other points from the left menu. - -## How to migrate from FunC - -If you've written contracts in FunC, migrating to Tolk is straightforward — and often results in simpler, more maintainable code. - -1. Try building a basic contract in Tolk — for example, a counter using `npm create ton@latest`. You'll quickly notice the shift from stack logic to expressive constructs with structured types, unions, pattern matching, `toCell()`, and more. -1. Explore the [Tolk vs FunC benchmarks](https://github.com/ton-blockchain/tolk-bench). These are real-world contracts (Jetton, NFT, Wallet, etc.) migrated from FunC — same logic, but expressed in a cleaner, more expressive style. -1. Read [Tolk vs FunC](/languages/tolk/from-func/tolk-vs-func) for a quick comparison of syntax and concepts. -1. Use the [FunC-to-Tolk converter](/languages/tolk/from-func/converter) to migrate existing codebases incrementally. - -## Example of a basic Tolk smart contract: +The language compiles to TVM (TON virtual machine) with zero overhead and full control over execution. ```tolk -import "utils" - -struct Storage { - counter: int32 -} - -fun Storage.load() { - return Storage.fromCell(contract.getData()); -} +type AllowedMessage = CounterIncrement | CounterReset fun onInternalMessage(in: InMessage) { - // ... + val msg = lazy AllowedMessage.fromSlice(in.body); + match (msg) { + CounterIncrement => { ... } + CounterReset => { ... } + } } -get fun currentCounter(): int { +get fun currentCounter() { val storage = lazy Storage.load(); return storage.counter; } ``` -
- Equivalent implementation in FunC +## Key features - ```func - #include "utils.fc"; +Tolk offers high-level readability while remaining low-level in nature: - global int ctx_counter; +- a robust type system to express any cell layout in TON +- lazy loading: unused fields are skipped automatically +- unified message composition and deployment +- a gas-efficient compiler targeting the Fift assembler +- friendly tooling and IDE integration - int load_data() impure { - slice cs = get_data().begin_parse(); - ctx_counter = cs~load_uint(32); - } + - () recv_internal(int msg_value, cell msg_full, slice msg_body) impure { - slice cs = msg_full.begin_parse(); - int flags = cs~load_uint(4); - if (flags & 1) { - return (); - } - ... - } +## Tolk is a replacement for FunC + +Tolk started as an evolution of FunC and is now the recommended language for TON smart contracts. + +If you are migrating from FunC: + +- Look through [benchmarks](https://github.com/ton-blockchain/tolk-bench): notice **30–50% lower gas fees**. +- Scan the page [Tolk vs FunC](/languages/tolk/from-func/tolk-vs-func) to get the overall picture. +- Use the [FunC-to-Tolk converter](/languages/tolk/from-func/converter) to migrate existing projects. + +## Quick start + +1. Run the command: + + ```bash + npm create ton@latest + ``` - int currentCounter() method_id { - load_data(); ;; fills global variables - return ctx_counter; - } - ``` -
+1. Enter a project name and choose "simple counter contract". -[FunC to Tolk converter](https://github.com/ton-blockchain/convert-func-to-tolk). +1. Follow the ["Your first smart contract"](/contract-dev/first-smart-contract) article + to get explanations. -## Tooling around Tolk +## IDE support -A set of tools is available to support development with Tolk — including IDE support, language services, and migration utilities. +All major IDEs support syntax highlighting and code completion: -- [JetBrains IDE plugin](https://github.com/ton-blockchain/intellij-ton). Adds support for Tolk, FunC, Fift, and TL-B. -- [VS Code extension](https://marketplace.visualstudio.com/items?itemName=ton-core.vscode-ton). Provides Tolk support and language features. -- [Language server](https://github.com/ton-blockchain/ton-language-server). Works with any LSP-compatible editor. -- [FunC-to-Tolk converter](https://github.com/ton-blockchain/convert-func-to-tolk). Migrates entire projects with a single `npx` command. -- [`tolk-js`](https://github.com/ton-blockchain/tolk-js). WASM wrapper for the Tolk compiler is integrated into Blueprint. +1. **JetBrains IDEs** (WebStorm, CLion, etc.) — via the [plugin](https://github.com/ton-blockchain/intellij-ton)
+ \=> install "TON" from Marketplace, [read more](/contract-dev/ide/jetbrains) -The Tolk compiler itself lives in the `ton` [repository](https://github.com/ton-blockchain/ton). +1. **VS Code** — via the [TON extension](https://marketplace.visualstudio.com/items?itemName=ton-core.vscode-ton)
+ \=> install "TON" from Marketplace, [read more](/contract-dev/ide/vscode) -## Is Tolk production-ready? +1. **Cursor and Windsurf** — via the [OpenVSX registry](https://open-vsx.org/extension/ton-core/vscode-ton)
+ \=> install from the website, [read more](/contract-dev/ide/vscode) -All contracts in the [Tolk vs FunC benchmarks](https://github.com/ton-blockchain/tolk-bench) pass the same test suites as their FunC counterparts — with identical logic and behavior. +1. **Neovim, Vim, Zed, etc.** — via the [language server](https://github.com/ton-blockchain/ton-language-server#other-editors)
+ \=> download an archive, consult the link above -Tolk is under active development, and some edge cases may still occur. Evaluate it for your use case and ensure thorough testing before production deployment. +## Where to go next -Regardless of the language, well-tested code is the key to building reliable smart contracts. +Recommended starting points: -## Issues and contacts +- [Basic syntax](/languages/tolk/basic-syntax) +- [Idioms and conventions](/languages/tolk/idioms-conventions) +- [Type system](/languages/tolk/types/list-of-types) +- [Message handling](/languages/tolk/features/message-handling) -If you encounter an issue, please connect with the developer community on [TON Dev chats](https://t.me/addlist/1r5Vcb8eljk5Yzcy) or create a [GitHub issue](https://github.com/ton-community/ton-docs/issues). +## External resources -## See also +Useful links outside this documentation: -- [Tolk vs FunC](/languages/tolk/from-func/tolk-vs-func) +- [TON Dev chats](https://t.me/addlist/1r5Vcb8eljk5Yzcy) in Telegram +- [@tolk\_lang channel](https://t.me/tolk_lang) in Telegram +- [GitHub](https://github.com/ton-blockchain/ton) for issues and sources From f34329ff43005752d5373853891e1b298e394b1b Mon Sep 17 00:00:00 2001 From: tolk-vm Date: Sun, 23 Nov 2025 22:45:06 +0400 Subject: [PATCH 10/11] [Tolk] Syntax highlighting via Prism.js --- extra.css | 53 ++++++++++ languages/tolk/basic-syntax.mdx | 3 + languages/tolk/features/asm-functions.mdx | 3 + .../tolk/features/auto-serialization.mdx | 3 + .../tolk/features/compiler-optimizations.mdx | 3 + languages/tolk/features/contract-getters.mdx | 3 + languages/tolk/features/contract-storage.mdx | 3 + languages/tolk/features/jetton-payload.mdx | 2 +- languages/tolk/features/lazy-loading.mdx | 3 + languages/tolk/features/message-handling.mdx | 4 + languages/tolk/features/message-sending.mdx | 3 + languages/tolk/features/standard-library.mdx | 3 + languages/tolk/from-func/converter.mdx | 3 + languages/tolk/from-func/stdlib-fc.mdx | 3 + languages/tolk/from-func/tolk-vs-func.mdx | 3 + languages/tolk/idioms-conventions.mdx | 3 + languages/tolk/overview.mdx | 3 + languages/tolk/syntax/conditions-loops.mdx | 4 + languages/tolk/syntax/exceptions.mdx | 3 + languages/tolk/syntax/functions-methods.mdx | 3 + languages/tolk/syntax/imports.mdx | 3 + languages/tolk/syntax/mutability.mdx | 3 + languages/tolk/syntax/operators.mdx | 4 + languages/tolk/syntax/pattern-matching.mdx | 3 + languages/tolk/syntax/structures-fields.mdx | 4 + languages/tolk/syntax/variables.mdx | 3 + languages/tolk/types/address.mdx | 3 + languages/tolk/types/aliases.mdx | 4 + languages/tolk/types/booleans.mdx | 3 + languages/tolk/types/callables.mdx | 3 + languages/tolk/types/cells.mdx | 5 +- languages/tolk/types/enums.mdx | 3 + languages/tolk/types/generics.mdx | 4 + languages/tolk/types/maps.mdx | 3 + languages/tolk/types/nullable.mdx | 8 +- languages/tolk/types/numbers.mdx | 3 + .../tolk/types/overall-serialization.mdx | 3 + languages/tolk/types/overall-tvm-stack.mdx | 3 + languages/tolk/types/strings.mdx | 4 + languages/tolk/types/structures.mdx | 16 ++-- languages/tolk/types/tensors.mdx | 3 + languages/tolk/types/tuples.mdx | 3 + .../tolk/types/type-checks-and-casts.mdx | 3 + languages/tolk/types/unions.mdx | 4 + languages/tolk/types/void-never.mdx | 3 + snippets/tolk-highlight.jsx | 96 +++++++++++++++++++ 46 files changed, 298 insertions(+), 10 deletions(-) create mode 100644 snippets/tolk-highlight.jsx diff --git a/extra.css b/extra.css index a1c8fbe4c..c012101c3 100644 --- a/extra.css +++ b/extra.css @@ -75,3 +75,56 @@ table { div[data-component-part="callout-content"]>.code-block:last-child { margin-bottom: 0; } + +/* + A temporary solution syntax highlighting of Tolk snippets: invoke Prism.js on a client-side. + See `snippets/tolk-highlight.jsx`. + @link https://github.com/ton-org/docs/issues/1473 + */ + +:root { + --tolk-token-comment: #808080; + --tolk-token-type-hint: #D500EC; + --tolk-token-keyword: #0000FF; + --tolk-token-struct: #007EA2; + --tolk-token-variable: #444444; + --tolk-token-attr-name: #808000; + --tolk-token-function: #A82D2D; + --tolk-token-number: #0B9000; + --tolk-token-string: #008000; + --tolk-token-string-bg: #FAF9EF; + --tolk-token-operator: #A0A000; + --tolk-token-punctuation: #808000; + --tolk-token-three-dots: #999999; +} + +.token.comment { color: var(--tolk-token-comment); font-style: italic; } +.token.type-hint { color: var(--tolk-token-type-hint); } +.token.boolean { color: var(--tolk-token-keyword); } +.token.keyword { color: var(--tolk-token-keyword); } +.token.self { color: var(--tolk-token-variable); font-weight: bold; } +.token.attr-name { color: var(--tolk-token-attr-name); } +.token.function { color: var(--tolk-token-function); } +.token.number { color: var(--tolk-token-number); } +.token.string { color: var(--tolk-token-string); background-color: var(--tolk-token-string-bg); } +.token.operator { color: var(--tolk-token-operator); } +.token.punctuation { color: var(--tolk-token-punctuation); } +.token.three-dots { color: var(--tolk-token-three-dots); } +.token.struct { color: var(--tolk-token-struct); } +.token.variable { color: var(--tolk-token-variable); } + +html.dark { + --tolk-token-comment: #808080; + --tolk-token-type-hint: #DF90F8; + --tolk-token-keyword: #D75F02; + --tolk-token-struct: #56C1FF; + --tolk-token-variable: #C5D2E0; + --tolk-token-attr-name: #808000; + --tolk-token-function: #F9B900; + --tolk-token-number: #33A033; + --tolk-token-string: #33A033; + --tolk-token-string-bg: #1B1C1E; + --tolk-token-operator: #A0A000; + --tolk-token-punctuation: #85B2A0; + --tolk-token-three-dots: #777777; +} diff --git a/languages/tolk/basic-syntax.mdx b/languages/tolk/basic-syntax.mdx index 2dc74f69e..fdc5508ce 100644 --- a/languages/tolk/basic-syntax.mdx +++ b/languages/tolk/basic-syntax.mdx @@ -4,6 +4,9 @@ sidebarTitle: "Basic syntax" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + Syntax of Tolk is similar to TypeScript, Rust, and Kotlin. It is designed to be straightforward to read and write. diff --git a/languages/tolk/features/asm-functions.mdx b/languages/tolk/features/asm-functions.mdx index 2a7d460b6..d9662eae3 100644 --- a/languages/tolk/features/asm-functions.mdx +++ b/languages/tolk/features/asm-functions.mdx @@ -3,6 +3,9 @@ title: "Assembler functions" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + Functions in Tolk may be defined using assembler code. It's a low-level feature that requires deep understanding of stack layout, [Fift](/languages/fift/overview), and [TVM](/tvm/overview). diff --git a/languages/tolk/features/auto-serialization.mdx b/languages/tolk/features/auto-serialization.mdx index 88a7f1018..57bdbf562 100644 --- a/languages/tolk/features/auto-serialization.mdx +++ b/languages/tolk/features/auto-serialization.mdx @@ -3,6 +3,9 @@ title: "Automatic serialization" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + All data in TON (messages, storage, etc.) is represented with **cells**. Tolk type system is designed to express cell contents, diff --git a/languages/tolk/features/compiler-optimizations.mdx b/languages/tolk/features/compiler-optimizations.mdx index 5aa31be36..0f52a4a74 100644 --- a/languages/tolk/features/compiler-optimizations.mdx +++ b/languages/tolk/features/compiler-optimizations.mdx @@ -3,6 +3,9 @@ title: "Compiler optimizations" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + Tolk compiler is smart enough to generate optimal bytecode from a clear, idiomatic code. The ideal target is "zero overhead": extracting variables and simple methods should not increase gas consumption. diff --git a/languages/tolk/features/contract-getters.mdx b/languages/tolk/features/contract-getters.mdx index e5b15d843..9bc1b3ef7 100644 --- a/languages/tolk/features/contract-getters.mdx +++ b/languages/tolk/features/contract-getters.mdx @@ -3,6 +3,9 @@ title: "Contract getters" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + Contract getters (or "get methods") are declared using `get fun xxx()`. They typically return data extracted from [storage](/languages/tolk/features/contract-storage). diff --git a/languages/tolk/features/contract-storage.mdx b/languages/tolk/features/contract-storage.mdx index 3938d241e..f71540d93 100644 --- a/languages/tolk/features/contract-storage.mdx +++ b/languages/tolk/features/contract-storage.mdx @@ -3,6 +3,9 @@ title: "Contract storage" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + Contract storage is not "something special". It is a regular `struct`, serialized into persistent blockchain data. diff --git a/languages/tolk/features/jetton-payload.mdx b/languages/tolk/features/jetton-payload.mdx index efcf7b2dc..b7b61c227 100644 --- a/languages/tolk/features/jetton-payload.mdx +++ b/languages/tolk/features/jetton-payload.mdx @@ -4,7 +4,7 @@ sidebarTitle: "Payload in jettons" --- import { Aside } from '/snippets/aside.jsx'; -import { TolkHighlight } from "/snippets/tolk-highlight.jsx"; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; diff --git a/languages/tolk/features/lazy-loading.mdx b/languages/tolk/features/lazy-loading.mdx index e098ececd..953546d9c 100644 --- a/languages/tolk/features/lazy-loading.mdx +++ b/languages/tolk/features/lazy-loading.mdx @@ -3,6 +3,9 @@ title: "Lazy loading" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + Tolk language has a magic feature — the `lazy` keyword. The compiler tracks exactly which fields are accessed, and automatically loads only those, skipping the rest. diff --git a/languages/tolk/features/message-handling.mdx b/languages/tolk/features/message-handling.mdx index 1cf73e37e..28feea6b1 100644 --- a/languages/tolk/features/message-handling.mdx +++ b/languages/tolk/features/message-handling.mdx @@ -2,6 +2,10 @@ title: "Handling messages" --- +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + + Each Tolk contract has specials **entrypoints** — reserved functions to handle various types of messages. From the language perspective, handling an incoming message is just ordinary code. diff --git a/languages/tolk/features/message-sending.mdx b/languages/tolk/features/message-sending.mdx index 30ff21175..1511ad602 100644 --- a/languages/tolk/features/message-sending.mdx +++ b/languages/tolk/features/message-sending.mdx @@ -3,6 +3,9 @@ title: "Sending messages" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + Tolk provides a high-level function `createMessage`. In practice, it's immediately followed by `send`: diff --git a/languages/tolk/features/standard-library.mdx b/languages/tolk/features/standard-library.mdx index 6fcae08b0..0b8e90b58 100644 --- a/languages/tolk/features/standard-library.mdx +++ b/languages/tolk/features/standard-library.mdx @@ -4,6 +4,9 @@ sidebarTitle: "Standard library" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + Tolk provides many functions for everyday use, available out of the box. They are called "standard library", or "stdlib". diff --git a/languages/tolk/from-func/converter.mdx b/languages/tolk/from-func/converter.mdx index 141b090d6..83e8366a6 100644 --- a/languages/tolk/from-func/converter.mdx +++ b/languages/tolk/from-func/converter.mdx @@ -4,6 +4,9 @@ sidebarTitle: "Converter" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + **The converter** may look like magic: any FunC project can be immediately transformed into Tolk with a single command: diff --git a/languages/tolk/from-func/stdlib-fc.mdx b/languages/tolk/from-func/stdlib-fc.mdx index bfcde5c6a..cb84e032c 100644 --- a/languages/tolk/from-func/stdlib-fc.mdx +++ b/languages/tolk/from-func/stdlib-fc.mdx @@ -4,6 +4,9 @@ sidebarTitle: "stdlib.fc mapping" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + FunC contracts depend on the `stdlib.fc` file, which contains `asm` functions closely tied to [TVM instructions](/tvm/instructions). diff --git a/languages/tolk/from-func/tolk-vs-func.mdx b/languages/tolk/from-func/tolk-vs-func.mdx index 58cb965a8..08cf6affb 100644 --- a/languages/tolk/from-func/tolk-vs-func.mdx +++ b/languages/tolk/from-func/tolk-vs-func.mdx @@ -3,6 +3,9 @@ title: "Tolk vs FunC" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + **FunC** is the first high-level language for writing smart contracts in TON. For years, it was the only option. diff --git a/languages/tolk/idioms-conventions.mdx b/languages/tolk/idioms-conventions.mdx index c277c189b..486428103 100644 --- a/languages/tolk/idioms-conventions.mdx +++ b/languages/tolk/idioms-conventions.mdx @@ -3,6 +3,9 @@ title: "Idioms and conventions" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + This section summarizes common patterns and conventions used in idiomatic Tolk code. diff --git a/languages/tolk/overview.mdx b/languages/tolk/overview.mdx index d8bc43fc2..0fdf771bc 100644 --- a/languages/tolk/overview.mdx +++ b/languages/tolk/overview.mdx @@ -4,6 +4,9 @@ sidebarTitle: "Overview" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + **Tolk** is a statically typed language designed for TON smart contracts. diff --git a/languages/tolk/syntax/conditions-loops.mdx b/languages/tolk/syntax/conditions-loops.mdx index 0318c203e..840d6b651 100644 --- a/languages/tolk/syntax/conditions-loops.mdx +++ b/languages/tolk/syntax/conditions-loops.mdx @@ -2,6 +2,10 @@ title: "Conditions and loops" --- +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + + Tolk provides flexible constructs for controlling contract flow. Use `if`, `assert`, and loops to express conditional logic. The `match` expression is a powerful "pattern-matching" feature. diff --git a/languages/tolk/syntax/exceptions.mdx b/languages/tolk/syntax/exceptions.mdx index f5a9092ce..ffc068a4c 100644 --- a/languages/tolk/syntax/exceptions.mdx +++ b/languages/tolk/syntax/exceptions.mdx @@ -3,6 +3,9 @@ title: "Exceptions, throw, assert" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + Tolk allows throwing exceptions and catching them via `try catch`. If an exception is not caught, the program terminates with `errCode`. diff --git a/languages/tolk/syntax/functions-methods.mdx b/languages/tolk/syntax/functions-methods.mdx index 2ce3f7e13..bc19fa9fb 100644 --- a/languages/tolk/syntax/functions-methods.mdx +++ b/languages/tolk/syntax/functions-methods.mdx @@ -3,6 +3,9 @@ title: "Functions and methods" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + Tolk allows declaring functions and methods: diff --git a/languages/tolk/syntax/imports.mdx b/languages/tolk/syntax/imports.mdx index 34f9b55ec..5be462723 100644 --- a/languages/tolk/syntax/imports.mdx +++ b/languages/tolk/syntax/imports.mdx @@ -3,6 +3,9 @@ title: "Imports and name resolution" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + In practice, contract sources are split into multiple files. A project containing multiple contracts shares the same codebase (messages, storage definitions, and related logic). diff --git a/languages/tolk/syntax/mutability.mdx b/languages/tolk/syntax/mutability.mdx index 54bd9ba23..26d832b46 100644 --- a/languages/tolk/syntax/mutability.mdx +++ b/languages/tolk/syntax/mutability.mdx @@ -3,6 +3,9 @@ title: "Mutability" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + Tolk follows **value semantics**: when calling a function, arguments are copied by value. There are no "pointers" or "references to objects". diff --git a/languages/tolk/syntax/operators.mdx b/languages/tolk/syntax/operators.mdx index c64780306..c9656402e 100644 --- a/languages/tolk/syntax/operators.mdx +++ b/languages/tolk/syntax/operators.mdx @@ -2,6 +2,10 @@ title: "Operators" --- +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + + Tolk provides standard operators for integers and booleans. This page lists them briefly, as they closely resemble other common languages. diff --git a/languages/tolk/syntax/pattern-matching.mdx b/languages/tolk/syntax/pattern-matching.mdx index fa206775c..81750b3fd 100644 --- a/languages/tolk/syntax/pattern-matching.mdx +++ b/languages/tolk/syntax/pattern-matching.mdx @@ -3,6 +3,9 @@ title: "Pattern matching" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + Tolk supports a `match` expression for pattern matching. It is a powerful construct that applies both to types and expressions. diff --git a/languages/tolk/syntax/structures-fields.mdx b/languages/tolk/syntax/structures-fields.mdx index b6cc98247..0326e0d7d 100644 --- a/languages/tolk/syntax/structures-fields.mdx +++ b/languages/tolk/syntax/structures-fields.mdx @@ -2,6 +2,10 @@ title: "Structures and fields" --- +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + + Tolk supports structures — similar to TypeScript classes. This page focuses on syntax details only. See also [structures in the type system](/languages/tolk/types/structures). diff --git a/languages/tolk/syntax/variables.mdx b/languages/tolk/syntax/variables.mdx index 0e85ff180..7e3e8a1cc 100644 --- a/languages/tolk/syntax/variables.mdx +++ b/languages/tolk/syntax/variables.mdx @@ -3,6 +3,9 @@ title: "Variables" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + Variables are declared with `val` (immutable) or `var` (mutable). diff --git a/languages/tolk/types/address.mdx b/languages/tolk/types/address.mdx index c479d9f64..3b08ee396 100644 --- a/languages/tolk/types/address.mdx +++ b/languages/tolk/types/address.mdx @@ -3,6 +3,9 @@ title: "Address" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + Every smart contract has an address that is used for all on-chain interactions. When a client sends a message to a contract, the message is effectively sent to its address. diff --git a/languages/tolk/types/aliases.mdx b/languages/tolk/types/aliases.mdx index ef8ef676a..391ec6d29 100644 --- a/languages/tolk/types/aliases.mdx +++ b/languages/tolk/types/aliases.mdx @@ -2,6 +2,10 @@ title: "Type aliases" --- +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + + Tolk supports type aliases, similar to TypeScript and Rust. An alias creates a new name for an existing type and remains fully interchangeable with it. diff --git a/languages/tolk/types/booleans.mdx b/languages/tolk/types/booleans.mdx index 1aebd464c..0d42035da 100644 --- a/languages/tolk/types/booleans.mdx +++ b/languages/tolk/types/booleans.mdx @@ -3,6 +3,9 @@ title: "Booleans" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + Tolk has a `bool` type which can have two values: `true` or `false`. diff --git a/languages/tolk/types/callables.mdx b/languages/tolk/types/callables.mdx index 131140322..98199639c 100644 --- a/languages/tolk/types/callables.mdx +++ b/languages/tolk/types/callables.mdx @@ -3,6 +3,9 @@ title: "Callables" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + Tolk has first-class functions: they can be passed as callbacks. Such values have callable types: `(...ArgsT) -> ReturnT`. diff --git a/languages/tolk/types/cells.mdx b/languages/tolk/types/cells.mdx index 2252ee17e..1ba0c76f2 100644 --- a/languages/tolk/types/cells.mdx +++ b/languages/tolk/types/cells.mdx @@ -3,6 +3,9 @@ title: "Cells, slices, builders" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + In TON, all data is stored in **cells**. Cells opened for reading are called **slices**. @@ -35,7 +38,7 @@ Since one cell can store only 1023 bits, when storage exceeds this limit, the so into multiple cells, so they become referencing each other. ```tolk -struct A { +struct Demo { ref1: cell // untyped ref ref2: Cell // typed ref ref3: Cell? // maybe ref diff --git a/languages/tolk/types/enums.mdx b/languages/tolk/types/enums.mdx index 86daa8952..1f937c11c 100644 --- a/languages/tolk/types/enums.mdx +++ b/languages/tolk/types/enums.mdx @@ -3,6 +3,9 @@ title: "Enums" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + Tolk supports enums, similar to TypeScript and C++ enums. diff --git a/languages/tolk/types/generics.mdx b/languages/tolk/types/generics.mdx index 4349ddc13..c1295fd9f 100644 --- a/languages/tolk/types/generics.mdx +++ b/languages/tolk/types/generics.mdx @@ -3,6 +3,10 @@ title: "Generic structs and aliases" sidebarTitle: "Generics" --- +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + + Generic structs and type aliases exist only at the type level and incur no runtime cost. ```tolk diff --git a/languages/tolk/types/maps.mdx b/languages/tolk/types/maps.mdx index ef18ab6bf..edda08ec7 100644 --- a/languages/tolk/types/maps.mdx +++ b/languages/tolk/types/maps.mdx @@ -4,6 +4,9 @@ sidebarTitle: "Maps" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + Tolk supports `map` — a high‑level type that encapsulates TVM dictionaries: diff --git a/languages/tolk/types/nullable.mdx b/languages/tolk/types/nullable.mdx index 5abe64d72..6086db5b8 100644 --- a/languages/tolk/types/nullable.mdx +++ b/languages/tolk/types/nullable.mdx @@ -3,6 +3,10 @@ title: "Nullable types, null safety" sidebarTitle: "Nullable types" --- +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + + Tolk supports nullable types `T?`, a shorthand for `T | null`. Any type can be made nullable: primitive types, structures, and other composites. `null` cannot be assigned to a non-nullable type. @@ -85,11 +89,11 @@ while (lastCell != null) { Smart casts apply to local variables, structure fields, and tensor/tuple indices. ```tolk -struct A { +struct HasOpt { optionalId: int? } -fun demo(obj: A) { +fun demo(obj: HasOpt) { if (obj.optionalId != null) { // obj.optionalId is `int` here } diff --git a/languages/tolk/types/numbers.mdx b/languages/tolk/types/numbers.mdx index a3e5f50c3..0144f37ca 100644 --- a/languages/tolk/types/numbers.mdx +++ b/languages/tolk/types/numbers.mdx @@ -3,6 +3,9 @@ title: "Numbers" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + Tolk has several types for integers: diff --git a/languages/tolk/types/overall-serialization.mdx b/languages/tolk/types/overall-serialization.mdx index 86a406573..4fbb297ec 100644 --- a/languages/tolk/types/overall-serialization.mdx +++ b/languages/tolk/types/overall-serialization.mdx @@ -4,6 +4,9 @@ sidebarTitle: "Overall: serialization" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + A consolidated summary of how each Tolk type is serialized into TL-B–compatible binary data. diff --git a/languages/tolk/types/overall-tvm-stack.mdx b/languages/tolk/types/overall-tvm-stack.mdx index 059e8bee3..8827fdacf 100644 --- a/languages/tolk/types/overall-tvm-stack.mdx +++ b/languages/tolk/types/overall-tvm-stack.mdx @@ -4,6 +4,9 @@ sidebarTitle: "Overall: TVM stack" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + A consolidated summary of how each Tolk type is represented on the stack. diff --git a/languages/tolk/types/strings.mdx b/languages/tolk/types/strings.mdx index d9d62e2b4..367fa03eb 100644 --- a/languages/tolk/types/strings.mdx +++ b/languages/tolk/types/strings.mdx @@ -3,6 +3,10 @@ title: "Strings (actually, slices)" sidebarTitle: "Strings" --- +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + + The TVM (virtual machine) has only `SLICE` (binary data). **There are no usual "strings"** (likewise, no floating-point numbers). However, several techniques exist to store and return string-like data on-chain. diff --git a/languages/tolk/types/structures.mdx b/languages/tolk/types/structures.mdx index dd0b9d4ef..44f6aea9a 100644 --- a/languages/tolk/types/structures.mdx +++ b/languages/tolk/types/structures.mdx @@ -2,6 +2,10 @@ title: "Structures" --- +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + + Tolk supports structures — similar to TypeScript classes, but designed to operate on TVM. ```tolk @@ -27,14 +31,14 @@ fun demo() { ## Two identical structures are not assignable ```tolk -struct A { v: int } -struct B { v: int } +struct SomeA { v: int } +struct SomeB { v: int } -fun acceptA(a: A) {} +fun acceptA(a: SomeA) {} -fun demo(a: A, b: B) { - b = a; // error, can not assign `A` to `B` - acceptA(b); // error, can not pass `B` to `A` +fun demo(a: SomeA, b: SomeB) { + b = a; // error, can not assign `SomeA` to `SomeB` + acceptA(b); // error, can not pass `SomeB` to `SomeA` } ``` diff --git a/languages/tolk/types/tensors.mdx b/languages/tolk/types/tensors.mdx index 9937487f7..a834c98f9 100644 --- a/languages/tolk/types/tensors.mdx +++ b/languages/tolk/types/tensors.mdx @@ -4,6 +4,9 @@ sidebarTitle: "Tensors" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + In Tolk, **a tensor** is `(T1, T2, ...)`. For example, `(int, slice)` — two values one by one. diff --git a/languages/tolk/types/tuples.mdx b/languages/tolk/types/tuples.mdx index 80991b786..08dcc3a89 100644 --- a/languages/tolk/types/tuples.mdx +++ b/languages/tolk/types/tuples.mdx @@ -4,6 +4,9 @@ sidebarTitle: "Tuples" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + In TVM (the virtual machine), a **tuple** is a dynamic container that stores from 0 to 255 elements in a single stack slot. Tolk supports two tuple forms: diff --git a/languages/tolk/types/type-checks-and-casts.mdx b/languages/tolk/types/type-checks-and-casts.mdx index 1853f67d8..376f8a421 100644 --- a/languages/tolk/types/type-checks-and-casts.mdx +++ b/languages/tolk/types/type-checks-and-casts.mdx @@ -3,6 +3,9 @@ title: "Type checks and casts" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + Different types may be converted to one another — either automatically or using the unsafe `as` operator. diff --git a/languages/tolk/types/unions.mdx b/languages/tolk/types/unions.mdx index 3214ec852..ac9b9d4fb 100644 --- a/languages/tolk/types/unions.mdx +++ b/languages/tolk/types/unions.mdx @@ -2,6 +2,10 @@ title: "Union types" --- +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + + Tolk supports union types `T1 | T2 | ...` similar to TypeScript. They allow a value to belong to one of several possible types. Pattern matching over unions is essential for message handling. diff --git a/languages/tolk/types/void-never.mdx b/languages/tolk/types/void-never.mdx index 777c0a120..d6bbb0473 100644 --- a/languages/tolk/types/void-never.mdx +++ b/languages/tolk/types/void-never.mdx @@ -4,6 +4,9 @@ sidebarTitle: "Void and never" --- import { Aside } from '/snippets/aside.jsx'; +import { TolkHighlight } from '/snippets/tolk-highlight.jsx'; + + Tolk supports the types `void` and `never`, similar to TypeScript. Both represent the absence of a value, but in different ways. diff --git a/snippets/tolk-highlight.jsx b/snippets/tolk-highlight.jsx new file mode 100644 index 000000000..3624c09c1 --- /dev/null +++ b/snippets/tolk-highlight.jsx @@ -0,0 +1,96 @@ +/* + A temporary solution syntax highlighting of Tolk snippets: invoke Prism.js on a client-side. + Sources of Prism.js are embedded right here, it's not downloaded separately. + @link https://github.com/ton-org/docs/issues/1473 + + All code blocks for the Tolk language are highlighted: + ```tolk + // automatically highlighted if `` exists on a page + ``` + + Visual appearance is defined in `extra.css`. + + Note: `` must be placed on a page to enable highlighting! + Now it's imported in all "/languages/tolk/..." pages, but not throughout the rest documentation. + */ + +export const TolkHighlight = () => { + const TOLK_GRAMMAR = { + 'comment': [ + { pattern: /\/\/.*/, greedy: true }, + // http://stackoverflow.com/questions/13014947/regex-to-match-a-c-style-multiline-comment/36328890#36328890 + { pattern: /\/\*[^*]*\*+([^/*][^*]*\*+)*\//, greedy: true }, + ], + 'attr-name': /@[a-zA-Z0-9_]+/, + 'keyword': /\b(do|if|as|is|try|else|while|break|throw|catch|return|assert|repeat|continue|asm|builtin|import|export|true|false|null|redef|mutate|tolk|global|const|var|val|fun|get\sfun|struct|match|type|lazy|enum|private|readonly)\b/, + 'function': /[a-zA-Z$_][a-zA-Z0-9$_]*(?=(<[^>]+>)*\()/, + 'type-hint': /\b(int|cell|void|never|bool|slice|tuple|builder|continuation|coins|int\d+|uint\d+|bytes\d+|bits\d+|address|any_address|map|dict)\b/, + 'boolean': /\b(false|true|null)\b/, + 'self': /\b(self)\b/, + 'number': /\b(-?\d+|0x[\da-fA-F]+|0b[01]+)\b/, + 'string': [ + { pattern: /"""[\s\S]*?"""/, greedy: true }, + { pattern: /"[^\n"]*"\w?/, greedy: true }, + ], + 'operator': new RegExp("\\+|-|\\*|/|%|\\?|:|=|<|>|!|&|\\||\\^|==|!=|<=|>=|<<|>>|&&|\\|\\||~/|\\^/|\\+=|-=|\\*=|/=|%=|&=|\\|=|\\^=|->|<=>|~>>|\\^>>|<<=|>>=|=>"), + 'three-dots': /\.\.\./, + 'punctuation': /[.,;(){}\[\]]/, + 'struct': /\b[A-Z][a-z][a-zA-Z0-9$_]*\b/, + 'variable': [ + { pattern: /`[^`]+`/ }, + { pattern: /\b[a-zA-Z$_][a-zA-Z0-9$_]*\b/ }, + ], + }; + + // code blocks inserted with ```tolk are rendered as actually, + // because Mintlify knows nothing about custom languages; + // that's why even ```text snippets are highlighted with Tolk grammar + // (there are a few in "/languages/tolk": Fift output and compilation errors) + function isTolkBlockCode(/**Element*/ codeElement) { + return codeElement.getAttribute('language') === 'text'; + } + + // check if Prism was called for + function isCodeHighlighted(/**Element*/ codeElement) { + return codeElement.classList.contains('language-tolk'); + } + + // call Prism for + function highlightCode(/**Element*/ codeElement, Prism) { + codeElement.classList.add('language-tolk'); + codeElement.parentElement.style.backgroundColor = ''; // parent is
+    codeElement.parentElement.classList.remove('shiki'); // avoid css mixing
+    try {
+      Prism.highlightElement(codeElement);
+    } catch (ex) {
+      console.error('suppressed Prism error for code', codeElement, ex);
+    }
+  }
+
+  function highlightAllCodeElementsOnPage(Prism) {
+    let allCodes = document.querySelectorAll('code');
+    for (let i = 0; i < allCodes.length; ++i) {
+      if (isTolkBlockCode(allCodes[i]) && !isCodeHighlighted(allCodes[i])) {
+        highlightCode(allCodes[i], Prism)
+      }
+    }
+
+    // call it iteratively in case of dynamic content changes
+    setTimeout(function() {
+      highlightAllCodeElementsOnPage(Prism)
+    }, 2000);
+  }
+
+  if (typeof window !== "undefined") {
+    /* PrismJS 1.30.0 */
+    var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=g.reach);A+=w.value.length,w=w.next){var P=w.value;if(n.length>e.length)return;if(!(P instanceof i)){var E,S=1;if(y){if(!(E=l(b,A,e,m))||E.index>=e.length)break;var L=E.index,O=E.index+E[0].length,C=A;for(C+=w.value.length;L>=C;)C+=(w=w.next).value.length;if(A=C-=w.value.length,w.value instanceof i)continue;for(var j=w;j!==n.tail&&(Cg.reach&&(g.reach=W);var I=w.prev;if(_&&(I=u(n,I,_),A+=_.length),c(n,I,S),w=u(n,I,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),S>1){var T={cause:f+","+d,reach:W};o(e,n,t,w.prev,A,T),g&&T.reach>g.reach&&(g.reach=T.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a"+i.content+""},!e.document)return e.addEventListener?(a.disableWorkerMessageHandler||e.addEventListener("message",(function(n){var t=JSON.parse(n.data),r=t.language,i=t.code,l=t.immediateClose;e.postMessage(a.highlight(i,a.languages[r],r)),l&&e.close()}),!1),a):a;var g=a.util.currentScript();function f(){a.manual||a.highlightAll()}if(g&&(a.filename=g.src,g.hasAttribute("data-manual")&&(a.manual=!0)),!a.manual){var h=document.readyState;"loading"===h||"interactive"===h&&g&&g.defer?document.addEventListener("DOMContentLoaded",f):window.requestAnimationFrame?window.requestAnimationFrame(f):window.setTimeout(f,16)}return a}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism);
+    Prism.languages.tolk = TOLK_GRAMMAR;
+
+    setTimeout(function() {
+      highlightAllCodeElementsOnPage(Prism);
+    }, 0);
+  }
+
+  // this snippet has no markup, it just provides highlighting in the background
+  return <>;
+};

From 8a3ead6eeb370cde78e5ab8627226c2b908e5114 Mon Sep 17 00:00:00 2001
From: tolk-vm 
Date: Mon, 24 Nov 2025 15:28:05 +0400
Subject: [PATCH 11/11] [Tolk] Modify cspell for a spellchecker to succeed

---
 .cspell.jsonc | 21 +++++++++++++++++++++
 1 file changed, 21 insertions(+)

diff --git a/.cspell.jsonc b/.cspell.jsonc
index 0a1902874..3acf6c100 100644
--- a/.cspell.jsonc
+++ b/.cspell.jsonc
@@ -83,6 +83,26 @@
         "!2lw-deny-list", // turns off the dictionary
       ]
     },
+    {
+      "filename": "languages/tolk/features/message-sending.mdx",
+      "ignoreWords": [
+        "StateInit",
+      ]
+    },
+    {
+      "filename": "languages/tolk/features/compiler-optimizations.mdx",
+      "ignoreWords": [
+        "fifting",
+      ]
+    },
+    {
+      "filename": "languages/tolk/from-func/tolk-vs-func.mdx",
+      "ignoreWords": [
+        "transpiles",
+        "Hindley",
+        "Milner",
+      ]
+    },
     {
       "filename": "**/api/**/*.{json,yml,yaml}",
       "ignoreWords": [
@@ -110,6 +130,7 @@
     "foundations/whitepapers/ton.mdx",
     "foundations/whitepapers/tvm.mdx",
     "languages/fift/whitepaper.mdx",
+    "languages/tolk/features/standard-library.mdx",
     // Generated files
     "tvm/instructions.mdx",
     // Binaries