Table of Contents

Single — 한 개의 생성규칙

🎓 심화 과정 이에요. 앞 Concat 장에서 순서(연접) 를 담는 법을 봤죠.
이제 그 순서에 "꼬리표" 를 붙여, 진짜 생성규칙 하나 로 만드는 단계예요.

먼저, 앞 장에서 만든 Concat 하나를 다시 떠올려 볼게요.

   [ Expr ] · [ '+' ] · [ Term ]      ← NonTerminalConcat (순서만 담은 리스트)

이건 순서 는 알아요.
그런데 — 이게 어느 규칙 의, 몇 번째 대안인지는 몰라요.

저자의 고민 — "이 순서가 누구의 몇 번째 인지 알아야 할 때가 있어"

우리 예제를 다시 봐요.

Expr : Expr '+' Term | Term ;

Expr 을 만드는 방법이 둘이었죠.
0번째는 Expr '+' Term, 1번째는 Term.

그런데 파서가 일하다 보면 — 표를 그리거나, 진단을 찍거나, 나중에 LR 상태를 만들 때 — "지금 이건 Expr 규칙의 0번째 대안 이야" 라고 콕 집어 말해야 할 때가 많아요.

벌거벗은 Concat(그냥 [Expr, '+', Term])만으로는 그걸 말할 수 없어요.
그래서 저자는 이렇게 판단했을 거예요.

"순서를 담는 Concat 은 이미 있어. 거기에 '어느 규칙''몇 번째 대안' 이라는 꼬리표 두 개만 더 붙이면, 그게 곧 생성규칙 하나 잖아."

그래서 만든 게 NonTerminalSingle 이에요.
이름 그대로 "낱개의(single) 생성규칙" 이에요.

📍 NonTerminalSingle : NonTerminalConcat · …/RegularGrammar/NonTerminalSingle.cs

여기서 : NonTerminalConcat 이 핵심이에요.
SingleConcat상속 해요 — 즉 순서(RHS)는 이미 자기 안에 갖고 있고, 거기에 꼬리표만 얹은 거죠.

public class NonTerminalSingle : NonTerminalConcat   // ← 순서(RHS)는 상속으로 이미 가짐
{
    private NonTerminal _wholeExpression;   // 꼬리표 ① "어느 규칙"  (Expr 전체)
    private sbyte       alterIndex = 0;     // 꼬리표 ② "몇 번째 대안" (0, 1, …)
}

그림으로 — Concat 위에 꼬리표 두 개

말로는 추상적이니 그려볼게요.
Expr0번째 대안을 Single 로 만들면 이래요.

   Expr : Expr '+' Term | Term ;
                  │  (0번째 대안을 떼어)
                  ▼
   NonTerminalSingle
     ├ Name        = "Expr"                 ← _wholeExpression 에서 가져옴
     ├ alterIndex  = 0                       ← "몇 번째 대안"
     └ (RHS — Concat 상속)  [ Expr ] · [ '+' ] · [ Term ]

그리고 이 Single 을 글자로 뽑으면(ToGrammarString()), 진짜 생성규칙 한 줄 이 나와요.

   ToGrammarString()  →  "Expr -> Expr '+' Term"

1번째 대안도 똑같이 만들 수 있죠.

   NonTerminalSingle  (alterIndex = 1)
     └ (RHS)  [ Term ]          →  "Expr -> Term"

보세요 — Concat 은 그냥 기호 줄 이었는데, Single 이 되니 "Expr 의 생성규칙 하나" 라는 어엿한 단위가 됐죠.

왜 이름(Name)만 베끼지 않고 규칙 전체 를 붙들까?

여기서 작지만 저자다운 결정이 하나 보여요.

꼬리표 ①은 그냥 문자열 "Expr" 만 저장해도 될 것 같죠?
그런데 코드는 규칙 전체 (_wholeExpression, NonTerminal 그 자체)를 붙들고 있어요.
그리고 필요한 정보들을 거기서 그때그때 꺼내 써요.

public UInt32 UniqueKey      => _wholeExpression.UniqueKey;
public bool   IsStartSymbol  => _wholeExpression.IsStartSymbol;
public bool   AutoGenerated  => _wholeExpression.AutoGenerated;
public bool   IsInduceEpsilon=> _wholeExpression.IsInduceEpsilon;
public string Name           => _wholeExpression.Name;

왜 이렇게 했을까요?

"이 생성규칙은 살아있는 Expr 규칙의 일부 야. Expr의 정체성(UniqueKey)이나 시작 기호 여부가 바뀌면, 이 Single도 자동으로 같이 따라가야지. 복사해 두면 따로 놀잖아."

즉 — 이름표를 베껴 붙이는 게 아니라, 원본에 연결 해 둔 거예요.
Symbol 장에서 본 "정체성은 UniqueKey" 철학과도 이어져요.
Single 의 정체성도 결국 원본 규칙의 키를 따라가니까요.

정체성 — "같은 규칙의 같은 대안"이면 같다

그럼 두 Single같은지 는 어떻게 볼까요?
코드를 보면 답이 나와요.

public override int GetHashCode()
    => Convert.ToInt32(this.UniqueKey.ToString() + this.alterIndex.ToString());

해시를 UniqueKeyalterIndex 를 이어 붙여 만들어요.
풀어 말하면 — "어느 규칙(UniqueKey)의, 몇 번째 대안(alterIndex)인가"가 곧 정체성 이에요.

구체적으로 볼게요.
ExprUniqueKey7 이라고 하면:

   Expr-0  ( UniqueKey=7, alterIndex=0 )  ─┐
   Expr-0  ( UniqueKey=7, alterIndex=0 )  ─┴→  같음   (해시 "70" 동일)
   Expr-1  ( UniqueKey=7, alterIndex=1 )  ───→  다름   (해시 "71")

어디서 따로 만들었든 Expr의 0번째 끼리는 같은 것으로 취급되고, Expr의 0번째1번째 는 다른 생성규칙으로 구분돼요.
파서가 "이 생성규칙, 아까 그거랑 같은 거 맞아?" 를 정확히 따질 수 있는 거죠.

한 발 앞서 — 이게 LR 아이템 의 씨앗이에요

조금만 미리 귀띔할게요.
Concat 장에서 LR 파서가 "어디까지 읽었나" 를 점() 으로 표시한다고 했죠?
A → α • β 처럼요.

점을 찍는 대상이 바로 이 Single(생성규칙) 이에요.

   Single        :  Expr -> Expr '+' Term          ("생성규칙" 그 자체)
   여기에 점을 찍으면 →  Expr -> Expr '+'Term     ("LR 아이템")

Single점 찍기 전의 생성규칙 본체 예요.
그래서 "어느 규칙의 몇 번째 대안" 이라는 정체성이 그렇게 중요했던 거고요.
(점을 찍어 LR 아이템을 만드는 이야기는 한참 뒤 LR 아이템 장에서 이어가요.)

한눈에 — Single의 전체 모습

NonTerminalSingle전체 골격 이에요.
로직은 비우고 무엇이 있는지 만 보여줘요.
(순서 관련 멤버는 부모 NonTerminalConcat 에서 그대로 물려받아요.)

public class NonTerminalSingle : NonTerminalConcat, IShowable
{
    // ── 꼬리표 (Single 이 더한 것) ───────────
    private NonTerminal _wholeExpression;   // 어느 규칙 (원본에 연결)
    private sbyte       alterIndex;          // 몇 번째 대안

    // ── 원본 규칙에서 그대로 가져오는 정보 ───
    public UInt32 UniqueKey      { get; }    // = _wholeExpression.UniqueKey
    public string Name           { get; }    // = _wholeExpression.Name
    public bool   IsStartSymbol  { get; }
    public bool   AutoGenerated  { get; }
    public bool   IsInduceEpsilon{ get; }

    // ── 생성 ────────────────────────────────
    public NonTerminalSingle(NonTerminal target, int index, uint priority, MeaningUnit mu = null);
    public NonTerminalSingle(NonTerminalSingle target);   // 복제

    // ── 변환 ────────────────────────────────
    public NonTerminal       ToNonTerminal();
    public NonTerminalConcat ToNonTerminalConcat();

    // ── 표현 ────────────────────────────────
    public string ToGrammarString();          // → "Expr -> Expr '+' Term"
    public string ToTreeString(ushort depth = 1);
    public override string ToString();         // → ToGrammarString()

    // ── 정체성 (규칙 + 대안번호) ─────────────
    public override int GetHashCode();         // UniqueKey 와 alterIndex 를 이어 붙임
    public bool Equals(NonTerminalSingle other);
    public static bool operator ==(NonTerminalSingle left, NonTerminalSingle right);
    public static bool operator !=(NonTerminalSingle left, NonTerminalSingle right);

    // … 순서(RHS) 관련 멤버는 전부 NonTerminalConcat 상속 …
}

한 줄로 줄이면 — Single = Concat(순서) + "어느 규칙" + "몇 번째 대안". 그래서 생성규칙 하나 가 돼요.

📐 저자의 설계 다이어그램

(저자 본인의 설계 노트라 열람 권한이 필요할 수 있어요.)

다음 장

Single생성규칙 하나 를 표현하는 법을 봤어요.

그런데 Expr 은 생성규칙이 여러 개(0번째, 1번째…) 였죠.
이 여러 개를 하나로 묶어 담는 그릇 이 마지막 조각이에요.
바로 NonTerminal 이 품고 있던 그 alters 의 정체이기도 하고요.

👉 Alter — 택일(대안들의 집합)