From 06262aeb4653f52cdd8a77f525e55382a7d9ae00 Mon Sep 17 00:00:00 2001 From: Jesse Gumm Date: Wed, 15 Nov 2023 15:30:27 -0600 Subject: [PATCH 1/7] Allow reserved words as record names w/out quotes This motivation for this comes from the Nitrogen Web Frameworks's heavy use of records, and a clash between HTML
element and the Erlang div operator. In Nitrogen, HTML elements are represented by Erlang records. For example: the HTML `` element is `#span{}` in Nitrogen. Logically, the heavily-used HTML `
` element would be represented by `#div{}` in Nitrogen, however, that specific syntax is illegal due to `div`'s reserved word status, and must instead be represented with `#'div'`. This syntax, however, is awkward, and has led to a workaround that *works*, but is itself awkward (using the term `#panel{}` instead of `#div{}` - but this in itself leads to a semantic clash, as some frontend HTML frameworks have their own 'panel' elements that might ideally be abstracted into a `#panel{}` element. But, As far as I understand, there is no potential syntax clash in allowing the syntax `#div` ito be acceptable, and have the parser recognize that the `div` (or any reserved word) in that `context` can only be an atom, and would never be an operator. So this change tweaks the grammar to recognize the circumstances of: `#reserved_word{}`. Further, this change does not change the way the records are defined (so the definition must still be defined with the atom properly wrapped in quotes (e.g. `-record('div', {a,b}).`). This PR also adds the appropriate tests in `erl_expand_records_SUITE`, which I wasn't sure if that was appropriate place, but it seemed the most relevant. --- lib/stdlib/src/erl_parse.yrl | 59 +++++++++++++++- lib/stdlib/test/erl_expand_records_SUITE.erl | 74 +++++++++++++++++++- 2 files changed, 130 insertions(+), 3 deletions(-) diff --git a/lib/stdlib/src/erl_parse.yrl b/lib/stdlib/src/erl_parse.yrl index 01540c00db94..e5c35ed2f165 100644 --- a/lib/stdlib/src/erl_parse.yrl +++ b/lib/stdlib/src/erl_parse.yrl @@ -34,7 +34,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 res_rec 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 @@ -192,6 +192,11 @@ 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 -> '#' res_rec '{' '}' : {type, ?anno('$1'), + record, [build_atom('$2')]}. +type -> '#' res_rec '{' field_types '}' : {type, ?anno('$1'), + record, [build_atom('$2')|'$4']}. + type -> binary_type : '$1'. type -> integer : '$1'. type -> char : '$1'. @@ -315,6 +320,10 @@ 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 -> '#' res_rec'.' atom : + {record_index,?anno('$1'),element(1, '$2'),'$4'}. +record_pat_expr -> '#' res_rec record_tuple : + {record,?anno('$1'),element(1, '$2'),'$3'}. list -> '[' ']' : {nil,?anno('$1')}. list -> '[' expr tail : {cons,?anno('$1'),'$2','$3'}. @@ -413,6 +422,19 @@ record_expr -> record_expr '#' atom '.' atom : record_expr -> record_expr '#' atom record_tuple : {record,?anno('$2'),'$1',element(3, '$3'),'$4'}. +record_expr -> '#' res_rec '.' atom : + {record_index,?anno('$1'),element(1, '$2'),'$4'}. +record_expr -> '#' res_rec record_tuple : + {record, ?anno('$1'), element(1, '$2'), '$3'}. +record_expr -> expr_max '#' res_rec '.' atom : + {record_field,?anno('$2'),'$1',element(1, '$3'),'$5'}. +record_expr -> expr_max '#' res_rec record_tuple : + {record,?anno('$2'),'$1',element(1, '$3'),'$4'}. +record_expr -> record_expr '#' res_rec '.' atom : + {record_field,?anno('$2'),'$1',element(1, '$3'),'$5'}. +record_expr -> record_expr '#' res_rec record_tuple : + {record,?anno('$2'),'$1',element(1, '$3'),'$4'}. + record_tuple -> '{' '}' : []. record_tuple -> '{' record_fields '}' : '$2'. @@ -587,6 +609,34 @@ comp_op -> '>' : '$1'. comp_op -> '=:=' : '$1'. comp_op -> '=/=' : '$1'. +res_rec -> 'after' : '$1'. +res_rec -> 'begin' : '$1'. +res_rec -> 'case' : '$1'. +res_rec -> 'try' : '$1'. +res_rec -> 'catch' : '$1'. +res_rec -> 'end' : '$1'. +res_rec -> 'fun' : '$1'. +res_rec -> 'if' : '$1'. +res_rec -> 'of' : '$1'. +res_rec -> 'receive' : '$1'. +res_rec -> 'when' : '$1'. +res_rec -> 'maybe' : '$1'. +res_rec -> 'else' : '$1'. +res_rec -> 'andalso' : '$1'. +res_rec -> 'orelse' : '$1'. +res_rec -> 'bnot' : '$1'. +res_rec -> 'not' : '$1'. +res_rec -> 'div' : '$1'. +res_rec -> 'rem' : '$1'. +res_rec -> 'band' : '$1'. +res_rec -> 'and' : '$1'. +res_rec -> 'bor' : '$1'. +res_rec -> 'bxor' : '$1'. +res_rec -> 'bsl' : '$1'. +res_rec -> 'bsr' : '$1'. +res_rec -> 'or' : '$1'. +res_rec -> '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']. @@ -1445,6 +1495,13 @@ build_bin_type([], Int) -> build_bin_type([{var, Aa, _}|_], _) -> ret_err(Aa, "Bad binary type"). +build_atom({Atom, Aa}) -> + {atom, Aa, Atom}. + +%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}. diff --git a/lib/stdlib/test/erl_expand_records_SUITE.erl b/lib/stdlib/test/erl_expand_records_SUITE.erl index fe055e03f773..13df96b9560e 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,76 @@ otp_7078(Config) when is_list(Config) -> run(Config, Ts, [strict_record_tests]), ok. +%% PR-7873. Reserved words as record names. +pr_7873(Config) when is_list(Config) -> + Words = [ + <<"after">>, + <<"begin">>, + <<"case">>, + <<"try">>, + <<"catch">>, + <<"end">>, + <<"fun">>, + <<"if">>, + <<"of">>, + <<"receive">>, + <<"when">>, + <<"maybe">>, + <<"else">>, + <<"andalso">>, + <<"orelse">>, + <<"bnot">>, + <<"not">>, + <<"div">>, + <<"rem">>, + <<"band">>, + <<"and">>, + <<"bor">>, + <<"bxor">>, + <<"bsl">>, + <<"bsr">>, + <<"or">>, + <<"xor">> + ], + + Code = <<" + -record('WORD', {a = 1}). + + -type x() :: #WORD{}. + + t() -> + 'WORD' = element(1, #WORD{}), + 2 = #WORD.a, + 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. + ">>, + F = fun(Word) -> + binary:replace(Code, <<"WORD">>, Word, [global]) + end, + Ts = lists:map(F, Words), + run(Config, Ts, [strict_record_tests]), + ok. + id(I) -> I. -record(side_effects, {a,b,c}). From dde9f78ec0f315a94eb8c7bbd546b3a793c4828e Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Fri, 6 Sep 2024 14:54:40 +0200 Subject: [PATCH 2/7] Improve naming, include 'cond' and 'let' --- lib/stdlib/src/erl_parse.yrl | 80 ++++++++++++++++++------------------ lib/stdlib/src/erl_scan.erl | 30 +++++++------- 2 files changed, 56 insertions(+), 54 deletions(-) diff --git a/lib/stdlib/src/erl_parse.yrl b/lib/stdlib/src/erl_parse.yrl index e5c35ed2f165..350dbffaeda6 100644 --- a/lib/stdlib/src/erl_parse.yrl +++ b/lib/stdlib/src/erl_parse.yrl @@ -34,7 +34,7 @@ list_comprehension lc_expr lc_exprs map_comprehension binary_comprehension tuple -record_expr record_tuple record_field record_fields res_rec +record_expr record_tuple record_field record_fields reserved_word 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 @@ -81,7 +81,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' @@ -192,9 +192,9 @@ 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 -> '#' res_rec '{' '}' : {type, ?anno('$1'), +type -> '#' reserved_word '{' '}' : {type, ?anno('$1'), record, [build_atom('$2')]}. -type -> '#' res_rec '{' field_types '}' : {type, ?anno('$1'), +type -> '#' reserved_word '{' field_types '}' : {type, ?anno('$1'), record, [build_atom('$2')|'$4']}. type -> binary_type : '$1'. @@ -320,9 +320,9 @@ 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 -> '#' res_rec'.' atom : +record_pat_expr -> '#' reserved_word '.' atom : {record_index,?anno('$1'),element(1, '$2'),'$4'}. -record_pat_expr -> '#' res_rec record_tuple : +record_pat_expr -> '#' reserved_word record_tuple : {record,?anno('$1'),element(1, '$2'),'$3'}. list -> '[' ']' : {nil,?anno('$1')}. @@ -422,17 +422,17 @@ record_expr -> record_expr '#' atom '.' atom : record_expr -> record_expr '#' atom record_tuple : {record,?anno('$2'),'$1',element(3, '$3'),'$4'}. -record_expr -> '#' res_rec '.' atom : +record_expr -> '#' reserved_word '.' atom : {record_index,?anno('$1'),element(1, '$2'),'$4'}. -record_expr -> '#' res_rec record_tuple : +record_expr -> '#' reserved_word record_tuple : {record, ?anno('$1'), element(1, '$2'), '$3'}. -record_expr -> expr_max '#' res_rec '.' atom : +record_expr -> expr_max '#' reserved_word '.' atom : {record_field,?anno('$2'),'$1',element(1, '$3'),'$5'}. -record_expr -> expr_max '#' res_rec record_tuple : +record_expr -> expr_max '#' reserved_word record_tuple : {record,?anno('$2'),'$1',element(1, '$3'),'$4'}. -record_expr -> record_expr '#' res_rec '.' atom : +record_expr -> record_expr '#' reserved_word '.' atom : {record_field,?anno('$2'),'$1',element(1, '$3'),'$5'}. -record_expr -> record_expr '#' res_rec record_tuple : +record_expr -> record_expr '#' reserved_word record_tuple : {record,?anno('$2'),'$1',element(1, '$3'),'$4'}. record_tuple -> '{' '}' : []. @@ -609,33 +609,35 @@ comp_op -> '>' : '$1'. comp_op -> '=:=' : '$1'. comp_op -> '=/=' : '$1'. -res_rec -> 'after' : '$1'. -res_rec -> 'begin' : '$1'. -res_rec -> 'case' : '$1'. -res_rec -> 'try' : '$1'. -res_rec -> 'catch' : '$1'. -res_rec -> 'end' : '$1'. -res_rec -> 'fun' : '$1'. -res_rec -> 'if' : '$1'. -res_rec -> 'of' : '$1'. -res_rec -> 'receive' : '$1'. -res_rec -> 'when' : '$1'. -res_rec -> 'maybe' : '$1'. -res_rec -> 'else' : '$1'. -res_rec -> 'andalso' : '$1'. -res_rec -> 'orelse' : '$1'. -res_rec -> 'bnot' : '$1'. -res_rec -> 'not' : '$1'. -res_rec -> 'div' : '$1'. -res_rec -> 'rem' : '$1'. -res_rec -> 'band' : '$1'. -res_rec -> 'and' : '$1'. -res_rec -> 'bor' : '$1'. -res_rec -> 'bxor' : '$1'. -res_rec -> 'bsl' : '$1'. -res_rec -> 'bsr' : '$1'. -res_rec -> 'or' : '$1'. -res_rec -> 'xor' : '$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 : 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. From 2b0e1ca01f4b148aae5ed2796cccb02643857aff Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Mon, 9 Sep 2024 13:54:24 +0200 Subject: [PATCH 3/7] Allow variable names as record names w/o quotes --- lib/stdlib/src/erl_parse.yrl | 81 +++++++++----------- lib/stdlib/test/erl_expand_records_SUITE.erl | 63 +++++++++------ 2 files changed, 77 insertions(+), 67 deletions(-) diff --git a/lib/stdlib/src/erl_parse.yrl b/lib/stdlib/src/erl_parse.yrl index 350dbffaeda6..d9af0227be3d 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 reserved_word +record_expr record_tuple record_field record_fields record_name 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 @@ -189,13 +190,12 @@ 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 -> '#' reserved_word '{' '}' : {type, ?anno('$1'), - record, [build_atom('$2')]}. -type -> '#' reserved_word '{' field_types '}' : {type, ?anno('$1'), - record, [build_atom('$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'. @@ -316,14 +316,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 -> '#' reserved_word '.' atom : - {record_index,?anno('$1'),element(1, '$2'),'$4'}. -record_pat_expr -> '#' reserved_word record_tuple : - {record,?anno('$1'),element(1, '$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'}. @@ -409,31 +405,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 -> '#' reserved_word '.' atom : - {record_index,?anno('$1'),element(1, '$2'),'$4'}. -record_expr -> '#' reserved_word record_tuple : - {record, ?anno('$1'), element(1, '$2'), '$3'}. -record_expr -> expr_max '#' reserved_word '.' atom : - {record_field,?anno('$2'),'$1',element(1, '$3'),'$5'}. -record_expr -> expr_max '#' reserved_word record_tuple : - {record,?anno('$2'),'$1',element(1, '$3'),'$4'}. -record_expr -> record_expr '#' reserved_word '.' atom : - {record_field,?anno('$2'),'$1',element(1, '$3'),'$5'}. -record_expr -> record_expr '#' reserved_word record_tuple : - {record,?anno('$2'),'$1',element(1, '$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'. @@ -609,6 +592,10 @@ 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'. @@ -1497,8 +1484,16 @@ build_bin_type([], Int) -> build_bin_type([{var, Aa, _}|_], _) -> ret_err(Aa, "Bad binary type"). -build_atom({Atom, Aa}) -> - {atom, Aa, Atom}. +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]), diff --git a/lib/stdlib/test/erl_expand_records_SUITE.erl b/lib/stdlib/test/erl_expand_records_SUITE.erl index 13df96b9560e..b1f2ebf8c0ed 100644 --- a/lib/stdlib/test/erl_expand_records_SUITE.erl +++ b/lib/stdlib/test/erl_expand_records_SUITE.erl @@ -758,40 +758,49 @@ otp_7078(Config) when is_list(Config) -> run(Config, Ts, [strict_record_tests]), ok. -%% PR-7873. Reserved words as record names. +%% PR-7873. Reserved words and variable names as record names pr_7873(Config) when is_list(Config) -> Words = [ + <<"Abc">>, <<"after">>, + <<"and">>, + <<"andalso">>, + <<"band">>, <<"begin">>, + <<"bnot">>, + <<"bor">>, + <<"bsl">>, + <<"bsr">>, + <<"bxor">>, <<"case">>, - <<"try">>, <<"catch">>, + <<"cond">>, + <<"div">>, + <<"else">>, <<"end">>, <<"fun">>, <<"if">>, - <<"of">>, - <<"receive">>, - <<"when">>, + <<"let">>, <<"maybe">>, - <<"else">>, - <<"andalso">>, - <<"orelse">>, - <<"bnot">>, <<"not">>, - <<"div">>, - <<"rem">>, - <<"band">>, - <<"and">>, - <<"bor">>, - <<"bxor">>, - <<"bsl">>, - <<"bsr">>, + <<"of">>, <<"or">>, + <<"orelse">>, + <<"receive">>, + <<"rem">>, + <<"try">>, + <<"when">>, <<"xor">> ], - Code = <<" - -record('WORD', {a = 1}). + Declarations = + [~"-record('WORD', {a = 1}).", + ~"-record('WORD', {a = 1 :: integer()}).", + ~"-record 'WORD', {a = 1}.", + ~"-record 'WORD', {a = 1 :: integer()}."], + + Code = + ~""" -type x() :: #WORD{}. @@ -799,6 +808,9 @@ pr_7873(Config) when is_list(Config) -> 'WORD' = element(1, #WORD{}), 2 = #WORD.a, A = #WORD{}, + A = # WORD{}, + A = #WORD {}, + A = # WORD {}, _ = #WORD{a=5}, 1 = A#WORD.a, _ = A#WORD{}, @@ -820,11 +832,14 @@ pr_7873(Config) when is_list(Config) -> match2(Rec, V) when Rec#WORD.a == V -> ok. match3(#WORD{a=#WORD{}}) -> ok. - ">>, - F = fun(Word) -> - binary:replace(Code, <<"WORD">>, Word, [global]) - end, - Ts = lists:map(F, Words), + + """, + Ts = + [binary:replace( + <>, <<"WORD">>, Word, [global]) + || Hdr <- Declarations, + Word <- Words], + run(Config, Ts, [strict_record_tests]), ok. From 7af6208d31b44cdb1a94a56e1813cc53ddf97e84 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Tue, 10 Sep 2024 09:14:21 +0200 Subject: [PATCH 4/7] Implement record style record definition, for all record names --- lib/stdlib/src/erl_parse.yrl | 40 ++++++++++++++++++-- lib/stdlib/test/erl_expand_records_SUITE.erl | 13 ++++++- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/lib/stdlib/src/erl_parse.yrl b/lib/stdlib/src/erl_parse.yrl index d9af0227be3d..69ef1303b1aa 100644 --- a/lib/stdlib/src/erl_parse.yrl +++ b/lib/stdlib/src/erl_parse.yrl @@ -35,7 +35,7 @@ list_comprehension lc_expr lc_exprs map_comprehension binary_comprehension tuple -record_expr record_tuple record_field record_fields record_name +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 @@ -48,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 @@ -92,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%'. @@ -128,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'). @@ -140,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']. @@ -1342,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). @@ -1404,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' -> @@ -1415,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); @@ -1545,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/test/erl_expand_records_SUITE.erl b/lib/stdlib/test/erl_expand_records_SUITE.erl index b1f2ebf8c0ed..11878e1ade3d 100644 --- a/lib/stdlib/test/erl_expand_records_SUITE.erl +++ b/lib/stdlib/test/erl_expand_records_SUITE.erl @@ -758,7 +758,8 @@ otp_7078(Config) when is_list(Config) -> run(Config, Ts, [strict_record_tests]), ok. -%% PR-7873. Reserved words and variable names as record names +%% PR-7873. Reserved words and variable names as record names, +%% and record style record declarations pr_7873(Config) when is_list(Config) -> Words = [ <<"Abc">>, @@ -797,7 +798,15 @@ pr_7873(Config) when is_list(Config) -> [~"-record('WORD', {a = 1}).", ~"-record('WORD', {a = 1 :: integer()}).", ~"-record 'WORD', {a = 1}.", - ~"-record 'WORD', {a = 1 :: integer()}."], + ~"-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 = ~""" From d9429e73e020afa0d2d5807148c9150ea9240b25 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Wed, 11 Sep 2024 15:15:45 +0200 Subject: [PATCH 5/7] Update reference manual about relaxed record name quoting --- .../doc/reference_manual/ref_man_records.md | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) 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}. +> ``` From f45d691599580aa4f204ee70f4abea7a46b7a426 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Fri, 13 Sep 2024 14:59:24 +0200 Subject: [PATCH 6/7] Font lock augmented record names --- lib/tools/emacs/erlang.el | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/tools/emacs/erlang.el b/lib/tools/emacs/erlang.el index fa041a8e5558..1484ddae43ca 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,14 @@ 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 "^\\(-" erlang-atom-regexp "\\)\\(\\s-\\|\\.\\|(\\|#\\)") 1 (if (boundp 'font-lock-preprocessor-face) 'font-lock-preprocessor-face 'font-lock-constant-face))) @@ -1240,14 +1243,15 @@ 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) + (list (concat "^-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'.") From 539299e3d3ebda83065dfeeaf7745f0fa6a1e944 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Fri, 13 Sep 2024 15:55:09 +0200 Subject: [PATCH 7/7] Improve attribute regexp for multiple on one row --- lib/tools/emacs/erlang.el | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/tools/emacs/erlang.el b/lib/tools/emacs/erlang.el index 1484ddae43ca..452bd971100c 100644 --- a/lib/tools/emacs/erlang.el +++ b/lib/tools/emacs/erlang.el @@ -1196,7 +1196,8 @@ behaviour.") (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))) @@ -1251,8 +1252,9 @@ This must be placed in front of `erlang-font-lock-keywords-vars'.") "\\_<\\([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-+\\)\\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'.")