Skip to content

Commit bd80e57

Browse files
committed
Inline single EBNF operators in single alternatives
1 parent 81e6f4c commit bd80e57

8 files changed

Lines changed: 173 additions & 60 deletions

File tree

‎.github/copilot-instructions.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,11 @@ See [plan-replaceEpsilonAnalyzer.md](../plan-replaceEpsilonAnalyzer.md), [TODO.m
4343
- Methods and local variables: `camelCase`
4444
- Private fields: `camelCase` — trailing underscore (`camelCase_`) only when the field name conflicts with a getter or setter of the same name
4545
- Module names: lowercase dot-separated packages (e.g. `gamma.grammar.Nonterminal`)
46-
- Do not abbreviate names — write `nonterminal` not `nt`, `alternative` not `alt`; exceptions: well-known domain abbreviations such as `lhs`, `rhs`, `eag`
46+
- Do not abbreviate names — write `nonterminal` not `nt`, `alternative` not `alt`, `operator` not `op`; exceptions: well-known domain abbreviations such as `lhs`, `rhs`, `eag`
4747

4848
### Code style
4949
- Avoid `cast` whenever possible; prefer design choices (templates, interfaces, `std.sumtype`) that eliminate the need for runtime type casting
50+
- Prefer a well-named helper function over a code block preceded by an explanatory comment
5051
- Defensive copies in constructors: use `.dup` on arrays
5152
- D `in`-contracts for preconditions (e.g. `in (lexer.front == Token.string_)`)
5253
- **No bare `true`/`false` arguments** — use `std.typecons.Flag` instead. Declare parameters as `Flag!"name"` and pass `Yes!"name"` / `No!"name"` at call sites. `Yes!` and `No!` convert implicitly to `bool`, so the function body needs no changes (see `Variable`'s `Unequal` flag for an example).

‎plan-replaceEpsilonAnalyzer.md‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,13 @@ Checkpoint: manual review + `dub test --build=unittest --config=example`
8383

8484
---
8585

86-
## Phase 2½: Shrink — merge named nonterminal with its sole EBNF operator in the parser
86+
## Phase 2½: Inline single EBNF operator — merge named nonterminal with its single EBNF operator in the parser
8787

8888
**Goal**: when a named rule's entire RHS is a single EBNF operator (`()`, `[]`, `{}`) with formal params and nothing else, the parser must NOT create a separate anonymous nonterminal. The named nonterminal *is* the operator. Fixing this at the grammar-model level eliminates all downstream workarounds.
8989

9090
### Background — what epsilon does
9191

92-
Epsilon's `Shrink` pass (called from `Specification()`) walks every hyper alternative. If it finds an `EAG.Grp` factor that is the sole factor in the alternative, carries no actual params, and whose body alternatives have formal params (i.e. an anonymous nonterminal whose rule has the shape `<formals>: body <formals>.`), it merges: `HNont[namedSym]` takes the signature and the body alternatives of the anonymous nonterminal directly. The anonymous nonterminal entry is then unused.
92+
Epsilon's `Shrink` pass (called from `Specification()`) walks every hyper alternative. If it finds an `EAG.Grp` factor that is the single factor in the alternative, carries no actual params, and whose body alternatives have formal params (i.e. an anonymous nonterminal whose rule has the shape `<formals>: body <formals>.`), it merges: `HNont[namedSym]` takes the signature and the body alternatives of the anonymous nonterminal directly. The anonymous nonterminal entry is then unused.
9393

9494
### What gamma currently does instead
9595

@@ -99,23 +99,23 @@ The parser creates an anonymous nonterminal via `hyperGrammarBuilder.buildAnonym
9999

100100
### Plan
101101

102-
- [ ] **Parser — detect the Shrinkable pattern at the point of EBNF operator creation**: after `parseHyperExpr` returns and `rule` is built, check:
102+
- [x] **Parser — detect the single-operator wrapper pattern at the point of EBNF operator creation**: after `parseHyperExpr` returns and `rule` is built, check:
103103
1. `hasFormalParams` — the anonymous nonterminal's lhs has a signature
104104
2. `nodes.empty` — nothing preceded the operator in this alternative (checked *before* `nodes ~= operator`)
105105
3. `enclosingNonterminal` is not an `AnonymousNonterminal` — we are inside a named rule
106106
4. Nothing follows: current `lexer.front` is a terminator (`|`, `.`, `)`, `]`, `}`, or empty)
107107

108108
When all four hold: instead of using `identifier` (freshly created anonymous nonterminal), patch the alternatives so their `lhs.nonterminal` is `enclosingNonterminal` (or build them that way from the start by passing `enclosingNonterminal` to `parseHyperExpr`). The `Operator` node inserted into the outer `nodes` then references `enclosingNonterminal` as the operator's rule's lhs — no anonymous nonterminal is created.
109109

110-
- [ ] **Remove signature trick from `parser.d`**: delete the `signatureByNonterminal[enclosingNonterminal]` block (currently lines ~574–587); it is no longer needed because the body alternatives already have the correct lhs nonterminal.
110+
- [x] **Remove signature trick from `parser.d`**: delete the `signatureByNonterminal[enclosingNonterminal]` block (currently lines ~574–587); it is no longer needed because the body alternatives already have the correct lhs nonterminal.
111111

112-
- [ ] **Remove `CheckRep` imitation from `EAGBuilder.buildAffixes()`**: delete the `repActualIsUndecided` block and the `affix_wellMatchedEmpty` helper; with the grammar model correct, the `repAlt.params` situation that triggered the imitation no longer arises.
112+
- [x] **Remove `CheckRep` imitation from `EAGBuilder.buildAffixes()`**: delete the `repActualIsUndecided` block and the `affix_wellMatchedEmpty` helper; with the grammar model correct, the `repAlt.params` situation that triggered the imitation no longer arises.
113113

114-
- [ ] **Remove the `open != '('` guard removal** (the last parser.d fix): the Shrinkable-pattern detection now handles all three bracket types uniformly; the explicit guard is moot.
114+
- [x] **Remove the `open != '('` guard removal** (the last parser.d fix): the single-operator wrapper detection now handles all three bracket types uniformly; the explicit guard is moot.
115115

116-
- [ ] **Simplify `buildHyper` and `compareHyper`** in `EAGBuilder.d`: remove any code paths that special-case named-vs-anonymous nonterminal merging; the grammar model now tells the truth.
116+
- [x] **Simplify `buildHyper` and `compareHyper`** in `EAGBuilder.d`: remove any code paths that special-case named-vs-anonymous nonterminal merging; the grammar model now tells the truth.
117117

118-
Checkpoint: `dub test --build=unittest --config=example` — all tests pass with the Shrink fix in place and workarounds removed
118+
Checkpoint: `dub test --build=unittest --config=example` — all tests pass with the single-EBNF-operator inlining in place and workarounds removed
119119

120120
---
121121

@@ -148,7 +148,7 @@ builder.compareMeta(); // logs/asserts diffs — free regression check
148148
- [x] `compareHyper()`: diff against `EAG.HNont[]`, walking the Alt/Factor chains structurally; translate embedded MNont/MTerm references through the same Id-bijection maps used in `compareMeta()`
149149
- Reference: epsilon/analyzer.d Specification() for exact buffer/pointer layout
150150

151-
- [ ] **Pending simplification**: once the parser performs Shrink (merging the named nonterminal with its sole-EBNF-operator anonymous nonterminal in the grammar model), the workarounds in `buildHyper` and `buildAffixes` that compensate for the grammar model having two distinct nonterminals where epsilon sees one will need to be removed.
151+
- [ ] **Pending simplification**: once the parser inlines the single EBNF operator (merging the named nonterminal with its single-EBNF-operator anonymous nonterminal in the grammar model), the workarounds in `buildHyper` and `buildAffixes` that compensate for the grammar model having two distinct nonterminals where epsilon sees one will need to be removed.
152152

153153
### 3c — Affix forms / parameter model
154154
- [ ] `buildAffixes()`: walk `Term` trees from Phase 1 (Variable/Composite hierarchy); emit builder's `VarRecord[]` (Def, Neg, Num, Sign), `NodeBuf[]`, `MSymBuf[]`, `ParamRecord[]`, `ScopeDesc[]`; pure format conversion — no re-parsing

‎src/gamma/grammar/Grammar.d‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import gamma.grammar.Nonterminal;
44
import gamma.grammar.Rule;
55
import gamma.grammar.Terminal;
66
import gamma.grammar.Visitor;
7+
import std.range;
78

89
public class Grammar
910
{
@@ -24,9 +25,11 @@ public class Grammar
2425

2526
this.nonterminals_ = nonterminals.dup;
2627
this.terminals_ = terminals.dup;
27-
this.rules_ = rules.dup;
28+
this.rules_ = new Rule[nonterminals.length]; // null for abandoned nonterminals
29+
foreach (rule; rules)
30+
this.rules_[rule.alternatives.front.lhs.nonterminal.index] = rule;
2831
this.startSymbol_ = startSymbol;
29-
this.isPlain_ = this.rules_
32+
this.isPlain_ = rules
3033
.all!(rule => rule.alternatives
3134
.all!(alternative => alternative.rhs
3235
.all!(node => cast(SymbolNode) node !is null)));
@@ -49,7 +52,10 @@ public class Grammar
4952

5053
public Rule[] rules()
5154
{
52-
return this.rules_;
55+
import std.algorithm : filter;
56+
import std.array : array;
57+
58+
return this.rules_.filter!(rule => rule !is null).array;
5359
}
5460

5561
public Nonterminal startSymbol()

‎src/gamma/grammar/GrammarBuilder.d‎

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ module gamma.grammar.GrammarBuilder;
22

33
import gamma.grammar.Alternative;
44
import gamma.grammar.Grammar;
5-
import gamma.grammar.hyper.AnonymousNonterminal;
65
import gamma.grammar.LhsNode;
76
import gamma.grammar.Nonterminal;
87
import gamma.grammar.Rule;
@@ -16,15 +15,15 @@ version (unittest) import gamma.util.Position;
1615

1716
public struct GrammarBuilder
1817
{
19-
private Nonterminal[string] nonterminalMap;
18+
package(gamma.grammar) Nonterminal[string] nonterminalMap;
2019

21-
private Nonterminal[] nonterminals;
20+
package(gamma.grammar) Nonterminal[] nonterminals;
2221

2322
private Terminal[string] terminalMap;
2423

2524
private Terminal[] terminals;
2625

27-
private Alternative[][] alternativesMap;
26+
package(gamma.grammar) Alternative[][] alternativesMap;
2827

2928
private bool[Nonterminal] undefinedNonterminals;
3029

@@ -46,26 +45,6 @@ public struct GrammarBuilder
4645
return nonterminal;
4746
}
4847

49-
public AnonymousNonterminal buildAnonymousNonterminal()
50-
{
51-
import std.exception : enforce;
52-
import std.format : format;
53-
54-
const index = this.nonterminals.length;
55-
auto nonterminal = new AnonymousNonterminal(index);
56-
57-
enforce(nonterminal.toString !in this.nonterminalMap,
58-
format!"generated nonterminal name already defined by the user: %s"(nonterminal));
59-
60-
this.nonterminalMap[nonterminal.toString] = nonterminal;
61-
this.nonterminals ~= nonterminal;
62-
this.alternativesMap ~= null;
63-
64-
// anonymous nonterminals for EBNF expressions are never undefined
65-
66-
return nonterminal;
67-
}
68-
6948
public Terminal buildTerminal(string representation)
7049
{
7150
Terminal terminal = this.terminalMap.get(representation, null);

‎src/gamma/grammar/hyper/EBNFConverter.d‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import gamma.grammar.hyper.Group;
66
import gamma.grammar.hyper.HyperLhsNode;
77
import gamma.grammar.hyper.HyperSymbolNode;
88
import gamma.grammar.hyper.HyperVisitor;
9+
import gamma.grammar.hyper.Operator;
910
import gamma.grammar.hyper.Option;
1011
import gamma.grammar.hyper.Repetition;
1112
import gamma.grammar.hyper.RepetitionAlternative;
@@ -57,7 +58,8 @@ private class EBNFConverter : HyperVisitor
5758
{
5859
this.rhsStack ~= null;
5960
alternative.rhs.each!(node => node.accept(this));
60-
this.alternatives ~= new Alternative(alternative.lhs, this.rhsStack.back, alternative.position);
61+
if (!alternative.isInlinedOperator)
62+
this.alternatives ~= new Alternative(alternative.lhs, this.rhsStack.back, alternative.position);
6163
this.rhsStack.popBack;
6264
}
6365

@@ -135,6 +137,17 @@ private class EBNFConverter : HyperVisitor
135137
if (!alternatives.empty)
136138
rules ~= new Rule(alternatives.array);
137139
}
140+
138141
return new Grammar(this.nonterminals, this.terminals, rules, this.startSymbol);
139142
}
140143
}
144+
145+
private bool isInlinedOperator(Alternative alternative)
146+
{
147+
if (alternative.rhs.length != 1)
148+
return false;
149+
150+
auto operator = cast(Operator) alternative.rhs.front;
151+
152+
return operator !is null && operator.rule.lhs.nonterminal == alternative.lhs.nonterminal;
153+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
module gamma.grammar.hyper.HyperGrammarBuilder;
2+
3+
import gamma.grammar.affixes.Signature;
4+
import gamma.grammar.Alternative;
5+
import gamma.grammar.GrammarBuilder;
6+
import gamma.grammar.hyper.AnonymousNonterminal;
7+
import gamma.grammar.hyper.Group;
8+
import gamma.grammar.hyper.HyperLhsNode;
9+
import gamma.grammar.hyper.Operator;
10+
import gamma.grammar.hyper.Option;
11+
import gamma.grammar.hyper.Repetition;
12+
import gamma.grammar.Node;
13+
import gamma.grammar.Nonterminal;
14+
import gamma.grammar.Rule;
15+
import std.range;
16+
17+
public struct HyperGrammarBuilder
18+
{
19+
GrammarBuilder builder;
20+
21+
alias builder this;
22+
23+
public AnonymousNonterminal buildAnonymousNonterminal()
24+
{
25+
import std.exception : enforce;
26+
import std.format : format;
27+
28+
const index = builder.nonterminals.length;
29+
auto nonterminal = new AnonymousNonterminal(index);
30+
31+
enforce(nonterminal.toString !in builder.nonterminalMap,
32+
format!"generated nonterminal name already defined by the user: %s"(nonterminal));
33+
34+
builder.nonterminalMap[nonterminal.toString] = nonterminal;
35+
builder.nonterminals ~= nonterminal;
36+
builder.alternativesMap ~= null;
37+
38+
return nonterminal;
39+
}
40+
41+
/**
42+
* Replaces each anonymous nonterminal whose EBNF operator appears exclusively on the RHS
43+
* of a single alternative with the nonterminal from the LHS.
44+
*
45+
* Returns: a map of nonterminal → signature for each nonterminal that was inlined
46+
* and whose operator carried formal parameters.
47+
*/
48+
public Signature[Nonterminal] inlineSingleOperators()
49+
{
50+
import std.algorithm : map;
51+
import std.array : array;
52+
53+
Signature[Nonterminal] signatureByNonterminal;
54+
55+
foreach (nonterminal; builder.nonterminals)
56+
{
57+
if (cast(AnonymousNonterminal) nonterminal)
58+
continue;
59+
60+
auto alternatives = builder.alternativesMap[nonterminal.index];
61+
62+
if (alternatives.length != 1)
63+
continue;
64+
65+
auto alternative = alternatives.front;
66+
67+
if (alternative.rhs.length != 1)
68+
continue;
69+
70+
auto operator = cast(Operator) alternative.rhs.front;
71+
72+
if (operator is null)
73+
continue;
74+
75+
auto alternativeLhs = cast(HyperLhsNode) alternative.lhs;
76+
77+
if (alternativeLhs !is null && alternativeLhs.params !is null)
78+
continue;
79+
80+
auto rule = new Rule(operator.rule.alternatives.map!(a => a.rebuild(nonterminal)).array);
81+
Node rebuiltOperator = rebuildOperator(operator, rule);
82+
auto rebuiltAlternative = new Alternative(alternative.lhs, [rebuiltOperator], alternative.position);
83+
84+
builder.alternativesMap[nonterminal.index][0] = rebuiltAlternative;
85+
86+
if (auto lhsNode = cast(HyperLhsNode) rule.lhs)
87+
if (lhsNode.signature !is null)
88+
signatureByNonterminal[nonterminal] = lhsNode.signature;
89+
}
90+
return signatureByNonterminal;
91+
}
92+
}
93+
94+
private Operator rebuildOperator(Operator operator, Rule rule)
95+
{
96+
if (auto group = cast(Group) operator)
97+
return new Group(group.params, rule, group.position);
98+
if (auto option = cast(Option) operator)
99+
return new Option(option.params, rule, option.endParams, option.position);
100+
if (auto repetition = cast(Repetition) operator)
101+
return new Repetition(repetition.params, rule, repetition.endParams, repetition.position);
102+
103+
assert(false, "unknown operator type");
104+
}
105+
106+
private Alternative rebuild(Alternative alternative, Nonterminal nonterminal)
107+
{
108+
import gamma.grammar.hyper.RepetitionAlternative : RepetitionAlternative;
109+
110+
auto alternativeLhs = cast(HyperLhsNode) alternative.lhs;
111+
auto lhs = new HyperLhsNode(nonterminal, alternativeLhs.signature, alternativeLhs.params, alternativeLhs.position);
112+
113+
if (auto repetitionAlt = cast(RepetitionAlternative) alternative)
114+
return new RepetitionAlternative(lhs, alternative.rhs, repetitionAlt.params, alternative.position);
115+
return new Alternative(lhs, alternative.rhs, alternative.position);
116+
}

‎src/gamma/input/epsilang/analyzer.d‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ class Analyzer
6969
import gamma.grammar.hyper.PrintingHyperVisitor : toPrettyString;
7070

7171
this.plainHyperGrammar_ = new HyperGrammar(convert(hyperEBNFGrammar), hyperEBNFGrammar.terms);
72-
log.trace!"converted hyper grammar:\n%s"(this.plainHyperGrammar_.toPrettyString);
72+
log.trace!"transformed BNF grammar:\n%s"(this.plainHyperGrammar_.toPrettyString);
7373
}
7474

7575
this.hyperGrammarProperties =

0 commit comments

Comments
 (0)