Designtypical

The Trade Nobody Escapes

Seven requirements, seven answers, and no row where the answer is a language. Every decision in this module buys one property by paying for it somewhere specific, and the discipline is naming the payment out loud.

The question

Given what I actually need, which set of language design decisions follows — and what am I paying for it?

SourceLexingTokensParsingASTSemanticsTypedIROptimizeCodegenMachine codeLinkExecute
What the program is here

The design space as a set of requirement-to-decision implications rather than as a ranking. Each row starts from something the system must do, names the decision that satisfies it, and names what that decision costs. Nothing here is a program, and the representation under construction is the same one from [[who-is-the-language-for]] — a language definition — now read as a set of purchases with prices attached.

What this phase may assume or do

A design decision is defensible only if the property it claims is actually enforced by something: a checker that rejects, a runtime that prevents, or a structural prohibition. Claiming memory safety while providing unchecked reinterpretation, or claiming determinism while permitting an unbounded collector pause, is not a trade-off — it is a guarantee that is not one. The precondition on every row below: name the enforcement mechanism, or the row is marketing.

Key points

  • There is no best language; there are requirements, the decisions they imply, and the prices those decisions carry.
  • The seven common requirements each have a well-understood answer, and each answer has a specific, nameable cost.
  • Conflicts between requirements are the real design work, and the standard resolutions — restricted dynamism, gradual typing, hybrid memory models, split systems — all weaken a guarantee locally in exchange for the other requirement.
  • The four costs people forget are build time, diagnostic architecture, correct programs rejected, and the annual tooling bill.
  • A design decision is only arguable if it names what it costs and who enforces the property it claims.

Requirements, not rankings

There is no best language, and any sentence that reaches for one has stopped being a design argument. What there is, is a set of requirements, each of which implies a decision, each of which has a price that someone pays. The whole of this module reduces to being able to state the price.

Read the table as implications, not as recommendations. Each row is "if this is genuinely a requirement, then this decision follows, and here is the bill". A system with three of these requirements has three bills and usually a conflict, which is the real content of the design work.

The requirement, the decision it implies, and what it costs
If the requirement isThe decision that followsPaid in
Native control: predictable latency, direct hardware access, no resident translatorAhead-of-time compilation to machine code, a closed world, defined memory layout — [[aot-compilation]]One artifact per target triple, a symbolication pipeline, and no optimization that depends on run-time knowledge
Portable, sandboxed execution: run untrusted code, one artifact everywhereA validated virtual instruction set — a bytecode VM, or WebAssembly — [[webassembly]], [[wasm-model]]A resident runtime on every host, no direct system access, and a memory model the host defines rather than you
Dynamic flexibility: change behavior at run time, inspect and extend types, interactive developmentTypes carried at run time, open classes, reflection, run-time code loadingA translator resident in every deployment, permanently conservative whole-program analysis, and errors that arrive at run time
Compile-time guarantees: catch classes of error before deployment, refactor large codebases safelyA rich static type system — sum types, exhaustiveness, parametric polymorphism — [[what-a-type-system-proves]], [[algebraic-data-types]]Longer builds, correct programs rejected, annotation burden at boundaries, and a learning cost paid by every new contributor
Memory safety without a collector: safety plus deterministic releaseOwnership and borrowing checked in the type system — [[ownership-types]], [[lifetime-analysis]]A checker that rejects correct programs, an unsafe escape hatch where the guarantee is discharged by a human, and the steepest learning curve in the table
Rapid scripting: get to a running program in minutes, no build step, iterate interactivelyAn interpreter or a bytecode VM, dynamic types, a REPL — [[tree-walk-interpreter]], [[bytecode-compiler]]Interpretive overhead on every execution, errors found at run time or not at all, and a refactoring story that depends on tests
Domain readability: non-programmers must read, review or write itA domain-specific language, ideally total and non-Turing-complete — [[dsl]], [[should-i-build-a-dsl]]The entire tooling bill: parser, diagnostics, formatter, editor support, documentation and versioning, for every year the language lives — [[dsl-tooling-cost]]

The conflicts are the actual design work

Any single row is easy. The work begins when two requirements are real at once, and the useful thing to know is which pairs conflict and how they are usually resolved.

Dynamic flexibility against native control. These are close to mutually exclusive, because dynamism means a translator is resident and native control means it is not. The standard resolutions are to restrict dynamism to a declared set of entry points, which is what native-image configuration does, or to split the system: a native core with a scripted extension layer, which is what game engines, editors and databases have converged on independently.

Compile-time guarantees against rapid iteration. Real, and less absolute than it looks. Gradual typing exists precisely to make the trade incremental — dynamic where the code is churning, static where it has stabilised — at the cost of a type system whose guarantees are local rather than global, which is [[gradual-typing]]. The honest version of the trade is that gradual typing does not give both; it gives you the ability to choose per module.

Memory safety without a collector against approachability. Ownership is the only mechanism that delivers the first, and its learning cost is the main thing that limits its adoption. The resolutions in practice are hybrid: a counted or collected escape inside an ownership language, or an ownership discipline applied only at boundaries. Both weaken the guarantee in the places they are used, which is the price and is usually worth paying.

Portability against performance. A validated portable target cannot expose everything a native one can, and the gap is real and measurable. What is worth knowing is that the gap has been narrowing for reasons that have nothing to do with cleverness and everything to do with the target growing features — which means the trade is dated, and any number you were told about it needs re-measuring.

The prices people forget to name

Four costs get systematically left out of these arguments, and each of them has sunk a real project.

  • Build time is a per-engineer, per-day cost. Monomorphization, whole-program optimization and full inference all buy something real and all lengthen every build for everyone forever. A language decision that adds ninety seconds to a build is a decision to spend a meaningful fraction of every engineer's day — see [[compiler-performance]] and [[incremental-compilation]].
  • Diagnostics are architecture, and they are the product. Spans threaded everywhere and constraint provenance in inference are the preconditions for a compiler that can explain itself, and they cannot be added later cheaply. A strict language without them is a language people describe as fighting them — [[language-ergonomics]].
  • Rejected correct programs are a real cost. Every static guarantee excludes programs that would have worked. The question is never whether that happens but whether the excluded programs are ones your audience writes, and the only way to find out is to have them write some.
  • The tooling bill for a new language is annual, not one-off. A parser is a week. Diagnostics, a formatter, editor support, a package story, documentation and versioning are years, and they are what determines whether anyone else can use the thing — [[dsl-tooling-cost]].

How to state a design decision so it can be argued with

The format that makes these discussions productive has four parts, and leaving out the third is what makes them go in circles.

State the requirement, in terms of what the system must do rather than what the language should be. State the decision that follows. State what it costs and who pays it. State the enforcement mechanism that makes the claimed property real. A decision written this way can be disagreed with on specifics; one written as "we should use a statically typed language" cannot be disagreed with at all, only sided with.

And when the requirements genuinely conflict, say which one loses. Every language in production lost something: Rust lost approachability, Go lost expressiveness for a decade, Python lost the ability to be compiled ahead of time, C lost memory safety, JavaScript lost the ability to fix anything. Naming the loss is what distinguishes a design from a wish list, and every one of those languages is enormously successful with the loss intact.

How it works

The steps, in the order the compiler takes them.

  • Write the requirements as things the system must do, with the constraint that made each one a requirement.
  • For each, derive the decision it implies from the rows above and note the cost in the same sentence.
  • Identify pairs of requirements whose decisions conflict, and choose a resolution explicitly rather than letting the implementation choose one.
  • For each claimed property, name the mechanism that enforces it — a checker, a runtime, or a structural prohibition — and reject any claim without one.
  • Record which requirement lost, so that a future maintainer knows it was a decision rather than an oversight.

How it breaks

What the engineer observes when it goes wrong — not what goes wrong internally.

  • A team chooses a language on benchmark results, and the cost that actually bites is build time, hiring, or the absence of a library nobody checked for.
  • A language claims a guarantee that nothing enforces, users rely on it, and the first violation is treated as an implementation bug rather than as a design defect.
  • A design discussion runs for months because nobody stated the requirement, so every argument is about preferences and none can be settled by evidence.
  • A guarantee is added late, thousands of existing programs violate it, and it ships as an opt-in flag — after which the language has two dialects forever.
  • A DSL is built for a readability requirement, the parser works in a week, and three years later the project is maintaining an editor plugin, a formatter and a package registry that nobody budgeted for.

When it helps

  • Choosing a language for a system with a hard constraint, where the constraint eliminates most candidates before any preference is expressed.
  • Reviewing a language or DSL proposal, where asking for the cost of each decision resolves most of it in one pass.
  • Explaining an existing language's unpopular decisions, nearly all of which are a price paid for a property that mattered to its audience.

When it hurts

  • Applied to a decision that does not matter. Most syntax choices, most standard-library naming and most formatting questions have no downstream cost, and treating them with this apparatus wastes the apparatus.
  • As a way to avoid deciding. Every row has a cost, so a sufficiently careful analysis can reject every option; at some point the requirement wins and the price gets paid.

What it costs

Every one of these is paid by something.

  • Making every trade explicit buys decisions that survive their authors and can be revisited when the requirements change, and costs time up front, plus the discomfort of writing down which requirement lost.
  • Choosing a language for one dominant requirement buys a system that satisfies it convincingly, and costs everything the other requirements needed — which is fine when they are genuinely secondary and expensive when someone was optimistic.
  • Building a new language rather than adapting an existing one buys exactly the properties you specified, and costs the entire tooling and ecosystem bill annually, forever, against a language that already has one.

What else you could do

What a different compiler or language does instead, and when that is better.

  • Do not choose one language. A native core with a scripted extension layer satisfies the native-control and dynamic-flexibility rows simultaneously, at the cost of an interoperability boundary — [[interoperability]].
  • Choose a language and change its defaults: strict flags, lint rules, a subset enforced in review. Buys most of a stricter language's guarantees without the migration, and costs a discipline that only holds while somebody enforces it.
  • Adopt a library instead of a language, which satisfies most domain-readability requirements at a small fraction of the cost — [[internal-vs-external-dsl]].
  • Defer: build the first version in whatever gets you a running system, and re-derive the decision when the requirements are known rather than guessed. The cost is a rewrite, and the rewrite is often cheaper than the wrong permanent choice.

See it for yourself

The flag, dump or tool that shows you this directly.

  • Price the build-time cost before adopting a language: build a realistic project of comparable size in each candidate and time a clean build and an incremental one. The second number matters more.
  • Price the diagnostic cost: write the same three mistakes in each candidate and read the messages. This is a five-minute test that predicts years of experience.
  • Price the rejected-program cost: have someone from the target audience write a real module. The friction shows up immediately or not at all.
  • Price the ecosystem: search for the three libraries the system will actually need, and check the last commit date on each.
  • For an existing language, read its rejected proposals. The reasons for rejection are the design constraints stated plainly, which the documentation rarely does.

Plausible wrong readings

Stated the way a confident engineer states them.

  • "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.
  • "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.
  • "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-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.

Misconceptions

The claim, and what is actually true.

Language choice is mostly about performance.
Performance is one row of seven, and for most systems it is decided by architecture and data layout rather than by language. Build time, diagnostics, hiring and ecosystem decide more projects than benchmarks do.
A trade-off means neither option is better.
It means neither is better in general. Given a stated requirement, one of them is usually clearly better, which is why stating the requirement is the whole technique.
Newer languages have fewer trade-offs.
They have different ones, and less evidence about them. What a new language has genuinely gained is the ability to choose defaults that older ones cannot change.

Go deeper

The same idea at increasing depth. Stop wherever it stops being useful.

overview

Start from what the system must do. Predictable latency and no resident translator means compiling ahead of time. Untrusted code means a sandboxed virtual machine. Interactive development means an interpreter. Catching errors before deployment means a rich static type system. Safety without a collector means ownership. Non-programmers reading it means a domain-specific language. Each of those buys something and charges for it, and the charge is the part worth writing down.

practical

Write the requirement, the decision, the cost and the enforcement mechanism in four lines. If you cannot write the cost line, you have not understood the decision; if you cannot write the enforcement line, the property you are claiming does not exist. When two requirements conflict, say which one loses and why, and record it — the next person will otherwise treat it as an oversight and reopen it.

advanced

What makes this domain's version of the trade-off argument different from the usual one is that every price has an address: a phase that pays it. Static guarantees are paid in the type checker and in build wall time; memory safety without a collector is paid in the borrow checker and in the drop insertion on unwind paths; portability is paid in the backend and in the loss of layout guarantees; dynamism is paid by every whole-program analysis that must now be conservative. Once a cost has an address, it can be measured, and once it can be measured, the argument stops being about taste. That is the only real technique in this module: refuse to accept a design claim until someone names the phase that pays for it.

How much this depends on

Nothing in this domain is true of every compiler. These say how much.

typicalThe requirement-to-decision implications describe how these trades have resolved across mainstream languages to date. They are strong regularities rather than theorems, and the interesting languages are usually the ones that paid an unusual cost to escape one — Erlang getting soft-real-time behavior from a collected language by making every process own its heap, for instance.
implementationSpecific claims about what any language currently gives up are version-dated. Go added generics, Java added ahead-of-time compilation and value types are in progress, Python added a JIT, and WebAssembly continues to gain capabilities. A trade you were told about five years ago should be re-checked before it is used to decide anything.
specThe enforcement requirement is not stylistic: a property with no enforcement mechanism is not a guarantee in any language definition sense. C's strict aliasing rule and Java's original memory model both illustrate what happens when a definition states something that no implementation reliably checks — programs rely on the violation, and fixing it becomes impossible.

If you were asked this in an interview

  • A system needs predictable latency, memory safety and a plugin architecture. Which of those loses, and what is your reasoning?
  • Name a language decision you consider a mistake, then state the requirement it was satisfying and who was paying for the alternative.
  • What does a rich static type system actually cost, beyond the annotations?

Connections

Securitysandboxing
Domains that do not exist yet
  • DevOps / Production Engineering — Build time, artifact size, deployment topology and hiring as inputs to a technology decision
    Several of the prices in this table are paid by the delivery pipeline and by the team rather than by the compiler. Naming them is ours; managing them is theirs.
  • Programming Languages & Runtime Internals — What each of these decisions costs at run time — pauses, footprint, dispatch, warmup
    This lesson prices decisions in compiler and toolchain terms. The run-time half of every price in the table is measured there, and a complete argument needs both halves.