Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions eo-parser/PARSER_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down Expand Up @@ -269,6 +270,17 @@ 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`. `? > 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 `<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.
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).

Expand Down
11 changes: 11 additions & 0 deletions eo-parser/src/main/java/org/eolang/parser/Emit.java
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,17 @@ void star() {
this.append(new Directives().attr("star", ""));
}

/**
* Add the {@code @types="forma …"} attribute to the most recently
* opened {@code <o>} — 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 <o>} — inline-binding marker per §3.12 / §9.4. Numeric
Expand Down
46 changes: 27 additions & 19 deletions eo-parser/src/main/java/org/eolang/parser/Eo.java
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand All @@ -352,16 +351,10 @@ 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() == '('
Expand All @@ -374,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(
Expand All @@ -401,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.
*
Expand Down
8 changes: 7 additions & 1 deletion eo-parser/src/main/java/org/eolang/parser/Kind.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
28 changes: 28 additions & 0 deletions eo-parser/src/main/java/org/eolang/parser/Level.java
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
*/
Expand Down Expand Up @@ -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
Expand Down
173 changes: 173 additions & 0 deletions eo-parser/src/main/java/org/eolang/parser/LnVoid.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* 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 / R-3.4.8 of the spec.
*
* <p>Form: {@code ? > name} with an optional atom-only forma-list tail
* {@code /&#123;forma …&#125;}. The {@code ?} declares a void attribute
* on the enclosing formation, equivalent to listing {@code name} among
* the bracket parameters; it emits the same {@code <o name='name'
* base='∅'/>} void child (§9.4), which {@code move-voids-up} hoists
* among the head voids.</p>
*
* <p>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.</p>
*
* <p>{@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
* <em>not</em> 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.</p>
*
* @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 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(),
"a void attribute must be written as `? > name`"
);
}
Comments.attach(globals, emit, this.span, suffix.present());
final Level level = new Transition(stack, this.span).apply(
Kind.VOID, 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;
}
}
3 changes: 2 additions & 1 deletion eo-parser/src/main/java/org/eolang/parser/Stack.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
*
* @since 0.1
*/
@SuppressWarnings("PMD.UnnecessaryLocalRule")
final class Stack {

/**
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
<xsl:template match="o[@atom]">
<xsl:apply-templates select="." mode="with-atom"/>
</xsl:template>
<xsl:template match="@types">
<xsl:attribute name="types">
<xsl:value-of separator=" " select="for $t in tokenize(., ' ') return if (contains($t, '.') or $t=$eo:phi or $t=$eo:program or $t=$eo:rho or $t=$eo:empty or $t=$eo:xi or $t=$eo:bottom or $t=/object/metas/meta[head='alias']/part[1]) then $t else concat('Φ.', $t)"/>
</xsl:attribute>
</xsl:template>
<xsl:template match="/object/metas/meta[head='also' or head='decorate']/(tail|part)">
<xsl:apply-templates select="." mode="meta"/>
</xsl:template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@
<xsl:apply-templates select="node()|@* except @atom"/>
</xsl:copy>
</xsl:template>
<xsl:template match="@types">
<xsl:attribute name="types">
<xsl:value-of separator=" " select="for $t in tokenize(., ' ') return (/object/metas/meta[head='alias' and part[1]=$t]/part[last()], $t)[1]"/>
</xsl:attribute>
</xsl:template>
<xsl:template match="/object/metas/meta[head='also' or head='decorate']/(tail|part)">
<xsl:variable name="meta" select="/object/metas/meta[head='alias' and part[1] = current()/text()]"/>
<xsl:copy>
Expand Down
Loading
Loading