Skip to content

Commit db5f0ca

Browse files
committed
feat(#5267): require vertical voids to precede all other attributes
1 parent db2fe2b commit db5f0ca

13 files changed

Lines changed: 133 additions & 4 deletions

File tree

eo-parser/PARSER_SPEC.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,13 @@ R-3.4.5. No double space between parameter names.
272272
R-3.4.6. The formation line may end with one of the optional name suffixes (§3.10).
273273
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.
274274
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 `<o>`, 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 `<o name='not-found' base='∅' types='Φ.string io.file-error'/>`. A `/{…}` forma-list on a void outside an atom is an error.
275+
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
276+
```
277+
[] > foo
278+
6 > six
279+
? > x
280+
```
281+
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.)
275282

276283
Outer kind: **`bare-formation`** (master; openness `open` for body).
277284

eo-parser/src/main/java/org/eolang/parser/Kind.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,13 @@ enum Kind {
9898
/**
9999
* Triple-quoted {@code """…"""} text block.
100100
*/
101-
TEXT_BLOCK;
101+
TEXT_BLOCK,
102+
103+
/**
104+
* Vertical void attribute {@code ? > name} (R-3.4.7). A closed leaf
105+
* that must precede every non-void child of its formation.
106+
*/
107+
VOID;
102108

103109
/**
104110
* Whether this kind is in the horizontally-completed set.

eo-parser/src/main/java/org/eolang/parser/Level.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ final class Level {
7878
*/
7979
private boolean taken;
8080

81+
/**
82+
* True once a non-void child has been added under this entry —
83+
* after which a {@link Kind#VOID} child is rejected (R-3.4.7,
84+
* voids must precede every other attribute).
85+
*/
86+
private boolean plain;
87+
8188
/**
8289
* For {@link Kind#COMPACT_TUPLE}: the {@code N} count from {@code *N}.
8390
*/
@@ -280,6 +287,27 @@ void consumeReceiver() {
280287
this.taken = true;
281288
}
282289

290+
/**
291+
* Observe a child of the given {@code kind} being added under this
292+
* entry, enforcing the void-ordering rule (R-3.4.7): a
293+
* {@link Kind#VOID} child is rejected once a non-void child has
294+
* appeared, and every non-void child records that fact.
295+
* @param shape Kind of the child being added
296+
* @param line Source line of the child (for the error)
297+
* @param column Source indent of the child (for the error)
298+
*/
299+
void observeVoid(final Kind shape, final int line, final int column) {
300+
if (shape == Kind.VOID && this.plain) {
301+
throw new ParseError(
302+
line, column,
303+
"a void attribute must be declared above all other attributes"
304+
);
305+
}
306+
if (shape != Kind.VOID) {
307+
this.plain = true;
308+
}
309+
}
310+
283311
/**
284312
* Set the compact-tuple {@code N} count.
285313
* @param value N

eo-parser/src/main/java/org/eolang/parser/LnVoid.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public void into(final Stack stack, final Globals globals, final Emit emit) {
6868
}
6969
Comments.attach(globals, emit, this.span, suffix.present());
7070
final Level level = new Transition(stack, this.span).apply(
71-
Kind.HEAD, Openness.VERTICAL_COMPLETED, suffix.present()
71+
Kind.VOID, Openness.VERTICAL_COMPLETED, suffix.present()
7272
);
7373
if (!formas.isEmpty() && !level.patom()) {
7474
throw new ParseError(

eo-parser/src/main/java/org/eolang/parser/Stack.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
*
2727
* @since 0.1
2828
*/
29-
@SuppressWarnings("PMD.UnnecessaryLocalRule")
3029
final class Stack {
3130

3231
/**
@@ -189,6 +188,7 @@ Level push(
189188
}
190189
parent = under.kind();
191190
patom = under.atom();
191+
under.observeVoid(kind, line, indent);
192192
}
193193
if (!this.levels.isEmpty()) {
194194
this.opener.beforeChild(this.top());
@@ -251,6 +251,7 @@ Level replace(final int line, final Kind kind, final Openness openness) {
251251
final Level under = this.top();
252252
parent = under.kind();
253253
patom = under.atom();
254+
under.observeVoid(kind, line, indent);
254255
this.opener.beforeChild(under);
255256
}
256257
final Level fresh = new Level(indent, line, kind, openness, parent, patom);

eo-parser/src/test/java/org/eolang/parser/EoTest.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,17 @@ void rejectsFormaListOutsideAtom() {
214214
);
215215
}
216216

217+
@Test
218+
void rejectsVoidBelowRegularAttribute() {
219+
MatcherAssert.assertThat(
220+
"a void declared after a regular attribute must be rejected",
221+
EoTest.render("[] > foo", " 6 > six", " ? > x", " 5 > five", " ? > y"),
222+
XhtmlMatchers.hasXPath(
223+
"/object/errors/error[contains(text(),'void attribute must be declared above')]"
224+
)
225+
);
226+
}
227+
217228
@Test
218229
void parsesAtomDeclaration() {
219230
MatcherAssert.assertThat(
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com
2+
# SPDX-License-Identifier: MIT
3+
---
4+
# yamllint disable rule:line-length
5+
sheets:
6+
- /org/eolang/parser/parse/wrap-method-calls.xsl
7+
- /org/eolang/parser/parse/vars-float-up.xsl
8+
- /org/eolang/parser/parse/move-voids-up.xsl
9+
- /org/eolang/parser/parse/build-fqns.xsl
10+
- /org/eolang/parser/parse/expand-aliases.xsl
11+
- /org/eolang/parser/parse/resolve-aliases.xsl
12+
- /org/eolang/parser/parse/add-default-package.xsl
13+
- /org/eolang/parser/parse/roll-bases.xsl
14+
- /org/eolang/parser/parse/decorate.xsl
15+
- /org/eolang/parser/parse/mandatory-as.xsl
16+
asserts:
17+
- /object[not(errors)]
18+
- /object/o[@name='fopen']/o[@name='e' and @base='∅' and @types='Φ.foo']
19+
input: |
20+
+alias foo
21+
22+
[] > fopen /file
23+
? > e /{foo}

eo-parser/src/test/resources/org/eolang/parser/eo-packs/parse/vertical-voids.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@ asserts:
2222
- /object/o[@name='foo']/o[4][@name='φ' and @base='Φ.hello']
2323
input: |
2424
[name] > foo
25-
hello > @
2625
? > bar
2726
? > test
27+
hello > @
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com
2+
# SPDX-License-Identifier: MIT
3+
---
4+
# yamllint disable rule:line-length
5+
line: 3
6+
message: "expected value"
7+
input: |
8+
# No comments.
9+
[] > foo
10+
bar ? baz > x
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com
2+
# SPDX-License-Identifier: MIT
3+
---
4+
# yamllint disable rule:line-length
5+
line: 3
6+
message: "expected value"
7+
input: |
8+
# No comments.
9+
[] > foo
10+
bar. ? q > m

0 commit comments

Comments
 (0)