Skip to content

Commit

Permalink
feat: map.deepEquals for deep map comparison (#637)
Browse files Browse the repository at this point in the history
Sometimes extensionally equal maps can have different hashes because of different encodings,
so `==` won't recognize extensionally equal maps as equal, but `deepEquals` will
  • Loading branch information
Gusarich authored Aug 28, 2024
1 parent 67ba46d commit 12def54
Show file tree
Hide file tree
Showing 8 changed files with 532 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,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)

### Changed

Expand Down
70 changes: 70 additions & 0 deletions src/abi/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -577,4 +577,74 @@ export const MapFunctions: Map<string, AbiFunction> = new Map([
},
},
],
[
"deepEquals",
{
name: "deepEquals",
resolve(ctx, args, ref) {
// Check arguments
if (args.length !== 2) {
throwCompilationError(
"deepEquals expects two arguments",
ref,
); // Ignore self argument
}
const self = args[0]!;
const other = args[1]!;
if (self.kind !== "map") {
throwCompilationError(
"deepEquals expects a map as self argument",
ref,
); // Should not happen
}
if (other.kind !== "map") {
throwCompilationError(
"deepEquals expects a map as other argument",
ref,
); // Should not happen
}

return { kind: "ref", name: "Bool", optional: false };
},
generate: (ctx, args, exprs, ref) => {
if (args.length !== 2) {
throwCompilationError(
"deepEquals expects two arguments",
ref,
); // Ignore self argument
}
const self = args[0]!;
const other = args[1]!;
if (self.kind !== "map") {
throwCompilationError(
"deepEquals expects a map as self argument",
ref,
); // Should not happen
}
if (other.kind !== "map") {
throwCompilationError(
"deepEquals expects a map as other argument",
ref,
); // Should not happen
}

// 257 for int, 267 for address
const keyLength =
self.key === "Int"
? self.keyAs
? self.keyAs.startsWith("int")
? parseInt(self.keyAs.slice(3))
: self.keyAs.startsWith("uint")
? parseInt(self.keyAs.slice(4))
: throwCompilationError(
"Invalid key serialization type", // Should not happen
ref,
)
: 257
: 267;

return `${ctx.used("__tact_dict_eq")}(${writeExpression(exprs[0]!, ctx)}, ${writeExpression(exprs[1]!, ctx)}, ${keyLength})`;
},
},
],
]);
123 changes: 123 additions & 0 deletions src/generator/writers/__snapshots__/writeSerialization.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,18 @@ return __tact_create_address(chain, hash);",
"name": "__tact_dict_get",
"signature": "(slice, int) __tact_dict_get(cell dict, int key_len, slice index)",
},
{
"code": {
"code": "asm(index dict key_len) "DICTDELGET" "NULLSWAPIFNOT2"",
"kind": "asm",
},
"comment": null,
"context": "stdlib",
"depends": Set {},
"flags": Set {},
"name": "__tact_dict_delete_get",
"signature": "(cell, (slice, int)) __tact_dict_delete_get(cell dict, int key_len, slice index)",
},
{
"code": {
"code": "asm(index dict key_len) "DICTGETREF" "NULLSWAPIFNOT"",
Expand Down Expand Up @@ -1622,6 +1634,35 @@ return ( a_is_null & b_is_null ) ? ( true ) : ( ( ( ~ a_is_null ) & ( ~ b_is_nul
"name": "__tact_slice_eq_bits_nullable",
"signature": "int __tact_slice_eq_bits_nullable(slice a, slice b)",
},
{
"code": {
"code": "(slice key, slice value, int flag) = __tact_dict_min(a, kl);
while (flag) {
(slice value_b, int flag_b) = b~__tact_dict_delete_get(kl, key);
ifnot (flag_b) {
return 0;
}
ifnot (value.slice_hash() == value_b.slice_hash()) {
return 0;
}
(key, value, flag) = __tact_dict_next(a, kl, key);
}
return null?(b);",
"kind": "generic",
},
"comment": null,
"context": "stdlib",
"depends": Set {
"__tact_dict_min",
"__tact_dict_delete_get",
"__tact_dict_next",
},
"flags": Set {
"inline",
},
"name": "__tact_dict_eq",
"signature": "int __tact_dict_eq(cell a, cell b, int kl)",
},
{
"code": {
"code": "return (null?(a)) ? (false) : (a == b);",
Expand Down Expand Up @@ -3816,6 +3857,18 @@ return __tact_create_address(chain, hash);",
"name": "__tact_dict_get",
"signature": "(slice, int) __tact_dict_get(cell dict, int key_len, slice index)",
},
{
"code": {
"code": "asm(index dict key_len) "DICTDELGET" "NULLSWAPIFNOT2"",
"kind": "asm",
},
"comment": null,
"context": "stdlib",
"depends": Set {},
"flags": Set {},
"name": "__tact_dict_delete_get",
"signature": "(cell, (slice, int)) __tact_dict_delete_get(cell dict, int key_len, slice index)",
},
{
"code": {
"code": "asm(index dict key_len) "DICTGETREF" "NULLSWAPIFNOT"",
Expand Down Expand Up @@ -5140,6 +5193,35 @@ return ( a_is_null & b_is_null ) ? ( true ) : ( ( ( ~ a_is_null ) & ( ~ b_is_nul
"name": "__tact_slice_eq_bits_nullable",
"signature": "int __tact_slice_eq_bits_nullable(slice a, slice b)",
},
{
"code": {
"code": "(slice key, slice value, int flag) = __tact_dict_min(a, kl);
while (flag) {
(slice value_b, int flag_b) = b~__tact_dict_delete_get(kl, key);
ifnot (flag_b) {
return 0;
}
ifnot (value.slice_hash() == value_b.slice_hash()) {
return 0;
}
(key, value, flag) = __tact_dict_next(a, kl, key);
}
return null?(b);",
"kind": "generic",
},
"comment": null,
"context": "stdlib",
"depends": Set {
"__tact_dict_min",
"__tact_dict_delete_get",
"__tact_dict_next",
},
"flags": Set {
"inline",
},
"name": "__tact_dict_eq",
"signature": "int __tact_dict_eq(cell a, cell b, int kl)",
},
{
"code": {
"code": "return (null?(a)) ? (false) : (a == b);",
Expand Down Expand Up @@ -7334,6 +7416,18 @@ return __tact_create_address(chain, hash);",
"name": "__tact_dict_get",
"signature": "(slice, int) __tact_dict_get(cell dict, int key_len, slice index)",
},
{
"code": {
"code": "asm(index dict key_len) "DICTDELGET" "NULLSWAPIFNOT2"",
"kind": "asm",
},
"comment": null,
"context": "stdlib",
"depends": Set {},
"flags": Set {},
"name": "__tact_dict_delete_get",
"signature": "(cell, (slice, int)) __tact_dict_delete_get(cell dict, int key_len, slice index)",
},
{
"code": {
"code": "asm(index dict key_len) "DICTGETREF" "NULLSWAPIFNOT"",
Expand Down Expand Up @@ -8658,6 +8752,35 @@ return ( a_is_null & b_is_null ) ? ( true ) : ( ( ( ~ a_is_null ) & ( ~ b_is_nul
"name": "__tact_slice_eq_bits_nullable",
"signature": "int __tact_slice_eq_bits_nullable(slice a, slice b)",
},
{
"code": {
"code": "(slice key, slice value, int flag) = __tact_dict_min(a, kl);
while (flag) {
(slice value_b, int flag_b) = b~__tact_dict_delete_get(kl, key);
ifnot (flag_b) {
return 0;
}
ifnot (value.slice_hash() == value_b.slice_hash()) {
return 0;
}
(key, value, flag) = __tact_dict_next(a, kl, key);
}
return null?(b);",
"kind": "generic",
},
"comment": null,
"context": "stdlib",
"depends": Set {
"__tact_dict_min",
"__tact_dict_delete_get",
"__tact_dict_next",
},
"flags": Set {
"inline",
},
"name": "__tact_dict_eq",
"signature": "int __tact_dict_eq(cell a, cell b, int kl)",
},
{
"code": {
"code": "return (null?(a)) ? (false) : (a == b);",
Expand Down
34 changes: 34 additions & 0 deletions src/generator/writers/writeStdlib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,14 @@ export function writeStdlib(ctx: WriterContext) {
ctx.asm(`asm(index dict key_len) "DICTGET" "NULLSWAPIFNOT"`);
});

ctx.fun("__tact_dict_delete_get", () => {
ctx.signature(
`(cell, (slice, int)) __tact_dict_delete_get(cell dict, int key_len, slice index)`,
);
ctx.context("stdlib");
ctx.asm(`asm(index dict key_len) "DICTDELGET" "NULLSWAPIFNOT2"`);
});

ctx.fun("__tact_dict_get_ref", () => {
ctx.signature(
`(cell, int) __tact_dict_get_ref(cell dict, int key_len, slice index)`,
Expand Down Expand Up @@ -1408,6 +1416,32 @@ export function writeStdlib(ctx: WriterContext) {
});
});

//
// Dictionary deep equality
//

ctx.fun(`__tact_dict_eq`, () => {
ctx.signature(`int __tact_dict_eq(cell a, cell b, int kl)`);
ctx.flag("inline");
ctx.context("stdlib");
ctx.body(() => {
ctx.write(`
(slice key, slice value, int flag) = ${ctx.used("__tact_dict_min")}(a, kl);
while (flag) {
(slice value_b, int flag_b) = b~${ctx.used("__tact_dict_delete_get")}(kl, key);
ifnot (flag_b) {
return 0;
}
ifnot (value.slice_hash() == value_b.slice_hash()) {
return 0;
}
(key, value, flag) = ${ctx.used("__tact_dict_next")}(a, kl, key);
}
return null?(b);
`);
});
});

//
// Int Eq
//
Expand Down
45 changes: 45 additions & 0 deletions src/test/e2e-emulated/contracts/map-comparison.tact
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
message Compare {
m1: map<Int as uint8, Bool>;
m2: map<Int as uint8, Bool>;
}

message CompareDeep {
m1: map<Int as uint8, Bool>;
m2: map<Int as uint8, Bool>;
}

contract MapComparisonTestContract {
receive() {}

receive(msg: Compare) {
require(msg.m1 == msg.m2, "Maps are not equal");
}

receive(msg: CompareDeep) {
require(msg.m1.deepEquals(msg.m2), "Maps are not equal");
}

get fun compareIntInt(m1: map<Int, Int>, m2: map<Int, Int>): Bool {
return m1.deepEquals(m2);
}

get fun compareIntCell(m1: map<Int, Cell>, m2: map<Int, Cell>): Bool {
return m1.deepEquals(m2);
}

get fun compareIntAddress(m1: map<Int, Address>, m2: map<Int, Address>): Bool {
return m1.deepEquals(m2);
}

get fun compareAddressInt(m1: map<Address, Int>, m2: map<Address, Int>): Bool {
return m1.deepEquals(m2);
}

get fun compareAddressCell(m1: map<Address, Cell>, m2: map<Address, Cell>): Bool {
return m1.deepEquals(m2);
}

get fun compareAddressAddress(m1: map<Address, Address>, m2: map<Address, Address>): Bool {
return m1.deepEquals(m2);
}
}
Loading

0 comments on commit 12def54

Please sign in to comment.