Attribution: This article was based on content by @veqq on hackernews.
Original: https://cs.lmu.edu/~ray/notes/languagedesignnotes/
Introduction
Thesis statement — Designing a programming language is about managing trade-offs: every choice in parsing, typing, compilation, and runtime moves cost and complexity between developer ergonomics, implementation effort, runtime performance, and safety. A practical language design aligns those trade-offs to the intended domain, developer expectations, and deployment environment.
Background: Lexical analysis tokenizes source text into symbols; parsing arranges tokens into syntactic structure.
This analysis synthesizes Ray’s “Designing a Language (2017)” notes (original source) with literature on compilers, type systems, runtimes, and modern toolchains to give a structured roadmap for language designers (credit: Ray, 2017). It compares multiple approaches across the compiler and runtime pipeline, shows common trade-offs, and gives concrete when-to-use recommendations.
Key Takeaways
- Choose grammar and parsing strategy to match language complexity and error-reporting needs; generated parsers speed development but handwritten parsers offer better control.
- Type systems are a balance between developer productivity and runtime guarantees: static typed with inference gives safety with low annotation cost; dynamic typing favors fast prototyping.
- Targeting LLVM/MLIR or WebAssembly gives high performance and portability; bytecode/VM paths or interpreters favor startup time and fast iteration.
- Memory management choices (GC vs ownership vs reference counting) map to latency, predictability, and ease of use—pick according to real-time needs and safety goals.
- Tooling (language server, macro system, package manager) heavily influences adoption; invest early in ergonomics.
Background & Context
┌──────────────────────────────┬──────────────────────────────┐
│ LL Parsing │ LR Parsing │
├──────────────────────────────┼──────────────────────────────┤
│ Top-down approach │ Bottom-up approach │
├──────────────────────────────┼──────────────────────────────┤
│ Consumes tokens to build │ Consumes tokens to build │
│ syntax trees │ syntax trees │
├──────────────────────────────┼──────────────────────────────┤
│ Grammar class │ Grammar class │
│ LL │ LR │
├──────────────────────────────┼──────────────────────────────┤
│ Example strategies │ Example strategies │
│ LL(k) │ LR(1) │
└──────────────────────────────┴──────────────────────────────┘
Figure: Background & Context
flowchart TD
A[Start] --> B(Define Grammar classes LL, LR)
B --> C{Top-down or Bottom-up?}
C -- Top-down --> D[LL strategy]
C -- Bottom-up --> E[LR strategy]
D --> F[Lexical Analysis]
F --> G[Parsing (LL(k))]
G --> H[Abstract Syntax Trees (ASTs)]
H --> I[Type Systems & Inference]
I --> J[Intermediate Representations (IRs)]
J --> K[Static Single Assignment (SSA) Form]
K --> L[Backends]
L --> M{Native code, LLVM/MLIR, WebAssembly?}
M -- Native code --> N[Runtime Services]
N --> O[Garbage Collection]
O --> P[Threading]
E --> Q[Lexical Analysis]
Q --> R[Parsing (LR(1))]
R --> S[Abstract Syntax Trees (ASTs)]
S --> T[Type Systems & Inference]
T --> U[Intermediate Representations (IRs)]
U --> V[Static Single Assignment (SSA) Form]
V --> W[Backends]
W --> X{Native code, LLVM/MLIR, WebAssembly?}
X -- LLVM/MLIR --> Y[Runtime Services]
Y --> Z[Garbage Collection]
Z --> AA[Threading]
X -- WebAssembly --> BB[Runtime Services]
BB --> CC[Garbage Collection]
CC --> DD[Threading]
AA --> EE[End]
DD --> EE
EE --> FF[End]
Background: Grammar classes (LL, LR) define how parsers consume tokens to build syntax trees—LL is top-down; LR is bottom-up.
Compiler and language design rests on core areas: lexical analysis and parsing (e.g., LL(k), LR(1) strategies), abstract syntax trees (ASTs), type systems and inference, intermediate representations (IRs) and static single assignment (SSA) form, backends (native code, LLVM/MLIR, WebAssembly), and runtime services (garbage collection, threading, JIT compilation). Aho et al. (2006) remains the practical reference for the compiler pipeline. LLVM’s modular IR and toolchain approach shaped many modern languages (Lattner and Adve, 2004). WebAssembly provides a fast, portable compilation target for web and server contexts (Haas et al., 2017).
Type theory foundations such as Hindley–Milner inference (Milner, 1978) and the modern type system treatments in Pierce (2002) help reason about safety vs flexibility. Garbage collection algorithms are surveyed by Jones and Lins (1996), which remain relevant when choosing tracing vs reference counting vs ownership models.
Detailed Comparison
Below I compare three design axes with alternatives: Parsing, Typing, and Backend/Runtime.
Parsing: Handwritten vs Generated vs Parser Combinators
- Handwritten recursive-descent parser
- Pros: fine-grained control, better error messages, easier to implement context-sensitive constructs.
- Cons: more work, error-prone for complex grammars.
- Generated parser (e.g., ANTLR, Bison)
- Pros: fast development, proven handling for complex grammars, often supports automatic AST generation.
- Cons: grammar rewrites to fit parser class, poorer custom error recovery.
- Parser combinators (embedded DSL)
- Pros: composability, good for incremental and extensible grammars.
- Cons: performance and stack use can be issues in some implementations.
Typing: Dynamic vs Static with Inference vs Gradual Typing
- Dynamic typing (Python, JavaScript)
- Pros: rapid prototyping, minimal boilerplate.
- Cons: runtime errors, harder to optimize.
- Static typing with inference (Hindley–Milner style)
- Pros: compile-time guarantees, low annotation burden.
- Cons: complexity for features like subtyping, effects.
- Gradual typing (TypeScript-style)
- Pros: migration path from dynamic to static; mixed guarantees.
- Cons: complex runtime/compile semantics, potential performance overhead.
Backend/Runtime: Interpreter vs Bytecode VM vs LLVM/JIT vs WebAssembly
- Interpreter
- Pros: fastest iteration, smallest runtime.
- Cons: poorer raw performance.
- Bytecode + VM (JVM/CLR-like)
- Pros: portable, easy to implement optimizations, faster than interpreter.
- Cons: custom VM maintenance.
- LLVM/MLIR backend (ahead-of-time or JIT)
- Pros: near-native performance, mature optimization pipeline (Lattner and Adve, 2004).
- Cons: larger toolchain, longer compile times, more complexity.
- WebAssembly target
- Pros: portable to browsers and many runtimes, fast startup in constrained environments (Haas et al., 2017).
- Cons: limited host integration (recently improving), constraints on system-level features.
Comparison table (high-level)
| Concern | Fast dev & REPL | Predictable low latency | High throughput/native perf | Portability to Web |
|---|---|---|---|---|
| Handwritten parser | Good | Good | Good | Good |
| Generated parser | Best | Moderate | Good | Good |
| Dynamic typing | Best | Poor | Poor | Good |
| Static with inference | Good | Good | Good | Good |
| Interpreter | Best | Poor | Poor | Moderate |
| Bytecode VM | Good | Moderate | Good | Moderate |
| LLVM/JIT | Moderate | Moderate | Best | Moderate |
| WebAssembly | Moderate | Moderate | Good | Best |
Citations: Aho et al. (2006); Lattner and Adve (2004); Haas et al. (2017); Milner (1978); Pierce (2002).
Performance Metrics
Real-world benchmarks illustrate the practical trade-offs:
- LLVM/JIT and AOT backends often achieve near-native speeds because of aggressive optimizations and SSA-based transformations. Lattner and Adve (2004) show that a well-optimized IR enables cross-language optimization and high throughput.
- WebAssembly frequently delivers performance within a small factor of native for compute-bound code in browsers; Haas et al. (2017) demonstrate that compilation and startup are optimized for the web.
- Generational and concurrent garbage collectors significantly reduce pause times compared to naïve stop-the-world collectors. Jones and Lins (1996) provide algorithmic context; modern GCs (HotSpot, V8) report millisecond-class pause reductions under typical workloads, though specifics vary by workload and heap size.
- Static-typed languages with zero-cost abstractions (e.g., Rust, C++) typically outperform garbage-collected languages in latency-sensitive contexts due to absence of GC pause behavior; however, GC languages can match throughput under high parallelism due to simpler programming models for shared mutable state. Empirical benchmarks vary by workload and implementation, so designers should benchmark their expected workloads.
When discussing numbers, remember they depend on workload, allocator, hardware, and concurrency model. Always benchmark representative scenarios.
Trade-offs
Parsing
- Generated parsers reduce upfront implementation time but can leak implementation constraints into language syntax.
- Handwritten parsers trade development speed for better diagnostics and context-sensitive handling.
Typing
- Static typing reduces runtime errors and enables optimizations; it increases compiler complexity and may require annotations.
- Dynamic typing maximizes developer speed at the expense of runtime safety and optimizability.
- Gradual typing gives migration flexibility but complicates semantics and runtime checks.
Backend/Runtime
- LLVM/MLIR: best raw performance and optimization leverage; higher build complexity and longer compile times.
- Bytecode VM: good portability and maintainability; less peak performance.
- WebAssembly: excellent portability for browser/server; evolving ecosystem for system-level OS features.
- GC vs Ownership vs RC:
- Tracing GC: low programmer burden, potential pauses; modern GCs are concurrent and generational (Jones and Lins, 1996).
- Reference counting: predictable but can suffer cyclic leaks unless cycle collection is added.
- Ownership/borrow checking (Rust-style): deterministic memory, no GC pauses, but steeper learning curve and more complex compiler.
Decision Matrix
Use this matrix to pick an approach based on three primary drivers: Domain, Latency Requirements, Team Expertise.
| Primary Driver | Parser | Typing | Backend | Memory Model |
|---|---|---|---|---|
| Rapid prototyping, DSLs | Parser combinators | Dynamic | Interpreter | Reference counting / GC |
| Systems programming, low latency | Handwritten (or generated deterministic) | Static with inference | LLVM/MLIR | Ownership / static lifetime |
| Web client/server portable | Generated/standard grammar | Gradual/Static | WebAssembly | GC or ownership with wasm support |
| Enterprise language (tooling + ecosystem) | Generated + custom error recovery | Static with gradual options | Bytecode VM | GC (mature collectors) |
When to Use (Concrete Scenarios)
- Build a scripting language for automation or data science: choose dynamic typing, interpreted REPL, and a simple GC or reference counting; prioritize good REPL ergonomics and fast turnaround.
- Design a language for embedded/real-time systems: use static typing with ownership and AOT compilation; avoid tracing GC to meet latency bounds.
- Create a general-purpose server language aiming for high throughput: static typing with inference, target LLVM for hot paths, and moderate GC tuned for throughput and low pause times.
- Targeting browser and multi-platform runtimes: design for WebAssembly as a first-class backend, keep language runtime small, and provide polyfills for host-specific features.
Conclusion
Language design is primarily a balancing act among developer ergonomics, safety, implementation effort, runtime performance, and portability. The right architecture depends on the domain: interpreters and dynamic typing for rapid iteration; static typing and LLVM for performance; WebAssembly for portability to the web; ownership models for deterministic memory without GC pauses. Use the decision matrix and trade-offs above to narrow choices, and validate with representative benchmarks and developer feedback.
Original source and further reading: Ray (2017), “Designing a Language” notes (https://cs.lmu.edu/~ray/notes/languagedesignnotes/).
References
- Aho, Sethi, Ullman (2006). Compilers: Principles, Techniques, and Tools.
- Lattner and Adve (2004). LLVM: A Compilation Framework for Lifelong Program Analysis & Transformation.
- Jones and Lins (1996). Garbage Collection: Algorithms for Automatic Dynamic Memory Management.
- Milner (1978). A Theory of Type Polymorphism in Programming.
- Pierce (2002). Types and Programming Languages.
- Haas et al. (2017). Bringing the Web up to Speed with WebAssembly.