Doc formatting adjustments

This commit is contained in:
2026-05-27 21:49:30 +09:00
parent 29619f08b7
commit 75bc52ede3
3 changed files with 305 additions and 285 deletions
+3 -2
View File
@@ -1,4 +1,5 @@
@author Craig Everett <craigeverett@qpq.swiss> [https://git.qpq.swiss/QPQ-AG/hakuzaru] @author Craig Everett <craigeverett@qpq.swiss> [https://zxq9.com]
@author Jarvis Carrol <jarviscarrol@qpq.swiss> [https://jarviscarroll.net/]
@version 0.9.2 @version 0.9.2
@title Hakuzaru: Gajumaru blockchain bindings for Erlang @title Hakuzaru: Gajumaru blockchain bindings for Erlang
@@ -21,7 +22,7 @@ After startup `hz_man' must be given the address and port of a list of Gajumaru
Note that the service nodes will need to have the dry-run endpoint enabled and the internal service query port made available in order to provide dry-runs and transaction submission. Note that the service nodes will need to have the dry-run endpoint enabled and the internal service query port made available in order to provide dry-runs and transaction submission.
When configuring chain nodes a list of nodes should be provided. When configuring chain nodes a list of nodes should be provided.
To avoid sync issues in the case of fast transaction formation/submission to the chain, only one node from the list of chain nodes is used for submitting transactions and querying `next_nonce/1`. To avoid sync issues in the case of fast transaction formation/submission to the chain, only one node from the list of chain nodes is used for submitting transactions and querying `next_nonce/1'.
This node is called "the sticky node". This node is called "the sticky node".
The first node in the list of chain nodes provided during configuration is designated as the sticky node. The first node in the list of chain nodes provided during configuration is designated as the sticky node.
+277 -261
View File
@@ -29,21 +29,6 @@
-include_lib("eunit/include/eunit.hrl"). -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() -type erlang_repr() :: erlang_repr_int()
| erlang_repr_address() | erlang_repr_address()
| erlang_repr_contract() | erlang_repr_contract()
@@ -58,6 +43,19 @@
| erlang_repr_tuple() | erlang_repr_tuple()
| erlang_repr_variant() | erlang_repr_variant()
| erlang_repr_record(). | 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() %-type erlang_repr() :: integer()
@@ -68,240 +66,231 @@
%| [erlang_repr()] %| [erlang_repr()]
%| #{erlang_repr() => 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(). -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>>}. -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>>}. -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>>}. -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(). -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(). -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(). -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(). -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(). -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()]. -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()}. -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()}. -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()}}. -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>
%% @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()}. -type function_spec() :: {[{string(), annotated_type()}], annotated_type()}.
%% @doc % The fully annotated spec of a contract entrypoint, for fast call formation
%% A fully annotated Sophia type % The first term is a list of parameter names and their types, as expected by
%% Sophia allows for arbitrary nesting of type aliases, each with parameters, % `erlang_args_to_fate/2', and the second term is a single type, as expected by
%% and each potentially substituting for another arbitrarily complex type % `fate_to_erlang/2'. See annotated_type/0 for the details of how these types
%% alias, so there is a potentially indefinite amount of work converting the % are represented and why, but for most purposes it is fine to just store and
%% type `my_type_alias` as it would appear in Sophia/in the ACI, into the % pass these type terms around without looking at their contents.
%% actual variant/record/list/map/tuple type expression that it ultimately
%% represents. To overcome this, we 'annotate' a type, recording what its
%% aliased name was, along with its actual definition.
%%
%% Normally you can extract the annotated types from a function_spec(), and
%% pass them into the conversion function that needs them, but it can also be
%% useful to walk through the annotated types yourself. Confusingly, if you
%% want to recursively descend down an annotated type, you want to recurse on
%% the third element in the tuple, not the first two, as the first two
%% represent incomplete levels of normalization, which can be more descriptive
%% for users, but aren't as actionable as the fully normalized third element.
%%
%% Despite the third term being the most important, it is kept at the end,
%% because that is what is most memorable, since each element of the triple is
%% more normalized than the last, and because that is what is easiest to read,
%% since the third term is usually an explosion of nested braces and brackets,
%% making anything written after it basically unreadable.
%%
%% If you look at examples of annotated types produced in your own programs,
%% you will tend to see things like {integer, alread_normalized, integer},
%% making it even less clear that the third element is the important one, or
%% why that is. For some fairly simple but informative examples, consider these
%% type aliases:
%% contract C =
%% record my_record('t) = {x: 't, y: 't}
%% type my_alias1 = int
%% type my_alias2 = list(my_alias1)
%% type my_alias3 = my_record(my_alias1)
%% If these type aliases appeared in a function spec, the AACI would represent
%% them as the following annotated types:
%% {"my_alias1", integer, integer}
%% {"my_alias2", {list, ["my_alias1"]}, {list, [{"my_alias1", integer, integer}]}}
%% {"my_alias3", {"my_record", ["my_alias1"]}, {record, [{"x", {"my_alias1", integer, integer}}, {"y", {"my_alias1", integer, integer}}]}}
%%
%% The first term is the type roughly as it appeared in the ACI, see
%% opaque_type/0 for more information.
%%
%% The second term is that same type but 'head normalized', chasing type
%% aliases iteratively, until it is some built in type like an integer, or some
%% user-defined record type or ADT. If the alias reduces to a list or map or
%% tuple with more aliased types nested inside, these nested type
%% subexpressions are not normalized any further, as the 'list' or 'map'
%% connective is considered the 'head' of the type expression, and is
%% normalized. Record type names and ADT names are not considered aliases, and
%% so are considered head normalized, but both can take parameters, which can
%% also stay un-normalized, as with lists or maps. If the head normalized type
%% is the same as the opaque type, then the atom 'already_normalized' is placed
%% instead, as a hint that instead of printing messages like
%% "my_alias1 (i.e. int)", a simple message like "list(my_record)" will do.
%%
%% The third term is the head normalized type with two changes, first, record
%% and variant definitions are subtituted in as well, giving a list of field
%% names or constructor names in full, and second, each subexpression is
%% recursively annotated, meaning its opaque, head-normalized, and fully
%% normalized parts also appear as triples.
-type annotated_type() :: {opaque_type(), already_normalized | opaque_type(), annotated_type_body()}. -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.
%% @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]} -type builtin_type(T) :: {bytes, [integer() | any]}
| {tuple, [T]} | {tuple, [T]}
@@ -317,56 +306,68 @@
| contract | contract
| channel | channel
| unknown_type. | 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 -type user_defined_type(T) :: {record, [{string(), T}]}
%% An opaque type as it originally appeared in a function spec. | {variant, [{string(), [T]}]}.
%% The Sophia compiler may have a different representation for these type % The connectives for defining new records and ADTs.
%% expressions, but we make a simple representation here as well. % Record types and ADTs can both appear in the original type definitions in
%% These type expressions are really function applications, in a limited sort % the body of a contract, as well as in the recursively normalized 'annotated
%% of rewrite calculus without higher order functions. After performing some % types' that the AACI stores. We use the same layout in both cases.
%% 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 -type opaque_type() :: string()
%% all % yet. | {string(), [opaque_type()]}
-type opaque_type() :: string() | {string(), [opaque_type()]} | builtin_type(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.
%% @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()} -type typedef_expression() :: {var, string()}
| string() | string()
| {string(), [typedef_expression()]} | {string(), [typedef_expression()]}
| builtin_type(typedef_expression()). | builtin_type(typedef_expression()).
%% @doc % The recursive type expressions that can appear in the definitions of type aliases.
%% A type definition as it appears in the AACI. % Similar to opaque_type(), but type aliases can take parameters as well,
%% A type definition has a list of parameter names, and then some body defined % which means those parameters can also appear anywhere within the recursive
%% using builtin type connectives, other defined types, and those parameters. % type expression that defines the type alias.
-type typedef() :: {[string()], typedef_body()}.
-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 %%% ACI/AACI
@@ -389,7 +390,6 @@ prepare_from_file(Path) ->
-spec prepare(ACI) -> AACI -spec prepare(ACI) -> AACI
when ACI :: term(), when ACI :: term(),
AACI :: aaci(). AACI :: aaci().
%% @doc %% @doc
%% Convert the ACI structure produced by the compiler into the AACI format used by Hakuzaru %% 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. %% See the documentation for the aaci/0 type for more information.
@@ -409,9 +409,9 @@ prepare(ACI) ->
% make error messages easier to understand. % make error messages easier to understand.
InternalTypeDefs = maps:merge(builtin_typedefs(), TypeDefs), InternalTypeDefs = maps:merge(builtin_typedefs(), TypeDefs),
Specs = annotate_function_specs(OpaqueSpecs, InternalTypeDefs, #{}), Specs = annotate_function_specs(OpaqueSpecs, InternalTypeDefs, #{}),
{aaci, Name, Specs, TypeDefs}. {aaci, Name, Specs, TypeDefs}.
-spec convert_aci_types(ACI) -> {Name, OpaqueSpecs, TypeDefs} -spec convert_aci_types(ACI) -> {Name, OpaqueSpecs, TypeDefs}
when ACI :: term(), when ACI :: term(),
Name :: string(), Name :: string(),
@@ -442,17 +442,20 @@ convert_aci_types(ACI) ->
% just pre-compute and acceleration. % just pre-compute and acceleration.
{Name, Specs, TypeDefMap}. {Name, Specs, TypeDefMap}.
convert_function_spec(#{name := NameBin, arguments := Args, returns := Result}) -> convert_function_spec(#{name := NameBin, arguments := Args, returns := Result}) ->
Name = binary_to_list(NameBin), Name = binary_to_list(NameBin),
ArgTypes = lists:map(fun convert_arg/1, Args), ArgTypes = lists:map(fun convert_arg/1, Args),
ResultType = opaque_type([], Result), ResultType = opaque_type([], Result),
{Name, ArgTypes, ResultType}. {Name, ArgTypes, ResultType}.
convert_arg(#{name := NameBin, type := TypeDef}) -> convert_arg(#{name := NameBin, type := TypeDef}) ->
Name = binary_to_list(NameBin), Name = binary_to_list(NameBin),
Type = opaque_type([], TypeDef), Type = opaque_type([], TypeDef),
{Name, Type}. {Name, Type}.
convert_namespace_typedefs(#{namespace := NS}) -> convert_namespace_typedefs(#{namespace := NS}) ->
Name = namespace_name(NS), Name = namespace_name(NS),
convert_typedefs(NS, Name); convert_typedefs(NS, Name);
@@ -486,12 +489,14 @@ convert_typedefs_loop([Next | Rest], NamePrefix, Converted) ->
Def = opaque_type(Params, DefACI), Def = opaque_type(Params, DefACI),
convert_typedefs_loop(Rest, NamePrefix, [Converted, {Name, Params, Def}]). convert_typedefs_loop(Rest, NamePrefix, [Converted, {Name, Params, Def}]).
-spec collect_opaque_types(Tree, TypeDefs) -> TypeDefs -spec collect_opaque_types(Tree, TypeDefs) -> TypeDefs
when Tree :: typedef_tree(), when Tree :: typedef_tree(),
TypeDefs :: #{string() => typedef()}. TypeDefs :: #{string() => typedef()}.
-type typedef_tree() :: {string(), [string()], typedef_body()} | list(typedef_tree()). -type typedef_tree() :: {string(), [string()], typedef_body()} | list(typedef_tree()).
collect_opaque_types([], Types) -> collect_opaque_types([], Types) ->
Types; Types;
collect_opaque_types([L | R], Types) -> collect_opaque_types([L | R], Types) ->
@@ -500,15 +505,17 @@ collect_opaque_types([L | R], Types) ->
collect_opaque_types({Name, Params, Def}, Types) -> collect_opaque_types({Name, Params, Def}, Types) ->
maps:put(Name, {Params, Def}, Types). maps:put(Name, {Params, Def}, Types).
%%% ACI Type -> Opaque Type %%% ACI Type -> Opaque Type
-spec opaque_type(Params, ACIType) -> Opaque -spec opaque_type(Params, ACIType) -> Opaque
when Params :: [string()], when Params :: [string()],
ACIType :: binary() | map(), ACIType :: binary() | map(),
Opaque :: opaque_type(). Opaque :: opaque_type().
% Convert an ACI type defintion/spec into the 'opaque type' representation that % Convert an ACI type defintion/spec into the 'opaque type' representation that
% our dereferencing algorithms can reason about. % our dereferencing algorithms can reason about.
opaque_type(Params, NameBin) when is_binary(NameBin) -> opaque_type(Params, NameBin) when is_binary(NameBin) ->
Name = opaque_type_name(NameBin), Name = opaque_type_name(NameBin),
case not is_atom(Name) and lists:member(Name, Params) of 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), [{Name, TypeArgs}] = maps:to_list(Pair),
{opaque_type_name(Name), [opaque_type(Params, Arg) || Arg <- TypeArgs]}. {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. % Atoms for any builtins that aren't qualified by a namespace in Sophia.
% Everything else stays as a string, user-defined or not. % Everything else stays as a string, user-defined or not.
opaque_type_name(<<"int">>) -> integer; opaque_type_name(<<"int">>) -> integer;
opaque_type_name(<<"bool">>) -> boolean; opaque_type_name(<<"bool">>) -> boolean;
opaque_type_name(<<"bits">>) -> bits; opaque_type_name(<<"bits">>) -> bits;
@@ -553,6 +561,7 @@ opaque_type_name(<<"map">>) -> map;
opaque_type_name(<<"channel">>) -> channel; opaque_type_name(<<"channel">>) -> channel;
opaque_type_name(Name) -> binary_to_list(Name). opaque_type_name(Name) -> binary_to_list(Name).
builtin_typedefs() -> builtin_typedefs() ->
#{"unit" => {[], {tuple, []}}, #{"unit" => {[], {tuple, []}},
"void" => {[], {variant, []}}, "void" => {[], {variant, []}},
@@ -610,14 +619,15 @@ builtin_typedefs() ->
"MCL_BLS12_381.fp" => {[], {bytes, [48]}} "MCL_BLS12_381.fp" => {[], {bytes, [48]}}
}. }.
%%% Opaque Type -> Accelerated 'Annotated' Type %%% Opaque Type -> Accelerated 'Annotated' Type
% Type preparation has two goals. First, we need a data structure that can be % 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 % traversed quickly, to take sophia-esque erlang expressions and turn them into
% fate-esque erlang expressions that gmbytecode can serialize. Second, we need % fate-esque erlang expressions that gmbytecode can serialize. Second, we need
% partially substituted names, so that error messages can be generated for why % 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 % "foobar" is not valid as the third field of a `bazquux', because the third
% field is supposed to be `option(integer)`, not `string`. % field is supposed to be `option(integer)', not `string'.
% %
% To achieve this we need three representations of each type expression, which % To achieve this we need three representations of each type expression, which
% together form an 'annotated type'. First, we need the fully opaque name, % 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 % 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 % 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 % can simply render the normalized type expression and know that the error will
% make sense. % make sense.
@@ -655,6 +665,7 @@ annotate_function_specs([{Name, ArgsOpaque, ResultOpaque} | Rest], Types, Specs)
NewSpecs = maps:put(Name, {Args, Result}, Specs), NewSpecs = maps:put(Name, {Args, Result}, Specs),
annotate_function_specs(Rest, Types, NewSpecs). annotate_function_specs(Rest, Types, NewSpecs).
-spec annotate_type(Opaque, Types) -> {ok, Annotated} -spec annotate_type(Opaque, Types) -> {ok, Annotated}
when Opaque :: opaque_type(), when Opaque :: opaque_type(),
Types :: #{string() => typedef()}, Types :: #{string() => typedef()},
@@ -697,6 +708,7 @@ annotate_type_subexpressions({T, ElemsOpaque}, Types) ->
{ok, Elems} = annotate_types(ElemsOpaque, Types, []), {ok, Elems} = annotate_types(ElemsOpaque, Types, []),
{ok, {T, Elems}}. {ok, {T, Elems}}.
-spec annotate_bindings(Bindings, Types, Acc) -> {ok, Annotated} -spec annotate_bindings(Bindings, Types, Acc) -> {ok, Annotated}
when Bindings :: [{string(), opaque_type()}], when Bindings :: [{string(), opaque_type()}],
Types :: #{string() => typedef()}, Types :: #{string() => typedef()},
@@ -715,6 +727,7 @@ annotate_variants([{Name, Elems} | Rest], Types, Acc) ->
annotate_variants([], _Types, Acc) -> annotate_variants([], _Types, Acc) ->
{ok, lists:reverse(Acc)}. {ok, lists:reverse(Acc)}.
% This function evaluates type aliases in a loop, until eventually a usable % This function evaluates type aliases in a loop, until eventually a usable
% definition is found. % definition is found.
normalize_opaque_type(T, Types) -> normalize_opaque_type(T, Types, true). 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, Each = fun(Type) -> substitute_opaque_type(Bindings, Type) end,
lists:map(Each, Types). lists:map(Each, Types).
%%% Erlang to FATE %%% Erlang to FATE
-spec erlang_args_to_fate(VarTypes, Terms) -> {ok, FATE} | {error, Errors} -spec erlang_args_to_fate(VarTypes, Terms) -> {ok, FATE} | {error, Errors}
@@ -818,15 +833,15 @@ substitute_opaque_types(Bindings, Types) ->
Errors :: [{Reason, [PathStep]}], Errors :: [{Reason, [PathStep]}],
Reason :: term(), Reason :: term(),
PathStep :: term(). PathStep :: term().
%% @doc %% @doc
%% Call erlang_to_fate/2 on a list of named values. %% 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. %% 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 %% 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 %% 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. %% are used to provide slightly more informative errors.
erlang_args_to_fate(VarTypes, Terms) -> erlang_args_to_fate(VarTypes, Terms) ->
@@ -838,6 +853,7 @@ erlang_args_to_fate(VarTypes, Terms) ->
DefLength < ArgLength -> {error, too_many_args} DefLength < ArgLength -> {error, too_many_args}
end. end.
-spec erlang_to_fate(Type, Erlang) -> {ok, FATE} | {error, Errors} -spec erlang_to_fate(Type, Erlang) -> {ok, FATE} | {error, Errors}
when Type :: annotated_type(), when Type :: annotated_type(),
FATE :: gmb_fate_data:fate_type(), FATE :: gmb_fate_data:fate_type(),
@@ -845,7 +861,6 @@ erlang_args_to_fate(VarTypes, Terms) ->
Errors :: [{Reason, [PathStep]}], Errors :: [{Reason, [PathStep]}],
Reason :: term(), Reason :: term(),
PathStep :: term(). PathStep :: term().
%% @doc %% @doc
%% Convert one Sophia-flavored Erlang term into one FATE-flavored Erlang terms. %% 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 %% 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). lists:foldl(F, [], Broken).
%%% FATE to Erlang %%% FATE to Erlang
% Not sure if this is needed... fate_to_erlang shouldn't fail. % 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) -> coerce_direction(Type, Term, from_fate) ->
fate_to_erlang(Type, Term). fate_to_erlang(Type, Term).
-spec fate_to_erlang(Type, FATE) -> {ok, Erlang} | {error, Errors} -spec fate_to_erlang(Type, FATE) -> {ok, Erlang} | {error, Errors}
when Type :: annotated_type(), when Type :: annotated_type(),
FATE :: gmb_fate_data:fate_type(), FATE :: gmb_fate_data:fate_type(),
@@ -1214,13 +1231,12 @@ coerce_direction(Type, Term, from_fate) ->
Errors :: [{Reason, [PathStep]}], Errors :: [{Reason, [PathStep]}],
Reason :: term(), Reason :: term(),
PathStep :: term(). PathStep :: term().
%% @doc %% @doc
%% Convert a FATE-flavored Erlang term into a Sophia-flavored Erlang term %% 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 %% 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 %% 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' %% 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 %% format now. See the documentation of the `erlang_repr/0' type for more
%% information. %% information.
fate_to_erlang({_, _, integer}, S) when is_integer(S) -> fate_to_erlang({_, _, integer}, S) when is_integer(S) ->
@@ -1303,6 +1319,7 @@ opaque_type_to_iolist(N, _) ->
io_lib:format("type ~p", [N]). io_lib:format("type ~p", [N]).
%%% AACI Getters %%% AACI Getters
-spec get_function_signature(AACI, Fun) -> {ok, Type} | {error, Reason} -spec get_function_signature(AACI, Fun) -> {ok, Type} | {error, Reason}
@@ -1310,14 +1327,13 @@ opaque_type_to_iolist(N, _) ->
Fun :: binary() | string(), Fun :: binary() | string(),
Type :: {term(), term()}, % FIXME Type :: {term(), term()}, % FIXME
Reason :: bad_fun_name. Reason :: bad_fun_name.
%% @doc %% @doc
%% Extract the type information for a particular function from the AACI %% Extract the type information for a particular function from the AACI
%% If you want to manually convert a FATE result into the Sophia-flavored %% 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 %% 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 %% contract call yourself, this function gives you all of the annotated types
%% associated with a contract entrypoint. For more information, see the %% 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) -> get_function_signature({aaci, _, FunDefs, _}, Fun) ->
case maps:find(Fun, FunDefs) of case maps:find(Fun, FunDefs) of
+8 -5
View File
@@ -53,7 +53,7 @@ parse_literal2(Result, Pos, String) ->
%% @doc %% @doc
%% Parse an untyped Sophia expression into a FATE term %% Parse an untyped Sophia expression into a FATE term
%% Like parse_literal/2, but will not produce type errors. This function can %% Like `parse_literal/2', but will not produce type errors. This function can
%% still produce parsing errors, and can produce errors when variants or %% still produce parsing errors, and can produce errors when variants or
%% records are encountered, since they can't be parsed unless you have type %% records are encountered, since they can't be parsed unless you have type
%% information. %% information.
@@ -67,6 +67,7 @@ parse_literal2(Result, Pos, String) ->
parse_literal(String) -> parse_literal(String) ->
parse_literal(unknown_type(), String). parse_literal(unknown_type(), String).
%%% Tokenizer %%% Tokenizer
-define(IS_LATIN_UPPER(C), (((C) >= $A) and ((C) =< $Z))). -define(IS_LATIN_UPPER(C), (((C) >= $A) and ((C) =< $Z))).
@@ -252,6 +253,8 @@ escape_char($\") -> "\\\"";
escape_char($\\) -> "\\\\"; escape_char($\\) -> "\\\\";
escape_char(I) -> I. escape_char(I) -> I.
%%% Sophia Literal Parser %%% Sophia Literal Parser
%%% This parser is a simple recursive descent parser, written explicitly in %%% This parser is a simple recursive descent parser, written explicitly in
@@ -961,7 +964,7 @@ wrap_error(Reason, _) -> Reason.
%% integers, and strings, but it will misinterpret the types of records and %% integers, and strings, but it will misinterpret the types of records and
%% unicode characters, and will crash the process if variants are encountered. %% unicode characters, and will crash the process if variants are encountered.
%% %%
%% fate_to_list/2 should be used whenever possible, especially since %% `fate_to_list/2' should be used whenever possible, especially since
%% transaction results are type checked by nodes at runtime. %% transaction results are type checked by nodes at runtime.
fate_to_list(Term) -> fate_to_list(Term) ->
@@ -975,7 +978,7 @@ fate_to_list(Term) ->
%% @doc %% @doc
%% Print a FATE term from gmbytecode in Sophia syntax %% Print a FATE term from gmbytecode in Sophia syntax
%% Like fate_to_list/1, but now type information from the AACI data structure %% Like `fate_to_list/1', but now type information from the AACI data structure
%% can be provided, in order to correctly interpret types like records, %% can be provided, in order to correctly interpret types like records,
%% variants, and unicode characters. If the type information you provide is %% variants, and unicode characters. If the type information you provide is
%% incorrect for the FATE term provided, then the function will fall back to %% incorrect for the FATE term provided, then the function will fall back to
@@ -988,7 +991,7 @@ fate_to_list(Type, Term) ->
%% @doc %% @doc
%% Print a FATE term in Sophia syntax, without concatenating %% Print a FATE term in Sophia syntax, without concatenating
%% The fate_to_list/1 function builds an iolist, and then concatenates it into %% The `fate_to_list/1' function builds an iolist, and then concatenates it into
%% a list. If you are going to put the term into a bigger iolist directly %% a list. If you are going to put the term into a bigger iolist directly
%% after, or write it to a streaming device, then it can save effort and memory %% after, or write it to a streaming device, then it can save effort and memory
%% to just use the iolist directly. %% to just use the iolist directly.
@@ -1007,7 +1010,7 @@ fate_to_iolist(Term) ->
%% @doc %% @doc
%% Print a FATE term in Sophia syntax, without concatenating %% Print a FATE term in Sophia syntax, without concatenating
%% Prints using type information, like fate_to_list/2, but without spending %% Prints using type information, like `fate_to_list/2', but without spending
%% time or memory concatenating the result into a list, like fate_to_iolist/1. %% time or memory concatenating the result into a list, like fate_to_iolist/1.
% Special case for singleton records, since they are erased during compilation. % Special case for singleton records, since they are erased during compilation.