Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 71 additions & 38 deletions tutorials/c-compiler/Codegen.hs
Original file line number Diff line number Diff line change
@@ -1,67 +1,102 @@
{-# 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

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 :
$items
|]
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)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
8 changes: 6 additions & 2 deletions tutorials/c-compiler/Emit.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
26 changes: 15 additions & 11 deletions tutorials/c-compiler/Main.hs
Original file line number Diff line number Diff line change
@@ -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 <file.c>
-- Produces an executable next to the source file (test-suite contract of
Expand All @@ -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)

Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions tutorials/c-compiler/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 15 additions & 12 deletions tutorials/c-compiler/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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. |
Expand Down Expand Up @@ -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`
Expand Down
Loading
Loading