diff --git a/src/hz.erl b/src/hz.erl index 37e590c..98ba4f4 100644 --- a/src/hz.erl +++ b/src/hz.erl @@ -46,6 +46,7 @@ acc_pending_txs/1, next_nonce/1, dry_run/1, dry_run/2, dry_run/3, % dry_run_map/1, + read_contract_getter/4, read_contract_getter/5, tx/1, tx_info/1, post_tx/1, contract/1, contract_code/1, contract_source/1, @@ -71,6 +72,7 @@ contract_call/5, contract_call/6, contract_call/10, + parse_tx_info/2, decode_bytearray/2, spend/5, spend/10, sign_tx/2, sign_tx/3, @@ -627,7 +629,8 @@ dry_run(TX) -> -spec dry_run(TX, Accounts) -> {ok, Result} | {error, Reason} when TX :: binary() | string(), - Accounts :: [pubkey()], + Accounts :: [Account], + Account :: {pubkey(), integer()} | #{string() => term()}, Result :: term(), % FIXME Reason :: term(). % FIXME %% @doc @@ -643,7 +646,8 @@ dry_run(TX, Accounts) -> -spec dry_run(TX, Accounts, KBHash) -> {ok, Result} | {error, Reason} when TX :: binary() | string(), - Accounts :: [pubkey()], + Accounts :: [Account], + Account :: {pubkey(), integer()} | #{string() => term()}, KBHash :: binary() | string(), Result :: term(), % FIXME Reason :: term(). % FIXME @@ -652,21 +656,85 @@ dry_run(TX, Accounts) -> %% hash provided. dry_run(TX, Accounts, KBHash) -> + NAccounts = lists:map(fun normalize_account/1, Accounts), KBB = to_binary(KBHash), TXB = to_binary(TX), DryData = #{top => KBB, - accounts => Accounts, + accounts => NAccounts, txs => [#{tx => TXB}], tx_events => true}, JSON = zj:binary_encode(DryData), request("/v3/dry_run", JSON). +normalize_account({Pubkey, Amount}) -> + PubkeyBin = unicode:characters_to_binary(Pubkey), + #{"pub_key" => PubkeyBin, "amount" => Amount}; +normalize_account(Val) -> + Val. + % TODO %dry_run_map(Map) -> % JSON = zj:binary_encode(Map), % request("/v3/dry_run", JSON). +parse_tx_info({error, Reason}, _) -> + {error, Reason}; +parse_tx_info({ok, Result}, Format) -> + parse_tx_info(Result, Format); +parse_tx_info(#{"call_info" := #{"contract_id" := Contract}}, deploy) -> + % TODO: What happens if a contract deploy goes wrong? + {ok, Contract}; +parse_tx_info(#{"call_info" := #{"return_type" := Status, + "return_value" := Value}}, + Format) -> + parse_tx_value(Status, Value, Format); +parse_tx_info(#{"reason" := Reason, + "parameter" := Parameter, + "info" := #{"error" := Reason2, + "path" := Path, + "data" := Data}}, + _)-> + % Overall dry run error. Informative, but annoyingly inconsistent with all + % other cases. + {error, {Reason, Reason2, [Parameter | Path], Data}}; +parse_tx_info(#{"results" := Results}, Format) -> + % Dry run result, could be multiple results or one, and each could be a + % success or an error. + parse_tx_info(Results, Format); +parse_tx_info([Next, Then | Rest], Format) -> + case Next of + #{"call_obj" := #{"return_type" := "ok"}} -> + % Success. Assume this transaction was just setting up conditions + % for later transactions, and move on. + parse_tx_info([Then | Rest], Format); + _ -> + % Some error. Stop here and parse the error out. + parse_tx_info(Next, Format) + end; +parse_tx_info([Last], Format) -> + parse_tx_info(Last, Format); +parse_tx_info(#{"reason" := Message}, _) -> + % Dry run error for individual tx. + {error, Message}; +parse_tx_info(#{"call_obj" := #{"return_type" := Status, + "return_value" := Value}}, + Format) -> + % Dry run result. At this point we can parse it the same way we parse + % tx_info. + parse_tx_value(Status, Value, Format). + +parse_tx_value("revert", Value, _) -> + Message = decode_bytearray(Value, fate), + {error, {abort, Message}}; +parse_tx_value("error", Value, _) -> + % gmser takes binary inputs and gives binary outputs + EncodedBinary = list_to_binary(Value), + {contract_bytearray, Binary} = gmser_api_encoder:decode(EncodedBinary), + Message = binary_to_list(Binary), + {error, {contract_error, Message}}; +parse_tx_value("ok", Value, Format) -> + decode_bytearray(Value, Format). -spec decode_bytearray_fate(EncodedStr) -> {ok, Result} | {error, Reason} when EncodedStr :: binary() | string(), @@ -720,6 +788,31 @@ decode_bytearray2(FATE, sophia) -> hz_sophia:fate_to_list(FATE); decode_bytearray2(FATE, {sophia, Type}) -> hz_sophia:fate_to_list(Type, FATE); decode_bytearray2(FATE, {erlang, Type}) -> hz_aaci:fate_to_erlang(Type, FATE). +read_contract_getter(AACI, ConID, Fun, Args) -> + case contract(ConID) of + {ok, #{"owner_id" := CallerID}} -> + read_contract_getter(CallerID, AACI, ConID, Fun, Args); + {error, Reason} -> + {error, Reason} + end. + +read_contract_getter(CallerID, AACI, ConID, Fun, Args) -> + case convert_args(AACI, Fun, Args) of + {ok, {ArgsFATE, ReturnFormat}} -> + read_contract_getter2(CallerID, ConID, Fun, ArgsFATE, ReturnFormat); + {error, Reason} -> + {error, Reason} + end. + +read_contract_getter2(CallerID, ConID, Fun, Args, ReturnFormat) -> + case contract_call(CallerID, {}, ConID, Fun, {fate, Args}) of + {ok, TX} -> + Result = dry_run(TX, [{CallerID, 1 bsl 80}]), + parse_tx_info(Result, ReturnFormat); + {error, Reason} -> + {error, Reason} + end. + to_binary(S) when is_binary(S) -> S; to_binary(S) when is_list(S) -> list_to_binary(S). @@ -1614,44 +1707,47 @@ min_gas() -> 200_000. -encode_call_data({aaci, _ContractName, FunDefs, _TypeDefs}, Fun, Args) -> - case maps:find(Fun, FunDefs) of - {ok, {ArgDef, _ResultDef}} -> encode_call_data2(ArgDef, Fun, Args); - error -> {error, bad_fun_name} - end; -encode_call_data({aaci, Label}, Fun, Args) -> - case hz_man:lookup_aaci(Label) of - {ok, AACI} -> encode_call_data(AACI, Fun, Args); - error -> {error, aaci_not_found} +encode_call_data(AACI, Fun, Args) -> + case convert_args(AACI, Fun, Args) of + {ok, {ArgsFATE, _}} -> + gmb_fate_abi:create_calldata(Fun, ArgsFATE); + {error, Reason} -> + {error, Reason} end. -encode_call_data2(ArgDef, Fun, {sophia, Args}) -> - case convert(ArgDef, Args) of - {ok, Converted} -> gmb_fate_abi:create_calldata(Fun, Converted); - Errors -> Errors - end; -encode_call_data2(ArgDef, Fun, {erlang, Args}) -> - case hz_aaci:erlang_args_to_fate(ArgDef, Args) of - {ok, Coerced} -> gmb_fate_abi:create_calldata(Fun, Coerced); - Errors -> Errors - end; -encode_call_data2(_, Fun, {fate, Args}) -> - % TODO: This should probably be moved back closer to the initiating call. - % 2026-02-13: Craig - gmb_fate_abi:create_calldata(Fun, Args); -encode_call_data2(ArgDef, Fun, Args) -> - encode_call_data2(ArgDef, Fun, {sophia, Args}). +convert_args(_, _, {fate, Args}) -> + {ok, {Args, fate}}; +convert_args(AACI, Fun, Args) -> + case aaci_lookup_spec(AACI, Fun) of + {ok, {ArgTypes, ReturnType}} -> + convert_args2(ArgTypes, Args, ReturnType); + {error, Reason} -> + {error, Reason} + end. -convert(Defs, Args) -> convert(Defs, Args, 1, [], []). +convert_args2(ArgTypes, {erlang, Args}, ReturnType) -> + case hz_aaci:erlang_args_to_fate(ArgTypes, Args) of + {ok, Converted} -> {ok, {Converted, {erlang, ReturnType}}}; + {error, Reason} -> {error, Reason} + end; +convert_args2(ArgTypes, {sophia, Args}, ReturnType) -> + case sophia_args_to_fate(ArgTypes, Args) of + {ok, Converted} -> {ok, {Converted, {sophia, ReturnType}}}; + {error, Reason} -> {error, Reason} + end; +convert_args2(ArgTypes, Args, ReturnType) -> + convert_args2(ArgTypes, {sophia, Args}, ReturnType). -convert([{Name, Def} | Defs], [Arg | Args], Nth, Terms, Errors) -> +sophia_args_to_fate(Defs, Args) -> sophia_args_to_fate(Defs, Args, 1, [], []). + +sophia_args_to_fate([{Name, Def} | Defs], [Arg | Args], Nth, Terms, Errors) -> case hz_sophia:parse_literal(Def, Arg) of - {ok, Term} -> convert(Defs, Args, Nth + 1, [Term | Terms], Errors); - {error, Reason} -> convert(Defs, Args, Nth + 1, Terms, [{Nth, Name, Reason} | Errors]) + {ok, Term} -> sophia_args_to_fate(Defs, Args, Nth + 1, [Term | Terms], Errors); + {error, Reason} -> sophia_args_to_fate(Defs, Args, Nth + 1, Terms, [{Nth, Name, Reason} | Errors]) end; -convert([], [], _, Terms, []) -> +sophia_args_to_fate([], [], _, Terms, []) -> {ok, lists:reverse(Terms)}; -convert([], [], _, _, Errors) -> +sophia_args_to_fate([], [], _, _, Errors) -> {error, Errors}. -spec sign_tx(Unsigned, SecKey) -> Result diff --git a/src/hz_aaci.erl b/src/hz_aaci.erl index 5fa642a..447fd02 100644 --- a/src/hz_aaci.erl +++ b/src/hz_aaci.erl @@ -527,10 +527,7 @@ opaque_type(Params, #{record := FieldDefs}) -> || #{name := Name, type := Type} <- FieldDefs], {record, Fields}; opaque_type(Params, #{variant := VariantDefs}) -> - ConvertVariant = fun(Pair) -> - [{Name, Types}] = maps:to_list(Pair), - {binary_to_list(Name), [opaque_type(Params, Type) || Type <- Types]} - end, + ConvertVariant = fun(Pair) -> opaque_variant_each(Params, Pair) end, Variants = lists:map(ConvertVariant, VariantDefs), {variant, Variants}; opaque_type(Params, #{tuple := TypeDefs}) -> @@ -541,6 +538,11 @@ opaque_type(Params, Pair) when is_map(Pair) -> [{Name, TypeArgs}] = maps:to_list(Pair), {opaque_type_name(Name), [opaque_type(Params, Arg) || Arg <- TypeArgs]}. +opaque_variant_each(Params, Pair) -> + [{Name, Types}] = maps:to_list(Pair), + ElemTypes = [opaque_type(Params, Type) || Type <- Types], + {binary_to_list(Name), ElemTypes}. + -spec opaque_type_name(binary()) -> atom() | string(). % Atoms for any builtins that aren't qualified by a namespace in Sophia. @@ -848,7 +850,7 @@ erlang_args_to_fate(VarTypes, Terms) -> DefLength = length(VarTypes), ArgLength = length(Terms), if - DefLength =:= ArgLength -> coerce_zipped_bindings(lists:zip(VarTypes, Terms), to_fate, arg); + DefLength =:= ArgLength -> coerce_zipped_bindings(lists:zip(VarTypes, Terms), arg); DefLength > ArgLength -> {error, too_few_args}; DefLength < ArgLength -> {error, too_many_args} end. @@ -926,7 +928,10 @@ erlang_to_fate({O, N, char}, Str) -> single_error({invalid, O, N, Str}) end; erlang_to_fate({O, N, {bytes, [Count]}}, Bytes) when is_bitstring(Bytes) -> - coerce_bytes(O, N, Count, Bytes); + case check_bytes(O, N, Count, Bytes) of + ok -> {ok, {bytes, Bytes}}; + {error, Reason} -> {error, Reason} + end; erlang_to_fate({_, _, bits}, Num) when is_integer(Num) -> {ok, {bits, Num}}; erlang_to_fate({_, _, bits}, Bits) when is_bitstring(Bits) -> @@ -934,19 +939,19 @@ erlang_to_fate({_, _, bits}, Bits) when is_bitstring(Bits) -> <> = Bits, {ok, {bits, IntValue}}; erlang_to_fate({_, _, {list, [Type]}}, Data) when is_list(Data) -> - coerce_list(Type, Data, to_fate); + coerce_list(Type, Data); erlang_to_fate({_, _, {map, [KeyType, ValType]}}, Data) when is_map(Data) -> - coerce_map(KeyType, ValType, Data, to_fate); + coerce_map(KeyType, ValType, Data); erlang_to_fate({O, N, {tuple, ElementTypes}}, Data) when is_tuple(Data) -> ElementList = tuple_to_list(Data), - coerce_tuple(O, N, ElementTypes, ElementList, to_fate); + coerce_tuple(O, N, ElementTypes, ElementList); erlang_to_fate({O, N, {variant, Variants}}, Name) when is_list(Name) -> erlang_to_fate({O, N, {variant, Variants}}, {Name}); erlang_to_fate({O, N, {variant, Variants}}, Data) when is_tuple(Data), tuple_size(Data) > 0 -> [Name | Terms] = tuple_to_list(Data), case lookup_variant(Name, Variants) of {Tag, TermTypes} -> - coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, to_fate); + coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms); not_found -> ValidNames = [Valid || {Valid, _} <- Variants], single_error({invalid_variant, O, N, Name, ValidNames}) @@ -954,17 +959,15 @@ erlang_to_fate({O, N, {variant, Variants}}, Data) when is_tuple(Data), tuple_siz erlang_to_fate({O, N, {record, MemberTypes}}, Map) when is_map(Map) -> coerce_map_to_record(O, N, MemberTypes, Map); erlang_to_fate({O, N, {unknown_type, _}}, Data) -> - case N of - already_normalized -> - Message = "Warning: Unknown type ~p. Using term ~p as is.~n", - io:format(Message, [O, Data]); - _ -> - Message = "Warning: Unknown type ~p (i.e. ~p). Using term ~p as is.~n", - io:format(Message, [O, N, Data]) - end, + warn_unknown_type(O, N, Data), {ok, Data}; erlang_to_fate({O, N, _}, Data) -> single_error({invalid, O, N, Data}). +warn_unknown_type(O, already_normalized, Data) -> + io:format("Warning: Unknown type ~p. Using term ~p as is.~n", [O, Data]); +warn_unknown_type(O, N, Data) -> + io:format("Warning: Unknown type ~p (i.e. ~p). Using term ~p as is.~n", [O, N, Data]). + coerce_chain_object(_, _, _, _, {raw, Binary}) -> {ok, Binary}; coerce_chain_object(O, N, T, Tag, S) -> @@ -988,78 +991,78 @@ decode_chain_object(Tag, S) -> error:incorrect_size -> {error, incorrect_size} end. -coerce_bytes(O, N, _, Bytes) when bit_size(Bytes) rem 8 /= 0 -> +check_bytes(O, N, _, Bytes) when bit_size(Bytes) rem 8 /= 0 -> single_error({partial_bytes, O, N, bit_size(Bytes)}); -coerce_bytes(_, _, any, Bytes) -> - {ok, Bytes}; -coerce_bytes(O, N, Count, Bytes) when byte_size(Bytes) /= Count -> +check_bytes(_, _, any, _) -> + ok; +check_bytes(O, N, Count, Bytes) when byte_size(Bytes) /= Count -> single_error({incorrect_size, O, N, Bytes}); -coerce_bytes(_, _, _, Bytes) -> - {ok, Bytes}. +check_bytes(_, _, _, _) -> + ok. -coerce_zipped_bindings(Bindings, Direction, Tag) -> - coerce_zipped_bindings(Bindings, Direction, Tag, [], []). +coerce_zipped_bindings(Bindings, Tag) -> + coerce_zipped_bindings(Bindings, Tag, [], []). -coerce_zipped_bindings([Next | Rest], Direction, Tag, Good, Broken) -> +coerce_zipped_bindings([Next | Rest], Tag, Good, Broken) -> {{ArgName, Type}, Term} = Next, - case coerce_direction(Type, Term, Direction) of + case erlang_to_fate(Type, Term) of {ok, NewTerm} -> - coerce_zipped_bindings(Rest, Direction, Tag, [NewTerm | Good], Broken); + coerce_zipped_bindings(Rest, Tag, [NewTerm | Good], Broken); {error, Errors} -> Wrapped = wrap_errors({Tag, ArgName}, Errors), - coerce_zipped_bindings(Rest, Direction, Tag, Good, [Wrapped | Broken]) + coerce_zipped_bindings(Rest, Tag, Good, [Wrapped | Broken]) end; -coerce_zipped_bindings([], _, _, Good, []) -> +coerce_zipped_bindings([], _, Good, []) -> {ok, lists:reverse(Good)}; -coerce_zipped_bindings([], _, _, _, Broken) -> +coerce_zipped_bindings([], _, _, Broken) -> {error, combine_errors(Broken)}. -coerce_list(Type, Elements, Direction) -> +coerce_list(Type, Elements) -> % 0 index since it represents a sophia list - coerce_list(Type, Elements, Direction, 0, [], []). + coerce_list(Type, Elements, 0, [], []). -coerce_list(Type, [Next | Rest], Direction, Index, Good, Broken) -> - case coerce_direction(Type, Next, Direction) of - {ok, Coerced} -> coerce_list(Type, Rest, Direction, Index + 1, [Coerced | Good], Broken); +coerce_list(Type, [Next | Rest], Index, Good, Broken) -> + case erlang_to_fate(Type, Next) of + {ok, Coerced} -> coerce_list(Type, Rest, Index + 1, [Coerced | Good], Broken); {error, Errors} -> Wrapped = wrap_errors({index, Index}, Errors), - coerce_list(Type, Rest, Direction, Index + 1, Good, [Wrapped | Broken]) + coerce_list(Type, Rest, Index + 1, Good, [Wrapped | Broken]) end; -coerce_list(_Type, [], _, _, Good, []) -> +coerce_list(_Type, [], _, Good, []) -> {ok, lists:reverse(Good)}; -coerce_list(_, [], _, _, _, Broken) -> +coerce_list(_, [], _, _, Broken) -> {error, combine_errors(Broken)}. -coerce_map(KeyType, ValType, Data, Direction) -> - coerce_map(KeyType, ValType, maps:iterator(Data), Direction, #{}, []). +coerce_map(KeyType, ValType, Data) -> + coerce_map(KeyType, ValType, maps:iterator(Data), #{}, []). -coerce_map(KeyType, ValType, Remaining, Direction, Good, Broken) -> +coerce_map(KeyType, ValType, Remaining, Good, Broken) -> case maps:next(Remaining) of {K, V, RemainingAfter} -> - coerce_map2(KeyType, ValType, RemainingAfter, Direction, Good, Broken, K, V); + coerce_map2(KeyType, ValType, RemainingAfter, Good, Broken, K, V); none -> coerce_map_finish(Good, Broken) end. -coerce_map2(KeyType, ValType, Remaining, Direction, Good, Broken, K, V) -> - case coerce_direction(KeyType, K, Direction) of +coerce_map2(KeyType, ValType, Remaining, Good, Broken, K, V) -> + case erlang_to_fate(KeyType, K) of {ok, KFATE} -> - coerce_map3(KeyType, ValType, Remaining, Direction, Good, Broken, K, V, KFATE); + coerce_map3(KeyType, ValType, Remaining, Good, Broken, K, V, KFATE); {error, Errors} -> Wrapped = wrap_errors(map_key, Errors), % Continue as if the key coerced successfully, so that we can give % errors for both the key and the value. - coerce_map3(KeyType, ValType, Remaining, Direction, Good, [Wrapped | Broken], K, V, error) + coerce_map3(KeyType, ValType, Remaining, Good, [Wrapped | Broken], K, V, error) end. -coerce_map3(KeyType, ValType, Remaining, Direction, Good, Broken, K, V, KFATE) -> - case coerce_direction(ValType, V, Direction) of +coerce_map3(KeyType, ValType, Remaining, Good, Broken, K, V, KFATE) -> + case erlang_to_fate(ValType, V) of {ok, VFATE} -> NewGood = Good#{KFATE => VFATE}, - coerce_map(KeyType, ValType, Remaining, Direction, NewGood, Broken); + coerce_map(KeyType, ValType, Remaining, NewGood, Broken); {error, Errors} -> Wrapped = wrap_errors({map_value, K}, Errors), - coerce_map(KeyType, ValType, Remaining, Direction, Good, [Wrapped | Broken]) + coerce_map(KeyType, ValType, Remaining, Good, [Wrapped | Broken]) end. coerce_map_finish(Good, []) -> @@ -1076,13 +1079,10 @@ lookup_variant(Name, [_ | Rest], Tag) -> lookup_variant(_Name, [], _Tag) -> not_found. -coerce_tuple(O, N, TermTypes, Terms, Direction) -> - case coerce_tuple_elements(TermTypes, Terms, Direction, tuple_element) of +coerce_tuple(O, N, TermTypes, Terms) -> + case coerce_elems_to_fate(TermTypes, Terms, tuple_element) of {ok, Converted} -> - case Direction of - to_fate -> {ok, {tuple, list_to_tuple(Converted)}}; - from_fate -> {ok, list_to_tuple(Converted)} - end; + {ok, {tuple, list_to_tuple(Converted)}}; {error, too_few_terms} -> single_error({tuple_too_few_terms, O, N, list_to_tuple(Terms)}); {error, too_many_terms} -> @@ -1090,19 +1090,14 @@ coerce_tuple(O, N, TermTypes, Terms, Direction) -> Errors -> Errors end. -coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, Direction) -> +coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms) -> % FIXME: we could go through and add the variant tag to the adt_element % paths? - case coerce_tuple_elements(TermTypes, Terms, Direction, adt_element) of + case coerce_elems_to_fate(TermTypes, Terms, adt_element) of {ok, Converted} -> - case Direction of - to_fate -> - Arities = [length(VariantTerms) - || {_, VariantTerms} <- Variants], - {ok, {variant, Arities, Tag, list_to_tuple(Converted)}}; - from_fate -> - {ok, list_to_tuple([Name | Converted])} - end; + Arities = [length(VariantTerms) + || {_, VariantTerms} <- Variants], + {ok, {variant, Arities, Tag, list_to_tuple(Converted)}}; {error, too_few_terms} -> single_error({adt_too_few_terms, O, N, Name, TermTypes, Terms}); {error, too_many_terms} -> @@ -1110,32 +1105,32 @@ coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, Direction) -> Errors -> Errors end. -coerce_tuple_elements(Types, Terms, Direction, Tag) -> +coerce_elems_to_fate(Types, Terms, Tag) -> % The sophia standard library uses 0 indexing for lists, and fst/snd/thd % for tuples... Not sure how we should report errors in tuples, then. - coerce_tuple_elements(Types, Terms, Direction, Tag, 0, [], []). + coerce_elems_to_fate(Types, Terms, Tag, 0, [], []). -coerce_tuple_elements([Type | Types], [Term | Terms], Direction, Tag, Index, Good, Broken) -> - case coerce_direction(Type, Term, Direction) of +coerce_elems_to_fate([Type | Types], [Term | Terms], Tag, Index, Good, Broken) -> + case erlang_to_fate(Type, Term) of {ok, Value} -> - coerce_tuple_elements(Types, Terms, Direction, Tag, Index + 1, [Value | Good], Broken); + coerce_elems_to_fate(Types, Terms, Tag, Index + 1, [Value | Good], Broken); {error, Errors} -> Wrapped = wrap_errors({Tag, Index}, Errors), - coerce_tuple_elements(Types, Terms, Direction, Tag, Index + 1, Good, [Wrapped | Broken]) + coerce_elems_to_fate(Types, Terms, Tag, Index + 1, Good, [Wrapped | Broken]) end; -coerce_tuple_elements([], [], _, _, _, Good, []) -> +coerce_elems_to_fate([], [], _, _, Good, []) -> {ok, lists:reverse(Good)}; -coerce_tuple_elements([], [], _, _, _, _, Broken) -> +coerce_elems_to_fate([], [], _, _, _, Broken) -> {error, combine_errors(Broken)}; -coerce_tuple_elements(_, [], _, _, _, _, _) -> +coerce_elems_to_fate(_, [], _, _, _, _) -> {error, too_few_terms}; -coerce_tuple_elements([], _, _, _, _, _, _) -> +coerce_elems_to_fate([], _, _, _, _, _) -> {error, too_many_terms}. coerce_map_to_record(O, N, MemberTypes, Map) -> case zip_record_fields(MemberTypes, Map) of {ok, Zipped} -> - case coerce_zipped_bindings(Zipped, to_fate, field) of + case coerce_zipped_bindings(Zipped, field) of {ok, [SingleElem]} -> % Singleton records aren't implemented as FATE tuples at % all. @@ -1152,31 +1147,6 @@ coerce_map_to_record(O, N, MemberTypes, Map) -> single_error({unexpected_fields, O, N, Names}) end. -coerce_record_to_map(O, N, MemberTypes, Tuple) -> - {Names, Types} = lists:unzip(MemberTypes), - Terms = tuple_to_list(Tuple), - % FIXME: We could go through and change the record_element paths into field - % paths? - case coerce_tuple_elements(Types, Terms, from_fate, record_element) of - {ok, Converted} -> - Map = maps:from_list(lists:zip(Names, Converted)), - {ok, Map}; - {error, too_few_terms} -> - single_error({record_too_few_terms, O, N, Tuple}); - {error, too_many_terms} -> - single_error({record_too_many_terms, O, N, Tuple}); - {error, Errors} -> - correct_record_error_paths(Names, Errors) - end. - -correct_record_error_paths(Names, Errors) -> - CorrectOne = fun({Error, [{record_element, N} | Path]}) -> - FieldName = lists:nth(N + 1, Names), - {Error, [{record_element, N, FieldName} | Path]} - end, - Corrected = lists:map(CorrectOne, Errors), - {error, Corrected}. - zip_record_fields(Fields, Map) -> case lists:mapfoldl(fun zip_record_field/2, {Map, []}, Fields) of {_, {_, Missing = [_|_]}} -> @@ -1217,20 +1187,10 @@ combine_errors(Broken) -> %%% FATE to Erlang -% Not sure if this is needed... fate_to_erlang shouldn't fail. -coerce_direction(Type, Term, to_fate) -> - erlang_to_fate(Type, Term); -coerce_direction(Type, Term, from_fate) -> - fate_to_erlang(Type, Term). - - --spec fate_to_erlang(Type, FATE) -> {ok, Erlang} | {error, Errors} +-spec fate_to_erlang(Type, FATE) -> Erlang when Type :: annotated_type(), FATE :: gmb_fate_data:fate_type(), - Erlang :: erlang_repr(), - Errors :: [{Reason, [PathStep]}], - Reason :: term(), - PathStep :: term(). + Erlang :: erlang_repr(). %% @doc %% Convert a FATE-flavored Erlang term into a Sophia-flavored Erlang term %% Typically this is called by hakuzaru for you when decoding results from the @@ -1240,83 +1200,81 @@ coerce_direction(Type, Term, from_fate) -> %% information. fate_to_erlang({_, _, integer}, S) when is_integer(S) -> - {ok, S}; + S; fate_to_erlang({_, _, address}, {address, Bin}) -> Address = gmser_api_encoder:encode(account_pubkey, Bin), - {ok, unicode:characters_to_list(Address)}; + unicode:characters_to_list(Address); fate_to_erlang({_, _, contract}, {contract, Bin}) -> Address = gmser_api_encoder:encode(contract_pubkey, Bin), - {ok, unicode:characters_to_list(Address)}; + unicode:characters_to_list(Address); fate_to_erlang({_, _, signature}, Bin) -> Address = gmser_api_encoder:encode(signature, Bin), - {ok, unicode:characters_to_list(Address)}; + unicode:characters_to_list(Address); %fate_to_erlang({_, _, channel}, {channel, S}) when is_binary(S) -> - %{ok, S}; + %S; fate_to_erlang({_, _, boolean}, true) -> - {ok, true}; + true; fate_to_erlang({_, _, boolean}, false) -> - {ok, false}; + false; fate_to_erlang({_, _, string}, Bin) -> - Str = binary_to_list(Bin), - {ok, Str}; + binary_to_list(Bin); fate_to_erlang({_, _, char}, Val) -> - {ok, Val}; -fate_to_erlang({O, N, {bytes, [Count]}}, Bytes) when is_bitstring(Bytes) -> - coerce_bytes(O, N, Count, Bytes); + Val; +fate_to_erlang({O, N, {bytes, [Count]}}, {bytes, Bytes}) when is_bitstring(Bytes) -> + case check_bytes(O, N, Count, Bytes) of + ok -> Bytes; + {error, Reason} -> erlang:exit(Reason) + end; fate_to_erlang({_, _, bits}, {bits, Num}) -> - {ok, Num}; + Num; fate_to_erlang({_, _, {list, [Type]}}, Data) when is_list(Data) -> - coerce_list(Type, Data, from_fate); + Each = fun(Elem) -> fate_to_erlang(Type, Elem) end, + lists:map(Each, Data); fate_to_erlang({_, _, {map, [KeyType, ValType]}}, Data) when is_map(Data) -> - coerce_map(KeyType, ValType, Data, from_fate); -fate_to_erlang({O, N, {tuple, ElementTypes}}, {tuple, Data}) -> + coerce_map_to_erlang(KeyType, ValType, maps:iterator(Data), #{}); +fate_to_erlang({_, _, {tuple, ElementTypes}}, {tuple, Data}) -> ElementList = tuple_to_list(Data), - coerce_tuple(O, N, ElementTypes, ElementList, from_fate); -fate_to_erlang({O, N, {variant, Variants}}, {variant, _, Tag, Tuple}) -> + Elems = coerce_elems_to_erlang(ElementTypes, ElementList), + list_to_tuple(Elems); +fate_to_erlang({_, _, {variant, Variants}}, {variant, _, Tag, Tuple}) -> Terms = tuple_to_list(Tuple), - {Name, TermTypes} = lists:nth(Tag + 1, Variants), - coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, from_fate); -fate_to_erlang({O, N, {record, [SingleMemberType]}}, Data) -> + {Name, Types} = lists:nth(Tag + 1, Variants), + Elems = coerce_elems_to_erlang(Types, Terms), + list_to_tuple([Name | Elems]); +fate_to_erlang({_, _, {record, [SingleField]}}, Data) -> % Singleton records aren't implemented as FATE tuples at all. - % Pretend they are, so we can get the full error indexing of the - % non-singletone case. - coerce_record_to_map(O, N, [SingleMemberType], {Data}); -fate_to_erlang({O, N, {record, MemberTypes}}, {tuple, Tuple}) -> - coerce_record_to_map(O, N, MemberTypes, Tuple); + coerce_record_to_map([SingleField], [Data], #{}); +fate_to_erlang({_, _, {record, MemberTypes}}, {tuple, Tuple}) -> + Terms = tuple_to_list(Tuple), + coerce_record_to_map(MemberTypes, Terms, #{}); fate_to_erlang({O, N, {unknown_type, _}}, Data) -> - case N of - already_normalized -> - Message = "Warning: Unknown type ~p. Using term ~p as is.~n", - io:format(Message, [O, Data]); - _ -> - Message = "Warning: Unknown type ~p (i.e. ~p). Using term ~p as is.~n", - io:format(Message, [O, N, Data]) - end, - {ok, Data}; -fate_to_erlang(Type, Data) -> - TypeStr = type_to_iolist(Type), - io:format("Warning: Could not coerce term into ~s. Using term as is: ~p~n", [TypeStr, Data]), - {ok, Data}. + warn_unknown_type(O, N, Data), + Data; +fate_to_erlang({O, N, _}, Data) -> + erlang:exit({invalid, O, N, Data}). -type_to_iolist({O, already_normalized, S}) -> - % Already normalized. Example output: - % type {map, [string, integer]} - opaque_type_to_iolist(O, S); -type_to_iolist({O, N, S}) -> - % Type alias. Print the alias, and then print the normalized version in - % parentheses. Example output: - % type "my_alias" (i.e. record type {"my_record_type", [integer]}) - io_lib:format("type ~p (i.e. ~s)", [O, opaque_type_to_iolist(N, S)]). +coerce_elems_to_erlang(Types, Elems) -> + Zipped = lists:zip(Types, Elems), + Each = fun({Type, Elem}) -> fate_to_erlang(Type, Elem) end, + lists:map(Each, Zipped). -opaque_type_to_iolist(N, {record, _}) -> - % N is the name of a record definition. - io_lib:format("record type ~p", [N]); -opaque_type_to_iolist(N, {variant, _}) -> - % N is the name of a variant definition. - io_lib:format("variant type ~p", [N]); -opaque_type_to_iolist(N, _) -> - % N is some other constructive type. - io_lib:format("type ~p", [N]). +coerce_record_to_map([{Name, Type} | Types], [Term | Terms], Acc) -> + Coerced = fate_to_erlang(Type, Term), + NewAcc = maps:put(Name, Coerced, Acc), + coerce_record_to_map(Types, Terms, NewAcc); +coerce_record_to_map([], [], Acc) -> + Acc. + +coerce_map_to_erlang(KeyType, ValType, Iter, Acc) -> + case maps:next(Iter) of + {KeyFATE, ValFATE, Rest} -> + Key = fate_to_erlang(KeyType, KeyFATE), + Val = fate_to_erlang(ValType, ValFATE), + NewAcc = maps:put(Key, Val, Acc), + coerce_map_to_erlang(KeyType, ValType, Rest, NewAcc); + none -> + Acc + end. @@ -1354,7 +1312,7 @@ check_erlang_to_fate(Type, Sophia, Fate) -> end. check_fate_to_erlang(Type, Fate, Sophia) -> - {ok, SophiaActual} = fate_to_erlang(Type, Fate), + SophiaActual = fate_to_erlang(Type, Fate), % Now check that the results were what we expected. case SophiaActual of Sophia -> @@ -1452,7 +1410,7 @@ coerce_record_test() -> coerce_bytes_test() -> {ok, Type} = annotate_type({tuple, [{bytes, [4]}, {bytes, [any]}]}, #{}), - check_roundtrip(Type, {<<"abcd">>, <<"efghi">>}, {tuple, {<<"abcd">>, <<"efghi">>}}). + check_roundtrip(Type, {<<"abcd">>, <<"efghi">>}, {tuple, {{bytes, <<"abcd">>}, {bytes, <<"efghi">>}}}). coerce_bits_test() -> {ok, Type} = annotate_type(bits, #{}), @@ -1471,7 +1429,7 @@ coerce_unicode_test() -> coerce_hash_test() -> {ok, Type} = annotate_type("hash", builtin_typedefs()), Hash = list_to_binary(lists:seq(1,32)), - check_roundtrip(Type, Hash, Hash), + check_roundtrip(Type, Hash, {bytes, Hash}), ok. @@ -1519,10 +1477,7 @@ singleton_record_substitution_test() -> {ok, {[], GOutput}} = get_function_signature(AACI, "g"), check_roundtrip(GOutput, #{"it" => #{"it" => 123}}, 123), {ok, {[], HOutput}} = get_function_signature(AACI, "h"), - check_roundtrip(HOutput, #{"it" => {123, 456}}, {tuple, {123, 456}}), - % Also check that records have accurate paths, since the implementation for - % record error paths is a bit fiddly. - {error, [{{tuple_too_many_terms, _, _, _}, [{record_element, 0, "it"}]}]} = fate_to_erlang(HOutput, {tuple, {1, 2, 3}}). + check_roundtrip(HOutput, #{"it" => {123, 456}}, {tuple, {123, 456}}). tuple_substitution_test() -> Contract = " diff --git a/src/hz_sophia.erl b/src/hz_sophia.erl index 2a9892e..da7dd5f 100644 --- a/src/hz_sophia.erl +++ b/src/hz_sophia.erl @@ -343,6 +343,12 @@ parse_expression2(_, _, _, Token) -> unknown_type() -> {unknown_type, already_normalized, unknown_type}. +int_type() -> + {integer, already_normalized, integer}. + +int_list_type() -> + {{list, [integer]}, alread_normalized, {list, [int_type()]}}. + expect_tokens([], Pos, String) -> {ok, {Pos, String}}; expect_tokens([Str | Rest], Pos, String) -> @@ -377,11 +383,14 @@ parse_alphanum(Type, Pos, String, ["Bits", "all"], Row, Start, End) -> typecheck_bits(Type, Pos, String, -1, Row, Start, End); parse_alphanum(Type, Pos, String, ["Bits", "none"], Row, Start, End) -> typecheck_bits(Type, Pos, String, 0, Row, Start, End); +parse_alphanum(Type, Pos, String, ["variant"], Row, Start, End) -> + parse_anonymous_variant(Type, Pos, String, Row, Start, End); parse_alphanum(Type, Pos, String, [[C | _] = S], Row, Start, End) when ?IS_LATIN_LOWER(C) -> % From a programming perspective, we are trying to parse a constant, so % an alphanum token can really only be a constructor, or a chain object. - % Constructors start with uppercase characters, so lowercase can only be a - % chain object. + % Constructors start with uppercase characters, and we have handled our + % made-up 'variant' case explicitly, so the only other lowercase constants + % are serialized chain objects. try case gmser_api_encoder:decode(unicode:characters_to_binary(S)) of {account_pubkey, Data} -> @@ -400,8 +409,8 @@ parse_alphanum(Type, Pos, String, [[C | _] = S], Row, Start, End) when ?IS_LATIN _:_ -> {error, {unexpected_identifier, S, Row, Start, End}} end; parse_alphanum(Type, Pos, String, Path, Row, Start, End) -> - % Inversely, chain object prefixes are always lowercase, so any other path - % must be a variant constructor, or invalid. + % Now having handled all lowercase terms, anything else must be uppercase, + % which is either a variant constructor, or totally invalid. parse_variant(Type, Pos, String, Path, Row, Start, End). typecheck_integer({_, _, integer}, Pos, String, Value, _, _, _) -> @@ -731,6 +740,12 @@ parse_variant({O, N, {variant, Variants}}, Pos, String, [Namespace, Constructor] _ -> {error, {invalid_constructor, O, N, Namespace ++ "." ++ Constructor, Row, Start, End}} end; +parse_variant({_, _, unknown_type}, Pos, String, ["None"], _, _, _) -> + % Special case for None without type info. + parse_variant3([0, 1], 0, [], Pos, String); +parse_variant({_, _, unknown_type}, Pos, String, ["Some"], _, _, _) -> + % Also a special case for Some. + parse_variant3([0, 1], 1, [unknown_type()], Pos, String); parse_variant({_, _, unknown_type}, _, _, _, Row, Start, End) -> {error, {unresolved_variant, Row, Start, End}}; parse_variant({O, N, _}, _, _, _, Row, Start, End) -> @@ -753,8 +768,7 @@ get_typename(Name) -> parse_variant2(O, N, Variants, Pos, String, Prefix, Constructor, Row, Start, End) -> case lookup_variant(Constructor, Variants, 0) of {ok, {Tag, ElemTypes}} -> - GetArity = fun({_, OtherElemTypes}) -> length(OtherElemTypes) end, - Arities = lists:map(GetArity, Variants), + Arities = get_arities(Variants), parse_variant3(Arities, Tag, ElemTypes, Pos, String); error -> {error, {invalid_constructor, O, N, Prefix ++ Constructor, Row, Start, End}} @@ -790,6 +804,112 @@ lookup_variant(Ident, [{Ident, ElemTypes} | _], Tag) -> lookup_variant(Ident, [_ | Rest], Tag) -> lookup_variant(Ident, Rest, Tag + 1). +get_arities(Variants) -> + GetArity = fun({_, OtherElemTypes}) -> length(OtherElemTypes) end, + lists:map(GetArity, Variants). + +parse_anonymous_variant({O, N, {variant, Variants}}, Pos, String, _, _, _) -> + parse_anonymous_variant2({O, N, {variant, Variants}}, Pos, String); +parse_anonymous_variant({O, N, unknown_type}, Pos, String, _, _, _) -> + parse_anonymous_variant2({O, N, unknown_type}, Pos, String); +parse_anonymous_variant({O, N, _}, _, _, Row, Start, End) -> + {error, {wrong_type, O, N, variant, Row, Start, End}}. + +parse_anonymous_variant2(Type, Pos, String) -> + case expect_tokens(["("], Pos, String) of + {ok, {NewPos, NewString}} -> + parse_anonymous_variant3(Type, NewPos, NewString); + {error, Reason} -> + {error, Reason} + end. + +parse_anonymous_variant3(Type, Pos, String) -> + case parse_arities(Type, Pos, String) of + {ok, {Arities, NewPos, NewString}} -> + parse_anonymous_variant4(Type, NewPos, NewString, Arities); + {error, Reason} -> + {error, Reason} + end. + +parse_anonymous_variant4(Type, Pos, String, Arities) -> + case expect_tokens([","], Pos, String) of + {ok, {NewPos, NewString}} -> + parse_anonymous_variant5(Type, NewPos, NewString, Arities); + {error, Reason} -> + {error, Reason} + end. + +parse_anonymous_variant5(Type, Pos, String, Arities) -> + case parse_anonymous_tag(Pos, String, Arities) of + {ok, {Tag, NewPos, NewString}} -> + parse_anonymous_variant6(Type, NewPos, NewString, Arities, Tag); + {error, Reason} -> + {error, Reason} + end. + +parse_anonymous_variant6(Type, Pos, String, Arities, Tag) -> + ElemTypes = infer_anonymous_variant_elem_types(Type, Arities, Tag), + case parse_multivalue3(ElemTypes, Pos, String, []) of + {ok, {Terms, NewPos, NewString}} -> + Result = {variant, Arities, Tag, list_to_tuple(Terms)}, + {ok, {Result, NewPos, NewString}}; + {error, Reason} -> + {error, Reason} + end. + +parse_arities(Type, Pos, String) -> + case next_token(Pos, String) of + {ok, {Token, NewPos, NewString}} -> + parse_arities2(Type, NewPos, NewString, Token); + {error, Reason} -> + {error, Reason} + end. + +parse_arities2(Type, Pos, String, Token = {_, _, _, Row, Start, _}) -> + case parse_expression2(int_list_type(), Pos, String, Token) of + {ok, {Arities, NewPos, NewString}} -> + parse_arities3(Type, NewPos, NewString, Arities, Row, Start); + {error, Reason} -> + {error, Reason} + end. + +parse_arities3({O, N, {variant, Variants}}, Pos, String, Arities, Row, Start) -> + ExpectedArities = get_arities(Variants), + case Arities == ExpectedArities of + true -> + {ok, {Arities, Pos, String}}; + false -> + {error, {wrong_arities, O, N, Arities, Row, Start}} + end; +parse_arities3(_, Pos, String, Arities, _, _) -> + {ok, {Arities, Pos, String}}. + +parse_anonymous_tag(Pos, String, Arities) -> + case next_token(Pos, String) of + {ok, {Token, NewPos, NewString}} -> + parse_anonymous_tag2(NewPos, NewString, Arities, Token); + {error, Reason} -> + {error, Reason} + end. + +parse_anonymous_tag2(Pos, String, Arities, Token = {_, _, _, Row, Start, End}) -> + TagCount = length(Arities), + case parse_expression2(int_type(), Pos, String, Token) of + {ok, {Tag, _, _}} when Tag < 0 -> + {error, {negative_tag, Tag, Row, Start, End}}; + {ok, {Tag, _, _}} when Tag >= TagCount -> + {error, {invalid_tag, Tag, TagCount, Row, Start, End}}; + Result -> + Result + end. + +infer_anonymous_variant_elem_types({_, _, {variant, Variants}}, _, Tag) -> + {_Name, ElemTypes} = lists:nth(Tag + 1, Variants), + ElemTypes; +infer_anonymous_variant_elem_types({_, _, unknown_type}, Arities, Tag) -> + Arity = lists:nth(Tag + 1, Arities), + lists:duplicate(Arity, unknown_type()). + %%% Record parsing parse_record_or_map({_, _, {map, [KeyType, ValueType]}}, Pos, String, _, _) -> @@ -1027,15 +1147,12 @@ fate_to_iolist(Type, {tuple, Tuple}) -> _ -> tuple_to_iolist([], Tuple) end; -fate_to_iolist(Type, {variant, _, Tag, Tuple}) -> +fate_to_iolist(Type, {variant, Arities, Tag, Tuple}) -> case Type of {O, N, {variant, VariantTypes}} when Tag < length(VariantTypes) -> variant_to_iolist(O, N, VariantTypes, Tag, Tuple); - {O, N, _} -> - % TODO: Make up a special syntax for anonymous variant terms. - erlang:exit({untyped_variant, O, N}); - _ -> - erlang:exit({untyped_variant, unknown_type, already_normalized}) + {_, _, _} -> + anonymous_variant_to_iolist(Arities, Tag, Tuple) end; fate_to_iolist(Type, List) when is_list(List) -> case Type of @@ -1130,6 +1247,22 @@ choose_variant_prefix(O, N) -> [] end. +% We don't have type information, but the Sophia programming language doesn't +% have syntax for anonymous variants, so we have to make a syntax up. This +% syntax is also supported when parsing terms, so that the output of one +% contract call can be fed easily into another contract call. +anonymous_variant_to_iolist(Arities, Tag, Tuple) -> + % Extract the elements of the tuple. + Elems = tuple_to_list(Tuple), + + % Turn the arities, tag, and elements into an iolist. + AritiesStr = list_to_iolist(int_type(), Arities), + TagStr = integer_to_list(Tag), + FullTermsStr = list_elems_to_iolist(unknown_type(), Elems, [AritiesStr, ", ", TagStr]), + + % Wrap that iolist in the anonymous 'variant' constructor. + ["variant(", FullTermsStr, ")"]. + multivalue_to_iolist([FirstType | ElemTypes], [FirstTerm | Elems]) -> FirstTermChars = fate_to_iolist(FirstType, FirstTerm), multivalue_to_iolist(ElemTypes, Elems, FirstTermChars); @@ -1282,16 +1415,18 @@ check_parser_roundtrip(Sophia) -> % syntax. Let's do a lenient test. roundtrip_parser_lenient(unknown_type(), Sophia, Fate). -check_parser_with_typedef(Typedef, Sophia) -> +check_parser_with_typedef(Typedef, Sophia, UntypedSophia) -> % Compile the type definitions alongside the usual literal expression. Source = "contract C =\n " ++ Typedef ++ "\n entrypoint f() = " ++ Sophia, {Fate, Type} = compile_entrypoint_value_and_type(Source, "f"), - % Do a typed parse, as usual, but there are probably record/variant - % definitions in the AACI, so untyped parses probably don't work, and - % variants often have optional namespaces, so the sophia result might not - % match exactly, but should still be equivalent. - roundtrip_parser_lenient(Type, Sophia, Fate). + % Do a typed parse, as usual. Variant namespaces can make pretty printing + % ambiguous, so make the roundtrip lenient. + roundtrip_parser_lenient(Type, Sophia, Fate), + % Do an untyped parse, but using a second special Sophia expression that + % doesn't require type info to parse. This one *doesn't* need to be + % lenient, since we are specifying a distinct sophia expression. + roundtrip_parser(unknown_type(), UntypedSophia, Fate). anon_types_test() -> % Integers. @@ -1323,6 +1458,10 @@ anon_types_test() -> check_parser_roundtrip("(1, [2, 3], (4, 5))"), % Map. check_parser_roundtrip("{[1] = 2, [3] = 4}"), + % Option. + check_parser_roundtrip("None"), + check_parser_roundtrip("Some(1)"), + check_parser_roundtrip("Some([1, 2, 3])"), ok. @@ -1342,7 +1481,7 @@ string_escape_codes_test() -> records_test() -> TypeDef = "record pair = {x: int, y: int}", Sophia = "{x = 1, y = 2}", - check_parser_with_typedef(TypeDef, Sophia), + check_parser_with_typedef(TypeDef, Sophia, "(1, 2)"), % The above won't run an untyped parse on the expression, but we can. It % will error, though. {error, {unresolved_record, _, _, _}} = parse_literal(unknown_type(), Sophia). @@ -1350,11 +1489,11 @@ records_test() -> variant_test() -> TypeDef = "datatype multi('a) = Zero | One('a) | Two('a, 'a)", - check_parser_with_typedef(TypeDef, "Zero"), - check_parser_with_typedef(TypeDef, "One(0)"), - check_parser_with_typedef(TypeDef, "Two(0, 1)"), - check_parser_with_typedef(TypeDef, "Two([], [1, 2, 3])"), - check_parser_with_typedef(TypeDef, "C.Zero"), + check_parser_with_typedef(TypeDef, "Zero", "variant([0, 1, 2], 0)"), + check_parser_with_typedef(TypeDef, "One(0)", "variant([0, 1, 2], 1, 0)"), + check_parser_with_typedef(TypeDef, "Two(0, 1)", "variant([0, 1, 2], 2, 0, 1)"), + check_parser_with_typedef(TypeDef, "Two([], [1, 2, 3])", "variant([0, 1, 2], 2, [], [1, 2, 3])"), + check_parser_with_typedef(TypeDef, "C.Zero", "variant([0, 1, 2], 0)"), {error, {unresolved_variant, _, _, _}} = parse_literal(unknown_type(), "Zero"), @@ -1362,10 +1501,10 @@ variant_test() -> ambiguous_variant_test() -> TypeDef = "datatype mytype = C | D", - check_parser_with_typedef(TypeDef, "C"), - check_parser_with_typedef(TypeDef, "D"), - check_parser_with_typedef(TypeDef, "C.C"), - check_parser_with_typedef(TypeDef, "C.D"), + check_parser_with_typedef(TypeDef, "C", "variant([0, 0], 0)"), + check_parser_with_typedef(TypeDef, "D", "variant([0, 0], 1)"), + check_parser_with_typedef(TypeDef, "C.C", "variant([0, 0], 0)"), + check_parser_with_typedef(TypeDef, "C.D", "variant([0, 0], 1)"), ok. @@ -1410,9 +1549,9 @@ bits_test() -> singleton_records_test() -> TypeDef = "record singleton('a) = {it: 'a}", - check_parser_with_typedef(TypeDef, "{it = 123}"), - check_parser_with_typedef(TypeDef, "{it = {it = {it = 5}}}"), - check_parser_with_typedef(TypeDef, "[{it = 1}, {it = 2}, {it = 3}]"), + check_parser_with_typedef(TypeDef, "{it = 123}", "123"), + check_parser_with_typedef(TypeDef, "{it = {it = {it = 5}}}", "5"), + check_parser_with_typedef(TypeDef, "[{it = 1}, {it = 2}, {it = 3}]", "[1, 2, 3]"), ok. @@ -1421,9 +1560,9 @@ singleton_variants_test() -> % actually a special case; singleton variants are in fact wrapped in the % FATE too. TypeDef = "datatype wrapped('a) = Wrap('a)", - check_parser_with_typedef(TypeDef, "Wrap(123)"), - check_parser_with_typedef(TypeDef, "Wrap(Wrap(123))"), - check_parser_with_typedef(TypeDef, "[Wrap(1), Wrap(2), Wrap(3)]"), + check_parser_with_typedef(TypeDef, "Wrap(123)", "variant([1], 0, 123)"), + check_parser_with_typedef(TypeDef, "Wrap(Wrap(123))", "variant([1], 0, variant([1], 0, 123))"), + check_parser_with_typedef(TypeDef, "[Wrap(1), Wrap(2), Wrap(3)]", "[variant([1], 0, 1), variant([1], 0, 2), variant([1], 0, 3)]"), ok.