NonTerminal — 안에 규칙을 품는 가지
🎓 심화 과정 이에요. 앞에서 Symbol(공통 추상)과 Terminal(잎)을 봤죠. 이제 나머지 한 갈래 — 가지, NonTerminal 입니다.
NonTerminal은 Expr, Term, Factor 같은 규칙 이에요.
Terminal과 결정적으로 다른 점이 하나 있어요.
Terminal은 잎이라 안이 비어 있었죠.
NonTerminal은 정반대 — 안에 뭔가를 잔뜩 품어요.
저자의 고민 — "규칙은 여러 방법 으로 만들어질 수 있잖아"
우리 예제의 첫 줄을 다시 볼게요.
Expr : Expr '+' Term | Term ;
Expr을 만드는 방법이 두 가지 죠.
Expr '+' Term 이거나, 그냥 Term 이거나, 둘 중 하나예요.
저자는 여기서 이렇게 판단했을 거예요.
"규칙 하나는 여러 개의 대안 으로 만들어질 수 있어. 그러면 NonTerminal은 자기 안에 그 대안들의 묶음 을 품고 있어야겠네."
그래서 NonTerminal은 alters 라는 걸 품어요.
📍
NonTerminal : Symbol·…/RegularGrammar/NonTerminal.cs
public class NonTerminal : Symbol, IEnumerable<NonTerminalSingle>
{
private NonTerminalAlter alters = new(); // ← 이 규칙의 "대안들"
public string Name { get; } // 규칙 이름 (Expr, Term …)
public bool IsStartSymbol { get; } // 이 규칙이 파싱의 출발점인가
}
Expr : Expr '+' Term | Term ; 이 한 줄이, 통째로 이 alters 안에 들어가는 거예요.
말로만 하면 추상적이죠?
구체적으로 어떤 모양인지 그려볼게요.
Expr : Expr '+' Term | Term ; │ ▼ 이 한 줄이 alters 안에 이렇게 담겨요 alters (NonTerminalAlter) ├─ 대안 0 : [ Expr ] · [ '+' ] · [ Term ] ← "Expr '+' Term" (순서대로) └─ 대안 1 : [ Term ] ← "Term"
| 가 대안을 가르고, 각 대안 안에서는 기호들이 순서대로 늘어서요. (이 "순서"와 "대안"을
코드에서 각각 어떻게 표현하는지가 — 다음 Concat 장부터의 주제예요.)
두 개의 꼬리표 — 이름과 시작 기호
규칙에는 정보 두 개가 더 붙어요.
① Name — 규칙의 이름
Name — 규칙의 이름이에요.
Expr, Term 같은 이름이에요. (Symbol의 정체성은 UniqueKey였죠? Name은 그것과 별개의 표시용 이에요.)
② IsStartSymbol — 파싱의 출발점인가
IsStartSymbol — 이 규칙이 문법의 시작 기호 인가를 표시해요.
시작 기호는 언어 전체를 만들어 내는 규칙이에요 — 파스 트리의 뿌리 이고, 파싱이 끝나면 도달해야 할 기호죠. (LR 파서는 입력을 첫 토큰부터 읽어 올라가서, 마지막에 이 기호로 접혀요.)
그 시작 기호에만 이 표시가 켜져요.
규칙에 생성규칙 달기 — AddItem / SetItem
그럼 우리가 코드로 Expr에 대안을 추가 하려면 어떻게 할까요?
NonTerminal이 제공하는 AddItem / SetItem 으로요.
// NonTerminal.cs
public void AddItem(NonTerminal production, MeaningUnit meaningUnit = null) { ... }
public void SetItem(NonTerminal production, MeaningUnit meaningUnit = null) { ... }
AddItem— 기존 대안들에 하나 더 얹어요.SetItem— 기존 걸 싹 지우고 새로 깔아요.
여기서 같이 받는 MeaningUnit 은 "이 규칙이 AST에서 어떤 의미 단위가 되는가" 인데 — 그건 나중에
따로 다뤄요.
지금은 "규칙에 대안을 다는 입구가 AddItem/SetItem 이구나" 까지면 충분해요.
순회하면 — 대안 하나하나가 튀어나와요
NonTerminal은 IEnumerable<NonTerminalSingle> 이에요.
즉 foreach (var single in Expr) 하면, 대안이 하나씩 NonTerminalSingle 로 나와요.
foreach (NonTerminalSingle single in Expr) // 0번째 대안, 1번째 대안 …
{
// single = "Expr의 N번째 대안"
}
이 NonTerminalSingle 이 정확히 뭔지는 — 바로 다음 주제예요.
한 장의 그림
지금까지를 그림으로 묶으면 이래요.
NonTerminal "Expr"
├ Name = "Expr", IsStartSymbol = ?
└ alters : NonTerminalAlter ← 대안들의 묶음
├ (대안 0) Expr '+' Term
└ (대안 1) Term
foreach 로 순회하면 → NonTerminalSingle 0, NonTerminalSingle 1
📐 저자의 설계 다이어그램
- NonTerminal의 자식(대안) 구조 — https://www.lucidchart.com/documents/edit/332a9afe-d053-4c13-ab2a-7110f25bff73/0
(저자 본인의 설계 노트라 열람 권한이 필요할 수 있어요.)
한눈에 — NonTerminal의 전체 모습
NonTerminal 클래스의 전체 골격 이에요.
로직은 비우고 무엇이 있는지 만 보여줘요. (Terminal보다
훨씬 풍성하죠 — 안에 대안들을 품으니까요.)
public class NonTerminal : Symbol, IEnumerable<NonTerminalSingle>, ICloneable
{
private NonTerminalAlter alters; // ← 대안들의 묶음 (핵심)
// ── 정보 ────────────────────────────────
public string Name { get; } // 규칙 이름
public bool IsStartSymbol { get; internal set; } // 파싱 출발점인가
public bool AutoGenerated { get; } // 수량자 등으로 자동 생성됐나
public bool IsInduceEpsilon { get; } // 대안 중 빈 것(ε)이 있나
public int Count { get; } // 대안 개수
public NonTerminalSingle this[int index] { get; } // N번째 대안
// ── 생성 ────────────────────────────────
public NonTerminal(string name, bool bStartSymbol = false, bool autoGenerated = false);
// ── 대안 달기 ───────────────────────────
public void AddItem(NonTerminal production, MeaningUnit mu = null); // 하나 더 얹기
public void AddItem(NonTerminal production, uint priority, MeaningUnit mu = null);
public void AddItem(Terminal item, MeaningUnit mu = null);
public void SetItem(NonTerminal production, MeaningUnit mu = null); // 싹 갈아끼우기
public void AddAsConcat(params Symbol[] symbols); // 한 줄(연접)을 대안으로
public void AddAsAlter(params Symbol[] symbols); // 각각을 따로 대안으로
// ── 탐색 / 치환 ─────────────────────────
public bool IsContain(Symbol item, ...);
public bool IsExistRefContent(NonTerminal target);
public void Replace(NonTerminal from, NonTerminal to, ...);
public HashSet<NonTerminal> ToNonTerminalSet();
// ── 순회 / 표현 ─────────────────────────
// IEnumerable<NonTerminalSingle> 명시적 구현 — public 아님, foreach 로만 접근
IEnumerator<NonTerminalSingle> IEnumerable<NonTerminalSingle>.GetEnumerator(); // 대안 하나씩 → NonTerminalSingle
public override string ToEbnfString(bool bContainLHS = false);
public override string ToGrammarString();
public override string ToString(); // → Name
}
다음 장
NonTerminal이 대안들의 묶음(alters) 을 품는다는 걸 봤어요.
그런데 그 대안 하나 는 코드 안에서 정확히 어떤 모양일까요?
Expr '+' Term 같은 순서(연접) 와, ... | Term 같은 선택(택일) — 이 둘의 정체를
다음 장에서 파헤쳐요.