Dawn Design Notes

中文 — the Chinese text is the original; this is its translation, and scripts/doc-check.py watches the two for drift.

Status: historical. This is the record of the decisions taken during M0–M4, kept item by item so that "why was it chosen this way at the time" has evidence behind it. Several of its premises have since been overturned by later milestones, the three that matter most:

  • "compiler budget 6–8 thousand lines (Kotlin)" and "implementation language = Kotlin" — the Kotlin implementation has been archived at the kotlin-final tag, and main holds only the self-hosted selfhost/ (some 35,000 lines of Dawn); see m8-selfhost-only.md.
  • "the unsafe escape hatch is not open to user code" — unsafe_pure is ordinary syntax now and users can write it; see spec.en.md §6.4 and LANG-01 in codebase-audit.md.
  • "no IR" — the argument held while the compiler was small; checker + emit are past ten thousand lines now, see ARCH-04 in codebase-audit.md.

The authoritative definition of the syntax and semantics is spec.en.md (that one is normative). This document is only about the "why".

1. Goals

2. Non-goals (explicitly out of scope for v0.1)

Not doingReason
~~full algebraic effects (handlers)~~ → the non-tail-resumptive tier (multiple resumption / continuation capture)~~an engineering black hole~~ → the tail-resumptive tier has landed (effect + with handle, spec §6.5); what is left of it needs continuation capture, which collides head-on with the C backend's Perceus + single-stack model
~~trait / typeclass~~~~pass functions explicitly for now, add it when it hurts~~ → it hurt; added 2026-07-13 (see D9)
async / coroutines!io + JVM virtual threads carry it; no colouring problem introduced into the language
macroscomptime covers the main cases, and it does not introduce a second language
custom operatorskeeps the ecosystem from growing into early Scala
Java → Dawn direction of interopa stable ABI in both directions is a big language's job
REPLconflicts with comptime and the whole-program compilation model; poor value for effort
custom GC / memory modelthe JVM's GC is our GC

3. Decision record

D1 The only backend = JVM bytecode (straight out of ASM)

native-image eats bytecode, so dawn build --native is just a native-image command tacked onto the end of the jar. The price is accepting the closed-world assumption, so the language design steers clear of its minefields on purpose:

D2 The effect system has only two levels: pure and io

A full Flix-style polymorphic effect system is beautiful, but it doubles type inference, error messages and the cost of teaching all at once. Dawn's lattice has just two points, {pure, io}, plus effect variables for the most minimal effect polymorphism (the effect of map(f) = the effect of f). In the implementation this is one more boolean lattice threaded through the type checker; the payoff is the big half: pure functions can be called from comptime, can be optimised aggressively, and need no mocks in tests.

Upgrade path: should finer effects such as !net / !fs be wanted later, the lattice grows from two points to a powerset — the syntactic slot (the ! suffix) is already reserved.

D3 comptime rather than macros; comptime does not manipulate types

A compiler must do constant folding → it therefore contains an AST interpreter → exposing that to the user is comptime. Only pure code is allowed, and the result must be a constant-serialisable value. Deliberately not doing Zig's "types are first-class comptime values" — that stirs the type checker and the evaluator into one pot, and is the kind of complexity a small implementation cannot ride. Generics go through separate, boring parametric polymorphism (erasure + boxing), uncoupled from comptime.

D4 Error handling = Result + ?, with panic as the backstop

Exceptions break "the signature is the contract" (a function that can blow up without being marked !io is a lie about purity). Recoverable errors go through Result[T, E] + ? propagation; unrecoverable ones go through panic() (the process terminates, mapped to throwing an Error on the JVM and to abort on native). panic needs no !io — it does not return.

D5 Java interop: one-way, all !io, null wrapped into Option automatically

D6 No intermediate IR

AST → type/effect checking (annotated on the AST) → bytecode straight out of ASM. Optimisation is left to the JVM JIT and to native-image. An IR gets introduced when an optimisation pass or a second backend is really needed — prepaying architectural cost for an imagined requirement is the most common way small projects die. The one exception is tail-recursion rewriting (an AST-level transformation).

D7 Implementation language = Kotlin

Thirty percent less boilerplate than Java (data classes, sealed classes and an embryonic pattern match are exactly what writing a compiler wants), the artifact is a jar all the same, and native-image can bootstrap the compiler itself all the same.

D8 The interop trio: SAM conversion, array pass-through, zero-copy List bridge (settled 2026-07-12)

A prerequisite for M6, accepted against the playground runner first. All four semantic decisions were checked against precedent in Kotlin/Scala/Clojure:

D9 trait = single-parameter nominal typeclass + dictionary passing (settled 2026-07-13)

The full design and implementation record is in trait.md; spec §3.5 is the normative summary. The main points:

D10 The pre-selfhost clearing batch (P0.6, settled 2026-07-22) — patching the semantic dark corners, plus three "leave it as it is"

After selfhosting, every semantic change has to be served to two compilers at once, so while there was still only one parser the debts turned up by a design review (against orthogonality / explicit-over-implicit / the standard list of language-design mistakes) were all settled at once. Fixed (each with its section in the spec and its commit): numeric edge semantics nailed down as guarantees (§4.3), Float/Bytes forbidden as Map/Set keys (§2.2), alias given its own keyword (§2.6), a field call colliding with a same-named function turned into an error (§2.4), break/continue (§4.7), a leading . continuing a line (§1.7), Cursor as an opaque type (§11). The three items left as they were after the review, with the reasoning recorded so it does not get relitigated:

Also: the review suspected "top-level declarations silently shadow the prelude" — the reality at P0.6 was that redefining a builtin/std name at the top level was an error. P0.7 has, per `stdlib-naming.md`, made a top-level fn / trait method shadowing a builtin/std function name legal, Rust-style (the resolution order was already this module's declarations → std → builtins; only the registration-time error was deleted; std's own pub fn len was the first beneficiary). Builtin/prelude type and trait names (Map/Option/Ord…) still cannot be redefined; spec §10.3 is the authoritative wording.

4. Acknowledgements for where the features came from

5. Milestones

Two tracks run horizontally through all of it: the LSP evolves in step with every feature (M1 proved the cadence workable; on 2026-07-12 completionProvider was completed — scope symbols/builtins/constructors/keywords + suppression inside strings and comments + ! triggering io, and the three language gaps the web framework knocked out in the same cut, G1 direct field calls / G2 type aliases / G3 rigid type parameters, landed the same day, spec §2.4/§2.6/§2.5); and the acceptance sample comes first — before work starts on a milestone, the thing it will be accepted against is written and committed.

trait v1 (2026-07-13, six cuts landed, see D9 and trait.md): single-parameter typeclasses + dictionary passing; the trait/impl/[T: Ord] syntax, coherence + the orphan rule, < <= > >= bridged to the built-in Ord, derive Ord, sort/sort_by/max/min/max_by/min_by; the acceptance sample examples/traits.dawn, tutorial §15, spec §3.5; 1094+ tests green, JVM/native compared and identical.

The ergonomics four-pack (2026-07-12, settled after surveying the []/inference/local function conventions of Kotlin/Rust/Scala and others, 973 tests green): ① [] subscripting — assertion semantics for List/Map (out of range / missing key panics), with get/map_get kept as the enquiring form returning Option, and List subscripting supported in comptime (spec §4.8); ② return for early exit — an expression of type Never, with the same scoping rule as ? (spec §4.9); ③ local named functions — fn name(...) inside a block, capturing, recursive, self-tail-calls compiled into a loop, with recursive calls compiled to a direct invokestatic on the impl method and self-references as values rebuilding the closure (spec §3.1, §12.4); ④ inferred signatures for private functions — a non-pub function may omit its return type and effect, derived in topological order over the call graph, with recursion/?/return requiring an annotation; const checking was split into two passes (register the types first, check the initialisers second) so that a const can call an inferred function (spec §3.1). The web framework and the site generator dogfooded [] the same day.