Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(codegen): make selector hack compatible with TON API #2398

Merged
merged 27 commits into from
Mar 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions dev-docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Internal infrastructure

- `internalExternalReceiversOutsideMethodsMap` have been reworked to ensure compatibility with explorers: PR [#2398](https://github.com/tact-lang/tact/pull/2398)

### Release contributors

- [Shvetc Andrei](https://github.com/Shvandre)

## [1.6.4] - 2025-03-18

### Language features
Expand Down
4 changes: 2 additions & 2 deletions docs/src/content/docs/book/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -408,9 +408,9 @@ This option can be used to provide an extra safety level or for debugging.

<Badge text="Available since Tact 1.6.3" variant="tip" size="medium"/><p/>

`false{:json}` by default.
`true{:json}` by default.

If set to `true{:json}`, stores internal and external receivers outside of the methods map.
If set to `true{:json}`, stores internal and external receivers outside the methods map.

Saves gas, but as a result of this optimization, the contract might not be correctly recognized and parsed by explorers and user wallets.

Expand Down
2 changes: 2 additions & 0 deletions spell/cspell-list.txt
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ memecoin
Merkle
minmax
mintable
misparse
misparsed
mktemp
multiformats
nanoton
Expand Down
16 changes: 16 additions & 0 deletions src/benchmarks/escrow/results_code_size.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,22 @@
"cells": "20",
"bits": "12447"
}
},
{
"label": "1.6.4 with no selector hack",
"pr": null,
"size": {
"cells": "21",
"bits": "12423"
}
},
{
"label": "1.6.4 with the new selector hack",
"pr": "https://github.com/tact-lang/tact/pull/2398",
"size": {
"cells": "20",
"bits": "12503"
}
}
]
}
20 changes: 20 additions & 0 deletions src/benchmarks/escrow/results_gas.json
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,26 @@
"approveTon": "6809",
"cancelTon": "4838"
}
},
{
"label": "1.6.4 with no selector hack",
"pr": null,
"gas": {
"fundingTon": "4689",
"changeCode": "4798",
"approveTon": "6907",
"cancelTon": "4936"
}
},
{
"label": "1.6.4 with the new selector hack",
"pr": "https://github.com/tact-lang/tact/pull/2398",
"gas": {
"fundingTon": "4643",
"changeCode": "4752",
"approveTon": "6861",
"cancelTon": "4890"
}
}
]
}
20 changes: 20 additions & 0 deletions src/benchmarks/jetton/results_code_size.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,26 @@
"wallet cells": "15",
"wallet bits": "8260"
}
},
{
"label": "1.6.4 with no selector hack",
"pr": null,
"size": {
"minter cells": "31",
"minter bits": "14646",
"wallet cells": "16",
"wallet bits": "8231"
}
},
{
"label": "1.6.4 with the new selector hack",
"pr": "https://github.com/tact-lang/tact/pull/2398",
"size": {
"minter cells": "29",
"minter bits": "14817",
"wallet cells": "15",
"wallet bits": "8316"
}
}
]
}
18 changes: 18 additions & 0 deletions src/benchmarks/jetton/results_gas.json
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,24 @@
"burn": "11839",
"discovery": "6109"
}
},
{
"label": "1.6.4 with no selector hack",
"pr": null,
"gas": {
"transfer": "15182",
"burn": "11979",
"discovery": "6179"
}
},
{
"label": "1.6.4 with the new selector hack",
"pr": "https://github.com/tact-lang/tact/pull/2398",
"gas": {
"transfer": "15146",
"burn": "11943",
"discovery": "6161"
}
}
]
}
20 changes: 20 additions & 0 deletions src/benchmarks/notcoin/results_code_size.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,26 @@
"wallet cells": "15",
"wallet bits": "8260"
}
},
{
"label": "1.6.4 with no selector hack",
"pr": null,
"size": {
"minter cells": "33",
"minter bits": "16299",
"wallet cells": "16",
"wallet bits": "8231"
}
},
{
"label": "1.6.4 with the new selector hack",
"pr": "https://github.com/tact-lang/tact/pull/2398",
"size": {
"minter cells": "31",
"minter bits": "16470",
"wallet cells": "15",
"wallet bits": "8316"
}
}
]
}
18 changes: 18 additions & 0 deletions src/benchmarks/notcoin/results_gas.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,24 @@
"burn": "11928",
"discovery": "5818"
}
},
{
"label": "1.6.4 with no selector hack",
"pr": null,
"gas": {
"transfer": "15940",
"burn": "12068",
"discovery": "5888"
}
},
{
"label": "1.6.4 with the new selector hack",
"pr": "https://github.com/tact-lang/tact/pull/2398",
"gas": {
"transfer": "15904",
"burn": "12032",
"discovery": "5870"
}
}
]
}
20 changes: 20 additions & 0 deletions src/benchmarks/wallet/results_gas.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,26 @@
"internalTransfer": "8711",
"extensionTransfer": "4780"
}
},
{
"label": "1.6.4 with no selector hack",
"pr": null,
"gas": {
"externalTransfer": "8069",
"addExtension": "8392",
"internalTransfer": "8991",
"extensionTransfer": "5060"
}
},
{
"label": "1.6.4 with the new selector hack",
"pr": "https://github.com/tact-lang/tact/pull/2398",
"gas": {
"externalTransfer": "8003",
"addExtension": "8326",
"internalTransfer": "8845",
"extensionTransfer": "4914"
}
}
]
}
4 changes: 2 additions & 2 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export type OptimizationOptions = {
readonly alwaysSaveContractData?: boolean;

/**
* If set to `true`, stores internal and external receivers outside of the methods map. Default is `false`.
* Saves gas, but as a result of this optimization, the contract might not be correctly recognized and parsed by explorers and user wallets.
* If set to `true`, stores internal and external receivers outside the methods map. Default is `true`.
* Saves gas, but as a result of this optimization, the contract might not be correctly recognized and parsed by indexers.
*/
readonly internalExternalReceiversOutsideMethodsMap?: boolean;
};
Expand Down
120 changes: 93 additions & 27 deletions src/generator/writers/writeContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,39 +451,105 @@ export function writeMainContract(

// fift injection, protected by a feature flag
if (enabledInternalExternalReceiversOutsideMethodsMap(wCtx.ctx)) {
wCtx.append(`() __tact_selector_hack_asm() impure asm """
@atend @ 1 {
execute current@ context@ current!
wCtx.append(`
() __tact_selector_hack_asm() impure asm """
@atend @ 1 {
execute current@ context@ current!
{
// The core idea of this function is to save gas by avoiding unnecessary dict jump, when recv_internal/recv_external is called
// We want to extract recv_internal/recv_external from the dict and select needed function
// not by jumping to the needed function by it's index, but by using usual IF statements.

}END> b> // Close previous builder, now we have a cell of previous code on top of the stack

<{ // Start of the new code builder
SETCP0
// Swap the new code builder with the previous code, now we have previous code on top of the stack
swap
// Transform cell to slice and load first ref from the previous code, now we have the dict on top of the stack
<s ref@`);
if (hasExternal) {
wCtx.append(`
// Extract the recv_external from the dict
dup -1 swap @procdictkeylen idict@ { "internal shortcut error" abort } ifnot
swap`);
}

wCtx.append(`
// Extract the recv_internal from the dict
dup 0 swap @procdictkeylen idict@ { "internal shortcut error" abort } ifnot
swap

// Delete the recv_internal from the dict
0 swap @procdictkeylen idict- drop
// Delete the recv_external from the dict (it's okay if it's not there)
-1 swap @procdictkeylen idict- drop
// Delete the __tact_selector_hack from the dict
65535 swap @procdictkeylen idict- drop

// Bring the code builder from the bottom of the stack
// because if recv_external extraction is optional, and the number of elements on the stack is not fixed
depth 1- roll
// Swap with the dict from which we extracted recv_internal and (maybe) recv_external
swap

// Check if the dict is empty
dup null?
// Store a copy of this flag in the bottom of the stack
dup depth 1- -roll
{
// If the dict is empty, just drop it (it will be null if it's empty)
drop
}
{
}END> b>

// If the dict is not empty, prepare continuation to be stored in c3
<{
SETCP0 DUP
IFNOTJMP:<{
DROP over <s ref@ 0 swap @procdictkeylen idict@ { "internal shortcut error" abort } ifnot @addop
}>`);
// Save this dict as first ref in this continuation, it will be pushed in runtime by DICTPUSHCONST
swap @procdictkeylen DICTPUSHCONST
// Jump to the needed function by it's index
DICTIGETJMPZ
// If such key is not found, throw 11 along with the key as an argument
11 THROWARG
}> PUSHCONT
// Store the continuation in c3
c3 POP
} cond

// Function id is on top of the (runtime) stack
DUP IFNOTJMP:<{
// place recv_internal here
DROP swap @addop
}>`);

if (hasExternal) {
wCtx.append(`DUP -1 EQINT IFJMP:<{
DROP over <s ref@ -1 swap @procdictkeylen idict@ { "internal shortcut error" abort } ifnot @addop
}>`);
wCtx.append(`
DUP INC IFNOTJMP:<{
// place recv_external here
DROP swap @addop
}>`);
}

wCtx.append(`swap <s ref@
0 swap @procdictkeylen idict- drop
-1 swap @procdictkeylen idict- drop
65535 swap @procdictkeylen idict- drop

@procdictkeylen DICTPUSHCONST DICTIGETJMPZ 11 THROWARG
}> b>
} : }END>c
current@ context! current!
} does @atend !
""";`);

wCtx.append(`() __tact_selector_hack() method_id(65535) {
return __tact_selector_hack_asm();
}`);
wCtx.append(`
// Bring back the flag, indicating if the dict is empty or not from the bottom of the stack
depth 1- roll
{
// If the dict is empty, throw 11
11 THROWARG
}
{
// If the dict is not empty, jump to continuation from c3
c3 PUSH JMPX
} cond
}> b>
} : }END>c
current@ context! current!
} does @atend !
""";`);

wCtx.append(`
() __tact_selector_hack() method_id(65535) {
return __tact_selector_hack_asm();
}`);
}
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/pipeline/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export function enableFeatures(
{
option:
config.options.optimizations
?.internalExternalReceiversOutsideMethodsMap ?? false,
?.internalExternalReceiversOutsideMethodsMap ?? true,
name: "internalExternalReceiversOutsideMethodsMap",
},
{
Expand Down
5 changes: 5 additions & 0 deletions src/test/e2e-emulated/__snapshots__/stdlib.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`stdlib should execute stdlib methods correctly: Gas consumed in segGasLimit() 1`] = `3881`;

exports[`stdlib should execute stdlib methods correctly: tvm_2023_07Upgrade 1`] = `1439`;
1 change: 1 addition & 0 deletions src/test/e2e-emulated/asm-shuffle-in-comptime.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ describe("asm-shuffle-in-comptime", () => {

beforeEach(async () => {
blockchain = await Blockchain.create();
blockchain.verbosity.print = false;
treasure = await blockchain.treasury("treasure");

contract = blockchain.openContract(await Test.fromInit());
Expand Down
Loading