17 Commits

Author SHA1 Message Date
Jarvis Carroll cf66548443 Fix read_contract_getter/4
This was meant to be a placeholder that I would catch and fix, but my test case never hit it! Whoops.
2026-06-15 04:52:19 +00:00
Jarvis Carroll 4302ae002c add parse_tx_info and read_contract_getter
parse_tx_info takes the output of tx_info OR dry_run and strips it down to a cb_ encoded binary,
and then passes that cb_ encoded binary to decode_bytearray, using the Format specified.

read_contract_getter combines contract_call and dry_run, but automatically identifies the owner of the contract,
and uses that as the caller, and gives the caller a huge amount of gajus for the purpose of the dry run, so that
the call always succeeds. This operation should be available in the node itself, rather than requiring us to do
this huge back and forth for something as simple as reading the contents of the blockchain, but at least we can
abstract over this in the tooling, and save the user from having to think about these steps.
2026-06-15 02:21:27 +00:00
Jarvis Carroll 6daad4974c unwrap fate_to_erlang results
fate_to_erlang can only really fail at runtime if the wrong AACI is
provided, in which case the details of how failure occured are not
helpful, or recoverable. Anything else will be so broken that dialyzer
will catch it, or is a bug in hakuzaru, that we want to know about.
2026-06-08 07:23:34 +00:00
Jarvis Carroll d323fb0f52 Add special anonymous variant syntax
This is outside of the scope of the sophia parser, but is a simple generalization to
'sophia terms' to make them able to represent any FATE term anonymously.

We also parse these anonymous variant expressions without type info, since it is convenient
for users to copy the output of one call into another call.

Anonymous parsing of None and Some was also added, since new users would be shocked if this
doesn't work, and advanced users will greatly appreciate that it does. The resulting FATE
terms are still rendered as variant([0, 1], ...), since user defined types can also have [0, 1]
as their arity list, and since automation and tooling programmers hate special case exceptions like that.

Anonymous parsing of other Chain and AENS terms are not added, since anonymous variants already cover those types,
so very little is gained by hard-coding such complex types into the term parser. Complex, version-specific compiler
types are already supported by hakuzaru, in the form of the ACI/AACI; parsing without AACI, on the other hand, is
intended to support language-agnostic communication using the primitives of FATE, and in general, variants
in FATE are anonymous.
2026-06-05 03:08:38 +00:00
Jarvis Carroll ea3a5453f2 fix bytes coerce logic 2026-05-28 00:41:51 +00:00
zxq9 75bc52ede3 Doc formatting adjustments 2026-05-27 21:49:30 +09:00
zxq9 29619f08b7 Remove stdout line 2026-05-27 16:50:23 +09:00
zxq9 af46223163 Minor fixes 2026-05-27 16:41:45 +09:00
zxq9 9cafdd2b0f Merge pull request 'Improve specs' (#31) from improve_specs into master
Reviewed-on: #31
2026-05-26 15:38:26 +09:00
zxq9 6d429aa6a4 Merge branch 'master' into improve_specs 2026-05-26 15:38:08 +09:00
zxq9 fcf85077b2 Minor 2026-05-26 15:36:16 +09:00
zxq9 3585dbe534 Merge pull request 'Doc update for hz_sophia and hz_aaci and some minor fixes' (#30) from spivee/docs into master
Reviewed-on: #30
2026-05-26 09:44:53 +09:00
dimitar.p.ivanov 9a7a2a98c4 General polish (#28)
Co-authored-by: Craig Everett <zxq9@zxq9.com>
Co-authored-by: Dimitar Ivanov <dimitar.p.ivanov@gmail.com>
Reviewed-on: #28
2026-05-22 16:54:16 +09:00
dimitar.p.ivanov 4530fd2e93 Merge pull request 'Fix typespec' (#29) from respec into improve_specs
Reviewed-on: #29
2026-05-22 15:57:11 +09:00
zxq9 2a7079129f Fix typespec
Source needs to be defined as a binary.
2026-05-21 17:07:57 +09:00
Dimitar Ivanov 88aeb39d4a Fix a contract create bug 2026-05-20 17:02:36 +03:00
Dimitar Ivanov f0f86ed36d Improve specs 2026-05-13 10:04:23 +03:00
5 changed files with 755 additions and 537 deletions
+3 -2
View File
@@ -1,4 +1,5 @@
@author Craig Everett <craigeverett@qpq.swiss> [https://git.qpq.swiss/QPQ-AG/hakuzaru] @author Craig Everett <craigeverett@qpq.swiss> [https://zxq9.com]
@author Jarvis Carrol <jarviscarrol@qpq.swiss> [https://jarviscarroll.net/]
@version 0.9.2 @version 0.9.2
@title Hakuzaru: Gajumaru blockchain bindings for Erlang @title Hakuzaru: Gajumaru blockchain bindings for Erlang
@@ -21,7 +22,7 @@ After startup `hz_man' must be given the address and port of a list of Gajumaru
Note that the service nodes will need to have the dry-run endpoint enabled and the internal service query port made available in order to provide dry-runs and transaction submission. Note that the service nodes will need to have the dry-run endpoint enabled and the internal service query port made available in order to provide dry-runs and transaction submission.
When configuring chain nodes a list of nodes should be provided. When configuring chain nodes a list of nodes should be provided.
To avoid sync issues in the case of fast transaction formation/submission to the chain, only one node from the list of chain nodes is used for submitting transactions and querying `next_nonce/1`. To avoid sync issues in the case of fast transaction formation/submission to the chain, only one node from the list of chain nodes is used for submitting transactions and querying `next_nonce/1'.
This node is called "the sticky node". This node is called "the sticky node".
The first node in the list of chain nodes provided during configuration is designated as the sticky node. The first node in the list of chain nodes provided during configuration is designated as the sticky node.
+142 -37
View File
@@ -45,7 +45,8 @@
acc/1, acc_at_height/2, acc_at_block_id/2, acc/1, acc_at_height/2, acc_at_block_id/2,
acc_pending_txs/1, acc_pending_txs/1,
next_nonce/1, next_nonce/1,
dry_run/1, dry_run/2, dry_run/3, dry_run_map/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, tx/1, tx_info/1,
post_tx/1, post_tx/1,
contract/1, contract_code/1, contract_source/1, contract/1, contract_code/1, contract_source/1,
@@ -71,6 +72,7 @@
contract_call/5, contract_call/5,
contract_call/6, contract_call/6,
contract_call/10, contract_call/10,
parse_tx_info/2,
decode_bytearray/2, decode_bytearray/2,
spend/5, spend/10, spend/5, spend/10,
sign_tx/2, sign_tx/3, sign_tx/2, sign_tx/3,
@@ -627,7 +629,8 @@ dry_run(TX) ->
-spec dry_run(TX, Accounts) -> {ok, Result} | {error, Reason} -spec dry_run(TX, Accounts) -> {ok, Result} | {error, Reason}
when TX :: binary() | string(), when TX :: binary() | string(),
Accounts :: [pubkey()], Accounts :: [Account],
Account :: {pubkey(), integer()} | #{string() => term()},
Result :: term(), % FIXME Result :: term(), % FIXME
Reason :: term(). % FIXME Reason :: term(). % FIXME
%% @doc %% @doc
@@ -643,7 +646,8 @@ dry_run(TX, Accounts) ->
-spec dry_run(TX, Accounts, KBHash) -> {ok, Result} | {error, Reason} -spec dry_run(TX, Accounts, KBHash) -> {ok, Result} | {error, Reason}
when TX :: binary() | string(), when TX :: binary() | string(),
Accounts :: [pubkey()], Accounts :: [Account],
Account :: {pubkey(), integer()} | #{string() => term()},
KBHash :: binary() | string(), KBHash :: binary() | string(),
Result :: term(), % FIXME Result :: term(), % FIXME
Reason :: term(). % FIXME Reason :: term(). % FIXME
@@ -652,21 +656,85 @@ dry_run(TX, Accounts) ->
%% hash provided. %% hash provided.
dry_run(TX, Accounts, KBHash) -> dry_run(TX, Accounts, KBHash) ->
NAccounts = lists:map(fun normalize_account/1, Accounts),
KBB = to_binary(KBHash), KBB = to_binary(KBHash),
TXB = to_binary(TX), TXB = to_binary(TX),
DryData = #{top => KBB, DryData = #{top => KBB,
accounts => Accounts, accounts => NAccounts,
txs => [#{tx => TXB}], txs => [#{tx => TXB}],
tx_events => true}, tx_events => true},
JSON = zj:binary_encode(DryData), JSON = zj:binary_encode(DryData),
request("/v3/dry_run", JSON). 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 % TODO
%dry_run_map(Map) -> %dry_run_map(Map) ->
% JSON = zj:binary_encode(Map), % JSON = zj:binary_encode(Map),
% request("/v3/dry_run", JSON). % 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} -spec decode_bytearray_fate(EncodedStr) -> {ok, Result} | {error, Reason}
when EncodedStr :: binary() | string(), 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, {sophia, Type}) -> hz_sophia:fate_to_list(Type, FATE);
decode_bytearray2(FATE, {erlang, Type}) -> hz_aaci:fate_to_erlang(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_binary(S) -> S;
to_binary(S) when is_list(S) -> list_to_binary(S). to_binary(S) when is_list(S) -> list_to_binary(S).
@@ -790,7 +883,7 @@ contract_code(ID) ->
Result :: {ok, Source} Result :: {ok, Source}
| {project, Bundle} | {project, Bundle}
| {error, Reason}, | {error, Reason},
Source :: string(), Source :: binary(),
Bundle :: [{FilePath :: string(), Contents :: binary()}], Bundle :: [{FilePath :: string(), Contents :: binary()}],
Reason :: chain_error() | string(). Reason :: chain_error() | string().
%% @doc %% @doc
@@ -811,6 +904,8 @@ extract(Blobby) ->
extract2(TarBaby) -> extract2(TarBaby) ->
case erl_tar:extract({binary, TarBaby}, [memory, compressed]) of case erl_tar:extract({binary, TarBaby}, [memory, compressed]) of
{ok, [{_, Source}]} ->
{ok, Source};
{ok, Bundle} -> {ok, Bundle} ->
{project, Bundle}; {project, Bundle};
{error,invalid_tar_checksum} -> {error,invalid_tar_checksum} ->
@@ -899,6 +994,12 @@ request(Path) ->
hz_man:request(unicode:characters_to_list(Path)). hz_man:request(unicode:characters_to_list(Path)).
-spec request(Path, Payload) -> {ok, Value} | {error, Reason}
when Path :: unicode:charlist(),
Payload :: unicode:charlist(),
Value :: map(),
Reason :: hz:chain_error().
request(Path, Payload) -> request(Path, Payload) ->
hz_man:request(unicode:characters_to_list(Path), Payload). hz_man:request(unicode:characters_to_list(Path), Payload).
@@ -934,7 +1035,7 @@ contract_create(CreatorID, Path, InitArgs) ->
Gas = 500000, Gas = 500000,
GasPrice = min_gas_price(), GasPrice = min_gas_price(),
contract_create(CreatorID, Nonce, contract_create(CreatorID, Nonce,
Amount, TTL, Gas, GasPrice, Gas, GasPrice, Amount, TTL,
Path, InitArgs); Path, InitArgs);
Error -> Error ->
Error Error
@@ -1606,44 +1707,47 @@ min_gas() ->
200_000. 200_000.
encode_call_data({aaci, _ContractName, FunDefs, _TypeDefs}, Fun, Args) -> encode_call_data(AACI, Fun, Args) ->
case maps:find(Fun, FunDefs) of case convert_args(AACI, Fun, Args) of
{ok, {ArgDef, _ResultDef}} -> encode_call_data2(ArgDef, Fun, Args); {ok, {ArgsFATE, _}} ->
error -> {error, bad_fun_name} gmb_fate_abi:create_calldata(Fun, ArgsFATE);
end; {error, Reason} ->
encode_call_data({aaci, Label}, Fun, Args) -> {error, Reason}
case hz_man:lookup_aaci(Label) of
{ok, AACI} -> encode_call_data(AACI, Fun, Args);
error -> {error, aaci_not_found}
end. end.
encode_call_data2(ArgDef, Fun, {sophia, Args}) -> convert_args(_, _, {fate, Args}) ->
case convert(ArgDef, Args) of {ok, {Args, fate}};
{ok, Converted} -> gmb_fate_abi:create_calldata(Fun, Converted); convert_args(AACI, Fun, Args) ->
Errors -> Errors case aaci_lookup_spec(AACI, Fun) of
end; {ok, {ArgTypes, ReturnType}} ->
encode_call_data2(ArgDef, Fun, {erlang, Args}) -> convert_args2(ArgTypes, Args, ReturnType);
case hz_aaci:erlang_args_to_fate(ArgDef, Args) of {error, Reason} ->
{ok, Coerced} -> gmb_fate_abi:create_calldata(Fun, Coerced); {error, Reason}
Errors -> Errors end.
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(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 case hz_sophia:parse_literal(Def, Arg) of
{ok, Term} -> convert(Defs, Args, Nth + 1, [Term | Terms], Errors); {ok, Term} -> sophia_args_to_fate(Defs, Args, Nth + 1, [Term | Terms], Errors);
{error, Reason} -> convert(Defs, Args, Nth + 1, Terms, [{Nth, Name, Reason} | Errors]) {error, Reason} -> sophia_args_to_fate(Defs, Args, Nth + 1, Terms, [{Nth, Name, Reason} | Errors])
end; end;
convert([], [], _, Terms, []) -> sophia_args_to_fate([], [], _, Terms, []) ->
{ok, lists:reverse(Terms)}; {ok, lists:reverse(Terms)};
convert([], [], _, _, Errors) -> sophia_args_to_fate([], [], _, _, Errors) ->
{error, Errors}. {error, Errors}.
-spec sign_tx(Unsigned, SecKey) -> Result -spec sign_tx(Unsigned, SecKey) -> Result
@@ -1741,6 +1845,7 @@ spend(SenderID, SecKey, RecipientID, Amount, Payload, Height, NetworkID) ->
TTL :: non_neg_integer(), TTL :: non_neg_integer(),
Nonce :: non_neg_integer(), Nonce :: non_neg_integer(),
Payload :: binary(), Payload :: binary(),
NetworkID :: unicode:chardata(),
Result :: term(), % FIXME Result :: term(), % FIXME
Reason :: chain_error() | string(). Reason :: chain_error() | string().
%% @doc %% @doc
+408 -437
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -172,7 +172,6 @@ start_link() ->
%% preparatory work necessary for proper function. %% preparatory work necessary for proper function.
init(none) -> init(none) ->
ok = io:format("hz_man starting.~n"),
State = #s{}, State = #s{},
{ok, State}. {ok, State}.
+181 -39
View File
@@ -53,7 +53,7 @@ parse_literal2(Result, Pos, String) ->
%% @doc %% @doc
%% Parse an untyped Sophia expression into a FATE term %% Parse an untyped Sophia expression into a FATE term
%% Like parse_literal/2, but will not produce type errors. This function can %% Like `parse_literal/2', but will not produce type errors. This function can
%% still produce parsing errors, and can produce errors when variants or %% still produce parsing errors, and can produce errors when variants or
%% records are encountered, since they can't be parsed unless you have type %% records are encountered, since they can't be parsed unless you have type
%% information. %% information.
@@ -67,6 +67,7 @@ parse_literal2(Result, Pos, String) ->
parse_literal(String) -> parse_literal(String) ->
parse_literal(unknown_type(), String). parse_literal(unknown_type(), String).
%%% Tokenizer %%% Tokenizer
-define(IS_LATIN_UPPER(C), (((C) >= $A) and ((C) =< $Z))). -define(IS_LATIN_UPPER(C), (((C) >= $A) and ((C) =< $Z))).
@@ -252,6 +253,8 @@ escape_char($\") -> "\\\"";
escape_char($\\) -> "\\\\"; escape_char($\\) -> "\\\\";
escape_char(I) -> I. escape_char(I) -> I.
%%% Sophia Literal Parser %%% Sophia Literal Parser
%%% This parser is a simple recursive descent parser, written explicitly in %%% This parser is a simple recursive descent parser, written explicitly in
@@ -340,6 +343,12 @@ parse_expression2(_, _, _, Token) ->
unknown_type() -> unknown_type() ->
{unknown_type, already_normalized, 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) -> expect_tokens([], Pos, String) ->
{ok, {Pos, String}}; {ok, {Pos, String}};
expect_tokens([Str | Rest], Pos, String) -> expect_tokens([Str | Rest], Pos, String) ->
@@ -374,11 +383,14 @@ parse_alphanum(Type, Pos, String, ["Bits", "all"], Row, Start, End) ->
typecheck_bits(Type, Pos, String, -1, Row, Start, End); typecheck_bits(Type, Pos, String, -1, Row, Start, End);
parse_alphanum(Type, Pos, String, ["Bits", "none"], Row, Start, End) -> parse_alphanum(Type, Pos, String, ["Bits", "none"], Row, Start, End) ->
typecheck_bits(Type, Pos, String, 0, 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) -> 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 % 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. % an alphanum token can really only be a constructor, or a chain object.
% Constructors start with uppercase characters, so lowercase can only be a % Constructors start with uppercase characters, and we have handled our
% chain object. % made-up 'variant' case explicitly, so the only other lowercase constants
% are serialized chain objects.
try try
case gmser_api_encoder:decode(unicode:characters_to_binary(S)) of case gmser_api_encoder:decode(unicode:characters_to_binary(S)) of
{account_pubkey, Data} -> {account_pubkey, Data} ->
@@ -397,8 +409,8 @@ parse_alphanum(Type, Pos, String, [[C | _] = S], Row, Start, End) when ?IS_LATIN
_:_ -> {error, {unexpected_identifier, S, Row, Start, End}} _:_ -> {error, {unexpected_identifier, S, Row, Start, End}}
end; end;
parse_alphanum(Type, Pos, String, Path, Row, Start, End) -> parse_alphanum(Type, Pos, String, Path, Row, Start, End) ->
% Inversely, chain object prefixes are always lowercase, so any other path % Now having handled all lowercase terms, anything else must be uppercase,
% must be a variant constructor, or invalid. % which is either a variant constructor, or totally invalid.
parse_variant(Type, Pos, String, Path, Row, Start, End). parse_variant(Type, Pos, String, Path, Row, Start, End).
typecheck_integer({_, _, integer}, Pos, String, Value, _, _, _) -> typecheck_integer({_, _, integer}, Pos, String, Value, _, _, _) ->
@@ -728,6 +740,12 @@ parse_variant({O, N, {variant, Variants}}, Pos, String, [Namespace, Constructor]
_ -> _ ->
{error, {invalid_constructor, O, N, Namespace ++ "." ++ Constructor, Row, Start, End}} {error, {invalid_constructor, O, N, Namespace ++ "." ++ Constructor, Row, Start, End}}
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) -> parse_variant({_, _, unknown_type}, _, _, _, Row, Start, End) ->
{error, {unresolved_variant, Row, Start, End}}; {error, {unresolved_variant, Row, Start, End}};
parse_variant({O, N, _}, _, _, _, Row, Start, End) -> parse_variant({O, N, _}, _, _, _, Row, Start, End) ->
@@ -750,8 +768,7 @@ get_typename(Name) ->
parse_variant2(O, N, Variants, Pos, String, Prefix, Constructor, Row, Start, End) -> parse_variant2(O, N, Variants, Pos, String, Prefix, Constructor, Row, Start, End) ->
case lookup_variant(Constructor, Variants, 0) of case lookup_variant(Constructor, Variants, 0) of
{ok, {Tag, ElemTypes}} -> {ok, {Tag, ElemTypes}} ->
GetArity = fun({_, OtherElemTypes}) -> length(OtherElemTypes) end, Arities = get_arities(Variants),
Arities = lists:map(GetArity, Variants),
parse_variant3(Arities, Tag, ElemTypes, Pos, String); parse_variant3(Arities, Tag, ElemTypes, Pos, String);
error -> error ->
{error, {invalid_constructor, O, N, Prefix ++ Constructor, Row, Start, End}} {error, {invalid_constructor, O, N, Prefix ++ Constructor, Row, Start, End}}
@@ -787,6 +804,112 @@ lookup_variant(Ident, [{Ident, ElemTypes} | _], Tag) ->
lookup_variant(Ident, [_ | Rest], Tag) -> lookup_variant(Ident, [_ | Rest], Tag) ->
lookup_variant(Ident, Rest, Tag + 1). 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 %%% Record parsing
parse_record_or_map({_, _, {map, [KeyType, ValueType]}}, Pos, String, _, _) -> parse_record_or_map({_, _, {map, [KeyType, ValueType]}}, Pos, String, _, _) ->
@@ -961,7 +1084,7 @@ wrap_error(Reason, _) -> Reason.
%% integers, and strings, but it will misinterpret the types of records and %% integers, and strings, but it will misinterpret the types of records and
%% unicode characters, and will crash the process if variants are encountered. %% unicode characters, and will crash the process if variants are encountered.
%% %%
%% fate_to_list/2 should be used whenever possible, especially since %% `fate_to_list/2' should be used whenever possible, especially since
%% transaction results are type checked by nodes at runtime. %% transaction results are type checked by nodes at runtime.
fate_to_list(Term) -> fate_to_list(Term) ->
@@ -975,7 +1098,7 @@ fate_to_list(Term) ->
%% @doc %% @doc
%% Print a FATE term from gmbytecode in Sophia syntax %% Print a FATE term from gmbytecode in Sophia syntax
%% Like fate_to_list/1, but now type information from the AACI data structure %% Like `fate_to_list/1', but now type information from the AACI data structure
%% can be provided, in order to correctly interpret types like records, %% can be provided, in order to correctly interpret types like records,
%% variants, and unicode characters. If the type information you provide is %% variants, and unicode characters. If the type information you provide is
%% incorrect for the FATE term provided, then the function will fall back to %% incorrect for the FATE term provided, then the function will fall back to
@@ -988,7 +1111,7 @@ fate_to_list(Type, Term) ->
%% @doc %% @doc
%% Print a FATE term in Sophia syntax, without concatenating %% Print a FATE term in Sophia syntax, without concatenating
%% The fate_to_list/1 function builds an iolist, and then concatenates it into %% The `fate_to_list/1' function builds an iolist, and then concatenates it into
%% a list. If you are going to put the term into a bigger iolist directly %% a list. If you are going to put the term into a bigger iolist directly
%% after, or write it to a streaming device, then it can save effort and memory %% after, or write it to a streaming device, then it can save effort and memory
%% to just use the iolist directly. %% to just use the iolist directly.
@@ -1007,7 +1130,7 @@ fate_to_iolist(Term) ->
%% @doc %% @doc
%% Print a FATE term in Sophia syntax, without concatenating %% Print a FATE term in Sophia syntax, without concatenating
%% Prints using type information, like fate_to_list/2, but without spending %% Prints using type information, like `fate_to_list/2', but without spending
%% time or memory concatenating the result into a list, like fate_to_iolist/1. %% time or memory concatenating the result into a list, like fate_to_iolist/1.
% Special case for singleton records, since they are erased during compilation. % Special case for singleton records, since they are erased during compilation.
@@ -1024,15 +1147,12 @@ fate_to_iolist(Type, {tuple, Tuple}) ->
_ -> _ ->
tuple_to_iolist([], Tuple) tuple_to_iolist([], Tuple)
end; end;
fate_to_iolist(Type, {variant, _, Tag, Tuple}) -> fate_to_iolist(Type, {variant, Arities, Tag, Tuple}) ->
case Type of case Type of
{O, N, {variant, VariantTypes}} when Tag < length(VariantTypes) -> {O, N, {variant, VariantTypes}} when Tag < length(VariantTypes) ->
variant_to_iolist(O, N, VariantTypes, Tag, Tuple); variant_to_iolist(O, N, VariantTypes, Tag, Tuple);
{O, N, _} -> {_, _, _} ->
% TODO: Make up a special syntax for anonymous variant terms. anonymous_variant_to_iolist(Arities, Tag, Tuple)
erlang:exit({untyped_variant, O, N});
_ ->
erlang:exit({untyped_variant, unknown_type, already_normalized})
end; end;
fate_to_iolist(Type, List) when is_list(List) -> fate_to_iolist(Type, List) when is_list(List) ->
case Type of case Type of
@@ -1127,6 +1247,22 @@ choose_variant_prefix(O, N) ->
[] []
end. 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]) -> multivalue_to_iolist([FirstType | ElemTypes], [FirstTerm | Elems]) ->
FirstTermChars = fate_to_iolist(FirstType, FirstTerm), FirstTermChars = fate_to_iolist(FirstType, FirstTerm),
multivalue_to_iolist(ElemTypes, Elems, FirstTermChars); multivalue_to_iolist(ElemTypes, Elems, FirstTermChars);
@@ -1279,16 +1415,18 @@ check_parser_roundtrip(Sophia) ->
% syntax. Let's do a lenient test. % syntax. Let's do a lenient test.
roundtrip_parser_lenient(unknown_type(), Sophia, Fate). 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. % Compile the type definitions alongside the usual literal expression.
Source = "contract C =\n " ++ Typedef ++ "\n entrypoint f() = " ++ Sophia, Source = "contract C =\n " ++ Typedef ++ "\n entrypoint f() = " ++ Sophia,
{Fate, Type} = compile_entrypoint_value_and_type(Source, "f"), {Fate, Type} = compile_entrypoint_value_and_type(Source, "f"),
% Do a typed parse, as usual, but there are probably record/variant % Do a typed parse, as usual. Variant namespaces can make pretty printing
% definitions in the AACI, so untyped parses probably don't work, and % ambiguous, so make the roundtrip lenient.
% variants often have optional namespaces, so the sophia result might not roundtrip_parser_lenient(Type, Sophia, Fate),
% match exactly, but should still be equivalent. % Do an untyped parse, but using a second special Sophia expression that
roundtrip_parser_lenient(Type, Sophia, Fate). % 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() -> anon_types_test() ->
% Integers. % Integers.
@@ -1320,6 +1458,10 @@ anon_types_test() ->
check_parser_roundtrip("(1, [2, 3], (4, 5))"), check_parser_roundtrip("(1, [2, 3], (4, 5))"),
% Map. % Map.
check_parser_roundtrip("{[1] = 2, [3] = 4}"), 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. ok.
@@ -1339,7 +1481,7 @@ string_escape_codes_test() ->
records_test() -> records_test() ->
TypeDef = "record pair = {x: int, y: int}", TypeDef = "record pair = {x: int, y: int}",
Sophia = "{x = 1, y = 2}", 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 % The above won't run an untyped parse on the expression, but we can. It
% will error, though. % will error, though.
{error, {unresolved_record, _, _, _}} = parse_literal(unknown_type(), Sophia). {error, {unresolved_record, _, _, _}} = parse_literal(unknown_type(), Sophia).
@@ -1347,11 +1489,11 @@ records_test() ->
variant_test() -> variant_test() ->
TypeDef = "datatype multi('a) = Zero | One('a) | Two('a, 'a)", TypeDef = "datatype multi('a) = Zero | One('a) | Two('a, 'a)",
check_parser_with_typedef(TypeDef, "Zero"), check_parser_with_typedef(TypeDef, "Zero", "variant([0, 1, 2], 0)"),
check_parser_with_typedef(TypeDef, "One(0)"), check_parser_with_typedef(TypeDef, "One(0)", "variant([0, 1, 2], 1, 0)"),
check_parser_with_typedef(TypeDef, "Two(0, 1)"), check_parser_with_typedef(TypeDef, "Two(0, 1)", "variant([0, 1, 2], 2, 0, 1)"),
check_parser_with_typedef(TypeDef, "Two([], [1, 2, 3])"), check_parser_with_typedef(TypeDef, "Two([], [1, 2, 3])", "variant([0, 1, 2], 2, [], [1, 2, 3])"),
check_parser_with_typedef(TypeDef, "C.Zero"), check_parser_with_typedef(TypeDef, "C.Zero", "variant([0, 1, 2], 0)"),
{error, {unresolved_variant, _, _, _}} = parse_literal(unknown_type(), "Zero"), {error, {unresolved_variant, _, _, _}} = parse_literal(unknown_type(), "Zero"),
@@ -1359,10 +1501,10 @@ variant_test() ->
ambiguous_variant_test() -> ambiguous_variant_test() ->
TypeDef = "datatype mytype = C | D", TypeDef = "datatype mytype = C | D",
check_parser_with_typedef(TypeDef, "C"), check_parser_with_typedef(TypeDef, "C", "variant([0, 0], 0)"),
check_parser_with_typedef(TypeDef, "D"), check_parser_with_typedef(TypeDef, "D", "variant([0, 0], 1)"),
check_parser_with_typedef(TypeDef, "C.C"), check_parser_with_typedef(TypeDef, "C.C", "variant([0, 0], 0)"),
check_parser_with_typedef(TypeDef, "C.D"), check_parser_with_typedef(TypeDef, "C.D", "variant([0, 0], 1)"),
ok. ok.
@@ -1407,9 +1549,9 @@ bits_test() ->
singleton_records_test() -> singleton_records_test() ->
TypeDef = "record singleton('a) = {it: 'a}", TypeDef = "record singleton('a) = {it: 'a}",
check_parser_with_typedef(TypeDef, "{it = 123}"), check_parser_with_typedef(TypeDef, "{it = 123}", "123"),
check_parser_with_typedef(TypeDef, "{it = {it = {it = 5}}}"), check_parser_with_typedef(TypeDef, "{it = {it = {it = 5}}}", "5"),
check_parser_with_typedef(TypeDef, "[{it = 1}, {it = 2}, {it = 3}]"), check_parser_with_typedef(TypeDef, "[{it = 1}, {it = 2}, {it = 3}]", "[1, 2, 3]"),
ok. ok.
@@ -1418,9 +1560,9 @@ singleton_variants_test() ->
% actually a special case; singleton variants are in fact wrapped in the % actually a special case; singleton variants are in fact wrapped in the
% FATE too. % FATE too.
TypeDef = "datatype wrapped('a) = Wrap('a)", TypeDef = "datatype wrapped('a) = Wrap('a)",
check_parser_with_typedef(TypeDef, "Wrap(123)"), check_parser_with_typedef(TypeDef, "Wrap(123)", "variant([1], 0, 123)"),
check_parser_with_typedef(TypeDef, "Wrap(Wrap(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)]"), check_parser_with_typedef(TypeDef, "[Wrap(1), Wrap(2), Wrap(3)]", "[variant([1], 0, 1), variant([1], 0, 2), variant([1], 0, 3)]"),
ok. ok.