Skip to content

Commit fd9bb17

Browse files
authored
Merge pull request #36 from rabbitmq/add-var_info-to-assembly
khepri_fun: Add guessed `var_info` annotations to assembly
2 parents 70056c4 + 5957b82 commit fd9bb17

File tree

3 files changed

+164
-6
lines changed

3 files changed

+164
-6
lines changed

src/khepri_fun.erl

Lines changed: 118 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,18 @@
6565
%% assembly. This breaks compile/1 and causes a cascade of errors.
6666
%%
6767
%% The following basically disable Dialyzer for this module unfortunately...
68+
%% This can be removed once we start using Erlang 25 to run Dialyzer.
6869
-dialyzer({nowarn_function, [compile/1,
6970
to_standalone_fun/2,
7071
to_standalone_fun1/2,
7172
to_standalone_fun2/2,
7273
to_standalone_env/1,
73-
to_standalone_arg/2]}).
74+
to_standalone_arg/2,
75+
handle_compilation_error/2,
76+
add_comment_and_retry/4,
77+
add_comment_to_function/7,
78+
add_comment_to_code/5,
79+
are_comments_conflicting/2]}).
7480

7581
-type fun_info() :: #{arity => arity(),
7682
env => any(),
@@ -187,8 +193,15 @@ to_standalone_fun2(
187193
should_process_function(
188194
Module, Name, Arity, Module, State);
189195
external ->
190-
should_process_function(
191-
Module, Name, Arity, undefined, State)
196+
_ = code:ensure_loaded(Module),
197+
case erlang:function_exported(Module, Name, Arity) of
198+
true ->
199+
should_process_function(
200+
Module, Name, Arity, undefined, State);
201+
false ->
202+
throw({call_to_unexported_function,
203+
{Module, Name, Arity}})
204+
end
192205
end,
193206
case ShouldProcess of
194207
true ->
@@ -263,9 +276,110 @@ compile(Asm) ->
263276
deterministic],
264277
case compile:forms(Asm, CompilerOptions) of
265278
{ok, Module, Beam, []} -> {Module, Beam};
266-
Error -> throw({compilation_failure, Error, Asm})
279+
Error -> handle_compilation_error(Asm, Error)
267280
end.
268281

282+
handle_compilation_error(
283+
Asm,
284+
{error,
285+
[{_GeneratedModuleName,
286+
[{_, beam_validator,
287+
{FailingFun,
288+
{{get_tuple_element, Src, _Element, _Dst},
289+
_,
290+
{bad_type,
291+
{needed, {t_tuple, _Size, _, _Fields} = NeededType},
292+
{actual, any}}}}}]}],
293+
[]} = Error) ->
294+
VarInfo = {var_info, Src, [{type, NeededType}]},
295+
Comment = {'%', VarInfo},
296+
add_comment_and_retry(Asm, Error, FailingFun, Comment);
297+
handle_compilation_error(
298+
Asm,
299+
%% Same as above, but returned by Erlang 23's compiler instead of Erlang 24+.
300+
{error,
301+
[{_GeneratedModuleName,
302+
[{beam_validator,
303+
{FailingFun,
304+
{{get_tuple_element, Src, _Element, _Dst},
305+
_,
306+
{bad_type,
307+
{needed, {t_tuple, _Size, _, _Fields} = NeededType},
308+
{actual, any}}}}}]}],
309+
[]} = Error) ->
310+
VarInfo = {var_info, Src, [{type, NeededType}]},
311+
Comment = {'%', VarInfo},
312+
add_comment_and_retry(Asm, Error, FailingFun, Comment);
313+
handle_compilation_error(Asm, Error) ->
314+
throw({compilation_failure, Error, Asm}).
315+
316+
add_comment_and_retry(
317+
Asm, Error, {GeneratedModuleName, Name, Arity} = _FailingFun, Comment) ->
318+
{GeneratedModuleName,
319+
Exports,
320+
Attributes,
321+
Functions,
322+
Labels} = Asm,
323+
Functions1 = add_comment_to_function(
324+
Asm, Error, Functions, Name, Arity, Comment, []),
325+
Asm1 = {GeneratedModuleName,
326+
Exports,
327+
Attributes,
328+
Functions1,
329+
Labels},
330+
compile(Asm1).
331+
332+
add_comment_to_function(
333+
Asm, Error,
334+
[#function{name = Name, arity = Arity, code = Code} = Function | Rest],
335+
Name, Arity, Comment, Result) ->
336+
Code1 = add_comment_to_code(Asm, Error, Code, Comment, []),
337+
Function1 = Function#function{code = Code1},
338+
lists:reverse(Result) ++ [Function1 | Rest];
339+
add_comment_to_function(
340+
Asm, Error,
341+
[Function | Rest], Name, Arity, Comment, Result) ->
342+
add_comment_to_function(
343+
Asm, Error, Rest, Name, Arity, Comment, [Function | Result]).
344+
345+
add_comment_to_code(
346+
Asm, Error,
347+
[{label, _} = Instruction | Rest],
348+
Comment, Result) ->
349+
add_comment_to_code(Asm, Error, Rest, Comment, [Instruction | Result]);
350+
add_comment_to_code(
351+
Asm, Error,
352+
[{func_info, _, _, _} = Instruction | Rest],
353+
Comment, Result) ->
354+
add_comment_to_code(Asm, Error, Rest, Comment, [Instruction | Result]);
355+
add_comment_to_code(
356+
Asm, Error,
357+
[{'%', _} = Instruction | Rest],
358+
Comment, Result) ->
359+
case are_comments_conflicting(Instruction, Comment) of
360+
false ->
361+
add_comment_to_code(
362+
Asm, Error, Rest, Comment, [Instruction | Result]);
363+
true ->
364+
throw(
365+
{conflicting_assembly_annotations,
366+
Instruction, Comment, Error, Asm})
367+
end;
368+
add_comment_to_code(
369+
_Asm, _Error,
370+
Rest,
371+
Comment, Result) ->
372+
lists:reverse(Result) ++ [Comment | Rest].
373+
374+
are_comments_conflicting(
375+
{'%', {var_info, Register, _}},
376+
{'%', {var_info, Register, _}}) ->
377+
%% If we are about to generate two `var_info' comments affecting the same
378+
%% register (i.e. same variable), we abort.
379+
true;
380+
are_comments_conflicting(_Comment1, _Comment2) ->
381+
false.
382+
269383
-spec exec(StandaloneFun, Args) -> Ret when
270384
StandaloneFun :: standalone_fun(),
271385
Args :: [any()],

test/mod_used_for_transactions.erl

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
-export([exported/0,
1111
get_lambda/0,
1212
%% We export this one just to try to prevent inlining.
13-
hash_term/1]).
13+
hash_term/1,
14+
make_record/1,
15+
outer_function/2]).
1416

1517
exported() -> unexported().
1618
unexported() -> ok.
@@ -22,3 +24,20 @@ get_lambda() ->
2224

2325
hash_term(Term) ->
2426
erlang:phash2(Term).
27+
28+
-record(my_record, {function}).
29+
30+
make_record(Function) ->
31+
#my_record{function = Function}.
32+
33+
outer_function(#my_record{function = Value} = MyRecord, Term) ->
34+
is_atom(Value) andalso
35+
inner_function(MyRecord, Term);
36+
outer_function(_, _) ->
37+
false.
38+
39+
inner_function(#my_record{function = hash_term = Function}, Term) ->
40+
_ = (?MODULE:Function(Term)),
41+
true;
42+
inner_function(_, _) ->
43+
false.

test/tx_funs.erl

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515

1616
-dialyzer([{no_return, [allowed_khepri_tx_api_test/0,
1717
allowed_erlang_expressions_test/0,
18-
allowed_erlang_module_api_test/0]}]).
18+
allowed_erlang_module_api_test/0]},
19+
{no_missing_calls,
20+
[extracting_unexported_external_function_test/0]}]).
1921

2022
-define(make_standalone_fun(Expression),
2123
begin
@@ -816,3 +818,26 @@ exec_with_standalone_fun_test() ->
816818
?assertEqual(result, khepri_fun:exec(StandaloneFun, [])),
817819
?assertEqual(result, khepri_fun:exec(StandaloneFun, [])),
818820
?assertEqual(result, khepri_fun:exec(StandaloneFun, [])).
821+
822+
record_matching_fun_clause_test() ->
823+
StandaloneFun = khepri_fun:to_standalone_fun(
824+
fun mod_used_for_transactions:outer_function/2,
825+
#{}),
826+
%% Dialyzer doesn't like that we ?assertMatch(#standalone_fun{},
827+
%% StandaloneFun), I don't know why... Let's verify we don't have a
828+
%% function object instead.
829+
?assertNot(is_function(StandaloneFun)),
830+
MyRecord1 = mod_used_for_transactions:make_record(hash_term),
831+
?assertEqual(true, khepri_fun:exec(StandaloneFun, [MyRecord1, a])),
832+
MyRecord2 = mod_used_for_transactions:make_record(non_existing),
833+
?assertEqual(false, khepri_fun:exec(StandaloneFun, [MyRecord2, a])),
834+
?assertEqual(false, khepri_fun:exec(StandaloneFun, [not_my_record, a])),
835+
ok.
836+
837+
extracting_unexported_external_function_test() ->
838+
?assertThrow(
839+
{call_to_unexported_function,
840+
{mod_used_for_transactions, inner_function, 2}},
841+
khepri_fun:to_standalone_fun(
842+
fun mod_used_for_transactions:inner_function/2,
843+
#{})).

0 commit comments

Comments
 (0)