diff --git a/CHANGELOG.md b/CHANGELOG.md index f27a4a7..7b4b022 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,51 +9,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed ### Removed +## [4.1.0-rc1] - 2019-11-25 +### Added +- Support encoding and decoding bit fields in call arguments and results. +### Changed +- Various improvements to FATE code generator. +### Removed + ## [4.0.0] - 2019-10-11 ### Added - `Address.to_contract` - casts an address to a (any) contract type. - Pragma to check compiler version, e.g. `@compiler >= 4.0`. -### Changed -- Nice type error if contract function is called as from a namespace. -- Fail on function definitions in contracts other than the main contract. -- Bug fix in variable optimization - don't discard writes to the store/state. -### Removed - -## [4.0.0-rc5] - 2019-09-27 -### Added -### Changed -- Bug fixes in error reporting. -- Bug fix in variable liveness analysis for FATE. -### Removed - -## [4.0.0-rc4] - 2019-09-13 -### Added - Handle numeric escapes, i.e. `"\x19Ethereum Signed Message:\n"`, and similar strings. -### Changed -### Removed - -## [4.0.0-rc3] - 2019-09-12 -### Added - `Bytes.concat` and `Bytes.split` are added to be able to (de-)construct byte arrays. - `[a..b]` language construct, returning the list of numbers between `a` and `b` (inclusive). Returns the empty list if `a` > `b`. - [Standard libraries] (https://github.com/aeternity/protocol/blob/master/contracts/sophia_stdlib.md) - Checks that `init` is not called from other functions. -### Changed -- Error messages are changed into a uniform format, and more helpful - messages have been added. -- `Crypto.` and `String.` for byte arrays now only - hash the actual byte array - not the internal ABI format. -- More strict checks for polymorphic oracles and higher order oracles - and entrypoints. -- `AENS.claim` is updated with a `NameFee` field - to be able to do - name auctions within contracts. -- Fixed a bug in `Bytes.to_str` for AEVM. -### Removed - -## [4.0.0-rc1] - 2019-08-22 -### Added - FATE backend - the compiler is able to produce VM code for both `AEVM` and `FATE`. Many of the APIs now take `{backend, aevm | fate}` to decide wich backend to produce artifacts for. @@ -70,6 +43,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 that shall be able to receive funds should be marked as payable. `Address.is_payable(a)` can be used to check if an (contract) address is payable or not. ### Changed +- Nice type error if contract function is called as from a namespace. +- Fail on function definitions in contracts other than the main contract. +- Bug fix in variable optimization - don't discard writes to the store/state. +- Bug fixes in error reporting. +- Bug fix in variable liveness analysis for FATE. +- Error messages are changed into a uniform format, and more helpful + messages have been added. +- `Crypto.` and `String.` for byte arrays now only + hash the actual byte array - not the internal ABI format. +- More strict checks for polymorphic oracles and higher order oracles + and entrypoints. +- `AENS.claim` is updated with a `NameFee` field - to be able to do + name auctions within contracts. +- Fixed a bug in `Bytes.to_str` for AEVM. - New syntax for tuple types. Now 0-tuple type is encoded as `unit` instead of `()` and regular tuples are encoded by interspersing inner types with `*`, for instance `int * string`. Parens are not necessary. Note it only affects the types, values remain as their were before, diff --git a/rebar.config b/rebar.config index 9342440..6bc50cc 100644 --- a/rebar.config +++ b/rebar.config @@ -15,7 +15,7 @@ {base_plt_apps, [erts, kernel, stdlib, crypto, mnesia]} ]}. -{relx, [{release, {aesophia, "4.0.0"}, +{relx, [{release, {aesophia, "4.1.0-rc1"}, [aesophia, aebytecode, getopt]}, {dev_mode, true}, diff --git a/src/aeso_compiler.erl b/src/aeso_compiler.erl index f6ecc13..6327f00 100644 --- a/src/aeso_compiler.erl +++ b/src/aeso_compiler.erl @@ -23,6 +23,7 @@ , decode_calldata/4 , parse/2 , add_include_path/2 + , validate_byte_code/3 ]). -include_lib("aebytecode/include/aeb_opcodes.hrl"). @@ -566,6 +567,87 @@ pp(Code, Options, Option, PPFun) -> ok end. +%% -- Byte code validation --------------------------------------------------- + +-define(protect(Tag, Code), fun() -> try Code catch _:Err1 -> throw({Tag, Err1}) end end()). + +-spec validate_byte_code(map(), string(), options()) -> ok | {error, [aeso_errors:error()]}. +validate_byte_code(#{ byte_code := ByteCode, payable := Payable }, Source, Options) -> + Fail = fun(Err) -> {error, [aeso_errors:new(data_error, Err)]} end, + case proplists:get_value(backend, Options, aevm) of + B when B /= fate -> Fail(io_lib:format("Unsupported backend: ~s\n", [B])); + fate -> + try + FCode1 = ?protect(deserialize, aeb_fate_code:strip_init_function(aeb_fate_code:deserialize(ByteCode))), + {FCode2, SrcPayable} = + ?protect(compile, + begin + {ok, #{ byte_code := SrcByteCode, payable := SrcPayable }} = + from_string1(fate, Source, Options), + FCode = aeb_fate_code:deserialize(SrcByteCode), + {aeb_fate_code:strip_init_function(FCode), SrcPayable} + end), + case compare_fate_code(FCode1, FCode2) of + ok when SrcPayable /= Payable -> + Not = fun(true) -> ""; (false) -> " not" end, + Fail(io_lib:format("Byte code contract is~s payable, but source code contract is~s.\n", + [Not(Payable), Not(SrcPayable)])); + ok -> ok; + {error, Why} -> Fail(io_lib:format("Byte code does not match source code.\n~s", [Why])) + end + catch + throw:{deserialize, _} -> Fail("Invalid byte code"); + throw:{compile, {error, Errs}} -> {error, Errs} + end + end. + +compare_fate_code(FCode1, FCode2) -> + Funs1 = aeb_fate_code:functions(FCode1), + Funs2 = aeb_fate_code:functions(FCode2), + Syms1 = aeb_fate_code:symbols(FCode1), + Syms2 = aeb_fate_code:symbols(FCode2), + FunHashes1 = maps:keys(Funs1), + FunHashes2 = maps:keys(Funs2), + case FunHashes1 == FunHashes2 of + false -> + InByteCode = [ binary_to_list(maps:get(H, Syms1)) || H <- FunHashes1 -- FunHashes2 ], + InSourceCode = [ binary_to_list(maps:get(H, Syms2)) || H <- FunHashes2 -- FunHashes1 ], + Msg = [ io_lib:format("- Functions in the byte code but not in the source code:\n" + " ~s\n", [string:join(InByteCode, ", ")]) || InByteCode /= [] ] ++ + [ io_lib:format("- Functions in the source code but not in the byte code:\n" + " ~s\n", [string:join(InSourceCode, ", ")]) || InSourceCode /= [] ], + {error, Msg}; + true -> + case lists:append([ compare_fate_fun(maps:get(H, Syms1), Fun1, Fun2) + || {{H, Fun1}, {_, Fun2}} <- lists:zip(maps:to_list(Funs1), + maps:to_list(Funs2)) ]) of + [] -> ok; + Errs -> {error, Errs} + end + end. + +compare_fate_fun(_Name, Fun, Fun) -> []; +compare_fate_fun(Name, {Attr, Type, _}, {Attr, Type, _}) -> + [io_lib:format("- The implementation of the function ~s is different.\n", [Name])]; +compare_fate_fun(Name, {Attr1, Type, _}, {Attr2, Type, _}) -> + [io_lib:format("- The attributes of the function ~s differ:\n" + " Byte code: ~s\n" + " Source code: ~s\n", + [Name, string:join([ atom_to_list(A) || A <- Attr1 ], ", "), + string:join([ atom_to_list(A) || A <- Attr2 ], ", ")])]; +compare_fate_fun(Name, {_, Type1, _}, {_, Type2, _}) -> + [io_lib:format("- The type of the function ~s differs:\n" + " Byte code: ~s\n" + " Source code: ~s\n", + [Name, pp_fate_sig(Type1), pp_fate_sig(Type2)])]. + +pp_fate_sig({[Arg], Res}) -> + io_lib:format("~s => ~s", [pp_fate_type(Arg), pp_fate_type(Res)]); +pp_fate_sig({Args, Res}) -> + io_lib:format("(~s) => ~s", [string:join([pp_fate_type(Arg) || Arg <- Args], ", "), pp_fate_type(Res)]). + +pp_fate_type(T) -> io_lib:format("~w", [T]). + %% ------------------------------------------------------------------- sophia_type_to_typerep(String) -> diff --git a/src/aeso_fcode_to_fate.erl b/src/aeso_fcode_to_fate.erl index 00aa43d..411f26b 100644 --- a/src/aeso_fcode_to_fate.erl +++ b/src/aeso_fcode_to_fate.erl @@ -11,19 +11,23 @@ -export([compile/2, term_to_fate/1]). +-ifdef(TEST). +-export([optimize_fun/4, to_basic_blocks/1]). +-endif. + %% -- Preamble --------------------------------------------------------------- -type scode() :: [sinstr()]. -type sinstr() :: {switch, arg(), stype(), [maybe_scode()], maybe_scode()} %% last arg is catch-all | switch_body - | tuple(). %% FATE instruction + | loop + | tuple() | atom(). %% FATE instruction -type arg() :: tuple(). %% Not exported: aeb_fate_ops:fate_arg(). %% Annotated scode -type scode_a() :: [sinstr_a()]. -type sinstr_a() :: {switch, arg(), stype(), [maybe_scode_a()], maybe_scode_a()} %% last arg is catch-all - | switch_body | {i, ann(), tuple()}. %% FATE instruction -type ann() :: #{ live_in := vars(), live_out := vars() }. @@ -41,82 +45,6 @@ -define(s, {store, 1}). -define(void, {var, 9999}). --define(IsState(X), (is_tuple(X) andalso tuple_size(X) =:= 2 andalso element(1, X) =:= var andalso element(2, X) < 0)). - --define(IsOp(Op), ( - Op =:= 'STORE' orelse - Op =:= 'ADD' orelse - Op =:= 'SUB' orelse - Op =:= 'MUL' orelse - Op =:= 'DIV' orelse - Op =:= 'MOD' orelse - Op =:= 'POW' orelse - Op =:= 'LT' orelse - Op =:= 'GT' orelse - Op =:= 'EQ' orelse - Op =:= 'ELT' orelse - Op =:= 'EGT' orelse - Op =:= 'NEQ' orelse - Op =:= 'AND' orelse - Op =:= 'OR' orelse - Op =:= 'NOT' orelse - Op =:= 'ELEMENT' orelse - Op =:= 'MAP_EMPTY' orelse - Op =:= 'MAP_LOOKUP' orelse - Op =:= 'MAP_LOOKUPD' orelse - Op =:= 'MAP_UPDATE' orelse - Op =:= 'MAP_DELETE' orelse - Op =:= 'MAP_MEMBER' orelse - Op =:= 'MAP_FROM_LIST' orelse - Op =:= 'MAP_TO_LIST' orelse - Op =:= 'MAP_SIZE' orelse - Op =:= 'NIL' orelse - Op =:= 'IS_NIL' orelse - Op =:= 'CONS' orelse - Op =:= 'HD' orelse - Op =:= 'TL' orelse - Op =:= 'LENGTH' orelse - Op =:= 'APPEND' orelse - Op =:= 'STR_JOIN' orelse - Op =:= 'INT_TO_STR' orelse - Op =:= 'ADDR_TO_STR' orelse - Op =:= 'STR_REVERSE' orelse - Op =:= 'STR_LENGTH' orelse - Op =:= 'INT_TO_ADDR' orelse - Op =:= 'VARIANT_TEST' orelse - Op =:= 'VARIANT_ELEMENT' orelse - Op =:= 'BITS_NONE' orelse - Op =:= 'BITS_ALL' orelse - Op =:= 'BITS_ALL_N' orelse - Op =:= 'BITS_SET' orelse - Op =:= 'BITS_CLEAR' orelse - Op =:= 'BITS_TEST' orelse - Op =:= 'BITS_SUM' orelse - Op =:= 'BITS_OR' orelse - Op =:= 'BITS_AND' orelse - Op =:= 'BITS_DIFF' orelse - Op =:= 'SHA3' orelse - Op =:= 'SHA256' orelse - Op =:= 'BLAKE2B' orelse - Op =:= 'VERIFY_SIG' orelse - Op =:= 'VERIFY_SIG_SECP256K1' orelse - Op =:= 'ECVERIFY_SECP256K1' orelse - Op =:= 'ECRECOVER_SECP256K1' orelse - Op =:= 'CONTRACT_TO_ADDRESS' orelse - Op =:= 'ADDRESS_TO_CONTRACT' orelse - Op =:= 'AUTH_TX_HASH' orelse - Op =:= 'BYTES_TO_INT' orelse - Op =:= 'BYTES_TO_STR' orelse - Op =:= 'BYTES_CONCAT' orelse - Op =:= 'BYTES_SPLIT' orelse - Op =:= 'ORACLE_CHECK' orelse - Op =:= 'ORACLE_CHECK_QUERY' orelse - Op =:= 'IS_ORACLE' orelse - Op =:= 'IS_CONTRACT' orelse - Op =:= 'IS_PAYABLE' orelse - Op =:= 'CREATOR' orelse - false)). - -record(env, { contract, vars = [], locals = [], current_function, tailpos = true }). %% -- Debugging -------------------------------------------------------------- @@ -125,12 +53,19 @@ is_debug(Tag, Options) -> Tags = proplists:get_value(debug, Options, []), Tags == all orelse lists:member(Tag, Tags). -debug(Tag, Options, Fmt, Args) -> +-define(debug(Tag, Options, Fmt, Args), + debug(Tag, Options, fun() -> io:format(Fmt, Args) end)). + +debug(Tag, Options, Fun) -> case is_debug(Tag, Options) of - true -> io:format(Fmt, Args); + true -> Fun(); false -> ok end. +-dialyzer({nowarn_function, [code_error/1]}). +code_error(Err) -> + aeso_errors:throw(aeso_code_errors:format(Err)). + %% -- Main ------------------------------------------------------------------- %% @doc Main entry point. @@ -140,7 +75,7 @@ compile(FCode, Options) -> SFuns = functions_to_scode(ContractName, Functions, Options), SFuns1 = optimize_scode(SFuns, Options), FateCode = to_basic_blocks(SFuns1), - debug(compile, Options, "~s\n", [aeb_fate_asm:pp(FateCode)]), + ?debug(compile, Options, "~s\n", [aeb_fate_asm:pp(FateCode)]), FateCode. make_function_id(X) -> @@ -223,8 +158,6 @@ bind_local(Name, Env) -> notail(Env) -> Env#env{ tailpos = false }. -code_error(Err) -> error(Err). - lookup_var(#env{vars = Vars}, X) -> case lists:keyfind(X, 1, Vars) of {_, Var} -> Var; @@ -664,23 +597,23 @@ flatten_s(I) -> I. optimize_fun(_Funs, Name, {Attrs, Sig, Code}, Options) -> Code0 = flatten(Code), - debug(opt, Options, "Optimizing ~s\n", [Name]), + ?debug(opt, Options, "Optimizing ~s\n", [Name]), Code1 = simpl_loop(0, Code0, Options), Code2 = desugar(Code1), {Attrs, Sig, Code2}. simpl_loop(N, Code, Options) when N >= ?MAX_SIMPL_ITERATIONS -> - debug(opt, Options, " No simpl_loop fixed_point after ~p iterations.\n\n", [N]), + ?debug(opt, Options, " No simpl_loop fixed_point after ~p iterations.\n\n", [N]), Code; simpl_loop(N, Code, Options) -> ACode = annotate_code(Code), - [ debug(opt, Options, " annotated:\n~s\n", [pp_ann(" ", ACode)]) || N == 0 ], + [ ?debug(opt, Options, " annotated:\n~s\n", [pp_ann(" ", ACode)]) || N == 0 ], Code1 = simplify(ACode, Options), - [ debug(opt, Options, " optimized:\n~s\n", [pp_ann(" ", Code1)]) || Code1 /= ACode ], + [ ?debug(opt, Options, " optimized:\n~s\n", [pp_ann(" ", Code1)]) || Code1 /= ACode ], Code2 = unannotate(Code1), case Code == Code2 of true -> - debug(opt, Options, " Reached simpl_loop fixed point after ~p iteration~s.\n\n", + ?debug(opt, Options, " Reached simpl_loop fixed point after ~p iteration~s.\n\n", [N, if N /= 1 -> "s"; true -> "" end]), Code2; false -> simpl_loop(N + 1, Code2, Options) @@ -700,11 +633,9 @@ pp_ann(Ind, [{switch, Arg, Type, Alts, Def} | Code]) -> || {Tag, Alt} <- lists:zip(Tags, Alts), Alt /= missing], [[Ind1, "_ =>\n", pp_ann(Ind2, Def)] || Def /= missing], pp_ann(Ind, Code)]; -pp_ann(Ind, [switch_body | Code]) -> - [Ind, "SWITCH-BODY\n", pp_ann(Ind, Code)]; pp_ann(Ind, [{i, #{ live_in := In, live_out := Out }, I} | Code]) -> Fmt = fun([]) -> "()"; - (Xs) -> string:join([lists:concat(["var", N]) || {var, N} <- Xs], " ") + (Xs) -> string:join([lists:flatten(pp_arg(X)) || X <- Xs], " ") end, Op = [Ind, pp_op(desugar_args(I))], Ann = [[" % ", Fmt(In), " -> ", Fmt(Out)] || In ++ Out /= []], @@ -712,70 +643,67 @@ pp_ann(Ind, [{i, #{ live_in := In, live_out := Out }, I} | Code]) -> pp_ann(Ind, Code)]; pp_ann(_, []) -> []. +pp_op(switch_body) -> "SWITCH-BODY"; pp_op(loop) -> "LOOP"; pp_op(I) -> aeb_fate_pp:format_op(I, #{}). -pp_arg(?i(I)) -> io_lib:format("~w", [I]); -pp_arg({arg, N}) -> io_lib:format("arg~p", [N]); -pp_arg(?s) -> "store1"; -pp_arg({var, N}) -> io_lib:format("var~p", [N]); -pp_arg(?a) -> "a". +pp_arg(?i(I)) -> io_lib:format("~w", [I]); +pp_arg({arg, N}) -> io_lib:format("arg~p", [N]); +pp_arg({store, N}) -> io_lib:format("store~p", [N]); +pp_arg({var, N}) -> io_lib:format("var~p", [N]); +pp_arg(?a) -> "a". %% -- Analysis -- annotate_code(Code) -> - {WCode, _} = ann_writes(Code, ordsets:new(), []), - {RCode, _} = ann_reads(WCode, ordsets:new(), []), - RCode. + annotate_code(5, [], Code). -%% Reverses the code -ann_writes(missing, Writes, []) -> {missing, Writes}; -ann_writes([switch_body | Code], Writes, Acc) -> - ann_writes(Code, Writes, [switch_body | Acc]); -ann_writes([{switch, Arg, Type, Alts, Def} | Code], Writes, Acc) -> - {Alts1, WritesAlts} = lists:unzip([ ann_writes(Alt, Writes, []) || Alt <- Alts ]), - {Def1, WritesDef} = ann_writes(Def, Writes, []), - Writes1 = ordsets:union(Writes, ordsets:intersection([WritesDef | WritesAlts])), - ann_writes(Code, Writes1, [{switch, Arg, Type, Alts1, Def1} | Acc]); -ann_writes([I | Code], Writes, Acc) -> - Ws = [ W || W <- var_writes(I), not ?IsState(W) ], - Writes1 = ordsets:union(Writes, Ws), - Ann = #{ writes_in => Writes, writes_out => Writes1 }, - ann_writes(Code, Writes1, [{i, Ann, I} | Acc]); -ann_writes([], Writes, Acc) -> - {Acc, Writes}. +annotate_code(Fuel, LiveTop, Code) -> + {Code1, LiveIn} = ann_live(LiveTop, Code, []), + case LiveIn == LiveTop of + true -> Code1; + false when Fuel =< 0 -> + code_error(liveness_analysis_out_of_fuel); + false -> annotate_code(Fuel - 1, LiveIn, Code) + end. -%% Takes reversed code and unreverses it. -ann_reads(missing, Reads, []) -> {missing, Reads}; -ann_reads([switch_body | Code], Reads, Acc) -> - ann_reads(Code, Reads, [switch_body | Acc]); -ann_reads([{switch, Arg, Type, Alts, Def} | Code], Reads, Acc) -> - {Alts1, ReadsAlts} = lists:unzip([ ann_reads(Alt, Reads, []) || Alt <- Alts ]), - {Def1, ReadsDef} = ann_reads(Def, Reads, []), - Reads1 = ordsets:union([[Arg], Reads, ReadsDef | ReadsAlts]), - ann_reads(Code, Reads1, [{switch, Arg, Type, Alts1, Def1} | Acc]); -ann_reads([{i, Ann, I} | Code], Reads, Acc) -> - #{ writes_in := WritesIn, writes_out := WritesOut } = Ann, - #{ read := Rs, write := W, pure := Pure } = attributes(I), +ann_live(_LiveTop, missing, _LiveOut) -> {missing, []}; +ann_live(_LiveTop, [], LiveOut) -> {[], LiveOut}; +ann_live(LiveTop, [I | Is], LiveOut) -> + {Is1, LiveMid} = ann_live(LiveTop, Is, LiveOut), + {I1, LiveIn} = ann_live1(LiveTop, I, LiveMid), + {[I1 | Is1], LiveIn}. + +ann_live1(_LiveTop, switch_body, LiveOut) -> + Ann = #{ live_in => LiveOut, live_out => LiveOut }, + {{i, Ann, switch_body}, LiveOut}; +ann_live1(LiveTop, loop, _LiveOut) -> + Ann = #{ live_in => LiveTop, live_out => [] }, + {{i, Ann, loop}, LiveTop}; +ann_live1(LiveTop, {switch, Arg, Type, Alts, Def}, LiveOut) -> + Read = [Arg || is_reg(Arg)], + {Alts1, LiveAlts} = lists:unzip([ ann_live(LiveTop, Alt, LiveOut) || Alt <- Alts ]), + {Def1, LiveDef} = ann_live(LiveTop, Def, LiveOut), + LiveIn = ordsets:union([Read, LiveDef | LiveAlts]), + {{switch, Arg, Type, Alts1, Def1}, LiveIn}; +ann_live1(_LiveTop, I, LiveOut) -> + #{ read := Reads0, write := W } = attributes(I), + Reads = lists:filter(fun is_reg/1, Reads0), %% If we write it here it's not live in (unless we also read it) - Reads1 = Reads -- [W], - Reads2 = - case {W, Pure andalso not ordsets:is_element(W, Reads)} of - %% This is a little bit dangerous: if writing to a dead variable, we ignore - %% the reads. Relies on dead writes to be removed by the - %% optimisations below (r_write_to_dead_var). - {{var, _}, true} -> Reads1; - _ -> ordsets:union(Reads1, Rs) - end, - LiveIn = ordsets:intersection(Reads2, WritesIn), - LiveOut = ordsets:intersection(Reads, WritesOut), - Ann1 = #{ live_in => LiveIn, live_out => LiveOut }, - ann_reads(Code, Reads2, [{i, Ann1, I} | Acc]); -ann_reads([], Reads, Acc) -> {Acc, Reads}. + LiveIn = ordsets:union(LiveOut -- [W], Reads), + Ann = #{ live_in => LiveIn, live_out => LiveOut }, + {{i, Ann, I}, LiveIn}. -%% Instruction attributes: reads, writes and purity (pure means no side-effects -%% aside from the reads and writes). +is_reg(?a) -> false; +is_reg(none) -> false; +is_reg(pc) -> false; +is_reg({immediate, _}) -> false; +is_reg({arg, _}) -> true; +is_reg({store, _}) -> true; +is_reg({var, _}) -> true. + +%% Instruction attributes: reads, writes and purity (pure means no writing to the chain). attributes(I) -> Set = fun(L) when is_list(L) -> ordsets:from_list(L); (X) -> ordsets:from_list([X]) end, @@ -784,12 +712,13 @@ attributes(I) -> Impure = fun(W, R) -> Attr(W, R, false) end, case I of loop -> Impure(pc, []); + switch_body -> Pure(none, []); 'RETURN' -> Impure(pc, []); {'RETURNR', A} -> Impure(pc, A); - {'CALL', _} -> Impure(?a, []); + {'CALL', A} -> Impure(?a, [A]); {'CALL_R', A, _, B, C, D} -> Impure(?a, [A, B, C, D]); {'CALL_GR', A, _, B, C, D, E} -> Impure(?a, [A, B, C, D, E]); - {'CALL_T', _} -> Impure(pc, []); + {'CALL_T', A} -> Impure(pc, [A]); {'CALL_VALUE', A} -> Pure(A, []); {'JUMP', _} -> Impure(pc, []); {'JUMPIF', A, _} -> Impure(pc, A); @@ -874,26 +803,26 @@ attributes(I) -> {'BYTES_TO_STR', A, B} -> Pure(A, [B]); {'BYTES_CONCAT', A, B, C} -> Pure(A, [B, C]); {'BYTES_SPLIT', A, B, C} -> Pure(A, [B, C]); - {'ORACLE_CHECK', A, B, C, D} -> Impure(A, [B, C, D]); - {'ORACLE_CHECK_QUERY', A, B, C, D, E} -> Impure(A, [B, C, D, E]); - {'IS_ORACLE', A, B} -> Impure(A, [B]); - {'IS_CONTRACT', A, B} -> Impure(A, [B]); - {'IS_PAYABLE', A, B} -> Impure(A, [B]); + {'ORACLE_CHECK', A, B, C, D} -> Pure(A, [B, C, D]); + {'ORACLE_CHECK_QUERY', A, B, C, D, E} -> Pure(A, [B, C, D, E]); + {'IS_ORACLE', A, B} -> Pure(A, [B]); + {'IS_CONTRACT', A, B} -> Pure(A, [B]); + {'IS_PAYABLE', A, B} -> Pure(A, [B]); {'CREATOR', A} -> Pure(A, []); {'ADDRESS', A} -> Pure(A, []); - {'BALANCE', A} -> Impure(A, []); - {'BALANCE_OTHER', A, B} -> Impure(A, [B]); + {'BALANCE', A} -> Pure(A, []); + {'BALANCE_OTHER', A, B} -> Pure(A, [B]); {'ORIGIN', A} -> Pure(A, []); {'CALLER', A} -> Pure(A, []); {'GASPRICE', A} -> Pure(A, []); - {'BLOCKHASH', A, B} -> Impure(A, [B]); + {'BLOCKHASH', A, B} -> Pure(A, [B]); {'BENEFICIARY', A} -> Pure(A, []); {'TIMESTAMP', A} -> Pure(A, []); {'GENERATION', A} -> Pure(A, []); {'MICROBLOCK', A} -> Pure(A, []); {'DIFFICULTY', A} -> Pure(A, []); {'GASLIMIT', A} -> Pure(A, []); - {'GAS', A} -> Impure(?a, A); + {'GAS', A} -> Pure(A, []); {'LOG0', A} -> Impure(none, [A]); {'LOG1', A, B} -> Impure(none, [A, B]); {'LOG2', A, B, C} -> Impure(none, [A, B, C]); @@ -905,10 +834,10 @@ attributes(I) -> {'ORACLE_QUERY', A, B, C, D, E, F, G, H} -> Impure(A, [B, C, D, E, F, G, H]); {'ORACLE_RESPOND', A, B, C, D, E, F} -> Impure(none, [A, B, C, D, E, F]); {'ORACLE_EXTEND', A, B, C} -> Impure(none, [A, B, C]); - {'ORACLE_GET_ANSWER', A, B, C, D, E} -> Impure(A, [B, C, D, E]); - {'ORACLE_GET_QUESTION', A, B, C, D, E}-> Impure(A, [B, C, D, E]); - {'ORACLE_QUERY_FEE', A, B} -> Impure(A, [B]); - {'AENS_RESOLVE', A, B, C, D} -> Impure(A, [B, C, D]); + {'ORACLE_GET_ANSWER', A, B, C, D, E} -> Pure(A, [B, C, D, E]); + {'ORACLE_GET_QUESTION', A, B, C, D, E}-> Pure(A, [B, C, D, E]); + {'ORACLE_QUERY_FEE', A, B} -> Pure(A, [B]); + {'AENS_RESOLVE', A, B, C, D} -> Pure(A, [B, C, D]); {'AENS_PRECLAIM', A, B, C} -> Impure(none, [A, B, C]); {'AENS_CLAIM', A, B, C, D, E} -> Impure(none, [A, B, C, D, E]); {'AENS_UPDATE', A, B, C, D, E, F} -> Impure(none, [A, B, C, D, E, F]); @@ -923,15 +852,17 @@ var_writes({i, _, I}) -> var_writes(I); var_writes(I) -> #{ write := W } = attributes(I), case W of - {var, _} -> [W]; - _ -> [] + {var, _} -> [W]; + {arg, _} -> [W]; + {store, _} -> [W]; + {stack, _} -> []; + none -> []; + pc -> [] end. -spec independent(sinstr_a(), sinstr_a()) -> boolean(). %% independent({switch, _, _, _, _}, _) -> false; %% Commented due to Dialyzer whinging independent(_, {switch, _, _, _, _}) -> false; -%% independent(switch_body, _) -> true; -independent(_, switch_body) -> true; independent({i, _, I}, {i, _, J}) -> #{ write := WI, read := RI, pure := PureI } = attributes(I), #{ write := WJ, read := RJ, pure := PureJ } = attributes(J), @@ -953,8 +884,6 @@ merge_ann(#{ live_in := LiveIn }, #{ live_out := LiveOut }) -> #{ live_in => LiveIn, live_out => LiveOut }. %% Swap two instructions. Precondition: the instructions are independent/2. -swap_instrs(I, switch_body) -> {switch_body, I}; -%% swap_instrs(switch_body, I) -> {I, switch_body}; %% Commented due to Dialyzer whinging swap_instrs({i, #{ live_in := Live1 }, I}, {i, #{ live_in := Live2, live_out := Live3 }, J}) -> %% Since I and J are independent the J can't read or write anything in %% that I writes. @@ -966,17 +895,16 @@ swap_instrs({i, #{ live_in := Live1 }, I}, {i, #{ live_in := Live2, live_out := {{i, #{ live_in => Live1, live_out => Live2_ }, J}, {i, #{ live_in => Live2_, live_out => Live3 }, I}}. -live_in(R, _) when ?IsState(R) -> true; +live_in({store, _}, _) -> true; live_in(R, #{ live_in := LiveIn }) -> ordsets:is_element(R, LiveIn); live_in(R, {i, Ann, _}) -> live_in(R, Ann); live_in(R, [I = {i, _, _} | _]) -> live_in(R, I); -live_in(R, [switch_body | Code]) -> live_in(R, Code); live_in(R, [{switch, A, _, Alts, Def} | _]) -> R == A orelse lists:any(fun(Code) -> live_in(R, Code) end, [Def | Alts]); live_in(_, missing) -> false; live_in(_, []) -> false. -live_out(R, _) when ?IsState(R) -> true; +live_out({store, _}, _) -> true; live_out(R, #{ live_out := LiveOut }) -> ordsets:is_element(R, LiveOut). %% -- Optimizations -- @@ -998,7 +926,7 @@ simpl_top(I, Code, Options) -> simpl_top(?SIMPL_FUEL, I, Code, Options). simpl_top(0, I, Code, _Options) -> - error({out_of_fuel, I, Code}); + code_error({optimizer_out_of_fuel, I, Code}); simpl_top(Fuel, I, Code, Options) -> apply_rules(Fuel, rules(), I, Code, Options). @@ -1010,7 +938,7 @@ apply_rules(Fuel, Rules, I, Code, Options) -> case is_debug(opt_rules, Options) of true -> {OldCode, NewCode} = drop_common_suffix([I | Code], New ++ Rest), - debug(opt_rules, Options, " Applied ~p:\n~s ==>\n~s\n", [RName, pp_ann(" ", OldCode), pp_ann(" ", NewCode)]); + ?debug(opt_rules, Options, " Applied ~p:\n~s ==>\n~s\n", [RName, pp_ann(" ", OldCode), pp_ann(" ", NewCode)]); false -> ok end, lists:foldr(Cons, Rest, New) @@ -1040,6 +968,7 @@ rules() -> ?RULE(r_swap_write), ?RULE(r_constant_propagation), ?RULE(r_prune_impossible_branches), + ?RULE(r_single_successful_branch), ?RULE(r_inline_store), ?RULE(r_float_switch_body) ]. @@ -1062,8 +991,9 @@ r_push_consume({i, Ann1, I}, [{i, Ann2, {'STORE', R, ?a}} | Code]) -> true -> false end; r_push_consume(_, _) -> false. -inline_push(Ann, Arg, Stack, [switch_body | Code], Acc) -> - inline_push(Ann, Arg, Stack, Code, [switch_body | Acc]); +inline_push(Ann, Arg, Stack, [{i, _, switch_body} = AI | Code], Acc) -> + {AI1, {i, Ann1, _}} = swap_instrs({i, Ann, {'STORE', ?a, Arg}}, AI), + inline_push(Ann1, Arg, Stack, Code, [AI1 | Acc]); inline_push(Ann1, Arg, Stack, [{i, Ann2, I} = AI | Code], Acc) -> case op_view(I) of {Op, R, As} -> @@ -1137,8 +1067,9 @@ r_swap_write(I = {i, _, _}, [J | Code]) -> end; r_swap_write(_, _) -> false. -r_swap_write(Pre, I, [switch_body | Code]) -> - r_swap_write([switch_body | Pre], I, Code); +r_swap_write(Pre, I, [{i, _, switch_body} = J | Code]) -> + {J1, I1} = swap_instrs(I, J), + r_swap_write([J1 | Pre], I1, Code); r_swap_write(Pre, I, Code0 = [J | Code]) -> case apply_rules_once(merge_rules(), I, Code0) of {_Rule, New, Rest} -> @@ -1154,18 +1085,20 @@ r_swap_write(Pre, I, Code0 = [J | Code]) -> r_swap_write(_, _, _) -> false. %% Precompute instructions with known values -r_constant_propagation(Cons = {i, _, {'CONS', R, _, _}}, [{i, Ann, {'IS_NIL', S, R}} | Code]) -> +r_constant_propagation(Cons = {i, Ann1, {'CONS', R, X, Xs}}, [{i, Ann, {'IS_NIL', S, R}} | Code]) -> Store = {i, Ann, {'STORE', S, ?i(false)}}, - case R of - ?a -> {[Store], Code}; - _ -> {[Cons, Store], Code} - end; -r_constant_propagation(Cons = {i, _, {'NIL', R}}, [{i, Ann, {'IS_NIL', S, R}} | Code]) -> + Cons1 = case R of + ?a -> {i, Ann1, {'CONS', ?void, X, Xs}}; + _ -> Cons + end, + {[Cons1, Store], Code}; +r_constant_propagation(Nil = {i, Ann1, {'NIL', R}}, [{i, Ann, {'IS_NIL', S, R}} | Code]) -> Store = {i, Ann, {'STORE', S, ?i(true)}}, - case R of - ?a -> {[Store], Code}; - _ -> {[Cons, Store], Code} - end; + Nil1 = case R of + ?a -> {i, Ann1, {'NIL', ?void}}; + _ -> Nil + end, + {[Nil1, Store], Code}; r_constant_propagation({i, Ann, I}, Code) -> case op_view(I) of false -> false; @@ -1182,20 +1115,21 @@ r_constant_propagation({i, Ann, I}, Code) -> end; r_constant_propagation(_, _) -> false. -eval_op('ADD', [X, Y]) -> X + Y; -eval_op('SUB', [X, Y]) -> X - Y; -eval_op('MUL', [X, Y]) -> X * Y; -eval_op('DIV', [X, Y]) when Y /= 0 -> X div Y; -eval_op('MOD', [X, Y]) when Y /= 0 -> X rem Y; -eval_op('POW', [_, _]) -> no_eval; -eval_op('LT', [X, Y]) -> X < Y; -eval_op('GT', [X, Y]) -> X > Y; -eval_op('EQ', [X, Y]) -> X =:= Y; -eval_op('ELT', [X, Y]) -> X =< Y; -eval_op('EGT', [X, Y]) -> X >= Y; -eval_op('NEQ', [X, Y]) -> X =/= Y; -eval_op('NOT', [X]) -> not X; -eval_op(_, _) -> no_eval. %% TODO: bits? +eval_op('ADD', [X, Y]) when is_integer(X), is_integer(Y) -> X + Y; +eval_op('SUB', [X, Y]) when is_integer(X), is_integer(Y) -> X - Y; +eval_op('MUL', [X, Y]) when is_integer(X), is_integer(Y) -> X * Y; +eval_op('DIV', [X, Y]) when is_integer(X), is_integer(Y), Y /= 0 -> X div Y; +eval_op('MOD', [X, Y]) when is_integer(X), is_integer(Y), Y /= 0 -> X rem Y; +eval_op('POW', [_, _]) -> no_eval; +eval_op('LT', [X, Y]) -> X < Y; +eval_op('GT', [X, Y]) -> X > Y; +eval_op('EQ', [X, Y]) -> X =:= Y; +eval_op('ELT', [X, Y]) -> X =< Y; +eval_op('EGT', [X, Y]) -> X >= Y; +eval_op('NEQ', [X, Y]) -> X =/= Y; +eval_op('NOT', [true]) -> false; +eval_op('NOT', [false]) -> true; +eval_op(_, _) -> no_eval. %% TODO: bits? %% Prune impossible branches from switches r_prune_impossible_branches({switch, ?i(V), Type, Alts, missing}, Code) -> @@ -1203,7 +1137,7 @@ r_prune_impossible_branches({switch, ?i(V), Type, Alts, missing}, Code) -> false -> false; Alt -> {Alt, Code} end; -r_prune_impossible_branches({switch, ?i(V), boolean, [False, True] = Alts, Def}, Code) -> +r_prune_impossible_branches({switch, ?i(V), boolean, [False, True] = Alts, Def}, Code) when V == true; V == false -> Alts1 = [if V -> missing; true -> False end, if V -> True; true -> missing end], case Alts == Alts1 of @@ -1215,7 +1149,7 @@ r_prune_impossible_branches({switch, ?i(V), boolean, [False, True] = Alts, Def}, end end; r_prune_impossible_branches(Variant = {i, _, {'VARIANT', R, ?i(_), ?i(Tag), ?i(_)}}, - [{switch, R, Type, Alts, missing} | Code]) -> + [{switch, R, Type = {variant, _}, Alts, missing} | Code]) when is_integer(Tag) -> case {R, lists:nth(Tag + 1, Alts)} of {_, missing} -> Alts1 = [missing || _ <- Alts], @@ -1232,7 +1166,7 @@ r_prune_impossible_branches(Variant = {i, _, {'VARIANT', R, ?i(_), ?i(Tag), ?i(_ end; r_prune_impossible_branches(_, _) -> false. -pick_branch(boolean, V, [False, True]) -> +pick_branch(boolean, V, [False, True]) when V == true; V == false -> Alt = if V -> True; true -> False end, case Alt of missing -> false; @@ -1241,10 +1175,62 @@ pick_branch(boolean, V, [False, True]) -> pick_branch(_Type, _V, _Alts) -> false. +%% If there's a single branch that doesn't abort we can push the code for that +%% out of the switch. +r_single_successful_branch({switch, R, Type, Alts, Def}, Code) -> + case push_code_out_of_switch([Def | Alts]) of + {_, none} -> false; + {_, many} -> false; + {_, [{i, _, switch_body}]} -> false; + {[Def1 | Alts1], PushedOut} -> + {[{switch, R, Type, Alts1, Def1} | PushedOut], Code} + end; +r_single_successful_branch(_, _) -> false. + +push_code_out_of_switch([]) -> {[], none}; +push_code_out_of_switch([Alt | Alts]) -> + {Alt1, PushedAlt} = push_code_out_of_alt(Alt), + {Alts1, PushedAlts} = push_code_out_of_switch(Alts), + Pushed = + case {PushedAlt, PushedAlts} of + {none, _} -> PushedAlts; + {_, none} -> PushedAlt; + _ -> many + end, + {[Alt1 | Alts1], Pushed}. + +push_code_out_of_alt(missing) -> {missing, none}; +push_code_out_of_alt([Body = {i, _, switch_body} | Code]) -> + case does_abort(Code) of + true -> {[Body | Code], none}; + false -> {[Body], [Body | Code]} %% Duplicate the switch_body, in case we apply this in the middle of a switch + end; +push_code_out_of_alt([{switch, R, Type, Alts, Def}]) -> + {[Def1 | Alts1], Pushed} = push_code_out_of_switch([Def | Alts]), + {[{switch, R, Type, Alts1, Def1}], Pushed}; +push_code_out_of_alt(Code) -> + {Code, many}. %% Conservative + +does_abort([I | Code]) -> + does_abort(I) orelse does_abort(Code); +does_abort({i, _, {'ABORT', _}}) -> true; +does_abort({i, _, {'EXIT', _}}) -> true; +does_abort(missing) -> true; +does_abort({switch, _, _, Alts, Def}) -> + lists:all(fun does_abort/1, [Def | Alts]); +does_abort(_) -> false. + %% STORE R A, SWITCH R --> SWITCH A -r_inline_switch_target(Store = {i, _, {'STORE', R, A}}, [{switch, R, Type, Alts, Def} | Code]) -> +r_inline_switch_target({i, Ann, {'STORE', R, A}}, [{switch, R, Type, Alts, Def} | Code]) -> + Ann1 = + case is_reg(A) of + true -> Ann#{ live_out := ordsets:add_element(A, maps:get(live_out, Ann)) }; + false -> Ann + end, + Store = {i, Ann1, {'STORE', R, A}}, Switch = {switch, A, Type, Alts, Def}, case R of + A -> false; ?a -> {[Switch], Code}; {var, _} -> case lists:any(fun(Alt) -> live_in(R, Alt) end, [Def | Alts]) of @@ -1257,8 +1243,9 @@ r_inline_switch_target(Store = {i, _, {'STORE', R, A}}, [{switch, R, Type, Alts, r_inline_switch_target(_, _) -> false. %% Float switch-body to closest switch -r_float_switch_body(I = {i, _, _}, [switch_body | Code]) -> - {[], [switch_body, I | Code]}; +r_float_switch_body(I = {i, _, _}, [J = {i, _, switch_body} | Code]) -> + {J1, I1} = swap_instrs(I, J), + {[], [J1, I1 | Code]}; r_float_switch_body(_, _) -> false. %% Inline stores @@ -1267,38 +1254,42 @@ r_inline_store({i, _, {'STORE', R, R}}, Code) -> r_inline_store(I = {i, _, {'STORE', R = {var, _}, A}}, Code) -> %% Not when A is var unless updating the annotations properly. Inline = case A of - {arg, _} -> true; - ?i(_) -> true; - _ -> false + {arg, _} -> true; + ?i(_) -> true; + {store, _} -> true; + _ -> false end, - if Inline -> r_inline_store([I], R, A, Code); + if Inline -> r_inline_store([I], false, R, A, Code); true -> false end; r_inline_store(_, _) -> false. -r_inline_store(Acc, R, A, [switch_body | Code]) -> - r_inline_store([switch_body | Acc], R, A, Code); -r_inline_store(Acc, R, A, [{i, Ann, I} | Code]) -> - #{ write := W, pure := Pure } = attributes(I), +r_inline_store(Acc, Progress, R, A, [I = {i, _, switch_body} | Code]) -> + r_inline_store([I | Acc], Progress, R, A, Code); +r_inline_store(Acc, Progress, R, A, [{i, Ann, I} | Code]) -> + #{ write := W } = attributes(I), Inl = fun(X) when X == R -> A; (X) -> X end, - case not live_in(R, Ann) orelse not Pure orelse lists:member(W, [R, A]) of - true -> false; - false -> - case op_view(I) of - {Op, S, As} -> - case lists:member(R, As) of - true -> - Acc1 = [{i, Ann, from_op_view(Op, S, lists:map(Inl, As))} | Acc], - case r_inline_store(Acc1, R, A, Code) of - false -> {lists:reverse(Acc1), Code}; - {_, _} = Res -> Res - end; - false -> - r_inline_store([{i, Ann, I} | Acc], R, A, Code) - end; - _ -> r_inline_store([{i, Ann, I} | Acc], R, A, Code) + case live_in(R, Ann) of + false -> false; %% No more reads of R + true -> + {I1, Progress1} = + case op_view(I) of + {Op, S, As} -> + case lists:member(R, As) of + true -> {from_op_view(Op, S, lists:map(Inl, As)), true}; + false -> {I, Progress} + end; + _ -> {I, Progress} + end, + Acc1 = [{i, Ann, I1} | Acc], + %% Stop if write to R or A + case lists:member(W, [R, A]) of + true when Progress1 -> {lists:reverse(Acc1), Code}; + true -> false; + false -> r_inline_store(Acc1, Progress1, R, A, Code) end end; -r_inline_store(_Acc, _, _, _) -> false. +r_inline_store(Acc, true, _, _, Code) -> {lists:reverse(Acc), Code}; +r_inline_store(_, false, _, _, _) -> false. %% Shortcut write followed by final read r_one_shot_var({i, Ann1, I}, [{i, Ann2, J} | Code]) -> @@ -1320,8 +1311,9 @@ r_one_shot_var(_, _) -> false. %% Remove writes to dead variables r_write_to_dead_var({i, _, {'STORE', ?void, ?a}}, _) -> false; %% Avoid looping r_write_to_dead_var({i, Ann, I}, Code) -> + #{ pure := Pure } = attributes(I), case op_view(I) of - {_Op, R = {var, _}, As} -> + {_Op, R, As} when R /= ?a, Pure -> case live_out(R, Ann) of false -> %% Subtle: we still have to pop the stack if any of the arguments @@ -1333,21 +1325,24 @@ r_write_to_dead_var({i, Ann, I}, Code) -> end; r_write_to_dead_var(_, _) -> false. +op_view({'ABORT', R}) -> {'ABORT', none, [R]}; op_view(T) when is_tuple(T) -> - case tuple_to_list(T) of - [Op, R | As] when ?IsOp(Op) -> - {Op, R, As}; - _ -> false + [Op, R | As] = tuple_to_list(T), + CheckReads = fun(Rs, X) -> case [] == Rs -- [dst, src] of true -> X; false -> false end end, + case attributes(list_to_tuple([Op, dst | [src || _ <- As]])) of + #{ write := dst, read := Rs } -> CheckReads(Rs, {Op, R, As}); + #{ write := none, read := Rs } -> CheckReads(Rs, {Op, none, [R | As]}); + _ -> false end; op_view(_) -> false. +from_op_view(Op, none, As) -> list_to_tuple([Op | As]); from_op_view(Op, R, As) -> list_to_tuple([Op, R | As]). %% Desugar and specialize and remove annotations -spec unannotate(scode_a()) -> scode(); (sinstr_a()) -> sinstr(); (missing) -> missing. -unannotate(switch_body) -> [switch_body]; unannotate({switch, Arg, Type, Alts, Def}) -> [{switch, Arg, Type, [unannotate(A) || A <- Alts], unannotate(Def)}]; unannotate(missing) -> missing; @@ -1363,6 +1358,7 @@ desugar({'ADD', A, A, ?i(1)}) -> [aeb_fate_ops:inc(desugar_arg(A))]; desugar({'SUB', ?a, ?a, ?i(1)}) -> [aeb_fate_ops:dec()]; desugar({'SUB', A, A, ?i(1)}) -> [aeb_fate_ops:dec(desugar_arg(A))]; desugar({'STORE', ?a, A}) -> [aeb_fate_ops:push(desugar_arg(A))]; +desugar({'STORE', R, ?a}) -> [aeb_fate_ops:pop(desugar_arg(R))]; desugar({switch, Arg, Type, Alts, Def}) -> [{switch, desugar_arg(Arg), Type, [desugar(A) || A <- Alts], desugar(Def)}]; desugar(missing) -> missing; @@ -1375,7 +1371,7 @@ desugar_args(I) when is_tuple(I) -> list_to_tuple([Op | lists:map(fun desugar_arg/1, Args)]); desugar_args(I) -> I. -desugar_arg(?s) -> {var, -1}; +desugar_arg({store, N}) -> {var, -N}; desugar_arg(A) -> A. %% -- Phase III -------------------------------------------------------------- @@ -1441,11 +1437,13 @@ block(Blk = #blk{code = [{switch, Arg, Type, Alts, Default} | Code], {DefRef, DefBlk} = case Default of missing when Catchall == none -> - FreshBlk([aeb_fate_ops:exit(?i(<<"Incomplete patterns">>))], none); + FreshBlk([aeb_fate_ops:abort(?i(<<"Incomplete patterns">>))], none); missing -> {Catchall, []}; _ -> FreshBlk(Default ++ [{jump, RestRef}], Catchall) %% ^ fall-through to the outer catchall end, + %% If we don't generate a switch, we need to pop the argument if on the stack. + Pop = [{'POP', ?void} || Arg == ?a], {Blk1, Code1, AltBlks} = case Type of boolean -> @@ -1461,7 +1459,7 @@ block(Blk = #blk{code = [{switch, Arg, Type, Alts, Default} | Code], _ -> FalseCode ++ [{jump, RestRef}] end, case lists:usort(Alts) == [missing] of - true -> {Blk#blk{code = [{jump, DefRef}]}, [], []}; + true -> {Blk#blk{code = Pop ++ [{jump, DefRef}]}, [], []}; false -> case Arg of ?i(false) -> {Blk#blk{code = ElseCode}, [], ThenBlk}; @@ -1471,18 +1469,28 @@ block(Blk = #blk{code = [{switch, Arg, Type, Alts, Default} | Code], end; tuple -> [TCode] = Alts, - {Blk#blk{code = TCode ++ [{jump, RestRef}]}, [], []}; + case TCode of + missing -> {Blk#blk{code = Pop ++ [{jump, DefRef}]}, [], []}; + _ -> {Blk#blk{code = Pop ++ TCode ++ [{jump, RestRef}]}, [], []} + end; {variant, [_]} -> %% [SINGLE_CON_SWITCH] Single constructor switches don't need a %% switch instruction. [AltCode] = Alts, - {Blk#blk{code = AltCode ++ [{jump, RestRef}]}, [], []}; + case AltCode of + missing -> {Blk#blk{code = Pop ++ [{jump, DefRef}]}, [], []}; + _ -> {Blk#blk{code = Pop ++ AltCode ++ [{jump, RestRef}]}, [], []} + end; {variant, _Ar} -> - MkBlk = fun(missing) -> {DefRef, []}; - (ACode) -> FreshBlk(ACode ++ [{jump, RestRef}], DefRef) - end, - {AltRefs, AltBs} = lists:unzip(lists:map(MkBlk, Alts)), - {Blk#blk{code = []}, [{switch, Arg, AltRefs}], lists:append(AltBs)} + case lists:usort(Alts) == [missing] of + true -> {Blk#blk{code = Pop ++ [{jump, DefRef}]}, [], []}; + false -> + MkBlk = fun(missing) -> {DefRef, []}; + (ACode) -> FreshBlk(ACode ++ [{jump, RestRef}], DefRef) + end, + {AltRefs, AltBs} = lists:unzip(lists:map(MkBlk, Alts)), + {Blk#blk{code = []}, [{switch, Arg, AltRefs}], lists:append(AltBs)} + end end, Blk2 = Blk1#blk{catchall = DefRef}, %% Update catchall ref block(Blk2, Code1 ++ Acc, DefBlk ++ RestBlk ++ AltBlks ++ Blocks, BlockAcc); @@ -1498,9 +1506,10 @@ optimize_blocks(Blocks) -> RBlockMap = maps:from_list(RBlocks), RBlocks1 = reorder_blocks(RBlocks, []), RBlocks2 = [ {Ref, inline_block(RBlockMap, Ref, Code)} || {Ref, Code} <- RBlocks1 ], - RBlocks3 = remove_dead_blocks(RBlocks2), - RBlocks4 = [ {Ref, tweak_returns(Code)} || {Ref, Code} <- RBlocks3 ], - Rev(RBlocks4). + RBlocks3 = shortcut_jump_chains(RBlocks2), + RBlocks4 = remove_dead_blocks(RBlocks3), + RBlocks5 = [ {Ref, tweak_returns(Code)} || {Ref, Code} <- RBlocks4 ], + Rev(RBlocks5). %% Choose the next block based on the final jump. reorder_blocks([], Acc) -> @@ -1536,6 +1545,21 @@ inline_block(BlockMap, Ref, [{jump, L} | Code] = Code0) when L /= Ref -> end; inline_block(_, _, Code) -> Code. +%% Shortcut jumps to blocks with a single jump +shortcut_jump_chains(RBlocks) -> + Subst = lists:foldl(fun({L1, [{jump, L2}]}, Sub) -> + Sub#{ L1 => maps:get(L2, Sub, L2) }; + (_, Sub) -> Sub end, #{}, RBlocks), + [ {Ref, update_labels(Subst, Code)} || {Ref, Code} <- RBlocks ]. + +update_labels(Sub, Ref) when is_reference(Ref) -> + maps:get(Ref, Sub, Ref); +update_labels(Sub, L) when is_list(L) -> + lists:map(fun(X) -> update_labels(Sub, X) end, L); +update_labels(Sub, T) when is_tuple(T) -> + list_to_tuple(update_labels(Sub, tuple_to_list(T))); +update_labels(_, X) -> X. + %% Remove unused blocks remove_dead_blocks(Blocks = [{Top, _} | _]) -> BlockMap = maps:from_list(Blocks), diff --git a/src/aesophia.app.src b/src/aesophia.app.src index d22866c..7efc2aa 100644 --- a/src/aesophia.app.src +++ b/src/aesophia.app.src @@ -1,6 +1,6 @@ {application, aesophia, [{description, "Contract Language for aeternity"}, - {vsn, "4.0.0"}, + {vsn, "4.1.0-rc1"}, {registered, []}, {applications, [kernel, diff --git a/test/aeso_compiler_tests.erl b/test/aeso_compiler_tests.erl index afa4e6a..50130e6 100644 --- a/test/aeso_compiler_tests.erl +++ b/test/aeso_compiler_tests.erl @@ -12,6 +12,14 @@ -include_lib("eunit/include/eunit.hrl"). +run_test(Test) -> + TestFun = list_to_atom(lists:concat([Test, "_test_"])), + [ begin + io:format("~s\n", [Label]), + Fun() + end || {Label, Fun} <- ?MODULE:TestFun() ], + ok. + %% Very simply test compile the given contracts. Only basic checks %% are made on the output, just that it is a binary which indicates %% that the compilation worked. @@ -702,3 +710,44 @@ failing_code_gen_contracts() -> "The state cannot contain functions in the AEVM. Use FATE if you need this.") ]. +validation_test_() -> + [{"Validation fail: " ++ C1 ++ " /= " ++ C2, + fun() -> + Actual = case validate(C1, C2) of + {error, Errs} -> Errs; + ok -> #{} + end, + check_errors(Expect, Actual) + end} || {C1, C2, Expect} <- validation_fails()] ++ + [{"Validation of " ++ C, + fun() -> + ?assertEqual(ok, validate(C, C)) + end} || C <- compilable_contracts()]. + +validation_fails() -> + [{"deadcode", "nodeadcode", + [<<"Data error:\n" + "Byte code does not match source code.\n" + "- Functions in the source code but not in the byte code:\n" + " .MyList.map2">>]}, + {"validation_test1", "validation_test2", + [<<"Data error:\n" + "Byte code does not match source code.\n" + "- The implementation of the function code_fail is different.\n" + "- The attributes of the function attr_fail differ:\n" + " Byte code: payable\n" + " Source code: \n" + "- The type of the function type_fail differs:\n" + " Byte code: integer => integer\n" + " Source code: {tvar,0} => {tvar,0}">>]}, + {"validation_test1", "validation_test3", + [<<"Data error:\n" + "Byte code contract is not payable, but source code contract is.">>]}]. + +validate(Contract1, Contract2) -> + ByteCode = #{ fate_code := FCode } = compile(fate, Contract1), + FCode1 = aeb_fate_code:serialize(aeb_fate_code:strip_init_function(FCode)), + Source = aeso_test_utils:read_contract(Contract2), + aeso_compiler:validate_byte_code(ByteCode#{ byte_code := FCode1 }, Source, + [{backend, fate}, {include, {file_system, [aeso_test_utils:contract_path()]}}]). + diff --git a/test/contracts/validation_test1.aes b/test/contracts/validation_test1.aes new file mode 100644 index 0000000..75b43d1 --- /dev/null +++ b/test/contracts/validation_test1.aes @@ -0,0 +1,4 @@ +contract ValidationTest = + payable entrypoint attr_fail() = () + entrypoint type_fail(x : int) = x + entrypoint code_fail(x) = x + 1 diff --git a/test/contracts/validation_test2.aes b/test/contracts/validation_test2.aes new file mode 100644 index 0000000..f77334e --- /dev/null +++ b/test/contracts/validation_test2.aes @@ -0,0 +1,4 @@ +contract ValidationTest = + entrypoint attr_fail() = () + entrypoint type_fail(x) = x + entrypoint code_fail(x) = x - 1 diff --git a/test/contracts/validation_test3.aes b/test/contracts/validation_test3.aes new file mode 100644 index 0000000..36af27d --- /dev/null +++ b/test/contracts/validation_test3.aes @@ -0,0 +1,4 @@ +payable contract ValidationTest = + payable entrypoint attr_fail() = () + entrypoint type_fail(x : int) = x + entrypoint code_fail(x) = x + 1