diff --git a/doc/overview.edoc b/doc/overview.edoc index 61eb6f3..76f6302 100644 --- a/doc/overview.edoc +++ b/doc/overview.edoc @@ -1,4 +1,5 @@ -@author Craig Everett [https://git.qpq.swiss/QPQ-AG/hakuzaru] +@author Craig Everett [https://zxq9.com] +@author Jarvis Carrol [https://jarviscarroll.net/] @version 0.9.2 @title Hakuzaru: Gajumaru blockchain bindings for Erlang @@ -21,7 +22,7 @@ After startup `hz_man' must be given the address and port of a list of Gajumaru Note that the service nodes will need to have the dry-run endpoint enabled and the internal service query port made available in order to provide dry-runs and transaction submission. When configuring chain nodes a list of nodes should be provided. -To avoid sync issues in the case of fast transaction formation/submission to the chain, only one node from the list of chain nodes is used for submitting transactions and querying `next_nonce/1`. +To avoid sync issues in the case of fast transaction formation/submission to the chain, only one node from the list of chain nodes is used for submitting transactions and querying `next_nonce/1'. This node is called "the sticky node". The first node in the list of chain nodes provided during configuration is designated as the sticky node. diff --git a/src/hz_aaci.erl b/src/hz_aaci.erl index 47d659d..5fa642a 100644 --- a/src/hz_aaci.erl +++ b/src/hz_aaci.erl @@ -29,21 +29,6 @@ -include_lib("eunit/include/eunit.hrl"). -%% @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() @@ -58,6 +43,19 @@ | 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() @@ -68,305 +66,308 @@ %| [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(). +% 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. -%% @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>>}. +% 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. -%% @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>>}. +% 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. -%% @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>>}. +% 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. -%% @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(). +% 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"'. -%% @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(). +% 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. -%% @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(). +% 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. -%% @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(). +% 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. -%% @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(). +% 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. -%% @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()]. +% The Erlang representation of a Sophia `list(_)' +% Simply a list. Each element of the list is converted forwards/backwards as +% normal. -%% @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()}. +% The Erlang representation of a Sophia `map(_, _)' +% Simply a map. Each key and value is converted forwards/backwards as normal. -%% @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(). +-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. -%% @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(). +-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. -%% @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()}. +% 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. -%% @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()}}. +% 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.
  • +%
+ -%% @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. +% 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: +%
+%   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. -%% @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 builtin_type(T) :: {bytes, [integer() | any]} - | {tuple, [T]} - | {list, [T]} - | {map, [T]} - | integer - | boolean - | bits - | char - | string - | address - | signature - | contract - | channel - | unknown_type. +-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. -%% @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 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. -%% @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()). -%% @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()}. +% 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. + -%% @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 @@ -389,7 +390,6 @@ prepare_from_file(Path) -> -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. @@ -409,9 +409,9 @@ prepare(ACI) -> % 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(), @@ -442,17 +442,20 @@ convert_aci_types(ACI) -> % 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); @@ -486,12 +489,14 @@ convert_typedefs_loop([Next | Rest], NamePrefix, Converted) -> 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) -> @@ -500,15 +505,17 @@ collect_opaque_types([L | R], Types) -> 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 @@ -534,10 +541,11 @@ opaque_type(Params, Pair) when is_map(Pair) -> [{Name, TypeArgs}] = maps:to_list(Pair), {opaque_type_name(Name), [opaque_type(Params, Arg) || Arg <- TypeArgs]}. --spec opaque_type_name(binary()) -> atom() | string(). +-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; @@ -553,6 +561,7 @@ opaque_type_name(<<"map">>) -> map; opaque_type_name(<<"channel">>) -> channel; opaque_type_name(Name) -> binary_to_list(Name). + builtin_typedefs() -> #{"unit" => {[], {tuple, []}}, "void" => {[], {variant, []}}, @@ -610,14 +619,15 @@ builtin_typedefs() -> "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`. +% "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, @@ -633,7 +643,7 @@ builtin_typedefs() -> % % 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 +% 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. @@ -655,6 +665,7 @@ 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} when Opaque :: opaque_type(), Types :: #{string() => typedef()}, @@ -697,6 +708,7 @@ 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()}, @@ -715,6 +727,7 @@ annotate_variants([{Name, Elems} | Rest], Types, 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). @@ -809,6 +822,8 @@ 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} @@ -818,15 +833,15 @@ substitute_opaque_types(Bindings, Types) -> 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 +%% 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 +%% +%% 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 +%% 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) -> @@ -838,6 +853,7 @@ erlang_args_to_fate(VarTypes, Terms) -> 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(), @@ -845,7 +861,6 @@ erlang_args_to_fate(VarTypes, Terms) -> 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 @@ -1199,6 +1214,7 @@ combine_errors(Broken) -> lists:foldl(F, [], Broken). + %%% FATE to Erlang % Not sure if this is needed... fate_to_erlang shouldn't fail. @@ -1207,6 +1223,7 @@ coerce_direction(Type, Term, to_fate) -> coerce_direction(Type, Term, from_fate) -> fate_to_erlang(Type, Term). + -spec fate_to_erlang(Type, FATE) -> {ok, Erlang} | {error, Errors} when Type :: annotated_type(), FATE :: gmb_fate_data:fate_type(), @@ -1214,13 +1231,12 @@ coerce_direction(Type, Term, from_fate) -> Errors :: [{Reason, [PathStep]}], 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 +%% 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) -> @@ -1303,6 +1319,7 @@ opaque_type_to_iolist(N, _) -> io_lib:format("type ~p", [N]). + %%% AACI Getters -spec get_function_signature(AACI, Fun) -> {ok, Type} | {error, Reason} @@ -1310,14 +1327,13 @@ opaque_type_to_iolist(N, _) -> 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. +%% documentation for the `annotated_type/0' type. get_function_signature({aaci, _, FunDefs, _}, Fun) -> case maps:find(Fun, FunDefs) of diff --git a/src/hz_sophia.erl b/src/hz_sophia.erl index f81d377..2a9892e 100644 --- a/src/hz_sophia.erl +++ b/src/hz_sophia.erl @@ -53,7 +53,7 @@ parse_literal2(Result, Pos, String) -> %% @doc %% Parse an untyped Sophia expression into a FATE term -%% Like parse_literal/2, but will not produce type errors. This function can +%% Like `parse_literal/2', but will not produce type errors. This function can %% still produce parsing errors, and can produce errors when variants or %% records are encountered, since they can't be parsed unless you have type %% information. @@ -67,6 +67,7 @@ parse_literal2(Result, Pos, String) -> parse_literal(String) -> parse_literal(unknown_type(), String). + %%% Tokenizer -define(IS_LATIN_UPPER(C), (((C) >= $A) and ((C) =< $Z))). @@ -252,6 +253,8 @@ escape_char($\") -> "\\\""; escape_char($\\) -> "\\\\"; escape_char(I) -> I. + + %%% Sophia Literal Parser %%% This parser is a simple recursive descent parser, written explicitly in @@ -961,7 +964,7 @@ wrap_error(Reason, _) -> Reason. %% 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 +%% `fate_to_list/2' should be used whenever possible, especially since %% transaction results are type checked by nodes at runtime. fate_to_list(Term) -> @@ -975,7 +978,7 @@ fate_to_list(Term) -> %% @doc %% Print a FATE term from gmbytecode in Sophia syntax -%% Like fate_to_list/1, but now type information from the AACI data structure +%% Like `fate_to_list/1', but now type information from the AACI data structure %% can be provided, in order to correctly interpret types like records, %% variants, and unicode characters. If the type information you provide is %% incorrect for the FATE term provided, then the function will fall back to @@ -988,7 +991,7 @@ fate_to_list(Type, Term) -> %% @doc %% Print a FATE term in Sophia syntax, without concatenating -%% The fate_to_list/1 function builds an iolist, and then concatenates it into +%% The `fate_to_list/1' function builds an iolist, and then concatenates it into %% a list. If you are going to put the term into a bigger iolist directly %% after, or write it to a streaming device, then it can save effort and memory %% to just use the iolist directly. @@ -1007,7 +1010,7 @@ fate_to_iolist(Term) -> %% @doc %% Print a FATE term in Sophia syntax, without concatenating -%% Prints using type information, like fate_to_list/2, but without spending +%% Prints using type information, like `fate_to_list/2', but without spending %% time or memory concatenating the result into a list, like fate_to_iolist/1. % Special case for singleton records, since they are erased during compilation.