Skip to content

Commit

Permalink
stdlib: allow variables in record definitions
Browse files Browse the repository at this point in the history
create an init function e.g. rec_init$^0, for each record
with definitions containing variables.

e.g.
-record(r, {f = fun(X)->case X of {y, Y} -> Y; _ -> X end, g=..., h=abc}).
foo(X)->\#r{}. --> foo(X)->(rec_init()){}.

rec_init() will initialize all fields with the default values

If one field is set and the omitted field default value has variables, then
a new init function is created that only initializes the omitted fields.

- removes lint error for variables in definitions
- updates erl_lint_SUITE and erl_expand_records_SUITE to work with this new behavior
- adds handling of records that are calling functions to the shell
  - records calling local non exported functions will fail initialization
  • Loading branch information
frazze-jobb committed Feb 17, 2025
1 parent befaffa commit f8dfae2
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 98 deletions.
34 changes: 23 additions & 11 deletions lib/stdlib/src/edlin_expand.erl
Original file line number Diff line number Diff line change
Expand Up @@ -729,30 +729,42 @@ expand_filepath(PathPrefix, Word) ->
X -> X
end.

shell(Fun) ->
{ok, [{atom, _, Fun1}], _} = erl_scan:string(Fun),
case shell:local_func(Fun1) of
shell(Fun) when is_atom(Fun) ->
case lists:member(Fun, [E || {E,_}<-get_exports(shell)]) of
true -> "shell";
false -> "user_defined"
_ -> "user_defined"
end;
shell(Fun) ->
case erl_scan:string(Fun) of
{ok, [{var, _, _}], _} -> [];
{ok, [{atom, _, Fun1}], _} ->
shell(Fun1)
end.

-doc false.
shell_default_or_bif(Fun) when is_atom(Fun) ->
case lists:member(Fun, [E || {E,_}<-get_exports(shell_default)]) of
true -> "shell_default";
_ -> bif(Fun)
end;
shell_default_or_bif(Fun) ->
case erl_scan:string(Fun) of
{ok, [{var, _, _}], _} -> [];
{ok, [{atom, _, Fun1}], _} ->
case lists:member(Fun1, [E || {E,_}<-get_exports(shell_default)]) of
true -> "shell_default";
_ -> bif(Fun)
end
shell_default_or_bif(Fun1)
end.

-doc false.
bif(Fun) ->
{ok, [{atom, _, Fun1}], _} = erl_scan:string(Fun),
case lists:member(Fun1, [E || {E,A}<-get_exports(erlang), erl_internal:bif(E,A)]) of
bif(Fun) when is_atom(Fun) ->
case lists:member(Fun, [E || {E,_}<-get_exports(erlang)]) of
true -> "erlang";
_ -> shell(Fun)
end;
bif(Fun) ->
case erl_scan:string(Fun) of
{ok, [{var, _, _}], _} -> [];
{ok, [{atom, _, Fun1}], _} ->
bif(Fun1)
end.

expand_string(Bef0) ->
Expand Down
76 changes: 71 additions & 5 deletions lib/stdlib/src/erl_expand_records.erl
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ Section [The Abstract Format](`e:erts:absform.md`) in ERTS User's Guide.
strict_ra=[], % Strict record accesses
checked_ra=[], % Successfully accessed records
dialyzer=false, % Compiler option 'dialyzer'
rec_init_count=0, % Number of generated record init functions
new_forms=#{}, % New forms
strict_rec_tests=true :: boolean()
}).

Expand Down Expand Up @@ -95,6 +97,12 @@ forms([{function,Anno,N,A,Cs0} | Fs0], St0) ->
forms([F | Fs0], St0) ->
{Fs,St} = forms(Fs0, St0),
{[F | Fs], St};
forms([], #exprec{new_forms=FsN}=St) ->
{[{'function', Anno,
maps:get(Def,FsN),
0,
[{'clause', Anno, [], [], [Def]}]}
|| {_,Anno,_}=Def <- maps:keys(FsN)], St};
forms([], St) -> {[],St}.

clauses([{clause,Anno,H0,G0,B0} | Cs0], St0) ->
Expand Down Expand Up @@ -262,6 +270,30 @@ not_a_tuple({op,_,_,_}) -> true;
not_a_tuple({op,_,_,_,_}) -> true;
not_a_tuple(_) -> false.

variables({var,_,'_'}) ->
[];
variables({var,_,V}) ->
[V];
variables({'fun',_,Def}) ->
%% The Def tuple has no annotation. Must handle it specially.
case Def of
{clauses,Cs} -> variables(Cs);
{function,F,A} -> variables([F,A]);
{function,M,F,A} -> variables([M,F,A])
end;
variables(Tuple) when is_tuple(Tuple) ->
[Tag,Anno|T] = tuple_to_list(Tuple),
true = is_atom(Tag),
true = erl_anno:is_anno(Anno),
variables(T);
variables(List) when is_list(List) ->
foldl(fun(E, Vs0) ->
Vs1 = variables(E),
ordsets:union(Vs0, Vs1)
end, [], List);
variables(_) ->
[].

record_test_in_body(Anno, Expr, Name, St0) ->
%% As Expr may have side effects, we must evaluate it
%% first and bind the value to a new variable.
Expand Down Expand Up @@ -333,11 +365,45 @@ expr({map_field_exact,Anno,K0,V0}, St0) ->
expr({record_index,Anno,Name,F}, St) ->
I = index_expr(Anno, F, Name, record_fields(Name, Anno, St)),
expr(I, St);
expr({record,Anno0,Name,Is}, St) ->
Anno = mark_record(Anno0, St),
expr({tuple,Anno,[{atom,Anno0,Name} |
record_inits(record_fields(Name, Anno0, St), Is)]},
St);
expr({record,Anno0,Name,Is}, St0) ->
Anno = mark_record(Anno0, St0),

RInit = [{atom,Anno,Name} |
record_inits(record_fields(Name, Anno0, St0), Is)],
Vars = variables(Is),
%% Check if there are variables in the initialized record. If
%% there are, we need to initialize the record using a generated
%% function
AnyVariables = not ordsets:is_subset(variables(RInit), Vars),
case AnyVariables of
true ->
%% Initialize the record with only the default values.
%% Setting fields that has been overridden to undefined.
UndefIs = [setelement(4,R,{atom,Anno,undefined}) || {record_field,_,_,_}=R<-Is],
RDefInit = [{atom,Anno,Name} |
record_inits(record_fields(Name, Anno, St0), UndefIs)],
{Def,St1} = expr({tuple,Anno,RDefInit}, St0),
Map0 = St1#exprec.new_forms,
{FName,St2} =
case Map0 of
#{Def := OldName} ->
{OldName,St1};
#{} ->
C = St1#exprec.rec_init_count,
NewName = list_to_atom("rec_init$^" ++
integer_to_list(C)),
Map = Map0#{Def => NewName},
{NewName,St1#exprec{rec_init_count=C+1,
new_forms=Map}}
end,
%% Replace the init record expression with a call expression
%% to the newly added function followed by a record update.
expr({record, Anno0, {call,Anno,{atom,Anno,FName},[]}, Name, Is},St2);
false ->
%% No free variables means that we can just output the
%% record as a tuple.
expr({tuple,Anno,RInit}, St0)
end;
expr({record_field,_A,R,Name,F}, St) ->
Anno = erl_parse:first_anno(R),
get_record_field(Anno, R, F, Name, St);
Expand Down
30 changes: 6 additions & 24 deletions lib/stdlib/src/erl_lint.erl
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,6 @@ value_option(Flag, Default, On, OnVal, Off, OffVal, Opts) ->
errors=[] :: [{file:filename(),error_info()}], %Current errors
warnings=[] :: [{file:filename(),error_info()}], %Current warnings
file = "" :: string(), %From last file attribute
recdef_top=false :: boolean(), %true in record initialisation
%outside any fun or lc
xqlc= false :: boolean(), %true if qlc.hrl included
called= [] :: [{fa(),anno()}], %Called functions
fun_used_vars = undefined %Funs used vars
Expand Down Expand Up @@ -469,8 +467,6 @@ format_error_1({shadowed_var,V,In}) ->
{~"variable ~w shadowed in ~w", [V,In]};
format_error_1({unused_var, V}) ->
{~"variable ~w is unused", [V]};
format_error_1({variable_in_record_def,V}) ->
{~"variable ~w in record definition", [V]};
format_error_1({stacktrace_guard,V}) ->
{~"stacktrace variable ~w must not be used in a guard", [V]};
format_error_1({stacktrace_bound,V}) ->
Expand Down Expand Up @@ -3090,21 +3086,14 @@ def_fields(Fs0, Name, St0) ->
case exist_field(F, Fs) of
true -> {Fs,add_error(Af, {redefine_field,Name,F}, St)};
false ->
St1 = St#lint{recdef_top = true},
{_,St2} = expr(V, [], St1),
%% Warnings and errors found are kept, but
%% updated calls, records, etc. are discarded.
St3 = St1#lint{warnings = St2#lint.warnings,
errors = St2#lint.errors,
called = St2#lint.called,
recdef_top = false},
{_,St1} = expr(V, [], St),
%% This is one way of avoiding a loop for
%% "recursive" definitions.
NV = case St2#lint.errors =:= St1#lint.errors of
NV = case St1#lint.errors =:= St#lint.errors of
true -> V;
false -> {atom,Aa,undefined}
end,
{[{record_field,Af,{atom,Aa,F},NV}|Fs],St3}
{[{record_field,Af,{atom,Aa,F},NV}|Fs],St1}
end
end, {[],St0}, Fs0).

Expand Down Expand Up @@ -4067,10 +4056,7 @@ comprehension_expr(E, Vt, St) ->
%% in ShadowVarTable (these are local variables that are not global variables).

lc_quals(Qs, Vt0, St0) ->
OldRecDef = St0#lint.recdef_top,
{Vt,Uvt,St} = lc_quals(Qs, Vt0, [], St0#lint{recdef_top = false}),
{Vt,Uvt,St#lint{recdef_top = OldRecDef}}.

lc_quals(Qs, Vt0, [], St0).
lc_quals([{zip,_Anno,Gens} | Qs], Vt0, Uvt0, St0) ->
St1 = are_all_generators(Gens,St0),
{Vt,Uvt,St} = handle_generators(Gens,Vt0,Uvt0,St1),
Expand Down Expand Up @@ -4205,13 +4191,12 @@ fun_clauses(Cs, Vt, St) ->
fun_clauses1(Cs, Vt, St).

fun_clauses1(Cs, Vt, St) ->
OldRecDef = St#lint.recdef_top,
{Bvt,St2} = foldl(fun (C, {Bvt0, St0}) ->
{Cvt,St1} = fun_clause(C, Vt, St0),
{vtmerge(Cvt, Bvt0),St1}
end, {[],St#lint{recdef_top = false}}, Cs),
end, {[],St}, Cs),
Uvt = vt_no_unsafe(vt_no_unused(vtold(Bvt, Vt))),
{Uvt,St2#lint{recdef_top = OldRecDef}}.
{Uvt,St2}.

fun_clause({clause,_Anno,H,G,B}, Vt0, St0) ->
{Hvt,Hnew,St1} = head(H, Vt0, [], St0), % No imported pattern variables
Expand Down Expand Up @@ -4289,9 +4274,6 @@ pat_var(V, Anno, Vt, New, St0) ->
{[{V,{bound,used,Ls}}],[],
%% As this is matching, exported vars are risky.
add_warning(Anno, {exported_var,V,From}, St)};
error when St0#lint.recdef_top ->
{[],[{V,{bound,unused,[Anno]}}],
add_error(Anno, {variable_in_record_def,V}, St0)};
error ->
%% add variable to NewVars, not yet used
{[],[{V,{bound,unused,[Anno]}}],St0}
Expand Down
Loading

0 comments on commit f8dfae2

Please sign in to comment.