type · match · effect · !io

Dawn

A small, elegant functional language: immutable data, algebraic data types with exhaustive pattern matching, effects written into the type signature. The compiler is self-hosted, and its two peer backends — JVM bytecode and C — give the same answer on the same source. A gate keeps that true, not a promise.

main.dawndawn run
type Shape =
  | Circle(r: Float)
  | Rect(w: Float, h: Float)

fn area(s: Shape) -> Float =
  match s {
    Circle(r) -> 3.14159 * r * r
    Rect(w, h) -> w * h
  }

pub fn main() -> Unit !io =
  [Circle(1.0), Rect(2.0, 3.0)]
  |> map(area)
  |> fold(0.0, (a, x) => a + x)
  |> t => println("total: $t")
total: 9.14159

Core features

Effects in the type

Functions are pure by default; touching IO requires !io on the signature — the signature tells you whether it reaches outside, so testing a pure function needs no mocks. A second axis is named effects you declare yourself: effect declares the operations, with handle answers them on the spot, and the label propagates along signatures until exactly one syntactic node subtracts it. That tier is specified, implemented on both backends and tested, and it has no internal consumer yet: neither the standard library nor the compiler declares a named effect.

effect Ask {
  fn ask() -> Int
}

# Pure. The signature says it asks; it does not say whom
fn total() -> Int !Ask = ask() + ask()

pub fn main() -> Unit !io = {
  with handle Ask { ask() => 21 }
  println("${total()}")
}
42

Compile-time evaluation: comptime

comptime { ... } is executed at compile time by the interpreter and the result is burned into the constant pool. There is no macro system, and none is needed — an ordinary function already runs at compile time.

fn fib(n: Int) -> Int =
  if n < 2 { n } else { fib(n - 1) + fib(n - 2) }

# Computed at compile time, burned into the constant pool
const FIB10: Int = comptime { fib(10) }

pub fn main() -> Unit !io = println("fib(10) = $FIB10")
fib(10) = 55

Two backends, one answer

JVM bytecode and C (handed on to cc) are peer roads. Wherever divergence would be easiest, the language owns the thing itself: Float rendering is Schubfach in pure Dawn, the Unicode case tables belong to the compiler, Map iteration order is pinned to insertion. The differential corpus is compiled and run on both sides on every push, comparing stdout, stderr and exit code — a divergence is a red build.

use std/map
use std/str

# One source, two backends, the same bytes on both
pub fn main() -> Unit !io = {
  let m = map.from([(1, "a"), (2, "b")])
  println("${0.1 + 0.2}")
  println(str.to_upper("île"))
  println("${map.keys(m)}")
}
0.30000000000000004
ÎLE
[1, 2]

Start with the tutorial; the authoritative definition of the language is the specification; every example runs as it stands under dawn run; the standard library API reference is here; and the "why" behind each design decision is in the design notes.

Every page of this site comes in both languages. The specification and the design notes are the one pair written in Chinese first and translated: they are living documents, edited in Chinese by every change to the language, so the Chinese half is the original and the English half is registered against it — scripts/doc-check.py goes red when the two drift apart. The rest of docs/ is design notes and plans whose reader is the author, and it stays monolingual. The code, the compiler's diagnostics and the standard library's doc comments are English throughout — including the entries on the standard library page, which are the compiler's own text.