Compare commits

8 Commits

Author SHA1 Message Date
zxq9 fcf85077b2 Minor 2026-05-26 15:36:16 +09:00
dimitar.p.ivanov 4530fd2e93 Merge pull request 'Fix typespec' (#29) from respec into improve_specs
Reviewed-on: #29
2026-05-22 15:57:11 +09:00
zxq9 2a7079129f Fix typespec
Source needs to be defined as a binary.
2026-05-21 17:07:57 +09:00
Dimitar Ivanov 88aeb39d4a Fix a contract create bug 2026-05-20 17:02:36 +03:00
Dimitar Ivanov f0f86ed36d Improve specs 2026-05-13 10:04:23 +03:00
Jarvis Carroll ed252b4c06 Also note index in record_element
I changed it from noting the index to just noting the field name, but
actually both pieces of information are important, since if there was
a type error, presumably the type information is actually wrong.

Now we put the index first, since that is the part of the FATE tuple
that failed, and then the field name that that would be if the type
information were correct, in case that is useful.
2026-05-12 06:07:58 +00:00
Jarvis Carroll 5dcc05d56a Change fate_to_erlang warning
This warning always confuses me. Usually it is a case I haven't actually implemented,
but I don't need the program to diagnose that for me, I need the program to tell me what the
type was, so that I can work out why it thinks it isn't implemented.

All three terms of the annotated type are relevant, but the annotated version can only differ from the normalized version if
it is a record or variant definition, so we special case those two just to communicate that the fact that it is *some* kind of record
did successfully pass through to the coerce logic, and otherwise we just try and print the opaque and normalized types faithfully.
2026-05-12 06:00:26 +00:00
Jarvis Carroll 2eca3a5338 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.
2026-05-12 04:23:21 +00:00
2 changed files with 71 additions and 12 deletions
+11 -3
View File
@@ -788,9 +788,9 @@ contract_code(ID) ->
Result :: {ok, Source}
| {project, Bundle}
| {error, Reason},
Source :: string(),
Source :: binary(),
Bundle :: [{FilePath :: string(), Contents :: binary()}],
Reason :: chain_error() | string().
Reason :: chain_error() | string().
%% @doc
%% Retrieve the code of a contract as represented on chain.
@@ -809,6 +809,8 @@ extract(Blobby) ->
extract2(TarBaby) ->
case erl_tar:extract({binary, TarBaby}, [memory, compressed]) of
{ok, [{_, Source}]} ->
{ok, Source};
{ok, Bundle} ->
{project, Bundle};
Error ->
@@ -895,6 +897,12 @@ request(Path) ->
hz_man:request(unicode:characters_to_list(Path)).
-spec request(Path, Payload) -> {ok, Value} | {error, Reason}
when Path :: unicode:charlist(),
Payload :: unicode:charlist(),
Value :: map(),
Reason :: hz:chain_error().
request(Path, Payload) ->
hz_man:request(unicode:characters_to_list(Path), Payload).
@@ -930,7 +938,7 @@ contract_create(CreatorID, Path, InitArgs) ->
Gas = 500000,
GasPrice = min_gas_price(),
contract_create(CreatorID, Nonce,
Amount, TTL, Gas, GasPrice,
Gas, GasPrice, Amount, TTL,
Path, InitArgs);
Error ->
Error
+60 -9
View File
@@ -796,6 +796,10 @@ coerce_map_to_record(O, N, MemberTypes, Map) ->
case zip_record_fields(MemberTypes, Map) of
{ok, Zipped} ->
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, {tuple, list_to_tuple(Converted)}};
Errors ->
@@ -821,10 +825,18 @@ coerce_record_to_map(O, N, MemberTypes, Tuple) ->
single_error({record_too_few_terms, O, N, Tuple});
{error, too_many_terms} ->
single_error({record_too_many_terms, O, N, Tuple});
Errors ->
Errors
{error, Errors} ->
correct_record_error_paths(Names, Errors)
end.
correct_record_error_paths(Names, Errors) ->
CorrectOne = fun({Error, [{record_element, N} | Path]}) ->
FieldName = lists:nth(N + 1, Names),
{Error, [{record_element, N, FieldName} | Path]}
end,
Corrected = lists:map(CorrectOne, Errors),
{error, Corrected}.
zip_record_fields(Fields, Map) ->
case lists:mapfoldl(fun zip_record_field/2, {Map, []}, Fields) of
{_, {_, Missing = [_|_]}} ->
@@ -915,6 +927,11 @@ fate_to_erlang({O, N, {variant, Variants}}, {variant, _, Tag, Tuple}) ->
Terms = tuple_to_list(Tuple),
{Name, TermTypes} = lists:nth(Tag + 1, Variants),
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}) ->
coerce_record_to_map(O, N, MemberTypes, Tuple);
fate_to_erlang({O, N, {unknown_type, _}}, Data) ->
@@ -927,15 +944,30 @@ fate_to_erlang({O, N, {unknown_type, _}}, Data) ->
io:format(Message, [O, N, Data])
end,
{ok, Data};
fate_to_erlang({O, N, _}, Data) ->
case N of
already_normalized ->
io:format("Warning: Unimplemented type ~p.~nUsing term as is:~n~p~n", [O, Data]);
_ ->
io:format("Warning: Unimplemented type ~p (i.e. ~p).~nUsing term as is:~n~p~n", [O, N, Data])
end,
fate_to_erlang(Type, Data) ->
TypeStr = type_to_iolist(Type),
io:format("Warning: Could not coerce term into ~s. Using term as is: ~p~n", [TypeStr, Data]),
{ok, Data}.
type_to_iolist({O, already_normalized, S}) ->
% Already normalized. Example output:
% type {map, [string, integer]}
opaque_type_to_iolist(O, S);
type_to_iolist({O, N, S}) ->
% Type alias. Print the alias, and then print the normalized version in
% parentheses. Example output:
% type "my_alias" (i.e. record type {"my_record_type", [integer]})
io_lib:format("type ~p (i.e. ~s)", [O, opaque_type_to_iolist(N, S)]).
opaque_type_to_iolist(N, {record, _}) ->
% N is the name of a record definition.
io_lib:format("record type ~p", [N]);
opaque_type_to_iolist(N, {variant, _}) ->
% N is the name of a variant definition.
io_lib:format("variant type ~p", [N]);
opaque_type_to_iolist(N, _) ->
% N is some other constructive type.
io_lib:format("type ~p", [N]).
%%% AACI Getters
@@ -1120,6 +1152,25 @@ record_substitution_test() ->
{ok, {[], Output}} = get_function_signature(AACI, "f"),
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, 0, "it"}]}]} = fate_to_erlang(HOutput, {tuple, {1, 2, 3}}).
tuple_substitution_test() ->
Contract = "
contract C =