Red Flags

Every plausible-sounding wrong reading the domain names, collected from the lessons that refute them. If you have said one of these, the lesson beside it is the one to read.

"The compiler decides what my code means." The language definition decides; the compiler decides how to achieve it. Where the definition is silent, the compiler decides — and that silence is the part worth learning.From Source to Behavior →
"Undefined behavior means it crashes." It means the definition imposes no requirement, so the most common outcome is that it works, quietly, until an optimization level or a compiler version changes.From Source to Behavior →
"If it produces the right answer, the program is correct." It produced the right answer under one implementation of a definition that permitted several. That is evidence, not correctness.From Source to Behavior →
"Source code is the program." Source code is one representation of it. The artifact that runs has usually lost the variable names, the comments and most of the structure — see [[information-loss]].From Source to Behavior →
"Every compiler has these thirteen phases." Every compiler has *some* sequence of representations. Thirteen is a description of one family, and a bad description of interpreters, JITs and transpilers.The Phases, and Why Each One Exists →
"More phases means a better compiler." More phases means more compile time and more surface. A phase is justified by a question, and a phase with no consumer is dead weight.The Phases, and Why Each One Exists →
"Optimization is a phase like the others." It is the one phase that adds nothing to the representation. It exists to remove work, and it is also the phase most able to make the compiler wrong.The Phases, and Why Each One Exists →
"The parser checks my program." The parser checks that the program has a valid shape. Whether it means anything is the next phase's problem entirely.The Phases, and Why Each One Exists →
"The middle-end optimizes my code." It optimizes an IR that your code was translated into. If the translation lost the fact that made the optimization possible, there is nothing to optimize.Frontend, Middle-End, Backend →
"LLVM is a compiler." LLVM is a middle-end, a set of backends and supporting libraries. Clang is the C and C++ frontend that uses it, and confusing the two makes the whole architecture unreadable — [[llvm]].Frontend, Middle-End, Backend →
"The backend is where performance is decided." Instruction selection and register allocation matter, but the transformations that change the asymptotics of a loop all happened in the middle-end, and the decision to expose them happened in the frontend.Frontend, Middle-End, Backend →
"Any language can be added by writing a frontend." Any language can be *executed* that way. Getting a language's own guarantees exploited usually requires an IR of your own above the shared one.Frontend, Middle-End, Backend →
"Python is interpreted, C++ is compiled." CPython compiles to bytecode and caches it; C++ has interpreters in daily production use. Both halves describe one common implementation and mistake it for the language.Compiler versus Interpreter Is Not a Binary →
"Compiled languages are faster." Ahead-of-time compilation to native code usually starts faster and often runs faster, but the comparison is between implementations on a workload, and shape three wins some of those workloads outright by specialising on facts shape two could not have.Compiler versus Interpreter Is Not a Binary →
"A JIT is just a compiler that runs late." It is a compiler that runs late *and can be wrong*, because it optimizes on observations rather than proofs. The guard-and-deoptimize machinery is what distinguishes it, and it has no analogue in shape two.Compiler versus Interpreter Is Not a Binary →
"Bytecode is machine code for a fake machine, so it is basically the same thing." The instruction set is designed for compactness, portability and easy verification rather than for silicon, which is why stack machines are common in bytecode and rare in hardware — [[stack-vs-register-vm]].Compiler versus Interpreter Is Not a Binary →
"TypeScript adds type safety at run time." It adds nothing at run time. Every guarantee it provides is a build-time guarantee about code that was checked, and data crossing a boundary from outside was not checked by anything.What Seven Real Languages Actually Do →
"Java is slow because it runs on a virtual machine." Steady-state HotSpot performance on scalar code is competitive with ahead-of-time compilation on many workloads; the costs that are real are start-up, memory footprint and allocation behavior, none of which is the dispatch loop.What Seven Real Languages Actually Do →
"Go compiles to native code, so it has no runtime." It has a substantial runtime — a scheduler, a garbage collector, growable stacks — statically linked into every binary. Native compilation and having a runtime are independent.What Seven Real Languages Actually Do →
"Rust and C++ are the same strategy." Both compile ahead of time, and their generic instantiation, their overflow semantics and their build-time evaluation models differ enough that performance advice does not transfer unexamined.What Seven Real Languages Actually Do →
"Ahead-of-time compilation is always faster." It always starts faster. Steady-state throughput on polymorphic code is a genuine contest, and speculative specialisation wins some of it outright.Ahead-of-Time Compilation →
"Native means no runtime." Go and Rust binaries contain runtime support — schedulers, collectors, unwinders — statically linked in. Native compilation removes the *translator*, not the runtime.Ahead-of-Time Compilation →
"Link-time optimization is free performance." It is a large increase in link time and memory, sometimes for single-digit percentage gains, and it must be measured on your program rather than assumed.Ahead-of-Time Compilation →
"If it compiled, it will run." It will run on this target triple, with this instruction-set baseline, against these library versions. Each of those is a way for a successful build to produce a binary that faults on someone else's machine.Ahead-of-Time Compilation →
"The debugger is broken." It is reporting a program that does not have the shape of your source any more. At -O0 it will tell you everything, which is the fastest way to confirm it.What Dies at Each Stage →
"Debug symbols make the program slower." They make the artifact larger. What makes the program slower is the lower optimization level usually enabled alongside them; -O2 -g is a normal and useful combination.What Dies at Each Stage →
"Stripping is a size optimization." It is a size optimization that permanently converts every future crash report into an unsolvable one, unless the symbols were archived first.What Dies at Each Stage →
"A source map is a debugging convenience." A source map published on a public server is a copy of your source code, served to anyone who requests it.What Dies at Each Stage →
"The compiler turns code into assembly." It turns code into a sequence of representations, and assembly is one of the last. Almost every interesting decision happens before it.One Line, All the Way Down →
"These stages happen in this order in every language." They happen in this order in an ahead-of-time compiler for a statically typed language. Python, JavaScript and TypeScript each take a genuinely different route — that is what [[four-languages-one-program]] exists to show.One Line, All the Way Down →
"The IR is what the CPU runs." Nothing runs the IR. It is a data structure inside a process that has usually exited before your program starts.One Line, All the Way Down →
"If I can see it in the source, the debugger can show it to me." Only if the compiler emitted metadata for it and the optimizer preserved that metadata. Frequently neither is true — [[debugging-optimized-code]].One Line, All the Way Down →
"One language is simply better than the others and the argument is about which." Better is only defined against an audience, a failure cost and a deployment environment. Change any of the three and the ranking changes.Who Is the Language For? →
"Powerful features are always better." Every feature is also a thing a reader must know to read the code, and a thing the compiler must handle in combination with every other feature. Go's early omissions were expensive and they were not accidents.Who Is the Language For? →
"You can add the guarantees later." You can add checks later. You cannot add a guarantee to a language whose existing programs violate it, which is why nullability, integer overflow and mutability are so hard to retrofit — [[nullability]].Who Is the Language For? →
"Syntax is where language design happens." Syntax is the cheapest part to change and the part users argue about most. The semantics decide what the compiler can prove and what the program can mean.Who Is the Language For? →
"These are advanced questions for language committees." They are decided in the first week of any language's life, including yours, by whoever writes the evaluator.The Questions a Language Definition Must Answer →
"Leaving it unspecified keeps our options open." It keeps the *implementation's* options open and closes the programmer's, and it does so silently, which is the worst combination of the two.The Questions a Language Definition Must Answer →
"Syntax first, semantics later." Semantics constrain syntax more than the reverse: a language whose parse depends on type information has already decided that its parser and type checker cannot be separated, which affects every tool anyone ever builds for it.The Questions a Language Definition Must Answer →
"We can specify it once we see what people do." What people do will be what the first implementation permitted, and by then specifying anything else breaks their code.The Questions a Language Definition Must Answer →
"A syntax error means my code is wrong." It means your text is not a program. Code that is entirely wrong can be syntactically perfect, and usually is.Syntax versus Semantics →
"Semantics is just types." Types are the largest part of most static semantics, and binding, initialisation, exhaustiveness, ownership and effect rules are all semantics too, each with its own phase.Syntax versus Semantics →
"Two languages with similar syntax are similar languages." C++ and Java share a great deal of surface and differ on assignment, memory, dispatch and generics — that is, on everything the syntax does not say.Syntax versus Semantics →
"The parser validates my program." The parser validates its shape. In a language with a type-dependent grammar it also needs the symbol table, and even then it is checking shape.Syntax versus Semantics →
"Ergonomics is subjective." The cost of the common case is countable, defaults are documented, orthogonality is visible in the rule count, and diagnostic quality can be compared side by side on identical errors.Ergonomics Is a Compiler Feature →
"Good error messages are a matter of writing better strings." They are a consequence of span threading, constraint provenance and recovery, all of which are decided before any message is written.Ergonomics Is a Compiler Feature →
"A more permissive language is more ergonomic." It is more permissive at the moment of writing and less at the moment of debugging, and the second moment is longer.Ergonomics Is a Compiler Feature →
"We can tighten the defaults later." You can add an opt-in. Changing a default rejects existing programs, which is why every language that has tried has shipped a flag instead.Ergonomics Is a Compiler Feature →
"We will decide the execution model later." Every dynamic feature added before that decision removes options from it, silently.Choosing an Execution Model →
"A JIT is strictly better than an interpreter." A JIT is a compiler running inside your process, consuming memory and producing unpredictable pauses. For short-lived processes and for latency-sensitive ones, that is a cost with no matching benefit.Choosing an Execution Model →
"Compiling to WebAssembly makes a language portable." It makes it portable to hosts that implement the proposals you depend on, with the memory and system-access model Wasm defines. That is a specific portability, not a general one.Choosing an Execution Model →
"Interpreters are for prototypes." Every production JavaScript and Java runtime contains one, permanently, because starting fast and falling back safely both require it.Choosing an Execution Model →
"Garbage collection means the programmer does not think about memory." It means the programmer does not think about *release*. Allocation rate, retention and object size still determine the program's behavior, and they are still the programmer's.Choosing How Memory Is Managed →
"Reference counting has no pauses." Dropping the last reference to the root of a large structure frees the whole structure at that point, which is an unbounded amount of work in one place — the pause moved rather than disappeared.Choosing How Memory Is Managed →
"The borrow checker prevents memory bugs." It prevents them in checked code. Unsafe blocks exist, are used by every non-trivial program indirectly through libraries, and are where the guarantee is discharged by a human.Choosing How Memory Is Managed →
"Arenas are a micro-optimization." Replacing a million small allocations with one is usually the largest single allocation improvement available, and it changes the deallocation cost to nearly zero.Choosing How Memory Is Managed →
"Async makes code concurrent." Async makes suspension explicit and lets one thread interleave many tasks. Whether anything runs in parallel is a scheduler question, and in a single-threaded executor the answer is no.Choosing a Concurrency Model →
"Message passing eliminates concurrency bugs." It eliminates data races. Deadlock, ordering assumptions and queue growth are untouched, and the last one is how message-passing systems usually fail.Choosing a Concurrency Model →
"The memory model only matters if I write lock-free code." It decides what the optimizer may do to ordinary code near synchronisation, which is why an optimizing compiler had to stop performing transformations that were legal before threads existed.Choosing a Concurrency Model →
"The compiler will vectorize it." It may, if it can prove independence and non-aliasing and the trip count works out. The remark flags will tell you which of those failed, and usually one has.Choosing a Concurrency Model →
"FFI is just calling a function." It is agreeing on a calling convention, a memory layout, an ownership discipline, an error protocol and a threading model, only the first of which the compiler checks.Interoperability Is a Language Design Decision →
"If it compiles, the layout matches." Nothing checks that the header you compiled against describes the library you loaded. That mismatch is the classic ABI break and it produces silent corruption.Interoperability Is a Language Design Decision →
"Garbage collection makes memory easier at the boundary." It makes it harder: the collector may move or free an object that foreign code holds, which is a problem manual languages simply do not have.Interoperability Is a Language Design Decision →
"We can add embeddability later." Embeddability constrains the collector, the scheduler, signal handling and initialisation. Languages that did not design for it have generally not retrofitted it.Interoperability Is a Language Design Decision →
"Some languages are just better." Better at a set of requirements, for an audience, on a deployment. Every language in wide use lost something specific, and the loss is usually visible in its most-criticised feature.The Trade Nobody Escapes →
"Static types are strictly better than dynamic ones." They buy a class of errors caught early and cost build time, annotation, rejected programs and a learning curve. Whether that trade is worth it depends on what a wrong program costs and when it is discovered.The Trade Nobody Escapes →
"We can have safety and simplicity and performance." You can have two convincingly. The systems that appear to have all three have moved the cost somewhere less visible — usually into build time, into a restricted subset, or into a runtime somebody else maintains.The Trade Nobody Escapes →
"The trade-offs are all well known, so this is just a matter of preference." The trade-offs are known; which ones apply to your system is not, and that is the part the discussion is actually for.The Trade Nobody Escapes →
"The grammar defines the language." It defines the syntax. Every rule about declaration, typing and effect lives outside it, and those are most of the language.Formal Grammars →
"If the grammar accepts it, it compiles." The grammar accepting it is the first of five or six gates. x = undeclared + "s" parses cleanly in most languages.Formal Grammars →
"Writing the grammar is the hard part of designing syntax." Deciding what must be rejected is the hard part. Writing productions for what you already know you want takes an afternoon.Formal Grammars →
"Nonterminal names do not matter." They become the vocabulary of every error message a generated parser emits.Formal Grammars →
"Leftmost and rightmost derivations give different trees." They give the same tree for an unambiguous grammar. Different trees mean the grammar is ambiguous, which is a different problem entirely.Productions & Derivations →
"The parser builds the derivation." The parser builds the tree; the derivation is a way of talking about the order it did so. Nothing stores a list of sentential forms.Productions & Derivations →
"If I can derive it, the parser will accept it." Only if the parser's algorithm can *find* that derivation deterministically with the lookahead it has. Derivability and parseability by a given algorithm are separate properties.Productions & Derivations →
"Bottom-up parsing means reading the input right to left." It reads left to right like everything else. The *derivation* it reconstructs is the rightmost one, reversed.Productions & Derivations →
"EBNF is more powerful than BNF." It describes exactly the same class of languages. Every EBNF grammar can be mechanically expanded into BNF, which is what tools that only accept BNF do internally.BNF & EBNF →
"The star tells me the list is left-associative." It tells you nothing about grouping. That is the whole point of this lesson.BNF & EBNF →
"{ x } means literally one or more braces." In ISO 14977 and in the Go specification it is the repetition operator. Which dialect you are reading has to be established first.BNF & EBNF →
"Since both forms accept the same inputs, I can pick either." They accept the same inputs and build different trees. For + nobody notices; for -, / and ** everybody does.BNF & EBNF →
"Programming languages are context-free." Their *core expression and statement syntax* usually is. C and C++ are not, and every language has rules — declaration, typing, arity — that no context-free grammar can express.Context-Free Grammars →
"Context-free means the grammar has no context." It means each production is applied without looking at surroundings. The parse tree has plenty of context; the *rules* may not consult it.Context-Free Grammars →
"A parser generator will tell me if my grammar is ambiguous." It cannot — the question is undecidable. It reports conflicts, which is a different and more conservative claim.Context-Free Grammars →
"If it needs a stack, it needs a parser." It needs a stack *of bounded shape*. Nested comments need a counter, which is why some lexers implement them without becoming parsers.Context-Free Grammars →
"An ambiguous grammar makes the parser fail." It makes the parser choose. Failure would be the good outcome; silence is what actually happens.Ambiguous Grammars →
"Bison reported a conflict, so my grammar is ambiguous." Possibly. Conflicts also arise from the one-token lookahead limit on grammars that are perfectly unambiguous, and the two need different fixes.Ambiguous Grammars →
"Adding %left '+' fixes the grammar." It fixes the *parser*. The grammar is still ambiguous as written, and anyone implementing from the grammar alone will get a different answer.Ambiguous Grammars →
"The dangling else is a theoretical problem." It is normatively resolved in five mainstream language specifications precisely because it was not.Ambiguous Grammars →
"Higher precedence means closer to the root." The opposite. Binding tighter means being applied first, which means being deeper.Operator Precedence →
"Precedence and evaluation order are the same thing." Precedence decides the tree shape. Evaluation order decides which operand of one node is computed first, and in C and C++ that is largely unspecified — see [[observable-behaviour]].Operator Precedence →
"Every language uses the same table." Only for * over +. Shift, bitwise operators and exponentiation-versus-unary-minus all differ across mainstream languages.Operator Precedence →
"The AST records the precedence." It records the shape precedence produced. The numbers are gone, which is why re-printing the source needs the table again.Operator Precedence →
"Associativity only matters for - and /." It matters for every operator that is not exactly associative, which includes floating-point + and *, and for every operator whose operands have side effects.Associativity →
"Left-associative means evaluated left to right." It fixes the tree, not the evaluation order of operands within a node. In C the operands of one + may be evaluated in either order regardless of associativity.Associativity →
"Exponentiation is right-associative everywhere." MATLAB and Excel are left-associative, and both are used for numerical work where the difference is a wrong answer rather than a style question.Associativity →
"x = y = 5 proves Python's assignment is right-associative." It proves Python has multi-target assignment statements. Assignment is not an expression in Python at all, and the walrus operator := was added precisely because it was not.Associativity →
"Left recursion is a bug in the grammar." It is a bug only relative to a top-down algorithm. Bison's manual recommends it, and the trees it produces are the ones you want.Left Recursion →
"Eliminating left recursion is a purely mechanical, safe rewrite." It is mechanical and it changes every parse tree. Safety requires restoring the fold direction in the AST construction.Left Recursion →
"PEG parsers cannot handle left recursion." Not since 2008 in theory and not since Python 3.9 in practice. Plain packrat cannot; several real tools can.Left Recursion →
"If the parser does not hang, there is no left recursion." A memoising or rewriting tool may have handled it silently. Whether it also preserved the associativity you wanted is a separate question worth checking.Left Recursion →
"The lexer removes whitespace." It removes whitespace *from the token stream*. Whether it is retained as trivia is a decision, and every formatter and language server retains it.Lexical Analysis →
"Tokens are words." They are the longest valid lexemes. >= is one token and 42 is one token; neither is a word, and a---b is three operators and two identifiers.Lexical Analysis →
"The lexer validates the program." It validates lexical structure only. let let let; lexes perfectly into three keywords and a semicolon, and fails in the parser.Lexical Analysis →
"Spans are line and column numbers." They are byte offsets. Line and column are derived from them by a separate line-index structure, which is why an off-by-one in the offsets shifts every reported position at once.Lexical Analysis →
"Keywords are matched by their own patterns." They are matched as identifiers and reclassified by lookup, which is why iffy is not if plus fy.Token Kinds →
"The token kind tells you what the thing means." It tells you what category it is. IDENTIFIER says nothing about whether the name is a variable, a function or a type — that is name resolution.Token Kinds →
"More token kinds is always more precise." It is more precise about categories the grammar may not distinguish, at the cost of table size and switch statements everywhere downstream.Token Kinds →
"A NUMBER token holds a number." In most compilers it holds the text. The value comes later, because the value depends on the target type, and the lexer does not know it.Token Kinds →
"Spans are for error messages." They are for error messages, fixes, hover, rename, find-references, coverage, profiling attribution, source maps and debugging. Error messages are the cheapest thing they buy.Token Metadata →
"The parser can recompute positions." It cannot. Once the token list exists, the mapping back to bytes is gone unless it was stored.Token Metadata →
"A column is a column." Byte, scalar and UTF-16 columns are three different numbers, and the LSP and your compiler almost certainly disagree about which one they mean.Token Metadata →
"Storing line and column is simpler." It is simpler until an edit happens above the token, or until a macro expands, at which point it is unusable.Token Metadata →
"Generated lexers are faster than hand-written ones." Not reliably. Modern hand-written scanners in Clang and rustc are tuned in ways a table-driven loop cannot be, and the reason production compilers hand-write them is diagnostics rather than speed.Implementing a Lexer →
"Maximal munch is obviously correct." It is a chosen rule, and C's a---b and x+++y are the standard demonstrations that it produces programs nobody wrote.Implementing a Lexer →
"A lexer never needs lookahead." It needs bounded lookahead whenever a valid token is a prefix of another valid token, which is true of >/>=, ./.. and ////.Implementing a Lexer →
"Error recovery is the parser's job." The scanner has its own recovery obligation: advance on failure, and report unterminated constructs at their start. Getting it wrong makes every later diagnostic worse.Implementing a Lexer →
"Regular expressions are the same as what my language calls regex." Backreferences, lookahead and recursion all leave the regular class, and the features that leave it are the features that force backtracking.Regular Languages →
"Regular means simple patterns." Regularity is about memory, not complexity. A float literal with an optional exponent is regular; two nested brackets are not.Regular Languages →
"If it needs a stack it needs a parser." It needs unbounded, stack-shaped memory. A depth counter is enough for nested comments, which is why some lexers implement them.Regular Languages →
"ReDoS is a bug in the pattern." It is an interaction between a pattern and a backtracking engine. The same pattern on a DFA engine is linear.Regular Languages →
"The automaton implements maximal munch." It does not. The automaton recognises; maximal munch is the scanner's rewind policy, and it needs two registers the automaton definition has no place for.Finite Automata →
"Every state a scanner passes through is accepting." The interesting states are the ones that are not — a trailing decimal point being the standard example, and the reason 1..5 works.Finite Automata →
"A DFA cannot be wrong, so a generated lexer cannot be wrong." The automaton is correct for the patterns given. Which states accept, and the priority between them, are the decisions, and both are yours.Finite Automata →
"Backing up is a theoretical concern." It is what flex --backup exists to report, and it is the difference between linear and quadratic scanning on the wrong pattern set.Finite Automata →
"An NFA is slower than a DFA." Per character, yes. End to end, an NFA simulation can win, because the DFA had to be constructed first and construction can dominate or fail.NFA vs DFA →
"Subset construction always blows up." It blows up on a specific family of patterns. Programming-language token sets are not in it, which is why eager determinisation is standard for lexers.NFA vs DFA →
"Nondeterministic means the machine guesses." It means several states are active at once. The simulation tracks all of them; nothing is guessed and nothing is random.NFA vs DFA →
"Minimisation is always worth it." It shrinks the table and destroys the state-to-pattern correspondence, which makes a generated scanner considerably harder to debug.NFA vs DFA →
"123abc is a lexer bug." It is maximal munch working correctly. Rejecting it is an extra rule some languages add for diagnostics, not a correction.Lexer Hazards →
"Contextual keywords are handled by the lexer." They are handled by the parser. The lexer's contribution is to *not* classify them, which is only possible because keywords are a table lookup.Lexer Hazards →
"Python's indentation is handled by the parser." The tokenizer emits INDENT and DEDENT with an explicit stack. The grammar downstream is ordinary and knows nothing about columns.Lexer Hazards →
"C's typedef problem is a historical curiosity." It is why C and C++ language servers need a build configuration to work at all, and why they show spurious errors before one is available.Lexer Hazards →
"A token always corresponds to a range of source characters." A Python DEDENT corresponds to none, and a > split out of a >> corresponds to half of one.Lexer Hazards →
"A parser understands whether the program makes semantic sense." It does not. "hello" - true parses without complaint; the objection comes from the type checker several phases later, and building the objection into the parser would break forward references.What a Parser Actually Does →
"The parser decides that * binds tighter than +." The grammar decides. The parser is a procedure that recovers the tree the grammar specifies, and a different grammar over the same tokens legitimately yields a different tree.What a Parser Actually Does →
"If it parses, the syntax is right." If it parses, the syntax matched *this* grammar. Whether the grammar matches the language specification is a separate question, and mismatches there are how two compilers for the same language disagree.What a Parser Actually Does →
"Parsing is the hard part of a compiler." Parsing is the best-understood part, with fifty years of theory and generated solutions. The hard parts are semantics, legality and diagnostics.What a Parser Actually Does →
"The AST is just a tidied-up parse tree." It is a different representation with different guarantees. Tidying implies you could untidy it; you cannot get the source text back.Parse Tree vs Abstract Syntax Tree →
"Parentheses are stored in the AST so we know the user wrote them." In almost every AST they are not. (1 + 2) * 3 and 1 + 2 * 3 produce different tree shapes, not different parenthesis nodes, and (a) and a produce identical trees.Parse Tree vs Abstract Syntax Tree →
"A CST is only useful for IDEs." It is also what lets a compiler produce good delimiter diagnostics and what lets a code-mod tool touch one line without rewriting the file.Parse Tree vs Abstract Syntax Tree →
"The AST is smaller so it is strictly better." It is smaller because it discarded things. Whether that is better depends entirely on whether anything downstream needed them, and the tools that need them are exactly the ones users notice.Parse Tree vs Abstract Syntax Tree →
"Left recursion produces a wrong tree." It produces no tree. The parser never consumes a token, so it recurses until the stack is exhausted — on input that is completely valid.Recursive Descent →
"Recursive descent can only parse simple languages." C, C++, Rust, Go, Java and JavaScript are all parsed this way in their primary implementations. The limitation is on grammar *form*, not on language complexity.Recursive Descent →
"Because it is hand-written, it must be slower than a generated table-driven parser." Hand-written recursive descent is usually faster in practice: no table indirection, excellent branch prediction, and the compiler can inline across the rule functions.Recursive Descent →
"The recursion in parseFactor calling parseExpression is the dangerous kind." It is not — a ( has already been consumed, so the parser has made progress. Recursion is only fatal when it can reach itself with the token position unchanged.Recursive Descent →
"Pratt parsing and precedence climbing are different algorithms." They are two presentations of the same loop. Pratt dispatches per-token handlers; precedence climbing branches on a precedence table. Both compute the same tree and both stop on the same comparison.Pratt Parsing →
"The binding-power numbers mean something." Only their relative order does. Doubling every number changes nothing; changing one by one can change the language.Pratt Parsing →
"Left-associative means the right binding power is lower." It is the other way round, and this is the single most common error. Left-associative gives the right side *more* power so the inner call stops early and the outer loop folds left.Pratt Parsing →
"Pratt parsing replaces recursive descent." It replaces the expression part of it. Statements and declarations remain descent in every production compiler that uses it.Pratt Parsing →
"It is a bottom-up technique because it builds from operands upward." It is top-down: it recurses before it reduces, and the call stack holds pending left operands rather than a table-driven state stack.Pratt Parsing →
"LL(1) means the parser looks at one token." It means the *decision* about which production to apply consults one token. The parser reads the whole input; it just never needs more than one token in hand to choose.LL Parsing, FIRST and FOLLOW →
"FOLLOW sets tell you what token comes next." They tell you what token *could* come after a nonterminal in some sentential form, across the whole grammar. That is a static over-approximation, not a prediction about this input, and it is why FOLLOW-based decisions can accept slightly more than the grammar describes.LL Parsing, FIRST and FOLLOW →
"If my grammar is not LL(1) I need a more powerful parser." Usually you need to rewrite the grammar — left recursion and common prefixes are both mechanically removable, and between them they account for most conflicts.LL Parsing, FIRST and FOLLOW →
"LL(2) is twice as powerful as LL(1)." Increasing k does enlarge the class of grammars, but the classic hard cases — left recursion, unbounded common prefixes — are not fixed by any finite k.LL Parsing, FIRST and FOLLOW →
"Recursive descent is not LL parsing." It is LL parsing with the stack in the call frames. Every branch it makes is a table lookup that was inlined.LL Parsing, FIRST and FOLLOW →
"LR is more powerful, so it is the better choice." It accepts more grammars. It also does not know what construct it is parsing, which is why the compilers you use every day are not LR — see [[ll-vs-lr]].LR Parsing →
"A shift/reduce conflict is a bug in the generator." It is the generator correctly reporting that the grammar does not determine the parse. The bug, if there is one, is in the grammar.LR Parsing →
"Bison defaults to shift, so conflicts are harmless." The default is usually what you wanted for dangling else and usually irrelevant elsewhere — and "usually" is doing all the work. A conflict count you do not track is a language definition you do not control.LR Parsing →
"LALR is a weaker approximation of LR(1), so it accepts fewer languages." It accepts fewer *grammars*. Every LALR(1) language is an LR(1) language and vice versa; the merge costs you particular grammars, not expressive power.LR Parsing →
"The parse stack holds the tree." It holds a viable prefix of grammar symbols. The tree is built by semantic actions attached to reductions, and if you attach none, an LR parser happily recognises the input and produces nothing at all.LR Parsing →
"Reduce means the parser removes something from the program." It replaces symbols on its own stack with the nonterminal they form. Nothing about the input changes, and reduce consumes no tokens at all.Shift and Reduce, Step by Step →
"If the top of the stack matches a production, reduce." That is exactly the mistake the notion of a handle exists to prevent. A stack top can match a production and still be the wrong reduction, killing an otherwise valid parse.Shift and Reduce, Step by Step →
"Shift/reduce conflicts happen because the parser is not smart enough." They happen because the grammar admits two derivations. A smarter parser (GLR) does not resolve the ambiguity, it returns both.Shift and Reduce, Step by Step →
"The parser builds the tree." The parser recognises the input. Tree construction happens in semantic actions attached to reductions, and a parser with no actions produces no tree while parsing perfectly.Shift and Reduce, Step by Step →
"LR is more powerful, therefore LR is better." More powerful over *grammars*. The compilers you use chose the weaker family for reasons that have nothing to do with which grammars it accepts.LL vs LR: Why Production Compilers Chose the Weaker One →
"Hand-written parsers are a legacy of the era before good tools." The direction of migration is the other way: GCC and Go moved from generated to hand-written, in 2004 and 2015 respectively, on modern toolchains.LL vs LR: Why Production Compilers Chose the Weaker One →
"LL(k) ⊂ LR(k) means LL parsers can parse fewer languages." It means fewer *grammars*. The language gap exists but is much narrower, and for a language you are designing you can simply choose a grammar in the smaller class.LL vs LR: Why Production Compilers Chose the Weaker One →
"Generated parsers are faster because tables are faster than function calls." Hand-written descent is usually at least as fast in practice: no table indirection, predictable branches, and cross-function inlining.LL vs LR: Why Production Compilers Chose the Weaker One →
"You cannot get good error messages from a generated parser." tree-sitter and ANTLR both do better than the Yacc baseline. The structural handicap is real; it is not an absolute ceiling.LL vs LR: Why Production Compilers Chose the Weaker One →
"Using a generator means I do not need to understand parsing." It means you need to understand grammars, ambiguity and the tool's conflict reports instead of control flow. The total amount to learn is not smaller.Parser Generators →
"A generator produces a slower parser." Generated table-driven parsers are fast. The performance difference between families is small and usually favours hand-written descent slightly, for locality reasons, not by enough to decide anything.Parser Generators →
"tree-sitter is a parser generator like Bison." It generates a parser, and it is built for a different job: tolerant, incremental parsing for editors, always returning a tree. Comparing it to Bison on grammar power misses what it is for.Parser Generators →
"If the conflict count is zero the grammar is correct." It is unambiguous for that algorithm class. Whether it describes the language you meant is a separate question no tool checks.Parser Generators →
"ANTLR is an LL(k) tool, so it is strictly weaker than Bison." ALL(*) simulates the grammar against the actual input and accepts many grammars no fixed k handles. The classic family comparison does not transfer.Parser Generators →
"Column means the same thing everywhere." It means at least four different things, and the tools in a single editing session — compiler, language server, editor — routinely use three of them.Source Locations →
"UTF-8 makes this simple because everything is bytes." It makes the *storage* simple. The moment a number is shown to a human or sent over LSP, the unit question returns.Source Locations →
"We can convert line and column back to an offset if we need to." Only if you still have the file exactly as it was read. If anything normalised it, the mapping is gone.Source Locations →
"Emoji are just another character." An emoji is one scalar value, four UTF-8 bytes and two UTF-16 code units, and possibly part of a longer grapheme cluster with a skin-tone modifier or a ZWJ sequence. It differs under every convention at once, which is why it is the standard test case.Source Locations →
"This only matters for internationalised software." It matters for any source file containing a non-ASCII character in a comment or a string literal, which in practice is most codebases.Source Locations →
"A span is where the error is." A diagnostic usually needs several: where it was detected, where the conflicting thing was declared, and where the fix goes. A single span is why some compilers can only say "somewhere around here".Spans and Ranges →
"Half-open versus closed is a style preference." It decides whether an insertion point is representable at all, which decides whether machine-applicable fixes are possible.Spans and Ranges →
"We can compute spans later from the AST." Only for nodes that correspond to source text. Anything desugared, synthesised or merged has no derivable span, and those are exactly the constructs whose errors are most confusing.Spans and Ranges →
"Spans are a frontend concern." They are how a debugger maps an instruction back to a line, how a coverage tool attributes execution, and how a source map works. They are a whole-pipeline concern that happens to originate in the frontend.Spans and Ranges →
"Storing spans is expensive so we should be selective." Selective means the feature you have not thought of yet is impossible. The industry answer is to make spans cheap enough to store unconditionally, not to store fewer.Spans and Ranges →
"More errors reported means better recovery." Reporting forty errors for one mistake is worse recovery, not better. The metric is whether the *second* error is independent of the first.Error Recovery →
"The compiler is confused after the first error." It is not confused; it is confidently parsing in the wrong context. That distinction matters, because it explains why the later errors are specific and wrong rather than vague.Error Recovery →
"Error recovery is a parser concern." It determines whether semantic phases run, whether an IDE can offer completions, and how the whole frontend handles failure. It is an architectural concern that shows up first in the parser.Error Recovery →
"Panic mode is a bad technique because it discards tokens." Every technique discards or invents something. Panic mode is the one whose guess is explicit and bounded, which is why it is still the default fifty years on.Error Recovery →
"If the parser recovers, later phases can just proceed." They must not. Type-checking a guessed parse is where the second wave of nonsense errors comes from.Error Recovery →
"Panic mode is crude, so good compilers do not use it." Every mainstream compiler uses it, augmented with per-construct sets, delimiter tracking and error productions. The augmentations are the difference, not the replacement.Parser Synchronization →
"The parser is confused after an error." It is not confused; it is confidently parsing in the wrong context. That is why the following messages are specific and wrong rather than vague.Parser Synchronization →
"Suppressing errors hides information." Recording them with a flag hides nothing; it moves the decision about what to show to the consumer, which is where it belongs.Parser Synchronization →
"A bigger sync set is safer." A bigger set means faster resynchronization and larger skipped regions, and everything inside a skipped region is a diagnostic that will never be produced.Parser Synchronization →
"Good error messages are a matter of wording." Wording is the last ten per cent. The rest is spans, secondary labels and context that earlier phases had to keep.Diagnostic Quality →
"The compiler cannot know what I meant." It frequently can, from the enclosing construct and the token it found, and the message improves enormously just by saying what it wanted and what it got.Diagnostic Quality →
"More detail is better." A message people skip conveys nothing. Structure — primary line terse, notes and helps beneath — beats length.Diagnostic Quality →
"This is a polish task for later." Almost every element requires information that earlier phases must have retained, so "later" usually means "never, without a frontend rewrite".Diagnostic Quality →
"If the compiler suggests it, it is right." It may be labelled maybe-incorrect, and the ones that are wrong are exactly the ones that still compile.Suggested Fixes →
"Auto-fix everything the compiler offers." Only the machine-applicable ones. Tools that apply every suggestion are how a codebase acquires changes nobody made deliberately.Suggested Fixes →
"Did you mean is just a string comparison." It is a candidate search plus a threshold, and the threshold is the design. Without one, every unresolved name gets a suggestion.Suggested Fixes →
"Suggestions are a nicety." They are the migration mechanism for a language's own breaking changes, which is the difference between a deprecation that lands and one that sits in a backlog.Suggested Fixes →
"The AST is the source code in tree form." It is the source code with everything the parser no longer needs deleted. You cannot print it back out and get your file.The Abstract Syntax Tree →
"The parser checks the program." The parser checks that the program is *grammatical*. let x = x; and let x: int = "hi"; are both perfectly grammatical and both wrong, and neither is the parser's business.The Abstract Syntax Tree →
"Every language construct is one node kind." Most frontends have several nodes per surface construct and several surface constructs per node — how many, and which way round, is the subject of [[ast-node-design]].The Abstract Syntax Tree →
"If the AST is right, the compiler is right." The AST is right about structure. It is silent on names, types, effects, reachability and everything else that makes a program mean something.The Abstract Syntax Tree →
"Arena indices are just an optimization." The performance is the smaller half. The real payoff is that a node reference becomes a plain copyable integer, which is what makes side tables, caches and incremental compilation tractable.Designing AST Nodes →
"A parent pointer is one field, how bad can it be." It converts the tree into a graph and takes immutability, sharing and incremental reparse with it. It is one field with architectural consequences.Designing AST Nodes →
"Desugaring early is strictly simpler." Simpler for the passes, worse for every message the user reads. The simplicity is real and it is paid for by someone who is not the compiler author.Designing AST Nodes →
"Visitors and tagged unions are alternatives to each other." They are answers to the *same* question from opposite ends of the expression problem, and a compiler often has both — see [[visitor-pattern]].Designing AST Nodes →
"Traversal order is an implementation choice." It determines which facts are available at each node. Choosing wrongly produces a compiler that reports errors on correct code, which is not a style question.Walking the Tree →
"Post-order is just bottom-up and bottom-up is more efficient." Neither is more efficient; they visit the same nodes the same number of times. They differ in *what is known* when the visit happens.Walking the Tree →
"A single well-written pass can do all of it." It can, until two of its jobs need opposite orders — and then it cannot, and untangling it is much more expensive than never fusing them.Walking the Tree →
"Deep recursion is fine, the compiler is not the bottleneck." Depth is bounded by the native stack, not by your patience. Machine-generated code routinely exceeds it and the failure is a crash without a message.Walking the Tree →
"The visitor pattern is object-oriented ceremony you can skip in a modern language." What you skip is the accept boilerplate. The shape — one handler per node kind, traversal factored out — remains, because it is what a pass *is*.The Visitor as the Shape of a Pass →
"Visitors are for traversal." They are for dispatch. Some visitors traverse and some do not, and confusing the two is how you write a pass that visits exactly one node.The Visitor as the Shape of a Pass →
"The expression problem is academic." It is the reason adding one node kind to a mature frontend is a multi-day change, and the reason your lint plugin silently ignored the new syntax.The Visitor as the Shape of a Pass →
"If my visit method is called, my pass saw that construct." Only if something recursed into it. Silence from a visitor is not evidence of absence.The Visitor as the Shape of a Pass →
"Constant folding on the AST is always safe — the values are right there." Division by zero, signed overflow, floating-point rounding modes and the target's NaN behavior all say otherwise. Folding is arithmetic *on the target's semantics*, not on the host's.Changing the Tree →
"Immutable trees are slow because they allocate." They allocate along one root path per edit and share everything else. The relevant comparison is against reparsing the file, which is what the mutable design has to do instead.Changing the Tree →
"A rewrite that produces an equivalent program is legal." Equivalent in value is not enough. Same effects, same order, same number of evaluations, same traps — all four, or it is not the same program.Changing the Tree →
"Desugaring is the parser's job, it saves a phase." It saves a phase and costs every diagnostic, every formatter and every refactoring the ability to speak the user's syntax.Changing the Tree →
"The compiler is the frontend's main user." It is one user, and it has the least demanding requirements of the six — it can reject bad input, run to completion, and exit.One Tree, Six Consumers →
"A parser is a parser; any of them will do for a linter." A second parser is a second definition of the language, and the two will diverge on exactly the constructs users complain about.One Tree, Six Consumers →
"Error recovery is a nice-to-have." It is the difference between a language server that works while you type and one that works only when your file is already correct, which is when you need it least.One Tree, Six Consumers →
"Making the frontend a library is just refactoring." It converts internal data structures into a compatibility surface, and that is an organisational commitment, not a code change.One Tree, Six Consumers →
"Semantic analysis is type checking." Type checking is one of five families. Name resolution, scope rules, flow analysis and declaration context are the others, and most compiler errors a beginner meets are not type errors.Semantic Analysis →
"If it compiles, it works." It is well-formed. Every logic bug you have ever written compiled.Semantic Analysis →
"The parser should catch undeclared variables." A context-free grammar cannot express it. That is a theorem, not a design preference.Semantic Analysis →
"Unreachable-code warnings are a lint." In several languages they are specified compiler behavior — Java requires a compile error for unreachable statements, and definite assignment is in the language specification, not in a style guide.Semantic Analysis →
"The symbol table maps names to types." It maps names to declarations. The type is one field, and it is not filled in until type checking runs — several passes after the entry exists.The Symbol Table →
"One table per program." One table per scope, arranged in a tree that mirrors the program's nesting. Flattening them is how block-local names leak.The Symbol Table →
"Lookup is O(1) because it is a hash map." Lookup within one scope is O(1); resolution walks the scope chain, so it is O(depth), which is why deeply nested code resolves marginally slower and why some compilers cache the answer on the node.The Symbol Table →
"Symbols in the compiler are the same symbols the linker sees." The linker sees only entities with external linkage, after mangling. Everything local — which is most of the table — never becomes a linker symbol at all.The Symbol Table →
"A scope is a pair of braces." A scope is whatever the language says introduces bindings. In Python braces do not exist and blocks do not scope; in JavaScript the same braces scope let and not var.Lexical Scope →
"Inner scopes can see outer variables, so scoping is about visibility." It is about *resolution order*. Visibility is what falls out: the outer binding is not hidden, it is simply found second.Lexical Scope →
"Lexical scope means resolved at compile time." It means resolved by textual nesting. An interpreter for a lexically-scoped language resolves at runtime and still gets lexical answers — the two questions are independent.Lexical Scope →
"Closures capture values." They capture *bindings*, which is why a captured loop variable can be observed changing, and why per-iteration binding is a separate language decision from having closures at all.Lexical Scope →
"Shadowing overwrites the outer variable." It does not touch it. Two bindings exist; only one is reachable at a given point.Shadowing →
"Shadowing is always a bug." In Rust it is idiomatic and often the clearest available code. The bug is shadowing with something *unrelated*, which is a narrower claim.Shadowing →
"If the compiler allows it, it must be safe." Every shadowing bug compiles. That is the entire problem.Shadowing →
"Python does not have shadowing because it has no block scope." It has function-level shadowing of globals, decided by whether a name is assigned anywhere in the function — a stronger and more surprising rule than block shadowing.Shadowing →
"Resolution is just a lookup in a table." It is for locals. For overloads it needs types, for imports it needs a module graph, and for methods it needs inference to have already happened.Name Resolution →
"An unresolved name means a typo." The most common cause in a module system is a missing import, which is why good compilers search names that are *not* in scope as well as names that are.Name Resolution →
"If two imports provide the same name, the compiler picks the better one." Only if the language defines "better". Where it does not, the correct behavior is an error, and a compiler that picks silently has introduced a bug you will meet later.Name Resolution →
"Resolution finishes before type checking." In a language with overloading or methods, they interleave, and the ordering rules are a specified part of the language.Name Resolution →
"Dynamic scoping means dynamic typing." Unrelated axes. Emacs Lisp is dynamically scoped and dynamically typed; Python is dynamically typed and lexically scoped; Common Lisp offers both scoping rules in one statically-compilable language.Static and Dynamic Scoping →
"My language is lexically scoped, so none of this applies." this, thread-locals, context variables and exception handler search are all in your language and all resolve by call path.Static and Dynamic Scoping →
"Dynamic scoping is obsolete." It is confined, not gone, and every framework that offers ambient context — React context, dependency injection scopes, async-local storage — has reinvented it deliberately.Static and Dynamic Scoping →
"A context variable is just a global." A global has one value for the process. A context variable has a different value per call path, which is exactly what makes it dynamic scoping and exactly why it propagates across some boundaries and not others.Static and Dynamic Scoping →
"Hoisting moves declarations to the top of the file." Nothing moves. Bindings are created when the scope is entered; the *initialisation* stays where it is written, which is the entire reason the temporal dead zone exists.Declaration Order →
"Order-independence needs a complicated algorithm." It needs one extra loop over the declarations. The complications only start when declarations depend on each other cyclically.Declaration Order →
"If top-level declarations are order-free, locals are too." Almost no language does this. Locals are position-dependent in Java, Rust, Go, C# and JavaScript alike, and for good reason: a local's value depends on execution order in a way a function's definition does not.Declaration Order →
"Python hoists functions." It does not. def executes; before it executes, the name does not exist. What Python decides statically is *locality*, not availability.Declaration Order →
"The typed AST is a different tree." It is the same tree with more known about it. If the shape changed, that was lowering, not annotation.The Annotated AST →
"Every node has a type." Expressions have types. Statements, declarations and blocks may or may not, depending on the language — in an expression-oriented language like Rust they do, and in C they do not.The Annotated AST →
"If the tree type-checks, the annotations are complete." Only for what was checked. Nodes on error paths carry error types, and a phase that treats an error type as a real one produces confident nonsense.The Annotated AST →
"Types are still there at runtime." Only if the language reifies them. TypeScript erases them entirely, Java erases generic arguments, and what a runtime cannot do about types was decided right here.The Annotated AST →
“If it compiles, it works.” It means a derivation exists under the rules. Whether the program computes the right answer is outside every mainstream type system’s theorem, and always was.What a Type System Actually Proves →
“Once it type-checks there is nothing left to test.” The type system and the test suite cover disjoint properties. Everything the checker declined to prove is the test suite’s entire job.What a Type System Actually Proves →
“Any rejection is the type system getting in the way.” Some are. Most are the conservative approximation doing exactly what it was designed to do, and the restructure it forces is usually the fix.What a Type System Actually Proves →
“A stronger type system is strictly better.” Stronger means more annotation, slower checking and more correct programs rejected. Better is whether that trade fits the codebase.What a Type System Actually Proves →
“Types are documentation.” They are, but that is a side effect. They are first a claim the machine checks, and a documentation-only type — one nothing enforces at the boundary — is a comment.What a Type System Actually Proves →
“Dynamically typed means untyped.” Values are typed, precisely and strongly, in Python, Ruby and Smalltalk. What is untyped is the *expression*, before a value exists.Static and Dynamic Typing, Compared Honestly →
“Static typing is faster.” The speed comes from unboxed representations and direct calls that static types make legal, not from the absence of a check. Say the mechanism or the claim is not checkable.Static and Dynamic Typing, Compared Honestly →
“TypeScript makes JavaScript statically typed.” It makes the build statically checked. The emitted program is the same dynamically typed JavaScript, which is exactly why runtime validation at boundaries is still required.Static and Dynamic Typing, Compared Honestly →
“Strong typing means static typing.” They are different axes. Python is strong and dynamic; C is static and comparatively weak. This one is worth correcting every time you hear it.Static and Dynamic Typing, Compared Honestly →
“Adding type annotations will speed up my Python.” Not in CPython — they are metadata. Speedups from annotations come from tools that *use* them to generate different code, such as Cython or mypyc, which is a different mechanism entirely.Static and Dynamic Typing, Compared Honestly →
“Statically typed languages reject 1 + "hello".” Java, C#, TypeScript, C and C++ all accept it, three of them by concatenating and two by doing pointer arithmetic. The split is per-language, not per-category.Type Checking: Is This Operation Defined for These Operands? →
“TypeScript catches 1 + "hello".” It types it as string on purpose, to match the JavaScript it emits. It catches 1 - "hello", which is a different rule.Type Checking: Is This Operation Defined for These Operands? →
“A coercion means the type system gave up.” A rule fired. Read the rule; the specification will name it.Type Checking: Is This Operation Defined for These Operands? →
“The compiler accepted it, so it means what I intended.” It means a derivation exists. The C++ pointer-arithmetic case is the cleanest demonstration that those are different claims.Type Checking: Is This Operation Defined for These Operands? →
“Adding a cast is harmless if it compiles.” A cast is an input to overload resolution. It can change which function runs, with no warning.Type Checking: Is This Operation Defined for These Operands? →
“The line means division.” It means implication: everything above must hold for the thing below to be concluded.Typing Rules: Reading the Notation With the Line Through It →
“The rules tell you how the checker works.” They tell you what is *valid*. The algorithm is a separate artifact and, for anything with subtyping or inference, a substantially different one.Typing Rules: Reading the Notation With the Line Through It →
“Γ is a global symbol table.” It is a per-judgment assumption set that grows as you descend into binding forms and shrinks as you leave them. That scoping *is* lexical scope.Typing Rules: Reading the Notation With the Line Through It →
“If I can write the rule, I can implement it.” Writing T-Sub takes one line. Implementing a checker with subtyping that terminates, is complete, and gives good errors is a research-scale problem in some type systems.Typing Rules: Reading the Notation With the Line Through It →
“The typed AST and the derivation are different things.” They are the same tree; the derivation additionally records which rule justified each node, and most compilers do not bother to store that.Typing Rules: Reading the Notation With the Line Through It →
“Γ is the symbol table.” It is a projection of it: the name-to-type map that the typing rules need. The symbol table also carries storage, mutability, spans and visibility, none of which appear in a rule.The Type Environment: What Γ Is, and Where the Compiler Keeps It →
“The turnstile means implies.” It separates assumptions from a claim. The implication in a rule runs vertically, across the line; the turnstile runs horizontally, within one judgment.The Type Environment: What Γ Is, and Where the Compiler Keeps It →
“Extending Γ modifies it.” In the notation it does not — Γ, x : T is a new environment. Whether the implementation mutates a stack or allocates a new map is a choice with consequences, and confusing the two is how scope-leak bugs get written.The Type Environment: What Γ Is, and Where the Compiler Keeps It →
“Undefined variable is a type error.” It is a name-resolution error, produced before the checker runs. They present similarly and come from different phases with different fixes.The Type Environment: What Γ Is, and Where the Compiler Keeps It →
“Γ exists at runtime.” Nothing named survives to runtime unless the compiler deliberately emitted it as debug metadata — [[debug-information]].The Type Environment: What Γ Is, and Where the Compiler Keeps It →
“Inference means the language figures out what I meant.” It finds a type consistent with the rules. If several are consistent it applies a defaulting rule, and defaulting is where the surprises live.Type Inference: Leaving the Type Off →
“Rust’s let works like C++’s auto.” It does not. auto is fixed by the initializer; Rust solves the whole body, so a later use can change the answer.Type Inference: Leaving the Type Off →
“Fewer annotations means better inference.” Better inference means the error lands where the mistake is. Those goals are in tension, and mainstream languages chose locality.Type Inference: Leaving the Type Off →
“Adding an annotation cannot change behaviour.” In any language with defaulting or literal-type widening, it can and does — the width of an integer, the mutability of an array literal, whether a TypeScript const keeps its literal type.Type Inference: Leaving the Type Off →
“Full inference is too hard to implement.” It is a well-understood algorithm of modest size. What is hard is producing a good error message from it.Type Inference: Leaving the Type Off →
“Hindley–Milner means no annotations are ever needed.” It means none are needed for the fragment it covers. Polymorphic recursion, higher-rank types and ambiguous class constraints all require them, in every language that offers them.Hindley–Milner: Inference Without a Single Annotation →
“Rust uses Hindley–Milner.” Rust requires signatures, has lifetime subtyping and has trait-based overloading. It uses unification inside a body; that is not the same claim.Hindley–Milner: Inference Without a Single Annotation →
“The value restriction is a compiler limitation.” It is a soundness requirement. Without it you can read an int as a string through a single mutable cell, with no unsafe construct anywhere.Hindley–Milner: Inference Without a Single Annotation →
“Generalizing more would accept more programs.” Generalizing a variable that is free in the environment accepts *unsound* programs. The condition is not conservatism, it is the correctness argument.Hindley–Milner: Inference Without a Single Annotation →
“HM is slow because it is exponential.” It is near-linear on human-written code. The exponential case requires deliberately nested lets and shows up in generated code, not in the average module.Hindley–Milner: Inference Without a Single Annotation →
“The occurs check is an optimization.” It is a correctness requirement. Without it the solver produces a cyclic term and a later phase does not terminate.Unification, and Why `T = List<T>` Must Fail →
T = List<T> is just a recursive type, and languages have those.” Recursive *type definitions* are fine — type List<T> = Nil | Cons(T, List<T>) names the recursion behind a constructor. An unguarded equation between a variable and a term containing it does not; the difference is iso-recursive versus equi-recursive types.Unification, and Why `T = List<T>` Must Fail →
“Unification handles subtyping if you are careful.” It solves equalities. Subtyping needs inequalities and a lattice, and that is a different algorithm with different complexity.Unification, and Why `T = List<T>` Must Fail →
“The error message tells me where the bug is.” It tells you where the first contradiction was found. Those coincide only when annotations bounded the search.Unification, and Why `T = List<T>` Must Fail →
“Unification is a type-system concept.” It is a general term-solving algorithm — Prolog resolution, pattern matching, template instantiation and generic call resolution are all the same machinery.Unification, and Why `T = List<T>` Must Fail →
“Generics are about avoiding code duplication.” That is the motivation. The guarantee is that a parametric function cannot depend on the type, and that guarantee is what erasure, refactoring safety and free theorems all rest on.Parametric Polymorphism and the Theorems You Get Free →
identity returns its argument by convention.” In a parametric language it is the only thing it can do. There is no other value of type T in scope.Parametric Polymorphism and the Theorems You Get Free →
“C++ templates are C++’s generics.” They are a code generation facility with similar syntax. Specialization and if constexpr mean a template’s signature constrains nothing about its body.Parametric Polymorphism and the Theorems You Get Free →
“Type erasure means generics are fake.” Erasure is what makes the guarantee enforceable at zero runtime cost. Reified generics are the ones that trade the guarantee away.Parametric Polymorphism and the Theorems You Get Free →
“Adding a constraint makes the generic stronger.” It makes it more capable and strictly weaker as a theorem, and it pushes a requirement onto every caller.Parametric Polymorphism and the Theorems You Get Free →
“Overloading and generics are the same feature.” One selects among several bodies; the other has one body. The distinction determines what the signature proves and what the compiler emits.Ad-Hoc Polymorphism: One Name, Different Code →
“Type classes are just interfaces.” Interfaces are declared by the type’s author; instances can be declared by anyone, for types they do not own. That difference is why trait-based ecosystems compose.Ad-Hoc Polymorphism: One Name, Different Code →
“Ad-hoc polymorphism is resolved at runtime.” Overloading, trait selection and instance resolution are all static in the mainstream designs. Virtual dispatch and dynamic protocols are the runtime cases, and they are a subset.Ad-Hoc Polymorphism: One Name, Different Code →
“Monomorphization is what generics mean.” It is one of three strategies. Haskell compiles one body and passes a dictionary; Java erases; Rust monomorphizes. The source looks similar and the binaries do not.Ad-Hoc Polymorphism: One Name, Different Code →
“Operator overloading is bad.” It is a trade. Matrix, money and duration types are better with it; a << that means “write to a stream” is worse with it. Say which case you are in.Ad-Hoc Polymorphism: One Name, Different Code →
“Subtyping means inheritance.” Inheritance is one way to establish the relation. Records, unions, intersections, top and bottom types and Rust lifetimes all produce it without any class involved.Subtyping: What `Dog <: Animal` Licenses →
“If it compiles, substitution is safe.” The compiler checked the signatures. Preconditions, postconditions and invariants were checked by nobody, and Square extends Rectangle is the standing counterexample.Subtyping: What `Dog <: Animal` Licenses →
“An upcast is always free at runtime.” On a single-inheritance reference, yes. Under multiple inheritance the pointer must be adjusted, and an interface upcast in Go or Java may allocate or build an interface table.Subtyping: What `Dog <: Animal` Licenses →
“Widening a parameter type is always backward compatible.” Not for an override, where it silently produces an overload and the base implementation runs instead.Subtyping: What `Dog <: Animal` Licenses →
int <: float because an int can be used where a float is expected.” That is a conversion with a representation change and possible precision loss, not subsumption. Subsumption never changes the value.Subtyping: What `Dog <: Animal` Licenses →
List<Dog> should obviously be a List<Animal>.” Only if you never write to it. The moment you can add a Cat, the widening is unsound, and Java’s arrays demonstrate exactly what goes wrong.Variance: Why `List<Dog>` Is Not a `List<Animal>` →
“Contravariance is a rare edge case.” It is the rule for every function parameter, which means it applies to every callback, comparator, event handler and visitor you pass anywhere.Variance: Why `List<Dog>` Is Not a `List<Animal>` →
ArrayStoreException means someone made a mistake with casts.” It means someone used array covariance, which the language permits without a cast or a warning.Variance: Why `List<Dog>` Is Not a `List<Animal>` →
“Variance is a Java wildcards problem.” Java made it visible. The rule applies in Kotlin, C#, Scala, Rust and TypeScript, and each solved it differently.Variance: Why `List<Dog>` Is Not a `List<Animal>` →
“Rust has no variance because you never write it.” Rust has full variance rules, inferred structurally. &mut T being invariant in T is why a great many borrow errors happen.Variance: Why `List<Dog>` Is Not a `List<Animal>` →
"string | number means it can do both string and number things." It can do neither, unrefined. That description belongs to A & B, which is the opposite construction.Union Types →
"The checker forgot my check, so it is broken." It discarded a refinement it could no longer justify — almost always because a call, a closure or a reassignment could have changed the value between the check and the use. The fix is to bind the narrowed value to a const, not to assert.Union Types →
"A union is a supertype of its members." It is a supertype in the assignability sense, and that is precisely why it offers fewer operations, not more. Reading it as a common base class predicts the wrong error messages.Union Types →
"Adding a member to a union is a safe, additive change." It is a breaking change for every consumer that exhaustively handled the old set, which is the same versioning problem as adding an enum value to a public API.Union Types →
"A & B means A or B, since it combines them." It means both at once. The value set shrinks; only the member list grows.Intersection Types →
"An impossible intersection is a compiler bug." It is a well-formed type with no inhabitants. Every type system worth using has one, and [[exhaustiveness-checking]] depends on it.Intersection Types →
"Intersection is the same as extending both interfaces." It is structurally similar and nominally nothing: no declaration is created, no instanceof relationship exists, and no runtime artifact is emitted.Intersection Types →
"If the type compiles, some value has it." Nothing checks inhabitation at the declaration. The first construction attempt is the check, and if nobody ever constructs one, nothing is ever checked.Intersection Types →
"Algebraic just means fancy enums." It means the type constructors form an algebra with addition and multiplication, and the arithmetic is a working tool for finding over-permissive data models, not a metaphor.Algebraic Data Types →
"A sum type is a union type." A sum is closed, tagged and nominal — declared once, with constructors. A union is open, untagged and structural — composable anywhere. See [[union-types]] for what falls out of that difference.Algebraic Data Types →
"Result is just a wrapper, exceptions do the same thing more conveniently." Exceptions are control flow and cannot be stored, collected, returned alongside a normal value, or transformed as data. That is the whole difference and it shows up the moment you want a list of results.Algebraic Data Types →
"Optional fields are the same thing with less ceremony." They admit combinations that the sum does not. The ceremony is the checking, and removing it is removing the guarantee.Algebraic Data Types →
"match is just a nicer switch." A switch compares a value against constants. A match tests a tree of constructors, binds payloads, and is checked for coverage — the last of which is the part that changes how code ages.Pattern Matching →
"My two guards cover everything, so the compiler should accept it." The compiler cannot evaluate your guards, and a checker that assumed guards were complete would be unsound. Move the distinction into the pattern if you want it checked.Pattern Matching →
"Arms are tried top to bottom, so I know exactly which comparisons run." You know the observable order of guards and arm bodies. Which tag tests the compiler performs, and in what order, is a code-generation decision.Pattern Matching →
"Python has pattern matching now, so it has the same guarantees." It has the patterns and not the closed variant set, so a missing case is silent. The guarantee came from the type system, not from the syntax.Pattern Matching →
"The compiler checks my switches are complete." It checks the ones it can, over closed sets, in languages that do it. In TypeScript it checks the ones where you asked; in Python it checks none.Exhaustiveness Checking →
"Adding an enum variant is a backwards-compatible change." It breaks every exhaustive consumer, which is the check working. Treating it as additive is how libraries break downstream builds in minor releases.Exhaustiveness Checking →
"A wildcard arm is a reasonable default." It is the off switch for this feature at that site. Sometimes correct, never automatic, and a bare _ => {} added to silence an error is almost always the wrong one.Exhaustiveness Checking →
"Exhaustiveness means the code handles every case correctly." It means every case is mentioned. An arm that returns a wrong value passes the check exactly as well as one that returns the right one.Exhaustiveness Checking →
"Option is safer than nullable." Within the code both cover, they catch the same mistake. They differ at boundaries, in composition, and in what a partially migrated codebase looks like — which is where the decision actually lives.Nullability and Optional Types →
"Option costs a word of memory." Often it costs nothing, because the compiler folds the tag into a bit pattern the payload cannot use. Sometimes it costs eight bytes. Check the type rather than assuming either.Nullability and Optional Types →
"The checker said it is not null, so it cannot be null." The checker reasoned about the code it compiled. A value from JSON, from reflection or from an unannotated module obeys no such rule.Nullability and Optional Types →
"! and .unwrap() are just noise you add to satisfy the compiler." They are assertions that a proof exists which the compiler could not find. If you cannot state the proof, the assertion is a scheduled failure.Nullability and Optional Types →
"TypeScript is a typed language, so type errors cannot reach production." The checker verifies the code you compiled. Every value entering from outside is unchecked, and any disables checking for everything it touches.Gradual Typing →
"any just means I have not written the type yet." It means the checker will accept this value anywhere and accept anything here. unknown is the type that means what people think any means.Gradual Typing →
"Casting with as converts the value." Nothing is converted and nothing is emitted. It is a statement to the checker, and if it is false, the type is simply wrong from then on.Gradual Typing →
"Runtime checking would be strictly better; TypeScript just did not bother." Sound gradual typing has a well-measured performance problem at partially typed boundaries. The choice was made with the evidence available, and the cost of the other option is not small.Gradual Typing →
"Structural typing is duck typing." Duck typing resolves at runtime and fails at the call. Structural typing decides statically, before anything runs; the resemblance is in the compatibility rule, not in when it is applied.Structural vs Nominal Typing →
"If it has the right fields, it is the right type." That is the definition in a structural system and false in a nominal one, and it is exactly why an invariant cannot live in a structural type name.Structural vs Nominal Typing →
"Branding adds a runtime tag." Nothing is emitted. The brand is a member the checker believes in and no value ever has, which is why the cast that mints one is unavoidable.Structural vs Nominal Typing →
"Nominal typing is just structural typing with extra steps." It answers a different question — declared identity rather than shape — which is why it can be decided in constant time and why it can be trusted to carry an invariant.Structural vs Nominal Typing →
"Sound means my program cannot crash." It means the model has no stuck states. Panics, exceptions and aborts are all defined steps, and every sound language has them.Type Soundness →
"An unsound type system is a broken type system." Several of the most widely used systems are unsound deliberately, in exchange for compatibility with code that already existed. The question is whether the hole is checked or accepted.Type Soundness →
"If it type-checks, the optimizer can trust it." Only if the system is sound with respect to a model that includes what the optimizer is assuming. That gap is where type holes turn into miscompilations.Type Soundness →
"Rust is sound, so unsafe is fine as long as it compiles." unsafe transfers the obligation to you. The safe code around it is sound conditional on your having discharged it, and nothing checks that you did.Type Soundness →
"Java generics are fake." They are fully checked at compile time and the checking is real. What is absent is the runtime representation, which is a lowering decision, not an absence of typing.Type Erasure and Reification →
"Erasure is a mistake nobody would repeat." It was the only strategy compatible with existing bytecode, and every language retrofitting generics onto a deployed runtime faces the same choice. TypeScript made the same one for the same reason.Type Erasure and Reification →
"Reified generics are strictly better." They cost runtime complexity and per-instantiation JIT work, and they were only possible because the CLR could be changed. Strictly better in isolation; not available to Java in 2004.Type Erasure and Reification →
"The cast the compiler inserts is redundant, so it is free." It is an instruction that executes. The JIT frequently removes it and is not obliged to, and in interpreted or cold code it is genuinely there.Type Erasure and Reification →
"Monomorphization makes code faster." It makes each call site better and the program larger. Whether that is a net win depends on how hot the sites are and whether the working set still fits in cache.Monomorphization →
"The compiler generates code for every possible type." It generates code for every type the program actually uses, discovered by walking the call graph from the roots. Unused instantiations cost nothing.Monomorphization →
"Generics are free in Rust." Free at runtime for the calls that get specialised and inlined; not free in binary size, compile time or instruction cache, and those costs are the ones people are surprised by.Monomorphization →
"Go has generics now, so it has the same performance story as Rust." Go compiles one body per GC shape and dispatches through a dictionary, which is a deliberately different point on the spectrum with a deliberately different cost profile.Monomorphization →
"The borrow checker prevents memory leaks." It does not, and leaking is explicitly safe. A reference cycle under reference counting leaks with the checker entirely satisfied; what is prevented is use-after-free, not failure to free.Ownership Types →
"Ownership is about memory." Memory is the flagship application. The mechanism is a resource protocol encoded in types, and locks, files and transactions use exactly the same machinery.Ownership Types →
"If it compiles, it is correct." It is free of one class of error, in the checked region, conditional on every unsafe block having upheld its obligations. Logic errors, deadlocks and leaks are all untouched.Ownership Types →
"The compiler is being unnecessarily strict." Frequently it is being strict about a program that happens to be correct, because the rule is a conservative approximation. That is a real cost of the design, not a misunderstanding on your part.Ownership Types →
"A lifetime annotation makes the value live longer." It makes a claim the checker must verify. Nothing is extended; if the claim cannot be justified the error simply moves.Lifetime Analysis →
"The lifetime is how long the data lives." It is the region over which the *reference* must be valid, which is normally much shorter. Confusing the two makes elision and variance unintelligible.Lifetime Analysis →
"The borrow ends at the closing brace." It ends at the last use, and has since NLL. Reasoning in scopes will predict rejections that no longer happen.Lifetime Analysis →
"Lifetimes cost something at runtime." They are erased before code generation. The cost is compile time and author effort, and there is no runtime residue at all.Lifetime Analysis →
"Effect systems are academic and nothing uses them." Checked exceptions, async, const, constexpr and unsafe are all partial effect systems in daily production use.Effect Systems →
"Function colouring is a design mistake." Propagation is the feature — a signature that did not change when the behaviour changed would be worthless. The mistake is the missing polymorphism that turns propagation into duplication.Effect Systems →
"Async is different from checked exceptions." They are the same mechanism: an effect declared on a function, propagated to callers, discharged at a boundary. The syntax differs and the shape does not.Effect Systems →
"Tracking purity is only useful for functional programming." It is what licenses hoisting, elimination and compile-time evaluation, which is why C compilers ship an unchecked, undefined-behavior-flavoured version of it.Effect Systems →
"The IR is what the CPU runs." Nothing runs the IR. It is a data structure inside a compiler process that has usually exited before your program starts.What an Intermediate Representation Is →
"The IR is just simplified assembly." Assembly commits to a register file, an instruction set and a calling convention. The IR commits to none of them, and a middle-end that assumes any of them is broken for the second target.What an Intermediate Representation Is →
"There is one IR." Most production compilers have three or four at different levels, and a pass is written against exactly one of them — [[ir-levels]].What an Intermediate Representation Is →
"Unoptimized IR shows what the machine will do." Unoptimized IR is mostly memory traffic the source never requested. It shows what the *frontend* emitted, which is a different claim.What an Intermediate Representation Is →
"LLVM is a compiler." LLVM is a middle-end and a collection of backends. Clang is the C and C++ compiler that uses it, and the distinction is exactly the M + N argument — [[llvm-architecture]].Why an IR Exists: M x N Becomes M + N →
"A shared IR means all languages compile to the same code." It means they share the same optimizer and code generators. What each frontend *emits* into that IR differs enormously, and that is where language differences survive.Why an IR Exists: M x N Becomes M + N →
"The IR is neutral, so nothing is lost." The IR is neutral because things are lost. That is the mechanism, not a side effect.Why an IR Exists: M x N Becomes M + N →
"M + N is always better than M x N." Not when M and N are both one. The arithmetic only favours the shared IR once you actually have several of something.Why an IR Exists: M x N Becomes M + N →
"More IRs means a slower compiler." It means more compile-time work per function and often *better* diagnostics and faster generated code. Whether it is a net loss depends entirely on which you are paying for.Levels of IR: High, Mid and Low →
"HIR, MIR and LLVM IR are just names for the same thing at different times." They are different data structures with different node sets. A pass written against one does not compile against another.Levels of IR: High, Mid and Low →
"The lowest level is the most accurate." It is the most target-specific. It is also the level at which the question "what did the programmer write" has no answer at all.Levels of IR: High, Mid and Low →
"You should optimize as early as possible." You should optimize at the level where the transformation is expressible and its precondition is checkable. Doing it early on a rich representation often means handling many more cases for the same result.Levels of IR: High, Mid and Low →
"The temporaries are wasteful — the compiler is generating bad code." They are how the compiler generates good code. Every one of them is deleted or assigned a register before anything is emitted.Three-Address Code →
"Three-address code means at most three operands." It means at most three addresses in the classical arithmetic form. Calls and phi nodes have more, in every real IR.Three-Address Code →
"Three-address code is SSA." It is not. SSA additionally requires that each name is assigned exactly once, which three-address code does not — x = x + 1 is perfectly legal three-address code.Three-Address Code →
"The evaluation order in the dump is the order the CPU will use." It is the order the compiler committed to. Later passes may reorder anything unobservable, and the hardware reorders again beneath that.Three-Address Code →
"Lowering is just code generation." Code generation is the last lowering. There are many before it, and most of them produce IR rather than instructions.Lowering →
"Lowering makes the program smaller." It nearly always makes it larger. One construct becomes several instructions; that is the trade being made.Lowering →
"If it lowers to the same thing, it performs the same." Only if the lowering happens before the passes that matter. The same source construct lowered at two different points in the pipeline can produce very different code — [[phase-ordering]].Lowering →
"Desugaring and lowering are different operations." They are the same operation at different levels. The useful distinction is when it happens and what it costs diagnostics, not what it is called.Lowering →
"LLVM IR is the right design and the others are compromises." LLVM IR is a design for a multi-language ahead-of-time compiler where compile time is secondary. Cranelift is a design for compiling on a request path. Neither would do the other's job well.Designing an IR: The Four Decisions →
"A typed IR prevents miscompilation." It prevents ill-typed IR. A pass can build a perfectly well-typed instruction that computes the wrong thing, and no verifier will notice.Designing an IR: The Four Decisions →
"Sea-of-nodes is strictly more powerful." It makes reordering free and everything else harder. The teams with the most experience of it have been moving away from it, which is the relevant evidence.Designing an IR: The Four Decisions →
"SSA is a property of the IR, so I can just declare it." It is an invariant that construction must establish and every pass must maintain, and it must be checked, or it is a claim rather than a property.Designing an IR: The Four Decisions →
"The verifier proves the compiler is correct." It proves the IR is well-formed. A pass that computes the wrong value while maintaining every invariant passes cleanly.IR Verification →
"If the verifier passes, the program will run correctly." The verifier says nothing about the program. It says the compiler has not corrupted its own data structure.IR Verification →
"Verification is a testing technique." It is a runtime assertion over a data structure, and it runs on every compilation in a debug build — it catches things no test case anticipated.IR Verification →
"We can turn it on later." Later, the invariants have already been violated in places that depend on the violation, and turning it on produces hundreds of failures nobody has time to triage.IR Verification →
"LLVM is a compiler." LLVM is a middle-end and a set of code generators. Clang is the C and C++ compiler built on it, and confusing the two loses the entire point of the arrangement.Many Frontends, One Backend →
"All LLVM languages produce similar code." They share the optimizer, not the IR they feed it. What each frontend emits differs enormously, and that is where language performance differences mostly live.Many Frontends, One Backend →
"If a language uses LLVM it inherits every LLVM target." It inherits the code generators. Whether the target works depends on runtime support — unwinding, threading, GC — that the backend has nothing to do with.Many Frontends, One Backend →
"Using LLVM means you cannot have language-specific optimization." Swift and Rust both have substantial language-specific optimizers, above LLVM, on their own IRs. Using a shared backend does not preclude a private middle-end.Many Frontends, One Backend →
"The CFG shows the order the program runs in." It shows every order the program *could* run in. A single execution is one path through it, and which path depends on data the graph does not contain.The Control-Flow Graph →
"Blocks correspond to source statements." Blocks correspond to straight-line runs of instructions. One source statement can span several blocks — any statement containing a short-circuit operator does — and one block can hold many statements.The Control-Flow Graph →
"An edge means the target runs next." It means it may run next. A conditional branch has two edges out and takes exactly one of them per execution.The Control-Flow Graph →
"The CFG is a DAG." It is a DAG only for a loop-free function. Loops make it cyclic, and that cyclicity is why dominance has to be computed iteratively.The Control-Flow Graph →
"A basic block is a source statement." A statement with a short-circuit operator or a conditional expression spans several blocks, and a run of simple statements is one block. There is no correspondence.Basic Blocks →
"A block always ends at a call." Only when the call may not return to the next instruction. In a language without exceptions, an ordinary call sits comfortably in the middle of a block.Basic Blocks →
"Fewer blocks is faster code." Block count is a property of the compiler's representation. Splitting a block emits no instructions and changes nothing about what runs.Basic Blocks →
"Blocks are the same thing as scopes." Scopes are lexical and nest; blocks are execution regions and form a graph. A single scope routinely contains dozens of blocks — [[lexical-scope]].Basic Blocks →
"The join block exists because the source had a statement after the if." It exists because control has to come back together. AtlasLang creates a join for every if whether or not anything follows, and prunes it later if nothing reaches it.Building the CFG →
"A back edge is an edge that goes upward in the listing." It is an edge whose target dominates its source. Block ordering in a listing is a printing convention and can be changed without changing the graph.Building the CFG →
"Critical edges are rare." Every if without an else produces one, and so does every short-circuit && or ||. They are among the most common shapes in real code.Building the CFG →
"Splitting an edge changes the program." It inserts a block containing a jump, executing on exactly the paths the edge did. It is one of the few transformations that is unconditionally safe.Building the CFG →
"A back edge is one that goes upward in the listing." It is an edge whose target dominates its source. Listing order is a printing convention and can be permuted freely without changing a single loop.Natural Loops →
"Every cycle is a loop." Only cycles with a single entry are natural loops. An irreducible cycle is a cycle and is not a loop by this definition, which is exactly why compilers decline to optimize it.Natural Loops →
"The loop body is everything between the header and the latch in the listing." It is everything that reaches the latch without passing the header, which is a graph property. Blocks printed in between may not be in the loop at all.Natural Loops →
"Finding loops requires knowing the source had a loop." It requires dominance and nothing else. A loop built by a previous optimization pass is found identically to one the author wrote.Natural Loops →
"A dominates B means A runs before B." It means that *if* B runs, A already ran. A may dominate B and neither may run at all, if the function returns first.Dominators →
"The immediate dominator is the predecessor." Only when there is exactly one predecessor. A join block's immediate dominator is usually a block above the branch, several steps up the graph — as b3's is b0 in the diamond above.Dominators →
"Dominance can be computed in one pass." Only for acyclic graphs. A back edge means a predecessor is unsettled when its successor is visited, and the algorithm has to come back.Dominators →
"If A dominates B, everything A computed is available at B." Only if nothing between them redefined or invalidated it. Dominance establishes that A executed, not that its results survived.Dominators →
"The dominator tree is a spanning tree of the CFG." It is not. Its edges frequently connect blocks that have no CFG edge between them — the join of a diamond is a child of the block above the branch, which is two CFG steps away.The Dominator Tree →
"A block's parent in the tree is its predecessor." Only when it has exactly one predecessor. Merge points have parents further up.The Dominator Tree →
"The tree has cycles if the CFG does." It never has cycles. A back edge targets an ancestor, and ancestors do not become children.The Dominator Tree →
"If I have the tree I do not need the CFG." The tree answers guarantee questions. Path questions, reachability and loop detection all need the graph.The Dominator Tree →
"The frontier of A is the blocks A can reach." It is the boundary — the blocks A can reach but does not dominate, whose predecessor on that path A does dominate. A can reach far beyond its frontier.The Dominance Frontier →
"Phis go at every merge point." They go where a *definition* stops dominating. A merge point where nothing relevant was redefined needs no phi, which is why the frontier and not the predecessor count is the rule.The Dominance Frontier →
"A block cannot be in its own dominance frontier." A loop header always is, because the back edge reaches it from a block it dominates. That self-reference is what places the loop-carried phi.The Dominance Frontier →
"One pass of frontier placement is enough." A phi is a definition, so placement must iterate. Missing the iteration is precisely how loop-carried phis go missing.The Dominance Frontier →
"SSA means the variable is assigned once, so the language must be immutable." No — the *IR value* is written once. The source variable is reassigned as often as the programmer wrote it; each reassignment simply gets its own name.Static Single Assignment →
"A loop breaks SSA, because the induction variable changes every iteration." It changes dynamically, which SSA permits. What a loop actually needs is a phi at the header, and that is a notation, not an exception.Static Single Assignment →
"If I see store in the IR dump, it is not SSA." Stores to memory are perfectly legal in SSA; the invariant constrains virtual registers, not memory. What promotion removes is stores to *promotable slots*.Static Single Assignment →
"SSA is an optimization." It is a representation. It performs no transformation on its own — every listing in this lesson computes exactly what the input computed.Static Single Assignment →
"The phi picks whichever operand is not undefined." It has no way to test that. It selects by *edge*, and both operands are perfectly defined values on their own paths.Phi Functions →
"A phi is a conditional move." A conditional move evaluates a condition at the point it runs. A phi refers to how control arrived, which is information the machine has already thrown away.Phi Functions →
"Phis cost instructions at run time." They cost copies, and often not even that — a good allocator coalesces the phi destination with its operands so that the copies disappear entirely.Phi Functions →
"If a phi's operands are all the same value, construction made a mistake." Construction deliberately does not check. Placement is decided by dominance frontiers alone, and removing the trivial ones is copy propagation's job — see [[ssa-construction]].Phi Functions →
"A phi goes wherever two paths merge." Wherever two paths merge *and* the variable has definitions on more than one of them, reachable through the frontier. Plenty of merges need no phi for a given variable.Constructing SSA →
"The algorithm figures out which values differ." It never looks at a value. That is why identical-operand phis come out of it and why the pass is a pure function of the CFG.Constructing SSA →
"Renaming is just walking the blocks in order." Block order is not the dominance order. Walking the block list works by accident on straight-line code and produces wrong scoping the moment there is a branch.Constructing SSA →
"mem2reg is an optimization pass." It is a representation change. It happens to delete memory traffic, but its purpose is to make everything after it possible.Constructing SSA →
"SSA makes the code faster." SSA makes analyses cheaper. The code gets faster because the compiler can afford more passes, which is a different causal chain and matters when you are deciding whether to pay for construction.Why SSA Helps →
"If there are no uses, it can be deleted." Only if it also has no side effect and cannot trap. This is the single most dangerous simplification in the lesson.Why SSA Helps →
"SSA gives you alias analysis for free." It gives you nothing at all about memory. Loads and stores have no def-use edges without a separate memory representation.Why SSA Helps →
"Def-use chains and use-def chains are the same thing." They are the two directions, and SSA makes the use-def direction trivial — one definition — while the def-use direction still needs a list.Why SSA Helps →
"Out-of-SSA is just deleting the phis." Deleting them is the last step. Everything before it is deciding where the copies go and in what order, and that is where the bugs are.Leaving SSA →
"A parallel copy is just several copies." It is several copies that all read the pre-copy state. Turning it into a sequence is a real algorithm with a cycle case, and the sequence is not unique.Leaving SSA →
"Save the value before it gets overwritten" — and then saving the source. The value at risk is the destination, because the source is still readable until something writes it, and the sequencing rule guarantees nothing does before its last reader.Leaving SSA →
"Critical edges are a theoretical concern." A single if with no else produces one. They are everywhere, and the reason they are usually invisible is that backends split them as a matter of routine.Leaving SSA →
"All these copies make SSA expensive at run time." Coalescing removes most of them. What is left is small, and measurable — look at the two llc dumps above rather than guessing.Leaving SSA →
"Pruned SSA is a different IR." It is the same IR with fewer phis. Any pass that works on minimal SSA works on pruned SSA unchanged.SSA Variants →
"Minimal SSA means the minimum possible number of phis." It means minimal under the dominance-frontier criterion. Pruned SSA has fewer, and is also correct.SSA Variants →
"A phi with one operand is a bug." In a loop exit block it is almost certainly LCSSA, deliberately placed to keep every escaping use rewritable in one place.SSA Variants →
"Gated SSA is what LLVM uses." LLVM uses ordinary phis and computes control dependence separately. Gated SSA is mostly a research form.SSA Variants →
"Data-flow analysis tells you what the program does." It tells you what is true on *all* paths, over-approximated. It is deliberately less precise than the program, and the imprecision is what makes it terminate.The Data-Flow Framework →
"Each analysis is its own algorithm." Each analysis is four choices handed to the same solver. Recognising this is most of what makes a new analysis quick to write.The Data-Flow Framework →
"If the analysis says a value might be live, it is live." It says it cannot prove otherwise. Over-approximation in the safe direction is the design, not a weakness.The Data-Flow Framework →
"SSA replaced data-flow analysis." SSA replaced several instances of it for value questions. Liveness is still a backward data-flow analysis in every backend, including SSA ones.The Data-Flow Framework →
"It terminates because the program is finite." The program being finite is necessary and not sufficient — the *lattice* must have finite height. An interval analysis on a finite program does not terminate without widening.Iterating to a Fixed Point →
"Choosing a better visit order gives a better answer." It gives the same answer sooner. If a different order changes your result, the transfer functions are not monotone and you have a bug.Iterating to a Fixed Point →
"The worklist algorithm is a different analysis." It is a different schedule for the same equations.Iterating to a Fixed Point →
"One pass over the blocks is enough if I order them well." Not with a back edge. A loop carries information from the bottom of the body to the top, and no order of blocks can visit a block before itself.Iterating to a Fixed Point →
"Backward analyses are harder." They are the same solver with succs instead of preds. What is harder is remembering that there may be several exit blocks.Forward and Backward Analysis →
"Forward means following the instruction order." It means following control-flow edges from predecessors. In a loop that is not the listing order, and assuming it is produces a solver that works on straight-line code only.Forward and Backward Analysis →
"An analysis can be run either way and you pick the more precise one." Running it the other way answers a different question. Precision does not enter into it.Forward and Backward Analysis →
"Liveness could be forward if I tracked definitions instead." Tracking definitions forwards is reaching definitions, which is a different analysis with a different answer. It cannot tell you whether anything will read the value.Forward and Backward Analysis →
"Reaching definitions tells you which assignment actually ran." It tells you which ones *could* have. Deciding which one did is a run-time question.Reaching Definitions →
"If the set has one element, the value is that definition's value." Only if the definition's own value has not since changed — which is guaranteed in SSA and needs proving otherwise.Reaching Definitions →
"SSA makes reaching definitions unnecessary." For register values, yes. For memory it is as necessary as ever, and that is where the hard optimizations are.Reaching Definitions →
"An empty reaching set means dead code." It means the variable has no definition reaching that use — an uninitialised read, which is a very different diagnosis.Reaching Definitions →
"A value is live from its definition to its last use." That is the live *interval*, an approximation. A value is live only where a future use actually needs it, and the difference is the holes.Liveness Analysis →
"A value not mentioned in a block is dead there." The loop above is the counterexample: %0 is live in a block that never names it, because a later block will read it.Liveness Analysis →
"SSA gives you liveness for free." SSA gives you use lists. Liveness is about program points, and an SSA backend still runs a real analysis — though it can use dominance to do it more cheaply.Liveness Analysis →
"If liveness is over-approximate, the code is just slower." Over-approximate is safe. Under-approximate is a corrupted value with no diagnostic, and the whole design of the analysis is arranged to err the safe way.Liveness Analysis →
"The expression appears twice, so the second one is redundant." Only if it is available — computed on every path, with no operand changed since, and the value still held.Available Expressions →
"Available means the value is in a register." It means the value has been computed. Keeping it somewhere is a separate obligation, and it is the obligation that costs register pressure.Available Expressions →
"CSE always makes the code faster." It removes instructions and adds pressure. Under a tight register budget the exchange can go the wrong way, and measuring is the only way to know.Available Expressions →
"SSA makes this analysis unnecessary." It replaces it for register expressions, where dominance gives availability. Memory still needs it, and memory is where most real redundancy lives.Available Expressions →
"Constant propagation and constant folding are the same pass." Folding evaluates operations on literals; propagation makes operands into literals. Running only one of them leaves most of the work undone.Constant Propagation →
"If the value is a constant, the compiler will fold it." Only if the compiler can prove it is that constant on every path, the operation cannot trap, and folding it agrees with the target. Any of the three can fail — and stating it as a certainty is exactly the kind of claim this domain requires a caveat for.Constant Propagation →
"SCCP is just a faster constant propagation." It is also strictly more precise, because it tracks reachability at the same time and therefore ignores assignments in blocks it has not proved reachable.Constant Propagation →
"A variable assigned two different constants is not constant, so nothing can be done." Nothing can be done *by this analysis*. Path duplication, specialisation or a run-time guard can each recover it, at a cost.Constant Propagation →
"The compiler evaluates any expression it can see the inputs of." Only if it can also prove the evaluation cannot fault and that its own arithmetic matches the target's. Division, floating point and anything calling into a library are all places it declines.Constant Folding →
"Folding and constant propagation are the same pass." They are separable, and separating them is what makes the pass manager legible: folding evaluates operations on literals, propagation carries a literal definition to its uses. AtlasLang keeps them apart on purpose.Constant Folding →
"If it folded here it will fold there." Folding is enabled by whatever made the operands constant — inlining, propagation, template instantiation — so the same source expression folds in one call site and not in another.Constant Folding →
"Dead code is code you cannot reach." That is unreachable code, which is a different pass — AtlasLang has unreachable-block-elimination for it. Dead code is reachable code whose result nobody uses.Dead Code Elimination →
"If the optimizer removed it, it was useless." It was *unobserved by the model the compiler is allowed to use*. Memory-mapped registers, other threads and secret material are all observers the model does not include unless you tell it.Dead Code Elimination →
"The compiler will not delete my function call." It will, the moment it can prove the callee is pure — which after inlining and interprocedural analysis is often. The uncertainty is the point: rely on an annotation or a barrier, not on an assumption.Dead Code Elimination →
"The compiler will not compute the same thing twice." It will, whenever it cannot prove the operands are unchanged — which for anything reading memory is most of the time.Common Subexpression Elimination →
"CSE always makes the code faster." It converts computation into a live value. If that value spills, the trade is arithmetic for memory traffic and the code is slower.Common Subexpression Elimination →
"The same expression in both arms of an if will be shared." Neither arm dominates the merge, so nothing is shared without first hoisting the expression above the branch — a different transformation with a different legality condition.Common Subexpression Elimination →
"Assignments between variables cost an instruction." At the IR level they usually cost nothing after this pass, and at the machine level they usually cost nothing after coalescing. Writing let tmp = value; for clarity is close to free.Copy Propagation →
"SSA removes the need for this pass." SSA makes the pass trivial to *justify*; the copies still exist and still have to be removed, and leaving SSA creates a fresh batch.Copy Propagation →
"A phi is always a merge." A phi is placed where a merge might be needed. Whether it merges anything is a separate question, answered later, by this pass.Copy Propagation →
"Shifts are faster than multiplies, so I should write shifts." On a current core the multiply is a few cycles, fully pipelined, and the compiler picks the better encoding per target anyway. What you lose is the reader's ability to see that you meant multiplication.Strength Reduction and Algebraic Identities →
"-ffast-math just makes floating point faster." It makes the compiler compile a different language, one where addition is associative and NaN does not exist. Programs that relied on either are now wrong.Strength Reduction and Algebraic Identities →
"x / 2 and x >> 1 are the same." For unsigned, yes. For signed, they differ for every negative odd value, because one truncates toward zero and the other toward negative infinity.Strength Reduction and Algebraic Identities →
"The compiler cannot simplify my float arithmetic because it is not smart enough." It is not permitted to. The identities are false over IEEE-754, and it is being correct rather than timid.Strength Reduction and Algebraic Identities →
"Marking a function inline makes it inline." In C and C++ that keyword is about linkage and multiple definitions. The compiler decides, and always_inline is the attribute that actually directs it.Inlining →
"Inlining removes function-call overhead, so more is better." The overhead removed is small; the size added is not. Past the point where the hot code stops fitting in the instruction cache, more inlining is measurably worse.Inlining →
"A function too big to inline cannot be optimized well." It is optimized normally — it just is not specialized to any one caller. Partial inlining of a hot early-return path is often the better answer anyway.Inlining →
"Small functions are always inlined." Not across translation units without LTO, not through function pointers or virtual calls without devirtualization, and not when the compiler's cost model disagrees with your estimate of small.Inlining →
"Virtual calls are slow because of the extra indirection." The indirection is a few cycles. The cost is the optimization barrier, which is unbounded and does not show up as time spent in the call instruction.Devirtualization →
"The compiler can devirtualize whenever there is only one implementation." Only if it can prove there will only ever be one. With dynamic linking, it cannot, which is why the same code devirtualizes under LTO and not under separate compilation.Devirtualization →
"A JIT devirtualizes because it has better analysis." It has worse analysis and better information — measured receiver types — plus the ability to undo the decision. Deoptimization is the enabling mechanism, not the analysis.Devirtualization →
"Devirtualization made my call direct, so the job is done." A direct call that is not inlined has bought almost nothing. Check -Rpass=inline next.Devirtualization →
"Partial evaluation is an academic idea." It is the mechanism behind template instantiation, monomorphization, JIT type specialization and constexpr — four things most engineers use every week.Partial Evaluation and Specialization →
"Specializing always makes it faster." It makes the residual program faster and the binary larger. Past the instruction cache, the second effect wins.Partial Evaluation and Specialization →
"A JIT specializes because it compiles at run time." It specializes because it can *observe*. Compiling at run time is what makes observation possible, not the benefit itself.Partial Evaluation and Specialization →
"Generics are free abstraction." Monomorphized generics are free at run time and expensive in code size and compile time; erased generics are the opposite. There is no version that is free in both.Partial Evaluation and Specialization →
"The compiler hoists anything that does not change." Only if it can also prove that running it eagerly cannot fault, or that the loop runs at least once. Invariance alone is not sufficient.Loop-Invariant Code Motion →
"Hoisting is always a win." It lengthens a live range across the loop. In a register-starved loop that means a spill, and a spill per iteration costs more than the arithmetic it replaced.Loop-Invariant Code Motion →
"A load that nothing in the loop obviously writes is invariant." Obvious to you is not proof to the compiler: a call it cannot see into, or a store through a pointer it cannot disambiguate, is enough to block it.Loop-Invariant Code Motion →
"Unrolling removes branches, and branches are expensive." A loop-back branch is predicted correctly nearly every iteration. The benefit that survives on modern cores is independence between the duplicated bodies, not the branches.Loop Unrolling →
"More unrolling is more speed." Every factor is a code-size multiplier. There is a maximum past which the instruction cache decides the outcome, and it is reached sooner than most people expect.Loop Unrolling →
"Unrolling by hand gives the compiler less to do." It usually gives the compiler a shape it recognises less well, and freezes a factor tuned for one machine into portable source.Loop Unrolling →
"The compiler unrolled it, so it must be faster." It unrolled it against a cost model with an estimated trip count. Without a profile that estimate is a guess, and measuring is the only check.Loop Unrolling →
"The loops do the same work, so the order does not matter." The arithmetic is the same and the memory traffic is not, and on current hardware the memory traffic is the time.Fusion, Fission, Interchange and Tiling →
"The compiler will reorder my loops if it helps." It will only if it can prove the reordering preserves every dependence, which requires disambiguating array subscripts and pointers. Aliasing and non-affine subscripts block it routinely.Fusion, Fission, Interchange and Tiling →
"Tiling is a compiler optimization." Mainstream compilers do not tile by default at any optimization level. It is done by hand, by a library, or by a polyhedral pass that must be explicitly enabled.Fusion, Fission, Interchange and Tiling →
"Fusion is better than fission." They are opposites and both are optimizations. Which one helps depends on whether the loop is limited by traversal count or by body size.Fusion, Fission, Interchange and Tiling →
"The compiler vectorizes anything parallel." It vectorizes what it can prove is parallel, which is a much smaller set, and only when its cost model agrees.Automatic Vectorization →
"My loop is simple, so it will vectorize." Simplicity in source says nothing about aliasing, trip count or whether a call is hiding in an operator overload. Ask the diagnostics.Automatic Vectorization →
"Vectorization made it slower, so the compiler is wrong." More likely the loop was memory-bound, or its trip count was small, and the cost model guessed the trip count.Automatic Vectorization →
"-ffast-math just enables vectorization." It changes floating-point semantics program-wide. Enabling vectorization is a consequence, and a #pragma omp simd reduction is the local way to get the same effect.Automatic Vectorization →
"The compiler reloads because it is not smart enough." It reloads because it is not *allowed* not to. Reordering across a possible alias would be a miscompilation.Alias Analysis →
"restrict is a hint." It is a promise. If it is false the program is undefined, and nothing checks it.Alias Analysis →
"Strict aliasing is a compiler optimization I can ignore." It is a language rule. Code that violates it is broken regardless of whether today's compiler exploits it.Alias Analysis →
"Casting a pointer and reading through it is fine because it works." It works until a compiler version, an optimization level or an inlining decision changes. That is the defining property of undefined behavior, not evidence of correctness.Alias Analysis →
"Escape analysis means allocation is free in managed languages." It means allocation is sometimes free, when the analysis succeeds, which depends on inlining and on call-site monomorphism you do not control.Escape Analysis →
"If I do not return the object, it will be stack-allocated." Only if nothing else retains it on any path, including inside calls the compiler cannot analyse.Escape Analysis →
"Scalar replacement and stack allocation are the same thing." Scalar replacement removes the object entirely and needs the stronger condition that no reference is taken; stack allocation keeps the object and only needs it not to outlive the frame.Escape Analysis →
"The analysis is a JIT feature." Go does it at compile time and reports it; C++14 permits allocation elision in the standard. It is a static analysis that a JIT happens to be in a better position to apply.Escape Analysis →
"Safe languages are slower because of bounds checks." They are slower where the checks survive. In the ordinary counted loop over an array, they do not, and the generated code is the same as the unchecked version.Bounds Check Elimination →
"Using unchecked indexing is how you make Rust fast." It is how you give up the guarantee. Check whether the bound check is still present first; usually it is not, and the unsafe version buys nothing.Bounds Check Elimination →
"The optimizer removes bounds checks at -O2." It removes the ones it can prove. Whether yours is provable depends on how you wrote the index, and that is under your control in a way the optimization level is not.Bounds Check Elimination →
"A bounds check is just a compare and a predicted branch." It is also an extra exit from the loop, which is what stops the vectorizer, and that cost is much larger than the compare.Bounds Check Elimination →
"The compiler optimizes my code." It rewrites your code under constraints you did not write down and it did not choose. Most of what it does not do, it declines to do because it could not prove something.Optimization Legality →
"If the optimizer removed it, it was useless." It was unobservable *under the model the language permits*. Hardware registers, other threads, secret material and elapsed time are all observers that model does not include unless you say so.Optimization Legality →
"An optimization is safe if the tests pass." Legality is a claim about every input. A pass whose precondition is wrong is wrong on the inputs nobody tested, which is where miscompilations are found — usually years later.Optimization Legality →
"Faster is the goal, so anything that makes it faster is an improvement." Anything that changes defined observable behavior is a wrong program, and a wrong program has no speed worth discussing.Optimization Legality →
"If I wrote it, it has to happen." Only if what it does is on the list. A store nobody can read, a computation nobody uses and a loop with no visible effect are all removable by design.Observable Behavior →
"volatile makes it thread-safe." It makes each access to that object happen. It orders nothing else, publishes nothing to another core, and provides no atomicity — the Java meaning of the keyword is a different guarantee with the same spelling.Observable Behavior →
"The compiler cannot change my program's memory usage." It reuses stack slots, promotes objects into registers, and in C++ is explicitly permitted to elide allocations. Memory usage is not observable.Observable Behavior →
"Evaluation order is left to right." In C it frequently is not, and the same compiler may choose differently in two places in one file. Relying on it is how code becomes portable to exactly one compiler version.Observable Behavior →
"The as-if rule is a loophole compilers exploit." It is the enabling clause for all optimization. Without it, nothing in this domain past the parser would be legal.The As-If Rule →
"As-if means the generated code corresponds to my source." It means the observable events correspond. The instruction sequence need have no relationship to the source structure whatsoever.The As-If Rule →
"If two compilers disagree, one of them is broken." Not where the language left something unspecified. Both may be conforming, and the program is the thing that is wrong.The As-If Rule →
"Copy elision is the as-if rule in action." It is the opposite: an explicit exception permitting an implementation to change observable behavior, which the general rule would forbid.The As-If Rule →
"Undefined behavior means the program crashes." Nothing is required to happen, including a crash. The most common outcome is that it works, until a compiler upgrade or an inlining decision changes which assumptions were reachable.Undefined Behavior →
"The compiler should warn me about it." It is not obliged to, it frequently cannot — deciding it in general is undecidable — and the passes that consume the assumption usually cannot tell that they are.Undefined Behavior →
"It only matters for weird code." Signed overflow in a loop counter and reading one type through a pointer to another are both ordinary code, and both are undefined.Undefined Behavior →
"If it works in practice, it is fine." It works with the premises the current compiler happened to use. The next version, or the same version with different inlining, uses different ones.Undefined Behavior →
"Undefined behavior is what happens when you do something wrong." It is a property the language assigns to a construct, in advance. The program is undefined whether or not the construct ever executes on your inputs — and if it does execute, the whole execution is unconstrained, not just that statement.Undefined Behavior →
"The compiler removed my security check on purpose." No pass knew it was a security check. It was a branch with a provably false condition, and those are removed everywhere.How Undefined Behavior Becomes Faster Code →
"This only happens with obscure optimizations." Branch simplification and dead-code elimination are the two most basic passes there are, and they are enabled at -O1.How Undefined Behavior Becomes Faster Code →
"Adding a null check makes the code safer." Adding it *after* a use adds nothing, because the use already supplied the premise that removes it.How Undefined Behavior Becomes Faster Code →
"The compiler should at least warn when it deletes a check." By the time the branch is deleted, the information that a language rule was involved has been laundered through two analyses. Some compilers warn in narrow cases; none can do it generally.How Undefined Behavior Becomes Faster Code →
"Language X has a better optimizer." Sometimes. More often it has better guarantees, and the optimizer is the same LLVM in both cases.Semantics Decide, Not Cleverness →
"Rust is fast because it has no garbage collector." That is one reason among several; the aliasing guarantee it hands the middle-end is a distinct and separately valuable one.Semantics Decide, Not Cleverness →
"If I write the same algorithm, I get the same code." The same algorithm in two languages hands the middle-end different premises, and the emitted code differs accordingly.Semantics Decide, Not Cleverness →
"Annotations are just hints." restrict and __attribute__((const)) are not hints. They are assertions whose falsity is undefined behavior, and the compiler will act on them without checking.Semantics Decide, Not Cleverness →
"-O3 is always fastest, so use it everywhere." It buys speed by spending code size, and on instruction-cache-bound code that trade can go the wrong way. It is a candidate to measure, not a default.Optimization Levels →
"Higher levels are more dangerous because they break the rules." Every level obeys the same legality preconditions. What higher levels do is expose latent undefined behavior in your source that lower levels happened not to act on.Optimization Levels →
"-Os is for embedded only." It is for anything where the binary is a payload, including WebAssembly served over a network and container images where the image size is the deploy time.Optimization Levels →
"-Ofast is just a faster -O3." It changes floating-point semantics, so it changes results. That belongs in a decision about numerical accuracy, not in a build-flag cleanup.Optimization Levels →
"A pass is an optimization." Many passes optimize nothing. Analyses compute facts, canonicalisers normalise form, and verifiers only check — and pipelines contain more of those than of transformations.Pass Pipelines →
"Passes are independent, so order does not matter." Order is most of the design. Each pass creates and destroys opportunities for the others — [[phase-ordering]].Pass Pipelines →
"More passes means better code." Beyond a point it means more compile time and more chances to leave the IR in a form the next matcher misses. Production pipelines are curated, not accumulated.Pass Pipelines →
"The pass manager just calls the passes in a list." It arbitrates analysis lifetimes, and that arbitration is the part that produces wrong code when it is wrong.Pass Pipelines →
"There is a correct pass order and good compilers use it." There is a well-tuned order for a benchmark suite. Different suites, and different programs, prefer different orders.Phase Ordering →
"Running everything twice fixes it." It recovers enablement and does nothing for the pairs that actively destroy each other's opportunities, such as unrolling and vectorization.Phase Ordering →
"Phase ordering is a correctness concern." It is a quality concern. Every order is legal, which is exactly why a badly ordered pipeline passes every test and simply produces worse code.Phase Ordering →
"The compiler will find it eventually if it iterates enough." Iteration recovers missed enablement. It does not recover an opportunity a transformation destroyed, because the form that opportunity needed no longer exists.Phase Ordering →
"The backend is the part that makes code fast." The backend mostly avoids making it slow. Inlining, loop transformation and redundancy elimination all happened upstream on the IR.Code Generation →
"Instruction selection picks the fastest instruction." It picks an instruction that implements the operation under the constraints it can see. Whether it is fastest depends on the microarchitecture, which the selector models only approximately.Code Generation →
"Assembly is what the CPU executes." Assembly is text. The encoded bytes are what the CPU fetches, and the CPU then reorders and renames them anyway.Code Generation →
"If the assembly looks right, the codegen is right." Assembly that looks right can still violate the calling convention, and that failure only appears when someone else's code calls it.Code Generation →
"lea is a memory instruction, so using it for arithmetic is a hack." lea computes an address and never accesses memory. Using it for arithmetic is exactly what the instruction is for once you see it as a three-operand adder.Instruction Selection →
"The compiler picked imul so multiplication must be cheap here." The compiler picked an instruction that implements the operation under its constraints. Cost is one input among several, and the model may be a generation out of date.Instruction Selection →
"Selection is a lookup table, so it cannot be wrong." Every entry in the table is a claim that two things compute the same value for all inputs. Those claims are where miscompilations live.Instruction Selection →
"If I write the shift myself instead of the multiply, I will get better code." On any optimizing compiler you will usually get the same code, and on a signed division you will get *different* semantics — which is a bug you introduced.Instruction Selection →
"Maximal munch is optimal because it takes the biggest tile." Biggest first is a greedy heuristic. It can leave a remainder that costs more than a smaller first tile would have.Tree Pattern Matching →
"The DP tiler produces the fastest code." It produces the cheapest cover for the costs it was given, and those costs do not model register pressure or the out-of-order engine.Tree Pattern Matching →
"Tiling works on the IR, so it sees the whole function." It works on expression trees cut out of the IR. Anything spanning a multiply-used value or a basic-block boundary is outside the tile.Tree Pattern Matching →
"A pattern is just a rewrite, so a wrong one makes the code slower." A wrong pattern makes the code *incorrect*. Patterns are equivalence claims, and the selector believes them.Tree Pattern Matching →
"The compiler reorders my code, so instruction order in the source does not matter." Instruction order in the *source* was already gone by the middle-end. What is being reordered here is machine instructions, subject to every dependence the language semantics imply.Instruction Scheduling →
"Out-of-order execution makes scheduling obsolete." It makes scheduling-for-latency much less valuable on big cores. It does nothing about register pressure, and it does not exist on in-order targets.Instruction Scheduling →
"A shorter critical path means faster code." Only if the critical path is what you were waiting on. A block bound by port throughput or by a cache miss does not care about its dependence height.Instruction Scheduling →
"The scheduler knows the latencies." It knows the latencies in its model for the CPU it was told to target. Cache misses, which dominate real programs, are not in any static model.Instruction Scheduling →
"The peephole optimizer is where the compiler makes code fast." It is where the compiler stops embarrassing itself. The speed came from the middle-end.Peephole Optimization →
"These redundancies are in my code." Almost none of them are. Self-moves, redundant jumps and spill-reload pairs are all artefacts of backend phases that could not see each other.Peephole Optimization →
"Deleting a mov is always safe." Deleting a move from a register *to itself* is safe on x86-64 at 64-bit width. Deleting a move between different registers requires knowing the destination is dead, and the window does not know that.Peephole Optimization →
"A bigger rule table is strictly better." Every rule is an unproven equivalence claim unless someone proved it, and rules that fire in table order make generated code sensitive to the order of a list.Peephole Optimization →
"rdi is the first argument." On System V, for integer and pointer arguments, for this platform. On Windows x64 it is rcx; on AArch64 it is x0; for floating point it is xmm0 on both x86 ABIs.Reading Assembly Output →
"Fewer instructions means faster." Not on a machine that issues several per cycle and stalls for hundreds on a cache miss. Instruction count is a proxy for code size, and only loosely for time.Reading Assembly Output →
"This is what my Python function compiles to." Python functions do not compile to machine code in CPython. What you disassembled was bytecode for a virtual machine, and the machine code that runs is the interpreter.Reading Assembly Output →
"The compiler generated bad code here." Before concluding that, check the optimization level, check that the function was not inlined into its caller and optimized there, and check that the ABI did not require what looks redundant.Reading Assembly Output →
"Each instruction is one byte of opcode plus operands." On x86 an instruction may have four prefixes, three opcode bytes, ModR/M, SIB, a displacement and an immediate. On AArch64 there is no opcode byte at all — the operation is a set of bit fields.Machine Code Encoding →
"Shorter encodings run faster." They occupy less instruction cache and less decode bandwidth, which can matter. The execution unit does not care how many bytes the instruction took to express.Machine Code Encoding →
"A disassembler shows what the CPU will execute." It shows what the CPU would execute starting from the offset you gave it. On a variable-length ISA a different starting offset yields a different and equally valid instruction stream.Machine Code Encoding →
"The assembler resolves all the addresses." It resolves the ones it knows. Anything outside the object file leaves a relocation for the linker, which is the whole reason object files have relocation tables.Machine Code Encoding →
"The target is the CPU architecture." The target is the architecture *and* the ABI *and* the OS conventions. Linux and Windows on the same x86-64 chip are different targets.What a Backend Must Know About Its Target →
"More registers is strictly better." More registers reduces spilling and increases the cost of saving them across calls and context switches. 16 versus 32 changes the allocator's job, not its difficulty.What a Backend Must Know About Its Target →
"WebAssembly is a bytecode, so it is like the JVM." It is a stack machine like the JVM and differs in the ways that matter here: structured control flow, linear memory, no garbage collection in the core specification, and a design intended for ahead-of-time compilation to machine code rather than interpretation.What a Backend Must Know About Its Target →
"-march=native makes my program faster." It makes the program use whatever the build machine has. Whether that is faster depends on the code, and whether it *runs* depends on the deployment machine having the same features.What a Backend Must Know About Its Target →
"More registers would remove the problem." More registers raises the pressure at which spilling starts and costs more state to save on a call and a context switch. AArch64 has nearly twice as many as x86-64 and still spills.Register Allocation →
"The allocator decides which variables are in registers." It decides which *values* are, and after the middle-end has run, the values often no longer correspond to source variables at all.Register Allocation →
"Spilling means the allocator failed." Below the peak-liveness bound, no allocation exists. The program was asking for more storage than the machine has.Register Allocation →
"Register allocation is a solved problem." Colouring is NP-complete, every production allocator is a heuristic, and the choice of heuristic is still a live engineering decision in every major compiler.Register Allocation →
"A variable is live for its whole scope." Scope is a source-level, lexical notion. Liveness is a data-flow notion about whether the value will be read again, and a variable in scope for fifty lines may be live for two.Live Ranges →
"Liveness is forward, because values flow forward." The value flows forward; the *question* is about future uses, so the analysis runs backward. Reaching definitions is the forward counterpart.Live Ranges →
"A value is dead after its last textual use." After its last *dynamic* use on every path, which on a loop back edge is not the same as the last line in the body.Live Ranges →
"A live range is one interval." It is one interval in our engine and in classic linear scan. In a precise allocator it is a set of intervals with holes, and the holes are where the extra registers come from.Live Ranges →
"An edge means the two values are related." An edge means they are alive at the same time. Two values that never interact at all interfere if their ranges overlap.The Interference Graph →
"A high-degree node must be spilled." High degree makes a node hard to colour, not impossible — its neighbours may share colours among themselves. That is precisely the observation Briggs added to Chaitin's algorithm.The Interference Graph →
"The graph tells you what the program does." It tells you nothing about what the program does. Every fact except conflict has been discarded, which is what makes it tractable.The Interference Graph →
"Colouring the graph is register allocation." Colouring is the easy half once you have decided what to spill. Spill selection is where the code quality is decided.The Interference Graph →
"The algorithm colours the graph directly." It removes nodes until the graph is empty, then colours them in reverse order. The removal order is the entire algorithm; the colouring step is trivial.Graph Colouring Allocation →
"A node with k or more neighbours must spill." That is Chaitin's version. Briggs's observation is that such a node often colours anyway, because its neighbours may not use k distinct colours.Graph Colouring Allocation →
"Once a value is spilled the allocator is done with it." Spill code introduces reload temporaries with their own tiny live ranges, so the graph changes and the whole process usually runs again.Graph Colouring Allocation →
"k is the number of registers." k is the number of registers *available to this value*, which precoloured neighbours, ABI constraints and fixed-operand instructions can reduce below the architectural count.Graph Colouring Allocation →
"Linear scan is a worse algorithm." It is a different point on a curve. At the tier where it is used, the alternative is not better code — it is the interpreter continuing to run while a slower compiler finishes.Linear Scan Allocation →
"It spills the least important value." It spills whichever of two candidates ends later. That correlates with unimportance only loosely, which is the algorithm's main weakness.Linear Scan Allocation →
"It is linear time." The sort is not, and modern versions with splitting are not linear either. The name describes the sweep, not the complexity.Linear Scan Allocation →
"JITs use it because they are simple." They use it because compilation happens while the user waits. The top tiers of the same JITs use much more elaborate allocators.Linear Scan Allocation →
"Spilling means the compiler ran out of registers." It means this value lost the competition for them. Above peak pressure, some value had to.Spilling →
"A spill is a memory access, so it is hundreds of cycles." A spill slot in the current frame is nearly always in L1, and a reload immediately after a store is usually satisfied by store-to-load forwarding. The bad case is real; it is not the typical case.Spilling →
"The allocator should just be smarter." Often it should, but the pressure it is resolving was created by inlining and unrolling decisions made much earlier, and no allocator can un-inline.Spilling →
"Spilled means the value lives in memory for the whole function." Only without live-range splitting. With splitting, a value can be in a register exactly where it is used and in memory elsewhere.Spilling →
"Removing an instruction cannot make the code slower." Coalescing removes a mov and raises a node's degree. If that pushes the graph past colourability, the removed mov is replaced by a spill, which is much worse.Coalescing and Rematerialization →
"Rematerialization is just constant folding." Folding evaluates at compile time and replaces the expression. Rematerialization keeps the computation and moves *copies of it* to the use sites, specifically to avoid occupying storage in between.Coalescing and Rematerialization →
"Coalescing is done by the peephole." A peephole can delete a mov from a register to itself. It cannot cause both ends of a copy to be assigned the same register, which is the part that does the work.Coalescing and Rematerialization →
"The moves in unoptimized output are the programmer's fault." They come from phi resolution, from two-address instruction shapes, and from calling conventions. None of the three is visible in the source.Coalescing and Rematerialization →
"Arguments are pushed on the stack." On every 64-bit convention in use the first several arguments are in registers and the stack is the overflow path. The pushing model is 32-bit x86 and has not been the default for twenty years.Calling Conventions →
"The calling convention is part of the instruction set." It is not. Windows x64 and System V run identical instructions and disagree about every register role, which is precisely why an object file can link successfully and still be wrong.Calling Conventions →
"The moves before a call are just the compiler being inefficient." They are the ABI shuffle, and the ones that look redundant are usually a cycle break. Removing them by hand produces a miscompilation.Calling Conventions →
"If it links, the conventions match." Linking matches symbol names. Nothing in an object file records which convention a function expects, so a mismatch is discovered by the program misbehaving.Calling Conventions →
"Local variables live on the stack." They live in registers when they can. A stack slot is what happens when they cannot, and the reasons are specific and enumerable.Stack Frame Layout →
"-fomit-frame-pointer is a micro-optimization with no downside." It is a real win and it is why your flame graphs are truncated. The trade is register pressure against observability, and it is a genuine argument with a live answer on both sides.Stack Frame Layout →
"The frame pointer is needed for exceptions." C++ unwinding uses .eh_frame, not the frame chain, and has done for decades. Frame pointers are for cheap stack walking, not for correctness.Stack Frame Layout →
"Frame layout follows declaration order." It follows alignment, lifetime overlap and hardening instrumentation. Compilers reorder slots freely and change the ordering between versions.Stack Frame Layout →
"If it links, the ABI matches." Linking checks symbol names and nothing else. Layout and convention mismatches are invisible to a linker by construction.What an ABI Actually Is →
"The ABI is the calling convention." The calling convention is one layer. Type layout, symbol naming, the object model and the exception mechanism are all part of the same contract.What an ABI Actually Is →
"Adding a field to the end of a struct is safe." Only if no caller allocates it, embeds it, takes its size, or stores an array of it. In a plain C header, callers do all four.What an ABI Actually Is →
"C++ has an ABI." C++ has several, and the standard specifies none of them. The Itanium C++ ABI is what Clang and GCC implement on Unix-likes; MSVC implements a different, incompatible one on Windows.What an ABI Actually Is →
"Mangled names are compiler-internal gibberish." They are a specified, decodable serialisation of a declaration. Anything that can read the grammar can reconstruct the signature exactly.Name Mangling →
"extern "C" makes the function C code." It changes the symbol name and the linkage convention. The body is still C++ and may use anything C++ offers.Name Mangling →
"If the demangled names match, the symbols match." They match only if the mangled strings match. Different toolchains produce different manglings for identical declarations.Name Mangling →
"Undefined reference means the library is missing." It means no object provided that exact string. A missing definition, a mangling mismatch, a hidden symbol and a bad link order all produce the same message.Name Mangling →
"Adding a private field is an implementation detail." It is a size change, and size is the most public thing about a type. Private controls access, not layout.ABI Stability →
"Semantic versioning covers this." Semver describes source compatibility unless a project says otherwise. Binary compatibility is a separate axis and needs a separate promise, which is what sonames and version tags exist to express.ABI Stability →
"Rust's lack of a stable ABI is a missing feature." It is a deliberate trade that buys field reordering and niche optimizations, with #[repr(C)] as the explicit opt-out when a frozen boundary is needed.ABI Stability →
"If it starts up, the versions are compatible." Only symbol resolution is checked at load. Layout mismatches produce no loader error and surface as corruption later.ABI Stability →
"Cross-compiling just needs a compiler that supports the architecture." It needs the target's headers, libraries and startup objects too. The code generator is the part that was already solved.Cross-Compilation →
"Go proves cross-compilation is easy." Go proves that removing the dependency on the target's C library makes it easy. Turn on cgo and Go faces exactly the same sysroot problem as everyone else.Cross-Compilation →
"If it links, it will run." Linking checks symbol names. A host header producing a wrong struct layout links perfectly and faults at runtime.Cross-Compilation →
"The build machine and the target are both Linux, so the environment is the same." Different architectures have different type widths, alignments, endianness and libc versions, and any of them is enough.Cross-Compilation →
"A triple has three parts." Most modern triples have four, and some have five. The name is historical and every toolchain parses more fields than it.Target Triples →
"The vendor field identifies the hardware vendor." It identifies almost nothing outside Apple platforms. unknown and pc are placeholders kept for positional parsing.Target Triples →
"Same architecture and same OS means compatible." gnu and musl, or msvc and gnu on Windows, are the same architecture and OS and are not interchangeable.Target Triples →
"The triple fully describes the platform." It carries no OS version, no CPU feature level and no libc version. All three independently decide whether a binary runs on a given machine.Target Triples →
"The linker just concatenates object files." It resolves a global name-matching problem, assigns every address in the program, and rewrites bytes throughout every input. Concatenation is the easy quarter of it.What a Linker Does →
"If it links, it works." Linking checks that every name has exactly one definition. It checks nothing about types, layouts or conventions — see [[abi]].What a Linker Does →
"The entry point is main." The entry point is _start from the C runtime, and a good deal runs before main is called.What a Linker Does →
"Link errors mean a missing library." They mean no definition was found for a name, which can equally be a missing definition, a hidden symbol, a mangling mismatch or a bad link order.What a Linker Does →
"An object file is machine code." It is machine code plus data plus a symbol table plus relocations plus debug metadata, and in a -g build the code is usually the smallest part.Object Files →
".bss is free." It is free on disk. It occupies exactly as much memory as .data would at runtime, and it is zeroed by the kernel, which is real work at first touch.Object Files →
"Sections and segments are the same thing." Sections are for the linker and the tools; segments are the few permission-bearing regions the loader maps. One executable describes both.Object Files →
"Stripping a binary makes it faster or smaller in memory." It makes the file smaller. The stripped sections were never loaded, so the running process is unchanged.Object Files →
"Undefined reference means a missing library." It means no definition for that exact string was found among the included objects. A missing library is one of at least four causes.Symbols and References →
"static makes a function faster." static gives it internal linkage, which keeps it out of the global symbol table and enables more aggressive inlining — the speed is a consequence of the visibility, not of the keyword.Symbols and References →
"If two definitions disagree, the linker will tell me." Only if both are strong. Weak definitions — every inline function and template instantiation — are merged silently without comparison.Symbols and References →
"A symbol in the library means I can call it." Only if it is in the *dynamic* symbol table. Hidden visibility leaves it visible to nm and unusable from outside.Symbols and References →
"Relocations are a legacy mechanism from before virtual memory." Virtual memory gives each process its own address space and does not tell a module where in it to sit. Shared libraries and ASLR both require exactly this machinery.Relocations →
"PIC means the code is slower everywhere." It costs an indirection on cross-module symbol access. Calls and data within the module are PC-relative and cost nothing extra on architectures with a PC-relative addressing mode.Relocations →
"The PLT exists to make calls faster." It exists so that call sites do not need patching and so resolution can be deferred. It makes each call marginally slower than a direct one would be.Relocations →
"relocation truncated to fit means a missing symbol." It means the symbol was found and the distance to it did not fit the field. It is a layout and reach problem.Relocations →
"Static linking includes the whole library." It includes the archive members that were needed. Unused members are never extracted, and unreferenced sections can be collected on top of that.Static Linking →
"Static binaries are always bigger." Compared against a dynamic binary alone, yes. Compared against the dynamic binary plus the shared objects it needs, often not.Static Linking →
"Static linking is a security improvement because there is nothing to hijack." It removes LD_PRELOAD-style interposition and adds the obligation to rebuild everything for every library CVE. That trade favours dynamic linking in most fleets.Static Linking →
"A statically linked glibc program has no runtime dependencies." NSS and getaddrinfo still load shared objects. Only musl-based or pure-Go binaries are genuinely dependency-free.Static Linking →
"GLIBC_2.34 not found means glibc is missing." glibc is present and is the correct file. It does not define that symbol at that version tag, and the loader refuses to guess.Dynamic Linking →
"Dynamic linking saves disk space." Its main saving is physical memory across processes. The disk saving is real and secondary, and disappears entirely if each application bundles its own copies.Dynamic Linking →
"The library my program uses is decided at build time." The library is *named* at build time. Which file provides it, and which definition wins, is decided by the loader on the machine that runs it.Dynamic Linking →
"Lazy binding is free." It defers cost rather than removing it, keeps the GOT writable, and moves the first call's resolution into whatever code path happens to reach it first.Dynamic Linking →
"The soname is documentation." It is a mechanism. It is recorded in consumers and is what the loader searches for, which is why bumping it lets two incompatible versions coexist.Shared Libraries →
"Hiding symbols is about encapsulation." Encapsulation is a side benefit. The measurable effects are a smaller symbol table, faster loading, direct internal calls and unblocked optimization.Shared Libraries →
"A .so is a .a that gets loaded at runtime." An archive is a container of object files with no code of its own. A shared object is a fully linked, position-independent image with its own dependencies, initializers and symbol tables.Shared Libraries →
"If the file is there, the library will load." The loader looks for the soname, not the file name. A file present under the wrong name is not found.Shared Libraries →
"LD_LIBRARY_PATH always wins." DT_RPATH outranks it. That is the entire reason DT_RUNPATH was introduced and is now the default.Symbol Resolution Order →
"Symbol collisions cause a link error." Only for two strong definitions in a static link. At load time the first in scope simply wins, silently.Symbol Resolution Order →
"LD_PRELOAD only affects the program I run it on." It is inherited by every child process, which is why setting it in a shell profile causes bugs weeks later in unrelated services.Symbol Resolution Order →
"Dropping privileges protects against preloading." The loader drops the unsafe variables only across a set-user-ID transition. A daemon whose environment an attacker can edit gets no such protection.Symbol Resolution Order →
"The program starts at main." It starts at _start, after every loaded object's initializers have already run. main is an ordinary function called by the runtime.The Loader →
"No such file or directory means the binary is missing." It usually means the *interpreter* named inside the binary is missing. The binary is right there.The Loader →
"Loading reads the program into memory." It maps it. Pages arrive on demand, which is why start-up does not scale with binary size.The Loader →
"Static initializers run in the order I wrote them." Within one translation unit, yes. Across translation units and across shared objects, the order is not specified and changes with link order.The Loader →
"The first compiler must have been written in assembly." It must have been written in something that already had an implementation. Assembly is one option; another existing high-level language is more common and much easier.Bootstrapping a Compiler →
"Once a language is self-hosting, there is no dependency on anything else." The build still starts from a binary somebody produced. The dependency moved rather than disappeared.Bootstrapping a Compiler →
"Stage 0 has to implement the language." It has to compile one program. Every feature the compiler's own source avoids is a feature stage 0 can omit.Bootstrapping a Compiler →
"Bootstrapping is a historical curiosity." It is the current build process for every self-hosted toolchain and it determines what you are trusting when you run it.Bootstrapping a Compiler →
"Stage 1 and stage 2 should be identical." They should not. They are the same program compiled by different compilers, and only from stage 2 onward is the chain a fixed point.Self-Hosting and the Three-Stage Build →
"A successful bootstrap proves the compiler is correct." It proves the chain is self-consistent. A bug shared by both stages, and any self-reproducing modification, passes it.Self-Hosting and the Three-Stage Build →
"A bootstrap comparison failure means a compiler bug." Most of them are non-determinism in the build. That is worth fixing and it is a different problem.Self-Hosting and the Three-Stage Build →
"Self-hosting is required for a serious language." It is a common choice with real benefits and real costs, and several serious compilers are written in something else entirely.Self-Hosting and the Three-Stage Build →
"This means compilers are backdoored." It means source review cannot rule it out. Those are very different claims and only the second is being made.Reflections on Trusting Trust →
"Open source solves this." Open source makes the source reviewable. The attack is specifically constructed to leave nothing in the source, and the binary that most people run was built by someone else.Reflections on Trusting Trust →
"Checksums prevent it." A checksum proves you received the file the publisher published. It says nothing about what that file does or what built it.Reflections on Trusting Trust →
"The bootstrap comparison would catch it." It cannot. Both stages carry the modification, so they agree, which is exactly what the check tests for.Reflections on Trusting Trust →
"The toolchain is the compiler." The compiler is one of a dozen programs that determine the artifact, and several of the others are never looked at by anyone.The Toolchain Is Your Trusted Computing Base →
"Building from source means I know what I am running." Building from source runs a large amount of software you did not review, some of it supplied by the dependencies you are building.The Toolchain Is Your Trusted Computing Base →
"A lockfile makes the build reproducible." It makes the dependency versions fixed. Timestamps, paths, parallelism and unpinned tools all remain, and any one of them is enough to break byte-identity.The Toolchain Is Your Trusted Computing Base →
"Adding a dependency is a runtime decision." In several ecosystems it grants code execution at build time, on the build machine, before the artifact ever runs.The Toolchain Is Your Trusted Computing Base →
"Reproducible means anyone can rebuild it." It means anyone with the same declared inputs gets the same bytes. If the input set includes a specific container digest, "anyone" means anyone with that image.Reproducible Compilation →
"A lockfile makes a build reproducible." It fixes dependency versions. Timestamps, paths, parallelism and iteration order are all still free to vary.Reproducible Compilation →
"Non-determinism in a compiler is harmless because the code is equivalent." It destroys the strongest test a self-hosted toolchain has, makes caching unsound, and makes independent verification impossible.Reproducible Compilation →
"If two builds differ, one of them is wrong." Usually both are correct and one of them recorded the time. The point is not correctness but verifiability.Reproducible Compilation →
"Bytecode is machine code for a fake CPU." It is an instruction set designed for a software interpreter, and that changes what is worth including: complex, high-level opcodes are cheap for a VM and expensive for hardware, which is why VM instruction sets look nothing like an ISA.Bytecode →
"Compiling to bytecode makes the program fast." It makes execution faster than walking the tree. It leaves you an order of magnitude behind native code, and closing that gap is what [[jit-compilation]] is for.Bytecode →
"Bytecode means the source is protected." It is usually easier to decompile than machine code, because it kept names, structure and types that a native backend would have discarded.Bytecode →
"One bytecode instruction is one machine instruction." One bytecode instruction is a whole interpreter iteration: fetch, decode, branch to a handler, execute, loop. Tens of machine instructions is normal.Bytecode →
"The stack is where variables live." Variables live in slots. The stack holds intermediate values of the expression currently being evaluated, and is normally empty between statements.Stack-Based Virtual Machines →
"Stack machines are slower because stacks are slow." The stack is an array and its top is always in cache. The cost is the extra *instructions*, and therefore the extra dispatches, not the memory.Stack-Based Virtual Machines →
"ADD takes no operands, so it must be a pseudo-instruction." It is a real instruction; its operand locations are simply fixed by convention rather than encoded.Stack-Based Virtual Machines →
"If the code type-checked, the bytecode is well-formed." Type checking happened on the AST. Nothing about a well-typed program prevents a code generator from emitting an unbalanced stack, which is why the verifier is a separate stage.Stack-Based Virtual Machines →
"Register VMs use the CPU's registers." They use an array of frame slots. Mapping those onto hardware registers is a JIT's job and does not happen in the interpreter.Register-Based Virtual Machines →
"Fewer instructions means proportionally faster." Part of the saving is returned as larger fetches and more decoding. The instruction-count reduction is the upper bound on the speedup, not the speedup.Register-Based Virtual Machines →
"Register machines are the modern design and stack machines are legacy." WebAssembly is a stack machine and was designed in the last decade. The choice tracks what the format is *for*, not when it was made.Register-Based Virtual Machines →
"Since there are hundreds of registers, allocation is free." It is cheap, not free, and it still has to be correct: a reused register whose value is live is a wrong-answer bug with no diagnostic.Register-Based Virtual Machines →
"Register VMs are 30% faster." Some measured conversion of some interpreter was, on some benchmark set. The direction transfers; the number does not.Stack VM vs Register VM →
"Stack VMs are simpler, therefore worse." They are simpler for the generator and the verifier, which is a feature when either of those is on someone else's critical path.Stack VM vs Register VM →
"Fewer instructions must mean less memory." Fewer, wider instructions can be more bytes in total, and instruction bytes compete for cache.Stack VM vs Register VM →
"The choice is permanent." The bytecode is internal unless you published it. If it is internal, you can change it — the cost is your tooling, not the world's.Stack VM vs Register VM →
"A tree-walking interpreter is not a real implementation." It is a complete and correct implementation of the language. It is slow, which is a different property.Tree-Walking Interpreters →
"Tree-walking is slow because recursion is slow." Recursion is cheap. The costs are the per-node dispatch, the scattered pointer loads, and — very often — name lookup by string.Tree-Walking Interpreters →
"It executes the source code." It executes a tree the parser and type checker already produced. The characters are long gone; only the spans survive.Tree-Walking Interpreters →
"Adding a bytecode stage means throwing this away." Keep it. It is your semantics oracle, and running both on the same inputs catches backend bugs nothing else will.Tree-Walking Interpreters →
"Bytecode generation is where the optimizations happen." Optimization happens on the IR, where values have names and dataflow analysis is cheap. What happens here is a local rewrite plus, at most, peepholes.Compiling to Bytecode →
"The stack replaces the registers, so slots are unnecessary." The stack replaces the destination register only for the instant between the operator and its consumer. Anything that must survive that instant — a named variable, a value with several uses — needs a slot.Compiling to Bytecode →
"Phi nodes are just moves, so the generator can emit them as copies." That is true and it is precisely what out-of-SSA does, on the incoming edges rather than at the phi's position. Emitting a copy where the phi sits is wrong, because it would run on every path rather than on the path it belongs to.Compiling to Bytecode →
"Forward jumps need a two-pass compiler." They need a patch list, which a single pass over the blocks can maintain. Two passes is one way to implement it, not a requirement.Compiling to Bytecode →
"Computed goto makes interpreters 2x faster." It made some interpreters meaningfully faster on the hardware where the technique was established. On recent predictors the measured gain is often small, and it is a portability and readability cost either way.The Dispatch Loop →
"The switch is slow because it is a switch." It is slow because it is one indirect branch whose target is essentially unpredictable. A switch over a variable the predictor can learn is not slow at all.The Dispatch Loop →
"Dispatch is where interpreters spend their time." It is where *some* interpreters spend a large share. In dynamically typed VMs, type tests and allocation frequently cost more, and optimizing dispatch there is optimizing the wrong thing.The Dispatch Loop →
"Once it is threaded there is nothing left to do." Superinstructions, stack caching and inline caching all still apply, and a JIT still beats all of them by removing the loop.The Dispatch Loop →
"Interpreters are slow because interpretation is inherently slow." They are slow for four separable, measurable reasons, and in any given VM one of them usually dominates. "Inherently" is what people say instead of measuring.Where an Interpreter's Time Actually Goes →
"Making the dispatch faster is how you speed up an interpreter." It is how you speed up an interpreter whose dispatch dominates. In a dynamically typed VM the type tests and the allocation usually cost more, and the profile will say so in about a minute.Where an Interpreter's Time Actually Goes →
"A superinstruction is just an optimization, so it cannot change behaviour." It changes the instruction indices, and anything that names an instruction index — a jump, a breakpoint, a stack trace, an instruction budget — is affected. It is a transformation with a legality condition like any other.Where an Interpreter's Time Actually Goes →
"JIT is always slower for short programs, so a specializing interpreter is strictly better." Warmup is a real cost and a real reason to pick an interpreter, but "strictly better" is not what a tiered system measures — the crossover point is a workload property, and [[tiered-compilation]] exists to have both.Where an Interpreter's Time Actually Goes →
"If I close the gap to native, the language becomes fast." The gap you can close is a multiplier on the work you are doing. Doing less work is a different and usually larger lever.Where an Interpreter's Time Actually Goes →
"The bytecode is part of the VM's state." It is not; it never changes during a run. That is exactly what lets it be shared between threads, memory-mapped from a file and cached across processes — and it is why a self-modifying VM, like a specializing interpreter, is a genuinely different and more complicated design.What a Virtual Machine Has to Hold →
"The operand stack and the call stack are the same stack." They hold different things with different lifetimes even when they share an array. One is expression scratch space; the other is the record of who called whom.What a Virtual Machine Has to Hold →
"If the program does not terminate, the VM hangs." Only if the VM chose to let it. Termination behaviour is a design decision made in the dispatch loop, and gas, reductions, timeouts and step budgets are four different answers to it.What a Virtual Machine Has to Hold →
"steps-exhausted means the program is buggy." It means the VM stopped counting. A correct program that needs more than the budget produces the same status, which is why the budget is reported alongside it and why raising it is a legitimate response.What a Virtual Machine Has to Hold →
"Locals are variables and the stack is temporaries, so a good compiler would use only one of them." They have different addressing modes for a reason: a slot is readable any number of times at any later point, and a stack entry is consumed by the next instruction that pops it.What a Virtual Machine Has to Hold →
"A JIT is a compiler that runs later." It is a compiler that runs *with different information and a different budget*, and both halves of that change what it should do. A JIT that behaved like an ahead-of-time compiler would be strictly worse than one.Just-in-Time Compilation →
"JIT compilation means the program is compiled every time it runs." Only the hot parts, only after they prove hot, and typically on a background thread. Most of the code in a large program is never compiled at all.Just-in-Time Compilation →
"Once it is compiled, it stays compiled." Compiled code is discarded when a guard fails, when the runtime invalidates an assumption, and sometimes when a code cache fills. Falling back to the interpreter is a normal event, not an error.Just-in-Time Compilation →
"A JIT makes a dynamic language as fast as a static one." It closes a large part of a large gap, on hot code, when the code is type-stable. Polymorphic, allocation-heavy or megamorphic code collects much less of that.Just-in-Time Compilation →
"The interpreter is just there for startup." The interpreter is also the fallback that makes speculation safe, and the definition of correct behaviour that the compiled code is checked against. Removing it removes the ability to be wrong safely.Just-in-Time Compilation →
"The JIT knows the types, so a dynamic language can be as fast as a static one." It knows what the types have been. Every use of that knowledge carries a check, the checks cost something, and code that is not type-stable collects none of the benefit.Why Runtime Information Helps →
"If a profile says a branch is always taken, the compiler can delete the other side." It can make the other side cold, out of line, and expensive to reach. It cannot delete it, because "always so far" is not "always" — unless the runtime can guarantee the condition globally and invalidate the code if that changes.Why Runtime Information Helps →
"Profile-guided optimization and JIT specialization are the same thing at different times." They differ in kind. PGO cannot check its assumptions at run time, so it may only use the profile for heuristics; a JIT may use it for semantics because it can guard.Why Runtime Information Helps →
"More profiling data is always better." Instrumentation is a tax on the unoptimized tier, feedback slots consume memory per site, and a profile that describes a finished execution phase is worse than no profile.Why Runtime Information Helps →
"A megamorphic site means the code is badly written." It often means the code is genuinely general — a serializer, a dispatcher, a framework hook. The right response is to stop speculating there, not to restructure the program around the compiler.Why Runtime Information Helps →
"The baseline tier is a worse version of the optimizing tier." It is a different kind of compiler with a different job: emit code as fast as it can be written, remove dispatch, assume nothing. Adding optimization passes to it would make it the wrong tool.Tiered Compilation →
"Once code reaches the top tier it stays there." Deoptimization moves it back down as a matter of routine, and repeated deoptimization can stop it being promoted again at all.Tiered Compilation →
"More tiers is always better." Every tier is another full implementation of the language's semantics and another place for the tiers to disagree. Engines add them when there is a measured gap, and the gap has to be large to justify one.Tiered Compilation →
"Warmup is just the first few calls." Warmup is however long it takes the profile to become representative and the compiles to complete, which for a large server application can be minutes and can be re-triggered by a traffic shift.Tiered Compilation →
"Tiering is a JIT thing, so ahead-of-time compiled languages do not have it." Anything that can replace code at run time can tier: .NET tiers over ReadyToRun images, and Android tiers over ahead-of-time compiled DEX. The relevant question is whether replacement is possible, not how the first version was produced.Tiered Compilation →
"Hot means the function takes a long time." It means the function has executed a lot. A single call that takes a second has an invocation count of one, which is exactly why back-edge counters exist.Profiling and Hotness →
"The right threshold can be computed." It is a bet on future execution frequency. It is tuned empirically, differs per engine and per workload, and has a cost in both directions.Profiling and Hotness →
"Profiling in production is expensive." The counters are already running in every JIT you use. A sampling profiler adds cost proportional to the sampling rate, not to the workload, and at typical rates it is a fraction of a percent.Profiling and Hotness →
"A CPU profile tells me what the JIT decided." It tells you where time was spent, attributed through inlining and tiering in ways that require reconstruction metadata to be right. The engine's own trace flags tell you what it decided.Profiling and Hotness →
"Counters must be accurate." They must be accurate enough to cross a threshold in roughly the right order. Engines deliberately trade exactness for the absence of synchronization on every call.Profiling and Hotness →
"On-stack replacement means replacing the code on the stack." It means replacing the *activation*: building a new frame in the compiled code's layout from the values in the old one and continuing there. The code was never on the stack.On-Stack Replacement →
"It is just tier-up for loops." The decision is the same; the mechanism is completely different. Ordinary tier-up patches an entry point and waits for a call. OSR translates a live frame between two incompatible layouts.On-Stack Replacement →
"The compiled code is the same, it is just entered elsewhere." Entering at a loop header establishes far less than entering at a function entry, so engines commonly compile a separate, less optimized version specifically for OSR.On-Stack Replacement →
"If a runtime does deoptimization it gets OSR for free." It gets the hard part — the discipline of maintaining a mapping between abstract and concrete frame states — but the direction, the entry-point selection and the compile strategy are all separate work.On-Stack Replacement →
"OSR is a benchmark artifact; real code calls functions." Server loops, event loops, game loops and batch jobs are all single long-lived activations. Benchmarks exposed the problem loudly; they did not invent it.On-Stack Replacement →
"The compiler assumes the types and hopes for the best." It checks them. The assumption appears in the generated code as a comparison and a branch, and there is a correct path out the other side.Speculative Optimization →
"Speculation makes the program probabilistically correct." It makes performance probabilistic. The program's behavior is the same under either outcome of the guard; that is the whole design requirement.Speculative Optimization →
"If the profile is accurate, no guard is needed." The profile describes what has happened. The guard is what makes what happens next safe, and no amount of past evidence substitutes for it.Speculative Optimization →
"More speculation is always better because guards are cheap." The guard is cheap; the failure is not. The expected cost is dominated by the failure probability, which is why engines back off from sites that deoptimize repeatedly.Speculative Optimization →
"Any observed fact can be speculated on." Only facts with a cheap, sufficient check. This rules out most semantic properties, which is why the catalogue of real speculations is short and looks similar across every engine.Speculative Optimization →
"A guard is just an if statement." It is a branch with an implicit exit out of the function to a reconstruction point, and it carries a description of the abstract machine state at that point. The metadata is the expensive half.Guards →
"Guards make the code slower, so a good JIT minimizes them." A good JIT minimizes them, and not because they are slow — because each one is a state map and a set of liveness obligations. The comparison itself is nearly free.Guards →
"If the guard passes, nothing was paid." The comparison and branch were paid, and more importantly the optimizer paid: values were kept alive and effects were not moved, because the guard might have failed.Guards →
"A stronger guard is a safer guard." A stronger guard fails more often, and failure is thousands of times more expensive than the check. The right guard is the weakest one still sufficient for the assumption.Guards →
"A null check always costs an instruction." On a runtime that can use a trapping load and a signal handler, the passing case costs nothing at all and the cost is entirely in the metadata that maps the fault address to a resume point.Guards →
"Deoptimization means the JIT made a mistake." It means an assumption stopped holding, which was always a possible outcome. The mechanism working is the system behaving as designed; the mechanism working *repeatedly for the same function* is the problem.Deoptimization →
"Deoptimization is expensive, so engines avoid it." It is expensive and rare, and its expense is not the interesting cost. The interesting cost is paid continuously by the optimizer, in transformations it may not perform because a state map must stay satisfiable.Deoptimization →
"The interpreter just picks up where the compiled code left off." It picks up at a bytecode offset in a frame that had to be constructed, possibly several frames, possibly including objects that had to be allocated because escape analysis had removed them.Deoptimization →
"If a value is dead, the compiler can remove it." Only if no state map names it and no rematerialization recipe is needed. Deadness in the ordinary sense is not sufficient in a speculative compiler, which is the single most surprising consequence of this machinery.Deoptimization →
"A deoptimization is like an exception." An exception is defined program behavior with defined semantics. A deoptimization is invisible to the program: the same computation continues, in different code, with the same result.Deoptimization →
"An inline cache caches the value." It caches the *resolution* — which target, which offset — not the data at that offset. The load still happens; what is skipped is working out where to load from.Inline Caches →
"Megamorphic means the cache failed." It means the cache correctly concluded that caching does not pay here. Continuing to extend the chain would be the failure.Inline Caches →
"Inline caches are a JIT feature." They predate JIT compilation and work in a pure interpreter. What a JIT adds is inlining through the cached target, which is a separate and larger thing.Inline Caches →
"A polymorphic site is nearly as good as a monomorphic one." For the lookup, roughly. For the compiler, not at all: a monomorphic site can be inlined behind one guard, and a four-way polymorphic site usually cannot.Inline Caches →
"Two objects with the same fields have the same shape." Only if the fields were added in the same order and by the same transitions. Constructing the same logical type two different ways produces two shapes and turns every site that sees both polymorphic.Inline Caches →
"JIT is always slower because compilation happens at runtime." Compilation is paid once per hot method and amortized across every subsequent execution, and the resulting code can be better than a static compiler's because it knows the actual types and branch bias. The cost is real and it is a warmup cost, not a permanent tax — which is why long-running services use JITs and short-lived tools should not.What a JIT Costs →
"Warmup is a benchmark artifact." It is a production cost paid after every deployment, every scale-out event and every traffic shift, and it is the reason load balancers ramp traffic to new instances.What a JIT Costs →
"The JIT compiles in the background, so it is free." The background thread runs on the same machine as the application, competing for CPU and cache, during the period the application is at its slowest.What a JIT Costs →
"Memory overhead is just the compiled code." It is bytecode plus profiles plus compiled code at several tiers plus deoptimization metadata, and the profile component scales with the whole loaded program rather than the hot part.What a JIT Costs →
"W^X is a hardening detail that does not affect language implementations." It decides whether a JIT can exist in a given environment at all, which is why iOS-restricted browsers and hardened server deployments run interpreters or ahead-of-time code.What a JIT Costs →
"A JIT and an ahead-of-time compiler are the same technology at different times, so the JIT strictly dominates." They make opposite bets about process lifetime and memory, and each bet is right for some deployments. Choosing between them is a real decision with real losers on both sides.What a JIT Costs →
"It is only sugar, so it is free." It is free for the back end and expensive for the grammar, the diagnostics and every future reader of the language.Syntactic Sugar →
"Sugar means the compiler literally substitutes the text." It substitutes a tree, and for several constructs the correct tree contains temporaries the text does not.Syntactic Sugar →
"If two forms produce the same bytecode they are the same feature." They produce the same bytecode for the cases you tested. Side-effecting operands, overloaded operators and short-circuiting are where they diverge.Syntactic Sugar →
"async/await is sugar over promises." It requires splitting the function and moving locals off the frame, which no local rewriting can do — see [[async-lowering]].Syntactic Sugar →
"Desugaring is a text transformation." It is a tree transformation, and for several constructs the correct tree contains bindings the text does not.Desugaring →
"If the compiler desugars it, I can write the desugared form myself and get identical code." Usually yes, and not when the rewrite uses names or intrinsics the surface language cannot express.Desugaring →
"Bad error messages are a quality problem the compiler team can just fix." They are frequently a consequence of when the rewrite happens, and fixing them means restructuring the frontend — which is why compilers do exactly that.Desugaring →
"A smaller core language is always the better design." It is the better design for the optimizer and the worse design for the person reading the error. Which one you are optimizing for is the actual decision.Desugaring →
"A closure captures the enclosing scope." It captures the variables it mentions. Capturing the whole scope is an implementation some early engines used, and it is the reason closure memory leaks were once much worse than they are now.Closures →
"Closures are just anonymous functions." An anonymous function that mentions nothing outside itself needs no environment and is a plain function pointer. Capture is the feature; anonymity is the syntax.Closures →
"A closure copies the variables it uses." In most garbage-collected languages it shares them, which is exactly why a loop-created closure sees the final value.Closures →
"Closures always allocate." They allocate when the environment must outlive the frame. A comparator passed to a sort in a language with escape analysis frequently allocates nothing.Closures →
"The loop bug proves JavaScript closures are broken." The closures are correct. var created one binding for the whole function and all three closures share it, exactly as binding capture specifies.Closure Conversion →
"let fixes closures." let changes how many bindings the loop creates. The closure machinery is identical in both versions.Closure Conversion →
"Closure conversion means every lambda allocates." Every lambda that captures something allocates unless a proof removes it. Capture-less lambdas usually compile to a static value.Closure Conversion →
"The environment holds the enclosing frame." It holds the captured variables. Holding the frame is an implementation some interpreters use and it is why closure retention used to be much worse.Closure Conversion →
"Lifting is strictly better than closure conversion because it does not allocate." It does not allocate and it cannot handle escaping functions, and its parameter cost can exceed the allocation it saved.Lambda Lifting →
"Any nested function can be lifted." Only those whose every use is a direct call. One use as a value anywhere defeats it.Lambda Lifting →
"The extra parameters are free because they are in registers." Only while there are registers. Past the target's argument-register count they are stack traffic on every call.Lambda Lifting →
"Lifting and inlining are the same thing." Inlining removes the call; lifting keeps the call and removes the nesting. A lifted function is still a function, and often still called from several places.Lambda Lifting →
"A generator keeps its stack frame alive." It does not have one while suspended. Its live locals are fields of a heap object, and the frame is created afresh on each resume.Lowering Coroutines →
"Coroutines are threads without the OS." Stackless coroutines are not threads in any sense: there is no stack, no scheduler entitlement, and no preemption. Suspension happens only where the source says it does.Lowering Coroutines →
"Function colouring is a design mistake someone could have avoided." It follows from lowering per function. The languages without it use stack switching instead and pay for it in memory per instance.Lowering Coroutines →
"The state object is the size of the frame." It is the size of the locals live across suspensions, which is usually much smaller and occasionally, through a missed liveness refinement, larger than anyone expects.Lowering Coroutines →
"async makes code faster." It makes waiting cheaper. CPU-bound code gains nothing and pays for the machinery.Lowering Async and Await →
"await blocks until the result arrives." It returns to the caller and arranges to be resumed later. Nothing is blocked; that is the entire point.Lowering Async and Await →
"async is sugar over promises." Promise chaining is a library pattern; async requires splitting a function and relocating its locals, which no library can do.Lowering Async and Await →
"If it is async it will not block the thread." Only the awaits give control back. A synchronous call between two awaits holds the thread for its full duration.Lowering Async and Await →
"Colouring is an arbitrary language restriction." It follows from lowering per function plus encoding pendingness in the return type. Languages that do neither have no colouring.Lowering Async and Await →
"Zero-cost exceptions are free." Free on the non-throwing instruction stream. The tables, the duplicated cleanups and the constrained optimizer are all real, and the throw is slow.Exception Handling →
"try/catch adds a branch at runtime." Under the table-driven scheme it adds no instructions at all. It adds a CFG edge and a table entry.Exception Handling →
"Exceptions are slow, so avoid them." Throwing is slow; having them costs size and some optimization. Using them for genuinely rare failures is usually the right call, and using them for control flow is not.Exception Handling →
"The compiler knows what a function can throw." In C++ it does not unless you tell it. That information lives in the source in Java and in the type in Rust, and nowhere at all in the default C++ case.Exception Handling →
"An exception passing through my function does not affect it." It does: your locals must be destroyed, so your function carries tables and landing pads even though it never mentions exceptions.Exception Handling →
"The unwinder follows frame pointers." Optimized code usually has none. It follows compiler-emitted rules indexed by return address.Stack Unwinding →
"Unwinding is a jump." It is a loop of table lookups and indirect calls, one per frame, done twice under the Itanium ABI.Stack Unwinding →
"Only C++ needs unwind tables." Profilers, debuggers and crash reporters need them, which is why they are enabled by default on targets where exceptions are rarely used.Stack Unwinding →
"If I catch the exception I can get a full stack trace." By the time the handler runs, the frames below it have been destroyed. Runtimes that provide good traces capture them at throw time.Stack Unwinding →
"A function without try or throw is unaffected." If it owns anything with a destructor, it has a landing pad and table entries.Stack Unwinding →
"A match tests every arm until one matches." That is the semantics. The generated code tests each discriminant once and the arm ordering is resolved at compile time.Compiling Pattern Matching →
"Matching is slower than a switch." A match on a dense tag compiles to the same jump table a switch does. It is slower when guards or sparse tags remove the compiler's options.Compiling Pattern Matching →
"Adding an arm adds a test." It usually adds a case to an existing switch. Adding a *wildcard* arm at a nested position can add a whole duplicated subtree.Compiling Pattern Matching →
"The compiler could just evaluate guards early and get a better tree." It cannot: a guard is arbitrary code, and running it before an earlier arm has been ruled out changes the program.Compiling Pattern Matching →
"Exhaustiveness checking is what makes match compilation work." They share an algorithm and answer different questions. A language can compile matches perfectly well with no exhaustiveness check at all, which is what Python does.Compiling Pattern Matching →
"Python reads my file line by line as it runs." It compiles the entire module first. A syntax error on the last line prevents the first line from executing.The CPython Pipeline →
"There is no compiler, so there is nothing to inspect." There are four intermediate representations, three of which have a standard-library module for dumping them.The CPython Pipeline →
"The .pyc makes the program run faster." It makes it *start* faster, once, by skipping compilation. Execution speed is identical.The CPython Pipeline →
"Bytecode is a Python-level specification, so I can target it from another language." It is a CPython implementation detail that changes between minor releases; the specified interface is the language, not the instruction set.The CPython Pipeline →
"CPython optimizes the obvious things like hoisting a loop-invariant attribute lookup." It cannot: the attribute lookup may run arbitrary code, so removing it would change observable behavior — see [[loop-invariant-code-motion]] for what would be required.The CPython Pipeline →
"JavaScript is interpreted." Every mainstream engine compiles it — first to bytecode, then, for hot code, to native machine code with the observed types specialised in.The JavaScript Pipeline →
"The JIT will optimize my code, so how I write it does not matter." The optimizing tier assumes what it observed. Code that makes observations unstable — inconsistent object shapes, megamorphic call sites — prevents the assumptions from being made at all.The JavaScript Pipeline →
"My function got slower, so V8 must have a bug." Far more often a guard started failing. Trace deoptimization before suspecting the engine.The JavaScript Pipeline →
"Tier names like TurboFan are how JavaScript works." They are one engine's names for one version's tiers. The shape transfers; the names do not.The JavaScript Pipeline →
"Warmup only matters for benchmarks." It decides whether a serverless handler that runs for 40ms ever reaches an optimizing tier at all.The JavaScript Pipeline →
"TypeScript checks types at runtime." It checks them at compile time and then deletes them. Nothing in the emitted file knows what a User is.The TypeScript Pipeline →
"If it compiles, the data is the right shape." It means the code is consistent with the claims you made. Every claim about external data was made by you, unverified.The TypeScript Pipeline →
"esbuild is a faster TypeScript compiler." It is a faster TypeScript *emitter*. It does not type-check, which is most of what tsc spends its time on.The TypeScript Pipeline →
"Adding types will make it faster." The types are erased before any engine sees them; the JavaScript engine specialises on observed runtime types instead — see [[javascript-pipeline]].The TypeScript Pipeline →
"as converts the value." It converts the checker's opinion. The value is untouched, and if the opinion is wrong nothing says so at that line.The TypeScript Pipeline →
"The compiler builds my program." It compiles one translation unit at a time and has never seen your program. The linker is the first stage with a whole-program view, and it understands almost nothing about C++.The C++ Pipeline →
"A header is compiled." A header is copied into every unit that includes it and compiled once per unit. That multiplication is the C++ build-time story.The C++ Pipeline →
"Undefined reference means my code is wrong." It means a promise made in a declaration was not kept by any object file on the link line. The cause is frequently a build configuration, not a source file.The C++ Pipeline →
"If it links, the units agree." They agree on mangled names. They may disagree about layout, inlining and enum sizes, and that disagreement is undefined behavior with no diagnostic.The C++ Pipeline →
"-O2 is applied to my whole program." It is applied within each translation unit. Cross-unit optimization requires LTO and is off by default.The C++ Pipeline →
"A macro is just an inline function." An inline function evaluates its arguments once, has types, participates in overload resolution and appears in the debugger. A macro has none of those properties.The Preprocessor →
"Parenthesising the macro makes it safe." Parentheses fix precedence. They do nothing about double evaluation, name capture or the fact that the expansion is re-parsed in an unknown context.The Preprocessor →
"The #ifdef code compiles; it is just disabled." It is discarded before parsing. Nothing has checked it, and it may not even be valid C++.The Preprocessor →
"Include guards mean the header is only processed once." Once per translation unit. The compiler still parses it for every unit that includes it.The Preprocessor →
"Macros are a legacy feature nobody uses." assert, every include guard, every platform check and most logging in C++ is a macro. The argument is about where they are appropriate, not whether they exist.The Preprocessor →
"Templates are C++'s generics." They are a code generation mechanism that can express generics, and also compile-time computation, per-type specialisation and value parameters. Generics in Java or TypeScript can express none of the last three.Templates →
"The compiler generates code for every possible type." It generates code for every type actually used. An unused member function of a class template is not even instantiated.Templates →
"Template errors are bad because compilers are bad at diagnostics." They were bad because the requirement was not written down anywhere the compiler could check before instantiating. Concepts fixed the cause, not the presentation.Templates →
"Templates make the program slower because there is more code." They make the *build* slower and the binary larger. The generated code is specialised and typically faster than an indirect-call design — at some point instruction-cache pressure can invert that, which is a measurement question.Templates →
"I can put a template in a .cpp file like any other function." Only if you explicitly instantiate it there for every type used. Otherwise the definition must be visible at the point of instantiation, which is why templates live in headers.Templates →
"The linker deduplicates the instantiations, so duplicate instantiation is free." It is free in the binary. It is paid in full, once per translation unit, in the compiler — and that is the cost people actually feel.Template Instantiation →
"Instantiating std::vector<Widget> generates all of std::vector." It generates the class and only the members you use. The rest is neither emitted nor checked.Template Instantiation →
"Two identical functions folded to one address, so the linker has a bug." That is identical code folding doing what it was asked to do. It is off by default on most toolchains precisely because it is observable.Template Instantiation →
"extern template makes the program faster." It makes the *build* faster and can make the program slower, by removing the definition the inliner needed.Template Instantiation →
"If both libraries built successfully, their instantiations must agree." Nothing checked. The ODR exception for templates is an obligation on you, and a violation links cleanly.Template Instantiation →
"constexpr means it runs at compile time." It means it *may*. With a run-time argument it is an ordinary function call, and nothing warns you.Compile-Time Evaluation →
"Compile-time evaluation makes the program faster." It removes startup work and guarantees folding. Steady-state throughput is usually unchanged, because the optimizer folded the same expressions anyway.Compile-Time Evaluation →
"If it compiles, it was evaluated early." Only if the context required a constant. static_assert or inspecting .rodata is the check; the keyword is not.Compile-Time Evaluation →
"More constexpr is strictly better." Each one is work moved into a build that every engineer pays on every change, running interpreted, with no debugger.Compile-Time Evaluation →
"Constant folding and constant evaluation are the same thing." Folding is an optimization the compiler may skip; evaluation is a semantic requirement that must succeed or the program is rejected.Compile-Time Evaluation →
"The borrow checker is a rule about scopes." It is a dataflow analysis over a control-flow graph; scopes were the old approximation, and replacing them is what made the modern rules possible.The Rust Pipeline →
"MIR is Rust's bytecode." Nothing executes MIR as a program. It is an internal analysis form, and the const evaluator that does interpret it is a compile-time facility, not a runtime.The Rust Pipeline →
"LLVM makes Rust safe." Every safety guarantee is established before LLVM is involved. LLVM is told about the results — noalias — and relies on them.The Rust Pipeline →
"My dependency is already compiled, so its code costs me nothing." Its generic code ships as MIR and is monomorphized in your crate, so it costs you compile time and binary size.The Rust Pipeline →
"A macro can check the type of its argument." Expansion runs before type checking. A macro sees tokens, which is why its errors appear after substitution rather than at the call.The Rust Pipeline →
"Go is fast to compile because the compiler is well written." It is well written, and that is not the reason. The reason is a set of language decisions — no headers, no cycles, no metaprogramming — each of which removed work the compiler would otherwise have to do.The Go Pipeline →
"Go does not use LLVM because it predates it." Go's toolchain postdates LLVM by years. The choice is about compile speed and about owning the contract between compiler and runtime.The Go Pipeline →
"Go generics are monomorphized like Rust's." They share one instance per GC shape and pass a dictionary. That is a third answer, and it performs like a third answer.The Go Pipeline →
"A Go binary is static, so it has no dependencies." True until cgo enters the graph, at which point the binary is dynamically linked against the system C library and nothing in your source says so.The Go Pipeline →
"The runtime is a library the compiler links in." It is that, and it also depends on tables the compiler must emit — stack maps, safepoints, function metadata. The two are designed together.The Go Pipeline →
"Two of these are compiled and two are interpreted." All four compile. The question is what they compile to, when, and what is left to do afterwards.Four Languages, One Program →
"They all end up doing the same thing, so the differences are academic." The differences decide startup time, warmup behaviour, what fails at run time, what must be installed, and what a profiler can even attribute time to.Four Languages, One Program →
"TypeScript is faster than JavaScript because it has types." It emits JavaScript, and the engine specialises on observed values regardless. What TypeScript buys is errors before you ship, not speed after.Four Languages, One Program →
"The C++ version is fastest, so it is the best route." It is fastest to run and slowest to change, needs recompilation per target, and offers no adaptation to what the program actually does.Four Languages, One Program →
"Python is slow because it is not compiled." It is compiled, to bytecode. It is slower because the compiler cannot prove anything about a program where names rebind and operators are user-defined, so the work stays generic.Four Languages, One Program →
"LLVM compiles my code." A client of LLVM compiles your code. Which client it is decides the frontend, the diagnostics, the language semantics and half of the performance.What LLVM Actually Is →
"Clang and LLVM are two names for the same thing." Clang is a C-family frontend and a tooling library. LLVM is the infrastructure it emits into, shared with a dozen unrelated frontends.What LLVM Actually Is →
"If two languages both use LLVM, they will generate the same code." They generate the same code only if their frontends emit the same IR with the same attributes, which they rarely do — the attributes are where the language's guarantees live.What LLVM Actually Is →
"LLVM IR is portable, so a bitcode file runs anywhere." It encodes target-specific decisions — type sizes, ABI-driven parameter lowering, target intrinsics — from the moment the frontend emits it.What LLVM Actually Is →
"Upgrading LLVM is a routine dependency bump." For a program that links its libraries it is an API migration, by policy, every release.What LLVM Actually Is →
"The IR makes the compiler portable." It makes the *optimizer* portable. The frontend still knows the target and the backend is entirely about it.The Three-Phase Architecture →
"With a shared middle-end, all languages get the same performance." They get the same optimizer. What it can do depends on what each frontend told it, and frontends differ enormously in that.The Three-Phase Architecture →
"Three phases means three passes." Each phase contains many passes; the phases are about who wrote the code and what it may assume, not about how many times the program is walked.The Three-Phase Architecture →
"If I emit IR, my language is done." You have skipped the optimizer and the backend. Everything about your language — checking, diagnostics, lowering, runtime — is still ahead of you.The Three-Phase Architecture →
"Language-specific optimizations belong in the middle-end where all the machinery is." They belong wherever the facts they need still exist, which for ownership, drops or exhaustiveness is before the seam.The Three-Phase Architecture →
"The IR is machine-independent, so it is portable." It contains type sizes, ABI-driven parameter lowering and sometimes target intrinsics, decided by the frontend before the middle-end ever ran.Reading LLVM IR →
"%1 and %2 are registers." They are SSA value names, unbounded in number. Which physical register anything lives in is decided much later, by [[register-allocation]].Reading LLVM IR →
"add nsw means the compiler checks for overflow." It means the opposite: the frontend has promised overflow does not happen, so the optimizer may assume it away.Reading LLVM IR →
"IR instruction count predicts performance." Instruction selection, scheduling and register allocation all happen afterwards, and one IR instruction may become none or seven.Reading LLVM IR →
"I can save this .ll file and use it next year." Textual IR has no compatibility promise. Use bitcode if you need it to survive an upgrade, and even then only within the supported window.Reading LLVM IR →
"Clang and LLVM are the same project, so the names are interchangeable." Clang is one frontend; LLVM is the infrastructure it and a dozen other frontends emit into.Clang →
"Clang has better error messages because its developers cared more." It has better error messages because the AST retains ranges, sugar and macro provenance, which costs memory and complexity. The design paid for the messages.Clang →
"clang-tidy is a linter, so it works like ESLint on any file." It needs the real compilation command, because a C++ translation unit does not exist without one.Clang →
"If clangd agrees, the build will agree." Only if clangd used the same flags. Different targets compiling the same file differently is the normal case in large builds.Clang →
"The AST is stable, so my tool will keep working." The AST is a supported product with an unstable C++ API. LibClang is the stable interface, and it deliberately exposes less.Clang →
"GCC is the old one and LLVM is the modern one." Both are actively developed, both have been substantially rearchitected, and GCC's current middle-end is younger than several LLVM subprojects.GCC →
"GIMPLE is GCC's version of LLVM IR." It occupies a similar position but is a different design, and it sits between two other representations that have no direct LLVM equivalents.GCC →
"One of them produces faster code." Which one wins depends on the program, the version, the target and the flags, and it changes in both directions between releases.GCC →
"The licence difference does not matter technically." It shapes who adopts each project and therefore where investment goes, which is about as technical a consequence as a licence can have.GCC →
"If my code compiles with one, it is correct." It means one implementation accepted it. Building with both is the cheap way to find out whether you were relying on something unspecified.GCC →
"The compiler said undefined reference." The linker did. The compiler was satisfied; it was told the symbol exists elsewhere and believed it.What a Toolchain Actually Contains →
"gcc is the compiler." gcc is a driver that runs a preprocessor, a compiler, an assembler and a linker. -### shows all four.What a Toolchain Actually Contains →
"If it builds, it will run." Building says nothing about whether the loader can find the shared libraries the executable records.What a Toolchain Actually Contains →
"The build system is part of the toolchain." It decides which toolchain invocations happen. Its failures are about dependency tracking and are invisible to every program it invokes.What a Toolchain Actually Contains →
"Debug information is only for debugging." Symbolication of production crash reports, profilers and sanitizers all read it, which is why stripping it has consequences beyond interactive debugging.What a Toolchain Actually Contains →
"WebAssembly is a language." It is a compilation target with a binary format and a readable text form. Almost nobody writes it by hand, and the text format exists mainly for tooling and teaching.WebAssembly as a Compilation Target →
"WebAssembly replaces JavaScript." It runs alongside it. In a browser the module usually needs JavaScript to reach the DOM at all, and the boundary between them is a design problem rather than a migration path.WebAssembly as a Compilation Target →
"A .wasm file is an executable." It is a module with no capabilities. Without a host supplying imports it cannot read a file, get the time or write output.WebAssembly as a Compilation Target →
"It runs at native speed." It runs at a fraction of native speed that depends on the workload and the runtime, for reasons that are structural — see [[wasm-vs-native]].WebAssembly as a Compilation Target →
"If it compiles to Wasm it will run anywhere." It will run on any host that implements the proposals it uses and provides the imports it declares. Both are real constraints.WebAssembly as a Compilation Target →
"WebAssembly is memory-safe." Its *boundary* is safe: a module cannot reach outside its own memory. A C program compiled to it can still overflow its own buffers and corrupt its own state.The WebAssembly Execution Model →
"It is a stack machine, so it must be slow." The stack machine is the encoding. Runtimes compile it to register machine code, and the operand stack does not exist at run time.The WebAssembly Execution Model →
"No goto means the language is limited." It means the compiler has to reconstruct structure, sometimes at a cost in size or speed. Any computable function is still expressible.The WebAssembly Execution Model →
"The sandbox prevents malicious code from doing damage." It prevents reaching outside the granted capabilities. If the host granted filesystem access, the module has filesystem access.The WebAssembly Execution Model →
"Memory can be freed with memory.grow." Linear memory grows and never shrinks. Freeing inside the module is the module's allocator returning bytes to its own heap, not to the host.The WebAssembly Execution Model →
"WebAssembly runs at near-native speed." It runs within a factor that depends heavily on the workload and the runtime, and for boundary-heavy code the factor is not small.WebAssembly Versus Native →
"The gap will close as runtimes improve." Part of it will. Bounds checking, the copying boundary and the absence of direct system calls are guarantees, not deficiencies.WebAssembly Versus Native →
"Wasm is smaller than a native binary." Sometimes. The comparison depends entirely on whether the language ships a runtime inside the module and whether the native binary links the system libraries dynamically.WebAssembly Versus Native →
"Startup is fast, so cold starts are solved." Startup is fast when the module is precompiled. A host that compiles at load pays milliseconds, which is the same order as the thing it was replacing.WebAssembly Versus Native →
"It is sandboxed, so it is safe to run anything." It is safe from reaching outside its granted capabilities. It can still exhaust time and memory unless the host limits both.WebAssembly Versus Native →
"A DSL is anything with custom syntax." Data formats have syntax and are not languages; internal DSLs have no syntax of their own and are.Domain-Specific Languages →
"A DSL has to be Turing-complete to be a real language." The best ones deliberately are not, and the guarantees they offer depend on that.Domain-Specific Languages →
"SQL is not a programming language, it is a query language." It is a language with a grammar, a semantics and a compiler that produces a plan — it is one of the most successful DSLs ever designed.Domain-Specific Languages →
"If it is only used internally, the tooling does not matter." Internal users have the same expectations and less patience, and they cannot search the web for your error message.Domain-Specific Languages →
"We already have a DSL, it is just some YAML with templating." That is a language with no parser, no types and no diagnostics, which is the expensive end of this decision rather than the cheap one.Domain-Specific Languages →
"Internal DSLs are just method chaining." Method chaining is one host's way of getting there. The defining property is that the calls build an inspectable program rather than doing the work.Internal versus External DSLs →
"External DSLs are more powerful." They have freer syntax. Power comes from semantics, and an internal DSL in a language with macros can be strictly more capable.Internal versus External DSLs →
"An internal DSL is a stepping stone and always the wrong final answer." It is frequently the right final answer, especially when the readers are the host language's own programmers.Internal versus External DSLs →
"Once I have a parser, the external DSL is done." The parser is the smallest piece of the work — see [[dsl-tooling-cost]].Internal versus External DSLs →
"SQL in a string is an external DSL, so I get the external benefits." You get the costs. The benefits require your own parser and checker, and a string has neither.Internal versus External DSLs →
"The default is no, so DSLs are a bad idea." SQL, regular expressions and every shader language passed this bar. The default exists because most proposals do not, not because none do.Should I Build a DSL? →
"We are only building a small language, so the tooling cost does not apply." Users of a small language expect the same diagnostics and editor support as users of a large one; the size of the grammar does not shrink the bill.Should I Build a DSL? →
"We will add the tooling once people use it." Adoption is bounded by the tooling, so this order does not converge.Should I Build a DSL? →
"A library cannot express this." Usually it can, less prettily. The cases where it genuinely cannot are the ones where the DSL removes a capability the host provides, which is worth naming explicitly.Should I Build a DSL? →
"The domain is stable because it has not changed recently." Check the concepts, not the values. Configuration keys changing is noise; what a rule *is* changing is the signal.Should I Build a DSL? →
"Compiling is always better than interpreting." Compiling to a host with different semantics is a correctness risk, and for a program evaluated once per request the difference is unmeasurable.Implementing a DSL →
"A bytecode VM is overkill for a small language." It is, unless you need to bound execution — at which point it is the cheapest way to get a step limit that actually works.Implementing a DSL →
"Generated code is an implementation detail." Users read it in stack traces and step through it in debuggers. It is an interface.Implementing a DSL →
"JSON with a schema is not really implementing a language." It is, and treating it as configuration rather than as language design is how these systems grow into unmaintainable ones.Implementing a DSL →
"We can add source spans later." Spans have to be threaded through every stage from the first line of the parser; retrofitting them means touching every stage — which is [[source-locations]], and it is the same lesson at a smaller scale.Implementing a DSL →
"YAML is broken." YAML behaves as specified. The specification chose implicit typing and significant indentation, and both choices have exactly the consequences they were always going to have.Configuration Languages →
"Quoting strings fixes the Norway problem." It fixes each instance. The class remains, because the hazard is that surface shape determines type, and the next unquoted value will do it again.Configuration Languages →
"YAML 1.2 fixed this." The core schema did, and most deployed parsers still default to 1.1 behaviour. What matters is which parser reads your file.Configuration Languages →
"A configuration language should be as expressive as possible." Expressiveness is what costs you static validation, review-by-reading and terminating evaluation. The restricted ones are restricted deliberately.Configuration Languages →
"Templating is simpler than adopting a configuration language." It is a language, with no parser for the pre-render text, no types and no diagnostics. It is the most expensive option, not the cheapest.Configuration Languages →
"The parser is the hard part." The parser has a textbook and a clear finish line. Everything else has neither, and everything else is what users touch.The Tooling Cost of a DSL →
"We can add the language server later." It requires a parser that recovers from errors and a front end organised as a query interface. If yours is not, later means a rewrite.The Tooling Cost of a DSL →
"Our users are internal, so the bar is lower." Internal users have the same expectations and less patience, and they cannot search the web for your error message.The Tooling Cost of a DSL →
"Good documentation makes up for bad error messages." An error message is read at the moment of confusion by someone who is not reading documentation. It is the documentation that matters most.The Tooling Cost of a DSL →
"A formatter is a nice-to-have." It is the cheapest way to permanently end a category of review argument, and it is far cheaper before the tree design is fixed than after.The Tooling Cost of a DSL →
"My code broke when I turned on optimization, so the optimizer is buggy." Far more often the optimizer started relying on an assumption the language gave it and your program violated. Run the sanitizers before forming the sentence.Miscompilation →
"The tests pass, so there is no miscompilation." The tests were compiled by the same compiler. A wrong transformation applied to both the assertion and the code under test is invisible.Miscompilation →
"Compiler bugs are so rare I can ignore them." Rare per compilation, common per ecosystem. The reason your compiler has few of them is that people run fuzzers at it continuously.Miscompilation →
"Adding volatile fixed it, so it was a compiler bug." volatile suppresses optimization on that object. It hides races and undefined behavior just as effectively as it hides compiler bugs, and it is not a synchronization primitive.Miscompilation →
"We have 90% coverage, so the compiler is tested." Coverage measures which lines ran, not which preconditions were violated. A legality guard is one line and needs two tests.Testing a Compiler →
"Golden tests are regression tests, so they establish correctness." They establish that behavior has not changed. If the recorded behavior was wrong, they lock the bug in and defend it.Testing a Compiler →
"Testing each pass in isolation is enough." Phase-ordering bugs are bugs in the interaction. By construction no isolated test can see them.Testing a Compiler →
"If the compiler builds a large project successfully, it works." Successfully building is a claim about crashes and rejections. Wrong-code bugs build perfectly.Testing a Compiler →
"The golden tests pass, so the compiler is correct." They pass because the output matches a recording. If the recording was wrong, they are defending the bug.Golden Tests →
"A big diff means a big change." It usually means you touched something heuristic. The size of a golden diff is a poor proxy for the size of a semantic change, which is exactly why blessing is dangerous.Golden Tests →
"Snapshot tests are lazy tests." They are the right tool for diagnostics and for tripwires on hard-won optimizations. They are the wrong tool for anything a heuristic decides, and the mistake is choosing the target, not choosing the technique.Golden Tests →
"We can add goldens now and worry about determinism later." Then the tests are flaky from day one and get disabled, and you have paid the cost and kept none of the benefit.Golden Tests →
"The two compilers produce different assembly, so one of them is wrong." They are supposed to. Only behavior is comparable.Differential Testing →
"A disagreement means the newer compiler is buggy." It means one of them is. Majority voting across three configurations is a heuristic, not a verdict, and the first suspect is your own program’s definedness.Differential Testing →
"Random program generation is easy, so this is easy." Generating programs is easy. Generating programs whose behavior the language pins down is the entire problem, and it is what Csmith is famous for.Differential Testing →
"If both compilers agree, the program is correct." They may agree because both exploit the same undefined behavior in the same way. Agreement is weak evidence and disagreement is strong evidence — the asymmetry is inherent.Differential Testing →
"Fuzzing means feeding random bytes to a program." That finds parser crashes. Finding wrong-code bugs needs a generator that produces valid, well-defined programs, which is a completely different artifact.Compiler Fuzzing →
"EMI mutations have to preserve the program’s meaning." They do not, and that is the point. They only have to be unreachable for the profiled input, which is a much weaker and much easier condition to guarantee.Compiler Fuzzing →
"If the fuzzer stops finding bugs, the compiler is correct." It means this generator has exhausted the shapes it can produce. Changing the generator usually restarts the flow immediately, which is itself the evidence.Compiler Fuzzing →
"Dead code cannot affect the compiler, so mutating it is pointless." Dead code changes inlining budgets, register pressure and CFG shape. That it affects the compiler is precisely why the technique works.Compiler Fuzzing →
"Translation validation proves the compiler correct." It proves something about the compilations it checked. Two different quantifiers, and the difference is the whole reason it is affordable.Translation Validation →
"If the validator passes, the code is correct." It says the transformation preserved the IR’s semantics. If the frontend lowered your source wrongly in the first place, both sides are equally wrong and the check is green.Translation Validation →
"A timeout means it is probably fine." A timeout means nothing was checked. A tool that conflates the two is worse than no tool, because it produces confidence without evidence.Translation Validation →
"The relation is that the two programs are equivalent." It is refinement, and it is one-directional. Equality would reject every optimization that narrows behavior or exploits undefined behavior.Translation Validation →
"A verified compiler cannot produce wrong code." It cannot produce wrong code in the verified region for a well-defined source program under a trusted semantics. Every clause of that sentence is doing work.Verified Compilers →
"Verification means bug-free." Bugs were found in CompCert — in the parser, in unverified support code, and as rejections and crashes. What was not found is a wrong-code bug in the verified middle-end.Verified Compilers →
"If it is proven, testing is unnecessary." The unverified perimeter still needs the ordinary test suite, and the semantics needs validating against the standard by human review, which is testing by another name.Verified Compilers →
"Verified compilers are slow, so the technique is impractical." The generated code is competitive at moderate optimization; the cost is at the aggressive end. And in the settings where this is used, the alternative is not a faster compiler but a much larger review burden.Verified Compilers →
"The compiler removed my security code, so it is a compiler bug." It is a legal transformation. The language’s observability model does not include an attacker reading memory after the function returns, so the store is dead by definition. Use a function that is specified not to be elided.Compilers and Security →
"volatile makes it secure." volatile prevents elision of the access on mainstream compilers today. It is not a specification, not a synchronization primitive, and not a defence against anything other than the optimizer.Compilers and Security →
"Disabling optimization would fix these problems." It masks some of them at a large performance cost, leaves the undefined behavior in place, and does nothing at all about a hostile toolchain.Compilers and Security →
"We compile from source, so the supply chain is covered." The compiler, the linker, every plugin, the build system and every dependency that runs code at build time are all inside the trust boundary, and none of them is your source.Compilers and Security →
"The static analyser found no issues, so the code is clean." It found no issues of the classes it checks, on the paths it explored, within its budget, with the models it has. Silence and safety are not distinguishable from the outside.Static Analysis →
"False positives mean the tool is broken." For a sound analysis they are a mathematical consequence of over-approximation. The question is the *rate*, and whether the tool tells you which assumption it could not discharge.Static Analysis →
"Static analysis is a security thing." Type checking, unused-variable warnings, exhaustiveness checking and dead-code detection are all static analysis. Security scanning is one application of a framework the compiler already runs.Static Analysis →
"We can just make it sound and precise if we throw more compute at it." Rice's theorem is not an engineering budget. Undecidability does not yield to a bigger machine; it yields only to restricting the language or accepting one of the two errors.Static Analysis →
"A finding in a file I did not touch is not my problem." Baselines exist for exactly this and are the right answer — but a baseline is a debt register, not a deletion.Static Analysis →
"Abstract interpretation simulates the program with fake values." It does not simulate an execution; it computes a property that holds over *all* executions simultaneously. There is no single trace being followed.Abstract Interpretation →
"Widening is a hack to make the tool faster." It is what makes the analysis terminate at all on an infinite-height domain. Without it the fixed-point iteration does not finish, at any speed.Abstract Interpretation →
"If the domain is sound, the tool is sound." Soundness of the domain plus unsound library models equals an unsound tool. Most real unsoundness lives in the models, not in the lattice.Abstract Interpretation →
"A more precise domain is strictly better." It is strictly slower, and on code that is dynamic enough it is equally imprecise while costing more — the precision only materialises if the program is in the shape the domain can describe.Abstract Interpretation →
"This is formal methods, so it does not apply to my compiler." Constant propagation, value-range propagation and flow-sensitive null checking are all abstract interpretation, and at least one of them ran on your last build.Abstract Interpretation →
"Control-flow analysis means analysing if-statements and loops." That is the intraprocedural half and it is the easy half. The term of art means resolving which functions a call site can reach.Control-Flow Analysis →
"The compiler knows what my function calls." For direct calls, yes. For a call through a variable, an interface or a function pointer, it knows a *set*, and the size of that set is what determines whether anything downstream can be optimized.Control-Flow Analysis →
"Tree shaking removes code that is not used." It removes code that is not *reachable in the graph it built*. Anything reached by a name looked up at runtime is invisible to that graph and needs to be declared.Control-Flow Analysis →
"A JIT does control-flow analysis better than a static compiler." It does something different: it observes rather than proves, so it is more precise about what happened and guarantees nothing about what could happen. That is why it needs guards.Control-Flow Analysis →
"If two subclasses exist, the call cannot be devirtualized." It can, if the analysis can prove only one is ever instantiated on the reachable paths — which is exactly what rapid type analysis is for.Control-Flow Analysis →
"Interprocedural means the compiler looks at the whole program." It means it looks past one function. Whole-program is a stronger and much rarer condition, and needs LTO or a closed world to be true at all.Interprocedural Analysis →
"Context-sensitive analysis is more accurate, so tools should use it." It is more accurate and exponentially more expensive, and the accuracy only materialises where callers genuinely differ. Most production toolchains buy the same precision more cheaply with inlining.Interprocedural Analysis →
"Summaries lose precision because they are approximations." A conditional summary can be as precise as re-analysis for the facts it can express. The loss is in expressiveness of the summary language, not in the idea of summarising.Interprocedural Analysis →
"If it works within a file it will work across files." Across a translation unit boundary the body is not there. Without LTO or shipped IR the analysis has nothing to summarise and falls back to the worst case.Interprocedural Analysis →
"The analysis is slow because the algorithm is bad." Usually it is slow because the call graph is dense or the context abstraction is too fine. Both are modelling choices, and both are fixable without touching the algorithm.Interprocedural Analysis →
"If it were important, the compiler would reject it." The compiler rejects what the language defines as invalid. A missing await is a real bug in every language that has one, and no language definition forbids it.Linters →
"Lints are about style." Style is one category. Floating promises, resource leaks, always-true comparisons and unsafe regex patterns are defect detection that happens to ship in a linter.Linters →
"More rules means better code." Past a precision threshold more rules means more suppressions and less attention on the rules that were working.Linters →
"An autofix is safe because a tool wrote it." An autofix is safe only if the rule proved it behavior-preserving. Several widely used fixes are explicitly documented as suggestions, not guarantees.Linters →
"We should lint formatting too, for consistency." A formatter makes formatting non-negotiable and produces no diagnostics at all. Every formatting lint you keep is noise in the channel where real findings appear.Linters →
"A formatter is a cosmetic tool." Its real product is a normal form, and the normal form is what makes diffs semantic and codemods reviewable. The aesthetics are incidental.Formatters →
"I can write one with regular expressions." You can write one that works on the examples you tried. Structure is required to know whether a brace is code or text, and comments are required to be preserved, and neither survives a regex.Formatters →
"An AST is enough — formatting does not change meaning." Formatting does not change meaning, which is exactly why the AST discarded the material the formatter needs. Comments and blank lines are not in the AST.Formatters →
"The formatter chose a strange line break." Almost always the group did not fit, so every separator in it broke. That is one rule applied consistently, not a heuristic misfiring.Formatters →
"Formatting is safe by definition." Not in JavaScript, where a line break can trigger semicolon insertion; not in Python, where indentation is syntax; not in C, where a macro ends at the newline.Formatters →
"A parse tree and a concrete syntax tree are different things." They are the same thing under two names, except that "concrete syntax tree" is usually used when trivia preservation is explicitly part of the contract.The Concrete Syntax Tree →
"Comments are stored as nodes in the tree." In every full-fidelity design worth copying they are trivia attached to a token, because a comment can appear between any two tokens and making it a node would require a slot everywhere.The Concrete Syntax Tree →
"The CST is what the compiler uses." Compilers overwhelmingly work on an AST. The CST exists for the tooling layer, and in many toolchains the compiler proper never sees it.The Concrete Syntax Tree →
"Full fidelity means you cannot change anything." It means every change is explicit. Editing is done by replacing a subtree and letting the unchanged siblings persist, which is precisely what makes minimal diffs possible.The Concrete Syntax Tree →
"If I keep spans on my AST nodes I have the same thing." Spans tell you where a node was. They do not tell you what was between two nodes, which is where every comment lives.The Concrete Syntax Tree →
"A language server is the compiler with a socket on it." It is the frontend rebuilt around three requirements the compiler does not have. Toolchains that tried the socket approach ended up rewriting the frontend anyway.The Language Server →
"Completion is a text-matching feature." Useful completion needs the type of the expression before the cursor, which needs resolution, which needs a tree — at the moment the code does not parse. That is why it is the hardest feature and the one that most often degrades to identifier matching.The Language Server →
"If it is slow, the machine is too small." Usually a dependency is over-broad, so an edit invalidates far more than it should. The fix is in the invalidation graph, not in the hardware.The Language Server →
"The IDE errors are wrong; the build is what matters." Both are right about different configurations. Almost always the server inferred different compilation settings, and the fix is to give it the real ones.The Language Server →
"Restarting the language server fixes bugs." It flushes a cache that had gone stale. The bug is a missing dependency edge, and it is still there.The Language Server →
"LSP is a language server." It is the protocol. The server is the program that implements it, and everything difficult — incrementality, error tolerance, the analysis itself — lives there, not in the protocol.The Language Server Protocol →
"If a server speaks LSP, the editor experience will be good." The protocol fixes the messages and says nothing about latency, correctness or which capabilities are implemented. A conforming server can recompile the world on every keystroke.The Language Server Protocol →
"Positions are character offsets." They are UTF-16 code-unit offsets by default. Those coincide only for text in the Basic Multilingual Plane, and diverge from bytes as soon as anything is non-ASCII.The Language Server Protocol →
"Diagnostics are returned when the editor asks." They are pushed by the server as notifications in the original design, because only the server knows when the analysis finished. A pull model was added later and both are in use.The Language Server Protocol →
"The protocol solved editor tooling." It solved the *integration* problem. Building a server that is fast and correct is the hard part and it is entirely unchanged by the protocol existing.The Language Server Protocol →
"Renaming is a text operation." It is a query over the symbol table plus a capture check. The text edit is the last and easiest step.Semantic Refactoring →
"If it compiles after the rename, the rename was correct." Capture produces code that compiles and binds differently. Compilation success is necessary and not sufficient.Semantic Refactoring →
"An AST-based codemod is safe because it understands the code." It understands the syntax. Unless it resolves names, it cannot tell your size from someone else's, and it is structurally equivalent to text replacement for that purpose.Semantic Refactoring →
"The IDE renamed everything, so I am done." It renamed everything in its index. Reflection, string keys, other languages, disabled configurations and external consumers are all outside it.Semantic Refactoring →
"Rename is the simple refactoring." It is the one with the simplest edit and a validity condition that most tools implement incompletely. Extract method has a harder analysis and a much more obvious failure when it goes wrong.Semantic Refactoring →
"Debug information makes the program slower." It adds sections to the file that are never loaded into memory during normal execution. It costs size and link time, and the instructions are the same — verify with a diff of the disassembly.Debug Information →
"A variable has an address." It has a location list. In optimized code a local frequently has no single home, and for part of its scope no home at all.Debug Information →
"Stripping the binary is a security measure." It removes names from the shipped artifact and does not remove the code or the behaviour. It also removes your ability to read a crash report unless the symbols were archived first.Debug Information →
"If the debugger shows a value, the value is right." It shows what the location list claims for that address. At high optimization levels the list can be stale or absent, and a stale entry looks exactly like a good one.Debug Information →
"-g and -O are alternatives." They are orthogonal flags. -g -O2 is a valid and usually correct combination, and it is what shipped software should be built with.Debug Information →
"The browser is running my TypeScript." It is running the minified JavaScript. The map only changes what devtools *displays* and how stack frames are reported.Source Maps →
"Source maps make the bundle slower." The bundle is byte-identical; the map is a separate file fetched only when a developer opens devtools, or by the error reporter out of band.Source Maps →
"If devtools shows my source, the maps are correct." It shows what the final map points at. Verify by reading the sources array, and by checking that a breakpoint actually binds where you set it.Source Maps →
"A source map is like DWARF." It is like the DWARF *line table* specifically. There are no variable locations, no types and no scopes — only positions and an array of original names.Source Maps →
"We disabled source maps in production for performance." Almost certainly the intent was privacy or artifact size. Generate them and upload them out of band; you lose nothing at runtime and gain readable crash reports.Source Maps →
"Release builds cannot have debug information." They can, they should, and the instructions are unchanged. Size is handled by splitting the symbols out, not by omitting them.Debug and Release Builds →
"It works in debug, so my code is correct and the optimizer is wrong." Far more often the program has undefined behavior that -O0 happened to tolerate. Run the sanitizers before forming the conclusion.Debug and Release Builds →
"-O3 is the fastest setting." It enables more aggressive inlining and unrolling, which grows code and can lose to -O2 through instruction-cache pressure. Measure on the real workload.Debug and Release Builds →
"Stripping the binary protects the code." It removes names from the shipped file. The behaviour is unchanged, and you have given up your own ability to read a crash unless you archived the symbols first.Debug and Release Builds →
"Assertions should stay on in production for safety." Sometimes — but an assert that aborts a service on a recoverable condition is a availability problem. Decide per assertion, and prefer logging an invariant violation to terminating on it.Debug and Release Builds →
"Optimized out means the debugger could not find the value." It means the value does not exist at this address. Nothing wrote it anywhere, because nothing needed it after its last use.Debugging Optimized Code →
"The variable is in scope, so it must have a value." Scope is a source-level property and liveness is a machine-level one. They stopped agreeing at register allocation.Debugging Optimized Code →
"The stack trace is wrong — that function is definitely on the stack." If it was inlined it has no frame, and if the call was in tail position the caller's frame is gone. Ask for inline frames explicitly before concluding the trace is broken.Debugging Optimized Code →
"I will just rebuild at -O0 to look at it." That changes timing, frame layout and inlining, and if the bug depends on any of those it will disappear. Try -Og or a single-function optnone first.Debugging Optimized Code →
"The compiler is jumping around because the debug info is broken." Interleaved line records are the correct description of scheduled code. Two source lines really are executing at once.Debugging Optimized Code →
"We ship stripped binaries, so we cannot symbolicate." Stripping is exactly the intended arrangement — provided the symbols were archived first and can be found by build ID.Symbolication →
"I can rebuild from the same tag to get symbols." Only if the build is reproducible. Otherwise the addresses shift and you get a trace that reads correctly and points at the wrong lines.Symbolication →
"The stack trace is complete." Not unless inline frames were expanded. A missing -i drops exactly the small hot functions where faults tend to be.Symbolication →
"Symbolication is a debugger feature." It is a build-and-release pipeline. The tool that does the lookup is trivial; keeping the symbols and matching them is the entire problem.Symbolication →
"Addresses in a crash report are meaningful." They are meaningful only relative to a module load base that ASLR chose at runtime. Without the module map they resolve to nothing.Symbolication →
"Fewer instructions means faster." Not reliably. A short dependent chain can lose to a longer independent one, and the out-of-order machine schedules underneath whatever you read.Reading What the Compiler Produced →
"The compiler did not do X." Check with a remark before asserting it. And if it genuinely did not, the missed remark usually names the specific fact it could not prove.Reading What the Compiler Produced →
"I saw it on Compiler Explorer, so that is what my build does." Your build has different flags, different headers, a different inlining context and possibly LTO. Re-check in context.Reading What the Compiler Produced →
"The assembly is the truth." It is the contract handed to the CPU, not the schedule the CPU executes. Instruction-level parallelism, renaming and speculation all happen after it.Reading What the Compiler Produced →
"This function is missing from the output, so it was optimized away." A static function with no callers is simply never emitted. Give it external linkage and look again.Reading What the Compiler Produced →
"The compilation unit is the file." It is the file *after preprocessing* in C and C++, and it is not the file at all in Rust, Go or Java.Compilation Units →
"Splitting a big file into ten small ones makes the build faster." It makes it more parallel and more incremental. If all ten include the same headers, the total parse work goes up, not down.Compilation Units →
"Header-only libraries have no build cost." They have no *link* cost. They move the entire cost into every unit that includes them.Compilation Units →
"If it links, the units agreed." Linkers match symbols, not meanings. Two units can hold contradictory definitions of the same type and link without complaint.Compilation Units →
"Separate compilation is just how compilers work." It is a choice, made for build economics. A compiler that reads the whole program at once is simpler and produces better code.Separate Compilation →
"The linker will catch mismatches." It matches symbol names. Layout, invariants and semantics are outside its knowledge entirely.Separate Compilation →
"Since each unit is optimized, the program is optimized." Each unit is optimized in ignorance of the others. The most valuable optimizations in a large program are exactly the ones that need both.Separate Compilation →
"LTO makes separate compilation unnecessary." LTO is built on top of it. The units are still compiled separately; what changes is what they contain.Separate Compilation →
"Modules are just namespaces with better syntax." Namespacing is one of four jobs, and the only one that costs nothing. The build-time job is the expensive and valuable one.Modules as Units of Separate Compilation →
"Modules make builds faster." They remove repeated parsing and add artifact management. On a codebase where parsing was not the bottleneck, they can make builds slower.Modules as Units of Separate Compilation →
"A module boundary is free at runtime." It is free only if the optimizer can still see through it. Making a boundary opaque removes cross-boundary inlining, and that is a runtime cost.Modules as Units of Separate Compilation →
"If the language has import, it has separate compilation." Python has import and compiles per module at run time; the two properties are independent.Modules as Units of Separate Compilation →
"An interface file is documentation." It is an input to the compiler. Its contents decide what type-checks and, in several languages, what gets inlined.Interface Files →
"A .d.ts is checked against the JavaScript." Nothing checks it. It is an assertion about code the type checker has never seen.Interface Files →
"If the interface did not change, nothing downstream can be affected." True for correctness, not for performance: an unpublished body change can still change what a dependent's already-compiled code does, because the call is real.Interface Files →
"Interface files can be cached across machines like object files." Only if the compiler version, flags and target match exactly. They are far more version-fragile than object files.Interface Files →
"Incremental compilation means only changed files are recompiled." It means only *affected* results are recomputed, which is both less — thanks to early cut-off — and more, since dependents can be affected without changing.Incremental Compilation →
"It is just caching." Caching without recorded dependencies is how you get stale output. The dependency tracking is the hard and load-bearing part.Incremental Compilation →
"Incremental builds are always faster." The first one is slower, CI builds are slower, and an edit that invalidates a root definition is slower than the equivalent non-incremental build because of the bookkeeping.Incremental Compilation →
"If it compiles incrementally, a clean build gives the same result." That is the goal, not a guarantee. It is exactly what a missed dependency edge breaks.Incremental Compilation →
"If A depends on B and B changes, A must be rebuilt." Only if B's *interface* changed. Most edits do not change it, which is why real rebuild sets are small.The Build Dependency Graph →
"The build graph is the include graph." The include graph is one source of edges. Flags, the compiler binary, generated files and the target are edges too, and forgetting them is the standard cause of stale builds.The Build Dependency Graph →
"More cores will fix the build." Not past the critical path. A deep chain of dependencies has a serial floor no amount of hardware removes.The Build Dependency Graph →
"Declaring dependencies by hand is fine if you are careful." Every codebase that tried this has missing edges. The compiler already knows what it read; use that.The Build Dependency Graph →
"The build system knows about my code." It knows filenames and command lines. Everything it appears to know about your code came from the compiler reporting it.Where the Compiler Ends and the Build System Begins →
"Timestamps are fine in practice." They fail on checkout, on restore, on same-second writes and on unconditional generators — all of which happen daily.Where the Compiler Ends and the Build System Begins →
"Content hashing makes builds slower." It adds I/O and removes rebuilds. On any project past trivial size the second dominates, which is why mature systems all moved to it.Where the Compiler Ends and the Build System Begins →
"If the key matches, the artifact is correct." Only if the key is complete. An undeclared input makes a matching key meaningless.Where the Compiler Ends and the Build System Begins →
"We build in Docker, so the build is hermetic." A container pins the base filesystem and nothing else. Network access, environment variables, mutable tags and host mounts all still reach inside.Hermetic Compilation →
"It builds the same everywhere, so it must be hermetic." It builds the same everywhere *so far*. Undeclared inputs that happen to agree today are the failure that is waiting, not the absence of one.Hermetic Compilation →
"Hermetic and reproducible mean the same thing." One is about what the build can read; the other is about what it writes. A hermetic build of a compiler that embeds a timestamp is still not reproducible.Hermetic Compilation →
"The compiler is part of the environment, not an input." It is the single most important input. A compiler upgrade that does not invalidate the cache is exactly how a stale object survives into a release.Hermetic Compilation →
"The highest optimization level is the right default for release." It is a defensible default, and on a program with a large working set it can be slower than a lower one. The workload decides.Compile Time versus Runtime →
"Slow builds are a machine problem." Machines are the cheap half. The expensive half is the changes engineers stop attempting when the loop is long.Compile Time versus Runtime →
"Compile time does not matter because CI is asynchronous." CI is not where the cost lands. The cost lands on the engineer waiting to know whether the last edit worked.Compile Time versus Runtime →
"If I turn on more optimization and nothing gets faster, the compiler is bad." The far more likely reading is that the generated code was not the bottleneck, which is a finding about your program, not the toolchain.Compile Time versus Runtime →
"Whole-program optimization means the compiler reads all my files." It means the *optimizer* sees all the bodies at once, which normally requires deferring code generation to link time. Reading the files is not the hard part.Whole-Program Optimization →
"Devirtualization works because the compiler is clever." It works because the world is closed. The same cleverness applied to a program that loads plugins is a miscompilation.Whole-Program Optimization →
"It only makes the binary bigger, because of inlining." It usually makes it smaller: dead-function elimination across the whole program typically removes more than inlining adds.Whole-Program Optimization →
"If it is slower to build, it must be faster to run." The build cost is certain and the runtime gain is not. On code that is not the bottleneck it is a pure loss.Whole-Program Optimization →
"LTO is an optimization level." It is a change to when code generation happens. The optimization level still applies, and applies at the link.Link-Time Optimization →
"ThinLTO is just LTO with a lower budget." It is a different algorithm: summaries and an import plan, rather than a merged module with fewer passes.Link-Time Optimization →
"LTO introduced a bug." It made an existing undefined-behavior or ODR problem exploitable. Turning the flag off hides the defect instead of fixing it.Link-Time Optimization →
"If the link succeeded, LTO ran." Objects produced without a plugin-aware archiver, or precompiled third-party objects, are silently linked as ordinary machine code.Link-Time Optimization →
"PGO tells the CPU which way branches go." Mostly the hardware already knows. The gain is dominated by where the instructions physically sit, which the predictor cannot help with.Profile-Guided Optimization →
"The profile lets the compiler remove paths that never run." Never, in a static compiler. An unexecuted path is still reachable; removing it needs a runtime guard, which is a JIT's mechanism.Profile-Guided Optimization →
"Any profile is better than none." An unrepresentative profile is worse than none, because it optimizes confidently for the wrong path — the subject of the next lesson.Profile-Guided Optimization →
"PGO makes the binary bigger." It commonly makes it smaller, because cold call sites stop being inlined while hot ones inline harder.Profile-Guided Optimization →
"Some profile is better than no profile." Only if it resembles production. A confidently wrong profile puts hot code in the cold section, which a heuristic build would never have done.What a Profile Costs You →
"The compiler will warn me if the profile is stale." Only if you asked it to. The sound behavior — dropping the profile for that function — is silent by default in mainstream toolchains.What a Profile Costs You →
"Sampled profiles are just lower-quality instrumented ones." They are a different trade: worse resolution, far better representativeness and freshness, and freshness is usually the one that matters.What a Profile Costs You →
"We checked in the profile, so the build is reproducible again." It is reproducible given the profile. It is no longer rebuildable from the source tree alone, and the cache key has to say so.What a Profile Costs You →
"A JIT is just PGO that happens at run time." It is PGO plus a guard. The guard is what permits transformations PGO can never apply, and it is the entire difference in capability.Feedback-Directed Optimization →
"A static compiler with a good enough profile could do what a JIT does." It could make the same decisions and not the same commitments. Without a place to put the undo path, speculation is unavailable at any level of analysis.Feedback-Directed Optimization →
"Feedback-directed means it adapts while running." Only in the dynamic case. PGO is feedback-directed and completely fixed once the binary is built.Feedback-Directed Optimization →
"Guards are an implementation detail of JITs." They are the licence. Remove the guard and every speculative transformation becomes a miscompilation.Feedback-Directed Optimization →
"The build is slow because the optimizer is doing a lot." In C++ and Rust the frontend usually dominates, and in C++ most of that is parsing the same headers repeatedly.The Compiler Is Also a Program With Performance Requirements →
"More cores will fix it." Only for the parallel part. Linking, full LTO and peak memory are all ceilings that cores do not raise.The Compiler Is Also a Program With Performance Requirements →
"Compile time is a machine cost." The machine is the cheap half. The expensive half is the changes engineers stop attempting.The Compiler Is Also a Program With Performance Requirements →
"A faster compiler is a worse compiler." It is a compiler making a different trade, sometimes with language support that removes the cost entirely rather than absorbing it.The Compiler Is Also a Program With Performance Requirements →
"The model returns JSON, so the output is structured." JSON is a syntax, not a schema and not a type system. {"tool": "delete_everything"} is perfectly well-formed JSON.A Plan Is a Program →
"Validation makes the agent safe." Validation makes the agent's output well-formed and permitted. Whether it is the right action is a semantic question no validator answers.A Plan Is a Program →
"If the plan type-checks, the arguments are correct." They are of the correct *kind*. An integer amount that is off by a factor of a hundred type-checks perfectly.A Plan Is a Program →
"This is just input validation with extra steps." Input validation checks a value against a predicate. This resolves names, checks types across a whole tree, and records authorization decisions — the extra steps are the ones that catch the interesting failures.A Plan Is a Program →
"We can skip the parser because the model almost always gets the format right." Almost always is the property that makes it dangerous: the failures are rare, correlated with unusual inputs, and therefore concentrated exactly where you are least able to reason about them.A Plan Is a Program →
"The plan DSL is just a JSON schema." A schema constrains one value. A grammar plus a resolver constrains a whole tree, including references between steps, which is where the interesting errors are.Agent DSLs and the Plan AST →
"An explicit plan slows the agent down." The parse costs microseconds against a model call measured in seconds. What it costs is expressiveness, and that is the trade worth arguing about.Agent DSLs and the Plan AST →
"If the plan parses, the tools exist." Parsing establishes shape. Name resolution against the registry is a separate phase, and conflating them produces a runtime error where a diagnostic belonged.Agent DSLs and the Plan AST →
"We can optimize the plan the way a compiler optimizes IR." Only with the purity and idempotence metadata that makes the rewrite legal. Compilers have that information about their own instructions; a tool registry usually does not have it about tools.Agent DSLs and the Plan AST →
"The model supports structured output, so the arguments are validated." Structured output constrains the syntax. Whether amount_cents is within your business limits is a check you still have to run.Typed Tool Calls →
"A schema is documentation." It is a contract that is enforced. If it is not the same artifact the model is shown, it is a contract one party has not read.Typed Tool Calls →
"Type-checking the call makes the call safe." It makes the call well-formed. Authorization is a separate phase and semantic correctness is not a phase at all.Typed Tool Calls →
"Extra fields are harmless, the tool ignores them." An ignored field is a divergence between what the model believed it requested and what happened, and it is invisible. That is worse than an error.Typed Tool Calls →
"Constrained decoding is strictly better." It is strictly better at syntax and can be worse at content, because the model is being prevented from taking the path it had the most probability mass on.Typed Tool Calls →
"Validation and authorization are the same check." Validation asks whether the request is well-formed. Authorization asks whether this caller may make it. A perfectly valid request from the wrong principal is the entire point of the second gate.Parse, Validate, Authorize, Execute →
"If each tool checks its own permissions we are covered." You are covered against calling a forbidden tool, and not against a plan that performs three permitted destructive steps before reaching the forbidden one.Parse, Validate, Authorize, Execute →
"The order does not matter as long as all the checks run." It matters twice: precise policy needs typed arguments, and an informative type error leaks a tool the caller was not allowed to know about.Parse, Validate, Authorize, Execute →
"Recording allows is noise; log the denials." The allows are the audit trail. A question about what an agent was permitted to do cannot be answered from denials.Parse, Validate, Authorize, Execute →
"JSON.parse in a try/catch is a parser." It is a parser with one bit of output. It cannot tell you where the problem was, what was expected, or whether it was recoverable, and those are the three things you need.Recovering Structure From Model Output →
"Auto-repair improves reliability." Deterministic repair improves reliability. Guessing repair converts loud failures into quiet wrong answers, which is worse on every axis that matters.Recovering Structure From Model Output →
"If it parses, we are fine." Parsing establishes shape. A perfectly-shaped call to the wrong tool with well-typed arguments parses beautifully.Recovering Structure From Model Output →
"Retries are free, we already have the model call budgeted." Retries multiply latency and cost, and they change the output — a system with silent retries has a different reproducibility story than its authors believe.Recovering Structure From Model Output →
"Just ask the model to be more careful about the format." Sometimes it helps and it is never a guarantee, because the format is being produced by sampling. Guarantees come from constrained decoding or from validating what came back.Recovering Structure From Model Output →
"It is a simulation of a compiler." It is a compiler. It lexes, parses, checks, lowers, optimizes, allocates and executes, and the panels are its output rather than illustrations of it.AtlasLang: The Whole Thing →
"It optimizes aggressively, so real compilers must too." It optimizes aggressively because AtlasLang defines almost nothing as observable. A language with pointers, threads or floats hands its optimizer a far harder problem.AtlasLang: The Whole Thing →
"The assembly panel shows what my CPU would run." It shows an x86-64-flavoured listing for the allocation the allocator chose. It is not assembled, and it models the calling convention only far enough to be legible.AtlasLang: The Whole Thing →
"If a program works here it is correct." AtlasLang integers do not wrap and the VM stops at a step budget. Both are places where a real target behaves differently.AtlasLang: The Whole Thing →
"The lexer understands the program." It classifies characters into groups. It does not know that a name refers to anything, that an operator has a precedence, or that a sequence of tokens is a valid statement.AtlasLang: The Lexer →
"Maximal munch is a heuristic." It is a rule, and it is implemented as an ordering: longer alternatives are tested first. There is no ambiguity in the implementation.AtlasLang: The Lexer →
"123abc is a lexer error in every language." In many it is two tokens and a parser error, which is exactly the confusing outcome our lexer exists to avoid. The detection is a design choice, not a requirement.AtlasLang: The Lexer →
"Spans are for error messages." They are for error messages, cross-panel highlighting, debug line tables, source maps, go-to-definition and every refactoring tool. They are the most reused output of the phase.AtlasLang: The Lexer →
"Precedence comes from the grammar." In this parser it comes from a table of integers. In a generated LR parser it comes from precedence declarations. Either way it is data somebody wrote, not a property of arithmetic.AtlasLang: The Parser →
"The parser rejects invalid programs." It rejects programs with invalid *structure*. let x: bool = 5; parses perfectly and is rejected two phases later.AtlasLang: The Parser →
"Panic-mode recovery means the parser guesses what you meant." It means it skips to a place it can be confident about and resumes. It guesses nothing; that is why the synchronization set is so small.AtlasLang: The Parser →
"Extra errors after the first are the parser being thorough." They are usually artefacts, which is exactly why they are flagged as cascading rather than presented alongside the real one.AtlasLang: The Parser →
"AtlasLang runs by walking the tree." It does not. It lowers to IR, optimizes and runs bytecode on a VM. This lesson describes the version it deliberately does not have.AtlasLang: Evaluating the Tree Directly →
"Tree-walking is the beginner version." It is the version whose correctness is easiest to argue and is frequently kept as the reference implementation for exactly that reason.AtlasLang: Evaluating the Tree Directly →
"Interpreters are slow because interpretation is slow." A tree-walker is slow because of per-node dispatch, pointer chasing and name lookups. A bytecode interpreter is also an interpreter and is far faster.AtlasLang: Evaluating the Tree Directly →
"You could optimize the tree instead of lowering it." You can rewrite a tree, and some compilers do. What you cannot do is ask it which definition reaches a use without building a different representation first.AtlasLang: Evaluating the Tree Directly →
"Shadowing is a language feature, so the compiler must implement it." Nothing implements it. It falls out of resolving from the innermost scope outward. What has to be implemented is not undoing it later.AtlasLang: Scopes, Shadowing and a Real Bug →
"The bug was a typo." It was a design decision — keying storage by name — that looked correct and is correct for every program without shadowing, which is most of the programs you would test by hand.AtlasLang: Scopes, Shadowing and a Real Bug →
"Symbol ids are an internal detail." They are the identity the entire back half of the compiler is written against. Any phase that falls back to the name reintroduces the bug in its own corner.AtlasLang: Scopes, Shadowing and a Real Bug →
"Two variables with the same name should share storage since only one is live." Only if that has been proved. Deciding when distinct values may share a location is register allocation, and it needs liveness that does not exist at lowering time.AtlasLang: Scopes, Shadowing and a Real Bug →
"The compiler could easily see that while (true) always returns." It could, with a special case for literal conditions, which is what Java does. The general question — which loops are entered — is undecidable, so any rule is a line drawn somewhere.AtlasLang: Three Types and One Honest Limitation →
"Rejecting correct programs is a bug." It is the chosen direction of incompleteness. The alternative direction accepts functions that fall off their end, which is a miscompilation rather than an inconvenience.AtlasLang: Three Types and One Honest Limitation →
"Type inference means you never write types." It means you do not write the ones that are locally derivable. Parameters and returns are required here for the same reason they are required in Rust, Go and C#.AtlasLang: Three Types and One Honest Limitation →
"It type-checks, so it will not fail at run time." It will not fail *in the ways the type system models*. Division by zero is not one of them, and no annotation in this language would make it one.AtlasLang: Three Types and One Honest Limitation →
"The bytecode is the optimized program." It is emitted from the pre-SSA IR, so it reflects the unoptimized form. The optimized SSA panel is a different program, and a phi node is why.AtlasLang: Bytecode and the Stack Machine →
"The STORE then LOAD is a bug." It is the virtual-register-to-slot mapping, kept deliberately so the IR and the bytecode line up instruction for instruction. A peephole pass would remove it.AtlasLang: Bytecode and the Stack Machine →
"Stack machines are slow because of the stack." They execute more instructions than a register machine, and the stack itself is an array with a top index. The cost is instruction count and dispatch, not the data structure.AtlasLang: Bytecode and the Stack Machine →
"The step budget is a bug I should raise." It is a design decision that makes a non-terminating program report steps-exhausted instead of hanging. The status field is the result.AtlasLang: Bytecode and the Stack Machine →
"The optimizer removes code that does nothing." It removes code whose *value* nothing uses and which has no effect. Those are two conditions, and print fails the second while satisfying the first.AtlasLang: Eight Passes and Two Guards →
"10 / 0 should be a compile error." Then a program with an unreachable division by zero would fail to build. The compiler declines to fold it and lets it fault where it would have faulted.AtlasLang: Eight Passes and Two Guards →
"More passes means better code." Passes enable each other and can also undo each other, and no single order is best for every program. That is phase ordering, and it is why the cap exists.AtlasLang: Eight Passes and Two Guards →
"This is what the VM runs." The VM runs bytecode emitted from the pre-SSA IR. The optimized SSA panel is a different program, and a phi node is the reason.AtlasLang: Eight Passes and Two Guards →
"Tooling comes after the language is finished." The tooling requirements change how the frontend is built, and retrofitting trivia or incrementality into a batch compiler usually means rewriting it.AtlasLang: What a Language Owes Its Users →
"A language server is a plugin." It is a process holding the compiler frontend, subject to four requirements a batch compiler does not have — chiefly that it works on code that does not compile.AtlasLang: What a Language Owes Its Users →
"Syntax highlighting is easy." Approximate highlighting is easy. Highlighting that is right about shadowing, unresolved names and the difference between a type and a variable requires the checker.AtlasLang: What a Language Owes Its Users →
"Good diagnostics are a matter of writing better messages." They are mostly a matter of recovery, non-cascading errors and spans. The wording is the last five percent.AtlasLang: What a Language Owes Its Users →