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.

This function is created for each module using the record.
In the shell they will belong to the shell_default module.

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$^0'()){}.

rec_init$^0() will initialize all fields with the default values.
If fields are set and the omitted field default value has variables, then
a new init function is created that only initializes the omitted fields.

e.g.
foo(X)->\#r{g=X}. --> foo(X)->('rec_init$^1()){g=X}.

- 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 with default values calling local non exported functions will not compile
    when the function is not available, the shell will be able to import it, but
    the local functions will have to be defined manually.
  • Loading branch information
frazze-jobb committed Feb 26, 2025
1 parent e5f45a7 commit 952de98
Show file tree
Hide file tree
Showing 8 changed files with 227 additions and 92 deletions.
20 changes: 18 additions & 2 deletions lib/stdlib/src/erl_error.erl
Original file line number Diff line number Diff line change
Expand Up @@ -536,10 +536,20 @@ location(L) ->
sep(1, S) -> S;
sep(_, S) -> [$\n | S].

is_rec_init(F) when is_atom(F) ->
case atom_to_binary(F) of
<<"rec_init$^", _/binary>> -> true;
_ -> false
end;
is_rec_init(_) -> false.

origin(1, M, F, A) ->
case is_op({M, F}, n_args(A)) of
{yes, F} -> <<"in operator ">>;
no -> <<"in function ">>
no -> case is_rec_init(F) of
true -> <<"in record">>;
false -> <<"in function ">>
end
end;
origin(_N, _M, _F, _A) ->
<<"in call from">>.
Expand Down Expand Up @@ -625,7 +635,13 @@ printable_list(_, As) ->
io_lib:printable_list(As).

mfa_to_string(M, F, A, Enc) ->
io_lib:fwrite(<<"~ts/~w">>, [mf_to_string({M, F}, A, Enc), A]).
case is_rec_init(F) of
true ->
<<"default value">>;
false ->
io_lib:fwrite(<<"~ts/~w">>,
[mf_to_string({M, F}, A, Enc), A])
end.

mf_to_string({M, F}, A, Enc) ->
case erl_internal:bif(M, F, A) of
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 952de98

Please sign in to comment.