From ae120c0b22d8888bfb4df5b232bbe605f73d76f0 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Tue, 3 Sep 2024 15:20:46 +0300 Subject: [PATCH 1/5] Match on variables instead of expressions --- README.md | 3 +- exploration/registry-xml/README.md | 3 +- exploration/selection-declaration.md | 8 ++- spec/data-model/README.md | 2 +- spec/data-model/message.dtd | 2 +- spec/data-model/message.json | 2 +- spec/errors.md | 50 +++++++++++------ spec/formatting.md | 41 +++++++------- spec/message.abnf | 6 +- spec/registry.md | 18 +++--- spec/syntax.md | 35 +++++------- test/tests/data-model-errors.json | 20 +++---- test/tests/functions/integer.json | 2 +- test/tests/functions/string.json | 8 +-- test/tests/pattern-selection.json | 74 ++++++++----------------- test/tests/syntax-errors.json | 37 ++++++------- test/tests/syntax.json | 14 ++--- test/tests/unsupported-expressions.json | 4 +- 18 files changed, 157 insertions(+), 172 deletions(-) diff --git a/README.md b/README.md index 2dc98ba327..6664279472 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,8 @@ Functions can optionally take _options_: Messages can use a _selector_ to choose between different _variants_, which correspond to the grammatical (or other) requirements of the language: - .match {$count :integer} + .input {$count :integer} + .match $count 0 {{You have no notifications.}} one {{You have {$count} notification.}} * {{You have {$count} notifications.}} diff --git a/exploration/registry-xml/README.md b/exploration/registry-xml/README.md index 75b0490417..a3a3a6890c 100644 --- a/exploration/registry-xml/README.md +++ b/exploration/registry-xml/README.md @@ -163,7 +163,8 @@ For the sake of brevity, only `locales="en"` is considered. Given the above description, the `:number` function is defined to work both in a selector and a placeholder: ``` -.match {$count :number} +.input {$count :number} +.match $count 1 {{One new message}} * {{{$count :number} new messages}} ``` diff --git a/exploration/selection-declaration.md b/exploration/selection-declaration.md index 8035b2943b..39215cc184 100644 --- a/exploration/selection-declaration.md +++ b/exploration/selection-declaration.md @@ -1,6 +1,6 @@ # Effect of Selectors on Subsequent Placeholders -Status: **Proposed, Ballot Requested** +Status: **Accepted**
Metadata @@ -12,6 +12,12 @@ Status: **Proposed, Ballot Requested**
Pull Requests
#755
#824
+
#860
+
#867
+
#877
+
Ballot
+
#872 (discussion)
+
#873 (voting)
diff --git a/spec/data-model/README.md b/spec/data-model/README.md index 9084e02039..f45fdc2104 100644 --- a/spec/data-model/README.md +++ b/spec/data-model/README.md @@ -85,7 +85,7 @@ interface PatternMessage { interface SelectMessage { type: "select"; declarations: Declaration[]; - selectors: Expression[]; + selectors: VariableRef[]; variants: Variant[]; } ``` diff --git a/spec/data-model/message.dtd b/spec/data-model/message.dtd index 6c49c358a4..7bba823fb5 100644 --- a/spec/data-model/message.dtd +++ b/spec/data-model/message.dtd @@ -16,7 +16,7 @@ body CDATA #IMPLIED > - + diff --git a/spec/data-model/message.json b/spec/data-model/message.json index ae2867d2c1..3df78cd52f 100644 --- a/spec/data-model/message.json +++ b/spec/data-model/message.json @@ -201,7 +201,7 @@ "declarations": { "$ref": "#/$defs/declarations" }, "selectors": { "type": "array", - "items": { "$ref": "#/$defs/expression" } + "items": { "$ref": "#/$defs/variable" } }, "variants": { "type": "array", diff --git a/spec/errors.md b/spec/errors.md index a2ddd669d3..347d7b2a24 100644 --- a/spec/errors.md +++ b/spec/errors.md @@ -46,7 +46,7 @@ or contains some error which leads to further errors, an implementation which does not emit all of the errors SHOULD prioritise _Syntax Errors_ and _Data Model Errors_ over others. -When an error occurs within a _selector_, +When an error occurs while resolving a _selector_, the _selector_ MUST NOT match any _variant_ _key_ other than the catch-all `*` and a _Resolution Error_ or a _Message Function Error_ MUST be emitted. @@ -85,13 +85,16 @@ does not equal the number of _selectors_. > Example invalid messages resulting in a _Variant Key Mismatch_ error: > > ``` -> .match {$one :func} +> .input {$one :func} +> .match $one > 1 2 {{Too many}} > * {{Otherwise}} > ``` > > ``` -> .match {$one :func} {$two :func} +> .input {$one :func} +> .input {$two :func} +> .match $one $two > 1 2 {{Two keys}} > * {{Missing a key}} > * * {{Otherwise}} @@ -105,13 +108,16 @@ does not include a _variant_ with only catch-all keys. > Example invalid messages resulting in a _Missing Fallback Variant_ error: > > ``` -> .match {$one :func} +> .input {$one :func} +> .match $one > 1 {{Value is one}} > 2 {{Value is two}} > ``` > > ``` -> .match {$one :func} {$two :func} +> .input {$one :func} +> .input {$two :func} +> .match $one $two > 1 * {{First is one}} > * 1 {{Second is one}} > ``` @@ -119,27 +125,27 @@ does not include a _variant_ with only catch-all keys. ### Missing Selector Annotation A **_Missing Selector Annotation_** error occurs when the _message_ -contains a _selector_ that does not have an _annotation_, -or contains a _variable_ that does not directly or indirectly reference a _declaration_ with an _annotation_. +contains a _selector_ that does not +directly or indirectly reference a _declaration_ with an _annotation_. > Examples of invalid messages resulting in a _Missing Selector Annotation_ error: > > ``` -> .match {$one} +> .match $one > 1 {{Value is one}} > * {{Value is not one}} > ``` > > ``` > .local $one = {|The one|} -> .match {$one} +> .match $one > 1 {{Value is one}} > * {{Value is not one}} > ``` > > ``` > .input {$one} -> .match {$one} +> .match $one > 1 {{Value is one}} > * {{Value is not one}} > ``` @@ -199,13 +205,16 @@ same list of _keys_ is used for more than one _variant_. > Examples of invalid messages resulting in a _Duplicate Variant_ error: > > ``` -> .match {$var :string} +> .input {$var :string} +> .match $var > * {{The first default}} > * {{The second default}} > ``` > > ``` -> .match {$x :string} {$y :string} +> .input {$x :string} +> .input {$y :string} +> .match $x $y > * foo {{The first "foo" variant}} > bar * {{The "bar" variant}} > * |foo| {{The second "foo" variant}} @@ -230,7 +239,8 @@ An **_Unresolved Variable_** error occurs when a variable reference c > ``` > > ``` -> .match {$var :func} +> .input {$var :func} +> .match $var > 1 {{The value is one.}} > * {{The value is not one.}} > ``` @@ -249,7 +259,8 @@ a reference to a function which cannot be resolved. > ``` > > ``` -> .match {|horse| :func} +> .local $horse = {|horse| :func} +> .match $horse > 1 {{The value is one.}} > * {{The value is not one.}} > ``` @@ -272,7 +283,8 @@ or for private implementation use that is not supported by the current implement > if done within a context that does not support the `^` private use sigil: > > ``` -> .match {|horse| ^private} +> .local $horse = {|horse| ^private} +> .match $horse > 1 {{The value is one.}} > * {{The value is not one.}} > ``` @@ -299,7 +311,7 @@ with a resolved value which does not support selection. > > ``` > .local $day = {|2024-05-01| :date} -> .match {$day} +> .match $day > * {{The due date is {$day}}} > ``` @@ -361,7 +373,8 @@ for that specific _function_. > ``` > > ``` -> .match {|horse| :number} +> .local $horse = {|horse| :number} +> .match $horse > 1 {{The value is one.}} > * {{The value is not one.}} > ``` @@ -398,7 +411,8 @@ does not match the expected implementation-defined format. > which is a requirement of the `:number` function: > > ``` -> .match {42 :number} +> .local $answer = {42 :number} +> .match $answer > 1 {{The value is one.}} > horse {{The value is a horse.}} > * {{The value is not one.}} diff --git a/spec/formatting.md b/spec/formatting.md index 5e71d24a35..480e93e31f 100644 --- a/spec/formatting.md +++ b/spec/formatting.md @@ -35,9 +35,9 @@ Formatting of a _message_ is defined by the following operations: > value of a given _expression_ until it is actually used by a > selection or formatting process. > However, when an _expression_ is resolved, it MUST behave as if all preceding -> _declarations_ and _selectors_ affecting _variables_ referenced by that _expression_ +> _declarations_ affecting _variables_ referenced by that _expression_ > have already been evaluated in the order in which the relevant _declarations_ -> and _selectors_ appear in the _message_. +> appear in the _message_. - **_Pattern Selection_** determines which of a message's _patterns_ is formatted. For a message with no _selectors_, this is simple as there is only one _pattern_. @@ -89,7 +89,7 @@ Implementations MAY include additional fields in their _formatting context_. ## Expression and Markup Resolution -_Expressions_ are used in _declarations_, _selectors_, and _patterns_. +_Expressions_ are used in _declarations_ and _patterns_. _Markup_ is only used in _patterns_. In a _declaration_, the resolved value of the _expression_ is bound to a _variable_, @@ -101,8 +101,6 @@ In an _input-declaration_, the _variable_ operand of the _variable-expression_ identifies not only the name of the external input value, but also the _variable_ to which the resolved value of the _variable-expression_ is bound. -In _selectors_, the resolved value of an _expression_ is used for _pattern selection_. - In a _pattern_, the resolved value of an _expression_ or _markup_ is used in its _formatting_. The form that resolved values take is implementation-dependent, @@ -452,7 +450,8 @@ according to their _key_ values and selecting the first one. > > For example, in the `pl` (Polish) locale, this _message_ cannot reach > > the `*` _variant_: > > ``` -> > .match {$num :integer} +> > .input {$num :integer} +> > .match $num > > 0 {{ }} > > one {{ }} > > few {{ }} @@ -472,13 +471,16 @@ Each _key_ corresponds to a _selector_ by its position in the _variant_. > For example, in this message: > > ``` -> .match {:one} {:two} {:three} +> .input {$one :number} +> .input {$two :number} +> .input {$three :number} +> .match $one $two $three > 1 2 3 {{ ... }} > ``` > -> The first _key_ `1` corresponds to the first _selector_ (`{:one}`), -> the second _key_ `2` to the second _selector_ (`{:two}`), -> and the third _key_ `3` to the third _selector_ (`{:three}`). +> The first _key_ `1` corresponds to the first _selector_ (`$one`), +> the second _key_ `2` to the second _selector_ (`$two`), +> and the third _key_ `3` to the third _selector_ (`$three`). To determine which _variant_ best matches a given set of inputs, each _selector_ is used in turn to order and filter the list of _variants_. @@ -491,15 +493,6 @@ Earlier _selectors_ in the _matcher_'s list of _selectors_ have a higher priorit When all of the _selectors_ have been processed, the earliest-sorted _variant_ in the remaining list of _variants_ is selected. -> [!NOTE] -> A _selector_ is not a _declaration_. -> Even when the same _function_ can be used for both formatting and selection -> of a given _operand_ -> the _annotation_ that appears in a _selector_ has no effect on subsequent -> _selectors_ nor on the formatting used in _placeholders_. -> To use the same value for selection and formatting, -> set its value with a `.input` or `.local` _declaration_. - This selection method is defined in more detail below. An implementation MAY use any pattern selection method, as long as its observable behavior matches the results of the method defined here. @@ -628,7 +621,9 @@ the variable reference `$bar` resolves to the string `'bar'`, pattern selection proceeds as follows for this message: ``` -.match {$foo :string} {$bar :string} +.input {$foo :string} +.input {$bar :string} +.match $foo $bar bar bar {{All bar}} foo foo {{All foo}} * * {{Otherwise}} @@ -659,7 +654,9 @@ Alternatively, with the same implementation and formatting context as in Example pattern selection would proceed as follows for this message: ``` -.match {$foo :string} {$bar :string} +.input {$foo :string} +.input {$bar :string} +.match $foo $bar * bar {{Any and bar}} foo * {{Foo and any}} foo bar {{Foo and bar}} @@ -708,7 +705,7 @@ the pattern selection proceeds as follows for this message: ``` .input {$count :number} -.match {$count} +.match $count one {{Category match for {$count}}} 1 {{Exact match for {$count}}} * {{Other match for {$count}}} diff --git a/spec/message.abnf b/spec/message.abnf index 3377275daa..7d8b6551a0 100644 --- a/spec/message.abnf +++ b/spec/message.abnf @@ -14,9 +14,9 @@ local-declaration = local s variable [s] "=" [s] expression quoted-pattern = "{{" pattern "}}" -matcher = match-statement 1*([s] variant) -match-statement = match 1*([s] selector) -selector = expression +matcher = match-statement s variant *([s] variant) +match-statement = match 1*(s selector) +selector = variable variant = key *(s key) [s] quoted-pattern key = literal / "*" diff --git a/spec/registry.md b/spec/registry.md index 918d7baedb..209c1b0280 100644 --- a/spec/registry.md +++ b/spec/registry.md @@ -51,9 +51,9 @@ The function `:string` has no options. #### Selection When implementing [`MatchSelectorKeys(resolvedSelector, keys)`](/spec/formatting.md#resolve-preferences) -where `resolvedSelector` is the resolved value of a _selector_ _expression_ +where `resolvedSelector` is the resolved value of a _selector_ and `keys` is a list of strings, -the `:string` selector performs as described below. +the `:string` function performs as described below. 1. Let `compare` be the string value of `resolvedSelector`. 1. Let `result` be a new empty list of strings. @@ -80,7 +80,8 @@ the `:string` selector performs as described below. > > For example: > ``` -> .match {$string :string} +> .input {$string :string} +> .match $string > | space key | {{Matches the string " space key "}} > * {{Matches the string "space key"}} > ``` @@ -195,7 +196,8 @@ but can cause problems in target locales that the original developer is not cons > For example, a naive developer might use a special message for the value `1` without > considering a locale's need for a `one` plural: > ``` -> .match {$var :number} +> .input {$var :number} +> .match $var > 1 {{You have one last chance}} > one {{You have {$var} chance remaining}} > * {{You have {$var} chances remaining}} @@ -311,7 +313,8 @@ but can cause problems in target locales that the original developer is not cons > For example, a naive developer might use a special message for the value `1` without > considering a locale's need for a `one` plural: > ``` -> .match {$var :integer} +> .input {$var :integer} +> .match $var > 1 {{You have one last chance}} > one {{You have {$var} chance remaining}} > * {{You have {$var} chances remaining}} @@ -398,7 +401,7 @@ Number selection has three modes: or to ordinal rule categories if there is no explicit match When implementing [`MatchSelectorKeys(resolvedSelector, keys)`](/spec/formatting.md#resolve-preferences) -where `resolvedSelector` is the resolved value of a _selector_ _expression_ +where `resolvedSelector` is the resolved value of a _selector_ and `keys` is a list of strings, numeric selectors perform as described below. @@ -448,7 +451,8 @@ If no rules match, return `other`. > > A message in Czech might be: > ``` -> .match {$numDays :number} +> .input {$numDays :number} +> .match $numDays > one {{{$numDays} den}} > few {{{$numDays} dny}} > many {{{$numDays} dne}} diff --git a/spec/syntax.md b/spec/syntax.md index 76fc745027..919540a976 100644 --- a/spec/syntax.md +++ b/spec/syntax.md @@ -214,12 +214,13 @@ external input value does not appear in a previous _declaration_. > [!NOTE] > These restrictions only apply to _declarations_. -> A _placeholder_ or _selector_ can apply a different annotation to a _variable_ +> A _placeholder_ can apply a different annotation to a _variable_ > than one applied to the same _variable_ named in a _declaration_. > For example, this message is _valid_: > ``` > .input {$var :number maximumFractionDigits=0} -> .match {$var :number maximumFractionDigits=2} +> .local $var2 = {$var :number maximumFractionDigits=2} +> .match $var2 > 0 {{The selector can apply a different annotation to {$var} for the purposes of selection}} > * {{A placeholder in a pattern can apply a different annotation to {$var :number maximumFractionDigits=3}}} > ``` @@ -373,22 +374,22 @@ satisfied: - The number of _keys_ on each _variant_ MUST be equal to the number of _selectors_. - At least one _variant_ MUST exist whose _keys_ are all equal to the "catch-all" key `*`. -- Each _selector_ MUST have an _annotation_, - or contain a _variable_ that directly or indirectly references a _declaration_ with an _annotation_. +- Each _selector_ MUST be a _variable_ that + directly or indirectly references a _declaration_ with an _annotation_. - Each _variant_ MUST use a list of _keys_ that is unique from that of all other _variants_ in the _message_. _Literal_ _keys_ are compared by their contents, not their syntactical appearance. ```abnf -matcher = match-statement 1*([s] variant) -match-statement = match 1*([s] selector) +matcher = match-statement s variant *([s] variant) +match-statement = match 1*(s selector) ``` > A _message_ with a _matcher_: > > ``` > .input {$count :number} -> .match {$count} +> .match $count > one {{You have {$count} notification.}} > * {{You have {$count} notifications.}} > ``` @@ -396,18 +397,18 @@ match-statement = match 1*([s] selector) > A _message_ containing a _matcher_ formatted on a single line: > > ``` -> .match {:platform} windows {{Settings}} * {{Preferences}} +> .local $os = {:platform} .match $os windows {{Settings}} * {{Preferences}} > ``` ### Selector -A **_selector_** is an _expression_ that ranks or excludes the +A **_selector_** is a _variable_ that ranks or excludes the _variants_ based on the value of the corresponding _key_ in each _variant_. The combination of _selectors_ in a _matcher_ thus determines which _pattern_ will be used during formatting. ```abnf -selector = expression +selector = variable ``` There MUST be at least one _selector_ in a _matcher_. @@ -418,7 +419,8 @@ There MAY be any number of additional _selectors_. > based on grammatical case: > > ``` -> .match {$userName :hasCase} +> .local $hasCase = {$userName :hasCase} +> .match $hasCase > vocative {{Hello, {$userName :person case=vocative}!}} > accusative {{Please welcome {$userName :person case=accusative}!}} > * {{Hello!}} @@ -429,7 +431,7 @@ There MAY be any number of additional _selectors_. > ``` > .input {$numLikes :integer} > .input {$numShares :integer} -> .match {$numLikes} {$numShares} +> .match $numLikes $numShares > 0 0 {{Your item has no likes and has not been shared.}} > 0 one {{Your item has no likes and has been shared {$numShares} time.}} > 0 * {{Your item has no likes and has been shared {$numShares} times.}} @@ -497,8 +499,7 @@ There are several types of _expression_ that can appear in a _message_. All _expressions_ share a common syntax. The types of _expression_ are: 1. The value of a _local-declaration_ -2. A _selector_ -3. A kind of _placeholder_ in a _pattern_ +2. A kind of _placeholder_ in a _pattern_ Additionally, an _input-declaration_ can contain a _variable-expression_. @@ -511,12 +512,6 @@ Additionally, an _input-declaration_ can contain a _variable-expression_. > .local $y = {|This is an expression|} > ``` > -> Selectors: -> -> ``` -> .match {$selector :functionRequired} -> ``` -> > Placeholders: > > ``` diff --git a/test/tests/data-model-errors.json b/test/tests/data-model-errors.json index 86a674c439..5620307d10 100644 --- a/test/tests/data-model-errors.json +++ b/test/tests/data-model-errors.json @@ -6,7 +6,7 @@ }, "tests": [ { - "src": ".match {$foo :x} * * {{foo}}", + "src": ".input {$foo :x} .match $foo * * {{foo}}", "expErrors": [ { "type": "variant-key-mismatch" @@ -14,7 +14,7 @@ ] }, { - "src": ".match {$foo :x} {$bar :x} * {{foo}}", + "src": ".input {$foo :x} input {$bar :x} .match $foo $bar * {{foo}}", "expErrors": [ { "type": "variant-key-mismatch" @@ -22,7 +22,7 @@ ] }, { - "src": ".match {:foo} 1 {{_}}", + "src": ".input {$foo :x} .match $foo 1 {{_}}", "expErrors": [ { "type": "missing-fallback-variant" @@ -30,7 +30,7 @@ ] }, { - "src": ".match {:foo} other {{_}}", + "src": ".input {$foo :x} .match $foo other {{_}}", "expErrors": [ { "type": "missing-fallback-variant" @@ -38,7 +38,7 @@ ] }, { - "src": ".match {:foo} {:bar} * 1 {{_}} 1 * {{_}}", + "src": ".input {$foo :x} input {$bar :x} .match $foo $bar * 1 {{_}} 1 * {{_}}", "expErrors": [ { "type": "missing-fallback-variant" @@ -46,7 +46,7 @@ ] }, { - "src": ".match {$foo} one {{one}} * {{other}}", + "src": ".input {$foo} .match $foo one {{one}} * {{other}}", "expErrors": [ { "type": "missing-selector-annotation" @@ -54,7 +54,7 @@ ] }, { - "src": ".input {$foo} .match {$foo} one {{one}} * {{other}}", + "src": ".local $foo = {$bar} .match $foo one {{one}} * {{other}}", "expErrors": [ { "type": "missing-selector-annotation" @@ -62,7 +62,7 @@ ] }, { - "src": ".local $foo = {$bar} .match {$foo} one {{one}} * {{other}}", + "src": ".input {$bar} .local $foo = {$bar} .match $foo one {{one}} * {{other}}", "expErrors": [ { "type": "missing-selector-annotation" @@ -166,7 +166,7 @@ ] }, { - "src": ".match {$var :string} * {{The first default}} * {{The second default}}", + "src": ".input {$var :string} .match $var * {{The first default}} * {{The second default}}", "expErrors": [ { "type": "duplicate-variant" @@ -174,7 +174,7 @@ ] }, { - "src": ".match {$x :string} {$y :string} * foo {{The first foo variant}} bar * {{The bar variant}} * |foo| {{The second foo variant}} * * {{The default variant}}", + "src": ".input {$x :string} .input {$y :string} .match $x $y * foo {{The first foo variant}} bar * {{The bar variant}} * |foo| {{The second foo variant}} * * {{The default variant}}", "expErrors": [ { "type": "duplicate-variant" diff --git a/test/tests/functions/integer.json b/test/tests/functions/integer.json index c8e75077a2..4ea96941e1 100644 --- a/test/tests/functions/integer.json +++ b/test/tests/functions/integer.json @@ -19,7 +19,7 @@ "exp": "hello 4" }, { - "src": ".match {$foo :integer} one {{one}} * {{other}}", + "src": ".input {$foo :integer} .match $foo 1 {{one}} * {{other}}", "params": [ { "name": "foo", diff --git a/test/tests/functions/string.json b/test/tests/functions/string.json index fab4595415..3543e7844a 100644 --- a/test/tests/functions/string.json +++ b/test/tests/functions/string.json @@ -7,7 +7,7 @@ }, "tests": [ { - "src": ".match {$foo :string} |1| {{one}} * {{other}}", + "src": ".input {$foo :string} .match $foo |1| {{one}} * {{other}}", "params": [ { "name": "foo", @@ -17,7 +17,7 @@ "exp": "one" }, { - "src": ".match {$foo :string} 1 {{one}} * {{other}}", + "src": ".input {$foo :string} .match $foo 1 {{one}} * {{other}}", "params": [ { "name": "foo", @@ -27,7 +27,7 @@ "exp": "one" }, { - "src": ".match {$foo :string} 1 {{one}} * {{other}}", + "src": ".input {$foo :string} .match $foo 1 {{one}} * {{other}}", "params": [ { "name": "foo", @@ -37,7 +37,7 @@ "exp": "other" }, { - "src": ".match {$foo :string} 1 {{one}} * {{other}}", + "src": ".input {$foo :string} .match $foo 1 {{one}} * {{other}}", "exp": "other", "expErrors": [ { diff --git a/test/tests/pattern-selection.json b/test/tests/pattern-selection.json index 314d2da78c..1ba94105b1 100644 --- a/test/tests/pattern-selection.json +++ b/test/tests/pattern-selection.json @@ -7,88 +7,58 @@ }, "tests": [ { - "src": ".match {1 :test:select} 1.0 {{1.0}} 1 {{1}} * {{other}}", + "src": ".local $x = {1 :test:select} .match $x 1.0 {{1.0}} 1 {{1}} * {{other}}", "exp": "1" }, { - "src": ".match {0 :test:select} 1.0 {{1.0}} 1 {{1}} * {{other}}", + "src": ".local $x = {0 :test:select} .match $x 1.0 {{1.0}} 1 {{1}} * {{other}}", "exp": "other" }, { - "src": ".match {$x :test:select} 1.0 {{1.0}} 1 {{1}} * {{other}}", + "src": ".input {$x :test:select} .match $x 1.0 {{1.0}} 1 {{1}} * {{other}}", "params": [{ "name": "x", "value": 1 }], "exp": "1" }, { - "src": ".match {$x :test:select} 1.0 {{1.0}} 1 {{1}} * {{other}}", + "src": ".input {$x :test:select} .match $x 1.0 {{1.0}} 1 {{1}} * {{other}}", "params": [{ "name": "x", "value": 2 }], "exp": "other" }, { - "src": ".input {$x} .match {$x :test:select} 1.0 {{1.0}} 1 {{1}} * {{other}}", + "src": ".input {$x :test:select} .local $y = {$x} .match $y 1.0 {{1.0}} 1 {{1}} * {{other}}", "params": [{ "name": "x", "value": 1 }], "exp": "1" }, { - "src": ".input {$x} .match {$x :test:select} 1.0 {{1.0}} 1 {{1}} * {{other}}", + "src": ".input {$x :test:select} .local $y = {$x} .match $y 1.0 {{1.0}} 1 {{1}} * {{other}}", "params": [{ "name": "x", "value": 2 }], "exp": "other" }, { - "src": ".input {$x :test:select} .match {$x} 1.0 {{1.0}} 1 {{1}} * {{other}}", - "params": [{ "name": "x", "value": 1 }], - "exp": "1" - }, - { - "src": ".input {$x :test:select} .match {$x} 1.0 {{1.0}} 1 {{1}} * {{other}}", - "params": [{ "name": "x", "value": 2 }], - "exp": "other" - }, - { - "src": ".input {$x :test:select} .local $y = {$x} .match {$y} 1.0 {{1.0}} 1 {{1}} * {{other}}", - "params": [{ "name": "x", "value": 1 }], - "exp": "1" - }, - { - "src": ".input {$x :test:select} .local $y = {$x} .match {$y} 1.0 {{1.0}} 1 {{1}} * {{other}}", - "params": [{ "name": "x", "value": 2 }], - "exp": "other" - }, - { - "src": ".match {1 :test:select decimalPlaces=1} 1.0 {{1.0}} 1 {{1}} * {{other}}", + "src": ".local $x = {1 :test:select decimalPlaces=1} .match $x 1.0 {{1.0}} 1 {{1}} * {{other}}", "exp": "1.0" }, { - "src": ".match {1 :test:select decimalPlaces=1} 1 {{1}} 1.0 {{1.0}} * {{other}}", + "src": ".local $x = {1 :test:select decimalPlaces=1} .match $x 1 {{1}} 1.0 {{1.0}} * {{other}}", "exp": "1.0" }, { - "src": ".match {1 :test:select decimalPlaces=9} 1.0 {{1.0}} 1 {{1}} * {{bad-option-value}}", + "src": ".local $x = {1 :test:select decimalPlaces=9} .match $x 1.0 {{1.0}} 1 {{1}} * {{bad-option-value}}", "exp": "bad-option-value", "expErrors": [{ "type": "bad-option" }, { "type": "bad-selector" }] }, { - "src": ".input {$x :test:select} .match {$x :test:select decimalPlaces=1} 1.0 {{1.0}} 1 {{1}} * {{other}}", - "params": [{ "name": "x", "value": 1 }], - "exp": "1.0" - }, - { - "src": ".input {$x :test:select decimalPlaces=1} .match {$x :test:select} 1.0 {{1.0}} 1 {{1}} * {{other}}", - "params": [{ "name": "x", "value": 1 }], - "exp": "1.0" - }, - { - "src": ".input {$x :test:select} .local $y = {$x :test:select decimalPlaces=1} .match {$y} 1.0 {{1.0}} 1 {{1}} * {{other}}", + "src": ".input {$x :test:select} .local $y = {$x :test:select decimalPlaces=1} .match $y 1.0 {{1.0}} 1 {{1}} * {{other}}", "params": [{ "name": "x", "value": 1 }], "exp": "1.0" }, { - "src": ".input {$x :test:select decimalPlaces=1} .local $y = {$x :test:select} .match {$y} 1.0 {{1.0}} 1 {{1}} * {{other}}", + "src": ".input {$x :test:select decimalPlaces=1} .local $y = {$x :test:select} .match $y 1.0 {{1.0}} 1 {{1}} * {{other}}", "params": [{ "name": "x", "value": 1 }], "exp": "1.0" }, { - "src": ".input {$x :test:select decimalPlaces=9} .match {$x :test:select decimalPlaces=1} 1.0 {{1.0}} 1 {{1}} * {{bad-option-value}}", + "src": ".input {$x :test:select decimalPlaces=9} .local $y = {$x :test:select decimalPlaces=1} .match 1.0 {{1.0}} 1 {{1}} * {{bad-option-value}}", "params": [{ "name": "x", "value": 1 }], "exp": "bad-option-value", "expErrors": [ @@ -98,21 +68,21 @@ ] }, { - "src": ".match {1 :test:select fails=select} 1.0 {{1.0}} 1 {{1}} * {{other}}", + "src": ".local $x = {1 :test:select fails=select} .match $x 1.0 {{1.0}} 1 {{1}} * {{other}}", "exp": "other", "expErrors": [{ "type": "bad-selector" }] }, { - "src": ".match {1 :test:select fails=format} 1.0 {{1.0}} 1 {{1}} * {{other}}", + "src": ".local $x = {1 :test:select fails=format} .match $x 1.0 {{1.0}} 1 {{1}} * {{other}}", "exp": "1" }, { - "src": ".match {1 :test:format} 1.0 {{1.0}} 1 {{1}} * {{other}}", + "src": ".local $x = {1 :test:format} .match $x 1.0 {{1.0}} 1 {{1}} * {{other}}", "exp": "other", "expErrors": [{ "type": "bad-selector" }] }, { - "src": ".match {$x :test:select} 1.0 {{1.0}} 1 {{1}} * {{other}}", + "src": ".input {$x :test:select} .match $x 1.0 {{1.0}} 1 {{1}} * {{other}}", "exp": "other", "expErrors": [ { "type": "unresolved-variable" }, @@ -121,28 +91,28 @@ ] }, { - "src": ".match {1 :test:select} {1 :test:select} 1 1 {{1,1}} 1 * {{1,*}} * 1 {{*,1}} * * {{*,*}}", + "src": ".local $x = {1 :test:select} .local $y = {1 :test:select} .match $x $y 1 1 {{1,1}} 1 * {{1,*}} * 1 {{*,1}} * * {{*,*}}", "exp": "1,1" }, { - "src": ".match {1 :test:select} {0 :test:select} 1 1 {{1,1}} 1 * {{1,*}} * 1 {{*,1}} * * {{*,*}}", + "src": ".local $x = {1 :test:select} .local $y = {0 :test:select} .match $x $y 1 1 {{1,1}} 1 * {{1,*}} * 1 {{*,1}} * * {{*,*}}", "exp": "1,*" }, { - "src": ".match {0 :test:select} {1 :test:select} 1 1 {{1,1}} 1 * {{1,*}} * 1 {{*,1}} * * {{*,*}}", + "src": ".local $x = {0 :test:select} .local $y = {1 :test:select} .match $x $y 1 1 {{1,1}} 1 * {{1,*}} * 1 {{*,1}} * * {{*,*}}", "exp": "*,1" }, { - "src": ".match {0 :test:select} {0 :test:select} 1 1 {{1,1}} 1 * {{1,*}} * 1 {{*,1}} * * {{*,*}}", + "src": ".local $x = {0 :test:select} .local $y = {0 :test:select} .match $x $y 1 1 {{1,1}} 1 * {{1,*}} * 1 {{*,1}} * * {{*,*}}", "exp": "*,*" }, { - "src": ".match {1 :test:select fails=select} {1 :test:select} 1 1 {{1,1}} 1 * {{1,*}} * 1 {{*,1}} * * {{*,*}}", + "src": ".local $x = {1 :test:select fails=select} .local $y = {1 :test:select} .match $x $y 1 1 {{1,1}} 1 * {{1,*}} * 1 {{*,1}} * * {{*,*}}", "exp": "*,1", "expErrors": [{ "type": "bad-selector" }] }, { - "src": ".match {1 :test:select} {1 :test:format} 1 1 {{1,1}} 1 * {{1,*}} * 1 {{*,1}} * * {{*,*}}", + "src": ".local $x = {1 :test:select} .local $y = {1 :test:format} .match $x $y 1 1 {{1,1}} 1 * {{1,*}} * 1 {{*,1}} * * {{*,*}}", "exp": "1,*", "expErrors": [{ "type": "bad-selector" }] } diff --git a/test/tests/syntax-errors.json b/test/tests/syntax-errors.json index 34d9aa4845..4b8a2bb303 100644 --- a/test/tests/syntax-errors.json +++ b/test/tests/syntax-errors.json @@ -155,26 +155,21 @@ { "src": ".local $bar = |foo| {{_}}" }, - { - "src": ".match {#foo} * {{foo}}" - }, - { - "src": ".match {} * {{foo}}" - }, - { - "src": ".match {|foo| :x} {|bar| :x} ** {{foo}}" - }, - { - "src": ".match * {{foo}}" - }, - { - "src": ".match {|x| :x} * foo" - }, - { - "src": ".match {|x| :x} * {{foo}} extra" - }, - { - "src": ".match |x| * {{foo}}" - } + { "src": ".match * {{foo}}" }, + { "src": ".match x * {{foo}}" }, + { "src": ".match |x| * {{foo}}" }, + { "src": ".match :x * {{foo}}" }, + { "src": ".match {$foo} * {{foo}}" }, + { "src": ".match {#foo} * {{foo}}" }, + { "src": ".input {$x :x} .match {$x} * {{foo}}" }, + { "src": ".input {$x :x} .match$x * {{foo}}" }, + { "src": ".input {$x :x} .match $x* {{foo}}" }, + { "src": ".input {$x :x} .match $x|x| {{foo}} * {{foo}}" }, + { "src": ".input {$x :x} .local $y = {y :y} .match $x$y * * {{foo}}" }, + { "src": ".input {$x :x} .local $y = {y :y} .match $x $y ** {{foo}}" }, + { "src": ".input {$x :x} .match $x" }, + { "src": ".input {$x :x} .match $x *" }, + { "src": ".input {$x :x} .match $x * foo" }, + { "src": ".input {$x :x} .match $x * {{foo}} extra" } ] } diff --git a/test/tests/syntax.json b/test/tests/syntax.json index 1a2d601a28..ef3643894b 100644 --- a/test/tests/syntax.json +++ b/test/tests/syntax.json @@ -171,8 +171,8 @@ "expErrors": [ { "type": "unsupported-statement" } ] }, { - "description": "message -> complex-message -> complex-body -> matcher -> match-statement variant -> match selector key quoted-pattern -> \".match\" expression literal quoted-pattern", - "src": ".match{a :f}a{{}}*{{}}", + "description": "message -> complex-message -> complex-body -> ... -> matcher -> match-statement variant -> match selector key quoted-pattern -> \".match\" variable literal quoted-pattern", + "src": ".local $a={a :f}.match $a a{{}}*{{}}", "exp": "", "expErrors": [ { "type": "unknown-function" } ] }, @@ -198,31 +198,31 @@ }, { "description": "... matcher -> match-statement [s] variant -> match 1*([s] selector) variant -> match selector selector variant -> match selector selector variant key s key quoted-pattern", - "src": ".match{a :f}{b :f}a b{{}}* *{{}}", + "src": ".local $a={a :f}.local $b={b :f}.match $a $b a b{{}}* *{{}}", "exp": "", "expErrors": [ { "type": "unknown-function" } ] }, { "description": "... matcher -> match-statement [s] variant -> match 1*([s] selector) variant -> match selector variant variant ...", - "src": ".match{a :f}a{{}}b{{}}*{{}}", + "src": ".local $a={a :f}.match $a a{{}}b{{}}*{{}}", "exp": "", "expErrors": [ { "type": "unknown-function" } ] }, { "description": "... variant -> key s quoted-pattern -> ...", - "src": ".match{a :f}a {{}}*{{}}", + "src": ".local $a={a :f}.match $a a {{}}*{{}}", "exp": "", "expErrors": [ { "type": "unknown-function" } ] }, { "description": "... variant -> key s key s quoted-pattern -> ...", - "src": ".match{a :f}{b :f}a b {{}}* *{{}}", + "src": ".local $a={a :f}.local $b={b :f}.match $a $b a b {{}}* *{{}}", "exp": "", "expErrors": [ { "type": "unknown-function" } ] }, { "description": "... key -> \"*\" ...", - "src": ".match{a :f}*{{}}", + "src": ".local $a={a :f}.match $a *{{}}", "exp": "", "expErrors": [ { "type": "unknown-function" } ] }, diff --git a/test/tests/unsupported-expressions.json b/test/tests/unsupported-expressions.json index f7d611509d..5bb5ea38df 100644 --- a/test/tests/unsupported-expressions.json +++ b/test/tests/unsupported-expressions.json @@ -21,7 +21,9 @@ { "src": "hello {|foo| *number}" }, { "src": "hello {?number}" }, { "src": "{ Date: Tue, 3 Sep 2024 20:14:25 +0300 Subject: [PATCH 2/5] Apply suggestions from code review Co-authored-by: Addison Phillips --- spec/errors.md | 2 +- spec/registry.md | 2 +- spec/syntax.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/errors.md b/spec/errors.md index 347d7b2a24..f11d00871f 100644 --- a/spec/errors.md +++ b/spec/errors.md @@ -48,7 +48,7 @@ SHOULD prioritise _Syntax Errors_ and _Data Model Errors_ over others. When an error occurs while resolving a _selector_, the _selector_ MUST NOT match any _variant_ _key_ other than the catch-all `*` -and a _Resolution Error_ or a _Message Function Error_ MUST be emitted. +and a _Bad Selector_ error MUST be emitted. ## Syntax Errors diff --git a/spec/registry.md b/spec/registry.md index 209c1b0280..8004afdfa8 100644 --- a/spec/registry.md +++ b/spec/registry.md @@ -53,7 +53,7 @@ The function `:string` has no options. When implementing [`MatchSelectorKeys(resolvedSelector, keys)`](/spec/formatting.md#resolve-preferences) where `resolvedSelector` is the resolved value of a _selector_ and `keys` is a list of strings, -the `:string` function performs as described below. +the `:string` selector function performs as described below. 1. Let `compare` be the string value of `resolvedSelector`. 1. Let `result` be a new empty list of strings. diff --git a/spec/syntax.md b/spec/syntax.md index 919540a976..cb71df3217 100644 --- a/spec/syntax.md +++ b/spec/syntax.md @@ -402,7 +402,7 @@ match-statement = match 1*(s selector) ### Selector -A **_selector_** is a _variable_ that ranks or excludes the +A **_selector_** is a _variable_ whose resolved value ranks or excludes the _variants_ based on the value of the corresponding _key_ in each _variant_. The combination of _selectors_ in a _matcher_ thus determines which _pattern_ will be used during formatting. From 8905c916b7f669b122dbbd67fe13861cf081eb28 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Tue, 3 Sep 2024 20:50:53 +0300 Subject: [PATCH 3/5] Apply suggestions from code review --- spec/errors.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/errors.md b/spec/errors.md index f11d00871f..8a823e08f8 100644 --- a/spec/errors.md +++ b/spec/errors.md @@ -46,7 +46,8 @@ or contains some error which leads to further errors, an implementation which does not emit all of the errors SHOULD prioritise _Syntax Errors_ and _Data Model Errors_ over others. -When an error occurs while resolving a _selector_, +When an error occurs while resolving a _selector_ +or calling MatchSelectorKeys with its resolved value, the _selector_ MUST NOT match any _variant_ _key_ other than the catch-all `*` and a _Bad Selector_ error MUST be emitted. From c5cda45f2431cf7fd77f25865fe21a13de348a39 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 4 Sep 2024 02:49:09 +0300 Subject: [PATCH 4/5] Add missing test changes noticed during implementation --- test/tests/data-model-errors.json | 2 +- test/tests/pattern-selection.json | 2 +- test/tests/syntax-errors.json | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/tests/data-model-errors.json b/test/tests/data-model-errors.json index 0904796002..dc79683ce0 100644 --- a/test/tests/data-model-errors.json +++ b/test/tests/data-model-errors.json @@ -182,7 +182,7 @@ ] }, { - "src": ".match {star :string} |*| {{Literal star}} * {{The default}}", + "src": ".local $star = {star :string} .match $star |*| {{Literal star}} * {{The default}}", "exp": "The default" } ] diff --git a/test/tests/pattern-selection.json b/test/tests/pattern-selection.json index 1ba94105b1..29dc146c19 100644 --- a/test/tests/pattern-selection.json +++ b/test/tests/pattern-selection.json @@ -58,7 +58,7 @@ "exp": "1.0" }, { - "src": ".input {$x :test:select decimalPlaces=9} .local $y = {$x :test:select decimalPlaces=1} .match 1.0 {{1.0}} 1 {{1}} * {{bad-option-value}}", + "src": ".input {$x :test:select decimalPlaces=9} .local $y = {$x :test:select decimalPlaces=1} .match $y 1.0 {{1.0}} 1 {{1}} * {{bad-option-value}}", "params": [{ "name": "x", "value": 1 }], "exp": "bad-option-value", "expErrors": [ diff --git a/test/tests/syntax-errors.json b/test/tests/syntax-errors.json index 49125d61a4..ca524c0dc6 100644 --- a/test/tests/syntax-errors.json +++ b/test/tests/syntax-errors.json @@ -158,6 +158,7 @@ { "src": ".local $bar = |foo| {{_}}" }, + { "src": ".match {{foo}}" }, { "src": ".match * {{foo}}" }, { "src": ".match x * {{foo}}" }, { "src": ".match |x| * {{foo}}" }, From 4dc0890040909457b1631c1d6d619f78fc36b6b7 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Tue, 10 Sep 2024 21:35:43 +0300 Subject: [PATCH 5/5] Empty commit to re-trigger CLA check