Compiler Pipeline — text থেকে machine code: পুরো ম্যাপ
Compiler Pipeline Overview
gcc hello.c-এর ভেতরে preprocess → lex → parse → semantic → IR → optimize → codegen → assemble → link — পুরো pipeline-এর ম্যাপ, আর প্রতিটা stage-এর আউটপুট নিজ হাতে দেখা।
আগে এটা বুঝি
আপনি হাজারবার এই কমান্ডটা চালিয়েছেন:
$ gcc hello.c && ./a.out
Hello, world!দুই সেকেন্ডে text ফাইল runnable binary হয়ে গেল। কিন্তু ওই দুই সেকেন্ডের ভেতরে ঠিক কী ঘটল? বেশিরভাগ course এখানেই থামে — “compiler compile করে” — আর আমরা মুখ থুঁটে থাকি।
Assembly module-এ আপনি উল্টো দিক থেকে এসেছিলেন: compiler-এর
output পড়েছেন (reading-compiler-output), x86-64 syntax,
<Term id="elf">ELF</Term> ফরম্যাট, linker/loader — সব। এখন
সময় উল্টো রাস্তায়: source থেকে binary, পুরো রাস্তাটা। আর শুধু
দেখা না — বানানো: এই module জুড়ে আমরা ধাপে ধাপে একটা toy
language-এর compiler বানাব:
lexer → parser → AST → semantic analysis → type check → IR → optimization → codegen → linking
আজকের লেসন সেই যাত্রার ম্যাপ।
আজকের প্রশ্ন দুইটা: (১) text থেকে machine code-এ যাওয়ার পথে ঠিক কোন কোন representation-এ আপনার কোড রূপ বদলায়? (২) এই পথটা কেন এতগুলো মাঝের stage-এ ভাগ করা?
মূল ধারণা
Pipeline-এর পুরো ম্যাপ
hello.c থেকে a.out — মাঝের সব stage একসাথে:
| # | Stage | Input | Output | Tool (GCC) |
|---|---|---|---|---|
| ১ | Preprocess | .c | .i (macro-expanded source) | cpp |
| ২ | Lexical analysis | .i characters | token stream | cc1 |
| ৩ | Parsing | tokens | parse tree → AST | cc1 |
| ৪ | Semantic analysis | AST | checked/annotated AST | cc1 |
| ৫ | IR generation | annotated AST | GIMPLE (GCC) / LLVM IR | cc1 |
| ৬ | Optimization | IR | better IR | cc1 (-O flags) |
| ৭ | Code generation | IR | .s assembly text | cc1 |
| ৮ | Assemble | .s | .o relocatable object | as |
| ৯ | Link | .o গুলো + libraries | executable | ld |
লক্ষ্য করুন — stage ২ থেকে ৭ পর্যন্ত সবকিছুর ভেতরে একটাই program:
cc1, GCC-র “compiler proper”। gcc নিজে কোনো compiler না;
ওটা একটা driver — conductor-এর মতো ঠিক সময়ে ঠিক instrument
ডেকে চালায়। (Clang-এ driver clang, compiler proper -cc1 mode;
macOS-এ gcc টাইপ করলে আসলে clang-ই চলে।) এটাই প্রথম বড় উপলব্ধি:
“the compiler” বলে একটা monolithic জিনিস নেই, আছে tools-এর chain।
Frontend / Middle-end / Backend
Stage ৯টাকে তিনটা বড় অঞ্চলে ভাগ করা হয়, আর এই ভাগটা পুরো module-এর navigation system:
┌─────────────── FRONTEND ───────────────┐
hello.c │ preprocess → lex → parse → semantics │ ভাষা-নির্ভর
└──────────────────┬─────────────────────┘ (C জানে)
▼
IR ─── মাঝের ভাষা, কোনো CPU-র না
▼
┌─────────────── MIDDLE-END ─────────────┐
│ optimization passes (-O1..-O3) │ ভাষা+CPU দুটোই
└──────────────────┬─────────────────────┘ নিরপেক্ষ
▼
IR
▼
┌─────────────── BACKEND ────────────────┐
│ instruction selection → register │ CPU-নির্ভর
│ allocation → scheduling → emit .s │ (x86-64 জানে)
└────────────────────────────────────────┘- Frontend জানে ভাষাটা কেমন — C-র
typedef, Rust-র borrow, Python-র indentation; text-কে যাচাই করে structure দেয়। - Backend জানে CPU-টা — register, instruction,
<Term id="cisc">CISC</Term>নাকি<Term id="risc">RISC</Term>। - Middle-end কোনোটাই জানে না — IR-এর উপর গাণিতিক transformation।
IR কেন দরকার — M×N problem
ধরুন IR নেই। তাহলে প্রতিটা ভাষার জন্য প্রতিটা CPU-র জন্য আলাদা backend লিখতে হবে:
IR ছাড়া (M×N) IR-সহ (M+N)
C ────▶ x86-64 C ──▶ ┌───┐ ┌───┐ ▶ x86-64
Rust ──▶ ARM64 Rust ─▶│ F │ ──▶ │ B │ ▶ ARM64
Go ────▶ x86-64 Go ──▶│ r │ IR │ o │ ▶ RISC-V
... ... │ o │ │ n │ ▶ WASM ...
└───┘ └───┘
3 ভাষা × 4 CPU = 12 টা 3 frontend + 4 backend = 7 টা অংশ৩টা ভাষা × ৪টা CPU = ১২টা full compiler — বৃদ্ধি গুণফলে। IR মাঝে বসিয়ে দিলে M+N টা অংশ — বৃদ্ধি যোগফলে। LLVM-এর business model এই হিসাবেই: Swift, Rust, Julia, Clang নিজেদের frontend লিখেছে, free-তে পেয়েছে x86, ARM, RISC-V, WASM — সব backend।
Phases বনাম passes
দুইটা শব্দ প্রায়ই গুলিয়ে ফেলা হয়, অথচ পার্থক্যটা মৌলিক:
| Phase | Pass | |
|---|---|---|
| কী | Pipeline-এর একটা stage, নির্দিষ্ট কাজ | IR-এর উপর একবার সম্পূর্ণ traversal |
| উদাহরণ | lexing, parsing, codegen | dead-code elimination, constant folding |
| কার মধ্যে | পুরো pipeline-এ | মূলত middle-end-এ |
| ক্রম | মোটামুটি নির্দিষ্ট | reorder করা যায়, বারবার চলতে পারে |
-O2 flag-টা আসলে কী করে? GCC-তে -O2 মানে প্রায় ৬০টার মতো
pass on হওয়া; -O0-এ প্রায় সব off। Pass-গুলো একটা worklist-এর
মতো চলে — এক pass-এর transformation আরেক pass-এর সুযোগ তৈরি করে,
তাই একই pass কখনো দুবারও চলে। <Term id="compiler-optimization-level">optimization level</Term>
আসলে pass selection-এর একটা preset, কোনো magic dial না।
ভেতরে কী ঘটছে
Driver-এর ভেতরে ঢুকে দেখা
gcc চালালে ঠিক কোন process গুলো spawn হয়, সেটা gcc নিজেই
বলতে পারে — -### flag দিলে ও কমান্ড execute না করে শুধু
print করে দেয়:
$ gcc -### hello.c
"/usr/lib/gcc/x86_64-linux-gnu/13/cc1" "-quiet" "hello.c"
"-dumpbase" "hello.c" "-o" "/tmp/ccAbCdEf.s"
"as" "--64" "-o" "/tmp/ccAbCdEf.o" "/tmp/ccAbCdEf.s"
"/usr/bin/ld" "-pie" "-o" "a.out" ...তিনটা লাইন — তিনটা child process:
cc1— পুরো compile:.c→.s(preprocessor in-process)।as— assembler:.s→.o; mnemonic থেকে<Term id="opcode">opcode</Term>encoding (assembler-linker-loader লেসন recap)।ld— linker:.o+ C library → executable।
প্রতিটা intermediate artifact-এর চেহারা
-save-temps দিলে যে ফাইলগুলো পড়ে থাকে, প্রতিটার identity:
| ফাইল | Stage | ভেতরে কী থাকে |
|---|---|---|
hello.i | preprocess | আপনার C-ই, কিন্তু macro expand হয়ে, #include খুলে যায়ে হাজার লাইন header code |
hello.s | codegen | assembly text — mnemonic, label, directive |
hello.o | assemble | <Term id="elf">ELF</Term> relocatable object — machine code, কিন্তু address গুলো এখনো “patch pending” |
.i খুললে সবচেয়ে চমকে দেওয়া বিষয়: ৭ লাইনের hello.c expand
হয়ে ৮০০+ লাইন — stdio.h-এর পুরো content টেক্সট-হিসেবে paste
হয়েছে। Preprocessor কোনো compiler না, একটা text substitution
engine — C-র macro এজন্যই বিপজ্জনক: type, scope, কিছুই জানে না।
.o-র “relocatable” মানে: কোন function-এর final address কেউ
জানে না। printf call instruction-টাতে লেখা আছে শুধু relocation
entry — বসানোর কাজ ld-এর। Assembly module-এর elf-format
লেসনে এই relocation table-গুলো hex dump-এ দেখেছেন।
উদাহরণ
একটা statement-এর পুরো জীবন
ছোট্ট একটা C statement নিয়ে পুরো pipeline ধরে trace করি:
int net = price * qty - discount;Stage ১ — Preprocess: এই line-এ কোনো macro নেই, তাই .i-তে
অক্ষত থাকে।
Stage ২ — Lex: character stream → tokens:
kw:int id:"net" op:= id:"price" op:* id:"qty"
op:- id:"discount" op:;Stage ৩–৪ — Parse + semantics: grammar rule ধরে tree দাঁড়ায়, আর symbol table থেকে প্রতিটা name-এর declaration মিলে যায়:
(= int)
/ \
"net" (-)
/ \
(*) "discount"
/ \
"price" "qty"Stage ৫ — IR generation: tree-টা linear three-address code হয়ে যায় (পরের লেসনগুলোতে এই form-টাই আমরা নিজেরাও বানাব):
t1 = price * qty
t2 = t1 - discount
net = t2Stage ৬ — Optimization: -O0-এ কিছুই হয় না। -O2-এ দেখা
যেত net পরে কোথাও ব্যবহৃত না হলে তিনটা instruction-ই
মুছে যেত — dead store elimination।
Stage ৭ — Codegen: x86-64 assembly (-O0, System V ABI,
stack-এ price=-8, qty=-12, discount=-16, net=-20 ধরে):
mov eax, DWORD PTR [rbp-8] ; eax = price
imul eax, eax, DWORD PTR [rbp-12] ; eax = price * qty
mov edx, DWORD PTR [rbp-16] ; edx = discount
sub eax, edx ; eax = t1 - discount
mov DWORD PTR [rbp-20], eax ; net = eaxStage ৮–৯: as mnemonic-গুলোকে byte encoding-এ নেয়
(mov eax, [rbp-8] → bytes 8B 45 F8), ld final address
বসিয়ে executable বানায়।
একটা statement, পাঁচটা representation: text → tokens → tree → linear IR → encoded bytes। বাকি module-এ প্রতিটা নিজের হাতে।
ছবিতে
পুরো pipeline এক ছবিতে — data কোথায় কোথায় বদলায়
hello.c
│ cpp (text → text)
▼
hello.i ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ফাইল boundary
│ cc1: lex (chars → tokens)
▼
token stream
│ cc1: parse (tokens → tree)
▼
AST
│ cc1: semantic analysis (annotations)
▼
annotated AST
│ cc1: lower to IR
▼
GIMPLE / LLVM IR ◀── optimization passes এখানে ঘোরে (-O1..O3)
│ cc1: codegen (IR → asm text)
▼
hello.s ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ফাইল boundary
│ as (mnemonic → bytes + relocations)
▼
hello.o ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ফাইল boundary
│ ld (address resolution + patching)
▼
a.out (executable ELF — loader এখন চালাতে পারবে)- FRONTENDtext → tokens → tree; ভুল ধরা (syntax); ভাষার সব knowledge
- MIDDLE-ENDIR → ভালো IR; pass-ভিত্তিক optimization; ভাষা-ও-CPU নিরপেক্ষ
- BACKENDIR → assembly; register allocation, instruction selection; CPU-র সব knowledge
ছবিটা মুখস্থ না — কেন ঠিক এই জায়গাগুলোতে boundary, সেটা বুঝুন: ফাইল boundaries যেখানে representation stable (text/byte stream); ভেতরের transitions সব memory-তে।
নিজে চালিয়ে দেখুন
gcc-এর আসল subprocess chain দেখা, আর সব intermediate ফাইল আটকে রাখা
প্রথমে একটা মিনিমাল hello.c বানান:
#include \<stdio.h>
int main(void) {
printf("Hello, pipeline!\n");
return 0;
}ধাপ ১ — driver কী কী চালায় দেখুন:
gcc -### hello.c 2>&1 | tail -5 # macOS-এ: clang -### ...Output-এ অন্তত তিনটা quoted command path — cc1 (macOS-এ
clang), as, ld।
ধাপ ২ — সব intermediate ফাইল সংগ্রহ করুন:
gcc -save-temps -O0 hello.c -o hello
wc -l hello.c hello.i hello.s # ~6 vs কয়েকশো vs কয়েক dozen
ls hello.i hello.s hello.oধাপ ৩ — প্রতিটা representation চিনুন:
head -20 hello.i # C-ই, কিন্তু typedef/prototype ভরা
grep -n "call" hello.s # printf call-site
file hello.o # "relocatable" objectধাপ ৪ — optimization পার্থক্য diff করুন:
gcc -save-temps -O0 hello.c -o hello && mv hello.s s_O0.s
gcc -save-temps -O2 hello.c -o hello && mv hello.s s_O2.s
diff s_O0.s s_O2.s | head -30-O2 version-এ লাইন কমে যাবে, printf হয়তো puts হয়ে গেছে
(নিউলাইন-শেষ string-এ GCC-র নিজস্ব optimization), আর frame pointer
setup হয়তো গায়েব।
"gcc" আসলে driver — কমপক্ত তিনটা আলাদা tool (cc1/cpp, as, ld) চালায়; pipeline-এর প্রতিটা stage-এর output (.i/.s/.o) সত্যিকারের ফাইল হিসেবে বের করা যায়।
নিজে বানান
Pipeline Detective — .s ফাইল খুলে C source-এর সাথে instruction-by-instruction মেলানো
- একটা ছোট C ফাইল লিখুন: arithmetic, if, loop
- gcc -O0 -fno-asynchronous-unwind-tables -S দিয়ে .s বানান (noise কমাতে)
- .s ফাইল cat/hexdump করে কাঠামো চিনুন: directive, label, instruction
- অন্তত ৫টা instruction-এ C source line annotate করুন
- gcc -g + objdump -dS দিয়ে annotation যাচাই করুন
লক্ষ্য: reading-compiler-output skill এবার নিজের হাতে — assembly-কে C-তে ফেরানো mapping চিনতে।
/* calc.c */
int compute(int base) {
int doubled = base * 2;
if (doubled \< 10) {
doubled = doubled + 100;
}
for (int i = 0; i \< 3; i++) {
doubled = doubled - i;
}
return doubled;
}gcc -O0 -S -fno-asynchronous-unwind-tables calc.c -o calc.s
cat calc.s.s ফাইলের general anatomy (আপনার output সামান্য আলাদা হতে পারে):
.text ← directive: code section
.globl compute ← directive: symbol export
compute: ← label: function শুরু
pushq %rbp ← ① prologue: frame pointer save
movq %rsp, %rbp ← ② prologue: নতুন frame ভিত্তি
movl %edi, -20(%rbp) ← ③ parameter `base` stack-এ
movl -20(%rbp), %eax ← ④ `base * 2`-এর জন্য load
addl %eax, %eax ← ⑤ `* 2` → `+ self` (strength reduction!)
movl %eax, -4(%rbp) ← `doubled = ...` store
cmpl $9, -4(%rbp) ← `if (doubled \< 10)` তুলনা
jge .L2 ← false হলে skip (direction inverted)
...
.L2: ← loop back-edge target
...
movl -4(%rbp), %eax ← `return doubled` load
popq %rbp ← ⑥ epilogue
retAnnotation worksheet — এই টেবিলটা নিজের .s থেকে fill করুন:
| Instruction | C line | কেন এই instruction দরকার |
|---|---|---|
pushq %rbp | function entry | ABI: caller-এর frame pointer save (stack-frames lesson recap) |
addl %eax, %eax | base * 2 | multiply-কে self-add করা হয়েছে — cheaper |
cmpl $9, -4(%rbp) | if (doubled \< 10) | \< 10 মানে \<= 9 — comparison flip |
jge .L2 | same | branch direction inverted: false-case-এ jump |
movl -4(%rbp), %eax | return doubled | return value %eax-এ (calling convention!) |
যাচাই:
gcc -g -O0 calc.c -o calc && objdump -dS calc | lessobjdump -dS debug info ব্যবহার করে assembly-র পাশে C line
দেখাবে — hand annotation-এর সাথে মিলিয়ে নিন; পার্থক্য থাকলে
সেটাই সবচেয়ে interesting শিক্ষা।
নিজে বাড়ান:
- একই ফাইল
-O2দিয়ে compile করে দেখুন loop unroll/branch inversion কোন annotation গুলো অচল করে দেয় compute-কে আরও parameter দিয়ে দেখুন কোন args register-এ আসে কোনটা stack-এ (System V: rdi, rsi, rdx, rcx…)
বাস্তব সিস্টেমে
Pipeline-টা বাস্তব জগতে কোথায় কোথায়
GCC ও Clang। আজকের ম্যাপের দুইটা production implementation: GCC-র middle-end GIMPLE (SSA-ভিত্তিক) তারপর RTL; Clang-এর LLVM IR।
rustc। নিজস্ব multi-level pipeline: AST → HIR → THIR → MIR (SSA-ish) → LLVM IR। “এক compiler-এ একাধিক IR” — প্রতিটা level নির্দিষ্ট analysis-এর জন্য tuned।
ccache ও distcc। Build cache tools pipeline-কেই exploit করে:
ccache preprocessed .i-কে hash key বানায় — header change হলে
miss, comment change হলে hit।
Language servers (rust-analyzer, clangd)। IDE-র autocomplete pipeline-এর frontend half reuse করে — parse + semantic analysis চালিয়ে AST/symbol table রাখে, backend কোনোদিন চালায় না।
WASM toolchain। Emscripten: C/C++ → LLVM IR → WASM bytecode — backend-টা বদলে “CPU” একটা virtual machine; M×N হিসাবে N-এর নতুন সদস্য, নতুন hardware ছাড়াই।
Babel/TypeScript। JS ecosystem-এও একই ছক: parse → AST → transform (plugins) → print। Babel plugin API আসলে middle-end pass API-রই JS version।
TinyCC (tcc)। উল্টো দিকের প্রমাণ: single-pass, কোনো আলাদা middle-end ছাড়াই C compile করে — কম speed-এর code কিন্তু compile হয় অবিশ্বাস্য দ্রুত। Pipeline design trade-off, law না।
যে ভুলগুলো সবাই করে
“"gcc" একটা compiler program।”
ঠিক অর্থে না — gcc একটা driver: subprocess হিসেবে cc1,
as, ld চালায়। -### experiment-এ আপনি নিজেই দেখেছেন তিনটা
আলাদা executable। এর ব্যবহারিক গুরুত্ব: gcc -v output পড়তে
পারলে build failure debug করা সহজ হয় — কোন tool fail করল
(linker? assembler?) সেটা আলাদা করে চিনতে হয়, আর error-এর
ধরন অনুযায়ী ভিন্ন জায়গায় সমস্যা খুঁটতে হয়।
“.s ফাইল মানেই প্রায়-final machine code, linking তো ব্যাপারই না।”
.s থেকে .o mechanical (encoding), কিন্তু .o তখনও executable
না — প্রতিটা cross-function reference unresolved। printf-এর
address কোথায়? উত্তর শুধু linker-এর কাছে। Assembly module-এর
assembler-linker-loader লেসনে দেখা relocation symbol-গুলো
(R_X86_64_PLT32) এখানেই ফিরে আসে।
“-O2 মানে "compiler ভালো করে optimize করল", আর কিছু না।”
-O2 একটা pass list — কোন pass, কোন order, কতবার preset
ঠিক করে। এজন্যই -O2 আর -Os ভিন্ন result; আর কখনো -O2
code -O0-এর চেয়ে ধীরও হয় (inlining-এ i-cache pressure)।
Optimization dial না — menu।
“Preprocessor কম্পাইলারের অংশ, তাই macro safe।”
Preprocessor pure textual substitution করে, tokens-এর আগেই।
#define SQUARE(x) ((x)*(x))-এ paren কেন? Expansion parse-এর
আগে হয় — operator precedence-ও macro-র control-এর বাইরে।
Frontend-এর সব বুদ্ধি preprocessor-এর জগতের পরে শুরু।
“IR শুধু academic concept — বাস্তব compiler সরাসরি AST থেকে assembly বানায়।”
1980-এর দশকের পর থেকে প্রায় প্রতিটা serious compiler IR ব্যবহার করে: GCC GIMPLE/RTL, LLVM IR, rustc MIR, HotSpot HIR/LIR, V8-র Sea of Nodes। IR ছাড়া M×N explosion ছাড়াও optimizer-এর playground নেই — IR-ই compiler-এর “মূল ভাষা”।
বুঝেছেন কি না দেখুন
1gcc hello.c চালালে কোন কোন subprocess কোন order-এ চলে, আর
প্রতিটার input/output ফাইল format কী?
স্মরণ
gcc hello.c চালালে কোন কোন subprocess কোন order-এ চলে, আর
প্রতিটার input/output ফাইল format কী?তিনটা subprocess: cc1 (.c → .s: preprocess+lex+parse+semantic+IR+opt+codegen),
as (.s → .o: relocatable ELF), ld (.o + libc → a.out)।
macOS-এ clang driver same job, internal names ভিন্ন।
2M×N problem-এ M=5 ভাষা, N=6 CPU হলে IR-সহ আর IR-ছাড়া ক’টা component
লাগবে? নতুন একটা CPU support করলে কতটা নতুন কাজ হয় প্রতিটা case-এ?
যুক্তি
IR ছাড়া: 5×6 = 30 full compiler; নতুন CPU = ৫টা নতুন full
compiler। IR-সহ: 5+6 = 11 অংশ; নতুন CPU = একটাই নতুন backend
— পাঁচ ভাষাই free support। এটাই LLVM-এর economics।
3-save-temps দিয়ে বানানো hello.i ফাইল ৯০০ লাইন, অথচ hello.c
ছিল ৬ লাইন। কোথায় এত লাইন এলো, আর এই ফাইলটা এখনো valid C কি?
প্রয়োগ
-save-temps দিয়ে বানানো hello.i ফাইল ৯০০ লাইন, অথচ hello.c
ছিল ৬ লাইন। কোথায় এত লাইন এলো, আর এই ফাইলটা এখনো valid C কি?লাইনগুলো এসেছে #include \<stdio.h> expansion থেকে — header-এর
prototype, struct definition, macro সব text-হিসেবে paste হয়েছে।
আর হ্যাঁ, .i fully valid C — আপনি চাইলে gcc hello.i directly
compile করতে পারেন, একই result। এজন্যই ccache .i-কে hash key
বানাতে পারে।
4Phase আর pass-এর মধ্যে পার্থক্য কী? “Constant folding একটা phase” —
এই বাক্যটা কোথায় ভুল?
যুক্তি
Phase হলো pipeline-এর নির্দিষ্ট functional stage (lex, parse…); pass হলো IR-এর উপর একবার traversal করা transformation। Constant folding একটা pass — fixed position নেই; অন্য pass-গুলো নতুন constant তৈরি করলে আবার চলে। Phase-এর মতো fixed slot-এর মালিক না হওয়াই এর ভুল।
5hello.o-তে printf call instruction-টা আছে কিন্তু printf-এর
code নেই। Linker ঠিক কী information ব্যবহার করে gap-টা পূরণ করে?
স্মরণ
hello.o-তে printf call instruction-টা আছে কিন্তু printf-এর
code নেই। Linker ঠিক কী information ব্যবহার করে gap-টা পূরণ করে?Assembler .o-তে একটা relocation entry রাখে: “offset X-এ
symbol printf-এর address বসাতে হবে, এই relocation type-এ”
(যেমন R_X86_64_PLT32)। Linker libc-র dynamic symbol table-এ
printf খুঁজে, PLT/GOT machinery setup করে (position-independent-code
লেসন দেখুন), তারপর offset X-এ ঠিক bytes লিখে দেয়। Symbol +
relocation table — এই দুটোই linker-এর ভাষা।
এরপর কী
এরপর কী
ম্যাপ হাতে আছে। পরের লেসন থেকে pipeline-এর প্রথম stage নিজের হাতে: Lexical Analysis —
- character stream → token stream —
Token(kind, value, line, col) - regex → NFA → DFA chain, maximal munch, keyword-vs-identifier
- C-র typedef ambiguity কেন lexer-parser handshake তৈরি করে
প্রথম build: toy language-এর পুরো Lexer class — positions সহ,
tests সহ।
আরও পড়ুন
- Compilers: Principles, Techniques, and Tools (2nd ed.), Chapters 1–2 — Aho, Sethi, Ullman, Lam (the Dragon Book) · Compiler-এর structure আর a simple syntax-directed translator-এর ধ্রুপদি আলোচনা — এই module-এর roadmap-এর ভিত্তি
- Engineering a Compiler (2nd ed.), Chapter 1 — Overview of Compilation — Keith Cooper, Linda Torczon · Frontend/middle/backend ভাগ আর IR-এর ভূমিকার সবচেয়ে ইঞ্জিনিয়ার-বান্ধব ব্যাখ্যা
- "LLVM: A Compilation Framework for Lifelong Program Analysis & Transformation" — Chris Lattner, Vikram Adve (CGO 2004) · IR-কে কেন্দ্র করে দাঁড় করানো একটা real ecosystem-এর মূল পেপার — পরের লেসনগুলোতে বারবার ফিরবে