Table of Contents

NonTerminal — 안에 규칙을 품는 가지

🎓 심화 과정 이에요. 앞에서 Symbol(공통 추상)과 Terminal(잎)을 봤죠. 이제 나머지 한 갈래 — 가지, NonTerminal 입니다.

NonTerminalExpr, Term, Factor 같은 규칙 이에요.

Terminal과 결정적으로 다른 점이 하나 있어요.

Terminal은 잎이라 안이 비어 있었죠.
NonTerminal은 정반대 — 안에 뭔가를 잔뜩 품어요.

저자의 고민 — "규칙은 여러 방법 으로 만들어질 수 있잖아"

우리 예제의 첫 줄을 다시 볼게요.

Expr : Expr '+' Term | Term ;

Expr을 만드는 방법이 두 가지 죠.
Expr '+' Term 이거나, 그냥 Term 이거나, 둘 중 하나예요.

저자는 여기서 이렇게 판단했을 거예요.

"규칙 하나는 여러 개의 대안 으로 만들어질 수 있어. 그러면 NonTerminal은 자기 안에 그 대안들의 묶음 을 품고 있어야겠네."

그래서 NonTerminalalters 라는 걸 품어요.

📍 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 이구나" 까지면 충분해요.

순회하면 — 대안 하나하나가 튀어나와요

NonTerminalIEnumerable<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의 전체 모습

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 같은 선택(택일) — 이 둘의 정체를 다음 장에서 파헤쳐요.

👉 Concat — 순서(연접)