diff --git a/tutorials/c-compiler/Codegen.hs b/tutorials/c-compiler/Codegen.hs index 7b1ff19..f255de1 100644 --- a/tutorials/c-compiler/Codegen.hs +++ b/tutorials/c-compiler/Codegen.hs @@ -1,25 +1,27 @@ {-# LANGUAGE QuasiQuotes #-} --- Stages 1-4 code generation: C AST -> assembly AST. Both sides of the +-- Stages 1-5 code generation: C AST -> assembly AST. Both sides of the -- translation are RTK-generated: the input is destructured with CQQ -- quasi-quotation patterns, the output is built with AsmQQ construction -- quotes and $-antiquote splices. -- --- Short-circuit && and || need fresh, unique jump labels, so generation runs --- in a State Int that hands out label numbers (see `fresh`). The functions --- that can emit labels are monadic (genProgram/genStatement/genExp/genBinary/ --- genAnd/genOr); the pure helpers (apply*/genUnaryOp/compareSet and the leaf --- builders) stay outside the monad. +-- Generation runs in a monad carrying two things: a Reader of the +-- variable->offset map that the Resolve pass computed (so a variable reference +-- knows its stack slot), and a State Int handing out unique labels for +-- short-circuit jumps. The functions that need either are monadic; the pure +-- helpers (apply*/genUnaryOp/compareSet and the leaf builders) stay outside. -- --- Token payloads (the integer literal, the function name) cannot be bound or --- spliced by an antiquote ($x works on whole syntax sorts only), so leaf --- nodes go through the grammar's named constructors: matching IntLit/Name to --- read a payload, mkImm/mkSym/jmpTo/... to build an assembly leaf (positioned --- with rtkNoPos; AST equality ignores positions by design). +-- Token payloads (the integer literal, the variable/function name) cannot be +-- bound or spliced by an antiquote, so leaf nodes go through the named +-- constructors: matching IntLit/Name to read a payload, mkImm/mkMem/mkSym/... +-- to build an assembly leaf (positioned with rtkNoPos; AST equality ignores +-- positions by design). module Codegen (codegen) where import Prelude hiding (exp) -- the Exp quoter is named exp, like Prelude.exp +import Control.Monad.Reader (ReaderT, runReaderT, asks) import Control.Monad.State (State, evalState, state) +import qualified Data.Map as M import CParser hiding (rtkNoPos) -- both parsers export rtkNoPos; we need AsmParser's import CQQ @@ -27,24 +29,28 @@ import CQQ import AsmParser import AsmQQ --- Label supply: a counter threaded through generation. -type Gen = State Int +import Resolve (VarMap) + +-- Reader: the per-function variable->offset map. State: the label counter. +type Gen = ReaderT VarMap (State Int) fresh :: Gen Int fresh = state (\n -> (n, n + 1)) -codegen :: Program -> Asm -codegen prog = evalState (genProgram prog) 0 +-- the stack slot an identifier resolves to (Resolve guaranteed it is present) +offsetOf :: Ident -> Gen Operand +offsetOf ident = asks (mkMem . (M.! identName ident)) + +codegen :: VarMap -> Program -> Asm +codegen vm prog = evalState (runReaderT (genProgram prog) vm) 0 genProgram :: Program -> Gen Asm genProgram [program| int $name ( ) { $stmts } |] = do + frame <- asks frameSize body <- concat <$> mapM genStatement stmts let sym = mkSym (identName name) -- C99 5.1.2.2.3: falling off the end of main returns 0 - items = body ++ [asmItems| - movl $0, %eax - ret - |] + items = prologue frame ++ body ++ [asmItems| movl $0, %eax |] ++ epilogue return [asm| .globl $sym $sym : @@ -52,16 +58,45 @@ genProgram [program| int $name ( ) { $stmts } |] = do |] genProgram other = error $ "codegen: unsupported program: " ++ show other +-- a 16-byte-aligned frame big enough for every local (4 bytes each) +frameSize :: VarMap -> Int +frameSize vm = ((4 * M.size vm + 15) `div` 16) * 16 + +-- set up the frame: save the caller's base pointer, point %rbp at this frame, +-- and carve out room for the locals (skipped when there are none) +prologue :: Int -> [AsmItem] +prologue n = + [asmItems| push %rbp + movq %rsp, %rbp |] + ++ [Subq rtkNoPos (mkImm n) rspOp | n > 0] + +-- tear the frame down; every return path ends here +epilogue :: [AsmItem] +epilogue = [asmItems| movq %rbp, %rsp + pop %rbp + ret |] + genStatement :: Statement -> Gen [AsmItem] genStatement [statement| return $e ; |] = do e' <- genExp e - return (e' ++ [asmItems| ret |]) + return (e' ++ epilogue) +genStatement [statement| int $name = $e ; |] = do -- declaration with initializer + e' <- genExp e + dst <- offsetOf name + return (e' ++ [asmItems| movl %eax, $dst |]) +genStatement [statement| int $name ; |] = return [] -- declaration, uninitialized: no code +genStatement [statement| $e ; |] = genExp e -- expression statement: evaluate, drop %eax genStatement other = error $ "codegen: unsupported statement: " ++ show other --- Evaluate an expression, leaving its value in %eax. One QQ pattern per --- precedence level picks the matching node out of the single Exp type; && and --- || short-circuit, so they are handled apart from the value operators. +-- Evaluate an expression, leaving its value in %eax. genExp :: Exp -> Gen [AsmItem] +genExp [exp| $name = $e |] = do -- assignment is an expression: store, keep value in %eax + e' <- genExp e + dst <- offsetOf name + return (e' ++ [asmItems| movl %eax, $dst |]) +genExp [exp| $name |] = do -- variable reference: load from the stack slot + src <- offsetOf name + return [asmItems| movl $src, %eax |] genExp [exp| $e1 || $e2 |] = genOr e1 e2 genExp [exp| $e1 && $e2 |] = genAnd e1 e2 genExp [exp| $e1 $eqop $e2 |] = genBinary e1 e2 (applyEqOp eqop) @@ -75,16 +110,14 @@ genExp other = error $ "codegen: unsupported expression: " ++ show other -- A binary operator over two computed values (arithmetic or comparison). -- Evaluate the right operand and push it, the left into %eax, pop the right --- into %ecx, then apply with the left in %eax. (Right-first leaves the left in --- %eax, where subl and idivl need it.) +-- into %ecx, then apply with the left in %eax. genBinary :: Exp -> Exp -> [AsmItem] -> Gen [AsmItem] genBinary e1 e2 apply = do r <- genExp e2 l <- genExp e1 return (r ++ [asmItems| push %rax |] ++ l ++ [asmItems| pop %rcx |] ++ apply) --- a && b: if a is 0 the result is 0 and b is never evaluated; otherwise the --- result is (b != 0). +-- a && b: if a is 0 the result is 0 and b is never evaluated; otherwise (b != 0) genAnd :: Exp -> Exp -> Gen [AsmItem] genAnd e1 e2 = do n <- fresh @@ -94,15 +127,14 @@ genAnd e1 e2 = do r <- genExp e2 return $ l ++ [asmItems| cmpl $0, %eax |] - ++ [jneTo rhs, jmpTo end, label rhs] -- a != 0 -> evaluate b; else fall to end with %eax = 0 + ++ [jneTo rhs, jmpTo end, label rhs] ++ r ++ [asmItems| cmpl $0, %eax movl $0, %eax setne %al |] ++ [label end] --- a || b: if a is nonzero the result is 1 and b is never evaluated; otherwise --- the result is (b != 0). +-- a || b: if a is nonzero the result is 1 and b is never evaluated; otherwise (b != 0) genOr :: Exp -> Exp -> Gen [AsmItem] genOr e1 e2 = do n <- fresh @@ -112,8 +144,8 @@ genOr e1 e2 = do r <- genExp e2 return $ l ++ [asmItems| cmpl $0, %eax |] - ++ [jeTo rhs] -- a == 0 -> evaluate b - ++ [asmItems| movl $1, %eax |] -- a != 0 -> result 1 + ++ [jeTo rhs] + ++ [asmItems| movl $1, %eax |] ++ [jmpTo end, label rhs] ++ r ++ [asmItems| cmpl $0, %eax @@ -148,20 +180,16 @@ applyAddOp other = error $ "codegen: unsupported additive operator: " ++ show ot applyMulOp :: MulOp -> [AsmItem] applyMulOp (Times _) = [asmItems| imull %ecx, %eax |] --- cdq sign-extends %eax into %edx:%eax; idivl divides that by %ecx, leaving the --- quotient in %eax applyMulOp (Divide _) = [asmItems| cdq idivl %ecx |] applyMulOp other = error $ "codegen: unsupported multiplicative operator: " ++ show other --- Apply a unary operator to the value already in %eax. Payload-free leaves, so --- matched by named constructor rather than quasi-quote. +-- Apply a unary operator to the value already in %eax. genUnaryOp :: UnaryOp -> [AsmItem] genUnaryOp (Neg _) = [asmItems| negl %eax |] genUnaryOp (Complement _) = [asmItems| notl %eax |] --- logical not: set %eax to 1 if the value was 0, else 0 genUnaryOp (Not _) = [asmItems| cmpl $0, %eax movl $0, %eax @@ -174,10 +202,15 @@ genUnaryOp other = error $ "codegen: unsupported unary operator: " ++ show other mkImm :: Int -> Operand mkImm = Imm rtkNoPos +mkMem :: Int -> Operand -- a local at off(%rbp) +mkMem off = Mem rtkNoPos off (Rbp rtkNoPos) + +rspOp :: Operand +rspOp = RegOp rtkNoPos (Rsp rtkNoPos) + mkSym :: String -> AsmId mkSym = Sym rtkNoPos --- jumps and label definitions: one AsmId field each, so built by constructor jmpTo, jeTo, jneTo, label :: AsmId -> AsmItem jmpTo = Jmp rtkNoPos jeTo = Je rtkNoPos diff --git a/tutorials/c-compiler/Emit.hs b/tutorials/c-compiler/Emit.hs index 24aa9e1..6d4604f 100644 --- a/tutorials/c-compiler/Emit.hs +++ b/tutorials/c-compiler/Emit.hs @@ -38,6 +38,8 @@ emitItem [asmItem| pop $dst |] = " pop " ++ emitOperand dst emitItem [asmItem| je $sym |] = " je " ++ symName sym emitItem [asmItem| jne $sym |] = " jne " ++ symName sym emitItem [asmItem| jmp $sym |] = " jmp " ++ symName sym +emitItem [asmItem| movq $src, $dst |] = " movq " ++ binOperands src dst +emitItem [asmItem| subq $src, $dst |] = " subq " ++ binOperands src dst emitItem [asmItem| ret |] = " ret" emitItem other = error $ "emitItem: unsupported item: " ++ show other @@ -47,16 +49,18 @@ binOperands src dst = emitOperand src ++ ", " ++ emitOperand dst emitOperand :: Operand -> String emitOperand (Imm _ n) = "$" ++ show n emitOperand (RegOp _ r) = emitReg r +emitOperand (Mem _ off r) = show off ++ "(" ++ emitReg r ++ ")" -- disp(base), e.g. -4(%rbp) emitOperand other = error $ "emitOperand: unsupported operand: " ++ show other --- The register set grew from one to five, so render it by named constructor --- rather than a quasi-quote per register. +-- Registers render by named constructor (there are several now). emitReg :: Reg -> String emitReg (Eax _) = "%eax" emitReg (Al _) = "%al" emitReg (Ecx _) = "%ecx" emitReg (Rax _) = "%rax" emitReg (Rcx _) = "%rcx" +emitReg (Rbp _) = "%rbp" +emitReg (Rsp _) = "%rsp" emitReg other = error $ "emitReg: unsupported register: " ++ show other symName :: AsmId -> String diff --git a/tutorials/c-compiler/Main.hs b/tutorials/c-compiler/Main.hs index 1c8619d..23d2918 100644 --- a/tutorials/c-compiler/Main.hs +++ b/tutorials/c-compiler/Main.hs @@ -1,5 +1,5 @@ --- ncc: a tiny C compiler (stage 1 of Nora Sandler's "Writing a C Compiler"), --- with the whole front end generated by RTK from c.pg. +-- ncc: a tiny C compiler (Nora Sandler's "Writing a C Compiler"), with the +-- whole front end generated by RTK from c.pg. -- -- Usage: ncc -- Produces an executable next to the source file (test-suite contract of @@ -16,7 +16,8 @@ import System.IO (hPutStrLn, stderr) import System.Process (rawSystem) import CLexer (scanTokens) -import CParser (Program, parseC) +import CParser (parseC) +import Resolve (resolve) import Codegen (codegen) import Emit (emit) @@ -32,23 +33,26 @@ main = do compileFile :: FilePath -> IO () compileFile path = do src <- readFile path - -- Lexical and syntax errors arrive as Left, before any output file is - -- created: invalid programs exit non-zero and leave no artifacts behind. - ast <- case scanTokens src >>= parseC of - Left err -> do - hPutStrLn stderr $ path ++ ": " ++ err - exitFailure - Right ast -> return (ast :: Program) + -- Lexical, syntax, and semantic errors all arrive as Left, before any output + -- file is created: invalid programs exit non-zero and leave no artifacts. + -- resolve runs between parse and codegen; its var->offset map feeds codegen. + asm <- case scanTokens src >>= parseC of + Left err -> reject err + Right ast -> case resolve ast of + Left err -> reject err + Right varmap -> return (codegen varmap ast) let asmPath = replaceExtension path "s" exePath = dropExtension path -- emit renders the program's instruction AST; the GNU-stack note is -- file-level ELF metadata, identical for every program and not modelled by -- the assembly grammar, so the driver appends it here next to the gcc call -- rather than threading it through codegen or the round-trippable emitter. - writeFile asmPath (emit (codegen ast) ++ gnuStackNote) + writeFile asmPath (emit asm ++ gnuStackNote) rc <- rawSystem "gcc" [asmPath, "-o", exePath] removeFile asmPath when (rc /= ExitSuccess) exitFailure + where + reject err = do hPutStrLn stderr (path ++ ": " ++ err); exitFailure -- Marks the stack non-executable. Without this section ld warns "missing -- .note.GNU-stack section implies executable stack" on every link. Linux/ELF diff --git a/tutorials/c-compiler/Makefile b/tutorials/c-compiler/Makefile index ab1f8ce..4f26037 100644 --- a/tutorials/c-compiler/Makefile +++ b/tutorials/c-compiler/Makefile @@ -36,10 +36,10 @@ $(GEN)/%.hs: $(GEN)/%.x $(GEN)/%.hs: $(GEN)/%.y cd $(ROOT) && cabal exec happy -- $(HERE)/$< --ghc -i$(HERE)/$(GEN)/$*.info -o $(HERE)/$@ -ncc: Main.hs Codegen.hs Emit.hs $(GENERATED) +ncc: Main.hs Resolve.hs Codegen.hs Emit.hs $(GENERATED) cd $(ROOT) && cabal exec -- ghc --make $(HERE)/Main.hs -i$(HERE) -i$(HERE)/$(GEN) -outputdir $(HERE)/build/ncc -o $(HERE)/ncc -test-qq: TestQQ.hs Codegen.hs Emit.hs $(GENERATED) +test-qq: TestQQ.hs Resolve.hs Codegen.hs Emit.hs $(GENERATED) cd $(ROOT) && cabal exec -- ghc --make $(HERE)/TestQQ.hs -i$(HERE) -i$(HERE)/$(GEN) -outputdir $(HERE)/build/test-qq -o $(HERE)/test-qq test: build diff --git a/tutorials/c-compiler/README.md b/tutorials/c-compiler/README.md index d9f8fa3..1810511 100644 --- a/tutorials/c-compiler/README.md +++ b/tutorials/c-compiler/README.md @@ -9,24 +9,26 @@ the C front end (lexer, parser, AST types, quasi-quoters) is generated from quasi-quotation splices instead of concatenating strings, and a generated assembly *parser* (a by-product) round-trip-tests the emitter. -**Status: stage 4**: integer `return`, the unary operators `-` `~` `!`, the -binary operators `+ - * /` (precedence cascade, parentheses), and the -relational/logical operators `== != < <= > >= && ||` with short-circuiting. -C source → assembly AST → AT&T text → executable via gcc. Verified against the +**Status: stage 5**: integer `return`, the unary `-` `~` `!`, binary `+ - * /` +(precedence cascade, parentheses), relational/logical `== != < <= > >= && ||` +with short-circuiting, and local variables (declarations, assignment, +references) with a stack frame and a name-resolution semantic pass. C source → +resolve → assembly AST → AT&T text → executable via gcc. Verified against the official [test suite](https://github.com/nlsandler/write_a_c_compiler) (stage -1: 12/12, stage 2: 11/11, stage 3: 16/16, stage 4: 24/24) in addition to the -local tests under [`tests/`](tests/). +1: 12/12, 2: 11/11, 3: 16/16, 4: 27/27, 5: 17/17) in addition to the local +tests under [`tests/`](tests/). ## Companion tutorial [`tutorial/`](tutorial/) retells Nora Sandler's series page by page with RTK — what the generator replaces (lexer, parser, AST, the boilerplate that walks it) and what you write instead (grammar rules, quasi-quotation patterns, -splices). Start at the [index](tutorial/README.md): stages 1–4 are covered by +splices). Start at the [index](tutorial/README.md): stages 1–5 are covered by [00 — Setup](tutorial/00-setup.md), [01 — Integers](tutorial/01-integers.md), [02 — Unary operators](tutorial/02-unary.md), -[03 — Binary operators](tutorial/03-binary.md), and -[04 — Relational and logical](tutorial/04-relational.md). +[03 — Binary operators](tutorial/03-binary.md), +[04 — Relational and logical](tutorial/04-relational.md), and +[05 — Local variables](tutorial/05-variables.md). This README is the reference companion to those pages: it catalogues the conventions and limitations below, which the pages link to as you hit them. @@ -37,6 +39,7 @@ conventions and limitations below, which the pages link to as you hit them. | `c.pg` | The C grammar (input language). | | `asm.pg` | The assembly grammar (output language). Everything under `gen/` is generated from these two. | | `Main.hs` | Compiler driver: `ncc file.c` produces an executable next to the source (the tutorial's test-suite contract), assembling/linking through gcc. | +| `Resolve.hs` | Semantic pass: resolves variable names to stack slots and rejects undeclared/redeclared variables. QQ for matching, SYB (`listify`) for the whole-tree query. | | `Codegen.hs` | C AST → assembly AST; QQ patterns on the C side, QQ construction + splices on the assembly side. | | `Emit.hs` | Assembly AST → AT&T text (RTK generates parsers, not pretty-printers; this is the hand-written half, kept honest by the round-trip test). | | `TestQQ.hs` | End-to-end tests of the full QQ feature set for both grammars, plus the emit/parse round trip. | @@ -161,11 +164,11 @@ long time; the causes are avoidable, and `c.pg` is written to avoid them: ## Roadmap -Following the blog series, one stage at a time. Stages 1–4 (integers, unary +Following the blog series, one stage at a time. Stages 1–5 (integers, unary operators, binary operators with the precedence cascade, relational/logical -operators with short-circuiting) are done; up next: +operators with short-circuiting, local variables with a name-resolution +semantic pass) are done; up next: -5. local variables (first semantic pass: variable resolution) 6. `if`/`else` and the conditional expression 7. compound statements and scoping 8. loops, `break`/`continue` diff --git a/tutorials/c-compiler/Resolve.hs b/tutorials/c-compiler/Resolve.hs new file mode 100644 index 0000000..ddfd4e9 --- /dev/null +++ b/tutorials/c-compiler/Resolve.hs @@ -0,0 +1,63 @@ +{-# LANGUAGE QuasiQuotes #-} + +-- The compiler's first semantic pass. It walks the function body in order, +-- assigning each declared local a stack-frame offset and rejecting programs +-- the grammar cannot: a variable used before (or without) a declaration, or +-- declared twice. (Assignment to a non-lvalue is already a syntax error, +-- because the grammar only allows a bare identifier on the left of `=`.) +-- +-- Collecting the variables a statement *uses* is a whole-subtree query, so it +-- is one SYB call -- `listify` over the derived Data instances -- rather than a +-- hand-written recursion over the entire Exp cascade. This is the division of +-- labour the plan calls for: quasi-quoters for targeted construction and +-- matching, generic programming for "find every X anywhere in this tree". +module Resolve (resolve, VarMap) where + +import qualified Data.Map as M +import Data.Data (Data) +import Data.Generics (listify) + +import CParser +import CQQ + +-- Each local maps to its byte offset from %rbp (negative: below the frame base). +type VarMap = M.Map String Int + +resolve :: Program -> Either String VarMap +resolve [program| int $name ( ) { $stmts } |] = resolveStmts stmts +resolve other = Left $ "resolve: unsupported program: " ++ show other + +resolveStmts :: [Statement] -> Either String VarMap +resolveStmts = go M.empty + where + go env [] = Right env + go env (s : rest) = case s of + DeclInit _ ident e -> declare env rest (identName ident) (checkUses env e) + Declare _ ident -> declare env rest (identName ident) (Right ()) + _ -> checkUses env s >> go env rest + + -- add a fresh local after validating its initializer against the vars in + -- scope *before* it (so `int a = a;` and a redeclaration are rejected) + declare env rest v initOk = + if v `M.member` env + then Left $ "duplicate declaration of variable '" ++ v ++ "'" + else initOk >> go (M.insert v (-4 * (M.size env + 1)) env) rest + + -- every variable referenced by `node` must already be in scope + checkUses env node = + case filter (`M.notMember` env) (referenced node) of + [] -> Right () + (v : _) -> Left $ "undeclared variable '" ++ v ++ "'" + +-- every identifier appearing anywhere in `node` (a use, or an assignment +-- target -- both must be declared) +referenced :: Data a => a -> [String] +referenced = map nameOf . listify isName + where isName (Name _ _) = True + isName _ = False + nameOf (Name _ s) = s + nameOf _ = "" + +identName :: Ident -> String +identName (Name _ s) = s +identName other = error $ "resolve: unexpected identifier node: " ++ show other diff --git a/tutorials/c-compiler/TestQQ.hs b/tutorials/c-compiler/TestQQ.hs index 0a98ca0..75cfaf0 100644 --- a/tutorials/c-compiler/TestQQ.hs +++ b/tutorials/c-compiler/TestQQ.hs @@ -34,6 +34,7 @@ import AsmParser hiding (rtkNoPos) -- both parsers export rtkNoPos (distinct typ import qualified AsmParser as A import AsmQQ +import Resolve (resolve) import Codegen (codegen) import Emit (emit) @@ -128,6 +129,23 @@ main = do case [exp| 5 <= 9 |] of [exp| $e1 $relop $e2 |] -> e1 == [exp| 5 |] && relop == [relOp| <= |] _ -> False + , check "variable reference is a Factor: [exp| foo |]" $ + [exp| foo |] == VarRef rtkNoPos (Name rtkNoPos "foo") + , check "assignment is the lowest-precedence Exp: [exp| x = 1 + 2 |]" $ + [exp| x = 1 + 2 |] + == Assign rtkNoPos (Name rtkNoPos "x") + (Add rtkNoPos (IntLit rtkNoPos 1) (Plus rtkNoPos) (IntLit rtkNoPos 2)) + , check "assignment is right-associative: [exp| a = b = c |]" $ + [exp| a = b = c |] + == Assign rtkNoPos (Name rtkNoPos "a") + (Assign rtkNoPos (Name rtkNoPos "b") (VarRef rtkNoPos (Name rtkNoPos "c"))) + , check "declaration statement: [statement| int x = 5 ; |]" $ + [statement| int x = 5 ; |] + == DeclInit rtkNoPos (Name rtkNoPos "x") (IntLit rtkNoPos 5) + , check "assignment pattern binds the target name: [exp| $name = $e |]" $ + case [exp| count = 0 |] of + [exp| $name = $e |] -> name == [ident| count |] && e == [exp| 0 |] + _ -> False ] putStrLn "-- assembly grammar --" @@ -156,20 +174,18 @@ main = do $sym : $items |] == parseAsmText " .globl main\nmain:\n movl $2, %eax\n ret\n" + , check "memory operand: [operand| -4(%rbp) |]" $ + [operand| -4(%rbp) |] == Mem A.rtkNoPos (-4) (Rbp A.rtkNoPos) , check "round trip: parse (emit asm) == asm" $ let prog = [asm| .globl main main : movl $42, %eax ret |] in parseAsmText (emit prog) == prog - , check "full pipeline: parse C -> codegen -> emit -> parse Asm" $ - parseAsmText (emit (codegen (parse "int main() { return 2; }"))) - == [asm| .globl main - main : - movl $2, %eax - ret - movl $0, %eax - ret |] + , check "full pipeline: parse C -> resolve -> codegen -> emit -> parse Asm" $ + let p = parse "int main() { int a = 2; return a; }" + a = codegen (either error id (resolve p)) p + in parseAsmText (emit a) == a ] unless (and (cResults ++ asmResults)) exitFailure diff --git a/tutorials/c-compiler/asm.pg b/tutorials/c-compiler/asm.pg index 6653a3c..24edf0e 100644 --- a/tutorials/c-compiler/asm.pg +++ b/tutorials/c-compiler/asm.pg @@ -1,6 +1,6 @@ grammar 'Asm'; -# x86-64 assembly (AT&T syntax), the subset emitted for stages 1-4. +# x86-64 assembly (AT&T syntax), the subset emitted for stages 1-5. # Grows stage by stage alongside c.pg. # # Design notes: @@ -48,20 +48,28 @@ AsmItem = Globl: '.globl' AsmId | Je: 'je' AsmId | Jne: 'jne' AsmId | Jmp: 'jmp' AsmId + | Movq: 'movq' Operand ',' Operand + | Subq: 'subq' Operand ',' Operand | Ret: 'ret' ; +# A Mem operand is a stack slot: signed displacement off a base register, in the +# AT&T disp(base) form -- a local at -4(%rbp), or (later) an argument at 8(%rbp). @shortcuts(src, dst) Operand = Imm: '$' num - | RegOp: Reg ; + | RegOp: Reg + | Mem: num '(' Reg ')' ; # %eax/%al are the 32- and 8-bit accumulators; %ecx the 32-bit second operand; -# push/pop work only at 64-bit width, so the same registers appear as %rax/%rcx -# there. The emitter just prints whichever name the AST carries. +# push/pop work only at 64-bit width, so those registers also appear as +# %rax/%rcx; %rbp/%rsp are the 64-bit frame and stack pointers. The emitter +# just prints whichever name the AST carries. Reg = Eax: '%eax' | Al: '%al' | Ecx: '%ecx' | Rax: '%rax' - | Rcx: '%rcx' ; + | Rcx: '%rcx' + | Rbp: '%rbp' + | Rsp: '%rsp' ; @shortcuts(sym) AsmId = Sym: asmid ; @@ -71,5 +79,7 @@ AsmId = Sym: asmid ; # ---------------------------------------------------------------------------- asmid = [a-zA-Z_][a-zA-Z_0-9]* ; -Int: num = [0-9]+ ; +# num is signed: stack displacements can be negative (-4(%rbp)). It reads as an +# Int, so the '-' is part of the number, not a separate operator token. +Int: num = '-'? [0-9]+ ; Ignore: ws = [ \t\n\r]+ ; diff --git a/tutorials/c-compiler/c.pg b/tutorials/c-compiler/c.pg index be3cf73..893bdf3 100644 --- a/tutorials/c-compiler/c.pg +++ b/tutorials/c-compiler/c.pg @@ -1,19 +1,21 @@ grammar 'C'; -# Stages 1-4 of Nora Sandler's "Writing a C Compiler" +# Stages 1-5 of Nora Sandler's "Writing a C Compiler" # (https://norasandler.com/2017/11/29/Write-a-Compiler.html): # # ::= -# ::= "int" "(" ")" "{" "}" -# ::= "return" ";" -# ::= "||" | # stage 4: logical -# ::= "&&" | # and relational -# ::= | -# ::= | -# ::= | # stage 3: binary -# ::= | -# ::= | # stage 2: unary -# ::= | "(" ")" +# ::= "int" "(" ")" "{" { } "}" +# ::= "return" ";" | ";" # stage 5: locals, +# | "int" [ "=" ] ";" # assignment +# ::= "=" | # stage 5: assign +# ::= "||" | # stage 4: logical +# ::= "&&" | # and relational +# ::= | +# ::= | +# ::= | # stage 3: binary +# ::= | +# ::= | # stage 2: unary +# ::= | | "(" ")" # stage 5: ref # ::= "+"|"-" ::= "*"|"/" ::= "=="|"!=" # ::= "<"|"<="|">"|">=" ::= "-"|"~"|"!" # @@ -39,23 +41,32 @@ Function = Func: 'int' Ident '(' ')' '{' StatementList '}' ; @shortcuts(stmts) StatementList = Statement* ; -Statement = Return: 'return' Exp ';' ; - -# The expression grammar is a precedence cascade: logical-or (lowest) over -# logical-and over equality over relational over additive over multiplicative -# over unary over Factor (atoms). Every level shares the type Exp via the -# `Exp:` annotation, and each higher-precedence level passes through with a -# ,-lift -- so there are no per-level wrapper constructors, just one Exp type -# with one constructor per operator. That is what lets a single QQ pattern -# match any expression node. Left recursion makes every operator -# left-associative; parentheses are a ,-lift in Factor, so they group without -# adding a node. +Statement = Return: 'return' Exp ';' + | ExpStmt: Exp ';' + | DeclInit: 'int' Ident '=' Exp ';' + | Declare: 'int' Ident ';' ; + +# The expression grammar is a precedence cascade: assignment (lowest) over +# logical-or over logical-and over equality over relational over additive over +# multiplicative over unary over Factor (atoms). Every level shares the type +# Exp via the `Exp:` annotation, and each higher-precedence level passes through +# with a ,-lift -- so there are no per-level wrapper constructors, just one Exp +# type with one constructor per operator. That is what lets a single QQ pattern +# match any expression node. Left recursion makes the operators +# left-associative; assignment is right-recursive (a = b = c is a = (b = c)); +# parentheses are a ,-lift in Factor, so they group without adding a node. # -# && and || get their own constructors (Or/And), not an operator sort: they -# are control flow (short-circuit), not operators over two computed values. +# Assignment's LHS is a bare Ident, not a general Exp: that keeps it LALR-clean +# (an Ident is an assignment target only when '=' follows, else it reduces to a +# Factor) and makes `a + 3 = 4` a *syntax* error, as the blog wants. && and || +# get their own constructors (Or/And), not an operator sort: they are control +# flow (short-circuit), not operators over two computed values. @shortcuts(e) -Exp = Or: Exp '||' LAndExp - | ,LAndExp ; +Exp = Assign: Ident '=' Exp + | ,LOrExp ; + +Exp: LOrExp = Or: LOrExp '||' LAndExp + | ,LAndExp ; Exp: LAndExp = And: LAndExp '&&' EqExp | ,EqExp ; @@ -76,6 +87,7 @@ Exp: UnaryExp = Unary: UnaryOp UnaryExp | ,Factor ; Exp: Factor = IntLit: intLit + | VarRef: Ident | '(' ,Exp ')' ; # '-' is shared between AddOp (binary) and UnaryOp (prefix); the cascade decides diff --git a/tutorials/c-compiler/tests/invalid/bad_lvalue.c b/tutorials/c-compiler/tests/invalid/bad_lvalue.c new file mode 100644 index 0000000..f751f0c --- /dev/null +++ b/tutorials/c-compiler/tests/invalid/bad_lvalue.c @@ -0,0 +1,5 @@ +int main() { + int a = 2; + a + 1 = 3; + return a; +} diff --git a/tutorials/c-compiler/tests/invalid/redeclare.c b/tutorials/c-compiler/tests/invalid/redeclare.c new file mode 100644 index 0000000..16aecd8 --- /dev/null +++ b/tutorials/c-compiler/tests/invalid/redeclare.c @@ -0,0 +1,5 @@ +int main() { + int a = 1; + int a = 2; + return a; +} diff --git a/tutorials/c-compiler/tests/invalid/undeclared.c b/tutorials/c-compiler/tests/invalid/undeclared.c new file mode 100644 index 0000000..24d6f39 --- /dev/null +++ b/tutorials/c-compiler/tests/invalid/undeclared.c @@ -0,0 +1,3 @@ +int main() { + return a; +} diff --git a/tutorials/c-compiler/tests/invalid/use_before_decl.c b/tutorials/c-compiler/tests/invalid/use_before_decl.c new file mode 100644 index 0000000..51df794 --- /dev/null +++ b/tutorials/c-compiler/tests/invalid/use_before_decl.c @@ -0,0 +1,5 @@ +int main() { + a = 3; + int a; + return a; +} diff --git a/tutorials/c-compiler/tests/valid/assign.c b/tutorials/c-compiler/tests/valid/assign.c new file mode 100644 index 0000000..d62c75e --- /dev/null +++ b/tutorials/c-compiler/tests/valid/assign.c @@ -0,0 +1,5 @@ +int main() { + int a; + a = 7; + return a; +} diff --git a/tutorials/c-compiler/tests/valid/assign_value.c b/tutorials/c-compiler/tests/valid/assign_value.c new file mode 100644 index 0000000..8b080c6 --- /dev/null +++ b/tutorials/c-compiler/tests/valid/assign_value.c @@ -0,0 +1,5 @@ +int main() { + int a; + int b = a = 4; + return a - b; +} diff --git a/tutorials/c-compiler/tests/valid/missing_return_with_var.c b/tutorials/c-compiler/tests/valid/missing_return_with_var.c new file mode 100644 index 0000000..07802da --- /dev/null +++ b/tutorials/c-compiler/tests/valid/missing_return_with_var.c @@ -0,0 +1,3 @@ +int main() { + int a; +} diff --git a/tutorials/c-compiler/tests/valid/multiple_vars.c b/tutorials/c-compiler/tests/valid/multiple_vars.c new file mode 100644 index 0000000..ad5cbbe --- /dev/null +++ b/tutorials/c-compiler/tests/valid/multiple_vars.c @@ -0,0 +1,5 @@ +int main() { + int a = 1; + int b = 2; + return a + b; +} diff --git a/tutorials/c-compiler/tests/valid/update.c b/tutorials/c-compiler/tests/valid/update.c new file mode 100644 index 0000000..547ede7 --- /dev/null +++ b/tutorials/c-compiler/tests/valid/update.c @@ -0,0 +1,5 @@ +int main() { + int a = 3; + a = a + 1; + return a; +} diff --git a/tutorials/c-compiler/tests/valid/var_init.c b/tutorials/c-compiler/tests/valid/var_init.c new file mode 100644 index 0000000..6bf3c71 --- /dev/null +++ b/tutorials/c-compiler/tests/valid/var_init.c @@ -0,0 +1,4 @@ +int main() { + int a = 5; + return a; +} diff --git a/tutorials/c-compiler/tutorial/05-variables.md b/tutorials/c-compiler/tutorial/05-variables.md new file mode 100644 index 0000000..b43567f --- /dev/null +++ b/tutorials/c-compiler/tutorial/05-variables.md @@ -0,0 +1,251 @@ +# 05 — Local variables + +← [04 — Relational and logical](04-relational.md) · [Tutorial index](README.md) + +Companion to **[Writing a C Compiler, Part 5](https://norasandler.com/2018/01/25/Write-a-Compiler-5.html)**. + +Stage 5 adds local variables: declarations (`int a = 2;`), assignment +(`a = a + 3;`), and references (`return a;`). Two things are genuinely new. +The compiler grows a **stack frame** — locals live in memory, addressed off a +frame pointer — and, for the first time, it gains a **semantic pass**: a check +that runs after parsing and rejects programs the *grammar* happily accepts but +that are nonetheless wrong (an undeclared variable, a variable declared twice). + +## 1. Assignment, declarations, and references + +> **Blog ⇄ RTK.** The blog extends its parser with assignment expressions, +> declaration statements, and variable references. Here it is a few more rules, +> with one careful choice about where assignment sits. + +Assignment is the *lowest*-precedence operator in C and is right-associative, +so it goes on top of the cascade, recursing right; declarations and bare +expressions become statements; and a variable reference is a new kind of +`Factor` ([`c.pg`](../c.pg)): + +``` +Statement = Return: 'return' Exp ';' + | ExpStmt: Exp ';' + | DeclInit: 'int' Ident '=' Exp ';' + | Declare: 'int' Ident ';' ; + +@shortcuts(e) +Exp = Assign: Ident '=' Exp | ,LOrExp ; -- right-recursive: a = b = c is a = (b = c) +... +Exp: Factor = IntLit: intLit | VarRef: Ident | '(' ,Exp ')' ; +``` + +The one subtle choice is that assignment's left side is a bare `Ident`, not a +general `Exp`. That does two jobs at once: + +- **It keeps the grammar LALR(1)-clean.** An identifier at the start of an + expression is ambiguous — is `a` the target of `a = …`, or a variable + reference? The parser resolves it with one token of lookahead: it shifts the + `Ident`, and if `=` follows it is an assignment, otherwise the `Ident` + reduces to a `Factor`. Because `=` never follows a `Factor` anywhere else in + the cascade, there is no conflict — `--analyze-conflicts` and happy both stay + quiet. (This is why `=` and `==` must be distinct tokens.) +- **It makes `a + 3 = 4` a *syntax* error, for free.** A non-identifier left + side simply will not parse, which is exactly what the blog wants — and what + the official suite's `syntax_err_bad_lvalue` expects. + +So the `Exp` type gains `Assign RtkPos Ident Exp` and `VarRef RtkPos Ident`, +and `Statement` gains the two declaration forms. + +## 2. The first semantic pass + +> **Blog ⇄ RTK.** The blog walks the AST to build a variable map and reject +> bad programs. So does this — but the "find every variable used here" query is +> one generic call, not a hand-written recursion over ten expression +> constructors. + +The grammar accepts `return a;` with no `a`, and `int a; int a;`. Catching +those is not the parser's job; it is a *pass* over the parsed tree. +[`Resolve.hs`](../Resolve.hs) walks the statement list in order, assigns each +declared local a stack offset, and rejects use-before-declaration and +redeclaration: + +```haskell +resolveStmts = go M.empty + where + go env [] = Right env + go env (s : rest) = case s of + DeclInit _ ident e -> declare env rest (identName ident) (checkUses env e) + Declare _ ident -> declare env rest (identName ident) (Right ()) + _ -> checkUses env s >> go env rest + + declare env rest v initOk = + if v `M.member` env + then Left ("duplicate declaration of variable '" ++ v ++ "'") + else initOk >> go (M.insert v (-4 * (M.size env + 1)) env) rest + + checkUses env node = + case filter (`M.notMember` env) (referenced node) of + [] -> Right () + (v : _) -> Left ("undeclared variable '" ++ v ++ "'") +``` + +`checkUses` needs the set of variables a statement (or an initializer) +references, *anywhere* inside it. Writing that as a recursion over `Or`/`And`/ +`Add`/`Mul`/`Unary`/`Assign`/`VarRef`/… would be ten dull cases. Instead it is +one **SYB** query — `listify` over the `Data` instance RTK already derives for +every AST type: + +```haskell +import Data.Generics (listify) + +referenced :: Data a => a -> [String] +referenced = map nameOf . listify isName + where isName (Name _ _) = True; isName _ = False + nameOf (Name _ s) = s; nameOf _ = "" +``` + +`listify isName` finds every `Ident` node at any depth and returns them as a +list. This is the division of labour the whole tutorial has been building +toward: **quasi-quoters for targeted construction and matching, generic +programming for whole-tree queries.** Use the right one for each job, and a +real pass stays short. + +`resolve` returns `Either String VarMap`, and the driver runs it between +parsing and code generation, with the same contract as a parse error — reject, +exit non-zero, write nothing: + +```haskell +asm <- case scanTokens src >>= parseC of + Left err -> reject err + Right ast -> case resolve ast of + Left err -> reject err + Right varmap -> return (codegen varmap ast) +``` + +## 3. Code generation grows a stack frame + +> **Blog ⇄ RTK.** The same frame setup and the same load/store as the blog, +> with the variable map threaded by a `Reader`. + +Code generation now reads the variable→offset map (from `Resolve`) and still +threads the label counter from stage 4, so it runs in +`ReaderT VarMap (State Int)`. Every function gets a prologue that saves the +caller's frame pointer, points `%rbp` at the new frame, and reserves space for +the locals; every return path runs the matching epilogue: + +```haskell +prologue n = [asmItems| push %rbp + movq %rsp, %rbp |] + ++ [Subq rtkNoPos (mkImm n) rspOp | n > 0] -- carve out n bytes + +epilogue = [asmItems| movq %rbp, %rsp + pop %rbp + ret |] +``` + +A variable reference loads from its slot; an assignment evaluates the +right-hand side and stores it — *and leaves the value in `%eax`*, because +assignment is an expression (`int b = a = 0` works because `a = 0` returns +`0`): + +```haskell +genExp [exp| $name = $e |] = do -- assignment + e' <- genExp e + dst <- offsetOf name + return (e' ++ [asmItems| movl %eax, $dst |]) +genExp [exp| $name |] = do -- reference + src <- offsetOf name + return [asmItems| movl $src, %eax |] +``` + +`offsetOf` is the only place the `Reader` is consulted — it looks the name up +in the map and builds a memory operand. The offset is an `Int` leaf, so it is +built with a constructor helper (`mkMem`), the same way immediates have been +since stage 1, not antiquoted. + +## 4. The assembly grammar grows + +[`asm.pg`](../asm.pg) gains 64-bit `movq`/`subq` (for the frame pointer and +stack pointer, which are 64-bit), the registers `%rbp`/`%rsp`, and a **memory +operand** — a signed displacement off a base register, AT&T `disp(base)`: + +``` +Operand = Imm: '$' num | RegOp: Reg | Mem: num '(' Reg ')' ; +Int: num = '-'? [0-9]+ ; -- signed: locals are at negative offsets, -4(%rbp) +``` + +Making `num` signed is what lets `-4(%rbp)` lex as one displacement; the `-` +is part of the number, and since `Movl` already takes two `Operand`s, it +handles a memory source or destination with no new instruction. + +## 5. Run it + +```bash +make build +printf 'int main() { int a = 5; a = a + 1; return a; }\n' > p.c +./ncc p.c && ./p; echo "exit: $?" +``` + +``` +exit: 6 +``` + +The whole frame is visible — set up, the local stored and loaded at +`-4(%rbp)`, then torn down: + +```asm +main: + push %rbp + movq %rsp, %rbp + subq $16, %rsp + movl $5, %eax + movl %eax, -4(%rbp) # int a = 5 + movl $1, %eax + push %rax + movl -4(%rbp), %eax + pop %rcx + addl %ecx, %eax # a + 1 + movl %eax, -4(%rbp) # a = ... + movl -4(%rbp), %eax # return a + movq %rbp, %rsp + pop %rbp + ret + ... +``` + +## 6. Test it + +```bash +make test # 88 checks +/tmp/wacc/test_compiler.sh "$PWD/ncc" 5 +``` + +``` +PASS assignment is right-associative: [exp| a = b = c |] +PASS full pipeline: parse C -> resolve -> codegen -> emit -> parse Asm +PASS tests/invalid/use_before_decl.c (rejected) +... +===================Stage 5 Summary================= +17 successes, 0 failures +``` + +A free bonus: stage 4's official score rose from 24 to **27**. Its +`skip_on_failure_*` short-circuit tests use local variables, so the harness +skipped them before; now they compile and pass — a second, independent check +that stage 4's short-circuit jumps are right. + +## What changed from stage 4 + +| | Stage 4 | Stage 5 | +|---|---|---| +| `c.pg` | comparisons/logicals | `+ Assign`, `VarRef`, declaration statements | +| **`Resolve.hs`** | — | **new: the first semantic pass (SYB query + var→offset map)** | +| `asm.pg` | jumps + setcc | `+ movq subq`, `Mem` operand, `%rbp`/`%rsp`, signed `num` | +| `Codegen.hs` | `State Int` | `ReaderT VarMap (State Int)`; prologue/epilogue; load/store | +| `Main.hs` | parse → codegen | parse → **resolve** → codegen | + +The structural step was the new pass — the first time the compiler rejects a +program for a reason the grammar cannot express. + +## Next + +Stage 6 adds `if`/`else` statements and the `?:` conditional expression — the +first *control flow* in the source language, which brings the classic +dangling-else parsing question and reuses the stage-4 label supply. It is task +**C6** in +[`docs/c-compiler-tutorial-plan.md`](../../../docs/c-compiler-tutorial-plan.md). diff --git a/tutorials/c-compiler/tutorial/README.md b/tutorials/c-compiler/tutorial/README.md index 3e83536..6d32602 100644 --- a/tutorials/c-compiler/tutorial/README.md +++ b/tutorials/c-compiler/tutorial/README.md @@ -27,9 +27,10 @@ verified against the build. | [02 — Unary operators](02-unary.md) | [Part 2](https://norasandler.com/2017/12/05/Write-a-Compiler-2.html) | `-`, `~`, `!` and nesting; codegen becomes recursive. | | [03 — Binary operators](03-binary.md) | [Part 3](https://norasandler.com/2017/12/15/Write-a-Compiler-3.html) | `+ - * /`, the precedence cascade, and a stack-machine codegen. | | [04 — Relational and logical](04-relational.md) | [Part 4](https://norasandler.com/2018/01/08/Write-a-Compiler-4.html) | `== != < <= > >= && ||`, short-circuiting, and the first stateful codegen. | +| [05 — Local variables](05-variables.md) | [Part 5](https://norasandler.com/2018/01/25/Write-a-Compiler-5.html) | Declarations, assignment, a stack frame, and the first semantic pass (SYB). | -Stages 5–10 (local variables through file-scope variables) are implemented one -at a time; each adds a page here. Until then they live as task descriptions in +Stages 6–10 (`if`/`else` through file-scope variables) are implemented one at a +time; each adds a page here. Until then they live as task descriptions in [`docs/c-compiler-tutorial-plan.md`](../../../docs/c-compiler-tutorial-plan.md). ## Conventions on these pages