Foundationপ্রথম নীতি থেকে
LEVEL 5লেসন ১/১৬কঠিন৪৫ মিনিট

Compiler Pipeline — text থেকে machine code: পুরো ম্যাপ

Compiler Pipeline Overview

gcc hello.c-এর ভেতরে preprocess → lex → parse → semantic → IR → optimize → codegen → assemble → link — পুরো pipeline-এর ম্যাপ, আর প্রতিটা stage-এর আউটপুট নিজ হাতে দেখা।

এই লেসন শেষে আপনি পারবেন

  • gcc একটা C ফাইলকে executable বানাতে কোন কোন subprocess (cpp/cc1/as/ld) চালায় -### output পড়ে ব্যাখ্যা করতে পারবেন
  • Pipeline-এর frontend, middle-end, backend ভাগটা আলাদা করে বলতে পারবেন, আর IR কেন M×N problem সমাধান করে তা উদাহরণসহ দেখাতে পারবেন
  • -save-temps দিয়ে .i, .s, .o intermediate ফাইলগুলো collect করে প্রতিটাতে ঠিক কোন stage-এর আউটপুট আছে চিনতে পারবেন
  • Phases আর passes-এর মধ্যে পার্থক্য করে বলতে পারবেন কেন optimizer pass-ভিত্তিক, phase-ভিত্তিক নয়
  • একটা .s assembly ফাইল খুলে অন্তত পাঁচটা instruction-কে C source-এর নির্দিষ্ট line-এর সাথে মিলিয়ে annotate করতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

আপনি হাজারবার এই কমান্ডটা চালিয়েছেন:

$ 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 একসাথে:

#StageInputOutputTool (GCC)
১Preprocess.c.i (macro-expanded source)cpp
২Lexical analysis.i characterstoken streamcc1
৩Parsingtokensparse tree → ASTcc1
৪Semantic analysisASTchecked/annotated ASTcc1
৫IR generationannotated ASTGIMPLE (GCC) / LLVM IRcc1
৬OptimizationIRbetter IRcc1 (-O flags)
৭Code generationIR.s assembly textcc1
৮Assemble.s.o relocatable objectas
৯Link.o গুলো + librariesexecutableld

লক্ষ্য করুন — 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 টা অংশ
বাম দিকে IR ছাড়া: M×N টা আলাদা compiler। ডান দিকে IR-সহ: M+N টা অংশ — প্রতিটা ভাষার একটা frontend, প্রতিটা CPU-র একটা backend।

৩টা ভাষা × ৪টা CPU = ১২টা full compiler — বৃদ্ধি গুণফলে। IR মাঝে বসিয়ে দিলে M+N টা অংশ — বৃদ্ধি যোগফলে। LLVM-এর business model এই হিসাবেই: Swift, Rust, Julia, Clang নিজেদের frontend লিখেছে, free-তে পেয়েছে x86, ARM, RISC-V, WASM — সব backend।

Phases বনাম passes

দুইটা শব্দ প্রায়ই গুলিয়ে ফেলা হয়, অথচ পার্থক্যটা মৌলিক:

PhasePass
কীPipeline-এর একটা stage, নির্দিষ্ট কাজIR-এর উপর একবার সম্পূর্ণ traversal
উদাহরণlexing, parsing, codegendead-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:

  1. cc1 — পুরো compile: .c → .s (preprocessor in-process)।
  2. as — assembler: .s → .o; mnemonic থেকে <Term id="opcode">opcode</Term> encoding (assembler-linker-loader লেসন recap)।
  3. ld — linker: .o + C library → executable।

প্রতিটা intermediate artifact-এর চেহারা

-save-temps দিলে যে ফাইলগুলো পড়ে থাকে, প্রতিটার identity:

ফাইলStageভেতরে কী থাকে
hello.ipreprocessআপনার C-ই, কিন্তু macro expand হয়ে, #include খুলে যায়ে হাজার লাইন header code
hello.scodegenassembly text — mnemonic, label, directive
hello.oassemble<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 = t2

Stage ৬ — 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 = eax

Stage ৮–৯: 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 এখন চালাতে পারবে)
প্রতিটা তীর চিহ্ন একটা representation change। ফাইল boundary (.i/.s/.o) শুধু তিনটা জায়গায় — বাকি সব transition একই process (cc1)-এর memory-তে ঘটে।
একটা কোড-fragment কোন অঞ্চলে কী হয়ে যায়
  1. FRONTENDtext → tokens → tree; ভুল ধরা (syntax); ভাষার সব knowledge
  2. MIDDLE-ENDIR → ভালো IR; pass-ভিত্তিক optimization; ভাষা-ও-CPU নিরপেক্ষ
  3. BACKENDIR → assembly; register allocation, instruction selection; CPU-র সব knowledge

ছবিটা মুখস্থ না — কেন ঠিক এই জায়গাগুলোতে boundary, সেটা বুঝুন: ফাইল boundaries যেখানে representation stable (text/byte stream); ভেতরের transitions সব memory-তে।

নিজে চালিয়ে দেখুন

EXPERIMENT

gcc-এর আসল subprocess chain দেখা, আর সব intermediate ফাইল আটকে রাখা

Linux / macOS (gcc বা clang)· ১৫ মিনিট

প্রথমে একটা মিনিমাল 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) সত্যিকারের ফাইল হিসেবে বের করা যায়।

নিজে বানান

BUILD IT

Pipeline Detective — .s ফাইল খুলে C source-এর সাথে instruction-by-instruction মেলানো

bash + annotation · ●●○○○
  1. একটা ছোট C ফাইল লিখুন: arithmetic, if, loop
  2. gcc -O0 -fno-asynchronous-unwind-tables -S দিয়ে .s বানান (noise কমাতে)
  3. .s ফাইল cat/hexdump করে কাঠামো চিনুন: directive, label, instruction
  4. অন্তত ৫টা instruction-এ C source line annotate করুন
  5. 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
        ret

Annotation worksheet — এই টেবিলটা নিজের .s থেকে fill করুন:

InstructionC lineকেন এই instruction দরকার
pushq %rbpfunction entryABI: caller-এর frame pointer save (stack-frames lesson recap)
addl %eax, %eaxbase * 2multiply-কে self-add করা হয়েছে — cheaper
cmpl $9, -4(%rbp)if (doubled \< 10)\< 10 মানে \<= 9 — comparison flip
jge .L2samebranch direction inverted: false-case-এ jump
movl -4(%rbp), %eaxreturn doubledreturn value %eax-এ (calling convention!)

যাচাই:

gcc -g -O0 calc.c -o calc && objdump -dS calc | less

objdump -dS debug info ব্যবহার করে assembly-র পাশে C line দেখাবে — hand annotation-এর সাথে মিলিয়ে নিন; পার্থক্য থাকলে সেটাই সবচেয়ে interesting শিক্ষা।

নিজে বাড়ান:

  1. একই ফাইল -O2 দিয়ে compile করে দেখুন loop unroll/branch inversion কোন annotation গুলো অচল করে দেয়
  2. 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-এর “মূল ভাষা”।

বুঝেছেন কি না দেখুন

1

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 ভিন্ন।

2

M×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 কি?

প্রয়োগ

লাইনগুলো এসেছে #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 বানাতে পারে।

4

Phase আর pass-এর মধ্যে পার্থক্য কী? “Constant folding একটা phase” — এই বাক্যটা কোথায় ভুল?

যুক্তি

Phase হলো pipeline-এর নির্দিষ্ট functional stage (lex, parse…); pass হলো IR-এর উপর একবার traversal করা transformation। Constant folding একটা pass — fixed position নেই; অন্য pass-গুলো নতুন constant তৈরি করলে আবার চলে। Phase-এর মতো fixed slot-এর মালিক না হওয়াই এর ভুল।

5

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-এর মূল পেপার — পরের লেসনগুলোতে বারবার ফিরবে