Handle singleton records in erlang_to_fate

I realized this case needed special handling in hz_sophia, but didn't
get around to covering it properly in the older hz_aaci analogues.

While I was at it, I went and improved the error paths for record elements.
This commit is contained in:
Jarvis Carroll
2026-05-12 04:23:21 +00:00
parent da92d80334
commit 2eca3a5338
+38 -2
View File
@@ -796,6 +796,10 @@ coerce_map_to_record(O, N, MemberTypes, Map) ->
case zip_record_fields(MemberTypes, Map) of case zip_record_fields(MemberTypes, Map) of
{ok, Zipped} -> {ok, Zipped} ->
case coerce_zipped_bindings(Zipped, to_fate, field) of case coerce_zipped_bindings(Zipped, to_fate, field) of
{ok, [SingleElem]} ->
% Singleton records aren't implemented as FATE tuples at
% all.
{ok, SingleElem};
{ok, Converted} -> {ok, Converted} ->
{ok, {tuple, list_to_tuple(Converted)}}; {ok, {tuple, list_to_tuple(Converted)}};
Errors -> Errors ->
@@ -821,10 +825,18 @@ coerce_record_to_map(O, N, MemberTypes, Tuple) ->
single_error({record_too_few_terms, O, N, Tuple}); single_error({record_too_few_terms, O, N, Tuple});
{error, too_many_terms} -> {error, too_many_terms} ->
single_error({record_too_many_terms, O, N, Tuple}); single_error({record_too_many_terms, O, N, Tuple});
Errors -> {error, Errors} ->
Errors correct_record_error_paths(Names, Errors)
end. end.
correct_record_error_paths(Names, Errors) ->
CorrectOne = fun({Error, [{record_element, N} | Path]}) ->
FieldName = lists:nth(N + 1, Names),
{Error, [{record_element, FieldName} | Path]}
end,
Corrected = lists:map(CorrectOne, Errors),
{error, Corrected}.
zip_record_fields(Fields, Map) -> zip_record_fields(Fields, Map) ->
case lists:mapfoldl(fun zip_record_field/2, {Map, []}, Fields) of case lists:mapfoldl(fun zip_record_field/2, {Map, []}, Fields) of
{_, {_, Missing = [_|_]}} -> {_, {_, Missing = [_|_]}} ->
@@ -915,6 +927,11 @@ fate_to_erlang({O, N, {variant, Variants}}, {variant, _, Tag, Tuple}) ->
Terms = tuple_to_list(Tuple), Terms = tuple_to_list(Tuple),
{Name, TermTypes} = lists:nth(Tag + 1, Variants), {Name, TermTypes} = lists:nth(Tag + 1, Variants),
coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, from_fate); coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, from_fate);
fate_to_erlang({O, N, {record, [SingleMemberType]}}, Data) ->
% Singleton records aren't implemented as FATE tuples at all.
% Pretend they are, so we can get the full error indexing of the
% non-singletone case.
coerce_record_to_map(O, N, [SingleMemberType], {Data});
fate_to_erlang({O, N, {record, MemberTypes}}, {tuple, Tuple}) -> fate_to_erlang({O, N, {record, MemberTypes}}, {tuple, Tuple}) ->
coerce_record_to_map(O, N, MemberTypes, Tuple); coerce_record_to_map(O, N, MemberTypes, Tuple);
fate_to_erlang({O, N, {unknown_type, _}}, Data) -> fate_to_erlang({O, N, {unknown_type, _}}, Data) ->
@@ -1120,6 +1137,25 @@ record_substitution_test() ->
{ok, {[], Output}} = get_function_signature(AACI, "f"), {ok, {[], Output}} = get_function_signature(AACI, "f"),
check_roundtrip(Output, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}). check_roundtrip(Output, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}).
singleton_record_substitution_test() ->
Contract = "
contract C =
record single('t) = { it: 't }
entrypoint f(): single(int) = { it = 1 }
entrypoint g(): single(single(int)) = { it = { it = 2 } }
entrypoint h(): single(int * int) = { it = (3, 4) }
",
{ok, AACI} = aaci_from_string(Contract),
{ok, {[], FOutput}} = get_function_signature(AACI, "f"),
check_roundtrip(FOutput, #{"it" => 123}, 123),
{ok, {[], GOutput}} = get_function_signature(AACI, "g"),
check_roundtrip(GOutput, #{"it" => #{"it" => 123}}, 123),
{ok, {[], HOutput}} = get_function_signature(AACI, "h"),
check_roundtrip(HOutput, #{"it" => {123, 456}}, {tuple, {123, 456}}),
% Also check that records have accurate paths, since the implementation for
% record error paths is a bit fiddly.
{error, [{{tuple_too_many_terms, _, _, _}, [{record_element, "it"}]}]} = fate_to_erlang(HOutput, {tuple, {1, 2, 3}}).
tuple_substitution_test() -> tuple_substitution_test() ->
Contract = " Contract = "
contract C = contract C =