From a1be8cbc141b00c3433e85a290b4c69682c4979e Mon Sep 17 00:00:00 2001 From: Jarvis Carroll Date: Tue, 23 Sep 2025 19:22:30 +1000 Subject: [PATCH] coerce hashes It turns out there are a lot of types that, like option, should only be valid as an opaque/normalized type, but should be substituted for something different in the flat representation. If we restructure things a little then we can implement all of these in one go. --- src/hz.erl | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/hz.erl b/src/hz.erl index 5d0b2f8..b48b891 100644 --- a/src/hz.erl +++ b/src/hz.erl @@ -1658,6 +1658,12 @@ normalize_opaque_type(T, Types) -> 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. % 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? @@ -1665,6 +1671,10 @@ 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]}}; normalize_opaque_type(T, Types, IsFirst) when is_list(T) -> normalize_opaque_type({T, []}, Types, IsFirst); normalize_opaque_type({T, TypeArgs}, Types, IsFirst) when is_list(T) -> @@ -1709,7 +1719,10 @@ normalize_opaque_type3(NextT, Types) -> % 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. @@ -2578,6 +2591,13 @@ coerce_unicode_test() -> {ok, $ḉ} = coerce(Type, <<"ḉ"/utf8>>, to_fate), ok. +coerce_hash_test() -> + {ok, Type} = annotate_type(hash, #{}), + Hash = list_to_binary(lists:seq(1,32)), + try_coerce(Type, Hash, Hash), + ok. + + %%% Complex AACI paramter and namespace tests @@ -2674,20 +2694,20 @@ obscure_aaci_test() -> entrypoint any_bytes(): bytes() = Bytes.to_any_size(#112233) entrypoint bits(): bits = Bits.all entrypoint character(): char = 'a' + entrypoint hash(): hash = #00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF ", {ok, AACI} = aaci_from_string(Contract), - IntAnnotated = {integer, already_normalized, integer}, - OptionFlat = {variant, [{"None", []}, {"Some", [IntAnnotated]}]}, - OptionAnnotated = {{option, [integer]}, already_normalized, OptionFlat}, - {ok, {[], OptionAnnotated}} = aaci_lookup_spec(AACI, "options"), + {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, {[], {{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, {[], {bits, _, _}}} = aaci_lookup_spec(AACI, "bits"), - {ok, {[], {_, _, char}}} = aaci_lookup_spec(AACI, "character"), + {ok, {[], {char, _, _}}} = aaci_lookup_spec(AACI, "character"), + + {ok, {[], {hash, _, _}}} = aaci_lookup_spec(AACI, "hash"), ok.