# 0. INTRODUCTION TO COMPILER 1. 컴파일러가 왜 필요? 1. 소스코드 vs 기계어의 차이 O 2. 자동 번역 필요 2. 컴파일러가 뭔데? 1. 컴파일러 vs 인터프리터 1. 번역과 직접 실행 2. 컴파일러는 번역 3. 번역하는 거라면 뭐가 중요할까? 1. 정확성 2. 개선 4. 그럼 컴파일러를 어떤 구조로 구현하지? 1. 가장 단순한 구조: 입력 n X 출력 m = $n \times m$개 모듈 2. 기본적 2단계 구조 1. 프론트엔드 -> IR -> 백엔드 2. $n+m$개 모듈 3. 새로운 3단계 구조 1. 정확성은 됐고, 개선도 필요함 2. 프론트엔드 -> IR -> 미들엔드 -> IR -> 백엔드 5. 구조 - 프론트엔드 (소스 -> IR) 1. 입력 언어를 1. 이해 2. 검사 3. 변환 2. 구성 요소 1. 렉서: 소스코드 -> 토큰 리스트 (오토마타) 2. 파서: 토큰 리스트 -> AST (오토마타) 3. Semantic Analyzer: AST -> AST (프로그래밍 언어론) 6. 구조 - 미들엔드 (IR -> IR) 1. IR을 1. 분석 2. 최적화 2. 구성 요소 1. optimizer 1~n개 (프로그래밍 언어론) 3. 최적화 어떻게 하는데? 1. 상수 전파 같은 거 7. 구조 - 백엔드 (IR -> target program (machine code)) 1. IR 기반으로 출력 언어(CPU 아키텍처)에 맞는 코드 생성 2. 구성 요소 1. 명령어 선택기: IR -> 의사타겟프로그램 (알고리즘) 2. 스케쥴러: 의사타겟프로그램 -> 의사타겟프로그램 (알고리즘) 3. 레지스터 할당기: 의사타겟프로그램->타겟프로그램 (알고리즘) 8. 컴파일러를 왜 배움? 1. 모든 이론 집합 2. 많이 사용 3. 좋은 코드 쓰려고 --- # 1. Lexer 1. 렉서가 어느 부분이었지? 1. 컴파일러 3단계 구조 중 프론트엔드 2. 프론트엔드 구조 중 첫 단계 2. 렉서가 뭐 하는데? 1. Source Program (string) → Lexer → Token list 3. 토큰? 토큰이 뭐야 1. 문법적으로 의미를 갖는 최소 단위 문자열 1. ID, NUM, PAREN, ... 2. lexeme(어휘소, 실제 값)이랑 같이 정의 1. 고정인 건 빼고 2. 주석/공백은 의미가 있을 경우에만 포함, 일반적으론 뺌 4. 이걸 왜 하는데? 1. 토큰리스트가 훨씬 다루기 좋으니까 5. 어떻게 동작하는데? 1. 1x23같은 어휘 lexical 오류는 잡고, 2. if of같은 문법 syntactic 오류는 통과시킨다 (파서가 함) 6. 어떤 걸 지켜야 구현이 가능하지? 1. **문자열을 토큰으로 구분할 수 있어야 하고** 2. **여기서 '토큰'은 명확히 정의되어 있어야 하고** 3. **효율적이면 좋음 (각 문자열을 O(1)번 보자)** 7. 토큰을 어떻게 정의하고 표현하지? 1. 언어가 뭔지부터 보자 1. Language = Alphabet + Grammar 2. 아 결국 Grammar 정의가 중요하겠네 2. 그럼 Grammar가 뭐지? 1. 4-tuple $(T, NT, S, P)$ 1. Terminal (epsilon 포함) 2. Non-Terminal 3. S 시작 기호 (NT에 포함) 4. Production 8. 그럼 토큰을 어떻게 효율적으로 정의하지? 1. 컴퓨터가 뭘 잘 인식하더라 1. 단순한 Regular Language 2. 그럼 Regular Language의 Regular Grammar는? 1. Left-Regular $A \to Ba$ 포함 2. Right-Regular $A \to aB$ 포함 3. 그럼 Regular Grammar로 문법 써보자 4. 복잡하네 9. 복잡한 걸 어떻게 해결할까? 1. Regular Expression을 사용하자 2. 그게 뭔데? 1. 문자열 패턴을 나타내는 방법 2. L(RE)가 언어임 3. RG보다 훨 쉬움 3. 정의는 어떻게 되는데? 1. Base Case: epsilon, a 2. Inductive Case 1. Concatenation: $R \cdot S$ 2. Alternation: $R \mid S$ 3. Kleene Star: $R^*$ 4. Group: $(R)$ 3. 우선순위는? $\ast \ > \ \cdot \ > \ \mid$ 4. 아 좀 불편한데 5. syntactic sugar 1. $R^+$ (=$RR^*$) 2. $R?$ (=$\epsilon \mid R$) 3. $[0\text{-}9]$, $[a\text{-}z]$, $[A\text{-}Z]$ 4. 그래서 이게 RG랑 똑같은 거임? 1. 동치. 2. RG로 표현된 언어는 해당하는 RE가 있고, 3. RE에 대해서도 이걸 표현하는 RG가 있음 10. 다시 돌아와서, 이제 진짜 토큰을 RE로 정의해보자 11. 아 근데 이걸로 구분이 돼? 1. RE/RG는 언어를 정의만 하잖아. 2. 인식하는 인식기가 있어야지. 3. 지금까지 읽은 게 어떤 상태인지를 추적하면 좋을 것 같은데 12. Finite State Machine (FSM)을 가져오자 1. 이게 뭔데 1. '유한한' 상태를 갖는 상태-상태 사이의 전이를 나타내는 장치 2. 5-tuple $(\Sigma, S, s_0, \delta, F)$ 1. $\Sigma$: **Alphabet**들의 집합 2. $S$: **State**들의 **유한** 집합 3. $s_0$: 시작 상태 ($s_0 \in S$) 4. $\delta$: state의 transition 함수 ($\delta: S \times \Sigma \to S$) 1. "현재 상태 + 입력 문자 → 다음 상태" 5. $F$: 종료 상태들의 집합 (공집합 가능) 13. 그럼 RE를 FSM으로 표현해보면 되겠네 1. 각 RE case에 대해 해보자 1. epsilon 활용해 $a^*$ 같은 것도 표현 가능 2. 이거 알고리즘으로 나타내면 Thomson's Construction 1. RE가 귀납이니까, 얘도 귀납적으로 정의됨 2. 각 RE 케이스에 대해 1. 표현 가능하고 2. start state, final state 1개 가짐 3. 바꿨어 4. 얘는 어떤 성질을 가짐? 1. 각 state에 대해 최대 1. 2개의 epsilon transition과 2. 1개의 알파벳 transition이 존재 2. 시간/공간 복잡도 O(n) (n=RE크기) 5. 이런 걸 FSM 중에서도 뭐라고 해? 6. NFA (non-deterministic finite automata?) 1. 5-tuple $(\Sigma, S, s_0, \delta, F)$ 2. 한 Transition으로 여러 state에 갈 수 있어서, Non-Deterministic 14. 아 근데 NFA는 좀 비효율적이네 1. epsilon transition 있어서 한 시점에 여러 상태가 있을 수 있음 2. 어떻게 해결하지? 1. DFA 15. DFA 1. 그게 뭔데 1. 5-tuple $(\Sigma, S, s_0, \delta, F)$ 2. 한 transition으로 **하나의 state**에 감 (Deterministic) 2. 만드는 알고리즘이 뭔데? 1. 아이디어: epsilon closures 1. epsilon-transition을 없애야 하니, 2. epsilon으로 갈 수 있는 모든 state를 묶어서 새 state로 3. 그럼 initial/final state는? 1. NFA 기반으로 결정 2. Subset Construct 알고리즘이라고 하자 1. $\text{NFA } (\Sigma, S, s_0, \delta, F) \to \text{DFA } (\Sigma, Q, q_0, \Delta, F')$ 3. 이건 대충 어떻게 구성되는데? 1. epsilon closure 알고리즘이랑 2. worklist 패턴 (BFS가 대표적인 거) 4. 이거 종료 가능해? 1. DFA도 FSM이니까 state가 유한하지 2. subset이 최대 $2^n$개밖에 안 나와 3. 그럼 끝나지 5. 내가 직접 하려면? 1. DFA 초기 상태 q0 = ε-Closure({NFA의 s0}) 2. WorkList = {q0}, Q = {q0} 3. WorkList에서 상태 하나 pop 4. 각 알파벳 a에 대해: (표 그려두고 따라가기) 1. q' = ε-Closure( δ(현재DFA상태, a) ) 2. δ(현재DFA상태, a) = 현재 DFA상태에 속한 모든 NFA상태에서 a로 가는 상태들의 합집합 3. q'가 새 상태면 Q, WorkList에 추가 4. Transition Table에 기록 5. WorkList가 빌 때까지 반복 6. 근데 transition table은 어떻게 그려? 1. 상태 | 입력값들로 7. initial, final 어떻게 한다고? 1. NFA 기반으로 2. 표시도 잊지 마 16. DFA로 정리됐네 17. 그럼 DFA 구현은 어떻게 하고, 렉서가 실제로 DFA를 어떻게 쓰지? 18. 그럼 DFA 구현은 어떻게? 1. 2D Array로 2. 매 transition마다 $O(1)$ 시간 복잡도 3. 공간 복잡도: $O(|S| \cdot |\Sigma|)$ 19. 렉서가 실제로 DFA를 어떻게 써? 1. 토큰이 여러개잖아 우리는 RE 하나로 DFA 하나만 할 줄 아는데 2. 그냥 | 로 합쳐서 RE를 만들고, 그걸 DFA로 변환해 20. 그럼 이렇게 하면 끝? ㄴㄴ 남은 문제가 있음 1. DFA가 비효율적일 수도 있고 2. Table 기반이라 비효율적이고 3. Token 끝 판단 어떻게 하는지도 좀... 4. 키워드랑 identifier 구분도 5. 공백 문제 6. 변수명 가능 문제 21. 근데 저걸 다 알 필요는 없지 1. 기억해야 하는 것: **Lexer는 RE로 만들어진다** --- # 2-1. Parser - Language & Grammar, Parser Overview 1. 이제 Parser로 넘어왔는데 2. parser는 문법 문제를 해결하는 거니까 기본 개념 (문법) 부터 보자 1. 저번에 봤던 건 넘어가고 2. 문법 표현하기 위한 meta syntax BNF라는 게 있어 1. T: `"..."` 2. NT: `<...>` 3. P: `<...> ::= ...` 3. 복잡하니까 강의에서는 다르게 정의함 4. G를 만족하는 문장 s를 만든다면, s는 언어 L(G)의 문장이겠지? 5. 문장을 어떻게 만드는데? 3. 문장 만들기: derivation 1. S로부터 P를 따라서 문장 만드는 과정 1. 단계마다 NT 선택하고 우변의 T로 대체 2. T로만 이루어질 때까지 반복 4. 근데 언어 종류가 많아 RL도 봤던 것처럼 1. 촘스키 계층 1. RL: 렉서 2. CFL: 파서 3. 나머지는 컴파 관심사가 아님 5. 그래서 RL이 뭐였지? 1. Left-Regular, Right-Regular 중 하나의 G를 따르면 RG, 곧 RL이었지 2. Right-Regular는 곧 FSM과 동치 (각각 NT를 앞의 T를 입력 받은 하나의 상태로 생각) 3. 기억 장치가 필요한 건 RL로 못 다뤄 6. 기억 장치가 필요한 건 RL 말고 뭘로 써야하지? 7. CFG 1. RL이랑 똑같은데, Production 형태에 제약이 없음 8. 그럼 아까 유도 배웠으니까 해볼까? 9. ㅈㄴ 기네 10. 간단하게 Parse Tree 써보자 1. 그게 뭔데 1. 유도 과정을 트리로 표현했어 1. T - Leaf 2. NT - Internal Node 2. 유도 순서는 딱히 안 정해둠 2. 그럼 유도 순서에는 뭐가 있음? 1. Left-most: 가장 왼쪽 NT부터 P 적용 2. Right-most: 가장 오른쪽 NT부터 P 적용 3. 그럼 같은 문장에 대한 유도는 항상 같은 Parse Tree 만듦? 4. 아닌 경우: Ambiguous Grammar 1. 어케 개선함? 2. 케이스별로 다름 1. Left-Recursive로 할 수도 있고, 1. 왼쪽에 같은 Non-terminal 재귀 → 왼쪽 결합 표현 3. 이거 언어마다 다 다름 11. 여기까지 기본 개념 봤으니까 12. Parser가 뭐였는지 복습하자 1. 컴파일러 - 프론트엔드 - Lexer 다음 2. Token list -> AST 3. CFG 씀 4. 오토마타 기반 13. AST가 뭔데? 1. Syntax tree 안에 CST, AST 있음 2. CST가 사실 아까 본 Parse Tree임 1. **불필요한 정보 많고** 2. **특정 언어 종속적이야** 1. 같은 의미를 가진 코드라도 문법이 다르면 CST가 달라진다 2. 모듈화된 컴파일러 구조에서는 Middle-End, Back-End가 Front-End와 독립적이어야 하므로, 언어 종속적인 표현은 재사용성을 해친다 3. 그래서 AST 씀 1. 핵심적인 표현만 추상화했어 1. 괄호, Child 1개면 제거하고 2. 중첩 구조는 표현 2. 언어 독립적이고 3. 의미적 요소 포함 시작 (IR 변환 시에 써야 하니까) 14. 파서 내부 구조는 어떤데? 1. Token list -> \[ CST Generator -> CST -> AST Generator ] -> AST 15. CST Generator는 뭐 하는데? 1. Token List로부터 역으로 Derivation을 알아낸다 2. Parse Tree를 복원한다 16. CST가 Parse Tree를 어떻게 복원하는데? 1. Top-Down S에서 확장 2. Bottom-Up s에서 확장 --- # 2-2. Parser - Top-Down Parsing (RDP, LL(1)) 1. Top-Down이 뭐지 1. S부터 시작해서 P를 선택하며 확장 2. 만약에 잘못 선택하면? 3. Backtrack해야 함 2. Backtrack을 구체화해보자 3. RDP (Recursive Descent Parsing) 1. 알고리즘 1. focus가 NT면 1. 확장해야지 2. focus가 T면 1. 소비하고 다음 걸로 넘어가야지 1. idx + 1해서 다음 토큰으로 이동 2. focus = stack.pop() 3. 그냥 다 끝났다면? 1. return tree해야지 4. 셋 다 아니라면? 1. focus는 T인데 tokens\[idx]와 불일치한다면? 2. backtrack해야지 1. 에러 저장하고 2. 확장 이전 상태로 restore하고 1. focus, stack, idx 복원 3. focus의 자식 노드도 제거해주자 4. 만약 1. 이번에 시도할 P 다 시도했다면? 1. 루트인데 실패면? 1. 구문 오류 2. 아직 루트는 아니라면? 1. 한 단계 더 올라가자 2. 아니라면? 1. 그럼 P 더 봐야지 2. 특징은? 1. Left-most derivation 1. 스택 구조상, 항상 가장 왼쪽 NT 처리 2. 종료 못 하는 경우도 있음 1. left-recursive 할 경우 1. T 만나기 전에 계속 NT 3. 백트랙킹 비효율적이야 3. 문제1: Left-Recursion 1. 새로운 NT 도입해 Right-Recursive로 만들기 2. 일반적 알고리즘은 없음 4. 문제2: Backtracking의 비효율성 1. 이건 못하겠는데? 2. 다른 Top-Down Parsing 없나 4. LL(1) Parser 1. Backtracking의 비효율성 문제가 있었지 1. 어떤 P를 고를지 몰라서 그런 거임 2. 앞에 하나를 보면 되겠네 2. LL(k) Parsing 1. Left-to-right 2. Left-most Derivation 3. K개의 lookahead 3. LL(1)은 결국 1개의 lookahead로 백트랙킹 없이 P를 선택해야 하는군 4. 필요한 개념: FIRST (1차) 1. 정의: 가장 처음에 등장할 수 있는 terminal들의 집합 2. 어떻게 계산? 1. 아이디어: NT 확장 1. $A \to a$ 면 $\{a\} \subseteq \text{FIRST}(A)$ 2. $A \to B$ 면 $\text{FIRST}(B) \subseteq \text{FIRST}(A)$ 3. $A \to BC$, $B \to \epsilon \vert ...$ 면 $\text{FIRST}(C) \subseteq \text{FIRST}(A)$ 1. 근데 $B$가 $\epsilon$을 만들 수 **있을 때만** C의 FIRST가 A의 FIRST에 포함되네 2. 그럼 NULLABLE이 필요하겠군 5. 필요한 개념: NULLABLE 1. P만 봐서는 최종적으로 사라질지를 모르니까 2. 정의: $X$로부터 $\epsilon$을 유도할 수 있다면, NULLABLE($X$) = true 3. 어떻게 계산? 1. 아이디어: NT 확장 2. 알고리즘 1. NULLABLE 두개 만들어두고 루프 돌때마다 똑같은지 확인. 변화가 없었으면 더 없는 거니까 종료 2. 루프 안에서는 T,NT 중 하나인 alpha에 대해 1. 이미 nullable 판별했으면 스킵 2. T면 당연히 false 3. NT면 P 집합 내의 각 P 감마에 대해 1. 감마가 epsilon이면 바로 nuallable 2. 감마를 구성하는 모든 기호가 nullable이면 감마도 nullable 4. 종료됨? 1. false->true 2. T cup NT 는 유효 3. 종료하겠네 6. 필요한 개념: FIRST (완전 버전) 1. 정의: $A \to \alpha,\gamma,\beta$ 일 때, $\text{NULLABLE}(\alpha) = \text{true}$ 이면 $\text{FIRST}(\gamma) \subseteq \text{FIRST}(A)$ 1. 즉, $\alpha$가 NULLABLE이면 $\alpha$가 전부 사라지고 $\gamma$가 첫 자리에 올 수 있는데, 그렇다면 "$A$로부터 유도되는 문장의 맨 첫 terminal이 $\gamma$에서 나올 수 있으므로" 포함 2. 알고리즘 1. 마찬가지로 FIRST 복붙해서 변화체크함 2. 루프 안에서 T,NT 중 하나인 alpha에 대해 1. 터미널이면 FIRST는 자기 ㅈ자신 2. NT면 감마 돌면서 1. 첫번째면 fIRST에 포함 2. 그 외 앞감마가 not nullable인지 보고, 1. not nullable: 불가능 2. nullable: first ㄱㄴ 3. 종료됨? 1. 추가만 되고 삭제는 안 되고 2. T도 유한하니까 종료됨 7. 이제 된 거 아님? 8. RDP 수행할 때 γ의 **바로 오른쪽에 등장할 수 있는 terminal들의 집합** 필요 9. 필요한 개념: FOLLOW 1. γ의 **바로 오른쪽에 등장할 수 있는 terminal들의 집합**. 2. 규칙 1. $A \to \alpha \gamma \beta \delta$, $\text{NULLABLE}(\beta) = \text{true}$면 $\text{FIRST}(\delta) \subseteq \text{FOLLOW}(\gamma)$ 2. $A \to \alpha \gamma \beta \delta$, $\text{NULLABLE}(\beta\delta) = \text{true}$ 면 $\text{FOLLOW}(A) \subseteq \text{FOLLOW}(\gamma)$ 3. 알고리즘 4. 끝남? ㅇㅇ 10. 이거 다 활용한 거: P 고르는 select + select 테이블로 그린 Parsing Table 1. parsing table 1. **Row**: focus (non-terminal) 2. **Column**: lookahead (terminal) (\$ 포함) 3. **Entry**: 선택할 production 4. **빈 칸**: 오류(Error) 11. CONFLICT 한계 1. FIRST/FIRST 1. Left-Factoring으로 해결 1. 공통 prefix를 factoring하여 분리한다. 새 NT 2. FIRST/FOLLOW 1. substitution 1. A의 production을 A가 쓰인 자리에 **직접 대입**. 3. FOLLOW/FOLLOW 1. 고객님은 일단 모호하세요 --- # 2-3. Bottom-Up Parsing (LR(0), SLR(1), LR(1)) Bottom-Up Parsing이란? → Right-most Derivation의 역순 Shift-Reduce Parsing (마커, S/R, 스택) — Bottom-Up의 보다 구체적 구현 → Reduce는 언제 하지? | 연산 | 조건 | 동작 | | ---------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | **Shift** | 현재 위치에서 적용할 production이 없음 | 다음 token으로 이동<br>$\alpha A. \gamma \delta$에서 $\alpha A \gamma . \delta$로 진행. | | **Reduce** | $A \to \beta$에 대해 $S \Rightarrow^* \alpha A a\delta \to \alpha\beta a\delta$가 존재 | 현재 위치에서 올바르게 적용할 수 있는 P를 모두 적용.<br>production 우변을 좌변 NT로 치환<br>$\alpha \beta. \gamma \delta$에서 $\alpha A. \gamma \delta$로 진행. | Handle → 언제 Reduce해야 하는지 확정 Reduce가 필요한 타이밍 정의 - Marker가 production 우변의 **끝**에 위치할 때 Handle 정의 Handle은 항상 Stack Top에만 존재한다 Shift-Reduce 기반 Bottom-Up Parsing 알고리즘 → Handle은 어떻게 찾지? Bottom-Up Parsing (2) — LR(0) Parser Handle을 효율적으로 찾을 수 없다 → 관찰에 의한 Heuristic이 필요하다 Stack 관찰 — Stack에는 항상 Production들의 Prefix만 존재한다 → 뭐라고 부를까? Viable Prefix — 아직 파싱 오류가 없다는 게 보장된, stack에 올라올 수 있는 올바른 문자열 **정의:** $\alpha \in (T \cup NT)^*$, $s \in T^*$ 일 때, $S \to^* \alpha . s$ 이면 $\alpha$를 **viable prefix**라고 한다. $S \to^* \alpha . s$ 의 의미 - $S$에서 시작해서 유도를 거듭하다 보면 - 어떤 중간 단계에서 $\alpha s$ 라는 문자열이 나오고 - 그 중 $\alpha$는 이미 stack에 쌓인 부분, $s$는 아직 안 읽은 나머지 입력 즉 **"$S$로부터 올바르게 유도되는 과정 중에 실제로 등장할 수 있는 stack의 내용"** 이 viable prefix. Viable Prefix — 활용 아이디어: Viable prefix를 인식하면 handle을 찾을 수 있다 → 어떻게 인식할 것인가? Viable Prefix — 인식 아이디어: Viable Prefix는 RL이니, FSM으로 인식해보자 LR(0) Item **(정의)** Production에 marker `.`로 현재 상태를 표시한 것을 **item** (혹은 **LR(0) item**)이라 한다. Shift-Reduce Conflict Reduce/Reduce Conflict 해결을 위한 **LR(k) Parsing:** - **L**: Left-to-right으로 입력을 읽음 - **R**: Right-most derivation을 역방향으로 탐색 - **k**: Reduce 결정 시 미리 살펴보는 lookahead token의 수 SLR(1) Parser `A → γ` 를 기반으로 한 reduce 연산을 수행할 때, **FOLLOW(A)** 를 확인한다. - lookahead token `a` 가 `a ∈ FOLLOW(A)` 일 때만 reduce! 근데 작동 안할 때도 있음 (대입 문법) SLR(1) 실패의 근본 원인 — LR(0) item 기반 Follow Set 과근사 → 다른 item을 사용해야 한다 LR(1) Item — Lookahead를 Item에 내재화 LR(1) Item의 정의 — LR(0) item과 terminal token의 쌍 → LR(1) Item 기반 VP DFA를 만들면 LR(1) Parser → 그런데, LR(1) Item은 어떻게 계산할까? LR(1) Item 계산 — Lookahead 계산 아이디어: `(S' → . S , $)`로부터 시작해, ![[스크린샷 2026-04-29 오전 12.19.41.png]] 장점: 넓은 문법 단점: 상태수 ㅈㄴ많음 + Reduce/Reduce Conflict RL ⊂ LL(1) ⊂ LR(1) ⊂ CFG AG ⊂ CFG ``` ┌────────────────────────────────────────────────────────┐ │ Context-Free Grammar │ │ │ │ ┌──────────────────────────┐ ┌──────────────────┐ │ │ │ LR(1) Grammar │ │ Ambiguous │ │ │ │ ┌─────────────┐ │ │ Grammar │ │ │ │ │ LL(1) │ │ │ │ │ │ │ │ Grammar │ │ │ │ │ │ │ │ ┌─────────┐ │ │ │ │ │ │ │ │ │ Regular │ │ │ │ │ │ │ │ │ │ Grammar │ │ │ │ │ │ │ │ │ └─────────┘ │ │ │ │ │ │ │ └─────────────┘ │ │ │ │ │ └──────────────────────────┘ └──────────────────┘ │ └────────────────────────────────────────────────────────┘ ``` --- # 3. Semantics