1
0
Fork 0
mirror of https://github.com/ton-blockchain/ton synced 2025-03-09 15:40:10 +00:00

[FunC] Auto-inline functions-wrappers T f(...args) { return anotherF(...args); }

This will allow to easily implement camelCase wrappers aside stdlib,
even without changing hashes of existing contracts.
Also, stdlib renamings could be easily performed in the same manner,
even with arguments reordered.
This commit is contained in:
Aleksandr Kirsanov 2024-04-26 14:27:16 +03:00
parent bac4e3df97
commit 18050a7591
No known key found for this signature in database
GPG key ID: B758BBAA01FFB3D3
12 changed files with 929 additions and 60 deletions

View file

@ -320,6 +320,9 @@ void AsmOpList::show_var_ext(std::ostream& os, std::pair<var_idx_t, const_idx_t>
os << '_' << i;
} else {
var_names_->at(i).show(os, 2);
// if (!var_names_->at(i).v_type->is_int()) {
// os << '<'; var_names_->at(i).v_type->print(os); os << '>';
// }
}
if ((unsigned)j < constants_.size() && constants_[j].not_null()) {
os << '=' << constants_[j];

View file

@ -31,7 +31,8 @@
["whales-nominators/nominators.fc", 8941364499854379927692172316865293429893094891593442801401542636695127885153]
["tact-examples/treasure_Treasure.code.fc", 13962538639825790677138656603323869918938565499584297120566680287245364723897]
["tact-examples/jetton_SampleJetton.code.fc", 94076762218493729104783735200107713211245710256802265203823917715299139499110]
["tact-examples/jetton_JettonDefaultWallet.code.fc", 29421313492520031238091587108198906058157443241743283101866538036369069620563]
// (April 2024) tact hashes changed, because '__tact_address_eq()' is now inlined as a wrapper
["tact-examples/treasure_Treasure.code.fc", 74579212939836529446778705921340099196942507859825095056546203678047252921894]
["tact-examples/jetton_SampleJetton.code.fc", 90109697379313597998231209537203822165325184678797680193687781490465875320451]
["tact-examples/jetton_JettonDefaultWallet.code.fc", 40972091374757565863193840427121230466303309310521439431197914284004190565629]
["tact-examples/maps_MapTestContract.code.fc", 22556550222249123835909180266811414538971143565993192846012583552876721649744]

View file

@ -0,0 +1,239 @@
;; Here we test "functions that just wrap other functions" (camelCase in particular):
;; > builder beginCell() { return begin_cell(); }
;; Such functions, when called, are explicitly inlined during code generation (even without `inline` modifier).
;; It means, that `beginCell()` is replaced to `begin_cell()` (and effectively to `NEWC`).
;; Moreover, body of `beginCell` is NOT codegenerated at all.
;; Hence, we can write camelCase wrappers (as well as more intelligible namings around stdlib functions)
;; without affecting performance and even bytecode hashes.
;; This works with ~functions also. And even works with wrappers of wrappers.
;; Moreover, such wrappers can reorder input parameters, see a separate test camel2.fc.
builder begin_cell() asm "NEWC";
cell end_cell(builder b) asm "ENDC";
builder store_ref(builder b, cell c) asm(c b) "STREF";
slice begin_parse(cell c) asm "CTOS";
slice skip_bits(slice s, int len) asm "SDSKIPFIRST";
(slice, ()) ~skip_bits(slice s, int len) asm "SDSKIPFIRST";
builder beginCell() { return begin_cell(); }
cell endCell(builder b) { return end_cell(b); }
builder storeRef(builder b, cell c) { return store_ref(b, c); }
builder storeUint(builder b, int i, int bw) { return store_uint(b, i, bw); }
;; 'inline' is not needed actually, but if it exists, it's just ignored
slice beginParse(cell c) inline { return begin_parse(c); }
slice skipBits(slice s, int len) inline { return skip_bits(s, len); }
(slice, ()) ~skipBits(slice s, int len) inline { return ~skip_bits(s, len); }
(slice, int) ~loadUint(slice s, int len) inline { return load_uint(s, len); }
(int, int, int) compute_data_size(cell c, int max_cells) impure asm "CDATASIZE";
(int, int, int) computeDataSize(cell c, int maxCells) impure { return compute_data_size(c, maxCells); }
cell new_dict() asm "NEWDICT";
cell idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET";
(cell, ()) ~idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET";
(slice, int) idict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGET" "NULLSWAPIFNOT";
(int, slice, int) idict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMIN" "NULLSWAPIFNOT2";
cell dict::new() { return new_dict(); }
cell dict::iset(cell dict, int keyLen, int index, slice value) { return idict_set(dict, keyLen, index, value); }
(cell, ()) ~dict::iset(cell dict, int keyLen, int index, slice value) { return ~idict_set(dict, keyLen, index, value); }
(slice, int) dict::tryIGet(cell dict, int keyLen, int index) { return idict_get?(dict, keyLen, index); }
(int, slice, int) dict::tryIGetMin(cell dict, int keyLen) { return idict_get_min?(dict, keyLen); }
tuple empty_tuple() asm "NIL";
forall X -> tuple tpush(tuple t, X value) asm "TPUSH";
forall X -> (tuple, ()) ~tpush(tuple t, X value) asm "TPUSH";
forall X, Y, Z -> Y triple_second([X, Y, Z] p) asm "SECOND";
forall X -> X null() asm "PUSHNULL";
tuple emptyTuple() { return empty_tuple(); }
tuple emptyTuple1() { return emptyTuple(); }
tuple emptyTuple11() { return emptyTuple1(); }
forall X -> tuple tuplePush(tuple t, X value) { return tpush(t, value); }
forall X -> (tuple, ()) ~tuplePush(tuple t, X value) { return ~tpush(t, value); }
forall X -> X tupleAt(tuple t, int index) { return at(t, index); }
forall X1, Y2, Z3 -> Y2 tripleSecond([X1, Y2, Z3] p) { return triple_second(p); }
forall X -> X nullValue() asm "PUSHNULL";
() throwIf(int excNo, int condition) impure { return throw_if(excNo, condition); }
tuple initial1(tuple x) { return x; }
_ initial2(x) { return initial1(x); }
int add(int x, int y) { return x + y; } ;; this is also a wrapper, as its body is _+_(x,y)
() fake1(int a, int b, int c) impure asm(a b c) "DROP DROP DROP";
() fake2(int a, int b, int c) impure asm(b c a) "DROP DROP DROP";
() fake3(int a, int b, int c) impure asm(c a b) "DROP DROP DROP";
() fake4(int a, int b, int c) impure asm(c b a) "DROP DROP DROP";
() fake1Wrapper(int a, int b, int c) { return fake1(a, b, c); }
() fake2Wrapper(int a, int b, int c) { return fake2(a, b, c); }
() fake3Wrapper(int a, int b, int c) { return fake3(a, b, c); }
() fake4Wrapper(int a, int b, int c) { return fake4(a, b, c); }
[int, int, int] test1() method_id(101) {
int x = 1;
int y = 1;
cell to_be_ref = beginCell().endCell();
builder in_c = beginCell().storeUint(123, 8);
in_c = storeRef(in_c, to_be_ref);
var (a, b, c) = computeDataSize(in_c.endCell(), 10);
throwIf(101, b != 8);
throwIf(101, c != 1);
return [a, add(b, x), add(c, y)];
}
[[int, int, int], int, int, int] test2() method_id(102) {
cell dict = dict::new();
dict = dict::iset(dict, 32, 456, beginCell().storeUint(4560, 32).endCell().beginParse());
dict.dict::iset(32, 789, beginCell().storeUint(7890, 32).endCell().beginParse());
dict~dict::iset(32, 123, beginCell().storeUint(0, 64).storeUint(1230, 32).storeUint(1231, 32).storeUint(1232, 32).endCell().beginParse());
var (mink, minv, _) = dict::tryIGetMin(dict, 32);
;; skip 64 bits
minv~skipBits(16);
minv = minv.skipBits(16);
minv.skipBits(16000); ;; does nothing
(minv, _) = ~skipBits(minv, 16);
skipBits(minv, 16000); ;; does nothing
minv~skipBits(16);
;; load 3*32
var minv1 = minv~loadUint(32);
var minv2 = minv~loadUint(32);
var minv3 = minv~loadUint(32);
var (_, found123) = dict::tryIGet(dict, 32, 123);
var (_, found456) = dict::tryIGet(dict, 32, 456);
var (_, found789) = dict::tryIGet(dict, 32, 789);
return [[minv1, minv2, minv3], found123, found456, found789];
}
tuple test3() method_id(103) {
tuple with34 = initial2(emptyTuple1());
with34~tuplePush(34);
tuple t = emptyTuple11();
t = tuplePush(t, 12);
tuplePush(t, emptyTuple11()); ;; does nothing
t~tuplePush(emptyTuple1());
t~tuplePush(with34.tupleAt(0));
t.tuplePush("123"s); ;; does nothing
[cell, int, cell] tri = [nullValue(), 90 + 1, null()];
int f = tripleSecond(tri);
(t, _) = ~tuplePush(t, f);
return t;
}
(int) test4(int a, int b, int c) method_id(104) {
fake1Wrapper(a, b, c);
fake2Wrapper(a, b, c);
fake3Wrapper(a, b, c);
fake4Wrapper(a, b, c);
return 10;
}
int main() {
return 0;
}
{-
method_id | in | out
TESTCASE | 101 | | [ 2 9 2 ]
TESTCASE | 102 | | [ [ 1230 1231 1232 ] -1 -1 0 ]
TESTCASE | 103 | | [ 12 [] 34 91 ]
@fif_codegen
"""
test1 PROC:<{
//
NEWC // _5
ENDC // to_be_ref
123 PUSHINT // to_be_ref _8=123
NEWC // to_be_ref _8=123 _9
8 STU // to_be_ref in_c
STREF // in_c
ENDC // _16
10 PUSHINT // _16 _17=10
CDATASIZE // a b c
OVER // a b c b
8 NEQINT // a b c _21
101 THROWIF
DUP // a b c c
1 NEQINT // a b c _25
101 THROWIF
SWAP // a c b
INC // a c _28
SWAP // a _28 c
INC // a _28 _29
TRIPLE // _27
}>
"""
@fif_codegen
"""
test2 PROC:<{
...
16 PUSHINT // dict minv _45=16
SDSKIPFIRST // dict minv
16 PUSHINT // dict minv _47=16
SDSKIPFIRST // dict minv
16 PUSHINT // dict minv _52=16
SDSKIPFIRST // dict minv
16 PUSHINT // dict minv _57=16
SDSKIPFIRST // dict minv
...
32 PUSHINT // dict minv1 minv2 minv3 found123 _78=456 dict _79=32
DICTIGET
NULLSWAPIFNOT // dict minv1 minv2 minv3 found123 _99 _100
789 PUSHINT
s2 POP
s0 s6 XCHG
32 PUSHINT // found456 minv1 minv2 minv3 found123 _83=789 dict _84=32
...
4 TUPLE // _86
}>
"""
@fif_codegen
"""
test3 PROC:<{
//
NIL // _1
initial1 CALLDICT // with34
...
TRIPLE // t tri
SECOND // t f
TPUSH // t
}>
"""
@fif_codegen
"""
test4 PROC:<{
// a b c
s2 s1 s0 PUSH3 // a b c a b c
DROP DROP DROP
s1 s0 s2 PUSH3 // a b c b c a
DROP DROP DROP
s0 s2 s1 PUSH3 // a b c c a b
DROP DROP DROP
s0 s2 XCHG // c b a
DROP DROP DROP
10 PUSHINT // _7=10
}>
"""
@fif_codegen_avoid DECLPROC beginCell
@fif_codegen_avoid DECLPROC storeUint
@fif_codegen_avoid DECLPROC storeRef
@fif_codegen_avoid DECLPROC computeDataSize
@fif_codegen_avoid DECLPROC tryIdictGet
@fif_codegen_avoid DECLPROC emptyTuple
@fif_codegen_avoid DECLPROC storeUint1
@fif_codegen_avoid DECLPROC initial2
@fif_codegen_avoid DECLPROC add
-}

View file

@ -0,0 +1,208 @@
;; Here we also test "functions that just wrap other functions" like in camel1.fc,
;; but when they reorder arguments, e.g.
;; > T f(x,y) { return anotherF(y,x); }
;; This also works, even for wrappers of wrappers, even if anotherF is asm(with reorder).
;; But swapping arguments may sometimes lead to bytecode changes (see test2),
;; both with compute-asm-ltr and without it.
#pragma compute-asm-ltr;
builder begin_cell() asm "NEWC";
cell end_cell(builder b) asm "ENDC";
slice begin_parse(cell c) asm "CTOS";
builder store_ref(builder b, cell c) asm(c b) "STREF";
builder beginCell() { return begin_cell(); }
cell endCell(builder b) { return end_cell(b); }
builder storeRef1(builder b, cell c) { return store_ref(b, c); }
builder storeRef2(cell c, builder b) { return store_ref(b, c); }
builder storeUint1(builder b, int x, int bw) { return store_uint(b, x, bw); }
builder storeUint2(builder b, int bw, int x) { return store_uint(b, x, bw); }
(int, int, int) compute_data_size(cell c, int max_cells) impure asm "CDATASIZE";
(int, int, int) computeDataSize1(cell c, int maxCells) impure { return compute_data_size(c, maxCells); }
(int, int, int) computeDataSize2(int maxCells, cell c) impure { return compute_data_size(c, maxCells); }
() throwIf(int condition, int excNo) impure { return throw_if(excNo, condition); }
() fake(int a, int b, int c) impure asm "DROP DROP DROP";
() fake2(int b, int c, int a) { return fake(a,b,c); }
() fake3(int c, int a, int b) { return fake(a,b,c); }
() fake4(int c, int b, int a) { return fake(a,b,c); }
(int, int, int) test1() method_id(101) {
int x = 1;
int y = 1;
cell to_be_ref = beginCell().endCell();
builder in_c = beginCell().storeUint1(123, 8);
in_c = storeRef1(in_c, to_be_ref);
var (a, b, c) = computeDataSize1(in_c.endCell(), 10);
throwIf(0, 101);
return (a, b + x, c + y);
}
(int, int, int) test2() method_id(102) {
int x = 1;
int y = 1;
cell to_be_ref = beginCell().endCell();
builder in_c = beginCell().storeUint2(8, 123);
in_c = storeRef2(to_be_ref, in_c);
var (a, b, c) = computeDataSize2(10, in_c.endCell());
return (a, b + x, c + y);
}
(int, int, int) test3() method_id(103) {
int x = 1;
int y = 1;
cell to_be_ref = begin_cell().end_cell();
builder in_c = begin_cell().store_uint(123, 8);
in_c = store_ref(in_c, to_be_ref);
var (a, b, c) = compute_data_size(in_c.end_cell(), 10);
return (a, b + x, c + y);
}
builder beginCell1() { return begin_cell(); }
builder beginCell11() { return beginCell1(); }
builder beginCell111() { return beginCell11(); }
cell endCell1(builder b) { return end_cell(b); }
cell endCell11(builder b) { return endCell1(b); }
slice beginParse1(cell c) { return begin_parse(c); }
slice beginParse11(cell c) { return beginParse1(c); }
builder storeInt1(builder b, int bw, int x) { return store_int(b, x, bw); }
builder storeInt11(int bw, int x, builder b) { return storeInt1(b, bw, x); }
builder storeInt111(builder b, int x, int bw) { return storeInt11(bw, x, b); }
slice test4() method_id(104) {
builder b = beginCell111();
b = storeInt11(32, 1, b);
b = storeInt111(b, 2, 32).storeInt111(3, 32);
return b.endCell11().beginParse11();
}
(int) test5(int a, int b, int c) method_id(105) {
fake(a, b, c);
fake2(b, c, a);
fake3(c, a, b);
fake4(c, b, a);
return a;
}
() main() {
throw(0);
}
{-
method_id | in | out
TESTCASE | 101 | | 2 9 2
TESTCASE | 102 | | 2 9 2
TESTCASE | 103 | | 2 9 2
TESTCASE | 104 | | CS{Cell{0018000000010000000200000003} bits: 0..96; refs: 0..0}
test1 and test3 fif code is absolutely identical, test2 (due to reorder) is a bit different:
@fif_codegen
"""
test1 PROC:<{
//
NEWC // _5
ENDC // to_be_ref
NEWC // to_be_ref _8
123 PUSHINT // to_be_ref _8 _9=123
SWAP // to_be_ref _9=123 _8
8 STU // to_be_ref in_c
STREF // in_c
ENDC // _16
10 PUSHINT // _16 _17=10
CDATASIZE // a b c
SWAP // a c b
INC // a c _22
SWAP // a _22 c
INC // a _22 _23
}>
"""
@fif_codegen
"""
test2 PROC:<{
//
NEWC // _5
ENDC // to_be_ref
NEWC // to_be_ref _8
123 PUSHINT // to_be_ref _8 _10=123
SWAP // to_be_ref _10=123 _8
8 STU // to_be_ref in_c
STREF // in_c
10 PUSHINT
SWAP
ENDC
SWAP
CDATASIZE // a b c
SWAP // a c b
INC // a c _19
SWAP // a _19 c
INC // a _19 _20
}>
"""
@fif_codegen
"""
test3 PROC:<{
//
NEWC // _5
ENDC // to_be_ref
NEWC // to_be_ref _8
123 PUSHINT // to_be_ref _8 _9=123
SWAP // to_be_ref _9=123 _8
8 STU // to_be_ref in_c
STREF // in_c
ENDC // _16
10 PUSHINT // _16 _17=10
CDATASIZE // a b c
SWAP // a c b
INC // a c _19
SWAP // a _19 c
INC // a _19 _20
}>
"""
@fif_codegen
"""
test4 PROC:<{
//
NEWC // b
1 PUSHINT // b _3=1
SWAP // _3=1 b
32 STI // b
2 PUSHINT
SWAP // _5=2 b
32 STI
3 PUSHINT
SWAP
32 STI // b
ENDC // _11
CTOS // _12
}>
"""
@fif_codegen
"""
test5 PROC:<{
// a b c
s2 s1 s0 PUSH3 // a b c a b c
DROP DROP DROP
s2 s1 s0 PUSH3 // a b c a b c
DROP DROP DROP
s2 s1 s0 PUSH3 // a b c a b c
DROP DROP DROP
s2 PUSH
-ROT // a a b c
DROP DROP DROP
}>
"""
@fif_codegen_avoid storeUint1
@fif_codegen_avoid storeUint2
-}

View file

@ -0,0 +1,104 @@
;; Here we test that if you declare a wrapper like
;; > builder beginCell() { return begin_cell(); }
;; but use it NOT only as a direct call, BUT as a 1-st class function
;; (save to a variable, return from a function, etc.)
;; it also works, since a function becomes codegenerated (though direct calls are expectedly inlined).
builder begin_cell() asm "NEWC";
cell end_cell(builder b) asm "ENDC";
builder store_ref(builder b, cell c) asm(c b) "STREF";
builder beginCell() { return begin_cell(); }
cell endCell(builder b) { return end_cell(b); }
builder storeRef(builder b, cell c) { return store_ref(b, c); }
builder storeUint3(int i, int bw, builder b) { return store_uint(b, i, bw); }
(int, int, int) compute_data_size(cell c, int max_cells) impure asm "CDATASIZE";
(int, int, int) computeDataSize2(int maxCells, cell c) impure { return compute_data_size(c, maxCells); }
tuple empty_tuple() asm "NIL";
forall X -> tuple tpush(tuple t, X value) asm "TPUSH";
forall X -> (tuple, ()) ~tpush(tuple t, X value) asm "TPUSH";
forall X -> X first(tuple t) asm "FIRST";
tuple emptyTuple() { return empty_tuple(); }
forall X -> tuple tuplePush(tuple t, X value) { return tpush(t, value); }
forall X -> (tuple, ()) ~tuplePush(tuple t, X value) { return ~tpush(t, value); }
forall X -> X tupleGetFirst(tuple t) { return first(t); }
(var, var) getBeginEnd() inline {
return (beginCell, endCell);
}
(builder) begAndStore(beg, store, int x) {
return store(x, 8, beg());
}
(int, int, int) test1() {
var (_, computer) = (0, computeDataSize2);
var (beg, end) = getBeginEnd();
tuple t = emptyTuple();
t~tuplePush(storeRef);
var refStorer = tupleGetFirst(t);
int x = 1;
int y = 1;
cell to_be_ref = beginCell().endCell();
builder in_c = begAndStore(beg, storeUint3, 123);
in_c = refStorer(in_c, to_be_ref);
var (a, b, c) = computer(10, end(in_c));
return (a, b + x, c + y);
}
(int, int, int) main() {
return test1();
}
{-
method_id | in | out
TESTCASE | 0 | | 2 9 2
@fif_codegen DECLPROC beginCell
@fif_codegen DECLPROC computeDataSize2
@fif_codegen
"""
storeUint3 PROC:<{
// i bw b
SWAP // i b bw
STUX // _3
}>
"""
@fif_codegen
"""
storeRef PROC:<{
// b c
SWAP // c b
STREF // _2
}>
"""
@fif_codegen
"""
CONT:<{
computeDataSize2 CALLDICT
}> // computer
getBeginEnd INLINECALLDICT // computer beg end
NIL // computer beg end t
...
NEWC // computer beg end refStorer _19
ENDC // computer beg end refStorer to_be_ref
...
CONT:<{
storeUint3 CALLDICT
}>
...
begAndStore CALLDICT // computer to_be_ref end refStorer in_c
"""
@fif_codegen_avoid emptyTuple
@fif_codegen_avoid tuplePush
-}

View file

@ -0,0 +1,130 @@
;; Here we test that a just-return function is not a valid wrapper, it will not be inlined.
;; (doesn't use all arguments, has different pureness, has method_id, etc.)
builder begin_cell() asm "NEWC";
cell end_cell(builder b) asm "ENDC";
slice begin_parse(cell c) asm "CTOS";
() set_data(cell c) impure asm "c4 POP";
cell get_data() asm "c4 PUSH";
tuple empty_tuple() asm "NIL";
forall X -> (tuple, ()) tpush(tuple t, X x) asm "TPUSH";
builder storeUint(builder b, int x, int unused) { return store_uint(b, x, x); }
() throwIf(int excNo, int cond) impure { throw_if(excNo, cond); }
() throwIf2(int excNo, int cond) { return throw_if(excNo, cond); }
_ initial1(x) { return x; }
_ initial2(x) { return initial1(x); }
tuple asm_func_4(int a, (int, (int, int)) b, int c) asm (b a c -> 0) "5 TUPLE";
_ asmFunc4(int a, (int, (int, int)) b, int c) { return asm_func_4(a, b, c); }
int postpone_elections() {
return false;
}
(int) setAndGetData(int ret) {
cell c = begin_cell().store_uint(ret, 8).end_cell();
set_data(c);
slice s = get_data().begin_parse();
throwIf(101, 0);
var unused = throwIf2(101, 0);
return s~load_uint(8);
}
int setAndGetDataWrapper(int ret) {
return setAndGetData(ret);
}
int test1() method_id(101) {
cell c = begin_cell().storeUint(32, 10000000).end_cell();
slice s = c.begin_parse();
return s~load_uint(32);
}
int test2(int ret) method_id {
return setAndGetDataWrapper(ret);
}
int test3() method_id(103) {
return initial2(10);
}
global tuple t;
int foo(int x) {
t~tpush(x);
return x * 10;
}
(tuple, tuple) test4() method_id(104) {
t = empty_tuple();
tuple t2 = asmFunc4(foo(11), (foo(22), (foo(33), foo(44))), foo(55));
return (t, t2);
}
int test5() method_id(105) {
if (1) {
return postpone_elections();
}
return 123;
}
int main(int ret) {
return setAndGetDataWrapper(ret);
}
int recv_external(int ret) {
return setAndGetData(ret);
}
{-
method_id | in | out
TESTCASE | 101 | | 32
TESTCASE | 103 | | 10
TESTCASE | 104 | | [ 11 22 33 44 55 ] [ 220 330 440 110 550 ]
TESTCASE | 105 | | 0
TESTCASE | 74435 | 99 | 99
TESTCASE | 0 | 98 | 98
TESTCASE | -1 | 97 | 97
@fif_codegen DECLPROC storeUint
@fif_codegen DECLPROC throwIf
@fif_codegen DECLPROC throwIf2
@fif_codegen DECLPROC postpone_elections
@fif_codegen 74435 DECLMETHOD test2
@fif_codegen
"""
test3 PROC:<{
//
10 PUSHINT // _0=10
initial2 CALLDICT // _1
}>
"""
@fif_codegen
"""
test2 PROC:<{
// ret
setAndGetData CALLDICT // _1
}>
"""
@fif_codegen
"""
11 PUSHINT
foo CALLDICT
22 PUSHINT
foo CALLDICT
33 PUSHINT
foo CALLDICT
44 PUSHINT
foo CALLDICT
55 PUSHINT
foo CALLDICT
asmFunc4 CALLDICT // t2
"""
@fif_codegen_avoid setAndGetDataWrapper
-}

View file

@ -439,6 +439,7 @@ bool Op::generate_code_step(Stack& stack) {
if (disabled()) {
return true;
}
// fun_ref can be nullptr for Op::_CallInd (invoke a variable, not a function)
SymValFunc* func = (fun_ref ? dynamic_cast<SymValFunc*>(fun_ref->value) : nullptr);
auto arg_order = (func ? func->get_arg_order() : nullptr);
auto ret_order = (func ? func->get_ret_order() : nullptr);
@ -488,27 +489,24 @@ bool Op::generate_code_step(Stack& stack) {
};
if (cl == _CallInd) {
exec_callxargs((int)right.size() - 1, (int)left.size());
} else if (auto asm_fv = dynamic_cast<const SymValAsmFunc*>(fun_ref->value)) {
std::vector<VarDescr> res;
res.reserve(left.size());
for (var_idx_t i : left) {
res.emplace_back(i);
}
asm_fv->compile(stack.o, res, args, where); // compile res := f (args)
} else {
auto func = dynamic_cast<const SymValAsmFunc*>(fun_ref->value);
if (func) {
std::vector<VarDescr> res;
res.reserve(left.size());
for (var_idx_t i : left) {
res.emplace_back(i);
}
func->compile(stack.o, res, args, where); // compile res := f (args)
auto fv = dynamic_cast<const SymValCodeFunc*>(fun_ref->value);
// todo can be fv == nullptr?
std::string name = sym::symbols.get_name(fun_ref->sym_idx);
if (fv && (fv->is_inline() || fv->is_inline_ref())) {
stack.o << AsmOp::Custom(name + " INLINECALLDICT", (int)right.size(), (int)left.size());
} else if (fv && fv->code && fv->code->require_callxargs) {
stack.o << AsmOp::Custom(name + (" PREPAREDICT"), 0, 2);
exec_callxargs((int)right.size() + 1, (int)left.size());
} else {
auto fv = dynamic_cast<const SymValCodeFunc*>(fun_ref->value);
std::string name = sym::symbols.get_name(fun_ref->sym_idx);
bool is_inline = (fv && (fv->flags & 3));
if (is_inline) {
stack.o << AsmOp::Custom(name + " INLINECALLDICT", (int)right.size(), (int)left.size());
} else if (fv && fv->code && fv->code->require_callxargs) {
stack.o << AsmOp::Custom(name + (" PREPAREDICT"), 0, 2);
exec_callxargs((int)right.size() + 1, (int)left.size());
} else {
stack.o << AsmOp::Custom(name + " CALLDICT", (int)right.size(), (int)left.size());
}
stack.o << AsmOp::Custom(name + " CALLDICT", (int)right.size(), (int)left.size());
}
}
stack.s.resize(k);

View file

@ -43,6 +43,35 @@ GlobalPragma pragma_compute_asm_ltr{"compute-asm-ltr"};
std::string generated_from, boc_output_filename;
ReadCallback::Callback read_callback;
// returns argument type of a function
// note, that when a function has multiple arguments, its arg type is a tensor (no arguments — an empty tensor)
// in other words, `f(int a, int b)` and `f((int,int) ab)` is the same when we speak about types
const TypeExpr *SymValFunc::get_arg_type() const {
if (!sym_type)
return nullptr;
func_assert(sym_type->constr == TypeExpr::te_Map || sym_type->constr == TypeExpr::te_ForAll);
const TypeExpr *te_map = sym_type->constr == TypeExpr::te_ForAll ? sym_type->args[0] : sym_type;
const TypeExpr *arg_type = te_map->args[0];
while (arg_type->constr == TypeExpr::te_Indirect) {
arg_type = arg_type->args[0];
}
return arg_type;
}
bool SymValCodeFunc::does_need_codegen() const {
if (flags & flagUsedAsNonCall) {
return true;
}
// when a function f() is just `return anotherF(...args)`, it doesn't need to be codegenerated at all,
// since all its usages are inlined
return !is_just_wrapper_for_another_f();
// in the future, we may want to implement a true AST inlining for `inline` functions also
// in the future, unused functions may also be excluded from codegen
}
td::Result<std::string> fs_read_callback(ReadCallback::Kind kind, const char* query) {
switch (kind) {
case ReadCallback::Kind::ReadFile: {
@ -124,12 +153,10 @@ void generate_output_func(SymDef* func_sym, std::ostream &outs, std::ostream &er
if (verbosity >= 2) {
errs << "\n---------- resulting code for " << name << " -------------\n";
}
bool inline_func = (func_val->flags & 1);
bool inline_ref = (func_val->flags & 2);
const char* modifier = "";
if (inline_func) {
if (func_val->is_inline()) {
modifier = "INLINE";
} else if (inline_ref) {
} else if (func_val->is_inline_ref()) {
modifier = "REF";
}
outs << std::string(indent * 2, ' ') << name << " PROC" << modifier << ":<{\n";
@ -140,12 +167,10 @@ void generate_output_func(SymDef* func_sym, std::ostream &outs, std::ostream &er
if (opt_level < 2) {
mode |= Stack::_DisableOpt;
}
auto fv = dynamic_cast<const SymValCodeFunc*>(func_sym->value);
// Flags: 1 - inline, 2 - inline_ref
if (fv && (fv->flags & 1) && code.ops->noreturn()) {
if (func_val->is_inline() && code.ops->noreturn()) {
mode |= Stack::_InlineFunc;
}
if (fv && (fv->flags & 3)) {
if (func_val->is_inline() || func_val->is_inline_ref()) {
mode |= Stack::_InlineAny;
}
code.generate_code(outs, mode, indent + 1);
@ -167,6 +192,13 @@ int generate_output(std::ostream &outs, std::ostream &errs) {
for (SymDef* func_sym : glob_func) {
SymValCodeFunc* func_val = dynamic_cast<SymValCodeFunc*>(func_sym->value);
func_assert(func_val);
if (!func_val->does_need_codegen()) {
if (verbosity >= 2) {
errs << func_sym->name() << ": code not generated, function does not need codegen\n";
}
continue;
}
std::string name = sym::symbols.get_name(func_sym->sym_idx);
outs << std::string(indent * 2, ' ');
if (func_val->method_id.is_null()) {
@ -182,6 +214,10 @@ int generate_output(std::ostream &outs, std::ostream &errs) {
}
int errors = 0;
for (SymDef* func_sym : glob_func) {
SymValCodeFunc* func_val = dynamic_cast<SymValCodeFunc*>(func_sym->value);
if (!func_val->does_need_codegen()) {
continue;
}
try {
generate_output_func(func_sym, outs, errs);
} catch (src::Error& err) {
@ -259,4 +295,4 @@ int func_proceed(const std::vector<std::string> &sources, std::ostream &outs, st
return 0;
}
} // namespace funC
} // namespace funC

View file

@ -219,6 +219,8 @@ struct TypeExpr {
std::ostream& print(std::ostream& os, int prio = 0) const;
void replace_with(TypeExpr* te2);
int extract_components(std::vector<TypeExpr*>& comp_list);
bool equals_to(const TypeExpr* rhs) const;
bool has_unknown_inside() const;
static int holes, type_vars;
static TypeExpr* new_hole() {
return new TypeExpr{te_Unknown, ++holes};
@ -752,26 +754,27 @@ struct CodeBlob {
struct SymVal : sym::SymValBase {
TypeExpr* sym_type;
td::RefInt256 method_id;
bool impure;
bool auto_apply{false};
short flags; // +1 = inline, +2 = inline_ref
SymVal(int _type, int _idx, TypeExpr* _stype = nullptr, bool _impure = false)
: sym::SymValBase(_type, _idx), sym_type(_stype), impure(_impure), flags(0) {
: sym::SymValBase(_type, _idx), sym_type(_stype), impure(_impure) {
}
~SymVal() override = default;
TypeExpr* get_type() const {
return sym_type;
}
virtual const std::vector<int>* get_arg_order() const {
return nullptr;
}
virtual const std::vector<int>* get_ret_order() const {
return nullptr;
}
};
struct SymValFunc : SymVal {
enum SymValFlag {
flagInline = 1, // function marked `inline`
flagInlineRef = 2, // function marked `inline_ref`
flagWrapsAnotherF = 4, // (T) thisF(...args) { return anotherF(...args); } (calls to thisF will be replaced)
flagUsedAsNonCall = 8, // used not only as `f()`, but as a 1-st class function (assigned to var, pushed to tuple, etc.)
};
td::RefInt256 method_id; // todo why int256? it's small
int flags{0};
std::vector<int> arg_order, ret_order;
#ifdef FUNC_DEBUG
std::string name; // seeing function name in debugger makes it much easier to delve into FunC sources
@ -784,12 +787,23 @@ struct SymValFunc : SymVal {
: SymVal(_Func, val, _ft, _impure), arg_order(_arg_order), ret_order(_ret_order) {
}
const std::vector<int>* get_arg_order() const override {
const std::vector<int>* get_arg_order() const {
return arg_order.empty() ? nullptr : &arg_order;
}
const std::vector<int>* get_ret_order() const override {
const std::vector<int>* get_ret_order() const {
return ret_order.empty() ? nullptr : &ret_order;
}
const TypeExpr* get_arg_type() const;
bool is_inline() const {
return flags & flagInline;
}
bool is_inline_ref() const {
return flags & flagInlineRef;
}
bool is_just_wrapper_for_another_f() const {
return flags & flagWrapsAnotherF;
}
};
struct SymValCodeFunc : SymValFunc {
@ -797,6 +811,7 @@ struct SymValCodeFunc : SymValFunc {
~SymValCodeFunc() override = default;
SymValCodeFunc(int val, TypeExpr* _ft, bool _impure = false) : SymValFunc(val, _ft, _impure), code(nullptr) {
}
bool does_need_codegen() const;
};
struct SymValType : sym::SymValBase {
@ -910,7 +925,7 @@ struct Expr {
_Tensor,
_Const,
_Var,
_Glob,
_GlobFunc,
_GlobVar,
_Letop,
_LetFirst,

View file

@ -340,16 +340,36 @@ std::vector<var_idx_t> Expr::pre_compile(CodeBlob& code, std::vector<std::pair<S
}
case _Apply: {
func_assert(sym);
auto func = dynamic_cast<SymValFunc*>(sym->value);
std::vector<var_idx_t> res;
if (func && func->arg_order.size() == args.size() && !(code.flags & CodeBlob::_ComputeAsmLtr)) {
SymDef* applied_sym = sym;
auto func = dynamic_cast<SymValFunc*>(applied_sym->value);
// replace `beginCell()` with `begin_cell()`
if (func && func->is_just_wrapper_for_another_f()) {
// body is { Op::_Import; Op::_Call; Op::_Return; }
const Op *op_call = dynamic_cast<SymValCodeFunc*>(func)->code->ops.get();
while (op_call->cl != Op::_Call) {
op_call = op_call->next.get();
}
applied_sym = op_call->fun_ref;
func = dynamic_cast<SymValFunc*>(applied_sym->value);
// soon we'll get rid of this pragma: it will be always on, we'll pass just {} here and below
bool compute_asm_ltr = code.flags & CodeBlob::_ComputeAsmLtr;
// a function may call anotherF with shuffled arguments: f(x,y) { return anotherF(y,x) }
// then op_call looks like (_1,_0), so use op_call->right for correct positions in Op::_Call below
// it's correct, since every argument has width 1
std::vector<var_idx_t> res_inner = pre_compile_tensor(args, code, lval_globs, compute_asm_ltr ? std::vector<var_idx_t>{} : func->arg_order);
res.reserve(res_inner.size());
for (var_idx_t right_idx : op_call->right) {
res.emplace_back(res_inner[right_idx]);
}
} else if (func && func->arg_order.size() == args.size() && !(code.flags & CodeBlob::_ComputeAsmLtr)) {
//std::cerr << "!!! reordering " << args.size() << " arguments of " << sym->name() << std::endl;
res = pre_compile_tensor(args, code, lval_globs, func->arg_order);
} else {
res = pre_compile_tensor(args, code, lval_globs, {});
}
auto rvect = new_tmp_vect(code);
auto& op = code.emplace_back(here, Op::_Call, rvect, std::move(res), sym);
auto& op = code.emplace_back(here, Op::_Call, rvect, res, applied_sym);
if (flags & _IsImpure) {
op.flags |= Op::_Impure;
}
@ -364,7 +384,7 @@ std::vector<var_idx_t> Expr::pre_compile(CodeBlob& code, std::vector<std::pair<S
}
return {val};
case _VarApply:
if (args[0]->cls == _Glob) {
if (args[0]->cls == _GlobFunc) {
auto res = args[1]->pre_compile(code);
auto rvect = new_tmp_vect(code);
auto& op = code.emplace_back(here, Op::_Call, rvect, std::move(res), args[0]->sym);
@ -388,8 +408,14 @@ std::vector<var_idx_t> Expr::pre_compile(CodeBlob& code, std::vector<std::pair<S
code.emplace_back(here, Op::_IntConst, rvect, intval);
return rvect;
}
case _Glob:
case _GlobFunc:
case _GlobVar: {
if (auto fun_ref = dynamic_cast<SymValFunc*>(sym->value)) {
fun_ref->flags |= SymValFunc::flagUsedAsNonCall;
if (!fun_ref->arg_order.empty() || !fun_ref->ret_order.empty()) {
throw src::ParseError(here, "Saving " + sym->name() + " into a variable will most likely lead to invalid usage, since it changes the order of variables on the stack");
}
}
auto rvect = new_tmp_vect(code);
if (lval_globs) {
lval_globs->push_back({ sym, rvect[0] });

View file

@ -423,8 +423,8 @@ bool check_global_func(const Lexem& cur, sym_idx_t func_name = 0) {
}
Expr* make_func_apply(Expr* fun, Expr* x) {
Expr* res;
if (fun->cls == Expr::_Glob) {
Expr* res{nullptr};
if (fun->cls == Expr::_GlobFunc) {
if (x->cls == Expr::_Tensor) {
res = new Expr{Expr::_Apply, fun->sym, x->args};
} else {
@ -664,7 +664,7 @@ Expr* parse_expr100(Lexer& lex, CodeBlob& code, bool nv) {
lex.cur().error_at("undefined identifier `", "`");
} else if (val->type == SymVal::_Func) {
res->e_type = val->get_type();
res->cls = Expr::_Glob;
res->cls = Expr::_GlobFunc;
auto_apply = val->auto_apply;
} else if (val->idx < 0) {
lex.cur().error_at("accessing variable `", "` being defined");
@ -1435,6 +1435,78 @@ TypeExpr* compute_type_closure(TypeExpr* expr, const std::vector<TypeExpr*>& typ
return expr;
}
// if a function looks like `T f(...args) { return anotherF(...args); }`,
// set a bit to flags
// then, all calls to `f(...)` will be effectively replaced with `anotherF(...)`
void detect_if_function_just_wraps_another(SymValCodeFunc* v_current, const td::RefInt256 &method_id) {
const std::string& function_name = v_current->code->name;
// in "AST" representation, the first is Op::_Import (input arguments, even if none)
const auto& op_import = v_current->code->ops;
func_assert(op_import && op_import->cl == Op::_Import);
// then Op::_Call (anotherF)
// when pragma allow-post-modification, it might be prepended with empty Op::_Let todo I don't like this
const Op* op_call = op_import->next.get();
while (op_call && op_call->cl == Op::_Let && op_call->disabled())
op_call = op_call->next.get();
if (!op_call || op_call->cl != Op::_Call)
return;
func_assert(op_call->left.size() == 1);
const auto& op_return = op_call->next;
if (!op_return || op_return->cl != Op::_Return || op_return->left.size() != 1)
return;
bool indices_expected = op_import->left.size() == op_call->left[0] && op_call->left[0] == op_return->left[0];
if (!indices_expected)
return;
const SymDef* f_called = op_call->fun_ref;
const SymValFunc* v_called = dynamic_cast<SymValFunc*>(f_called->value);
if (!v_called)
return;
// `return` must use all arguments, e.g. `return (_0,_2,_1)`, not `return (_0,_1,_1)`
int args_used_mask = 0;
for (var_idx_t arg_idx : op_call->right) {
args_used_mask |= 1 << arg_idx;
}
if (args_used_mask != (1 << op_call->right.size()) - 1)
return;
// detect getters (having method_id), they should not be treated as wrappers
// v_current->method_id will be assigned later; todo refactor function parsing completely, it's weird
// moreover, `recv_external()` and others are also exported, but FunC is unaware of method_id
// (it's assigned by Fift later)
// so, for now, just handle "special" function names, the same as in Asm.fif
if (!method_id.is_null())
return;
if (function_name == "main" || function_name == "recv_internal" || function_name == "recv_external" ||
function_name == "run_ticktock" || function_name == "split_prepare" || function_name == "split_install")
return;
// all types must be strictly defined (on mismatch, a compilation error will be triggered anyway)
if (v_called->sym_type->has_unknown_inside() || v_current->sym_type->has_unknown_inside())
return;
// avoid situations like `f(int a, (int,int) b)`, inlining will be cumbersome
if (v_current->get_arg_type()->get_width() != op_call->right.size())
return;
// 'return true;' (false, nil) are (surprisingly) also function calls, with auto_apply=true
if (v_called->auto_apply)
return;
// they must have the same pureness
if (v_called->impure != v_current->impure || v_current->is_inline_ref())
return;
// ok, f_current is a wrapper
v_current->flags |= SymValFunc::flagWrapsAnotherF;
if (verbosity >= 2) {
std::cerr << function_name << " -> " << f_called->name() << std::endl;
}
}
void parse_func_def(Lexer& lex) {
SrcLocation loc{lex.cur().loc};
sym::open_scope(lex);
@ -1453,9 +1525,12 @@ void parse_func_def(Lexer& lex) {
if (impure) {
lex.next();
}
int f = 0;
if (lex.tp() == _Inline || lex.tp() == _InlineRef) {
f = (lex.tp() == _Inline) ? 1 : 2;
int flags_inline = 0;
if (lex.tp() == _Inline) {
flags_inline = SymValFunc::flagInline;
lex.next();
} else if (lex.tp() == _InlineRef) {
flags_inline = SymValFunc::flagInlineRef;
lex.next();
}
td::RefInt256 method_id;
@ -1533,6 +1608,7 @@ void parse_func_def(Lexer& lex) {
code->loc = loc;
// code->print(std::cerr); // !!!DEBUG!!!
func_sym_code->code = code;
detect_if_function_just_wraps_another(func_sym_code, method_id);
} else {
Lexem asm_lexem = lex.cur();
SymValAsmFunc* asm_func = parse_asm_func_body(lex, func_type, arg_list, ret_type, impure);
@ -1555,7 +1631,7 @@ void parse_func_def(Lexer& lex) {
func_sym->value = asm_func;
}
if (method_id.not_null()) {
auto val = dynamic_cast<SymVal*>(func_sym->value);
auto val = dynamic_cast<SymValFunc*>(func_sym->value);
if (!val) {
lex.cur().error("cannot set method id for unknown function `"s + func_name.str + "`");
}
@ -1566,14 +1642,14 @@ void parse_func_def(Lexer& lex) {
val->method_id->to_dec_string() + " to a different value " + method_id->to_dec_string());
}
}
if (f) {
auto val = dynamic_cast<SymVal*>(func_sym->value);
if (flags_inline) {
auto val = dynamic_cast<SymValFunc*>(func_sym->value);
if (!val) {
lex.cur().error("cannot set unknown function `"s + func_name.str + "` as an inline");
}
if (!(val->flags & 3)) {
val->flags = (short)(val->flags | f);
} else if ((val->flags & 3) != f) {
if (!val->is_inline() && !val->is_inline_ref()) {
val->flags |= flags_inline;
} else if ((val->flags & (SymValFunc::flagInline | SymValFunc::flagInlineRef)) != flags_inline) {
lex.cur().error("inline mode for `"s + func_name.str + "` changed with respect to a previous declaration");
}
}

View file

@ -115,6 +115,39 @@ int TypeExpr::extract_components(std::vector<TypeExpr*>& comp_list) {
return res;
}
bool TypeExpr::equals_to(const TypeExpr *rhs) const {
const TypeExpr *l = this;
const TypeExpr *r = rhs;
while (l->constr == te_Indirect)
l = l->args[0];
while (r->constr == te_Indirect)
r = r->args[0];
bool eq = l->constr == r->constr && l->value == r->value &&
l->minw == r->minw && l->maxw == r->maxw &&
l->was_forall_var == r->was_forall_var &&
l->args.size() == r->args.size();
if (!eq)
return false;
for (int i = 0; i < l->args.size(); ++i) {
if (!l->args[i]->equals_to(r->args[i]))
return false;
}
return true;
}
bool TypeExpr::has_unknown_inside() const {
if (constr == te_Unknown)
return true;
for (const TypeExpr* inner : args) {
if (inner->has_unknown_inside())
return true;
}
return false;
}
TypeExpr* TypeExpr::new_map(TypeExpr* from, TypeExpr* to) {
return new TypeExpr{te_Map, std::vector<TypeExpr*>{from, to}};
}