From 9513bd3c34465d2e9c4897be7d47edccefc2f270 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Thu, 10 Oct 2024 05:18:32 +0200 Subject: [PATCH 1/3] feat(docs): `map.deepEquals()` Didn't went too deep into describing how the long and short key labels might be used to manually construct the HashmapE (FunC's dict) as it's way too low-level for the general audience. But I did link the TL-B page which showcases all the intricacies of that process for the curious :) --- CHANGELOG.md | 1 + docs/src/content/docs/book/maps.mdx | 25 ++++++++++++++++++++++++ docs/src/content/docs/book/operators.mdx | 12 ++++++++++-- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e35106d56..ab4967d76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Docs: the `description` property to the frontmatter of the each page for better SEO: PR [#916](https://github.com/tact-lang/tact/pull/916) - Docs: Google Analytics tags per every page: PR [#921](https://github.com/tact-lang/tact/pull/921) - Ability to specify a compile-time method ID expression for getters: PR [#922](https://github.com/tact-lang/tact/pull/922) and PR [#932](https://github.com/tact-lang/tact/pull/932) +- Docs: the `deepEquals` method for map type: PR [#939](https://github.com/tact-lang/tact/pull/939) ### Changed diff --git a/docs/src/content/docs/book/maps.mdx b/docs/src/content/docs/book/maps.mdx index 0eb18a9a7..014706ae8 100644 --- a/docs/src/content/docs/book/maps.mdx +++ b/docs/src/content/docs/book/maps.mdx @@ -3,6 +3,8 @@ title: Maps description: "The composite type map is used as a way to associate keys with corresponding values of various types" --- +import { Badge } from '@astrojs/starlight/components'; + The [composite type](/book/types#composite-types) `map{:tact}` is used as a way to associate keys of type `K{:tact}` with corresponding values of type `V{:tact}`. For example, `map{:tact}` uses [`Int{:tact}`][int] type for its keys and values: @@ -151,6 +153,29 @@ if (fizz == null) { } ``` +### Compare with `.deepEquals()` {#deepequals} + +

+ +The `.deepEquals(){:tact}` [method](/book/functions#extension-function) on maps returns `true{:tact}` if all the entries of the map match respective entries of another map and their [underlying serialization logic](https://docs.ton.org/develop/data-formats/tl-b-types#hashmap) is exactly the same. Returns `false{:tact}` otherwise. + +```tact +let fizz: map = emptyMap(); +let buzz: map = emptyMap(); + +fizz.set(1, 2); +buzz.set(1, 2); + +fizz.deepEquals(buzz); // true +fizz == buzz; // true, and uses much less gas to compute +``` + +:::note + + This function is very gas expensive, and for the majority of cases it'll be enough to use the shallow comparison via the [equality `=={:tact}`](/book/operators#binary-equality) or [inequality `!={:tact}`](/book/operators#binary-equality) operators. + +::: + ### Convert to a `Cell`, `.asCell()` {#ascell} Use `.asCell(){:tact}` [method](/book/functions#extension-function) on maps to convert all their values to a [`Cell{:tact}`][cell] type. Be mindful, that [`Cell{:tact}`][cell] type is able to store up to 1023 bits, so converting larger maps to the Cell will result in error. diff --git a/docs/src/content/docs/book/operators.mdx b/docs/src/content/docs/book/operators.mdx index 98c16a045..46f06c16e 100644 --- a/docs/src/content/docs/book/operators.mdx +++ b/docs/src/content/docs/book/operators.mdx @@ -382,8 +382,8 @@ Both operators can be applied to the following list of types and values: * [`Int{:tact}`][int] * [`Bool{:tact}`][bool] * [`Address{:tact}`][p] -* [`Cell{:tact}`][cell], implicitly compares via `.hash(){:tact}` -* [`Slice{:tact}`][slice], implicitly compares via `.hash(){:tact}` +* [`Cell{:tact}`][cell], implicitly compares via [`.hash(){:tact}`](/ref/core-cells#cellhash) +* [`Slice{:tact}`][slice], implicitly compares via [`.hash(){:tact}`](/ref/core-cells#slicehash) * [`String{:tact}`][p] * [`map{:tact}`](/book/maps), but only if their key and value types are identical * [Optionals and `null{:tact}` value](/book/optionals) @@ -431,6 +431,14 @@ nullable == anotherNullable; // false nullable != anotherNullable; // true ``` +:::note + + Binary equality `=={:tact}` and inequality `!={:tact}` operators implicitly compare [maps](/book/maps) by the hashes of their respective [cells][cell] via [`.hash(){:tact}`](/ref/core-cells#cellhash) function. While this is ok for the majority of cases because most map serializers work identically to the one from TON Blockchain sources, it's still possible to get false negative results by serializing a map manually or by changing the logic of the serializer in some library. + + If you need to guarantee that maps you're comparing are equal, and you're willing to pay a lot more gas for it, use the [`map.deepEquals(){:tact}`](/book/maps#deepequals) function. + +::: + ### Bitwise AND, `&` {#binary-bitwise-and} Binary ampersand (_bitwise AND_) operator `&{:tact}` applies a [bitwise AND](https://en.wikipedia.org/wiki/Bitwise_operation#AND), which performs the [logical AND](#binary-logical-and) operation on each pair of the corresponding bits of operands. This is useful when we want to clear selected bits off a number, where each bit represents an individual flag or a boolean state, which makes it possible to "store" up to $257$ boolean values per integer, as all integers in Tact are $257$-bit signed. From a57effcdfa7b65e6ac3fb09a06c9d7a184ff4ac9 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Thu, 10 Oct 2024 07:16:19 +0200 Subject: [PATCH 2/3] chore: retroactive CHANGELOG update --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab4967d76..cb609ca03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Docs: the `description` property to the frontmatter of the each page for better SEO: PR [#916](https://github.com/tact-lang/tact/pull/916) - Docs: Google Analytics tags per every page: PR [#921](https://github.com/tact-lang/tact/pull/921) - Ability to specify a compile-time method ID expression for getters: PR [#922](https://github.com/tact-lang/tact/pull/922) and PR [#932](https://github.com/tact-lang/tact/pull/932) -- Docs: the `deepEquals` method for map type: PR [#939](https://github.com/tact-lang/tact/pull/939) ### Changed @@ -64,7 +63,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The `storeBit` method for `Builder` type and the `loadBit` method for `Slice` type: PR [#699](https://github.com/tact-lang/tact/pull/699) - The `toSlice` method for structs and messages: PR [#630](https://github.com/tact-lang/tact/pull/630) - Wider range of serialization options for integers — `uint1` through `uint256` and `int1` through `int257`: PR [#558](https://github.com/tact-lang/tact/pull/558) -- The `deepEquals` method for the `Map` type: PR [#637](https://github.com/tact-lang/tact/pull/637) +- The `deepEquals` method for the `Map` type: PR [#637](https://github.com/tact-lang/tact/pull/637), PR [#939](https://github.com/tact-lang/tact/pull/939) - `asm` bodies for module-level functions: PR [#769](https://github.com/tact-lang/tact/pull/769), PR [#825](https://github.com/tact-lang/tact/pull/825) - Corresponding stdlib functions for new TVM instructions from 2023.07 and 2024.04 upgrades: PR [#331](https://github.com/tact-lang/tact/pull/331). Added the `storeBuilder` extension function and `gasConsumed`, `getComputeFee`, `getStorageFee`, `getForwardFee`, `getSimpleComputeFee`, `getSimpleForwardFee`, `getOriginalFwdFee`, `myStorageDue` functions. - `slice`, `rawSlice`, `ascii` and `crc32` built-in functions: PR [#787](https://github.com/tact-lang/tact/pull/787), PR [#799](https://github.com/tact-lang/tact/pull/799) From 3ba35247aa78e1358d41450038a9d6f646b90eed Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Mon, 14 Oct 2024 23:36:41 +0200 Subject: [PATCH 3/3] fix: apply suggestions after code review --- docs/src/content/docs/book/maps.mdx | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/src/content/docs/book/maps.mdx b/docs/src/content/docs/book/maps.mdx index a89b039a7..108e71613 100644 --- a/docs/src/content/docs/book/maps.mdx +++ b/docs/src/content/docs/book/maps.mdx @@ -157,7 +157,7 @@ if (fizz == null) {

-The `.deepEquals(){:tact}` [method](/book/functions#extension-function) on maps returns `true{:tact}` if all the entries of the map match respective entries of another map and their [underlying serialization logic](https://docs.ton.org/develop/data-formats/tl-b-types#hashmap) is exactly the same. Returns `false{:tact}` otherwise. +The `.deepEquals(){:tact}` [method](/book/functions#extension-function) on maps returns `true{:tact}` if all entries of the map match corresponding entries of another map, ignoring possible differences in the [underlying serialization logic][hashmap]. Returns `false{:tact}` otherwise. ```tact let fizz: map = emptyMap(); @@ -170,6 +170,28 @@ fizz.deepEquals(buzz); // true fizz == buzz; // true, and uses much less gas to compute ``` +Using `.deepEquals(){:tact}` is very important in cases where a map comes from the third-party source that doesn't provide any guarantees about the [serialization layout][hashmap]. For one such example, consider the following code: + +```typescript title="some-typescript-code.ts" +// First map, with long labels +const m1 = beginCell() + .storeUint(2, 2) // long label + .storeUint(8, 4) // key length + .storeUint(1, 8) // key + .storeBit(true) // value + .endCell(); + +// Second map, with short labels +const m2 = beginCell() + .storeUint(0, 1) // short label + .storeUint(0b111111110, 9) // key length + .storeUint(1, 8) // key + .storeBit(true) // value + .endCell(); +``` + +There, both maps are formed manually and both contain the same key-value pair. If you were to send both of those maps in a message to the Tact contract, and then compare them with `.deepEquals(){:tact}` and [equality operator `=={:tact}`](/book/operators#binary-equality), the former would produce `true{:tact}` because both maps have the same entry, while the latter would produce `false{:tact}`, because it only does the shallow comparison of map hashes. And those differ since the maps are serialized differently. + :::note This function is very gas expensive, and for the majority of cases it'll be enough to use the shallow comparison via the [equality `=={:tact}`](/book/operators#binary-equality) or [inequality `!={:tact}`](/book/operators#binary-equality) operators. @@ -355,3 +377,5 @@ If you still need a large map or an unbound (infinitely large) map, it's better [p]: /book/types#primitive-types [int]: /book/integers [cell]: /book/cells#cells + +[hashmap]: https://docs.ton.org/develop/data-formats/tl-b-types#hashmap