From 822bb2baedcb85dfb015f87c989bead9faae0547 Mon Sep 17 00:00:00 2001 From: maxonfjvipon Date: Fri, 26 Jun 2026 14:29:03 +0300 Subject: [PATCH 1/6] feat(#5267): parse vertical void attributes (? > name) --- eo-parser/PARSER_SPEC.md | 2 ++ .../java/org/eolang/parser/Emissions.java | 2 ++ .../src/main/java/org/eolang/parser/Eo.java | 1 + .../main/java/org/eolang/parser/Tokens.java | 5 ++++ .../main/java/org/eolang/parser/Value.java | 8 ++++++ .../test/java/org/eolang/parser/EoTest.java | 9 +++++++ .../java/org/eolang/parser/ValueTest.java | 11 +++++++- .../parser/eo-packs/parse/vertical-voids.yaml | 27 +++++++++++++++++++ 8 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 eo-parser/src/test/resources/org/eolang/parser/eo-packs/parse/vertical-voids.yaml diff --git a/eo-parser/PARSER_SPEC.md b/eo-parser/PARSER_SPEC.md index 50db26cc4b7..61d960707f1 100644 --- a/eo-parser/PARSER_SPEC.md +++ b/eo-parser/PARSER_SPEC.md @@ -168,6 +168,7 @@ The parser recognises the following lexical tokens: | `ROOT` | `Q` | | `XI` | `$` | | `TERM` | `T` — the bottom term (§9.3), similar to `⊥` in 𝜑-calculus. A self-contained single-character token; carries no arguments and no chain. | +| `VOID` | `?` — the vertical-void marker (§3.4). A `? > name` body line declares a void attribute, equivalent to listing `name` in `[…]`. | | `INT` | optional sign, then `0` or non-zero digit string. | | `FLOAT` | optional sign, digits, `.`, digits, optional exponent. | | `HEX` | `0x` followed by hex digits. | @@ -269,6 +270,7 @@ R-3.4.3. `^` (`RHO` token) is rejected as a parameter name; only `NAME` and `PHI R-3.4.4. No leading/trailing space inside `[ ]`. R-3.4.5. No double space between parameter names. R-3.4.6. The formation line may end with one of the optional name suffixes (§3.10). +R-3.4.7. A void attribute may also be declared **vertically** as a `? > name` body line of the formation (the `?` is the `VOID` token of §2.3). It is equivalent to listing `name` among the bracket parameters: it emits the same void child (§9.4), and `move-voids-up` hoists it among the head voids in source order, so `[name] > foo` with body lines `? > bar` and `? > test` is identical in XMIR to `[name bar test] > foo`. The `? > name` form requires a name suffix and is legal only as a direct child of a formation; a bare `?`, or a `?` line elsewhere, is an error. Reverse printing canonicalises every void to the bracket form, since the two are indistinguishable in XMIR. Outer kind: **`bare-formation`** (master; openness `open` for body). diff --git a/eo-parser/src/main/java/org/eolang/parser/Emissions.java b/eo-parser/src/main/java/org/eolang/parser/Emissions.java index 94e96d35b87..7574db73aac 100644 --- a/eo-parser/src/main/java/org/eolang/parser/Emissions.java +++ b/eo-parser/src/main/java/org/eolang/parser/Emissions.java @@ -140,6 +140,8 @@ static void openValue( emit.object(name, Emissions.rootBase(value.raw()), line, value.pos()); } else if (value.kind() == Value.Kind.TERM) { emit.object(name, "⊥", line, value.pos()); + } else if (value.kind() == Value.Kind.VOID) { + emit.object(name, "∅", line, value.pos()); } else if (value.kind() == Value.Kind.GROUP) { final String inner = value.raw().substring(1, value.raw().length() - 1); final int phi = Emissions.topLevelInlinePhi(inner); diff --git a/eo-parser/src/main/java/org/eolang/parser/Eo.java b/eo-parser/src/main/java/org/eolang/parser/Eo.java index 3bd4e2a1e89..53dc28074ec 100644 --- a/eo-parser/src/main/java/org/eolang/parser/Eo.java +++ b/eo-parser/src/main/java/org/eolang/parser/Eo.java @@ -367,6 +367,7 @@ private static Line classify(final Span span) { || span.head() == '(' || span.head() == 'Q' || span.head() == 'T' + || span.head() == '?' || span.head() == '@' || span.head() == '^' || span.head() == '$' diff --git a/eo-parser/src/main/java/org/eolang/parser/Tokens.java b/eo-parser/src/main/java/org/eolang/parser/Tokens.java index 3ba1e7eaed6..9155610eae8 100644 --- a/eo-parser/src/main/java/org/eolang/parser/Tokens.java +++ b/eo-parser/src/main/java/org/eolang/parser/Tokens.java @@ -128,6 +128,11 @@ Value readValue() { Value.Kind.TERM, "T", this.span.indent() + this.cursor, this.cursor + 1 ); this.cursor = this.cursor + 1; + } else if (first == '?') { + value = new Value( + Value.Kind.VOID, "?", this.span.indent() + this.cursor, this.cursor + 1 + ); + this.cursor = this.cursor + 1; } else if (first >= 'a' && first <= 'z') { value = this.readName(); } else { diff --git a/eo-parser/src/main/java/org/eolang/parser/Value.java b/eo-parser/src/main/java/org/eolang/parser/Value.java index bb594702377..4ad600a2384 100644 --- a/eo-parser/src/main/java/org/eolang/parser/Value.java +++ b/eo-parser/src/main/java/org/eolang/parser/Value.java @@ -215,6 +215,14 @@ enum Kind { */ TERM, + /** + * {@code ?} — a vertical void attribute (§3.4). Declared as a + * {@code ? > name} body line; {@link Emissions} maps it to a + * {@code @base='∅'} void child, the same element a bracket + * parameter emits. + */ + VOID, + /** * Paren group — {@code (expr)}. The {@code raw()} string holds * the bracketed text including the surrounding diff --git a/eo-parser/src/test/java/org/eolang/parser/EoTest.java b/eo-parser/src/test/java/org/eolang/parser/EoTest.java index ff74df27773..cff994b2095 100644 --- a/eo-parser/src/test/java/org/eolang/parser/EoTest.java +++ b/eo-parser/src/test/java/org/eolang/parser/EoTest.java @@ -158,6 +158,15 @@ void parsesFormationWithVoidParams() { ); } + @Test + void parsesVerticalVoidAttribute() { + MatcherAssert.assertThat( + "a `? > x` body line must emit a void param inside the formation", + EoTest.render("[] > foo", " ? > x"), + XhtmlMatchers.hasXPath("/object/o[@name='foo']/o[@name='x' and @base='∅']") + ); + } + @Test void parsesAtomDeclaration() { MatcherAssert.assertThat( diff --git a/eo-parser/src/test/java/org/eolang/parser/ValueTest.java b/eo-parser/src/test/java/org/eolang/parser/ValueTest.java index 1058ee0bc98..6d1ce6c287c 100644 --- a/eo-parser/src/test/java/org/eolang/parser/ValueTest.java +++ b/eo-parser/src/test/java/org/eolang/parser/ValueTest.java @@ -56,7 +56,7 @@ void exposesEveryKind() { MatcherAssert.assertThat( "Value.Kind must enumerate every kind the parser currently recognises", Value.Kind.values().length, - Matchers.equalTo(10) + Matchers.equalTo(11) ); } @@ -140,4 +140,13 @@ void retainsTermKind() { Matchers.equalTo(Value.Kind.TERM) ); } + + @Test + void retainsVoidKind() { + MatcherAssert.assertThat( + "VOID must be one of the recognised value kinds for the `?` vertical void", + new Value(Value.Kind.VOID, "?", 0, 1).kind(), + Matchers.equalTo(Value.Kind.VOID) + ); + } } diff --git a/eo-parser/src/test/resources/org/eolang/parser/eo-packs/parse/vertical-voids.yaml b/eo-parser/src/test/resources/org/eolang/parser/eo-packs/parse/vertical-voids.yaml new file mode 100644 index 00000000000..ff4e2671638 --- /dev/null +++ b/eo-parser/src/test/resources/org/eolang/parser/eo-packs/parse/vertical-voids.yaml @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com +# SPDX-License-Identifier: MIT +--- +# yamllint disable rule:line-length +sheets: + - /org/eolang/parser/parse/wrap-method-calls.xsl + - /org/eolang/parser/parse/vars-float-up.xsl + - /org/eolang/parser/parse/move-voids-up.xsl + - /org/eolang/parser/parse/build-fqns.xsl + - /org/eolang/parser/parse/expand-aliases.xsl + - /org/eolang/parser/parse/resolve-aliases.xsl + - /org/eolang/parser/parse/add-default-package.xsl + - /org/eolang/parser/parse/roll-bases.xsl + - /org/eolang/parser/parse/decorate.xsl + - /org/eolang/parser/parse/mandatory-as.xsl +asserts: + - /object[not(errors)] + - /object/o[@name='foo'][count(o[@base='∅'])=3] + - /object/o[@name='foo']/o[1][@name='name' and @base='∅'] + - /object/o[@name='foo']/o[2][@name='bar' and @base='∅'] + - /object/o[@name='foo']/o[3][@name='test' and @base='∅'] + - /object/o[@name='foo']/o[4][@name='φ' and @base='Φ.hello'] +input: | + [name] > foo + hello > @ + ? > bar + ? > test From 5569f942b1e1ea2a3664e3b87ff03379d640d93e Mon Sep 17 00:00:00 2001 From: maxonfjvipon Date: Fri, 26 Jun 2026 14:52:13 +0300 Subject: [PATCH 2/6] fix(#5267): make ? a dedicated void line, illegal as a value --- eo-parser/PARSER_SPEC.md | 2 +- .../java/org/eolang/parser/Emissions.java | 2 - .../src/main/java/org/eolang/parser/Eo.java | 47 +++++++------ .../main/java/org/eolang/parser/LnVoid.java | 66 +++++++++++++++++++ .../main/java/org/eolang/parser/Tokens.java | 5 -- .../main/java/org/eolang/parser/Value.java | 8 --- .../test/java/org/eolang/parser/EoTest.java | 27 ++++++++ .../java/org/eolang/parser/ValueTest.java | 11 +--- 8 files changed, 122 insertions(+), 46 deletions(-) create mode 100644 eo-parser/src/main/java/org/eolang/parser/LnVoid.java diff --git a/eo-parser/PARSER_SPEC.md b/eo-parser/PARSER_SPEC.md index 61d960707f1..c6ad75e4d5d 100644 --- a/eo-parser/PARSER_SPEC.md +++ b/eo-parser/PARSER_SPEC.md @@ -270,7 +270,7 @@ R-3.4.3. `^` (`RHO` token) is rejected as a parameter name; only `NAME` and `PHI R-3.4.4. No leading/trailing space inside `[ ]`. R-3.4.5. No double space between parameter names. R-3.4.6. The formation line may end with one of the optional name suffixes (§3.10). -R-3.4.7. A void attribute may also be declared **vertically** as a `? > name` body line of the formation (the `?` is the `VOID` token of §2.3). It is equivalent to listing `name` among the bracket parameters: it emits the same void child (§9.4), and `move-voids-up` hoists it among the head voids in source order, so `[name] > foo` with body lines `? > bar` and `? > test` is identical in XMIR to `[name bar test] > foo`. The `? > name` form requires a name suffix and is legal only as a direct child of a formation; a bare `?`, or a `?` line elsewhere, is an error. Reverse printing canonicalises every void to the bracket form, since the two are indistinguishable in XMIR. +R-3.4.7. A void attribute may also be declared **vertically** as a `? > name` body line of the formation (the `?` is the `VOID` token of §2.3). It is equivalent to listing `name` among the bracket parameters: it emits the same void child (§9.4), and `move-voids-up` hoists it among the head voids in source order, so `[name] > foo` with body lines `? > bar` and `? > test` is identical in XMIR to `[name bar test] > foo`. `? > name` is the **only** shape the `?` marker may take: the marker is not a value, so it may not appear as an argument (`foo ? bar`), a method receiver (`?.read`), or a reversed-dispatch argument (`foo. ? q`), and a bare `?` or any other trailing tokens are an error. The form requires a name suffix and is legal only as a direct child of a formation, which has no children of its own (a deeper-indent line under a void is rejected). Reverse printing canonicalises every void to the bracket form, since the two are indistinguishable in XMIR. Outer kind: **`bare-formation`** (master; openness `open` for body). diff --git a/eo-parser/src/main/java/org/eolang/parser/Emissions.java b/eo-parser/src/main/java/org/eolang/parser/Emissions.java index 7574db73aac..94e96d35b87 100644 --- a/eo-parser/src/main/java/org/eolang/parser/Emissions.java +++ b/eo-parser/src/main/java/org/eolang/parser/Emissions.java @@ -140,8 +140,6 @@ static void openValue( emit.object(name, Emissions.rootBase(value.raw()), line, value.pos()); } else if (value.kind() == Value.Kind.TERM) { emit.object(name, "⊥", line, value.pos()); - } else if (value.kind() == Value.Kind.VOID) { - emit.object(name, "∅", line, value.pos()); } else if (value.kind() == Value.Kind.GROUP) { final String inner = value.raw().substring(1, value.raw().length() - 1); final int phi = Emissions.topLevelInlinePhi(inner); diff --git a/eo-parser/src/main/java/org/eolang/parser/Eo.java b/eo-parser/src/main/java/org/eolang/parser/Eo.java index 53dc28074ec..5ec6ad3685d 100644 --- a/eo-parser/src/main/java/org/eolang/parser/Eo.java +++ b/eo-parser/src/main/java/org/eolang/parser/Eo.java @@ -339,7 +339,6 @@ private static boolean closesTextBlock(final Span span, final Globals globals) { * @return The classified line * @checkstyle NPathComplexityCheck (60 lines) */ - @SuppressWarnings("PMD.NPathComplexity") private static Line classify(final Span span) { final Line line; if (span.blank()) { @@ -352,22 +351,15 @@ private static Line classify(final Span span) { line = new LnFormation(span); } else if (span.head() == '.') { line = new LnMethod(span); + } else if (span.head() == '?') { + line = new LnVoid(span); } else if (span.head() >= 'a' && span.head() <= 'z') { - if (Eo.reversedDispatch(span)) { - line = new LnReversed(span); - } else if (Eo.onlyPhi(span)) { - line = new LnOnlyPhi(span); - } else if (Eo.compactTuple(span)) { - line = new LnCompactTuple(span); - } else { - line = new LnApplication(span); - } + line = Eo.applicative(span, Eo.reversedDispatch(span)); } else if (span.head() == '*' || span.head() == '"' || span.head() == '(' || span.head() == 'Q' || span.head() == 'T' - || span.head() == '?' || span.head() == '@' || span.head() == '^' || span.head() == '$' @@ -375,15 +367,7 @@ private static Line classify(final Span span) { || span.head() >= 'A' && span.head() <= 'F' || span.head() == '-' || Eo.signedDigit(span)) { - if (Eo.rootReversedDispatch(span)) { - line = new LnReversed(span); - } else if (Eo.onlyPhi(span)) { - line = new LnOnlyPhi(span); - } else if (Eo.compactTuple(span)) { - line = new LnCompactTuple(span); - } else { - line = new LnApplication(span); - } + line = Eo.applicative(span, Eo.rootReversedDispatch(span)); } else if (span.body().codePoints().findFirst().orElse(0) == 0x1F335) { line = (stack, globals, emit) -> { throw new ParseError( @@ -402,6 +386,29 @@ private static Line classify(final Span span) { return line; } + /** + * Classify an application-like line — the inner dispatch shared by + * the identifier-headed and root-headed line groups (§3.1): + * reversed dispatch, only-phi formation, compact tuple, or plain + * application. + * @param span The line span + * @param reversed Whether the line is a reversed dispatch + * @return The line shape + */ + private static Line applicative(final Span span, final boolean reversed) { + final Line line; + if (reversed) { + line = new LnReversed(span); + } else if (Eo.onlyPhi(span)) { + line = new LnOnlyPhi(span); + } else if (Eo.compactTuple(span)) { + line = new LnCompactTuple(span); + } else { + line = new LnApplication(span); + } + return line; + } + /** * End-of-stream checks — §8 of the spec. * diff --git a/eo-parser/src/main/java/org/eolang/parser/LnVoid.java b/eo-parser/src/main/java/org/eolang/parser/LnVoid.java new file mode 100644 index 00000000000..29cdbe27106 --- /dev/null +++ b/eo-parser/src/main/java/org/eolang/parser/LnVoid.java @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com + * SPDX-License-Identifier: MIT + */ +package org.eolang.parser; + +/** + * A vertical void-attribute line — R-3.4.7 of the spec. + * + *

Form: {@code ? > name}. The {@code ?} declares a void attribute on + * the enclosing formation, equivalent to listing {@code name} among the + * bracket parameters; it emits the same {@code } void child (§9.4), which {@code move-voids-up} hoists + * among the head voids.

+ * + *

{@code ? > name} is the only shape the {@code ?} marker may take — + * never an argument, a method receiver, or anywhere else a value is + * expected. The marker is therefore not a {@link Value} kind; + * this line is its sole producer, and a {@code ?} in any value position + * fails as "expected value".

+ * + *

Cross-line behaviour: a closed leaf + * ({@link Openness#VERTICAL_COMPLETED}), so a deeper-indent line under a + * void is rejected — a void has no children.

+ * + * @since 0.1 + */ +final class LnVoid implements Line { + + /** + * The line's source span. + */ + private final Span span; + + /** + * Ctor. + * @param source The source span + */ + LnVoid(final Span source) { + this.span = source; + } + + @Override + public void into(final Stack stack, final Globals globals, final Emit emit) { + Blanks.checkPlain(this.span, globals, emit); + final Suffix suffix = new Suffix( + this.span.body().substring(1), this.span, this.span.indent() + 1 + ); + if (suffix.form() != Suffix.Form.NAME || suffix.constant() || suffix.atom()) { + throw new ParseError( + this.span.line(), this.span.indent(), + "a void attribute must be written as `? > name`" + ); + } + Comments.attach(globals, emit, this.span, suffix.present()); + new Transition(stack, this.span).apply( + Kind.HEAD, Openness.VERTICAL_COMPLETED, suffix.present() + ); + globals.clearBlanks(); + globals.markEmitted(); + emit.object( + suffix.attribute(this.span.line(), this.span.indent()), + "∅", this.span.line(), this.span.indent() + ); + } +} diff --git a/eo-parser/src/main/java/org/eolang/parser/Tokens.java b/eo-parser/src/main/java/org/eolang/parser/Tokens.java index 9155610eae8..3ba1e7eaed6 100644 --- a/eo-parser/src/main/java/org/eolang/parser/Tokens.java +++ b/eo-parser/src/main/java/org/eolang/parser/Tokens.java @@ -128,11 +128,6 @@ Value readValue() { Value.Kind.TERM, "T", this.span.indent() + this.cursor, this.cursor + 1 ); this.cursor = this.cursor + 1; - } else if (first == '?') { - value = new Value( - Value.Kind.VOID, "?", this.span.indent() + this.cursor, this.cursor + 1 - ); - this.cursor = this.cursor + 1; } else if (first >= 'a' && first <= 'z') { value = this.readName(); } else { diff --git a/eo-parser/src/main/java/org/eolang/parser/Value.java b/eo-parser/src/main/java/org/eolang/parser/Value.java index 4ad600a2384..bb594702377 100644 --- a/eo-parser/src/main/java/org/eolang/parser/Value.java +++ b/eo-parser/src/main/java/org/eolang/parser/Value.java @@ -215,14 +215,6 @@ enum Kind { */ TERM, - /** - * {@code ?} — a vertical void attribute (§3.4). Declared as a - * {@code ? > name} body line; {@link Emissions} maps it to a - * {@code @base='∅'} void child, the same element a bracket - * parameter emits. - */ - VOID, - /** * Paren group — {@code (expr)}. The {@code raw()} string holds * the bracketed text including the surrounding diff --git a/eo-parser/src/test/java/org/eolang/parser/EoTest.java b/eo-parser/src/test/java/org/eolang/parser/EoTest.java index cff994b2095..324ea2b8391 100644 --- a/eo-parser/src/test/java/org/eolang/parser/EoTest.java +++ b/eo-parser/src/test/java/org/eolang/parser/EoTest.java @@ -167,6 +167,33 @@ void parsesVerticalVoidAttribute() { ); } + @Test + void rejectsVoidWithMethodDispatch() { + MatcherAssert.assertThat( + "a `?.method` dispatch on the void marker must be rejected", + EoTest.render("[] > foo", " ?.read > x"), + XhtmlMatchers.hasXPath("/object/errors/error") + ); + } + + @Test + void rejectsVoidAsArgument() { + MatcherAssert.assertThat( + "the void marker `?` must not be accepted as a horizontal argument", + EoTest.render("[] > foo", " bar ? baz > x"), + XhtmlMatchers.hasXPath("/object/errors/error") + ); + } + + @Test + void rejectsVoidInReversedArgument() { + MatcherAssert.assertThat( + "the void marker `?` must not be accepted as a reversed-dispatch argument", + EoTest.render("[] > foo", " bar. ? q > m"), + XhtmlMatchers.hasXPath("/object/errors/error") + ); + } + @Test void parsesAtomDeclaration() { MatcherAssert.assertThat( diff --git a/eo-parser/src/test/java/org/eolang/parser/ValueTest.java b/eo-parser/src/test/java/org/eolang/parser/ValueTest.java index 6d1ce6c287c..1058ee0bc98 100644 --- a/eo-parser/src/test/java/org/eolang/parser/ValueTest.java +++ b/eo-parser/src/test/java/org/eolang/parser/ValueTest.java @@ -56,7 +56,7 @@ void exposesEveryKind() { MatcherAssert.assertThat( "Value.Kind must enumerate every kind the parser currently recognises", Value.Kind.values().length, - Matchers.equalTo(11) + Matchers.equalTo(10) ); } @@ -140,13 +140,4 @@ void retainsTermKind() { Matchers.equalTo(Value.Kind.TERM) ); } - - @Test - void retainsVoidKind() { - MatcherAssert.assertThat( - "VOID must be one of the recognised value kinds for the `?` vertical void", - new Value(Value.Kind.VOID, "?", 0, 1).kind(), - Matchers.equalTo(Value.Kind.VOID) - ); - } } From 8896caa0ab383a6865c0b93e6d7eea529bff4a34 Mon Sep 17 00:00:00 2001 From: maxonfjvipon Date: Fri, 26 Jun 2026 15:39:07 +0300 Subject: [PATCH 3/6] =?UTF-8?q?feat(#5267):=20parse=20atom=20void=20forma-?= =?UTF-8?q?list=20/{=E2=80=A6}=20into=20raw=20@types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/org/eolang/parser/Emit.java | 11 ++ .../main/java/org/eolang/parser/LnVoid.java | 139 ++++++++++++++++-- .../test/java/org/eolang/parser/EoTest.java | 20 +++ 3 files changed, 154 insertions(+), 16 deletions(-) diff --git a/eo-parser/src/main/java/org/eolang/parser/Emit.java b/eo-parser/src/main/java/org/eolang/parser/Emit.java index 44f05d81c30..808bbe92fe9 100644 --- a/eo-parser/src/main/java/org/eolang/parser/Emit.java +++ b/eo-parser/src/main/java/org/eolang/parser/Emit.java @@ -277,6 +277,17 @@ void star() { this.append(new Directives().attr("star", "")); } + /** + * Add the {@code @types="forma …"} attribute to the most recently + * opened {@code } — the space-separated forma-list of an atom's + * vertical void error-branch (R-3.4.8). Each token is resolved like + * an {@code @atom} by later passes. + * @param formas Space-separated forma list + */ + void types(final String formas) { + this.append(new Directives().attr("types", formas)); + } + /** * Add the {@code @as=tag} attribute to the most recently opened * {@code } — inline-binding marker per §3.12 / §9.4. Numeric diff --git a/eo-parser/src/main/java/org/eolang/parser/LnVoid.java b/eo-parser/src/main/java/org/eolang/parser/LnVoid.java index 29cdbe27106..ef700823238 100644 --- a/eo-parser/src/main/java/org/eolang/parser/LnVoid.java +++ b/eo-parser/src/main/java/org/eolang/parser/LnVoid.java @@ -5,23 +5,28 @@ package org.eolang.parser; /** - * A vertical void-attribute line — R-3.4.7 of the spec. + * A vertical void-attribute line — R-3.4.7 / R-3.4.8 of the spec. * - *

Form: {@code ? > name}. The {@code ?} declares a void attribute on - * the enclosing formation, equivalent to listing {@code name} among the - * bracket parameters; it emits the same {@code Form: {@code ? > name} with an optional atom-only forma-list tail + * {@code /{forma …}}. The {@code ?} declares a void attribute + * on the enclosing formation, equivalent to listing {@code name} among + * the bracket parameters; it emits the same {@code } void child (§9.4), which {@code move-voids-up} hoists * among the head voids.

* - *

{@code ? > name} is the only shape the {@code ?} marker may take — - * never an argument, a method receiver, or anywhere else a value is - * expected. The marker is therefore not a {@link Value} kind; - * this line is its sole producer, and a {@code ?} in any value position - * fails as "expected value".

+ *

In an atom (a formation whose head carries {@code /sig}) the void + * may carry the forma-list tail — the formas the atom supplies as the + * arguments of that error branch. They emit as a {@code @types} + * attribute (R-3.4.8) whose space-separated tokens are resolved like an + * {@code @atom} by later passes. The forma-list is rejected outside an + * atom.

* - *

Cross-line behaviour: a closed leaf - * ({@link Openness#VERTICAL_COMPLETED}), so a deeper-indent line under a - * void is rejected — a void has no children.

+ *

{@code ? > name} (with the optional tail) is the only shape the + * {@code ?} marker may take — never an argument, a method receiver, or + * anywhere else a value is expected. The marker is therefore + * not a {@link Value} kind; this line is its sole producer. + * Cross-line behaviour: a closed leaf + * ({@link Openness#VERTICAL_COMPLETED}), so a void has no children.

* * @since 0.1 */ @@ -43,9 +48,18 @@ final class LnVoid implements Line { @Override public void into(final Stack stack, final Globals globals, final Emit emit) { Blanks.checkPlain(this.span, globals, emit); - final Suffix suffix = new Suffix( - this.span.body().substring(1), this.span, this.span.indent() + 1 - ); + final String tail = this.span.body().substring(1); + final int brace = tail.indexOf("/{"); + final String formas; + final String head; + if (brace < 0) { + formas = ""; + head = tail; + } else { + formas = LnVoid.formas(tail, brace, this.span); + head = tail.substring(0, brace); + } + final Suffix suffix = new Suffix(head, this.span, this.span.indent() + 1); if (suffix.form() != Suffix.Form.NAME || suffix.constant() || suffix.atom()) { throw new ParseError( this.span.line(), this.span.indent(), @@ -53,14 +67,107 @@ public void into(final Stack stack, final Globals globals, final Emit emit) { ); } Comments.attach(globals, emit, this.span, suffix.present()); - new Transition(stack, this.span).apply( + final Level level = new Transition(stack, this.span).apply( Kind.HEAD, Openness.VERTICAL_COMPLETED, suffix.present() ); + if (!formas.isEmpty() && !level.patom()) { + throw new ParseError( + this.span.line(), this.span.indent(), + "a `/{…}` forma-list is allowed only inside an atom" + ); + } globals.clearBlanks(); globals.markEmitted(); emit.object( suffix.attribute(this.span.line(), this.span.indent()), "∅", this.span.line(), this.span.indent() ); + if (!formas.isEmpty()) { + emit.types(formas); + } + } + + /** + * Extract and validate the forma-list tail, returning + * the space-separated formas with each leading {@code Q.} promoted + * to {@code Φ.} (R-9.3), exactly as an {@code /sig} signature is + * promoted. + * @param tail The line body after the {@code ?} + * @param brace Index of the forma-list marker in {@code tail} + * @param span The source span (for errors) + * @return Space-separated promoted formas + */ + private static String formas(final String tail, final int brace, final Span span) { + final int close = tail.indexOf('}', brace + 2); + if (close < 0) { + throw new ParseError( + span.line(), span.indent(), + "a `/{…}` forma-list must end with `}`" + ); + } + int after = close + 1; + while (after < tail.length() && tail.charAt(after) == ' ') { + after = after + 1; + } + if (after < tail.length()) { + throw new ParseError( + span.line(), span.indent(), + "trailing garbage after a `/{…}` forma-list" + ); + } + return LnVoid.promoted(tail.substring(brace + 2, close), span); + } + + /** + * Split the forma-list body on single spaces, promoting each + * {@code Q.}-rooted forma to {@code Φ.} and rejecting empty or + * double-spaced entries. + * @param inside The text inside the forma-list braces + * @param span The source span (for errors) + * @return Space-separated promoted formas + */ + private static String promoted(final String inside, final Span span) { + if (inside.isEmpty()) { + throw new ParseError( + span.line(), span.indent(), + "a `/{…}` forma-list must name at least one forma" + ); + } + final StringBuilder out = new StringBuilder(inside.length()); + int idx = 0; + while (idx < inside.length()) { + int end = idx; + while (end < inside.length() && inside.charAt(end) != ' ') { + end = end + 1; + } + if (end == idx) { + throw new ParseError( + span.line(), span.indent(), + "forma names in a `/{…}` list must be separated by exactly one space" + ); + } + if (out.length() > 0) { + out.append(' '); + } + out.append(LnVoid.root(inside.substring(idx, end))); + idx = end + 1; + } + return out.toString(); + } + + /** + * Promote a leading {@code Q.} to {@code Φ.} per R-9.3; other formas + * pass through unchanged. + * @param forma Raw forma name + * @return Promoted forma + */ + private static String root(final String forma) { + final String promoted; + if (forma.startsWith("Q.")) { + promoted = "Φ".concat(forma.substring(1)); + } else { + promoted = forma; + } + return promoted; } } diff --git a/eo-parser/src/test/java/org/eolang/parser/EoTest.java b/eo-parser/src/test/java/org/eolang/parser/EoTest.java index 324ea2b8391..d000b542678 100644 --- a/eo-parser/src/test/java/org/eolang/parser/EoTest.java +++ b/eo-parser/src/test/java/org/eolang/parser/EoTest.java @@ -194,6 +194,26 @@ void rejectsVoidInReversedArgument() { ); } + @Test + void parsesAtomVoidWithFormaList() { + MatcherAssert.assertThat( + "a void in an atom must carry its `/{…}` forma-list as a raw @types attribute", + EoTest.render("[] > fopen /file", " ? > not-found /{string io.file-error}"), + XhtmlMatchers.hasXPath( + "/object/o[@name='fopen']/o[@name='not-found' and @base='∅' and @types='string io.file-error']" + ) + ); + } + + @Test + void rejectsFormaListOutsideAtom() { + MatcherAssert.assertThat( + "a `/{…}` forma-list on a void outside an atom must be rejected", + EoTest.render("[] > foo", " ? > x /{string}"), + XhtmlMatchers.hasXPath("/object/errors/error") + ); + } + @Test void parsesAtomDeclaration() { MatcherAssert.assertThat( From db2fe2be5467e11c6a5917c02c3487b404f15b7b Mon Sep 17 00:00:00 2001 From: maxonfjvipon Date: Fri, 26 Jun 2026 15:58:13 +0300 Subject: [PATCH 4/6] feat(#5267): resolve @types formas per-token like @atom --- eo-parser/PARSER_SPEC.md | 1 + .../parser/parse/add-default-package.xsl | 5 ++++ .../eolang/parser/parse/resolve-aliases.xsl | 5 ++++ .../eo-packs/parse/atom-void-formas.yaml | 25 +++++++++++++++++++ 4 files changed, 36 insertions(+) create mode 100644 eo-parser/src/test/resources/org/eolang/parser/eo-packs/parse/atom-void-formas.yaml diff --git a/eo-parser/PARSER_SPEC.md b/eo-parser/PARSER_SPEC.md index c6ad75e4d5d..8f6ba2615dd 100644 --- a/eo-parser/PARSER_SPEC.md +++ b/eo-parser/PARSER_SPEC.md @@ -271,6 +271,7 @@ R-3.4.4. No leading/trailing space inside `[ ]`. R-3.4.5. No double space between parameter names. R-3.4.6. The formation line may end with one of the optional name suffixes (§3.10). R-3.4.7. A void attribute may also be declared **vertically** as a `? > name` body line of the formation (the `?` is the `VOID` token of §2.3). It is equivalent to listing `name` among the bracket parameters: it emits the same void child (§9.4), and `move-voids-up` hoists it among the head voids in source order, so `[name] > foo` with body lines `? > bar` and `? > test` is identical in XMIR to `[name bar test] > foo`. `? > name` is the **only** shape the `?` marker may take: the marker is not a value, so it may not appear as an argument (`foo ? bar`), a method receiver (`?.read`), or a reversed-dispatch argument (`foo. ? q`), and a bare `?` or any other trailing tokens are an error. The form requires a name suffix and is legal only as a direct child of a formation, which has no children of its own (a deeper-indent line under a void is rejected). Reverse printing canonicalises every void to the bracket form, since the two are indistinguishable in XMIR. +R-3.4.8. In an **atom** (a formation whose head carries `/sig`), a vertical void may carry a brace-delimited forma-list — `? > name /{forma (SPACE forma)*}` — declaring the formas the atom supplies as the arguments of that error branch. Each `forma` is a dotted name (`NAME ('.' NAME)*`), optionally rooted at `Q`. The list emits as a `@types` attribute on the void's ``, whose space-separated tokens are resolved exactly like an `@atom`: a leading `Q.` is promoted to `Φ.` at parse (R-9.3), then each token is alias-expanded and, when dotless and non-special, prefixed with `Φ.`. Example: `? > not-found /{string io.file-error}` emits ``. A `/{…}` forma-list on a void outside an atom is an error. Outer kind: **`bare-formation`** (master; openness `open` for body). diff --git a/eo-parser/src/main/resources/org/eolang/parser/parse/add-default-package.xsl b/eo-parser/src/main/resources/org/eolang/parser/parse/add-default-package.xsl index 62aff7e395a..43e6bfade7f 100644 --- a/eo-parser/src/main/resources/org/eolang/parser/parse/add-default-package.xsl +++ b/eo-parser/src/main/resources/org/eolang/parser/parse/add-default-package.xsl @@ -30,6 +30,11 @@ + + + + + diff --git a/eo-parser/src/main/resources/org/eolang/parser/parse/resolve-aliases.xsl b/eo-parser/src/main/resources/org/eolang/parser/parse/resolve-aliases.xsl index 32d309007af..1ef79d14740 100644 --- a/eo-parser/src/main/resources/org/eolang/parser/parse/resolve-aliases.xsl +++ b/eo-parser/src/main/resources/org/eolang/parser/parse/resolve-aliases.xsl @@ -44,6 +44,11 @@ + + + + + diff --git a/eo-parser/src/test/resources/org/eolang/parser/eo-packs/parse/atom-void-formas.yaml b/eo-parser/src/test/resources/org/eolang/parser/eo-packs/parse/atom-void-formas.yaml new file mode 100644 index 00000000000..56ec0fa8f6a --- /dev/null +++ b/eo-parser/src/test/resources/org/eolang/parser/eo-packs/parse/atom-void-formas.yaml @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com +# SPDX-License-Identifier: MIT +--- +# yamllint disable rule:line-length +sheets: + - /org/eolang/parser/parse/wrap-method-calls.xsl + - /org/eolang/parser/parse/vars-float-up.xsl + - /org/eolang/parser/parse/move-voids-up.xsl + - /org/eolang/parser/parse/build-fqns.xsl + - /org/eolang/parser/parse/expand-aliases.xsl + - /org/eolang/parser/parse/resolve-aliases.xsl + - /org/eolang/parser/parse/add-default-package.xsl + - /org/eolang/parser/parse/roll-bases.xsl + - /org/eolang/parser/parse/decorate.xsl + - /org/eolang/parser/parse/mandatory-as.xsl +asserts: + - /object[not(errors)] + - /object/o[@name='fopen']/o[@name='not-found' and @base='∅' and @types='Φ.string io.file-error'] + - /object/o[@name='fopen']/o[@name='cant-open' and @base='∅' and @types='Φ.a.b.c'] +input: | + +alias foo a.b.c + + [] > fopen /file + ? > not-found /{string io.file-error} + ? > cant-open /{foo} From db5f0ca8a2a1e297d7bf23a0a32e4c84f2332f8d Mon Sep 17 00:00:00 2001 From: maxonfjvipon Date: Fri, 26 Jun 2026 17:14:57 +0300 Subject: [PATCH 5/6] feat(#5267): require vertical voids to precede all other attributes --- eo-parser/PARSER_SPEC.md | 7 +++++ .../src/main/java/org/eolang/parser/Kind.java | 8 +++++- .../main/java/org/eolang/parser/Level.java | 28 +++++++++++++++++++ .../main/java/org/eolang/parser/LnVoid.java | 2 +- .../main/java/org/eolang/parser/Stack.java | 3 +- .../test/java/org/eolang/parser/EoTest.java | 11 ++++++++ .../parse/atom-void-single-alias.yaml | 23 +++++++++++++++ .../parser/eo-packs/parse/vertical-voids.yaml | 2 +- .../parser/eo-typos/void/as-argument.yaml | 10 +++++++ .../eo-typos/void/as-reversed-argument.yaml | 10 +++++++ .../parser/eo-typos/void/below-regular.yaml | 13 +++++++++ .../void/forma-list-outside-atom.yaml | 10 +++++++ .../parser/eo-typos/void/method-dispatch.yaml | 10 +++++++ 13 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 eo-parser/src/test/resources/org/eolang/parser/eo-packs/parse/atom-void-single-alias.yaml create mode 100644 eo-parser/src/test/resources/org/eolang/parser/eo-typos/void/as-argument.yaml create mode 100644 eo-parser/src/test/resources/org/eolang/parser/eo-typos/void/as-reversed-argument.yaml create mode 100644 eo-parser/src/test/resources/org/eolang/parser/eo-typos/void/below-regular.yaml create mode 100644 eo-parser/src/test/resources/org/eolang/parser/eo-typos/void/forma-list-outside-atom.yaml create mode 100644 eo-parser/src/test/resources/org/eolang/parser/eo-typos/void/method-dispatch.yaml diff --git a/eo-parser/PARSER_SPEC.md b/eo-parser/PARSER_SPEC.md index 8f6ba2615dd..8c7ac74cc9d 100644 --- a/eo-parser/PARSER_SPEC.md +++ b/eo-parser/PARSER_SPEC.md @@ -272,6 +272,13 @@ R-3.4.5. No double space between parameter names. R-3.4.6. The formation line may end with one of the optional name suffixes (§3.10). R-3.4.7. A void attribute may also be declared **vertically** as a `? > name` body line of the formation (the `?` is the `VOID` token of §2.3). It is equivalent to listing `name` among the bracket parameters: it emits the same void child (§9.4), and `move-voids-up` hoists it among the head voids in source order, so `[name] > foo` with body lines `? > bar` and `? > test` is identical in XMIR to `[name bar test] > foo`. `? > name` is the **only** shape the `?` marker may take: the marker is not a value, so it may not appear as an argument (`foo ? bar`), a method receiver (`?.read`), or a reversed-dispatch argument (`foo. ? q`), and a bare `?` or any other trailing tokens are an error. The form requires a name suffix and is legal only as a direct child of a formation, which has no children of its own (a deeper-indent line under a void is rejected). Reverse printing canonicalises every void to the bracket form, since the two are indistinguishable in XMIR. R-3.4.8. In an **atom** (a formation whose head carries `/sig`), a vertical void may carry a brace-delimited forma-list — `? > name /{forma (SPACE forma)*}` — declaring the formas the atom supplies as the arguments of that error branch. Each `forma` is a dotted name (`NAME ('.' NAME)*`), optionally rooted at `Q`. The list emits as a `@types` attribute on the void's ``, whose space-separated tokens are resolved exactly like an `@atom`: a leading `Q.` is promoted to `Φ.` at parse (R-9.3), then each token is alias-expanded and, when dotless and non-special, prefixed with `Φ.`. Example: `? > not-found /{string io.file-error}` emits ``. A `/{…}` forma-list on a void outside an atom is an error. +R-3.4.9. Vertical voids must stay **on top**: every `? > name` line must precede all non-void attributes of the formation body. A void that follows a non-void child — or sits between non-void children — is an error (`a void attribute must be declared above all other attributes`). So +``` +[] > foo + 6 > six + ? > x +``` +is rejected, whereas `? > x` above `6 > six` is accepted. (Bracket-head voids are always above the body, so the rule constrains only the relative order of body lines.) Outer kind: **`bare-formation`** (master; openness `open` for body). diff --git a/eo-parser/src/main/java/org/eolang/parser/Kind.java b/eo-parser/src/main/java/org/eolang/parser/Kind.java index be48a54ec62..f9711f72949 100644 --- a/eo-parser/src/main/java/org/eolang/parser/Kind.java +++ b/eo-parser/src/main/java/org/eolang/parser/Kind.java @@ -98,7 +98,13 @@ enum Kind { /** * Triple-quoted {@code """…"""} text block. */ - TEXT_BLOCK; + TEXT_BLOCK, + + /** + * Vertical void attribute {@code ? > name} (R-3.4.7). A closed leaf + * that must precede every non-void child of its formation. + */ + VOID; /** * Whether this kind is in the horizontally-completed set. diff --git a/eo-parser/src/main/java/org/eolang/parser/Level.java b/eo-parser/src/main/java/org/eolang/parser/Level.java index 2a3c6b23300..3b2a36b5f31 100644 --- a/eo-parser/src/main/java/org/eolang/parser/Level.java +++ b/eo-parser/src/main/java/org/eolang/parser/Level.java @@ -78,6 +78,13 @@ final class Level { */ private boolean taken; + /** + * True once a non-void child has been added under this entry — + * after which a {@link Kind#VOID} child is rejected (R-3.4.7, + * voids must precede every other attribute). + */ + private boolean plain; + /** * For {@link Kind#COMPACT_TUPLE}: the {@code N} count from {@code *N}. */ @@ -280,6 +287,27 @@ void consumeReceiver() { this.taken = true; } + /** + * Observe a child of the given {@code kind} being added under this + * entry, enforcing the void-ordering rule (R-3.4.7): a + * {@link Kind#VOID} child is rejected once a non-void child has + * appeared, and every non-void child records that fact. + * @param shape Kind of the child being added + * @param line Source line of the child (for the error) + * @param column Source indent of the child (for the error) + */ + void observeVoid(final Kind shape, final int line, final int column) { + if (shape == Kind.VOID && this.plain) { + throw new ParseError( + line, column, + "a void attribute must be declared above all other attributes" + ); + } + if (shape != Kind.VOID) { + this.plain = true; + } + } + /** * Set the compact-tuple {@code N} count. * @param value N diff --git a/eo-parser/src/main/java/org/eolang/parser/LnVoid.java b/eo-parser/src/main/java/org/eolang/parser/LnVoid.java index ef700823238..a27d55021da 100644 --- a/eo-parser/src/main/java/org/eolang/parser/LnVoid.java +++ b/eo-parser/src/main/java/org/eolang/parser/LnVoid.java @@ -68,7 +68,7 @@ public void into(final Stack stack, final Globals globals, final Emit emit) { } Comments.attach(globals, emit, this.span, suffix.present()); final Level level = new Transition(stack, this.span).apply( - Kind.HEAD, Openness.VERTICAL_COMPLETED, suffix.present() + Kind.VOID, Openness.VERTICAL_COMPLETED, suffix.present() ); if (!formas.isEmpty() && !level.patom()) { throw new ParseError( diff --git a/eo-parser/src/main/java/org/eolang/parser/Stack.java b/eo-parser/src/main/java/org/eolang/parser/Stack.java index 22c159ae921..8bd4152778b 100644 --- a/eo-parser/src/main/java/org/eolang/parser/Stack.java +++ b/eo-parser/src/main/java/org/eolang/parser/Stack.java @@ -26,7 +26,6 @@ * * @since 0.1 */ -@SuppressWarnings("PMD.UnnecessaryLocalRule") final class Stack { /** @@ -189,6 +188,7 @@ Level push( } parent = under.kind(); patom = under.atom(); + under.observeVoid(kind, line, indent); } if (!this.levels.isEmpty()) { this.opener.beforeChild(this.top()); @@ -251,6 +251,7 @@ Level replace(final int line, final Kind kind, final Openness openness) { final Level under = this.top(); parent = under.kind(); patom = under.atom(); + under.observeVoid(kind, line, indent); this.opener.beforeChild(under); } final Level fresh = new Level(indent, line, kind, openness, parent, patom); diff --git a/eo-parser/src/test/java/org/eolang/parser/EoTest.java b/eo-parser/src/test/java/org/eolang/parser/EoTest.java index d000b542678..5695f450c37 100644 --- a/eo-parser/src/test/java/org/eolang/parser/EoTest.java +++ b/eo-parser/src/test/java/org/eolang/parser/EoTest.java @@ -214,6 +214,17 @@ void rejectsFormaListOutsideAtom() { ); } + @Test + void rejectsVoidBelowRegularAttribute() { + MatcherAssert.assertThat( + "a void declared after a regular attribute must be rejected", + EoTest.render("[] > foo", " 6 > six", " ? > x", " 5 > five", " ? > y"), + XhtmlMatchers.hasXPath( + "/object/errors/error[contains(text(),'void attribute must be declared above')]" + ) + ); + } + @Test void parsesAtomDeclaration() { MatcherAssert.assertThat( diff --git a/eo-parser/src/test/resources/org/eolang/parser/eo-packs/parse/atom-void-single-alias.yaml b/eo-parser/src/test/resources/org/eolang/parser/eo-packs/parse/atom-void-single-alias.yaml new file mode 100644 index 00000000000..c1bcb34f464 --- /dev/null +++ b/eo-parser/src/test/resources/org/eolang/parser/eo-packs/parse/atom-void-single-alias.yaml @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com +# SPDX-License-Identifier: MIT +--- +# yamllint disable rule:line-length +sheets: + - /org/eolang/parser/parse/wrap-method-calls.xsl + - /org/eolang/parser/parse/vars-float-up.xsl + - /org/eolang/parser/parse/move-voids-up.xsl + - /org/eolang/parser/parse/build-fqns.xsl + - /org/eolang/parser/parse/expand-aliases.xsl + - /org/eolang/parser/parse/resolve-aliases.xsl + - /org/eolang/parser/parse/add-default-package.xsl + - /org/eolang/parser/parse/roll-bases.xsl + - /org/eolang/parser/parse/decorate.xsl + - /org/eolang/parser/parse/mandatory-as.xsl +asserts: + - /object[not(errors)] + - /object/o[@name='fopen']/o[@name='e' and @base='∅' and @types='Φ.foo'] +input: | + +alias foo + + [] > fopen /file + ? > e /{foo} diff --git a/eo-parser/src/test/resources/org/eolang/parser/eo-packs/parse/vertical-voids.yaml b/eo-parser/src/test/resources/org/eolang/parser/eo-packs/parse/vertical-voids.yaml index ff4e2671638..32e8deee229 100644 --- a/eo-parser/src/test/resources/org/eolang/parser/eo-packs/parse/vertical-voids.yaml +++ b/eo-parser/src/test/resources/org/eolang/parser/eo-packs/parse/vertical-voids.yaml @@ -22,6 +22,6 @@ asserts: - /object/o[@name='foo']/o[4][@name='φ' and @base='Φ.hello'] input: | [name] > foo - hello > @ ? > bar ? > test + hello > @ diff --git a/eo-parser/src/test/resources/org/eolang/parser/eo-typos/void/as-argument.yaml b/eo-parser/src/test/resources/org/eolang/parser/eo-typos/void/as-argument.yaml new file mode 100644 index 00000000000..f182c7704a7 --- /dev/null +++ b/eo-parser/src/test/resources/org/eolang/parser/eo-typos/void/as-argument.yaml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com +# SPDX-License-Identifier: MIT +--- +# yamllint disable rule:line-length +line: 3 +message: "expected value" +input: | + # No comments. + [] > foo + bar ? baz > x diff --git a/eo-parser/src/test/resources/org/eolang/parser/eo-typos/void/as-reversed-argument.yaml b/eo-parser/src/test/resources/org/eolang/parser/eo-typos/void/as-reversed-argument.yaml new file mode 100644 index 00000000000..e6083062cc0 --- /dev/null +++ b/eo-parser/src/test/resources/org/eolang/parser/eo-typos/void/as-reversed-argument.yaml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com +# SPDX-License-Identifier: MIT +--- +# yamllint disable rule:line-length +line: 3 +message: "expected value" +input: | + # No comments. + [] > foo + bar. ? q > m diff --git a/eo-parser/src/test/resources/org/eolang/parser/eo-typos/void/below-regular.yaml b/eo-parser/src/test/resources/org/eolang/parser/eo-typos/void/below-regular.yaml new file mode 100644 index 00000000000..4cd464afb75 --- /dev/null +++ b/eo-parser/src/test/resources/org/eolang/parser/eo-typos/void/below-regular.yaml @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com +# SPDX-License-Identifier: MIT +--- +# yamllint disable rule:line-length +line: 4 +message: "void attribute must be declared above" +input: | + # No comments. + [] > foo + 6 > six + ? > x + 5 > five + ? > y diff --git a/eo-parser/src/test/resources/org/eolang/parser/eo-typos/void/forma-list-outside-atom.yaml b/eo-parser/src/test/resources/org/eolang/parser/eo-typos/void/forma-list-outside-atom.yaml new file mode 100644 index 00000000000..915a9c6c966 --- /dev/null +++ b/eo-parser/src/test/resources/org/eolang/parser/eo-typos/void/forma-list-outside-atom.yaml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com +# SPDX-License-Identifier: MIT +--- +# yamllint disable rule:line-length +line: 3 +message: "allowed only inside an atom" +input: | + # No comments. + [] > foo + ? > x /{string} diff --git a/eo-parser/src/test/resources/org/eolang/parser/eo-typos/void/method-dispatch.yaml b/eo-parser/src/test/resources/org/eolang/parser/eo-typos/void/method-dispatch.yaml new file mode 100644 index 00000000000..206679df193 --- /dev/null +++ b/eo-parser/src/test/resources/org/eolang/parser/eo-typos/void/method-dispatch.yaml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com +# SPDX-License-Identifier: MIT +--- +# yamllint disable rule:line-length +line: 3 +message: "a void attribute must be written as" +input: | + # No comments. + [] > foo + ?.read > x From bb6890164775467cca3df3135c26d373c4758122 Mon Sep 17 00:00:00 2001 From: maxonfjvipon Date: Fri, 26 Jun 2026 17:28:28 +0300 Subject: [PATCH 6/6] style(#5267): surround R-3.4.9 fenced block with blank lines (markdownlint MD031) --- eo-parser/PARSER_SPEC.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/eo-parser/PARSER_SPEC.md b/eo-parser/PARSER_SPEC.md index 8c7ac74cc9d..b87d08b4159 100644 --- a/eo-parser/PARSER_SPEC.md +++ b/eo-parser/PARSER_SPEC.md @@ -272,12 +272,14 @@ R-3.4.5. No double space between parameter names. R-3.4.6. The formation line may end with one of the optional name suffixes (§3.10). R-3.4.7. A void attribute may also be declared **vertically** as a `? > name` body line of the formation (the `?` is the `VOID` token of §2.3). It is equivalent to listing `name` among the bracket parameters: it emits the same void child (§9.4), and `move-voids-up` hoists it among the head voids in source order, so `[name] > foo` with body lines `? > bar` and `? > test` is identical in XMIR to `[name bar test] > foo`. `? > name` is the **only** shape the `?` marker may take: the marker is not a value, so it may not appear as an argument (`foo ? bar`), a method receiver (`?.read`), or a reversed-dispatch argument (`foo. ? q`), and a bare `?` or any other trailing tokens are an error. The form requires a name suffix and is legal only as a direct child of a formation, which has no children of its own (a deeper-indent line under a void is rejected). Reverse printing canonicalises every void to the bracket form, since the two are indistinguishable in XMIR. R-3.4.8. In an **atom** (a formation whose head carries `/sig`), a vertical void may carry a brace-delimited forma-list — `? > name /{forma (SPACE forma)*}` — declaring the formas the atom supplies as the arguments of that error branch. Each `forma` is a dotted name (`NAME ('.' NAME)*`), optionally rooted at `Q`. The list emits as a `@types` attribute on the void's ``, whose space-separated tokens are resolved exactly like an `@atom`: a leading `Q.` is promoted to `Φ.` at parse (R-9.3), then each token is alias-expanded and, when dotless and non-special, prefixed with `Φ.`. Example: `? > not-found /{string io.file-error}` emits ``. A `/{…}` forma-list on a void outside an atom is an error. -R-3.4.9. Vertical voids must stay **on top**: every `? > name` line must precede all non-void attributes of the formation body. A void that follows a non-void child — or sits between non-void children — is an error (`a void attribute must be declared above all other attributes`). So +R-3.4.9. Vertical voids must stay **on top**: every `? > name` line must precede all non-void attributes of the formation body. A void that follows a non-void child — or sits between non-void children — is an error (`a void attribute must be declared above all other attributes`). For example, + ``` [] > foo 6 > six ? > x ``` + is rejected, whereas `? > x` above `6 > six` is accepted. (Bracket-head voids are always above the body, so the rule constrains only the relative order of body lines.) Outer kind: **`bare-formation`** (master; openness `open` for body).