-
Notifications
You must be signed in to change notification settings - Fork 3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
stdlib: create an init function for records with complex default values #9373
base: master
Are you sure you want to change the base?
stdlib: create an init function for records with complex default values #9373
Conversation
CT Test Results 2 files 97 suites 1h 8m 18s ⏱️ For more details on these failures, see this check. Results for commit 952de98. ♻️ This comment has been updated with latest results. To speed up review, make sure that you have read Contributing to Erlang/OTP and that all checks pass. See the TESTING and DEVELOPMENT HowTo guides for details about how to run test locally. Artifacts// Erlang/OTP Github Action Bot |
d544c98
to
e4b004f
Compare
Now, as "free vars" are allowed in record fields, these free vars can affect each other. -module(z).
-export([mk1/0]).
-record(a, {
a = X = id(1),
b = X = id(2)
}).
id(X) -> X.
mk1() ->
#a{}.
|
Thanks, I'll get to it soon! |
Actually, I spoke to soon. This is as intended. Just like how it works if you try to update the record like this: But I will update the linter so that it warns for this. |
4ed5f0c
to
fc096c6
Compare
Any chance of not including the new function in the error description? So, from the other comment… 1> z:mk1().
** exception error: no match of right hand side value 2
in function z:'rec_init$^0'/0 (z.erl, line 13) I would've preferred to see something like… 1> z:mk1().
** exception error: no match of right hand side value 2
in function z:mk1/0 (z.erl, line 13) |
Tail calls, like in this case, makes it invisible in the stacktrace. Would that work for you? |
Another option would be to explicitly prevent the compiler from tail calling into this helper function, and always emit a full call with building a stack frame |
With the current implementation it's possible to crash -module(z).
-export([mk1/0]).
-record(a, {
a = X = 1,
b = X
}).
mk1() ->
#a{a = 2}.
The code after expansion is: -file("z.erl", 1).
-module(z).
-export([mk1/0]).
-record(a,{a = X = 1, b = X}).
mk1() ->
begin
REC0 = 'rec_init$^0'(),
case REC0 of
{a, _, _} ->
setelement(2, REC0, 2);
_ ->
error({badrecord, REC0})
end
end.
'rec_init$^0'() ->
{a, undefined, X}. |
The test case |
5e9f9b8
to
cf5cbab
Compare
I think my attempt at being clever about this failed, and a naiver approach is better.
In the above case, you will apply the initialize values, (a=2 in this case) #a{2,X,3}, check if there are variables left in the expression.
In this case after the initializing values are applied, you have #a{2,1,3}, there are no variables, so its not necessary to run the rec_init() function. If you have side effects (which you shouldn't) in your records default value, you have to think a second time if that really is what you want. Since they might be running when you do not expect it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So far I've looked at erl_error
and erl_expand_record
.
My comments are very nit-picky. We care very much about a consistent code-style in the compiler (and compiler-adjacent modules in STDLIB).
lib/stdlib/src/erl_error.erl
Outdated
case is_rec_init(F) of | ||
true -> <<"default value">>; | ||
_ -> io_lib:fwrite(<<"~ts/~w">>, [mf_to_string({M, F}, A, Enc), A]) | ||
end. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
case is_rec_init(F) of | |
true -> <<"default value">>; | |
_ -> io_lib:fwrite(<<"~ts/~w">>, [mf_to_string({M, F}, A, Enc), A]) | |
end. | |
case is_rec_init(F) of | |
true -> | |
<<"default value">>; | |
false -> | |
io_lib:fwrite(<<"~ts/~w">>, | |
[mf_to_string({M, F}, A, Enc), A]) | |
end. |
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">>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any test of this added code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes a testcase has been added
[traverse_af(Ast, Fun, Fun(Ast,Acc)) || Ast <- AF]; | ||
traverse_af(AF, Fun, Acc) when is_tuple(AF) -> | ||
%% Iterate each tuple element, if the element is an AF, traverse it | ||
[[(fun (List) when is_list(List) -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A tip: there is no need create and call a fun here. You can just use a case
or an if
here.
throw:{error,unsafe_variable} -> true | ||
end. | ||
free_variables1({'fun',_anno,{clauses, _}}, Acc) -> | ||
{function,Acc}; %% tag that we are in a 'fun' now that can define new variables |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is correct. Only variables defined in the function head will be shadowed. Variables defined in the function body will be matched against variables having the same name in the enclosing function.
In my suggested simplified function, I don't try to handle funs; I think they are used too infrequently to be worth the effort.
"Bound" variables from previous fields are not propagated correctly to next fields. This definition is accepted: -record(r1, {
a = X = 1,
c = X
}). While this definition is rejected: -record(r1, {
a = X = 1,
b,
c = X
}).
|
Thinking about it a bit more, I actually find it very surprising we want to allow variables "leaking" across fields in default values - we don't allow that in regular record creation! For example this is an error:
Why should moving this into the default expression somehow make this compile and OK? This sounds quite inconsistent to me. |
I agree, - this introduces one more "rule" about scoping of variables that is already quite complicated and sometimes surprising. |
Okey, I must have been confused because I thought this was valid! I'll fix the behavior next week then. |
cf5cbab
to
f8dfae2
Compare
bc79697
to
d1830d7
Compare
prevents a crashing the shell when completion is attempted on "case(". OTP-19511
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.
d1830d7
to
952de98
Compare
records that have field default values containing variables that are "free" was unsafe in functions that have variables with the same name. This commit creates init function for records to protect the variables in the default value.
e.g.
-record(r, {f = fun(X)->case X of {y, Y} -> Y; _ -> X end, g=..., h=abc}). foo(X)->#r{}. --> foo(X)->(r_init()){}.
r_init() will only initialize fields that will not be updated e.g.
foo(X)->#r{f=X} --> foo(X)->(r_init_f()){f=X}.
r_init_f will only initialize g and h with its default value, f will be initialized to undefined.
r_init() functions will not be generated if all fields of the record that contains "free variables" are initialized by the user.
e.g.
foo(X)->#r{f=X,g=X}. --> foo(X)->{r,X,X,abc}.
closes #9317