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 이 핵심이에요.
Single 은 Concat 을 상속 해요 — 즉
순서(RHS)는 이미 자기 안에 갖고 있고, 거기에 꼬리표만 얹은 거죠.
public class NonTerminalSingle : NonTerminalConcat // ← 순서(RHS)는 상속으로 이미 가짐
{
private NonTerminal _wholeExpression; // 꼬리표 ① "어느 규칙" (Expr 전체)
private sbyte alterIndex = 0; // 꼬리표 ② "몇 번째 대안" (0, 1, …)
}
그림으로 — Concat 위에 꼬리표 두 개
말로는 추상적이니 그려볼게요.
Expr 의 0번째 대안을 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());
해시를 UniqueKey 와 alterIndex 를 이어 붙여 만들어요.
풀어 말하면 —
"어느 규칙(UniqueKey)의, 몇 번째 대안(alterIndex)인가"가 곧 정체성 이에요.
구체적으로 볼게요.
Expr 의 UniqueKey 가 7 이라고 하면:
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)과 대안 구조 — https://www.lucidchart.com/documents/edit/332a9afe-d053-4c13-ab2a-7110f25bff73/0
(저자 본인의 설계 노트라 열람 권한이 필요할 수 있어요.)
다음 장
Single 로 생성규칙 하나 를 표현하는 법을 봤어요.
그런데 Expr 은 생성규칙이 여러 개(0번째, 1번째…) 였죠.
이 여러 개를 하나로 묶어 담는
그릇 이 마지막 조각이에요.
바로 NonTerminal 이 품고 있던 그 alters 의
정체이기도 하고요.