Compare commits

20 Commits

Author SHA1 Message Date
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
Jarvis Carroll 9fc89c0c22 Add type information for hz_sophia
This one was much simpler to do than hz_aaci, since it doesn't introduce any new types.

I could have made note of some of the conventions, where a type can be represented in multiple
ways in Sophia syntax, or where these functions are actually more lenient than the compiler, but
it isn't as easy to break those notes up from the basic function usage, like it was in hz_aaci,
where those aforementioned new types are used.
2026-05-19 12:29:30 +00:00
Jarvis Carroll 23c13f607e Document hz_aaci functions
Once the types were documented, the functions were easy to document. Just say "see erlang_expr/0 for details" over and over! ;p
2026-05-19 12:29:30 +00:00
Jarvis Carroll 8bc79d3b3f Fix dialyzer warnings for hz_aaci
A while ago I tried dialyzer and discovered that actually a lot of the AACI generation process
never fails, and whatever the one failure was that was possible, I think I decided was unnecessary,
and made that produce {ok, unknown_type} instead. I then set the types of these functions to be
{ok, Result} | {error, none()}, since there were in fact no errors, but dialyzer still spewed out
warnings for all the case blocks that redundantly check for these impossible error conditions.

Anyway now that is fixed! The behavior and external interface are all the same still, there are just fewer warnings
now.

Also added specs for a couple more internal functions, just because.
So noperhaps realized that actually none of it should fail. I never went in and
removed all the {ok, _} wrappers, but I did at least type the functions with the correct error list
2026-05-19 12:29:30 +00:00
Jarvis Carroll 3fae9a2edd Document hz_aaci types 2026-05-19 12:29:30 +00:00
zxq9 a3b19747b6 Adjust error condition 2026-05-18 12:56:31 +09:00
zxq9 f8e9333b4b Doc update 2026-05-14 11:01:37 +09:00
zxq9 eaccd50764 Fill in the holes in hz.erl docs and make hz_fetcher.erl 2026-05-13 19:48:49 +09:00
zxq9 9fd8dbd1a6 Merge branch 'master' into docs 2026-05-13 19:25:59 +09:00
Dimitar Ivanov f0f86ed36d Improve specs 2026-05-13 10:04:23 +03:00
Jarvis Carroll ed252b4c06 Also note index in record_element
I changed it from noting the index to just noting the field name, but
actually both pieces of information are important, since if there was
a type error, presumably the type information is actually wrong.

Now we put the index first, since that is the part of the FATE tuple
that failed, and then the field name that that would be if the type
information were correct, in case that is useful.
2026-05-12 06:07:58 +00:00
Jarvis Carroll 5dcc05d56a Change fate_to_erlang warning
This warning always confuses me. Usually it is a case I haven't actually implemented,
but I don't need the program to diagnose that for me, I need the program to tell me what the
type was, so that I can work out why it thinks it isn't implemented.

All three terms of the annotated type are relevant, but the annotated version can only differ from the normalized version if
it is a record or variant definition, so we special case those two just to communicate that the fact that it is *some* kind of record
did successfully pass through to the coerce logic, and otherwise we just try and print the opaque and normalized types faithfully.
2026-05-12 06:00:26 +00:00
Jarvis Carroll 2eca3a5338 Handle singleton records in erlang_to_fate
I realized this case needed special handling in hz_sophia, but didn't
get around to covering it properly in the older hz_aaci analogues.

While I was at it, I went and improved the error paths for record elements.
2026-05-12 04:23:21 +00:00
zxq9 e595991616 Correct keyblock() type 2026-05-10 21:02:23 +09:00
13 changed files with 685 additions and 113 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
@author Craig Everett <craigeverett@qpq.swiss> [https://git.qpq.swiss/QPQ-AG/hakuzaru]
@version 0.9.1
@version 0.9.2
@title Hakuzaru: Gajumaru blockchain bindings for Erlang
@doc
+1 -1
View File
@@ -3,7 +3,7 @@
{included_applications,[]},
{applications,[stdlib,kernel]},
{description,"Gajumaru interoperation library"},
{vsn,"0.9.1"},
{vsn,"0.9.2"},
{modules,[hakuzaru,hz,hz_aaci,hz_fetcher,hz_format,hz_grids,
hz_key_master,hz_man,hz_sophia,hz_sup]},
{mod,{hakuzaru,[]}}]}.
+1 -1
View File
@@ -6,7 +6,7 @@
%%% @end
-module(hakuzaru).
-vsn("0.9.1").
-vsn("0.9.2").
-author("Craig Everett <ceverett@tsuriai.jp>").
-copyright("Craig Everett <ceverett@tsuriai.jp>").
-license("GPL-3.0-or-later").
+81 -13
View File
@@ -23,7 +23,7 @@
%%% @end
-module(hz).
-vsn("0.9.1").
-vsn("0.9.2").
-author("Craig Everett <ceverett@tsuriai.jp>").
-copyright("Craig Everett <ceverett@tsuriai.jp>").
-license("GPL-3.0-or-later").
@@ -125,13 +125,14 @@
% "info" => contract_byte_array(),
% "miner" => account_id(),
% "nonce" => non_neg_integer(),
% "pow" => [non_neg_integer()],
% "prev_hash" => microblock_hash(),
% "prev_key_hash" => keyblock_hash(),
% "seal" => #{"data" => [int()],
% "signature" => signature()}
% "state_hash" => block_state_hash(),
% "target" => non_neg_integer(),
% "time" => non_neg_integer(),
% "version" => 5}.
% "version" => 1}.
% </pre>
-type microblock_header() :: #{string() => term()}.
% <pre>
@@ -353,7 +354,7 @@ top_height() ->
-spec top_block() -> {ok, TopBlock} | {error, Reason}
when TopBlock :: microblock_header(),
when TopBlock :: microblock_header() | keyblock(),
Reason :: chain_error().
%% @doc
%% Returns the header of the current top block.
@@ -661,9 +662,10 @@ dry_run(TX, Accounts, KBHash) ->
request("/v3/dry_run", JSON).
dry_run_map(Map) ->
JSON = zj:binary_encode(Map),
request("/v3/dry_run", JSON).
% TODO
%dry_run_map(Map) ->
% JSON = zj:binary_encode(Map),
% request("/v3/dry_run", JSON).
-spec decode_bytearray_fate(EncodedStr) -> {ok, Result} | {error, Reason}
@@ -788,9 +790,9 @@ contract_code(ID) ->
Result :: {ok, Source}
| {project, Bundle}
| {error, Reason},
Source :: string(),
Source :: binary(),
Bundle :: [{FilePath :: string(), Contents :: binary()}],
Reason :: chain_error() | string().
Reason :: chain_error() | string().
%% @doc
%% Retrieve the code of a contract as represented on chain.
@@ -809,10 +811,14 @@ extract(Blobby) ->
extract2(TarBaby) ->
case erl_tar:extract({binary, TarBaby}, [memory, compressed]) of
{ok, [{_, Source}]} ->
{ok, Source};
{ok, Bundle} ->
{project, Bundle};
{error,invalid_tar_checksum} ->
{ok, TarBaby};
Error ->
io:format("Dis chit happen: ~tp~n", [Error]),
ok = io:format("erl_tar:extract/2 error: ~tp~n", [Error]),
{ok, TarBaby}
end.
@@ -895,6 +901,12 @@ request(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) ->
hz_man:request(unicode:characters_to_list(Path), Payload).
@@ -930,7 +942,7 @@ contract_create(CreatorID, Path, InitArgs) ->
Gas = 500000,
GasPrice = min_gas_price(),
contract_create(CreatorID, Nonce,
Amount, TTL, Gas, GasPrice,
Gas, GasPrice, Amount, TTL,
Path, InitArgs);
Error ->
Error
@@ -1642,6 +1654,14 @@ convert([], [], _, Terms, []) ->
convert([], [], _, _, Errors) ->
{error, Errors}.
-spec sign_tx(Unsigned, SecKey) -> Result
when Unsigned :: string(),
SecKey :: binary(),
Result :: {ok, SignedTX} | {error, Reason},
SignedTX :: binary(),
Reason :: chain_error().
%% @doc
%% Signs transaction data with the provided secret key for the currently selected network.
sign_tx(Unsigned, SecKey) ->
case network_id() of
@@ -1649,6 +1669,15 @@ sign_tx(Unsigned, SecKey) ->
Error -> Error
end.
-spec sign_tx(Unsigned, SecKey, NetworkID) -> SignedTX
when Unsigned :: string(),
SecKey :: binary(),
NetworkID :: string(),
SignedTX :: binary().
%% @doc
%% Signs transaction data with the provided secret key using the provided network ID.
sign_tx(Unsigned, SecKey, MNetworkID) ->
UnsignedBin = unicode:characters_to_binary(Unsigned),
NetworkID = unicode:characters_to_binary(MNetworkID),
@@ -1668,10 +1697,21 @@ sign_tx(Unsigned, SecKey, MNetworkID) ->
gmser_api_encoder:encode(transaction, SignedTX).
spend(SenderID, SecKey, ReceipientID, Amount, Payload) ->
-spec spend(SenderID, SecKey, RecipientID, Amount, Payload) -> {ok, Result} | {error, Reason}
when SenderID :: string(),
SecKey :: binary(),
RecipientID :: string(),
Amount :: non_neg_integer(),
Payload :: binary(),
Result :: term(), % FIXME
Reason :: chain_error() | string().
%% @doc
%% Forms a spend transaction and submits it to the chain.
spend(SenderID, SecKey, RecipientID, Amount, Payload) ->
case status() of
{ok, #{"top_block_height" := Height, "network_id" := NetworkID}} ->
spend(SenderID, SecKey, ReceipientID, Amount, Payload, Height, NetworkID);
spend(SenderID, SecKey, RecipientID, Amount, Payload, Height, NetworkID);
Error ->
Error
end.
@@ -1698,6 +1738,22 @@ spend(SenderID, SecKey, RecipientID, Amount, Payload, Height, NetworkID) ->
end.
-spec spend(SenderID, SecKey, RecipientID, Amount,
GasPrice, Gas, TTL, Nonce, Payload, NetworkID) -> {ok, Result} | {error, Reason}
when SenderID :: string(),
SecKey :: binary(),
RecipientID :: string(),
Amount :: non_neg_integer(),
GasPrice :: pos_integer(),
Gas :: pos_integer(),
TTL :: non_neg_integer(),
Nonce :: non_neg_integer(),
Payload :: binary(),
Result :: term(), % FIXME
Reason :: chain_error() | string().
%% @doc
%% Forms a spend transaction and submits it to the chain.
spend(SenderID,
SecKey,
RecipientID,
@@ -1810,6 +1866,10 @@ spend3(DSenderID,
when Message :: binary(),
SecKey :: binary(),
Sig :: binary().
%% @doc
%% Accepts a string to be signed, prepends the prefix `"Gajumaru Signed Message:\n"',
%% encodes the string with `vencode/1', then hashes the encoded message and signs the
%% hash.
sign_message(Message, SecKey) ->
Prefix = message_sig_prefix(),
@@ -1888,6 +1948,12 @@ eu(N, Size) ->
when Binary :: binary(),
SecKey :: binary(),
Sig :: binary().
%% @doc
%% This procedure signs an arbitrary binary blob with a special binary prefix
%% attached. The reason for the binary prefix is to prevent signing of dangerous
%% binaries which could be used to authorized dangerous actions on chain.
%% The signature target becomes: `<<"Gajumaru Signed Binary:", Binary/binary>>'
%% before being hashed, and then the resulting hash is signed.
sign_binary(Binary, SecKey) ->
Prefix = binary_sig_prefix(),
@@ -1902,6 +1968,8 @@ sign_binary(Binary, SecKey) ->
PubKey :: pubkey(),
Result :: {ok, Outcome :: boolean()}
| {error, Reason :: term()}.
%% @doc
%% Verifies a signature created with the `sign_binary/2' function.
verify_bin_signature(Sig, Binary, PubKey) ->
case gmser_api_encoder:decode(PubKey) of
+465 -77
View File
@@ -10,7 +10,7 @@
%%% @end
-module(hz_aaci).
-vsn("0.9.1").
-vsn("0.9.2").
-author("Jarvis Carroll <spiveehere@gmail.com>").
-copyright("Craig Everett <ceverett@tsuriai.jp>").
-license("GPL-3.0-or-later").
@@ -29,14 +29,281 @@
-include_lib("eunit/include/eunit.hrl").
-type aaci() :: {aaci, string(), #{string() => function_spec()}, #{string() => typedef()}}.
-type function_spec() :: {[{string(), annotated_type()}], annotated_type()}.
-type typedef() :: {[string()], typedef_rhs()}.
%% @doc
%% The Sophia-flavored 'Erlang representation' of on-chain data.
%% Data is stored and manipulated on the chain without knowledge of Sophia
%% types, which leads to a specialized representation that is confusing to
%% manipulate directly. If you want to form contract arguments using an Erlang
%% program, or pattern match the outputs of a contract call using an Erlang
%% program, this Sophia-flavored representation is much more convenient. It
%% de-anonymizes variant types and record types, and is more lenient in how it
%% interprets a variety of cryptographic, binary, and string data types.
%%
%% When calling functions that manipulate this erlang representation, AACI type
%% information representing the Sophia type of that term must be provided. The
%% Sophia type used to produce that AACI type will determine what Erlang terms
%% are actually accepted without producing errors.
%%
-type erlang_repr() :: erlang_repr_int()
| erlang_repr_address()
| erlang_repr_contract()
| erlang_repr_signature()
| erlang_repr_bool()
| erlang_repr_string()
| erlang_repr_char()
| erlang_repr_bytes()
| erlang_repr_bits()
| erlang_repr_list()
| erlang_repr_map()
| erlang_repr_tuple()
| erlang_repr_variant()
| erlang_repr_record().
%-type erlang_repr() :: integer()
%| string()
%| boolean()
%| binary()
%| tuple() % Tuples, variants, or raw addresses
%| [erlang_repr()]
%| #{erlang_repr() => erlang_repr()}.
%% @doc
%% The Erlang representation of a Sophia `int`
%% Integers will be used as-is. Strings will be parsed using list_to_integer/1.
%% fate_to_erlang/2 always produces the integer representation.
-type erlang_repr_int() :: integer() | string().
%% @doc
%% The Erlang representation of a Sophia `address`
%% This can either be the "ak_..." string produced by gmserialization,
%% GajuDesk, etc. or a 'raw' binary of 32 bytes. fate_to_erlang/2 always
%% produces the "ak_..." string as an Erlang list. The Sophia-flavored Erlang
%% representation should not be used if this is undesirable.
-type erlang_repr_address() :: unicode:chardata() | {raw, <<_:32*8>>}.
%% @doc
%% The Erlang representation of a Sophia `contract`
%% This can either be the "ct_..." string produced by gmserialization,
%% GajuDesk, etc. or a 'raw' binary of 32 bytes. fate_to_erlang/2 always
%% produces the "ct_..." string as an Erlang list.
-type erlang_repr_contract() :: unicode:chardata() | {raw, <<_:32*8>>}.
%% @doc
%% The Erlang representation of a Sophia `signature`
%% This can either be the "sg_..." string produced by gmserialization,
%% GajuDesk, etc. or a 'raw' binary of 64 bytes. (Not 32.) Unlike addresses and
%% contracts, 'raw' binaries can be wrapped or unwrapped when representing a
%% signature. fate_to_erlang/2 always produces the "sg_..." string as an Erlang
%% list.
-type erlang_repr_signature() :: unicode:chardata() | <<_:64*8>> | {raw, <<_:64*8>>}.
%% @doc
%% The Erlang representation of a Sophia `bool`
%% fate_to_erlang/2 always produces atoms, but erlang_to_fate/2 also accepts
%% the lists "true" and "false".
-type erlang_repr_bool() :: true | false | string().
%% @doc
%% The Erlang representation of a Sophia `string`
%% The conversion uses unicode:characters_to_binary/1, so a list, a UTF8
%% binary, or an iolist mixing both are all acceptable inputs. fate_to_erlang/2
%% always produces a list.
-type erlang_repr_string() :: unicode:chardata().
%% @doc
%% The Erlang representation of a Sophia `char`
%% On-chain a `char` means one unicode code point, and is just a FATE integer.
%% fate_to_erlang/2 will provide this integer as-is, but erlang_to_fate/2 can
%% be passed an arbitrary unicode string, as long as it decodes to a single
%% unicode code point.
-type erlang_repr_char() :: integer() | unicode:chardata().
%% @doc
%% The Erlang representation of Sophia `bytes()`
%% Sophia has fixed-length `bytes(10)` etc. and variable length `bytes()`.
%% These are treated the same in the Erlang representation, but
%% erlang_to_fate/2 will check the length of the binary in the fixed length
%% case, and provide errors if it doesn't agree.
-type erlang_repr_bytes() :: binary().
%% @doc
%% The Erlang representation of Sophia `bits()`
%% FATE has a representation of bitstrings that one might call novel. A
%% FATE/Sophia bitstring is actually represented as an integer, so there is no
%% concept of bitstring 'length', all bitstrings have infinitely many leading
%% zeroes, if the integer is positive, and, surprisingly, infinitely many
%% leading ones, if the integer is negative! To represent this in the general
%% case, erlang_to_fate/2 accepts arbitrary integers, positive or negative, and
%% fate_to_erlang/2 always produces integers, but for convenience,
%% erlang_to_fate/2 also accepts arbitrary Erlang bitstrings, which are
%% converted into positive integers, i.e. '0 by default' FATE bitstrings.
-type erlang_repr_bits() :: bitstring().
%% @doc
%% The Erlang representation of a Sophia `list(_)`
%% Simply a list. Each element of the list is converted forwards/backwards as
%% normal.
-type erlang_repr_list() :: [erlang_repr()].
%% @doc
%% The Erlang representation of a Sophia `map(_, _)`
%% Simply a map. Each key and value is converted forwards/backwards as normal.
-type erlang_repr_map() :: #{erlang_repr() => erlang_repr()}.
%% @doc
%% The Erlang representation of a Sophia tuple
%% In Sophia these types are written `a * b`, `a * b * c`, and so on. Despite
%% the binary infix notation, a product of more than two types gives a single
%% tuple type with that many elements, so (1, 2, 3) is an int * int * int.
%% gmbytecode requires FATE tuples to be wrapped in {tuple, {X, Y}}, etc. but
%% the Erlang representation specifically requires that the tuple be provided
%% without any wrappers, so {X, Y}, etc. These representations cannot be mixed,
%% since at the highest level they are both just tuples. Each element of the
%% tuple is also converted forwards/backwards as normal. Although FATE has
%% singleton tuples, Sophia doesn't, so an ACI/AACI will never produce a
%% singleton tuple in an interface; if your contract takes singleton tuples,
%% these Sophia representations will probably still work, but you won't be able
%% to generate the AACI that makes them work, so it is likely simpler to just
%% use the FATE representation.
-type erlang_repr_tuple() :: {} | {erlang_repr(), erlang_repr()} | tuple().
%% @doc
%% The Erlang representation of a Sophia ADT
%% Sophia has a `datatype` keyword that allows the definition of algebraic data
%% types, also known as variants, tagged unions, sum types, coproduct types,
%% etc. In Erlang these are normally represented as an atom, or as a tuple
%% whose first term is an atom, so for familiarity, erlang_to_fate/2 accepts
%% lists in place of atoms, or tuples whose first term is a list. Note that
%% constructors in Sophia have to be capitalized, so actual atoms wouldn't be
%% that convenient. fate_to_erlang/2 always produces a tuple whose first term
%% is a list, even if that tuple is a singleton. This allows the user to
%% blindly call element(0) or tuple_to_list(_) without annoying special cases.
%%
%% Sophia also has a few built-in algebraic data types, for building its
%% standard library, and for exposing certain FATE primitives, which will
%% therefore also use this representation, e.g. "None", {"None"}, or
%% {"Some", Datum} for the `option(_)` type.
-type erlang_repr_variant() :: string() | {string()} | {string(), erlang_repr()} | tuple().
%% @doc
%% The Erlang representation of a Sophia record type
%% Sophia has a `record` keyword, that allows the definition of new record
%% types. Sophia records are meant to be reminiscent of Sophia maps, so in the
%% Erlang representation of Sophia records, we use a map, with strings as keys,
%% and arbitrary erlang_repr() terms as values.
-type erlang_repr_record() :: #{string() => erlang_repr()}.
%% @doc
%% The Accelerated Aeternity Contract Interface
%% Sophia tooling was originally written around a javascript use-case, but
%% hakuzaru is written for Erlang, so we don't really want to walk through big
%% JSON trees every time we do an on-chain action, so the AACI exists to
%% accelerate these actions, so that interacting with contract entrypoints from
%% within a pure Erlang environment is convenient and fast.
%%
%% The layout may change, but an AACI basically consists of three parts:
%% - The name of the contract,
%% - The 'annotated' entrypoint specs, designed for fast conversion to/from
%% the representation used on-chain, see function_spec/0,
%% - The 'opaque' type definitions, all the internal type aliases and
%% definitions within the contract and its imported namespaces.
-type aaci() :: {aaci, string(), #{string() => function_spec()}, #{string() => typedef()}}.
%% @doc
%% The fully annotated spec of a contract entrypoint, for fast call formation
%% The first term is a list of parameter names and their types, as expected by
%% erlang_args_to_fate/2, and the second term is a single type, as expected by
%% fate_to_erlang/2. See annotated_type/0 for the details of how these types
%% are represented and why, but for most purposes it is fine to just store and
%% pass these type terms around without looking at their contents.
-type function_spec() :: {[{string(), annotated_type()}], annotated_type()}.
%% @doc
%% A fully annotated Sophia type
%% Sophia allows for arbitrary nesting of type aliases, each with parameters,
%% and each potentially substituting for another arbitrarily complex type
%% alias, so there is a potentially indefinite amount of work converting the
%% type `my_type_alias` as it would appear in Sophia/in the ACI, into the
%% actual variant/record/list/map/tuple type expression that it ultimately
%% represents. To overcome this, we 'annotate' a type, recording what its
%% aliased name was, along with its actual definition.
%%
%% Normally you can extract the annotated types from a function_spec(), and
%% pass them into the conversion function that needs them, but it can also be
%% useful to walk through the annotated types yourself. Confusingly, if you
%% want to recursively descend down an annotated type, you want to recurse on
%% the third element in the tuple, not the first two, as the first two
%% represent incomplete levels of normalization, which can be more descriptive
%% for users, but aren't as actionable as the fully normalized third element.
%%
%% Despite the third term being the most important, it is kept at the end,
%% because that is what is most memorable, since each element of the triple is
%% more normalized than the last, and because that is what is easiest to read,
%% since the third term is usually an explosion of nested braces and brackets,
%% making anything written after it basically unreadable.
%%
%% If you look at examples of annotated types produced in your own programs,
%% you will tend to see things like {integer, alread_normalized, integer},
%% making it even less clear that the third element is the important one, or
%% why that is. For some fairly simple but informative examples, consider these
%% type aliases:
%% contract C =
%% record my_record('t) = {x: 't, y: 't}
%% type my_alias1 = int
%% type my_alias2 = list(my_alias1)
%% type my_alias3 = my_record(my_alias1)
%% If these type aliases appeared in a function spec, the AACI would represent
%% them as the following annotated types:
%% {"my_alias1", integer, integer}
%% {"my_alias2", {list, ["my_alias1"]}, {list, [{"my_alias1", integer, integer}]}}
%% {"my_alias3", {"my_record", ["my_alias1"]}, {record, [{"x", {"my_alias1", integer, integer}}, {"y", {"my_alias1", integer, integer}}]}}
%%
%% The first term is the type roughly as it appeared in the ACI, see
%% opaque_type/0 for more information.
%%
%% The second term is that same type but 'head normalized', chasing type
%% aliases iteratively, until it is some built in type like an integer, or some
%% user-defined record type or ADT. If the alias reduces to a list or map or
%% tuple with more aliased types nested inside, these nested type
%% subexpressions are not normalized any further, as the 'list' or 'map'
%% connective is considered the 'head' of the type expression, and is
%% normalized. Record type names and ADT names are not considered aliases, and
%% so are considered head normalized, but both can take parameters, which can
%% also stay un-normalized, as with lists or maps. If the head normalized type
%% is the same as the opaque type, then the atom 'already_normalized' is placed
%% instead, as a hint that instead of printing messages like
%% "my_alias1 (i.e. int)", a simple message like "list(my_record)" will do.
%%
%% The third term is the head normalized type with two changes, first, record
%% and variant definitions are subtituted in as well, giving a list of field
%% names or constructor names in full, and second, each subexpression is
%% recursively annotated, meaning its opaque, head-normalized, and fully
%% normalized parts also appear as triples.
-type annotated_type() :: {opaque_type(), already_normalized | opaque_type(), annotated_type_body()}.
%% @doc
%% The primitive connectives that complex type expressions can be built out of.
%% It takes a parameter, since builtin_type(opaque_type()),
%% builtin_type(annotated_type()), and builtin_type(typedef_expression()) are
%% all useful recursive applications of these connectives.
-type annotated_type() :: {opaque_type(), already_normalized | opaque_type(), builtin_type(annotated_type())}.
-type builtin_type(T) :: {bytes, [integer() | any]}
| {record, [{string(), T}]}
| {variant, [{string(), [T]}]}
| {tuple, [T]}
| {list, [T]}
| {map, [T]}
@@ -51,17 +318,55 @@
| channel
| unknown_type.
%% @doc
%% The connectives for defining new records and ADTs.
%% Record types and ADTs can both appear in the original type definitions in
%% the body of a contract, as well as in the recursively normalized 'annotated
%% types' that the AACI stores. We use the same layout in both cases.
-type user_defined_type(T) :: {record, [{string(), T}]} | {variant, [{string(), [T]}]}.
%% @doc
%% An opaque type as it originally appeared in a function spec.
%% The Sophia compiler may have a different representation for these type
%% expressions, but we make a simple representation here as well.
%% These type expressions are really function applications, in a limited sort
%% of rewrite calculus without higher order functions. After performing some
%% rewrites, the format actually stays the same, so the second term in a type
%% triple is also this 'opaque type', but that is a coincidence; this type is
%% primarily designed to represent types that haven't been head-normalized at
%% all % yet.
-type opaque_type() :: string() | {string(), [opaque_type()]} | builtin_type(opaque_type()).
-type typedef_rhs() :: {var, string()} | string() | {string(), [opaque_type()]} | builtin_type(typedef_rhs()).
%% @doc
%% The recursively annotated part of an annotated type triple
%% This can be any anonymous type connective, with annotated types inside, or
%% it can be a record definition, with annotated types for fields, or it can be
%% an ADT definition, with annotated types for each constructor input.
-type annotated_type_body() :: builtin_type(annotated_type()) | user_defined_type(annotated_type()).
-type erlang_repr() :: integer()
| string()
| boolean()
| binary()
| tuple() % Tuples, variants, or raw addresses
| [erlang_repr()]
| #{erlang_repr() => erlang_repr()}.
%% @doc
%% The recursive type expressions that can appear in the definitions of type aliases.
%% Similar to opaque_type(), but type aliases can take parameters as well,
%% which means those parameters can also appear anywhere within the recursive
%% type expression that defines the type alias.
-type typedef_expression() :: {var, string()}
| string()
| {string(), [typedef_expression()]}
| builtin_type(typedef_expression()).
%% @doc
%% A type definition as it appears in the AACI.
%% A type definition has a list of parameter names, and then some body defined
%% using builtin type connectives, other defined types, and those parameters.
-type typedef() :: {[string()], typedef_body()}.
%% @doc
%% The possible right-hand-sides of a type definition
%% A type definition means a type alias, a record definition, or an ADT
%% definition. Aliases are just some type expression, possibly with type
%% parameters, and records and variants are already defined above in
%% user_defined_type/1, with arbitrary type expressions in each one, but again,
%% they could contain type parameters as well.
-type typedef_body() :: typedef_expression() | user_defined_type(typedef_expression()).
%%% ACI/AACI
@@ -70,8 +375,10 @@
AACI :: aaci(),
Reason :: term().
%% @doc
%% Compile a contract and extract the function spec meta for use in future formation
%% of calldata
%% Compile a contract and extract the contract type information for forming contract calls
%% This is the simplest (but slowest) way of getting access to the AACI
%% structure for a contract. Having the AACI is not strictly necessary, but
%% makes it much more convenient to form contract calls and view their results.
prepare_from_file(Path) ->
case so_compiler:file(Path, [{aci, json}]) of
@@ -83,6 +390,10 @@ prepare_from_file(Path) ->
when ACI :: term(),
AACI :: aaci().
%% @doc
%% Convert the ACI structure produced by the compiler into the AACI format used by Hakuzaru
%% See the documentation for the aaci/0 type for more information.
prepare(ACI) ->
% We want to take the types represented by the ACI, things like N1.T(N2.T),
% and dereference them down to concrete types like
@@ -179,7 +490,7 @@ convert_typedefs_loop([Next | Rest], NamePrefix, Converted) ->
when Tree :: typedef_tree(),
TypeDefs :: #{string() => typedef()}.
-type typedef_tree() :: {string(), [string()], typedef_rhs()} | list(typedef_tree()).
-type typedef_tree() :: {string(), [string()], typedef_body()} | list(typedef_tree()).
collect_opaque_types([], Types) ->
Types;
@@ -194,7 +505,7 @@ collect_opaque_types({Name, Params, Def}, Types) ->
-spec opaque_type(Params, ACIType) -> Opaque
when Params :: [string()],
ACIType :: binary() | map(),
Opaque :: typedef_rhs().
Opaque :: opaque_type().
% Convert an ACI type defintion/spec into the 'opaque type' representation that
% our dereferencing algorithms can reason about.
@@ -326,6 +637,16 @@ builtin_typedefs() ->
% can simply render the normalized type expression and know that the error will
% make sense.
-spec annotate_function_specs(OpaqueSpecs, Types, Acc) -> Specs
when OpaqueSpecs :: [{string(), ArgsOpaque, ResultOpaque}],
ArgsOpaque :: [{string(), opaque_type()}],
ResultOpaque :: opaque_type(),
Types :: #{string() => typedef()},
Acc :: #{string() => {ArgsAnnotated, ResultAnnotated}},
Specs :: #{string() => {ArgsAnnotated, ResultAnnotated}},
ArgsAnnotated :: [{string(), annotated_type()}],
ResultAnnotated :: annotated_type().
annotate_function_specs([], _Types, Specs) ->
Specs;
annotate_function_specs([{Name, ArgsOpaque, ResultOpaque} | Rest], Types, Specs) ->
@@ -334,40 +655,29 @@ annotate_function_specs([{Name, ArgsOpaque, ResultOpaque} | Rest], Types, Specs)
NewSpecs = maps:put(Name, {Args, Result}, Specs),
annotate_function_specs(Rest, Types, NewSpecs).
-spec annotate_type(Opaque, Types) -> {ok, Annotated} | {error, Reason}
-spec annotate_type(Opaque, Types) -> {ok, Annotated}
when Opaque :: opaque_type(),
Types :: #{string() => typedef()},
Annotated :: annotated_type(),
Reason :: none().
Annotated :: annotated_type().
annotate_type(T, Types) ->
case normalize_opaque_type(T, Types) of
{ok, _, _, unknown_type} ->
{ok, {T, unknown_type, unknown_type}};
{ok, AlreadyNormalized, NOpaque, NExpanded} ->
annotate_type2(T, AlreadyNormalized, NOpaque, NExpanded, Types);
Error ->
Error
annotate_type2(T, AlreadyNormalized, NOpaque, NExpanded, Types)
end.
annotate_type2(T, _, _, unknown_type, _) ->
% If a type is unknown, then it should not be reported as the normalized
% name.
{ok, {T, unknown_type, unknown_type}};
annotate_type2(T, AlreadyNormalized, NOpaque, NExpanded, Types) ->
case annotate_type_subexpressions(NExpanded, Types) of
{ok, Flat} ->
case AlreadyNormalized of
true -> {ok, {T, already_normalized, Flat}};
false -> {ok, {T, NOpaque, Flat}}
end;
Error ->
Error
{ok, Flat} = annotate_type_subexpressions(NExpanded, Types),
case AlreadyNormalized of
true -> {ok, {T, already_normalized, Flat}};
false -> {ok, {T, NOpaque, Flat}}
end.
annotate_types([T | Rest], Types, Acc) ->
case annotate_type(T, Types) of
{ok, Type} -> annotate_types(Rest, Types, [Type | Acc]);
Error -> Error
end;
{ok, Type} = annotate_type(T, Types),
annotate_types(Rest, Types, [Type | Acc]);
annotate_types([], _Types, Acc) ->
{ok, lists:reverse(Acc)}.
@@ -378,34 +688,30 @@ annotate_type_subexpressions({bytes, [Count]}, _Types) ->
% opaque type.
{ok, {bytes, [Count]}};
annotate_type_subexpressions({variant, VariantsOpaque}, Types) ->
case annotate_variants(VariantsOpaque, Types, []) of
{ok, Variants} -> {ok, {variant, Variants}};
Error -> Error
end;
{ok, Variants} = annotate_variants(VariantsOpaque, Types, []),
{ok, {variant, Variants}};
annotate_type_subexpressions({record, FieldsOpaque}, Types) ->
case annotate_bindings(FieldsOpaque, Types, []) of
{ok, Fields} -> {ok, {record, Fields}};
Error -> Error
end;
{ok, Fields} = annotate_bindings(FieldsOpaque, Types, []),
{ok, {record, Fields}};
annotate_type_subexpressions({T, ElemsOpaque}, Types) ->
case annotate_types(ElemsOpaque, Types, []) of
{ok, Elems} -> {ok, {T, Elems}};
Error -> Error
end.
{ok, Elems} = annotate_types(ElemsOpaque, Types, []),
{ok, {T, Elems}}.
-spec annotate_bindings(Bindings, Types, Acc) -> {ok, Annotated}
when Bindings :: [{string(), opaque_type()}],
Types :: #{string() => typedef()},
Acc :: [{string(), annotated_type()}],
Annotated :: [{string(), annotated_type()}].
annotate_bindings([{Name, T} | Rest], Types, Acc) ->
case annotate_type(T, Types) of
{ok, Type} -> annotate_bindings(Rest, Types, [{Name, Type} | Acc]);
Error -> Error
end;
{ok, Next} = annotate_type(T, Types),
annotate_bindings(Rest, Types, [{Name, Next} | Acc]);
annotate_bindings([], _Types, Acc) ->
{ok, lists:reverse(Acc)}.
annotate_variants([{Name, Elems} | Rest], Types, Acc) ->
case annotate_types(Elems, Types, []) of
{ok, ElemsFlat} -> annotate_variants(Rest, Types, [{Name, ElemsFlat} | Acc]);
Error -> Error
end;
{ok, ElemsFlat} = annotate_types(Elems, Types, []),
annotate_variants(Rest, Types, [{Name, ElemsFlat} | Acc]);
annotate_variants([], _Types, Acc) ->
{ok, lists:reverse(Acc)}.
@@ -513,6 +819,16 @@ substitute_opaque_types(Bindings, Types) ->
Reason :: term(),
PathStep :: term().
%% @doc
%% Call erlang_to_fate/2 on a list of named values.
%% See the documentation for the erlang_repr/0 type for more information on the
%% format required.
%% This is mainly used by hz.erl to form contract calls. The parameter names
%% and parameter types are provided in one zipped list, exactly as they appear
%% in the AACI datatype, and then a second list of concrete arguments are
%% provided in the format that erlang_to_fate/2 expects. The parameter names
%% are used to provide slightly more informative errors.
erlang_args_to_fate(VarTypes, Terms) ->
DefLength = length(VarTypes),
ArgLength = length(Terms),
@@ -530,6 +846,15 @@ erlang_args_to_fate(VarTypes, Terms) ->
Reason :: term(),
PathStep :: term().
%% @doc
%% Convert one Sophia-flavored Erlang term into one FATE-flavored Erlang terms.
%% This is not usually used on its own, since if you need to form a contract
%% call, you have a list of arguments, not a single argument. Nonetheless, if
%% for some reason you want to use a mix of FATE-flavored Erlang terms and
%% Sophia-flavored Erlang terms in one function call, it may be useful to
%% convert the Sophia-flavored terms individually, to form a single
%% FATE-flavored list for call formation.
erlang_to_fate({_, _, integer}, S) when is_integer(S) ->
{ok, S};
erlang_to_fate({O, N, integer}, S) when is_list(S) ->
@@ -796,6 +1121,10 @@ 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
{ok, [SingleElem]} ->
% Singleton records aren't implemented as FATE tuples at
% all.
{ok, SingleElem};
{ok, Converted} ->
{ok, {tuple, list_to_tuple(Converted)}};
Errors ->
@@ -821,10 +1150,18 @@ coerce_record_to_map(O, N, MemberTypes, Tuple) ->
single_error({record_too_few_terms, O, N, Tuple});
{error, too_many_terms} ->
single_error({record_too_many_terms, O, N, Tuple});
Errors ->
Errors
{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 = [_|_]}} ->
@@ -878,6 +1215,14 @@ coerce_direction(Type, Term, from_fate) ->
Reason :: term(),
PathStep :: term().
%% @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
%% chain, if you ask for the 'erlang' format, but you can call this function
%% manually if you have a result in the 'fate' format, and need the 'erlang'
%% format now. See the documentation of the erlang_repr/0 type for more
%% information.
fate_to_erlang({_, _, integer}, S) when is_integer(S) ->
{ok, S};
fate_to_erlang({_, _, address}, {address, Bin}) ->
@@ -915,6 +1260,11 @@ fate_to_erlang({O, N, {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) ->
% 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);
fate_to_erlang({O, N, {unknown_type, _}}, Data) ->
@@ -927,15 +1277,30 @@ fate_to_erlang({O, N, {unknown_type, _}}, Data) ->
io:format(Message, [O, N, Data])
end,
{ok, Data};
fate_to_erlang({O, N, _}, Data) ->
case N of
already_normalized ->
io:format("Warning: Unimplemented type ~p.~nUsing term as is:~n~p~n", [O, Data]);
_ ->
io:format("Warning: Unimplemented type ~p (i.e. ~p).~nUsing term as is:~n~p~n", [O, N, Data])
end,
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}.
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)]).
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]).
%%% AACI Getters
@@ -947,9 +1312,12 @@ fate_to_erlang({O, N, _}, Data) ->
Reason :: bad_fun_name.
%% @doc
%% Look up the type information of a given function, in the AACI provided by
%% prepare_contract/1. This type information, particularly the return type, is
%% useful for calling decode_bytearray/2.
%% Extract the type information for a particular function from the AACI
%% If you want to manually convert a FATE result into the Sophia-flavored
%% Erlang representation, or manually convert some or all of the inputs for a
%% contract call yourself, this function gives you all of the annotated types
%% associated with a contract entrypoint. For more information, see the
%% documentation for the annotated_type/0 type.
get_function_signature({aaci, _, FunDefs, _}, Fun) ->
case maps:find(Fun, FunDefs) of
@@ -1050,9 +1418,9 @@ coerce_tuple_test() ->
check_roundtrip(Type, {123, "456"}, {tuple, {123, <<"456">>}}).
coerce_variant_test() ->
{ok, Type} = annotate_type({variant, [{"A", [integer]},
{"B", [integer, integer]}]},
#{}),
Definition = {variant, [{"A", [integer]},
{"B", [integer, integer]}]},
{ok, Type} = annotate_type("t", #{"t" => {[], Definition}}),
check_roundtrip(Type, {"A", 123}, {variant, [1, 2], 0, {123}}),
check_roundtrip(Type, {"B", 456, 789}, {variant, [1, 2], 1, {456, 789}}).
@@ -1062,7 +1430,8 @@ coerce_option_test() ->
check_roundtrip(Type, {"Some", 1}, {variant, [0, 1], 1, {1}}).
coerce_record_test() ->
{ok, Type} = annotate_type({record, [{"a", integer}, {"b", integer}]}, #{}),
Definition = {record, [{"a", integer}, {"b", integer}]},
{ok, Type} = annotate_type("t", #{"t" => {[], Definition}}),
check_roundtrip(Type, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}).
coerce_bytes_test() ->
@@ -1120,6 +1489,25 @@ record_substitution_test() ->
{ok, {[], Output}} = get_function_signature(AACI, "f"),
check_roundtrip(Output, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}).
singleton_record_substitution_test() ->
Contract = "
contract C =
record single('t) = { it: 't }
entrypoint f(): single(int) = { it = 1 }
entrypoint g(): single(single(int)) = { it = { it = 2 } }
entrypoint h(): single(int * int) = { it = (3, 4) }
",
{ok, AACI} = aaci_from_string(Contract),
{ok, {[], FOutput}} = get_function_signature(AACI, "f"),
check_roundtrip(FOutput, #{"it" => 123}, 123),
{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}}).
tuple_substitution_test() ->
Contract = "
contract C =
+9 -1
View File
@@ -1,5 +1,13 @@
%%% @private
%%% Hakuzaru Request Fetcher
%%%
%%% This module defines the request workers.
%%% Each request to a remote chain node is handled by a worker that is spawned
%%% to handle it and terminates on completion.
%%% @end
-module(hz_fetcher).
-vsn("0.9.1").
-vsn("0.9.2").
-author("Craig Everett <ceverett@tsuriai.jp>").
-copyright("Craig Everett <ceverett@tsuriai.jp>").
-license("MIT").
+18 -1
View File
@@ -21,7 +21,7 @@
%%% @end
-module(hz_format).
-vsn("0.9.1").
-vsn("0.9.2").
-author("Craig Everett <ceverett@tsuriai.jp>").
-copyright("Craig Everett <ceverett@tsuriai.jp>").
-license("GPL-3.0-or-later").
@@ -462,9 +462,26 @@ ranks(heresy) ->
["k ", "m ", "b ", "t ", "q ", "e ", "z ", "y ", "r ", "Q "].
-spec mark(Unit) -> Mark
when Unit :: gaju | puck,
Mark :: $木 | $本.
%% @doc
%% Retrieve the unicode codepoint for the `gaju' mark (木) or the `puck' mark (本).
mark(gaju) -> $木;
mark(puck) -> $本.
-spec one(Unit) -> Pucks
when Unit :: gaju | puck,
Pucks :: 1_000_000_000_000_000_000 | 1.
%% @doc
%% Quickly resolve the number of pucks in a given unit.
%%
%% The number of pucks in a gaju is so large that it can be a little bit annoying
%% to remember the exact amount. This is a helper to simplify this when writing
%% an app against the hakuzaru library when dealing in either unit.
one(gaju) -> 1_000_000_000_000_000_000;
one(puck) -> 1.
+34 -2
View File
@@ -37,7 +37,7 @@
%%% @end
-module(hz_grids).
-vsn("0.9.1").
-vsn("0.9.2").
-export([url/2, url/3, url/4, parse/1, req/2, req/3, req/4]).
@@ -47,7 +47,7 @@
Result :: {ok, GRIDS} | uri_string:uri_error(),
GRIDS :: uri_string:uri_string().
%% @doc
%% Takes
%% Takes an instruction and an HTTP endpoint location and forms a GRIDS URL.
url(Instruction, HTTP) ->
case uri_string:parse(HTTP) of
@@ -134,6 +134,8 @@ qwargs(Amount, Payload) ->
Amount :: non_neg_integer(),
Payload :: binary(),
URL :: string().
%% @doc
%% Translate a GRIDS URL into an Erlang terms instruction.
parse(GRIDS) ->
case uri_string:parse(GRIDS) of
@@ -190,13 +192,43 @@ l_to_i(S) ->
end.
-spec req(Type, Message) -> Format
when Type :: sign | tx | ack,
Message :: string() | binary(),
Format :: map().
%% @doc
%% @equiv req(Type, Message, false)
req(Type, Message) ->
req(Type, Message, false).
-spec req(Type, Message, ID) -> Format
when Type :: sign | tx | ack,
Message :: string() | binary(),
ID :: false | string() | binary(),
Format :: map().
%% @doc
%% Creates a GRIDS message format with the current `NetworkID'.
%%
%% The `ID' parameter indicates which key the requestee should sign with or
%% is `false' to indicate that which key to sign with is up to the requestee.
%% @equiv req(Type, Message, ID, CurrentNetworkID)
req(Type, Message, ID) ->
{ok, NetworkID} = hz:network_id(),
req(Type, Message, ID, NetworkID).
-spec req(Type, Message, ID, NetworkID) -> Format
when Type :: sign | tx | ack,
Message :: string() | binary(),
ID :: false | string() | binary(),
NetworkID :: string() | binary(),
Format :: map().
%% @doc
%% Creates a GRIDS message format.
req(sign, Message, ID, NetworkID) ->
#{"grids" => 1,
"chain" => "gajumaru",
+1 -1
View File
@@ -8,7 +8,7 @@
%%% @end
-module(hz_key_master).
-vsn("0.9.1").
-vsn("0.9.2").
-export([make_key/1, encode/1, decode/1]).
-export([lcg/1]).
+1 -1
View File
@@ -9,7 +9,7 @@
%%% @end
-module(hz_man).
-vsn("0.9.1").
-vsn("0.9.2").
-behavior(gen_server).
-author("Craig Everett <ceverett@tsuriai.jp>").
-copyright("Craig Everett <ceverett@tsuriai.jp>").
+70 -11
View File
@@ -1,29 +1,31 @@
-module(hz_sophia).
-vsn("0.9.1").
-vsn("0.9.2").
-author("Jarvis Carroll <spiveehere@gmail.com>").
-copyright("Jarvis Carroll <spiveehere@gmail.com>").
-license("GPL-3.0-or-later").
-export([parse_literal/1, parse_literal/2]).
-export([parse_literal/2, parse_literal/1]).
-export([fate_to_list/1, fate_to_list/2, fate_to_iolist/1, fate_to_iolist/2]).
-include_lib("eunit/include/eunit.hrl").
-spec parse_literal(Sophia) -> {ok, FATE} | {error, Reason}
when Sophia :: string(),
FATE :: gmb_fate_data:fate_type(),
Reason :: term().
parse_literal(String) ->
parse_literal(unknown_type(), String).
-spec parse_literal(Type, Sophia) -> {ok, FATE} | {error, Reason}
when Type :: hz_aaci:annotated_type(),
Sophia :: string(),
FATE :: gmb_fate_data:fate_type(),
Reason :: term().
%% @doc
%% Parse a typed Sophia expression into a FATE term
%% The Sophia expression must consist only of literals, thus making a 'Sophia
%% term', which means no arithmetic, no function calls, no variables, etc.
%% The FATE term is in the format that gmbytecode expects as input, for forming
%% contract calls, etc. Used by the hz module to implement the 'sophia' format.
%%
%% The function takes type information retrieved from the AACI data structure,
%% which is used to interpret record types and variant types, but is also used
%% to check inputs and generate errors.
parse_literal(Type, String) ->
case parse_expression(Type, {1, 1}, String) of
{ok, {Result, NewPos, NewString}} ->
@@ -43,6 +45,28 @@ parse_literal2(Result, Pos, String) ->
{error, Reason}
end.
-spec parse_literal(Sophia) -> {ok, FATE} | {error, Reason}
when Sophia :: string(),
FATE :: gmb_fate_data:fate_type(),
Reason :: term().
%% @doc
%% Parse an untyped Sophia expression into a FATE term
%% Like parse_literal/2, but will not produce type errors. This function can
%% still produce parsing errors, and can produce errors when variants or
%% records are encountered, since they can't be parsed unless you have type
%% information.
%%
%% Note that since records are implemented as tuples, if you are trying to call
%a function that you know takes a record, but you don't have type information
%% available in the context where the expression is being passed, then tuples
%% can be used instead. This does not work if you have type information,
%% though, as tuples and records are different Sophia/AACI types.
parse_literal(String) ->
parse_literal(unknown_type(), String).
%%% Tokenizer
-define(IS_LATIN_UPPER(C), (((C) >= $A) and ((C) =< $Z))).
@@ -927,6 +951,19 @@ wrap_error(Reason, _) -> Reason.
when FATE :: gmb_fate_data:fate_type(),
Sophia :: string().
%% @doc
%% Print a FATE term from gmbytecode in Sophia syntax
%% FATE terms usually come from using gmbytecode to decode the result of an
%% on-chain transaction.
%%
%% This function does not use any type information to interpret the data, and
%% so can make mistakes. It's okay for interpreting tuples, lists, maps,
%% integers, and strings, but it will misinterpret the types of records and
%% unicode characters, and will crash the process if variants are encountered.
%%
%% fate_to_list/2 should be used whenever possible, especially since
%% transaction results are type checked by nodes at runtime.
fate_to_list(Term) ->
fate_to_list(unknown_type(), Term).
@@ -935,10 +972,27 @@ fate_to_list(Term) ->
FATE :: gmb_fate_data:fate_type(),
Sophia :: string().
%% @doc
%% Print a FATE term from gmbytecode in Sophia syntax
%% 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,
%% variants, and unicode characters. If the type information you provide is
%% incorrect for the FATE term provided, then the function will fall back to
%% untyped pretty printing like in fate_to_list/1, but this is not recommended,
%% as correct type information should always be available.
fate_to_list(Type, Term) ->
IOList = fate_to_iolist(Type, Term),
unicode:characters_to_list(IOList).
%% @doc
%% Print a FATE term in Sophia syntax, without concatenating
%% 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
%% after, or write it to a streaming device, then it can save effort and memory
%% to just use the iolist directly.
-spec fate_to_iolist(FATE) -> Sophia
when FATE :: gmb_fate_data:fate_type(),
Sophia :: iolist().
@@ -951,6 +1005,11 @@ fate_to_iolist(Term) ->
FATE :: gmb_fate_data:fate_type(),
Sophia :: iolist().
%% @doc
%% Print a FATE term in Sophia syntax, without concatenating
%% 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.
% Special case for singleton records, since they are erased during compilation.
fate_to_iolist({_, _, {record, [{FieldName, FieldType}]}}, Term) ->
singleton_record_to_iolist(FieldName, FieldType, Term);
+1 -1
View File
@@ -9,7 +9,7 @@
%%% @end
-module(hz_sup).
-vsn("0.9.1").
-vsn("0.9.2").
-behaviour(supervisor).
-author("Craig Everett <zxq9@zxq9.com>").
-copyright("Craig Everett <zxq9@zxq9.com>").
+2 -2
View File
@@ -1,10 +1,10 @@
{name,"Hakuzaru"}.
{type,app}.
{modules,[]}.
{author,"Craig Everett"}.
{prefix,"hz"}.
{author,"Craig Everett"}.
{desc,"Gajumaru interoperation library"}.
{package_id,{"otpr","hakuzaru",{0,9,1}}}.
{package_id,{"otpr","hakuzaru",{0,9,2}}}.
{deps,[{"otpr","sophia",{9,0,0}},
{"otpr","gmserialization",{0,1,3}},
{"otpr","gmbytecode",{3,4,1}},