| git.druid.rocks | index | druid520 | nscc | docs/ | nscc.btft |
docs/nscc.btft
[table class="topnav"]
[tr]
[td class="logotab"]nscc[e]
[td][see name="index"]index[e][e]
[td][see name="nsc"]nsc[e][e]
[td][see name="ir"]ir[e][e]
[td][see name="nscc"]nscc[e][e]
[e]
[e]
[h1]nscc: the compiler itself[e]
[h2]the driver[e]
nscc is a posix sh script, deliberately just glue: it finds its own directory, puts the jcplib/ perl modules on PERL5LIB, exports NSCC_FILE (so every err: line carries the real filename), and runs the eight stages, each reading the last one's output on stdin and writing its own to stdout (each stage is its own mini pipeline of in -- preproc -- proc -- out). stages run against per-stage files in a temp dir: posix sh has no pipefail, so one big pipe would let an upstream stage's failure vanish behind the last stage's exit code -- with set -e and one file per stage, every stage's failure stops the build with its own err: message.
flags: -j writes the intermediates next to the input (foo.toks.j0 ... foo.s) instead of the temp dir, for eyeballing a failed compile; -s builds a target header ("target freestanding os=<uname -s>", plus " osver=<uname -r>" on netbsd) and injects it between jdesugar4 and jlower5, so the IRs from .lir.j5 down are self-describing and jemit8 emits nscc's own freestanding _start (no crt1, no libc) plus, on netbsd, the .note.netbsd.ident section every netbsd executable must carry (charge's exact src/netbsd-note.s shape, version encoded major*100000000 + minor*1000000 + teeny*100); -o names the output .s (default stdout). NSCC_OS/NSCC_OSVER override the uname detection for cross-compiling.
[h2]the stages[e]
jlex0 - the lexer: char classes, the keyword set (the single source of truth, see nsc.btft), numeric literals, string/char literals, /* */ comments, line/column tracking. output .toks.j0.
jparse1 - the parser: the grammar, precedence climbing, error recovery at statement boundaries (one error per statement, skip to the next ; or }). output .cst.j1.
jscope2 - symbols and scopes: name resolution (including which kind of call a bare name(args) is -- a function name is a direct call, a variable is an indirect call through its ptr -- and &f of a function), decl vs definition merging, the global keyword -> linkage, extern-style forward decls, shadowing, redefinition errors, break/continue placement. output .ast.j2.
jtype3 - types: type checking, explicit-cast insertion, the width theorem, the fail-fast guards (shift range, division by zero, INT_MIN / -1, falling off a non-void function, main's signature, the null-pointer and i64-literal-cast bans, arity), indirect calls (the callee must be a ptr; no signature to check, result i64), sizeof folding. output .tast.j3.
jdesugar4 - desugaring: while -> label+goto+if, switch -> a compare chain of nested ifs (no fallthrough), ?: -> if+assign with a fresh tmp, &&/|| -> short-circuit ifs (each side's own stmts stay inside the branch that evaluates it), ! -> == 0, compound assign -> simple assign (a deref whose address contains a call gets bound to a ptr tmp so the call runs once), ++/-- -> load/op/store, break/continue -> goto. output .cast.j4.
jlower5 - lowering: flat three-address code, a fresh %tN per intermediate, explicit loads/stores, local slots, string blob dedupe (same literal, same .LCn). output .lir.j5.
jselect6 - instruction selection: x86 mnemonics, sysv calling convention (args in rdi rsi rdx rcx r8 r9, result in rax -- the same framing for a direct call NAME and an indirect icall through a vreg), addressing modes, conditional branches. output .mir0.j6.
jalloc7 - the dumb allocator: every vreg gets its own 8-byte stack slot (no liveness, nothing clever -- slow, but correct and done); real registers appear only where one instruction immediately needs one (an indirect call's target goes through %r11, which no arg, result or scratch use ever touches). output .mir1.j7.
jemit8 - emission: att syntax, .globl only for linkage=global, .type/.size, and the width model (registers hold 64-bit sign-extended values: loads sign-extend movsbq/movswq/movslq/movq, ALL arithmetic is the q form, stores are 8 bytes wide -- the declared width lives at the load/store boundary, so i8/i16/i32 wrapping comes out right with no register-size games); imm64 constants that do not fit a signed imm32 ride movabsq through %rax; an indirect call is call *%r11; a ret becomes the full epilogue; strings land in .rodata as .LC0... (.byte per byte plus the trailing NUL), defined globals in .data. output: x86_64 att asm, no gnu extensions (no %gs: prefixes, no {sae}, no @PLT/@GOTPCREL operand modifiers) -- what jemit8 prints is exactly what gas consumes.
[h2]jcplib[e]
two modules, shared by every stage: jcplib::tree (the s-expression reader/writer for .j1-.j4 -- see ir.btft for the format it pins) and jcplib::util (the type set, and the one error idiom, exactly two shapes, no others: err: message. when there is no position, or err: stage: file:line:col: message. when there is -- the caller owns the whole prefix, util only adds err: and the trailing period. a malformed IR is always a build-stopping bug, never something a stage silently skips past).
[h2]the mk build system[e]
same shape as mp's own: makefile (default: b, plus c for clean), mk.conf (src/out/ln/lnflgs -- nscc is currently perl-only, so no cflags; if any stage ever moves to c89, its flags go into mk.conf copied verbatim from mp's own, the full -std=c89 -Wall -Wextra ... set plus the sanitizer probe in mk/b.sh), mk/b.sh (a perl syntax gate on every src/*.pl and src/jcplib/*.pm -- both streams silenced, since perl prints "syntax OK" to stdout on old perls and stderr on new ones -- then symlinks: bare nscc from src/nscc.sh, one bare symlink per stage from src/j*.pl, the whole jcplib/ directory; the .pl/.sh suffixes are strictly a src/ thing, exactly how mp installs its mp.<command> files bare), and mk/c.sh. out/ is the built tree.
[h2]the test suite[e]
test/run.sh compiles each tNN.nsc, links, runs, and checks the exit code (reported uniformly: ok: name (exit N)); the neg cases assert that a deliberately broken program fails to compile with the expected message fragment, reported the same way so a passing run's log never contains an err: line. t12 and t23 link multiple translation units (and share a global) to prove extern-style linking; t33 and the s_ checks exercise -s (linux exit syscall 60, netbsd exit syscall 1 plus the ident note); t53 is a function-pointer dispatch table whose exit code only comes out right if every indirect call reached the right function; a determinism check asserts the whole pipeline is a pure function of its input; and every generated .s is also assembled with bare as to keep the no-gnu-extensions claim honest. run.sh needs a built tree first (make), and -j keeps intermediates.