Compare commits

..

2 Commits

Author SHA1 Message Date
Jarvis Carroll
057861e9cf Add a map for builtin types
This makes it much easier to implement all these standard library
things.

In doing so I changed the convention for option, hash, unit, to be
stringy rather than atoms.

Also I changed some error messages based on what was more helpful during
debugging of the unit tests.
2025-09-24 16:27:32 +10:00
Jarvis Carroll
7ffc96b68a Refactor type normalization
Some of these checks were redundant, and we probably don't actually need
substitution to wrap success/failure, since it isn't expected to fail
anyway... Now the logic is much simpler, and adding more built-in type
definitions should be easy.
2025-09-24 14:10:29 +10:00

View File

@ -1419,7 +1419,8 @@ prepare_aaci(ACI) ->
% down to the concrete types they actually represent. We annotate each
% subexpression of this concrete type with other info too, in case it helps
% make error messages easier to understand.
Specs = annotate_function_specs(OpaqueSpecs, TypeDefs, #{}),
InternalTypeDefs = maps:merge(builtin_typedefs(), TypeDefs),
Specs = annotate_function_specs(OpaqueSpecs, InternalTypeDefs, #{}),
{aaci, Name, Specs, TypeDefs}.
@ -1526,34 +1527,39 @@ opaque_type(Params, Pair) when is_map(Pair) ->
[{Name, TypeArgs}] = maps:to_list(Pair),
{opaque_type_name(Name), [opaque_type(Params, Arg) || Arg <- TypeArgs]}.
% Atoms for builtins, strings (lists) for user-defined types.
%
% There are some magic built in types that may or may not also need atoms to
% represent them, and may or may not need to be handled explicitly in
% coerce/3, if we can't flatten them directly
%
% These types represent some FATE variant:
% Chain.ttl, AENS.pointee, AENS.name, AENSv2.pointee, AENSv2.name,
% Chain.ga_meta_tx, Chain.paying_for_tx, Chain.base_tx,
%
% And then MCL_BLS12_381.fr represent bytes(32), and MCL_BLS12_381.fp
% represents bytes(48).
% Atoms for any builtins that aren't qualified by a namespace in Sophia.
% Everything else stays as a string, user-defined or not.
opaque_type_name(<<"int">>) -> integer;
opaque_type_name(<<"bool">>) -> boolean;
opaque_type_name(<<"bits">>) -> bits;
opaque_type_name(<<"char">>) -> char;
opaque_type_name(<<"string">>) -> string;
opaque_type_name(<<"address">>) -> address;
opaque_type_name(<<"hash">>) -> hash;
opaque_type_name(<<"signature">>) -> signature;
opaque_type_name(<<"contract">>) -> contract;
opaque_type_name(<<"list">>) -> list;
opaque_type_name(<<"map">>) -> map;
opaque_type_name(<<"option">>) -> option;
opaque_type_name(<<"name">>) -> name;
% I'm not sure how to produce channels in Sophia source, but they seem to exist
% in gmb still.
opaque_type_name(<<"channel">>) -> channel;
opaque_type_name(Name) -> binary_to_list(Name).
builtin_typedefs() ->
#{"unit" => {[], {tuple, []}},
"hash" => {[], {bytes, [32]}},
"option" => {["'T"], {variant, [{"None", []},
{"Some", [{var, "'T"}]}]}},
"Chain.ttl" => {[], {variant, [{"FixedTTL", [{list, [integer]}]},
{"RelativeTTL", [{list, [integer]}]}]}},
"AENS.pointee" => {[], {variant, [{"AccountPt", [{list, [address]}]},
{"OraclePt", [{list, [address]}]},
{"ContractPt", [{list, [address]}]},
{"ChannelPt", [{list, [address]}]}]}},
"AENS.name" => {[], {variant, [{"Name", [address,
"Chain.ttl",
{map, [string, "AENS.pointee"]}]}]}}
}.
% 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
@ -1595,6 +1601,10 @@ annotate_type(T, Types) ->
Error
end.
annotate_type2(T, _, _, unknown_type, _) ->
% If a type is unknown, then it should not be reported as the normalized
% name.
{ok, {T, unknown_type, unknown_type}};
annotate_type2(T, AlreadyNormalized, NOpaque, NExpanded, Types) ->
case annotate_type_subexpressions(NExpanded, Types) of
{ok, Flat} ->
@ -1652,129 +1662,105 @@ annotate_variants([{Name, Elems} | Rest], Types, Acc) ->
annotate_variants([], _Types, Acc) ->
{ok, lists:reverse(Acc)}.
normalize_opaque_type(T, Types) ->
case type_is_expanded(T) of
false -> normalize_opaque_type(T, Types, true);
true -> {ok, true, T, T}
end.
% This function evaluates type aliases in a loop, until eventually a usable
% definition is found.
%
% It also evaluates built-in and standard library types such as options and
% names, to their defined variant representation, as well as evaluating
% certain binary types like hash, fp, and fr, into their byte representations.
normalize_opaque_type(T, Types) -> normalize_opaque_type(T, Types, true).
% FIXME detect infinite loops
% FIXME detect builtins with the wrong number of arguments
% FIXME should nullary types have an empty list of arguments added before now?
normalize_opaque_type({option, [T]}, _Types, IsFirst) ->
% Just like user-made ADTs, 'option' is considered part of the type, and so
% options are considered normalised.
{ok, IsFirst, {option, [T]}, {variant, [{"None", []}, {"Some", [T]}]}};
normalize_opaque_type(hash, _Types, IsFirst) ->
% For coercion purposes, hash is indistinguishable from bytes(32), so we
% treat it like a type alias.
{ok, IsFirst, hash, {bytes, [32]}};
% These types represent some FATE variant:
% Chain.ttl, AENS.pointee, AENS.name, AENSv2.pointee, AENSv2.name,
% Chain.ga_meta_tx, Chain.paying_for_tx, Chain.base_tx,
%
% And then MCL_BLS12_381.fr represent bytes(32), and MCL_BLS12_381.fp
% represents bytes(48).
normalize_opaque_type(T, _Types, IsFirst) when is_atom(T) ->
% Once we have eliminated the above rewrite cases, all other cases are
% handled explicitly by the coerce logic, and so are considered normalized.
{ok, IsFirst, T, T};
normalize_opaque_type(Type = {T, _}, _Types, IsFirst) when is_atom(T) ->
% Once we have eliminated the above rewrite cases, all other cases are
% handled explicitly by the coerce logic, and so are considered normalized.
{ok, IsFirst, Type, Type};
normalize_opaque_type(T, Types, IsFirst) when is_list(T) ->
% Lists/strings indicate userspace types, which may require arg
% substitutions. Convert to an explicit but empty arg list, for uniformity.
normalize_opaque_type({T, []}, Types, IsFirst);
normalize_opaque_type({T, TypeArgs}, Types, IsFirst) when is_list(T) ->
case maps:find(T, Types) of
%{error, invalid_aci}; % FIXME more info
error ->
{ok, IsFirst, {T, TypeArgs}, {unknown_type, TypeArgs}};
% We couldn't find this named type... Keep building the AACI, but
% mark this type expression as unknown, so that FATE coercions
% aren't attempted.
{ok, IsFirst, {T, TypeArgs}, unknown_type};
{ok, {TypeParamNames, Definition}} ->
Bindings = lists:zip(TypeParamNames, TypeArgs),
normalize_opaque_type2(T, TypeArgs, Types, IsFirst, Bindings, Definition)
% We have a definition for this type, including names for whatever
% args we have been given. Subtitute our args into this.
NewType = substitute_opaque_type(TypeParamNames, Definition, TypeArgs),
% Now continue on to see if we need to restart the loop or not.
normalize_opaque_type2(IsFirst, {T, TypeArgs}, NewType, Types)
end.
normalize_opaque_type2(T, TypeArgs, Types, IsFirst, Bindings, Definition) ->
SubResult =
case Bindings of
[] -> {ok, Definition};
_ -> substitute_opaque_type(Bindings, Definition)
end,
case SubResult of
% Type names were already normalized if they were ADTs or records,
% since for those connectives the name is considered part of the type.
{ok, NextT = {variant, _}} ->
{ok, IsFirst, {T, TypeArgs}, NextT};
{ok, NextT = {record, _}} ->
{ok, IsFirst, {T, TypeArgs}, NextT};
% Everything else has to be substituted down to a built-in connective
% to be considered normalized.
{ok, NextT} ->
normalize_opaque_type3(NextT, Types);
Error ->
Error
end.
normalize_opaque_type2(IsFirst, PrevType, NextType = {variant, _}, _) ->
% We have reduced to a variant. Report the type name as the normalized
% type, but also provide the variant definition itself as the candidate
% flattened type for further annotation.
{ok, IsFirst, PrevType, NextType};
normalize_opaque_type2(IsFirst, PrevType, NextType = {record, _}, _) ->
% We have reduced to a record. Report the type name as the normalized
% type, but also provide the record definition itself as the candidate
% flattened type for further annotation.
{ok, IsFirst, PrevType, NextType};
normalize_opaque_type2(_, _, NextType, Types) ->
% Not a variant or record yet, so go back to the start of the loop.
% It will no longer be the first iteration.
normalize_opaque_type(NextType, Types, false).
% while this does look like normalize_opaque_type/2, it sets IsFirst to false
% instead of true, and is part of the loop, instead of being an initial
% condition for the loop.
normalize_opaque_type3(NextT, Types) ->
case type_is_expanded(NextT) of
false -> normalize_opaque_type(NextT, Types, false);
true -> {ok, false, NextT, NextT}
end.
% Perform a beta-reduction on a type expression.
substitute_opaque_type([], Definition, _) ->
% There are no parameters to substitute. This is the simplest way of
% defining type aliases, records, and variants, so we should make sure to
% short circuit all the recursive descent logic, since it won't actually
% do anything.
Definition;
substitute_opaque_type(TypeParamNames, Definition, TypeArgs) ->
% Bundle the param names alongside the args that we want to substitute, so
% that we can keyfind the one list.
Bindings = lists:zip(TypeParamNames, TypeArgs),
substitute_opaque_type(Bindings, Definition).
% Strings indicate names that should be substituted. Atoms indicate built in
% types, which don't need to be expanded, except for option.
% TODO: Stop calling this, so that we can stop redundantly enumerating all the
% built in types.
type_is_expanded({option, _}) -> false;
type_is_expanded(hash) -> false;
type_is_expanded(X) when is_atom(X) -> true;
type_is_expanded({X, _}) when is_atom(X) -> true;
type_is_expanded(_) -> false.
% Skip traversal if there is nothing to substitute. This will often be the
% most common case.
substitute_opaque_type(Bindings, {var, VarName}) ->
case lists:keyfind(VarName, 1, Bindings) of
false -> {error, invalid_aci};
{_, TypeArg} -> {ok, TypeArg}
end;
substitute_opaque_type(Bindings, {variant, Args}) ->
case substitute_variant_types(Bindings, Args, []) of
{ok, Result} -> {ok, {variant, Result}};
Error -> Error
end;
substitute_opaque_type(Bindings, {record, Args}) ->
case substitute_record_types(Bindings, Args, []) of
{ok, Result} -> {ok, {record, Result}};
Error -> Error
{_, TypeArg} -> TypeArg;
% No valid ACI will create this case. Regardless, the user should
% still be able to specify arbitrary gmb FATE terms for whatever this
% is meant to be.
false -> unknown_type
end;
substitute_opaque_type(Bindings, {variant, Variants}) ->
Each = fun({VariantName, Elements}) ->
NewElements = substitute_opaque_types(Bindings, Elements),
{VariantName, NewElements}
end,
NewVariants = lists:map(Each, Variants),
{variant, NewVariants};
substitute_opaque_type(Bindings, {record, Fields}) ->
Each = fun({FieldName, FieldType}) ->
NewType = substitute_opaque_type(Bindings, FieldType),
{FieldName, NewType}
end,
NewFields = lists:map(Each, Fields),
{record, NewFields};
substitute_opaque_type(Bindings, {Connective, Args}) ->
case substitute_opaque_types(Bindings, Args, []) of
{ok, Result} -> {ok, {Connective, Result}};
Error -> Error
end;
NewArgs = substitute_opaque_types(Bindings, Args),
{Connective, NewArgs};
substitute_opaque_type(_Bindings, Type) ->
{ok, Type}.
Type.
substitute_variant_types(Bindings, [{VariantName, Elements} | Rest], Acc) ->
case substitute_opaque_types(Bindings, Elements, []) of
{ok, Result} -> substitute_variant_types(Bindings, Rest, [{VariantName, Result} | Acc]);
Error -> Error
end;
substitute_variant_types(_Bindings, [], Acc) ->
{ok, lists:reverse(Acc)}.
substitute_record_types(Bindings, [{ElementName, Type} | Rest], Acc) ->
case substitute_opaque_type(Bindings, Type) of
{ok, Result} -> substitute_record_types(Bindings, Rest, [{ElementName, Result} | Acc]);
Error -> Error
end;
substitute_record_types(_Bindings, [], Acc) ->
{ok, lists:reverse(Acc)}.
substitute_opaque_types(Bindings, [Next | Rest], Acc) ->
case substitute_opaque_type(Bindings, Next) of
{ok, Result} -> substitute_opaque_types(Bindings, Rest, [Result | Acc]);
Error -> Error
end;
substitute_opaque_types(_Bindings, [], Acc) ->
{ok, lists:reverse(Acc)}.
substitute_opaque_types(Bindings, Types) ->
Each = fun(Type) -> substitute_opaque_type(Bindings, Type) end,
lists:map(Each, Types).
coerce_bindings(VarTypes, Terms, Direction) ->
DefLength = length(VarTypes),
@ -1843,6 +1829,10 @@ coerce({O, N, signature}, S, to_fate) ->
coerce({_, _, signature}, Bin, from_fate) ->
Address = gmser_api_encoder:encode(signature, Bin),
{ok, unicode:characters_to_list(Address)};
%coerce({_, _, channel}, S, to_fate) when is_binary(S) ->
%{ok, {channel, S}};
%coerce({_, _, channel}, {channel, S}, from_fate) when is_binary(S) ->
%{ok, S};
coerce({_, _, boolean}, true, _) ->
{ok, true};
coerce({_, _, boolean}, "true", _) ->
@ -2569,6 +2559,11 @@ coerce_variant_test() ->
try_coerce(Type, {"A", 123}, {variant, [1, 2], 0, {123}}),
try_coerce(Type, {"B", 456, 789}, {variant, [1, 2], 1, {456, 789}}).
coerce_option_test() ->
{ok, Type} = annotate_type({"option", [integer]}, builtin_typedefs()),
try_coerce(Type, {"None"}, {variant, [0, 1], 0, {}}),
try_coerce(Type, {"Some", 1}, {variant, [0, 1], 1, {1}}).
coerce_record_test() ->
{ok, Type} = annotate_type({record, [{"a", integer}, {"b", integer}]}, #{}),
try_coerce(Type, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}).
@ -2592,7 +2587,7 @@ coerce_unicode_test() ->
ok.
coerce_hash_test() ->
{ok, Type} = annotate_type(hash, #{}),
{ok, Type} = annotate_type("hash", builtin_typedefs()),
Hash = list_to_binary(lists:seq(1,32)),
try_coerce(Type, Hash, Hash),
ok.
@ -2695,19 +2690,26 @@ obscure_aaci_test() ->
entrypoint bits(): bits = Bits.all
entrypoint character(): char = 'a'
entrypoint hash(): hash = #00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF
entrypoint unit(): unit = ()
entrypoint ttl(x): Chain.ttl = FixedTTL(x)
entrypoint pointee(x): AENS.pointee = AENS.AccountPt(x)
entrypoint name(x, y, z): AENS.name = AENS.Name(x, y, z)
",
{ok, AACI} = aaci_from_string(Contract),
{ok, {[], {{option, [integer]}, _, _}}} = aaci_lookup_spec(AACI, "options"),
{ok, {[], {{bytes, [4]}, _, _}}} = aaci_lookup_spec(AACI, "fixed_bytes"),
{ok, {[], {{bytes, [any]}, _, _}}} = aaci_lookup_spec(AACI, "any_bytes"),
{ok, {[], {bits, _, _}}} = aaci_lookup_spec(AACI, "bits"),
{ok, {[], {char, _, _}}} = aaci_lookup_spec(AACI, "character"),
{ok, {[], {hash, _, _}}} = aaci_lookup_spec(AACI, "hash"),
{ok, {[], {{"option", [integer]}, _, {variant, [{"None", []}, {"Some", [_]}]}}}} = aaci_lookup_spec(AACI, "options"),
{ok, {[], {"hash", _, {bytes, [32]}}}} = aaci_lookup_spec(AACI, "hash"),
{ok, {[], {"unit", _, {tuple, []}}}} = aaci_lookup_spec(AACI, "unit"),
{ok, {_, {"Chain.ttl", _, {variant, _}}}} = aaci_lookup_spec(AACI, "ttl"),
{ok, {_, {"AENS.pointee", _, {variant, _}}}} = aaci_lookup_spec(AACI, "pointee"),
{ok, {_, {"AENS.name", _, {variant, _}}}} = aaci_lookup_spec(AACI, "name"),
ok.