6daad4974c
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.
1629 lines
70 KiB
Erlang
1629 lines
70 KiB
Erlang
%%% @doc
|
|
%%% Sophia datatype manipulations for Hakuzaru
|
|
%%%
|
|
%%% Sophia and FATE are two subtly different machine languages with two
|
|
%%% different type systems. Application developers probably think about their
|
|
%%% smart contracts in terms of the Sophia types, but the node will only accept
|
|
%%% the corresponding FATE types, and both of these type systems result in
|
|
%%% different erlang terms for representing the same thing. This module defines
|
|
%%% the conversion between these different representations of the same data.
|
|
%%% @end
|
|
|
|
-module(hz_aaci).
|
|
-vsn("0.9.2").
|
|
-author("Jarvis Carroll <spiveehere@gmail.com>").
|
|
-copyright("Craig Everett <ceverett@tsuriai.jp>").
|
|
-license("GPL-3.0-or-later").
|
|
|
|
% Contract call and serialization interface functions
|
|
-export([prepare_from_file/1,
|
|
prepare/1,
|
|
erlang_to_fate/2,
|
|
fate_to_erlang/2,
|
|
erlang_args_to_fate/2,
|
|
get_function_signature/2]).
|
|
|
|
%%% Types
|
|
|
|
-export_type([aaci/0, annotated_type/0, erlang_repr/0]).
|
|
|
|
-include_lib("eunit/include/eunit.hrl").
|
|
|
|
-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().
|
|
% 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() :: integer()
|
|
%| string()
|
|
%| boolean()
|
|
%| binary()
|
|
%| tuple() % Tuples, variants, or raw addresses
|
|
%| [erlang_repr()]
|
|
%| #{erlang_repr() => erlang_repr()}.
|
|
|
|
|
|
-type erlang_repr_int() :: integer() | string().
|
|
% 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_address() :: unicode:chardata() | {raw, <<_:32*8>>}.
|
|
% 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_contract() :: unicode:chardata() | {raw, <<_:32*8>>}.
|
|
% 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_signature() :: unicode:chardata() | <<_:64*8>> | {raw, <<_:64*8>>}.
|
|
% 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_bool() :: true | false | string().
|
|
% 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_string() :: unicode:chardata().
|
|
% 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_char() :: integer() | unicode:chardata().
|
|
% 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_bytes() :: binary().
|
|
% 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_bits() :: bitstring().
|
|
% 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_list() :: [erlang_repr()].
|
|
% The Erlang representation of a Sophia `list(_)'
|
|
% Simply a list. Each element of the list is converted forwards/backwards as
|
|
% normal.
|
|
|
|
|
|
-type erlang_repr_map() :: #{erlang_repr() => erlang_repr()}.
|
|
% The Erlang representation of a Sophia `map(_, _)'
|
|
% Simply a map. Each key and value is converted forwards/backwards as normal.
|
|
|
|
|
|
-type erlang_repr_tuple() :: {}
|
|
| {erlang_repr(), erlang_repr()}
|
|
| tuple().
|
|
% 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_variant() :: string()
|
|
| {string()}
|
|
| {string(), erlang_repr()}
|
|
| tuple().
|
|
% 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_record() :: #{string() => erlang_repr()}.
|
|
% 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 aaci() :: {aaci, string(), #{string() => function_spec()}, #{string() => typedef()}}.
|
|
% 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:
|
|
% <ul>
|
|
% <li>The name of the contract,</li>
|
|
% <li>The 'annotated' entrypoint specs, designed for fast conversion to/from
|
|
% the representation used on-chain, see `function_spec/0',</li>
|
|
% <li>The 'opaque' type definitions, all the internal type aliases and
|
|
% definitions within the contract and its imported namespaces.</li>
|
|
% </ul>
|
|
|
|
|
|
-type function_spec() :: {[{string(), annotated_type()}], annotated_type()}.
|
|
% 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 annotated_type() :: {opaque_type(), already_normalized | opaque_type(), annotated_type_body()}.
|
|
% 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:
|
|
% <pre>
|
|
% 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)
|
|
% </pre>
|
|
% If these type aliases appeared in a function spec, the AACI would represent
|
|
% them as the following annotated types:
|
|
% <pre>
|
|
% {"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}}]}}
|
|
% </pre>
|
|
%
|
|
% 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 builtin_type(T) :: {bytes, [integer() | any]}
|
|
| {tuple, [T]}
|
|
| {list, [T]}
|
|
| {map, [T]}
|
|
| integer
|
|
| boolean
|
|
| bits
|
|
| char
|
|
| string
|
|
| address
|
|
| signature
|
|
| contract
|
|
| channel
|
|
| unknown_type.
|
|
% 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 user_defined_type(T) :: {record, [{string(), T}]}
|
|
| {variant, [{string(), [T]}]}.
|
|
% 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 opaque_type() :: string()
|
|
| {string(), [opaque_type()]}
|
|
| builtin_type(opaque_type()).
|
|
% 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 annotated_type_body() :: builtin_type(annotated_type())
|
|
| user_defined_type(annotated_type()).
|
|
% 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 typedef_expression() :: {var, string()}
|
|
| string()
|
|
| {string(), [typedef_expression()]}
|
|
| builtin_type(typedef_expression()).
|
|
% 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() :: {[string()], typedef_body()}.
|
|
% 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_body() :: typedef_expression()
|
|
| user_defined_type(typedef_expression()).
|
|
% 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.
|
|
|
|
|
|
|
|
%%% ACI/AACI
|
|
|
|
-spec prepare_from_file(Path) -> {ok, AACI} | {error, Reason}
|
|
when Path :: file:filename(),
|
|
AACI :: aaci(),
|
|
Reason :: term().
|
|
%% @doc
|
|
%% 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
|
|
{ok, #{aci := ACI}} -> {ok, prepare(ACI)};
|
|
Error -> Error
|
|
end.
|
|
|
|
-spec prepare(ACI) -> AACI
|
|
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
|
|
% {tuple, [integer, string]}. Our type dereferencing algorithms
|
|
% shouldn't act directly on the JSON-based structures that the compiler
|
|
% gives us, though, though, so before we do the analysis, we should strip
|
|
% the ACI down to a list of 'opaque' type defintions and function specs.
|
|
{Name, OpaqueSpecs, TypeDefs} = convert_aci_types(ACI),
|
|
|
|
% Now that we have the opaque types, we can dereference the function specs
|
|
% down to the concrete types they actually represent. We annotate each
|
|
% subexpression of this concrete type with other info too, in case it helps
|
|
% make error messages easier to understand.
|
|
InternalTypeDefs = maps:merge(builtin_typedefs(), TypeDefs),
|
|
Specs = annotate_function_specs(OpaqueSpecs, InternalTypeDefs, #{}),
|
|
{aaci, Name, Specs, TypeDefs}.
|
|
|
|
|
|
-spec convert_aci_types(ACI) -> {Name, OpaqueSpecs, TypeDefs}
|
|
when ACI :: term(),
|
|
Name :: string(),
|
|
OpaqueSpecs :: [{string(), [{string(), opaque_type()}], opaque_type()}],
|
|
TypeDefs :: #{string() => typedef()}.
|
|
|
|
convert_aci_types(ACI) ->
|
|
% Find the main contract, so we can get the specifications of its
|
|
% entrypoints.
|
|
[{NameBin, SpecDefs}] =
|
|
[{N, F}
|
|
|| #{contract := #{kind := contract_main,
|
|
functions := F,
|
|
name := N}} <- ACI],
|
|
Name = binary_to_list(NameBin),
|
|
% Turn these specifications into opaque types that we can reason about.
|
|
Specs = lists:map(fun convert_function_spec/1, SpecDefs),
|
|
|
|
% These specifications can reference other type definitions from the main
|
|
% contract and any other namespaces, so extract these types and convert
|
|
% them too.
|
|
TypeDefTree = lists:map(fun convert_namespace_typedefs/1, ACI),
|
|
% The tree structure of the ACI naturally leads to a tree of opaque types,
|
|
% but we want a map, so flatten it out before we continue.
|
|
TypeDefMap = collect_opaque_types(TypeDefTree, #{}),
|
|
|
|
% This is all the information we actually need from the ACI, the rest is
|
|
% just pre-compute and acceleration.
|
|
{Name, Specs, TypeDefMap}.
|
|
|
|
|
|
convert_function_spec(#{name := NameBin, arguments := Args, returns := Result}) ->
|
|
Name = binary_to_list(NameBin),
|
|
ArgTypes = lists:map(fun convert_arg/1, Args),
|
|
ResultType = opaque_type([], Result),
|
|
{Name, ArgTypes, ResultType}.
|
|
|
|
|
|
convert_arg(#{name := NameBin, type := TypeDef}) ->
|
|
Name = binary_to_list(NameBin),
|
|
Type = opaque_type([], TypeDef),
|
|
{Name, Type}.
|
|
|
|
|
|
convert_namespace_typedefs(#{namespace := NS}) ->
|
|
Name = namespace_name(NS),
|
|
convert_typedefs(NS, Name);
|
|
convert_namespace_typedefs(#{contract := NS}) ->
|
|
Name = namespace_name(NS),
|
|
ImplicitTypes = convert_implicit_types(NS, Name),
|
|
ExplicitTypes = convert_typedefs(NS, Name),
|
|
[ImplicitTypes, ExplicitTypes].
|
|
|
|
namespace_name(#{name := NameBin}) ->
|
|
binary_to_list(NameBin).
|
|
|
|
convert_implicit_types(#{state := StateDefACI}, Name) ->
|
|
StateDefOpaque = opaque_type([], StateDefACI),
|
|
[{Name, [], contract},
|
|
{Name ++ ".state", [], StateDefOpaque}];
|
|
convert_implicit_types(_, Name) ->
|
|
[{Name, [], contract}].
|
|
|
|
convert_typedefs(#{typedefs := TypeDefs}, Name) ->
|
|
convert_typedefs_loop(TypeDefs, Name ++ ".", []).
|
|
|
|
% Take a namespace that has already had a period appended, and use that as a
|
|
% prefix to convert and annotate a list of types.
|
|
convert_typedefs_loop([], _NamePrefix, Converted) ->
|
|
Converted;
|
|
convert_typedefs_loop([Next | Rest], NamePrefix, Converted) ->
|
|
#{name := NameBin, vars := ParamDefs, typedef := DefACI} = Next,
|
|
Name = NamePrefix ++ binary_to_list(NameBin),
|
|
Params = [binary_to_list(Param) || #{name := Param} <- ParamDefs],
|
|
Def = opaque_type(Params, DefACI),
|
|
convert_typedefs_loop(Rest, NamePrefix, [Converted, {Name, Params, Def}]).
|
|
|
|
|
|
-spec collect_opaque_types(Tree, TypeDefs) -> TypeDefs
|
|
when Tree :: typedef_tree(),
|
|
TypeDefs :: #{string() => typedef()}.
|
|
|
|
-type typedef_tree() :: {string(), [string()], typedef_body()} | list(typedef_tree()).
|
|
|
|
|
|
collect_opaque_types([], Types) ->
|
|
Types;
|
|
collect_opaque_types([L | R], Types) ->
|
|
NewTypes = collect_opaque_types(L, Types),
|
|
collect_opaque_types(R, NewTypes);
|
|
collect_opaque_types({Name, Params, Def}, Types) ->
|
|
maps:put(Name, {Params, Def}, Types).
|
|
|
|
|
|
|
|
%%% ACI Type -> Opaque Type
|
|
|
|
-spec opaque_type(Params, ACIType) -> Opaque
|
|
when Params :: [string()],
|
|
ACIType :: binary() | map(),
|
|
Opaque :: opaque_type().
|
|
% Convert an ACI type defintion/spec into the 'opaque type' representation that
|
|
% our dereferencing algorithms can reason about.
|
|
|
|
opaque_type(Params, NameBin) when is_binary(NameBin) ->
|
|
Name = opaque_type_name(NameBin),
|
|
case not is_atom(Name) and lists:member(Name, Params) of
|
|
false -> Name;
|
|
true -> {var, Name}
|
|
end;
|
|
opaque_type(Params, #{record := FieldDefs}) ->
|
|
Fields = [{binary_to_list(Name), opaque_type(Params, Type)}
|
|
|| #{name := Name, type := Type} <- FieldDefs],
|
|
{record, Fields};
|
|
opaque_type(Params, #{variant := VariantDefs}) ->
|
|
ConvertVariant = fun(Pair) -> opaque_variant_each(Params, Pair) end,
|
|
Variants = lists:map(ConvertVariant, VariantDefs),
|
|
{variant, Variants};
|
|
opaque_type(Params, #{tuple := TypeDefs}) ->
|
|
{tuple, [opaque_type(Params, Type) || Type <- TypeDefs]};
|
|
opaque_type(_, #{bytes := Count}) ->
|
|
{bytes, [Count]};
|
|
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.
|
|
% Everything else stays as a string, user-defined or not.
|
|
|
|
opaque_type_name(<<"int">>) -> integer;
|
|
opaque_type_name(<<"bool">>) -> boolean;
|
|
opaque_type_name(<<"bits">>) -> bits;
|
|
opaque_type_name(<<"char">>) -> char;
|
|
opaque_type_name(<<"string">>) -> string;
|
|
opaque_type_name(<<"address">>) -> address;
|
|
opaque_type_name(<<"signature">>) -> signature;
|
|
opaque_type_name(<<"contract">>) -> contract;
|
|
opaque_type_name(<<"list">>) -> list;
|
|
opaque_type_name(<<"map">>) -> map;
|
|
% I'm not sure how to produce channels in Sophia source, but they seem to exist
|
|
% in gmb still.
|
|
opaque_type_name(<<"channel">>) -> channel;
|
|
opaque_type_name(Name) -> binary_to_list(Name).
|
|
|
|
|
|
builtin_typedefs() ->
|
|
#{"unit" => {[], {tuple, []}},
|
|
"void" => {[], {variant, []}},
|
|
"hash" => {[], {bytes, [32]}},
|
|
"option" => {["'T"], {variant, [{"None", []},
|
|
{"Some", [{var, "'T"}]}]}},
|
|
"Chain.ttl" => {[], {variant, [{"FixedTTL", [integer]},
|
|
{"RelativeTTL", [integer]}]}},
|
|
"AENS.pointee" => {[], {variant, [{"AccountPt", [address]},
|
|
{"OraclePt", [address]},
|
|
{"ContractPt", [address]},
|
|
{"ChannelPt", [address]}]}},
|
|
"AENS.name" => {[], {variant, [{"Name", [address,
|
|
"Chain.ttl",
|
|
{map, [string, "AENS.pointee"]}]}]}},
|
|
"AENSv2.pointee" => {[], {variant, [{"AccountPt", [address]},
|
|
{"OraclePt", [address]},
|
|
{"ContractPt", [address]},
|
|
{"ChannelPt", [address]},
|
|
{"DataPt", [{bytes, [any]}]}]}},
|
|
"AENSv2.name" => {[], {variant, [{"Name", [address,
|
|
"Chain.ttl",
|
|
{map, [string, "AENSv2.pointee"]}]}]}},
|
|
"Chain.ga_meta_tx" => {[], {variant, [{"GAMetaTx", [address, integer]}]}},
|
|
"Chain.paying_for_tx" => {[], {variant, [{"PayingForTx", [address, integer]}]}},
|
|
"Chain.base_tx" => {[], {variant, [{"SpendTx", [address, integer, string]},
|
|
{"OracleRegisterTx", []},
|
|
{"OracleQueryTx", []},
|
|
{"OracleResponseTx", []},
|
|
{"OracleExtendTx", []},
|
|
{"NamePreclaimTx", []},
|
|
{"NameClaimTx", ["hash"]},
|
|
{"NameUpdateTx", [string]},
|
|
{"NameRevokeTx", ["hash"]},
|
|
{"NameTransferTx", [address, string]},
|
|
{"ChannelCreateTx", [address]},
|
|
{"ChannelDepositTx", [address, integer]},
|
|
{"ChannelWithdrawTx", [address, integer]},
|
|
{"ChannelForceProgressTx", [address]},
|
|
{"ChannelCloseMutualTx", [address]},
|
|
{"ChannelCloseSoloTx", [address]},
|
|
{"ChannelSlashTx", [address]},
|
|
{"ChannelSettleTx", [address]},
|
|
{"ChannelSnapshotSoloTx", [address]},
|
|
{"ContractCreateTx", [integer]},
|
|
{"ContractCallTx", [address, integer]},
|
|
{"GAAttachTx", []}]}},
|
|
"Chain.tx" => {[], {record, [{"paying_for", {"option", ["Chain.paying_for_tx"]}},
|
|
{"ga_metas", {list, ["Chain.ga_meta_tx"]}},
|
|
{"actor", address},
|
|
{"fee", integer},
|
|
{"ttl", integer},
|
|
{"tx", "Chain.base_tx"}]}},
|
|
"MCL_BLS12_381.fr" => {[], {bytes, [32]}},
|
|
"MCL_BLS12_381.fp" => {[], {bytes, [48]}}
|
|
}.
|
|
|
|
|
|
%%% Opaque Type -> Accelerated 'Annotated' Type
|
|
|
|
% Type preparation has two goals. First, we need a data structure that can be
|
|
% traversed quickly, to take sophia-esque erlang expressions and turn them into
|
|
% fate-esque erlang expressions that gmbytecode can serialize. Second, we need
|
|
% partially substituted names, so that error messages can be generated for why
|
|
% "foobar" is not valid as the third field of a `bazquux', because the third
|
|
% field is supposed to be `option(integer)', not `string'.
|
|
%
|
|
% To achieve this we need three representations of each type expression, which
|
|
% together form an 'annotated type'. First, we need the fully opaque name,
|
|
% "bazquux", then we need the normalized name, which is an opaque name with the
|
|
% bare-minimum substitution needed to make the outer-most type-constructor an
|
|
% identifiable built-in, ADT, or record type, and then we need the dereferenced
|
|
% type, which is the raw {variant, [{Name, Fields}, ...]} or
|
|
% {record, [{Name, Type}]} expression that can be used in actual Sophia->FATE
|
|
% coercion. The type sub-expressions in these dereferenced types will each be
|
|
% fully annotated as well, i.e. they will each contain *all three* of the above
|
|
% representations, so that coercion of subexpressions remains fast AND
|
|
% informative.
|
|
%
|
|
% In a lot of cases the opaque type given will already be normalized, in which
|
|
% case either the normalized field or the non-normalized field of an annotated
|
|
% type can simple be the atom `already_normalized', which means error messages
|
|
% 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) ->
|
|
{ok, Args} = annotate_bindings(ArgsOpaque, Types, []),
|
|
{ok, Result} = annotate_type(ResultOpaque, Types),
|
|
NewSpecs = maps:put(Name, {Args, Result}, Specs),
|
|
annotate_function_specs(Rest, Types, NewSpecs).
|
|
|
|
|
|
-spec annotate_type(Opaque, Types) -> {ok, Annotated}
|
|
when Opaque :: opaque_type(),
|
|
Types :: #{string() => typedef()},
|
|
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)
|
|
end.
|
|
|
|
annotate_type2(T, AlreadyNormalized, NOpaque, NExpanded, Types) ->
|
|
{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) ->
|
|
{ok, Type} = annotate_type(T, Types),
|
|
annotate_types(Rest, Types, [Type | Acc]);
|
|
annotate_types([], _Types, Acc) ->
|
|
{ok, lists:reverse(Acc)}.
|
|
|
|
annotate_type_subexpressions(PrimitiveType, _Types) when is_atom(PrimitiveType) ->
|
|
{ok, PrimitiveType};
|
|
annotate_type_subexpressions({bytes, [Count]}, _Types) ->
|
|
% bytes is weird, because it has an argument, but that argument isn't an
|
|
% opaque type.
|
|
{ok, {bytes, [Count]}};
|
|
annotate_type_subexpressions({variant, VariantsOpaque}, Types) ->
|
|
{ok, Variants} = annotate_variants(VariantsOpaque, Types, []),
|
|
{ok, {variant, Variants}};
|
|
annotate_type_subexpressions({record, FieldsOpaque}, Types) ->
|
|
{ok, Fields} = annotate_bindings(FieldsOpaque, Types, []),
|
|
{ok, {record, Fields}};
|
|
annotate_type_subexpressions({T, ElemsOpaque}, Types) ->
|
|
{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) ->
|
|
{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) ->
|
|
{ok, ElemsFlat} = annotate_types(Elems, Types, []),
|
|
annotate_variants(Rest, Types, [{Name, ElemsFlat} | Acc]);
|
|
annotate_variants([], _Types, Acc) ->
|
|
{ok, lists:reverse(Acc)}.
|
|
|
|
|
|
% This function evaluates type aliases in a loop, until eventually a usable
|
|
% definition is found.
|
|
normalize_opaque_type(T, Types) -> normalize_opaque_type(T, Types, true).
|
|
|
|
% FIXME detect infinite loops
|
|
% FIXME detect builtins with the wrong number of arguments
|
|
% FIXME should nullary types have an empty list of arguments added before now?
|
|
normalize_opaque_type(T, _Types, IsFirst) when is_atom(T) ->
|
|
% Once we have eliminated the above rewrite cases, all other cases are
|
|
% handled explicitly by the coerce logic, and so are considered normalized.
|
|
{ok, IsFirst, T, T};
|
|
normalize_opaque_type(Type = {T, _}, _Types, IsFirst) when is_atom(T) ->
|
|
% Once we have eliminated the above rewrite cases, all other cases are
|
|
% handled explicitly by the coerce logic, and so are considered normalized.
|
|
{ok, IsFirst, Type, Type};
|
|
normalize_opaque_type(T, Types, IsFirst) when is_list(T) ->
|
|
% Lists/strings indicate userspace types, which may require arg
|
|
% substitutions. Convert to an explicit but empty arg list, for uniformity.
|
|
normalize_opaque_type({T, []}, Types, IsFirst);
|
|
normalize_opaque_type({T, TypeArgs}, Types, IsFirst) when is_list(T) ->
|
|
case maps:find(T, Types) of
|
|
error ->
|
|
% We couldn't find this named type... Keep building the AACI, but
|
|
% mark this type expression as unknown, so that FATE coercions
|
|
% aren't attempted.
|
|
{ok, IsFirst, {T, TypeArgs}, unknown_type};
|
|
{ok, {TypeParamNames, Definition}} ->
|
|
% We have a definition for this type, including names for whatever
|
|
% args we have been given. Subtitute our args into this.
|
|
NewType = substitute_opaque_type(TypeParamNames, Definition, TypeArgs),
|
|
% Now continue on to see if we need to restart the loop or not.
|
|
normalize_opaque_type2(IsFirst, {T, TypeArgs}, NewType, Types)
|
|
end.
|
|
|
|
normalize_opaque_type2(IsFirst, PrevType, NextType = {variant, _}, _) ->
|
|
% We have reduced to a variant. Report the type name as the normalized
|
|
% type, but also provide the variant definition itself as the candidate
|
|
% flattened type for further annotation.
|
|
{ok, IsFirst, PrevType, NextType};
|
|
normalize_opaque_type2(IsFirst, PrevType, NextType = {record, _}, _) ->
|
|
% We have reduced to a record. Report the type name as the normalized
|
|
% type, but also provide the record definition itself as the candidate
|
|
% flattened type for further annotation.
|
|
{ok, IsFirst, PrevType, NextType};
|
|
normalize_opaque_type2(_, _, NextType, Types) ->
|
|
% Not a variant or record yet, so go back to the start of the loop.
|
|
% It will no longer be the first iteration.
|
|
normalize_opaque_type(NextType, Types, false).
|
|
|
|
% Perform a beta-reduction on a type expression.
|
|
substitute_opaque_type([], Definition, _) ->
|
|
% There are no parameters to substitute. This is the simplest way of
|
|
% defining type aliases, records, and variants, so we should make sure to
|
|
% short circuit all the recursive descent logic, since it won't actually
|
|
% do anything.
|
|
Definition;
|
|
substitute_opaque_type(TypeParamNames, Definition, TypeArgs) ->
|
|
% Bundle the param names alongside the args that we want to substitute, so
|
|
% that we can keyfind the one list.
|
|
Bindings = lists:zip(TypeParamNames, TypeArgs),
|
|
substitute_opaque_type(Bindings, Definition).
|
|
|
|
substitute_opaque_type(Bindings, {var, VarName}) ->
|
|
case lists:keyfind(VarName, 1, Bindings) of
|
|
{_, TypeArg} -> TypeArg;
|
|
% No valid ACI will create this case. Regardless, the user should
|
|
% still be able to specify arbitrary gmb FATE terms for whatever this
|
|
% is meant to be.
|
|
false -> unknown_type
|
|
end;
|
|
substitute_opaque_type(Bindings, {variant, Variants}) ->
|
|
Each = fun({VariantName, Elements}) ->
|
|
NewElements = substitute_opaque_types(Bindings, Elements),
|
|
{VariantName, NewElements}
|
|
end,
|
|
NewVariants = lists:map(Each, Variants),
|
|
{variant, NewVariants};
|
|
substitute_opaque_type(Bindings, {record, Fields}) ->
|
|
Each = fun({FieldName, FieldType}) ->
|
|
NewType = substitute_opaque_type(Bindings, FieldType),
|
|
{FieldName, NewType}
|
|
end,
|
|
NewFields = lists:map(Each, Fields),
|
|
{record, NewFields};
|
|
substitute_opaque_type(Bindings, {Connective, Args}) ->
|
|
NewArgs = substitute_opaque_types(Bindings, Args),
|
|
{Connective, NewArgs};
|
|
substitute_opaque_type(_Bindings, Type) ->
|
|
Type.
|
|
|
|
substitute_opaque_types(Bindings, Types) ->
|
|
Each = fun(Type) -> substitute_opaque_type(Bindings, Type) end,
|
|
lists:map(Each, Types).
|
|
|
|
|
|
|
|
%%% Erlang to FATE
|
|
|
|
-spec erlang_args_to_fate(VarTypes, Terms) -> {ok, FATE} | {error, Errors}
|
|
when VarTypes :: [{string(), annotated_type()}],
|
|
Terms :: [erlang_repr()],
|
|
FATE :: gmb_fate_data:fate_type(),
|
|
Errors :: [{Reason, [PathStep]}],
|
|
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' 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),
|
|
if
|
|
DefLength =:= ArgLength -> coerce_zipped_bindings(lists:zip(VarTypes, Terms), arg);
|
|
DefLength > ArgLength -> {error, too_few_args};
|
|
DefLength < ArgLength -> {error, too_many_args}
|
|
end.
|
|
|
|
|
|
-spec erlang_to_fate(Type, Erlang) -> {ok, FATE} | {error, Errors}
|
|
when Type :: annotated_type(),
|
|
FATE :: gmb_fate_data:fate_type(),
|
|
Erlang :: erlang_repr(),
|
|
Errors :: [{Reason, [PathStep]}],
|
|
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) ->
|
|
try
|
|
Val = list_to_integer(S),
|
|
{ok, Val}
|
|
catch
|
|
error:badarg -> single_error({invalid, O, N, S})
|
|
end;
|
|
erlang_to_fate({O, N, address}, S) ->
|
|
coerce_chain_object(O, N, address, account_pubkey, S);
|
|
erlang_to_fate({O, N, contract}, S) ->
|
|
coerce_chain_object(O, N, contract, contract_pubkey, S);
|
|
erlang_to_fate({_, _, signature}, S) when is_binary(S) andalso (byte_size(S) =:= 64) ->
|
|
% Usually to pass a binary in, you need to wrap it as {raw, Binary}, but
|
|
% since sg_... strings OR hex blobs can be used as signatures in Sophia, we
|
|
% special case this case based on the length. Even if a binary starts with
|
|
% "sg_", 64 characters is not enough to represent a 64 byte signature, so
|
|
% the most optimistic interpretation is to use the binary directly.
|
|
{ok, S};
|
|
erlang_to_fate({O, N, signature}, S) ->
|
|
coerce_chain_object(O, N, signature, signature, S);
|
|
%erlang_to_fate({_, _, channel}, S) when is_binary(S) ->
|
|
%{ok, {channel, S}};
|
|
erlang_to_fate({_, _, boolean}, true) ->
|
|
{ok, true};
|
|
erlang_to_fate({_, _, boolean}, "true") ->
|
|
{ok, true};
|
|
erlang_to_fate({_, _, boolean}, false) ->
|
|
{ok, false};
|
|
erlang_to_fate({_, _, boolean}, "false") ->
|
|
{ok, false};
|
|
erlang_to_fate({O, N, string}, Str) ->
|
|
case unicode:characters_to_binary(Str) of
|
|
{error, _, _} ->
|
|
single_error({invalid, O, N, Str});
|
|
{incomplete, _, _} ->
|
|
single_error({invalid, O, N, Str});
|
|
StrBin ->
|
|
{ok, StrBin}
|
|
end;
|
|
erlang_to_fate({_, _, char}, Val) when is_integer(Val) ->
|
|
{ok, Val};
|
|
erlang_to_fate({O, N, char}, Str) ->
|
|
Result = unicode:characters_to_list(Str),
|
|
case Result of
|
|
{error, _, _} ->
|
|
single_error({invalid, O, N, Str});
|
|
{incomplete, _, _} ->
|
|
single_error({invalid, O, N, Str});
|
|
[C] ->
|
|
{ok, C};
|
|
_ ->
|
|
single_error({invalid, O, N, Str})
|
|
end;
|
|
erlang_to_fate({O, N, {bytes, [Count]}}, Bytes) when is_bitstring(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) ->
|
|
Size = bit_size(Bits),
|
|
<<IntValue:Size>> = Bits,
|
|
{ok, {bits, IntValue}};
|
|
erlang_to_fate({_, _, {list, [Type]}}, Data) when is_list(Data) ->
|
|
coerce_list(Type, Data);
|
|
erlang_to_fate({_, _, {map, [KeyType, ValType]}}, Data) when is_map(Data) ->
|
|
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);
|
|
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);
|
|
not_found ->
|
|
ValidNames = [Valid || {Valid, _} <- Variants],
|
|
single_error({invalid_variant, O, N, Name, ValidNames})
|
|
end;
|
|
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) ->
|
|
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) ->
|
|
case decode_chain_object(Tag, S) of
|
|
{ok, Data} -> {ok, coerce_chain_object2(T, Data)};
|
|
{error, Reason} -> single_error({Reason, O, N, S})
|
|
end.
|
|
|
|
coerce_chain_object2(address, Data) -> {address, Data};
|
|
coerce_chain_object2(contract, Data) -> {contract, Data};
|
|
coerce_chain_object2(signature, Data) -> Data.
|
|
|
|
decode_chain_object(Tag, S) ->
|
|
try
|
|
case gmser_api_encoder:decode(unicode:characters_to_binary(S)) of
|
|
{Tag, Data} -> {ok, Data};
|
|
{_, _} -> {error, wrong_prefix}
|
|
end
|
|
catch
|
|
error:missing_prefix -> {error, missing_prefix};
|
|
error:incorrect_size -> {error, incorrect_size}
|
|
end.
|
|
|
|
check_bytes(O, N, _, Bytes) when bit_size(Bytes) rem 8 /= 0 ->
|
|
single_error({partial_bytes, O, N, bit_size(Bytes)});
|
|
check_bytes(_, _, any, _) ->
|
|
ok;
|
|
check_bytes(O, N, Count, Bytes) when byte_size(Bytes) /= Count ->
|
|
single_error({incorrect_size, O, N, Bytes});
|
|
check_bytes(_, _, _, _) ->
|
|
ok.
|
|
|
|
coerce_zipped_bindings(Bindings, Tag) ->
|
|
coerce_zipped_bindings(Bindings, Tag, [], []).
|
|
|
|
coerce_zipped_bindings([Next | Rest], Tag, Good, Broken) ->
|
|
{{ArgName, Type}, Term} = Next,
|
|
case erlang_to_fate(Type, Term) of
|
|
{ok, NewTerm} ->
|
|
coerce_zipped_bindings(Rest, Tag, [NewTerm | Good], Broken);
|
|
{error, Errors} ->
|
|
Wrapped = wrap_errors({Tag, ArgName}, Errors),
|
|
coerce_zipped_bindings(Rest, Tag, Good, [Wrapped | Broken])
|
|
end;
|
|
coerce_zipped_bindings([], _, Good, []) ->
|
|
{ok, lists:reverse(Good)};
|
|
coerce_zipped_bindings([], _, _, Broken) ->
|
|
{error, combine_errors(Broken)}.
|
|
|
|
coerce_list(Type, Elements) ->
|
|
% 0 index since it represents a sophia list
|
|
coerce_list(Type, Elements, 0, [], []).
|
|
|
|
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, Index + 1, Good, [Wrapped | Broken])
|
|
end;
|
|
coerce_list(_Type, [], _, Good, []) ->
|
|
{ok, lists:reverse(Good)};
|
|
coerce_list(_, [], _, _, Broken) ->
|
|
{error, combine_errors(Broken)}.
|
|
|
|
coerce_map(KeyType, ValType, Data) ->
|
|
coerce_map(KeyType, ValType, maps:iterator(Data), #{}, []).
|
|
|
|
coerce_map(KeyType, ValType, Remaining, Good, Broken) ->
|
|
case maps:next(Remaining) of
|
|
{K, V, RemainingAfter} ->
|
|
coerce_map2(KeyType, ValType, RemainingAfter, Good, Broken, K, V);
|
|
none ->
|
|
coerce_map_finish(Good, Broken)
|
|
end.
|
|
|
|
coerce_map2(KeyType, ValType, Remaining, Good, Broken, K, V) ->
|
|
case erlang_to_fate(KeyType, K) of
|
|
{ok, 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, Good, [Wrapped | Broken], K, V, error)
|
|
end.
|
|
|
|
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, NewGood, Broken);
|
|
{error, Errors} ->
|
|
Wrapped = wrap_errors({map_value, K}, Errors),
|
|
coerce_map(KeyType, ValType, Remaining, Good, [Wrapped | Broken])
|
|
end.
|
|
|
|
coerce_map_finish(Good, []) ->
|
|
{ok, Good};
|
|
coerce_map_finish(_, Broken) ->
|
|
{error, combine_errors(Broken)}.
|
|
|
|
lookup_variant(Name, Variants) -> lookup_variant(Name, Variants, 0).
|
|
|
|
lookup_variant(Name, [{Name, Terms} | _], Tag) ->
|
|
{Tag, Terms};
|
|
lookup_variant(Name, [_ | Rest], Tag) ->
|
|
lookup_variant(Name, Rest, Tag + 1);
|
|
lookup_variant(_Name, [], _Tag) ->
|
|
not_found.
|
|
|
|
coerce_tuple(O, N, TermTypes, Terms) ->
|
|
case coerce_elems_to_fate(TermTypes, Terms, tuple_element) of
|
|
{ok, Converted} ->
|
|
{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} ->
|
|
single_error({tuple_too_many_terms, O, N, list_to_tuple(Terms)});
|
|
Errors -> Errors
|
|
end.
|
|
|
|
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_elems_to_fate(TermTypes, Terms, adt_element) of
|
|
{ok, Converted} ->
|
|
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} ->
|
|
single_error({adt_too_many_terms, O, N, Name, TermTypes, Terms});
|
|
Errors -> Errors
|
|
end.
|
|
|
|
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_elems_to_fate(Types, Terms, Tag, 0, [], []).
|
|
|
|
coerce_elems_to_fate([Type | Types], [Term | Terms], Tag, Index, Good, Broken) ->
|
|
case erlang_to_fate(Type, Term) of
|
|
{ok, Value} ->
|
|
coerce_elems_to_fate(Types, Terms, Tag, Index + 1, [Value | Good], Broken);
|
|
{error, Errors} ->
|
|
Wrapped = wrap_errors({Tag, Index}, Errors),
|
|
coerce_elems_to_fate(Types, Terms, Tag, Index + 1, Good, [Wrapped | Broken])
|
|
end;
|
|
coerce_elems_to_fate([], [], _, _, Good, []) ->
|
|
{ok, lists:reverse(Good)};
|
|
coerce_elems_to_fate([], [], _, _, _, Broken) ->
|
|
{error, combine_errors(Broken)};
|
|
coerce_elems_to_fate(_, [], _, _, _, _) ->
|
|
{error, too_few_terms};
|
|
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, 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 ->
|
|
Errors
|
|
end;
|
|
{error, {missing_fields, Missing}} ->
|
|
single_error({missing_fields, O, N, Missing});
|
|
{error, {unexpected_fields, Unexpected}} ->
|
|
Names = [Name || {Name, _} <- maps:to_list(Unexpected)],
|
|
single_error({unexpected_fields, O, N, Names})
|
|
end.
|
|
|
|
zip_record_fields(Fields, Map) ->
|
|
case lists:mapfoldl(fun zip_record_field/2, {Map, []}, Fields) of
|
|
{_, {_, Missing = [_|_]}} ->
|
|
{error, {missing_fields, lists:reverse(Missing)}};
|
|
{_, {Remaining, _}} when map_size(Remaining) > 0 ->
|
|
{error, {unexpected_fields, Remaining}};
|
|
{Zipped, _} ->
|
|
{ok, Zipped}
|
|
end.
|
|
|
|
zip_record_field({Name, Type}, {Remaining, Missing}) ->
|
|
case maps:take(Name, Remaining) of
|
|
{Term, RemainingAfter} ->
|
|
ZippedTerm = {{Name, Type}, Term},
|
|
{ZippedTerm, {RemainingAfter, Missing}};
|
|
error ->
|
|
{missing, {Remaining, [Name | Missing]}}
|
|
end.
|
|
|
|
% Wraps a single error in a list, along with an empty path, so that other
|
|
% accumulating error handlers can work with it.
|
|
single_error(Reason) ->
|
|
{error, [{Reason, []}]}.
|
|
|
|
wrap_errors(Location, Errors) ->
|
|
F = fun({Error, Path}) ->
|
|
{Error, [Location | Path]}
|
|
end,
|
|
lists:map(F, Errors).
|
|
|
|
combine_errors(Broken) ->
|
|
F = fun(NextErrors, Acc) ->
|
|
NextErrors ++ Acc
|
|
end,
|
|
lists:foldl(F, [], Broken).
|
|
|
|
|
|
|
|
%%% FATE to Erlang
|
|
|
|
-spec fate_to_erlang(Type, FATE) -> Erlang
|
|
when Type :: annotated_type(),
|
|
FATE :: gmb_fate_data:fate_type(),
|
|
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
|
|
%% 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) ->
|
|
S;
|
|
fate_to_erlang({_, _, address}, {address, Bin}) ->
|
|
Address = gmser_api_encoder:encode(account_pubkey, Bin),
|
|
unicode:characters_to_list(Address);
|
|
fate_to_erlang({_, _, contract}, {contract, Bin}) ->
|
|
Address = gmser_api_encoder:encode(contract_pubkey, Bin),
|
|
unicode:characters_to_list(Address);
|
|
fate_to_erlang({_, _, signature}, Bin) ->
|
|
Address = gmser_api_encoder:encode(signature, Bin),
|
|
unicode:characters_to_list(Address);
|
|
%fate_to_erlang({_, _, channel}, {channel, S}) when is_binary(S) ->
|
|
%S;
|
|
fate_to_erlang({_, _, boolean}, true) ->
|
|
true;
|
|
fate_to_erlang({_, _, boolean}, false) ->
|
|
false;
|
|
fate_to_erlang({_, _, string}, Bin) ->
|
|
binary_to_list(Bin);
|
|
fate_to_erlang({_, _, char}, Val) ->
|
|
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}) ->
|
|
Num;
|
|
fate_to_erlang({_, _, {list, [Type]}}, Data) when is_list(Data) ->
|
|
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_to_erlang(KeyType, ValType, maps:iterator(Data), #{});
|
|
fate_to_erlang({_, _, {tuple, ElementTypes}}, {tuple, Data}) ->
|
|
ElementList = tuple_to_list(Data),
|
|
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, 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.
|
|
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) ->
|
|
warn_unknown_type(O, N, Data),
|
|
Data;
|
|
fate_to_erlang({O, N, _}, Data) ->
|
|
erlang:exit({invalid, O, N, Data}).
|
|
|
|
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).
|
|
|
|
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.
|
|
|
|
|
|
|
|
%%% AACI Getters
|
|
|
|
-spec get_function_signature(AACI, Fun) -> {ok, Type} | {error, Reason}
|
|
when AACI :: aaci(),
|
|
Fun :: binary() | string(),
|
|
Type :: {term(), term()}, % FIXME
|
|
Reason :: bad_fun_name.
|
|
%% @doc
|
|
%% 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
|
|
{ok, A} -> {ok, A};
|
|
error -> {error, bad_fun_name}
|
|
end.
|
|
|
|
|
|
%%% Simple FATE/erlang tests
|
|
|
|
check_erlang_to_fate(Type, Sophia, Fate) ->
|
|
{ok, FateActual} = erlang_to_fate(Type, Sophia),
|
|
case FateActual of
|
|
Fate ->
|
|
ok;
|
|
_ ->
|
|
erlang:error({to_fate_failed, Fate, FateActual})
|
|
end.
|
|
|
|
check_fate_to_erlang(Type, Fate, Sophia) ->
|
|
SophiaActual = fate_to_erlang(Type, Fate),
|
|
% Now check that the results were what we expected.
|
|
case SophiaActual of
|
|
Sophia ->
|
|
ok;
|
|
_ ->
|
|
erlang:error({from_fate_failed, Sophia, SophiaActual})
|
|
end.
|
|
|
|
% Round trip coerce run for the eunit tests below. If these results don't match
|
|
% then the test should fail.
|
|
check_roundtrip(Type, Sophia, Fate) ->
|
|
check_erlang_to_fate(Type, Sophia, Fate),
|
|
check_fate_to_erlang(Type, Fate, Sophia),
|
|
|
|
% Finally, check that the FATE result is something that gmb understands.
|
|
gmb_fate_encoding:serialize(Fate),
|
|
ok.
|
|
|
|
coerce_int_test() ->
|
|
{ok, Type} = annotate_type(integer, #{}),
|
|
check_roundtrip(Type, 123, 123).
|
|
|
|
coerce_address_test() ->
|
|
{ok, Type} = annotate_type(address, #{}),
|
|
check_roundtrip(Type,
|
|
"ak_2FTnrGfV8qsfHpaSEHpBrziioCpwwzLqSevHqfxQY3PaAAdARx",
|
|
{address, <<164,136,155,90,124,22,40,206,255,76,213,56,238,123,
|
|
167,208,53,78,40,235,2,163,132,36,47,183,228,151,9,
|
|
210,39,214>>}).
|
|
|
|
coerce_contract_test() ->
|
|
{ok, Type} = annotate_type(contract, #{}),
|
|
check_roundtrip(Type,
|
|
"ct_2FTnrGfV8qsfHpaSEHpBrziioCpwwzLqSevHqfxQY3PaAAdARx",
|
|
{contract, <<164,136,155,90,124,22,40,206,255,76,213,56,238,123,
|
|
167,208,53,78,40,235,2,163,132,36,47,183,228,151,9,
|
|
210,39,214>>}).
|
|
|
|
coerce_signature_test() ->
|
|
{ok, Type} = annotate_type(signature, #{}),
|
|
check_roundtrip(Type,
|
|
"sg_XDyF8LJC4tpMyAySvpaG1f5V9F2XxAbRx9iuVjvvdNMwVracLhzAuXhRM5kXAFtpwW1DCHuz5jGehUayCah4jub32Ti2n",
|
|
<<231,4,97,129,16,173,37,42,194,249,28,94,134,163,208,84,22,135,
|
|
169,85,212,142,14,12,233,252,97,50,193,158,229,51,123,206,222,
|
|
249,2,3,85,173,106,150,243,253,89,128,248,52,195,140,95,114,
|
|
233,110,119,143,206,137,124,36,63,154,85,7>>).
|
|
|
|
coerce_signature_binary_test() ->
|
|
{ok, Type} = annotate_type(signature, #{}),
|
|
Binary = <<231,4,97,129,16,173,37,42,194,249,28,94,134,163,208,84,22,135,
|
|
169,85,212,142,14,12,233,252,97,50,193,158,229,51,123,206,222,
|
|
249,2,3,85,173,106,150,243,253,89,128,248,52,195,140,95,114,
|
|
233,110,119,143,206,137,124,36,63,154,85,7>>,
|
|
{ok, Binary} = erlang_to_fate(Type, {raw, Binary}),
|
|
{ok, Binary} = erlang_to_fate(Type, Binary),
|
|
ok.
|
|
|
|
coerce_bool_test() ->
|
|
{ok, Type} = annotate_type(boolean, #{}),
|
|
check_roundtrip(Type, true, true),
|
|
check_roundtrip(Type, false, false).
|
|
|
|
coerce_string_test() ->
|
|
{ok, Type} = annotate_type(string, #{}),
|
|
check_roundtrip(Type, "hello world", <<"hello world">>).
|
|
|
|
coerce_list_test() ->
|
|
{ok, Type} = annotate_type({list, [string]}, #{}),
|
|
check_roundtrip(Type, ["hello world", [65, 32, 65]], [<<"hello world">>, <<65, 32, 65>>]).
|
|
|
|
coerce_map_test() ->
|
|
{ok, Type} = annotate_type({map, [string, {list, [integer]}]}, #{}),
|
|
check_roundtrip(Type, #{"a" => "a", "b" => "b"}, #{<<"a">> => "a", <<"b">> => "b"}).
|
|
|
|
coerce_tuple_test() ->
|
|
{ok, Type} = annotate_type({tuple, [integer, string]}, #{}),
|
|
check_roundtrip(Type, {123, "456"}, {tuple, {123, <<"456">>}}).
|
|
|
|
coerce_variant_test() ->
|
|
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}}).
|
|
|
|
coerce_option_test() ->
|
|
{ok, Type} = annotate_type({"option", [integer]}, builtin_typedefs()),
|
|
check_roundtrip(Type, {"None"}, {variant, [0, 1], 0, {}}),
|
|
check_roundtrip(Type, {"Some", 1}, {variant, [0, 1], 1, {1}}).
|
|
|
|
coerce_record_test() ->
|
|
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() ->
|
|
{ok, Type} = annotate_type({tuple, [{bytes, [4]}, {bytes, [any]}]}, #{}),
|
|
check_roundtrip(Type, {<<"abcd">>, <<"efghi">>}, {tuple, {{bytes, <<"abcd">>}, {bytes, <<"efghi">>}}}).
|
|
|
|
coerce_bits_test() ->
|
|
{ok, Type} = annotate_type(bits, #{}),
|
|
check_roundtrip(Type, 5, {bits, 5}).
|
|
|
|
coerce_char_test() ->
|
|
{ok, Type} = annotate_type(char, #{}),
|
|
check_roundtrip(Type, $?, $?).
|
|
|
|
coerce_unicode_test() ->
|
|
{ok, Type} = annotate_type(char, #{}),
|
|
% Latin Small Letter C with cedilla and acute
|
|
{ok, $ḉ} = erlang_to_fate(Type, <<"ḉ"/utf8>>),
|
|
ok.
|
|
|
|
coerce_hash_test() ->
|
|
{ok, Type} = annotate_type("hash", builtin_typedefs()),
|
|
Hash = list_to_binary(lists:seq(1,32)),
|
|
check_roundtrip(Type, Hash, {bytes, Hash}),
|
|
ok.
|
|
|
|
|
|
%%% Complex AACI paramter and namespace tests
|
|
|
|
aaci_from_string(String) ->
|
|
case so_compiler:from_string(String, [{aci, json}]) of
|
|
{ok, #{aci := ACI}} -> {ok, prepare(ACI)};
|
|
Error -> Error
|
|
end.
|
|
|
|
namespace_coerce_test() ->
|
|
Contract = "
|
|
namespace N =
|
|
record pair = { a : int, b : int }
|
|
|
|
contract C =
|
|
entrypoint f(): N.pair = { a = 1, b = 2 }
|
|
",
|
|
{ok, AACI} = aaci_from_string(Contract),
|
|
{ok, {[], Output}} = get_function_signature(AACI, "f"),
|
|
check_roundtrip(Output, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}).
|
|
|
|
record_substitution_test() ->
|
|
Contract = "
|
|
contract C =
|
|
record pair('t) = { a : 't, b : 't }
|
|
entrypoint f(): pair(int) = { a = 1, b = 2 }
|
|
",
|
|
{ok, AACI} = aaci_from_string(Contract),
|
|
{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}}).
|
|
|
|
tuple_substitution_test() ->
|
|
Contract = "
|
|
contract C =
|
|
type triple('t1, 't2) = int * 't1 * 't2
|
|
entrypoint f(): triple(int, string) = (1, 2, \"hello\")
|
|
",
|
|
{ok, AACI} = aaci_from_string(Contract),
|
|
{ok, {[], Output}} = get_function_signature(AACI, "f"),
|
|
check_roundtrip(Output, {1, 2, "hello"}, {tuple, {1, 2, <<"hello">>}}).
|
|
|
|
variant_substitution_test() ->
|
|
Contract = "
|
|
contract C =
|
|
datatype adt('a, 'b) = Left('a, 'b) | Right('b, int)
|
|
entrypoint f(): adt(string, int) = Left(\"hi\", 1)
|
|
",
|
|
{ok, AACI} = aaci_from_string(Contract),
|
|
{ok, {[], Output}} = get_function_signature(AACI, "f"),
|
|
check_roundtrip(Output, {"Left", "hi", 1}, {variant, [2, 2], 0, {<<"hi">>, 1}}),
|
|
check_roundtrip(Output, {"Right", 2, 3}, {variant, [2, 2], 1, {2, 3}}).
|
|
|
|
nested_coerce_test() ->
|
|
Contract = "
|
|
contract C =
|
|
type pair('t) = 't * 't
|
|
record r = { f1 : pair(int), f2: pair(string) }
|
|
entrypoint f(): r = { f1 = (1, 2), f2 = (\"a\", \"b\") }
|
|
",
|
|
{ok, AACI} = aaci_from_string(Contract),
|
|
{ok, {[], Output}} = get_function_signature(AACI, "f"),
|
|
check_roundtrip(Output,
|
|
#{ "f1" => {1, 2}, "f2" => {"a", "b"}},
|
|
{tuple, {{tuple, {1, 2}}, {tuple, {<<"a">>, <<"b">>}}}}).
|
|
|
|
state_coerce_test() ->
|
|
Contract = "
|
|
contract C =
|
|
type state = int
|
|
entrypoint init(): state = 0
|
|
",
|
|
{ok, AACI} = aaci_from_string(Contract),
|
|
{ok, {[], Output}} = get_function_signature(AACI, "init"),
|
|
check_roundtrip(Output, 0, 0).
|
|
|
|
param_test() ->
|
|
Contract = "
|
|
contract C =
|
|
type state = int
|
|
entrypoint init(x): state = x
|
|
",
|
|
{ok, AACI} = aaci_from_string(Contract),
|
|
{ok, {[{"x", Input}], Output}} = get_function_signature(AACI, "init"),
|
|
check_roundtrip(Input, 0, 0),
|
|
check_roundtrip(Output, 0, 0).
|
|
|
|
%%% Obscure Sophia types where we should check the AACI as well
|
|
|
|
obscure_aaci_test() ->
|
|
Contract = "
|
|
include \"Set.aes\"
|
|
contract C =
|
|
entrypoint options(): option(int) = None
|
|
entrypoint fixed_bytes(): bytes(4) = #DEADBEEF
|
|
entrypoint any_bytes(): bytes() = Bytes.to_any_size(#112233)
|
|
entrypoint bits(): bits = Bits.all
|
|
entrypoint character(): char = 'a'
|
|
entrypoint hash(): hash = #00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF
|
|
entrypoint unit(): unit = ()
|
|
|
|
entrypoint ttl(x): Chain.ttl = FixedTTL(x)
|
|
entrypoint paying_for(x, y): Chain.paying_for_tx = Chain.PayingForTx(x, y)
|
|
entrypoint ga_meta_tx(x, y): Chain.ga_meta_tx = Chain.GAMetaTx(x, y)
|
|
entrypoint base_tx(x, y, z): Chain.base_tx = Chain.SpendTx(x, y, z)
|
|
entrypoint tx(a, b, c, d, e, f): Chain.tx =
|
|
{paying_for = a,
|
|
ga_metas = b,
|
|
actor = c,
|
|
fee = d,
|
|
ttl = e,
|
|
tx = f}
|
|
|
|
entrypoint pointee(x): AENS.pointee = AENS.AccountPt(x)
|
|
entrypoint name(x, y, z): AENS.name = AENS.Name(x, y, z)
|
|
entrypoint pointee2(x): AENSv2.pointee = AENSv2.DataPt(x)
|
|
entrypoint name2(x, y, z): AENSv2.name = AENSv2.Name(x, y, z)
|
|
|
|
entrypoint fr(x): MCL_BLS12_381.fr = x
|
|
entrypoint fp(x): MCL_BLS12_381.fp = x
|
|
|
|
entrypoint set(): Set.set(int) = Set.new()
|
|
|
|
",
|
|
{ok, AACI} = aaci_from_string(Contract),
|
|
|
|
{ok, {[], {{bytes, [4]}, _, _}}} = get_function_signature(AACI, "fixed_bytes"),
|
|
{ok, {[], {{bytes, [any]}, _, _}}} = get_function_signature(AACI, "any_bytes"),
|
|
{ok, {[], {bits, _, _}}} = get_function_signature(AACI, "bits"),
|
|
{ok, {[], {char, _, _}}} = get_function_signature(AACI, "character"),
|
|
|
|
{ok, {[], {{"option", [integer]}, _, {variant, [{"None", []}, {"Some", [_]}]}}}} = get_function_signature(AACI, "options"),
|
|
{ok, {[], {"hash", _, {bytes, [32]}}}} = get_function_signature(AACI, "hash"),
|
|
{ok, {[], {"unit", _, {tuple, []}}}} = get_function_signature(AACI, "unit"),
|
|
|
|
{ok, {_, {"Chain.ttl", _, {variant, _}}}} = get_function_signature(AACI, "ttl"),
|
|
{ok, {_, {"Chain.paying_for_tx", _, {variant, _}}}} = get_function_signature(AACI, "paying_for"),
|
|
{ok, {_, {"Chain.ga_meta_tx", _, {variant, _}}}} = get_function_signature(AACI, "ga_meta_tx"),
|
|
{ok, {_, {"Chain.base_tx", _, {variant, _}}}} = get_function_signature(AACI, "base_tx"),
|
|
{ok, {_, {"Chain.tx", _, {record, _}}}} = get_function_signature(AACI, "tx"),
|
|
|
|
{ok, {_, {"AENS.pointee", _, {variant, _}}}} = get_function_signature(AACI, "pointee"),
|
|
{ok, {_, {"AENS.name", _, {variant, _}}}} = get_function_signature(AACI, "name"),
|
|
{ok, {_, {"AENSv2.pointee", _, {variant, _}}}} = get_function_signature(AACI, "pointee2"),
|
|
{ok, {_, {"AENSv2.name", _, {variant, _}}}} = get_function_signature(AACI, "name2"),
|
|
|
|
{ok, {_, {"MCL_BLS12_381.fr", _, {bytes, [32]}}}} = get_function_signature(AACI, "fr"),
|
|
{ok, {_, {"MCL_BLS12_381.fp", _, {bytes, [48]}}}} = get_function_signature(AACI, "fp"),
|
|
|
|
{ok, {[], {{"Set.set", [integer]}, _, {record, [{"to_map", _}]}}}} = get_function_signature(AACI, "set"),
|
|
|
|
ok.
|
|
|
|
name_coerce_test() ->
|
|
AddrSoph = "ak_2FTnrGfV8qsfHpaSEHpBrziioCpwwzLqSevHqfxQY3PaAAdARx",
|
|
AddrFate = {address, <<164,136,155,90,124,22,40,206,255,76,213,56,238,123,
|
|
167,208,53,78,40,235,2,163,132,36,47,183,228,151,9,
|
|
210,39,214>>},
|
|
{ok, TTL} = annotate_type("Chain.ttl", builtin_typedefs()),
|
|
TTLSoph = {"FixedTTL", 0},
|
|
TTLFate = {variant, [1, 1], 0, {0}},
|
|
check_roundtrip(TTL, TTLSoph, TTLFate),
|
|
{ok, Pointee} = annotate_type("AENS.pointee", builtin_typedefs()),
|
|
PointeeSoph = {"AccountPt", AddrSoph},
|
|
PointeeFate = {variant, [1, 1, 1, 1], 0, {AddrFate}},
|
|
check_roundtrip(Pointee, PointeeSoph, PointeeFate),
|
|
{ok, Name} = annotate_type("AENS.name", builtin_typedefs()),
|
|
NameSoph = {"Name", AddrSoph, TTLSoph, #{"myname" => PointeeSoph}},
|
|
NameFate = {variant, [3], 0, {AddrFate, TTLFate, #{<<"myname">> => PointeeFate}}},
|
|
check_roundtrip(Name, NameSoph, NameFate).
|
|
|
|
void_coerce_test() ->
|
|
% Void itself can't be represented, but other types built out of void are
|
|
% valid.
|
|
{ok, NonOption} = annotate_type({"option", ["void"]}, builtin_typedefs()),
|
|
check_roundtrip(NonOption, {"None"}, {variant, [0, 1], 0, {}}),
|
|
{ok, NonList} = annotate_type({list, ["void"]}, builtin_typedefs()),
|
|
check_roundtrip(NonList, [], []).
|
|
|