Add type information for hz_sophia

This one was much simpler to do than hz_aaci, since it doesn't introduce any new types.

I could have made note of some of the conventions, where a type can be represented in multiple
ways in Sophia syntax, or where these functions are actually more lenient than the compiler, but
it isn't as easy to break those notes up from the basic function usage, like it was in hz_aaci,
where those aforementioned new types are used.
This commit is contained in:
Jarvis Carroll
2026-05-19 12:21:17 +00:00
parent 23c13f607e
commit 9fc89c0c22
+69 -10
View File
@@ -4,26 +4,28 @@
-copyright("Jarvis Carroll <spiveehere@gmail.com>").
-license("GPL-3.0-or-later").
-export([parse_literal/1, parse_literal/2]).
-export([parse_literal/2, parse_literal/1]).
-export([fate_to_list/1, fate_to_list/2, fate_to_iolist/1, fate_to_iolist/2]).
-include_lib("eunit/include/eunit.hrl").
-spec parse_literal(Sophia) -> {ok, FATE} | {error, Reason}
when Sophia :: string(),
FATE :: gmb_fate_data:fate_type(),
Reason :: term().
parse_literal(String) ->
parse_literal(unknown_type(), String).
-spec parse_literal(Type, Sophia) -> {ok, FATE} | {error, Reason}
when Type :: hz_aaci:annotated_type(),
Sophia :: string(),
FATE :: gmb_fate_data:fate_type(),
Reason :: term().
%% @doc
%% Parse a typed Sophia expression into a FATE term
%% The Sophia expression must consist only of literals, thus making a 'Sophia
%% term', which means no arithmetic, no function calls, no variables, etc.
%% The FATE term is in the format that gmbytecode expects as input, for forming
%% contract calls, etc. Used by the hz module to implement the 'sophia' format.
%%
%% The function takes type information retrieved from the AACI data structure,
%% which is used to interpret record types and variant types, but is also used
%% to check inputs and generate errors.
parse_literal(Type, String) ->
case parse_expression(Type, {1, 1}, String) of
{ok, {Result, NewPos, NewString}} ->
@@ -43,6 +45,28 @@ parse_literal2(Result, Pos, String) ->
{error, Reason}
end.
-spec parse_literal(Sophia) -> {ok, FATE} | {error, Reason}
when Sophia :: string(),
FATE :: gmb_fate_data:fate_type(),
Reason :: term().
%% @doc
%% Parse an untyped Sophia expression into a FATE term
%% Like parse_literal/2, but will not produce type errors. This function can
%% still produce parsing errors, and can produce errors when variants or
%% records are encountered, since they can't be parsed unless you have type
%% information.
%%
%% Note that since records are implemented as tuples, if you are trying to call
%a function that you know takes a record, but you don't have type information
%% available in the context where the expression is being passed, then tuples
%% can be used instead. This does not work if you have type information,
%% though, as tuples and records are different Sophia/AACI types.
parse_literal(String) ->
parse_literal(unknown_type(), String).
%%% Tokenizer
-define(IS_LATIN_UPPER(C), (((C) >= $A) and ((C) =< $Z))).
@@ -927,6 +951,19 @@ wrap_error(Reason, _) -> Reason.
when FATE :: gmb_fate_data:fate_type(),
Sophia :: string().
%% @doc
%% Print a FATE term from gmbytecode in Sophia syntax
%% FATE terms usually come from using gmbytecode to decode the result of an
%% on-chain transaction.
%%
%% This function does not use any type information to interpret the data, and
%% so can make mistakes. It's okay for interpreting tuples, lists, maps,
%% integers, and strings, but it will misinterpret the types of records and
%% unicode characters, and will crash the process if variants are encountered.
%%
%% fate_to_list/2 should be used whenever possible, especially since
%% transaction results are type checked by nodes at runtime.
fate_to_list(Term) ->
fate_to_list(unknown_type(), Term).
@@ -935,10 +972,27 @@ fate_to_list(Term) ->
FATE :: gmb_fate_data:fate_type(),
Sophia :: string().
%% @doc
%% Print a FATE term from gmbytecode in Sophia syntax
%% Like fate_to_list/1, but now type information from the AACI data structure
%% can be provided, in order to correctly interpret types like records,
%% variants, and unicode characters. If the type information you provide is
%% incorrect for the FATE term provided, then the function will fall back to
%% untyped pretty printing like in fate_to_list/1, but this is not recommended,
%% as correct type information should always be available.
fate_to_list(Type, Term) ->
IOList = fate_to_iolist(Type, Term),
unicode:characters_to_list(IOList).
%% @doc
%% Print a FATE term in Sophia syntax, without concatenating
%% The fate_to_list/1 function builds an iolist, and then concatenates it into
%% a list. If you are going to put the term into a bigger iolist directly
%% after, or write it to a streaming device, then it can save effort and memory
%% to just use the iolist directly.
-spec fate_to_iolist(FATE) -> Sophia
when FATE :: gmb_fate_data:fate_type(),
Sophia :: iolist().
@@ -951,6 +1005,11 @@ fate_to_iolist(Term) ->
FATE :: gmb_fate_data:fate_type(),
Sophia :: iolist().
%% @doc
%% Print a FATE term in Sophia syntax, without concatenating
%% Prints using type information, like fate_to_list/2, but without spending
%% time or memory concatenating the result into a list, like fate_to_iolist/1.
% Special case for singleton records, since they are erased during compilation.
fate_to_iolist({_, _, {record, [{FieldName, FieldType}]}}, Term) ->
singleton_record_to_iolist(FieldName, FieldType, Term);