Skip to content
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

Allow reserved words as record names w/out quotes #7873

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 110 additions & 24 deletions lib/stdlib/src/erl_parse.yrl
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@

Nonterminals
form
reserved_word
attribute attr_val
function function_clauses function_clause
clause_args clause_guard clause_body
@@ -34,7 +35,7 @@ list_comprehension lc_expr lc_exprs
map_comprehension
binary_comprehension
tuple
record_expr record_tuple record_field record_fields
record_expr record_tuple record_field record_fields record_name record_spec
map_expr map_tuple map_field map_field_assoc map_field_exact map_fields map_key
if_expr if_clause if_clauses case_expr cr_clause cr_clauses receive_expr
fun_expr fun_clause fun_clauses atom_or_var integer_or_var
@@ -47,7 +48,8 @@ binary bin_elements bin_element bit_expr sigil
opt_bit_size_expr bit_size_expr opt_bit_type_list bit_type_list bit_type
top_type top_types type typed_expr typed_attr_val
type_sig type_sigs type_guard type_guards fun_type binary_type
type_spec spec_fun typed_exprs typed_record_fields field_types field_type
type_spec spec_fun typed_exprs
typed_record_spec typed_record_fields field_types field_type
map_pair_types map_pair_type
bin_base_type bin_unit_type
maybe_expr maybe_match_exprs maybe_match
@@ -81,7 +83,7 @@ char integer float atom sigil_prefix string sigil_suffix var

'(' ')' ',' '->' '{' '}' '[' ']' '|' '||' '<-' ';' ':' '#' '.'
'after' 'begin' 'case' 'try' 'catch' 'end' 'fun' 'if' 'of' 'receive' 'when'
'maybe' 'else'
'maybe' 'else' 'cond' 'let'
'andalso' 'orelse'
'bnot' 'not'
'*' '/' 'div' 'rem' 'band' 'and'
@@ -91,7 +93,8 @@ char integer float atom sigil_prefix string sigil_suffix var
'<<' '>>'
'!' '=' '::' '..' '...'
'?='
'spec' 'callback' % helper
%% helper: special handling in parse_form like reserved word
'spec' 'callback' 'record'
dot
'%ssa%'.

@@ -127,6 +130,9 @@ form -> function dot : '$1'.
attribute -> '-' atom attr_val : build_attribute('$2', '$3').
attribute -> '-' atom typed_attr_val : build_typed_attribute('$2','$3').
attribute -> '-' atom '(' typed_attr_val ')' : build_typed_attribute('$2','$4').
attribute -> '-' 'record' record_spec : build_attribute(build_atom('$2'), '$3').
attribute -> '-' 'record' typed_record_spec : build_typed_attribute(build_atom('$2'), '$3').
attribute -> '-' 'record' '(' typed_record_spec ')' : build_typed_attribute(build_atom('$2'), '$4').
attribute -> '-' 'spec' type_spec : build_type_spec('$2', '$3').
attribute -> '-' 'callback' type_spec : build_type_spec('$2', '$3').

@@ -139,6 +145,19 @@ spec_fun -> atom ':' atom : {'$1', '$3'}.
typed_attr_val -> expr ',' typed_record_fields : {typed_record, '$1', '$3'}.
typed_attr_val -> expr '::' top_type : {type_def, '$1', '$3'}.

%% Pretty much like attr_val, but record name must be an atom,
%% to not allow variable names as record names when there is no leading '#'
record_spec -> atom : ['$1'].
record_spec -> atom ',' exprs: ['$1' | '$3'].
record_spec -> '(' atom ',' exprs ')': ['$2' | '$4'].
%% More record like record declararion that allows record_name
record_spec -> '#' record_name : ['$2'].
record_spec -> '#' record_name exprs: ['$2' | '$3'].
record_spec -> '(' '#' record_name exprs ')': ['$3' | '$4'].

typed_record_spec -> atom ',' typed_record_fields : {typed_record, '$1', '$3'}.
typed_record_spec -> '#' record_name typed_record_fields : {typed_record, '$2', '$3'}.

typed_record_fields -> '{' typed_exprs '}' : {tuple, ?anno('$1'), '$2'}.

typed_exprs -> typed_expr : ['$1'].
@@ -189,9 +208,13 @@ type -> '#' '{' '}' : {type, ?anno('$1'), map, []}.
type -> '#' '{' map_pair_types '}' : {type, ?anno('$1'), map, '$3'}.
type -> '{' '}' : {type, ?anno('$1'), tuple, []}.
type -> '{' top_types '}' : {type, ?anno('$1'), tuple, '$2'}.
type -> '#' atom '{' '}' : {type, ?anno('$1'), record, ['$2']}.
type -> '#' atom '{' field_types '}' : {type, ?anno('$1'),
record, ['$2'|'$4']}.
type -> '#' record_name '{' '}' : {type, ?anno('$1'),
record,
[build_atom('$2')]}.
type -> '#' record_name '{' field_types '}' : {type, ?anno('$1'),
record,
[build_atom('$2')|'$4']}.

type -> binary_type : '$1'.
type -> integer : '$1'.
type -> char : '$1'.
@@ -311,10 +334,10 @@ pat_expr_max -> '(' pat_expr ')' : '$2'.
map_pat_expr -> '#' map_tuple :
{map, ?anno('$1'),'$2'}.

record_pat_expr -> '#' atom '.' atom :
{record_index,?anno('$1'),element(3, '$2'),'$4'}.
record_pat_expr -> '#' atom record_tuple :
{record,?anno('$1'),element(3, '$2'),'$3'}.
record_pat_expr -> '#' record_name '.' atom :
{record_index,?anno('$1'),record_name('$2'),'$4'}.
record_pat_expr -> '#' record_name record_tuple :
{record,?anno('$1'),record_name('$2'),'$3'}.

list -> '[' ']' : {nil,?anno('$1')}.
list -> '[' expr tail : {cons,?anno('$1'),'$2','$3'}.
@@ -400,18 +423,18 @@ map_key -> expr : '$1'.
%% N.B. Field names are returned as the complete object, even if they are
%% always atoms for the moment, this might change in the future.

record_expr -> '#' atom '.' atom :
{record_index,?anno('$1'),element(3, '$2'),'$4'}.
record_expr -> '#' atom record_tuple :
{record,?anno('$1'),element(3, '$2'),'$3'}.
record_expr -> expr_max '#' atom '.' atom :
{record_field,?anno('$2'),'$1',element(3, '$3'),'$5'}.
record_expr -> expr_max '#' atom record_tuple :
{record,?anno('$2'),'$1',element(3, '$3'),'$4'}.
record_expr -> record_expr '#' atom '.' atom :
{record_field,?anno('$2'),'$1',element(3, '$3'),'$5'}.
record_expr -> record_expr '#' atom record_tuple :
{record,?anno('$2'),'$1',element(3, '$3'),'$4'}.
record_expr -> '#' record_name '.' atom :
{record_index,?anno('$1'),record_name('$2'),'$4'}.
record_expr -> '#' record_name record_tuple :
{record,?anno('$1'),record_name('$2'),'$3'}.
record_expr -> expr_max '#' record_name '.' atom :
{record_field,?anno('$2'),'$1',record_name('$3'),'$5'}.
record_expr -> expr_max '#' record_name record_tuple :
{record,?anno('$2'),'$1',record_name('$3'),'$4'}.
record_expr -> record_expr '#' record_name '.' atom :
{record_field,?anno('$2'),'$1',record_name('$3'),'$5'}.
record_expr -> record_expr '#' record_name record_tuple :
{record,?anno('$2'),'$1',record_name('$3'),'$4'}.

record_tuple -> '{' '}' : [].
record_tuple -> '{' record_fields '}' : '$2'.
@@ -587,6 +610,40 @@ comp_op -> '>' : '$1'.
comp_op -> '=:=' : '$1'.
comp_op -> '=/=' : '$1'.

record_name -> atom : '$1'.
record_name -> var : '$1'.
record_name -> reserved_word : '$1'.

reserved_word -> 'after' : '$1'.
reserved_word -> 'and' : '$1'.
reserved_word -> 'andalso' : '$1'.
reserved_word -> 'band' : '$1'.
reserved_word -> 'begin' : '$1'.
reserved_word -> 'bnot' : '$1'.
reserved_word -> 'bor' : '$1'.
reserved_word -> 'bsl' : '$1'.
reserved_word -> 'bsr' : '$1'.
reserved_word -> 'bxor' : '$1'.
reserved_word -> 'case' : '$1'.
reserved_word -> 'catch' : '$1'.
reserved_word -> 'cond' : '$1'.
reserved_word -> 'div' : '$1'.
reserved_word -> 'else' : '$1'.
reserved_word -> 'end' : '$1'.
reserved_word -> 'fun' : '$1'.
reserved_word -> 'if' : '$1'.
reserved_word -> 'let' : '$1'.
reserved_word -> 'maybe' : '$1'.
reserved_word -> 'not' : '$1'.
reserved_word -> 'of' : '$1'.
reserved_word -> 'or' : '$1'.
reserved_word -> 'orelse' : '$1'.
reserved_word -> 'receive' : '$1'.
reserved_word -> 'rem' : '$1'.
reserved_word -> 'try' : '$1'.
reserved_word -> 'when' : '$1'.
reserved_word -> 'xor' : '$1'.

ssa_check_when_clauses -> ssa_check_when_clause : ['$1'].
ssa_check_when_clauses -> ssa_check_when_clause ssa_check_when_clauses :
['$1'|'$2'].
@@ -1303,6 +1360,10 @@ parse_form([{'-',A1},{atom,A2,callback}|Tokens]) ->
NewTokens = [{'-',A1},{'callback',A2}|Tokens],
?ANNO_CHECK(NewTokens),
parse(NewTokens);
parse_form([{'-',A1},{atom,A2,record}|Tokens]) ->
NewTokens = [{'-',A1},{'record',A2}|Tokens],
?ANNO_CHECK(NewTokens),
parse(NewTokens);
parse_form(Tokens) ->
?ANNO_CHECK(Tokens),
parse(Tokens).
@@ -1365,6 +1426,12 @@ parse_term(Tokens) ->
build_typed_attribute({atom,Aa,record},
{typed_record, {atom,_An,RecordName}, RecTuple}) ->
{attribute,Aa,record,{RecordName,record_tuple(RecTuple)}};
build_typed_attribute({atom,Aa,record},
{typed_record, {var,_An,RecordName}, RecTuple}) ->
{attribute,Aa,record,{RecordName,record_tuple(RecTuple)}};
build_typed_attribute({atom,Aa,record},
{typed_record, {ReservedWord,_An}, RecTuple}) ->
{attribute,Aa,record,{ReservedWord,record_tuple(RecTuple)}};
build_typed_attribute({atom,Aa,Attr},
{type_def, {call,_,{atom,_,TypeName},Args}, Type})
when Attr =:= 'type' ; Attr =:= 'opaque' ->
@@ -1376,7 +1443,7 @@ build_typed_attribute({atom,Aa,Attr},
"bad type variable")
end, Args),
{attribute,Aa,Attr,{TypeName,Type,Args}};
build_typed_attribute({atom,Aa,Attr}=Abstr,_) ->
build_typed_attribute({atom,Aa,Attr}=Abstr,_What) ->
case Attr of
record -> error_bad_decl(Abstr, record);
type -> error_bad_decl(Abstr, type);
@@ -1445,6 +1512,21 @@ build_bin_type([], Int) ->
build_bin_type([{var, Aa, _}|_], _) ->
ret_err(Aa, "Bad binary type").

build_atom({atom, _Aa, _Name} = Atom) -> Atom;
build_atom({ReservedWord, Aa}) -> {atom, Aa, ReservedWord};
build_atom({var, Aa, Name}) -> {atom, Aa, Name}.

record_name(RecordName) ->
case RecordName of
{atom, _Aa, Name} -> Name;
{var, _Aa, Name} -> Name;
{ReservedWord, _Aa} -> ReservedWord
end.

%print(X) ->
% io:format("Details: ~p~n",[X]),
% X.

build_type({atom, A, Name}, Types) ->
Tag = type_tag(Name, length(Types)),
{Tag, A, Name, Types}.
@@ -1491,6 +1573,10 @@ build_attribute({atom,Aa,record}, Val) ->
case Val of
[{atom,_An,Record},RecTuple] ->
{attribute,Aa,record,{Record,record_tuple(RecTuple)}};
[{var,_An,Record},RecTuple] ->
{attribute,Aa,record,{Record,record_tuple(RecTuple)}};
[{Record,_An},RecTuple] ->
{attribute,Aa,record,{Record,record_tuple(RecTuple)}};
[Other|_] -> error_bad_decl(Other, record)
end;
build_attribute({atom,Aa,file}, Val) ->
30 changes: 15 additions & 15 deletions lib/stdlib/src/erl_scan.erl
Original file line number Diff line number Diff line change
@@ -2160,30 +2160,30 @@ reserved_word(Atom) ->
%% reserved words.
-doc false.
f_reserved_word('after') -> true;
f_reserved_word('and') -> true;
f_reserved_word('andalso') -> true;
f_reserved_word('band') -> true;
f_reserved_word('begin') -> true;
f_reserved_word('bnot') -> true;
f_reserved_word('bor') -> true;
f_reserved_word('bsl') -> true;
f_reserved_word('bsr') -> true;
f_reserved_word('bxor') -> true;
f_reserved_word('case') -> true;
f_reserved_word('try') -> true;
f_reserved_word('cond') -> true;
f_reserved_word('catch') -> true;
f_reserved_word('andalso') -> true;
f_reserved_word('orelse') -> true;
f_reserved_word('cond') -> true;
f_reserved_word('div') -> true;
f_reserved_word('end') -> true;
f_reserved_word('fun') -> true;
f_reserved_word('if') -> true;
f_reserved_word('let') -> true;
f_reserved_word('not') -> true;
f_reserved_word('of') -> true;
f_reserved_word('or') -> true;
f_reserved_word('orelse') -> true;
f_reserved_word('receive') -> true;
f_reserved_word('when') -> true;
f_reserved_word('bnot') -> true;
f_reserved_word('not') -> true;
f_reserved_word('div') -> true;
f_reserved_word('rem') -> true;
f_reserved_word('band') -> true;
f_reserved_word('and') -> true;
f_reserved_word('bor') -> true;
f_reserved_word('bxor') -> true;
f_reserved_word('bsl') -> true;
f_reserved_word('bsr') -> true;
f_reserved_word('or') -> true;
f_reserved_word('try') -> true;
f_reserved_word('when') -> true;
f_reserved_word('xor') -> true;
f_reserved_word(_) -> false.
98 changes: 96 additions & 2 deletions lib/stdlib/test/erl_expand_records_SUITE.erl
Original file line number Diff line number Diff line change
@@ -39,7 +39,7 @@
-export([attributes/1, expr/1, guard/1,
init/1, pattern/1, strict/1, update/1,
otp_5915/1, otp_7931/1, otp_5990/1,
otp_7078/1, maps/1,
otp_7078/1, pr_7873/1, maps/1,
side_effects/1]).

init_per_testcase(_Case, Config) ->
@@ -59,7 +59,7 @@ all() ->

groups() ->
[{tickets, [],
[otp_5915, otp_7931, otp_5990, otp_7078]}].
[otp_5915, otp_7931, otp_5990, otp_7078, pr_7873]}].

init_per_suite(Config) ->
Config.
@@ -758,6 +758,100 @@ otp_7078(Config) when is_list(Config) ->
run(Config, Ts, [strict_record_tests]),
ok.

%% PR-7873. Reserved words and variable names as record names,
%% and record style record declarations
pr_7873(Config) when is_list(Config) ->
Words = [
<<"Abc">>,
<<"after">>,
<<"and">>,
<<"andalso">>,
<<"band">>,
<<"begin">>,
<<"bnot">>,
<<"bor">>,
<<"bsl">>,
<<"bsr">>,
<<"bxor">>,
<<"case">>,
<<"catch">>,
<<"cond">>,
<<"div">>,
<<"else">>,
<<"end">>,
<<"fun">>,
<<"if">>,
<<"let">>,
<<"maybe">>,
<<"not">>,
<<"of">>,
<<"or">>,
<<"orelse">>,
<<"receive">>,
<<"rem">>,
<<"try">>,
<<"when">>,
<<"xor">>
],

Declarations =
[~"-record('WORD', {a = 1}).",
~"-record('WORD', {a = 1 :: integer()}).",
~"-record 'WORD', {a = 1}.",
~"-record 'WORD', {a = 1 :: integer()}.",
~"-record(#WORD{a = 1}).",
~"-record(#WORD{a = 1 :: integer()}).",
~"-record #WORD{a = 1}.",
~"-record #WORD{a = 1 :: integer()}.",
~"-record # WORD{a = 1}.",
~"-record #WORD {a = 1}.",
~"-record # WORD {a = 1}.",
~"-record #'WORD'{a = 1}."],

Code =
~"""
-type x() :: #WORD{}.
t() ->
'WORD' = element(1, #WORD{}),
2 = #WORD.a,
A = #WORD{},
A = # WORD{},
A = #WORD {},
A = # WORD {},
_ = #WORD{a=5},
1 = A#WORD.a,
_ = A#WORD{},
C = A#WORD{a = 2},
2 = C#WORD.a,
#WORD{a = X} = C,
2 = X,
D = #WORD{a = 2}#WORD{a = 3},
4 = D#WORD{a = 4}#WORD.a,
3 = match1(D),
ok = match2(D, 3),
ok = match3(#WORD{a=#WORD{}}),
ok.
-spec match1(x()) -> any().
match1(#WORD{a = X}) -> X.
-spec match2(#WORD{}, any()) -> ok.
match2(Rec, V) when Rec#WORD.a == V -> ok.
match3(#WORD{a=#WORD{}}) -> ok.
""",
Ts =
[binary:replace(
<<Hdr/binary, Code/binary>>, <<"WORD">>, Word, [global])
|| Hdr <- Declarations,
Word <- Words],

run(Config, Ts, [strict_record_tests]),
ok.

id(I) -> I.

-record(side_effects, {a,b,c}).
22 changes: 14 additions & 8 deletions lib/tools/emacs/erlang.el
Original file line number Diff line number Diff line change
@@ -1166,8 +1166,9 @@ behaviour.")

(defvar erlang-font-lock-keywords-operators
(list
(list erlang-operators-regexp
1 'font-lock-builtin-face))
(list erlang-operators-regexp 1 'font-lock-builtin-face)
;; Don't highlight record names
(list (concat "#\\s-*" erlang-operators-regexp) 1 nil t))
"Font lock keyword highlighting Erlang operators.")

(defvar erlang-font-lock-keywords-dollar
@@ -1188,12 +1189,15 @@ behaviour.")

(defvar erlang-font-lock-keywords-keywords
(list
(list erlang-keywords-regexp 1 'font-lock-keyword-face))
(list erlang-keywords-regexp 1 'font-lock-keyword-face)
;; Don't highlight record names
(list (concat "#\\s-*" erlang-keywords-regexp) 1 nil t))
"Font lock keyword highlighting Erlang keywords.")

(defvar erlang-font-lock-keywords-attr
(list
(list (concat "^\\(-" erlang-atom-regexp "\\)\\(\\s-\\|\\.\\|(\\)")
(list (concat "\\(?:^\\s-*\\|\\.\\s-+\\)"
"\\(-" erlang-atom-regexp "\\)\\(\\s-\\|\\.\\|(\\|#\\)")
1 (if (boundp 'font-lock-preprocessor-face)
'font-lock-preprocessor-face
'font-lock-constant-face)))
@@ -1240,15 +1244,17 @@ This must be placed in front of `erlang-font-lock-keywords-vars'.")

(defvar erlang-font-lock-keywords-records
(list
(list (concat "#\\s *" erlang-atom-regexp)
1 'font-lock-type-face)
(list (concat "#\\s-*\\(" erlang-atom-regexp "\\|"
erlang-variable-regexp "\\)")
1 'font-lock-type-face)
;; Don't highlight numerical constants.
(list (if erlang-regexp-modern-p
"\\_<\\([0-9]+\\(_[0-9]+\\)*#[0-9a-zA-Z]+\\(_[0-9a-zA-Z]+\\)*\\)"
"\\<\\([0-9]+\\(_[0-9]+\\)*#[0-9a-zA-Z]+\\(_[0-9a-zA-Z]+\\)*\\)")
1 nil t)
(list (concat "^-record\\s-*(\\s-*" erlang-atom-regexp)
1 'font-lock-type-face))
(list (concat "\\(?:^\\s-*\\|\\.\\s-*\\)"
"-record\\s-*\\(?:(\\|\\s-+\\)\\s-*" erlang-atom-regexp)
1 'font-lock-type-face))
"Font lock keyword highlighting Erlang records.
This must be placed in front of `erlang-font-lock-keywords-vars'.")

45 changes: 45 additions & 0 deletions system/doc/reference_manual/ref_man_records.md
Original file line number Diff line number Diff line change
@@ -39,6 +39,15 @@ used.
FieldN [= ExprN]}).
```

> #### Change {: .info }
>
> Since OTP 28.0 the record creation syntax is allowed when defining a record:
> ```erlang
> -record #Name{Field1 [= Expr1],
> ...
> FieldN [= ExprN]}).
> ```
The default value for a field is an arbitrary expression, except that it must
not use any variables.
@@ -230,3 +239,39 @@ record_info(size, Record) -> Size

`Size` is the size of the tuple representation, that is, one more than the
number of fields.

## Record name quoting

A record name is an atom. Atoms can be quoted.
[Reserved words](reference_manual.md#reserved-words) and variable
names are not atoms, unless quoted.

For example `div` is the integer division operator so it cannot be used
as a record name unless quoted:

``` erlang
-record('div', {field :: integer()}).

foo() -> #'div'{field = 17}.
```

The same applies to a variable name such as `Var`:

``` erlang
-record('Var', {field :: integer()}).

foo() -> #'Var'{field = 4711}.
```

> #### Change {: .info }
>
> Since OTP-28.0, a name after the `#` operator doesn't have to be quoted,
> and since record definition can be done with record creation syntax,
> this also works:
> ``` erlang
> -record #div{field :: integer()}).
> -record #Var{field :: integer()}).
>
> foo() -> #div{field = 17}.
> bar() -> #Var{field = 4711}.
> ```

Unchanged files with check annotations Beta

F = fun() -> a end,
LocalFun = term_to_string(F),
S = LocalFun ++ ".",
"1:2: syntax error before: Fun" = comm_err(S)

Check warning on line 3146 in lib/stdlib/test/shell_SUITE.erl

GitHub Actions / CT Test Results

otp_14296 failed

artifacts/Unit Test Results/stdlib_junit.xml [took 0s]
Raw output
Test otp_14296 in shell_SUITE failed!
{{badmatch,"1:5: syntax error before: '<'"},
 [{shell_SUITE,otp_14296,1,[{file,"shell_SUITE.erl"},{line,3146}]},
  {test_server,ts_tc,3,[{file,"test_server.erl"},{line,1794}]},
  {test_server,run_test_case_eval1,6,[{file,"test_server.erl"},{line,1303}]},
  {test_server,run_test_case_eval,9,[{file,"test_server.erl"},{line,1235}]}]}
end(),
fun() ->