diff --git a/lib/stdlib/src/erl_parse.yrl b/lib/stdlib/src/erl_parse.yrl index 01540c00db94..69ef1303b1aa 100644 --- a/lib/stdlib/src/erl_parse.yrl +++ b/lib/stdlib/src/erl_parse.yrl @@ -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) -> diff --git a/lib/stdlib/src/erl_scan.erl b/lib/stdlib/src/erl_scan.erl index 899785ae3d8a..b61e34d1839d 100644 --- a/lib/stdlib/src/erl_scan.erl +++ b/lib/stdlib/src/erl_scan.erl @@ -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. diff --git a/lib/stdlib/test/erl_expand_records_SUITE.erl b/lib/stdlib/test/erl_expand_records_SUITE.erl index fe055e03f773..11878e1ade3d 100644 --- a/lib/stdlib/test/erl_expand_records_SUITE.erl +++ b/lib/stdlib/test/erl_expand_records_SUITE.erl @@ -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( + <>, <<"WORD">>, Word, [global]) + || Hdr <- Declarations, + Word <- Words], + + run(Config, Ts, [strict_record_tests]), + ok. + id(I) -> I. -record(side_effects, {a,b,c}). diff --git a/lib/tools/emacs/erlang.el b/lib/tools/emacs/erlang.el index fa041a8e5558..452bd971100c 100644 --- a/lib/tools/emacs/erlang.el +++ b/lib/tools/emacs/erlang.el @@ -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'.") diff --git a/system/doc/reference_manual/ref_man_records.md b/system/doc/reference_manual/ref_man_records.md index 3e6536749a8c..8b9d931e03d6 100644 --- a/system/doc/reference_manual/ref_man_records.md +++ b/system/doc/reference_manual/ref_man_records.md @@ -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}. +> ```