diff --git a/crypto/func/asmops.cpp b/crypto/func/asmops.cpp index 71ee58f6..960cacde 100644 --- a/crypto/func/asmops.cpp +++ b/crypto/func/asmops.cpp @@ -320,6 +320,9 @@ void AsmOpList::show_var_ext(std::ostream& os, std::pair 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]; diff --git a/crypto/func/auto-tests/legacy_tests.jsonl b/crypto/func/auto-tests/legacy_tests.jsonl index 15cd7275..0075bf83 100644 --- a/crypto/func/auto-tests/legacy_tests.jsonl +++ b/crypto/func/auto-tests/legacy_tests.jsonl @@ -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] diff --git a/crypto/func/auto-tests/tests/camel1.fc b/crypto/func/auto-tests/tests/camel1.fc new file mode 100644 index 00000000..1146f697 --- /dev/null +++ b/crypto/func/auto-tests/tests/camel1.fc @@ -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 +-} diff --git a/crypto/func/auto-tests/tests/camel2.fc b/crypto/func/auto-tests/tests/camel2.fc new file mode 100644 index 00000000..32c32e08 --- /dev/null +++ b/crypto/func/auto-tests/tests/camel2.fc @@ -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 +-} diff --git a/crypto/func/auto-tests/tests/camel3.fc b/crypto/func/auto-tests/tests/camel3.fc new file mode 100644 index 00000000..4ae2fc41 --- /dev/null +++ b/crypto/func/auto-tests/tests/camel3.fc @@ -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 +-} diff --git a/crypto/func/auto-tests/tests/camel4.fc b/crypto/func/auto-tests/tests/camel4.fc new file mode 100644 index 00000000..1f014dc0 --- /dev/null +++ b/crypto/func/auto-tests/tests/camel4.fc @@ -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 +-} diff --git a/crypto/func/codegen.cpp b/crypto/func/codegen.cpp index de45c841..610bd2a9 100644 --- a/crypto/func/codegen.cpp +++ b/crypto/func/codegen.cpp @@ -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(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(fun_ref->value)) { + std::vector 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(fun_ref->value); - if (func) { - std::vector 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(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(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); diff --git a/crypto/func/func.cpp b/crypto/func/func.cpp index 39648c05..42cea251 100644 --- a/crypto/func/func.cpp +++ b/crypto/func/func.cpp @@ -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 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(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(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(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 &sources, std::ostream &outs, st return 0; } -} // namespace funC \ No newline at end of file +} // namespace funC diff --git a/crypto/func/func.h b/crypto/func/func.h index f4756edd..e968978c 100644 --- a/crypto/func/func.h +++ b/crypto/func/func.h @@ -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& 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* get_arg_order() const { - return nullptr; - } - virtual const std::vector* 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 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* get_arg_order() const override { + const std::vector* get_arg_order() const { return arg_order.empty() ? nullptr : &arg_order; } - const std::vector* get_ret_order() const override { + const std::vector* 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, diff --git a/crypto/func/gen-abscode.cpp b/crypto/func/gen-abscode.cpp index fe2d1850..60e8c8e7 100644 --- a/crypto/func/gen-abscode.cpp +++ b/crypto/func/gen-abscode.cpp @@ -340,16 +340,36 @@ std::vector Expr::pre_compile(CodeBlob& code, std::vector(sym->value); std::vector res; - if (func && func->arg_order.size() == args.size() && !(code.flags & CodeBlob::_ComputeAsmLtr)) { + SymDef* applied_sym = sym; + auto func = dynamic_cast(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(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(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 res_inner = pre_compile_tensor(args, code, lval_globs, compute_asm_ltr ? std::vector{} : 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 Expr::pre_compile(CodeBlob& code, std::vectorcls == _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 Expr::pre_compile(CodeBlob& code, std::vector(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] }); diff --git a/crypto/func/parse-func.cpp b/crypto/func/parse-func.cpp index 0e037821..80cd3568 100644 --- a/crypto/func/parse-func.cpp +++ b/crypto/func/parse-func.cpp @@ -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& 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(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(func_sym->value); + auto val = dynamic_cast(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(func_sym->value); + if (flags_inline) { + auto val = dynamic_cast(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"); } } diff --git a/crypto/func/unify-types.cpp b/crypto/func/unify-types.cpp index 22b33281..e8a9d078 100644 --- a/crypto/func/unify-types.cpp +++ b/crypto/func/unify-types.cpp @@ -115,6 +115,39 @@ int TypeExpr::extract_components(std::vector& 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{from, to}}; }