Guarantees, compiled in.

Reactivity is the language, not a library. derived recomputes when its inputs change, and the compiler proves that recomputing only what changed equals recomputing everything. No subscription bookkeeping, no dirty marking, no glitches. Reactive and memory safety are the same problem: tracking who reads and writes what. Solve reactivity, and the analysis a borrow checker would do is already done — for free. The same move then generalizes to safety, memory, determinism, and performance.

owned values copyproofs travel with the codeno garbage collector

Read RFC 001 GitHub

Run Etch

etch hello.etch     // compiles to a native binary
./hello             // prints hello

A program, in hello.etch:

fn main() {
    println("hello");
}

etch emits machine code directly — no clang, no LLVM, no outside optimizer. The compiler is self-hosted. The only outside parts are the system linker and the debug format, listed in note 1 of section 伍.

Incremental, as semantics

Etch's core is that derived and effect are language primitives, not a runtime library. derived declares a value that recomputes when its inputs change; effect runs after each change. The compiler knows the dependency graph, so incremental updates are proven equal to full recomputation (incremental ≡ from-scratch).

let count: int = 0;
derived doubled { count * 2 }
effect { println(toString(doubled)); }   // prints 0 now

fn main() {
    count = 1;   // prints 2
    count = 2;   // prints 4
}

This toy leaves out the effect rows: main writing the global count, and the effect reading doubled, are carried by inferred rows in the function signatures. A full listing would print those rows beside the signatures. (Here effect is the reactive block, not the effect row in a function's type.)

This works because Etch commits the work a reactive runtime used to do — subscription bookkeeping, dirty marking, topological sorting — into the compiler as spec. You don't manage subscriptions, write dependency arrays, or worry about glitches. That's the name: the reactive behavior is etched into the machine code at compile time — there is no runtime to erase it.

Even cycles are handled: a cycle in the reactive graph is admitted only if the compiler can prove it converges. Concretely, a derived that tracks a running maximum — best reads its own previous value and the newest score, joined by max — is a cycle, but max is monotone and bounded (the value only rises, and scores are finite), so the compiler proves it reaches a fixed point instead of looping. A cycle with no such proof is rejected.

The same move, generalized

The same move applies everywhere. Value semantics — owned values copy; &ref is the scoped by-reference escape. Effect rows — what a function reads or writes is in its type. Prove-or-reject — safety and termination are proven up front, or bounded by a named limit. Regions — no garbage collector. Bit-identical determinism — the same result on every machine. Integers are 64-bit; a range like int(0..9) is itself a type, so bounds travel with the value.

If it compiles, you can rely on:

One rule runs through the whole language: a cost isn't forbidden — it's named and listed. The tax bill (section 柒) is where those costs get itemized.

The checker isn't perfect: some programs that would stop still get rejected (Collatz is the usual example). Restructure the loop or add a fueled bound.

fn clamp(x: int(0..100)) -> int { return x; }
fn main() {
    println(clamp(50));   // 50
}

This program compiles and runs. The interpreter is a second, independent implementation of the language, kept to check that the compiled version agrees with it — both give the same result here (prints 50). Grounded in RFC 001.

Pass an argument it can't prove, and it rejects the program, naming the missing proof:

fn clip(x: int(0..10)) -> int { return x; }
fn main() {
    clip(11);
}
error: call to clip: argument `x` not proven ⊆ [0, 10] (got [11, 11])

No GC, without the borrow checker

No garbage collector isn't the hard part — Rust already has that. The hard part is no GC without the borrow checker's lifetime annotations. Etch gets both.

Rust

You write references (&, &mut) and lifetimes. The borrow checker proves no two names alias and mutate at once, so memory is freed deterministically. You pay with the annotations, and you get zero-copy where the borrow allows.

Etch

Owned values copy (copy-on-write); &ref is the scoped by-reference escape, with no lifetime annotations to write. The read/write sets the reactive graph already needs — effect rows — are the same information the borrow checker otherwise enforces through annotations; Etch has it because reactivity required it anyway. Regions reclaim memory: bump allocation, bulk release at frame exit, O(1), no GC.

Concretely, a function that returns one of two borrowed strings:

Rust — you write the lifetime:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

Etch — values copy, nothing to annotate:

fn longest(x: string, y: string) -> string {
    if x.length() > y.length() { x } else { y }
}

The cost just moves. Etch pays implicit copy-on-write copies (named in the build report) and a compile-time proof of region lifetimes and memory bounds; Rust pays explicit borrow and lifetime annotations.

No LLVM, no outside optimizer

Etch emits machine code directly — there is no LLVM, no clang, no external optimizer in the path.

the usual way — an optimizing compiler in the middle Etch — that middle layer is gone source frontend IR optimizer ~80 passes machine code ← the layer Etch removes source frontend + proofs direct emission one node = one instruction machine code

Four reasons:

Cost legibility

Every cost is named in the source (P1, P3). An outside optimizer would rewrite the code afterward, so the named costs would describe a program that no longer exists.

Provability

The object-level proofs depend on a fixed source-to-output correspondence. Optimizer passes break that correspondence, turning the proof from hand-auditable into "prove an entire optimizing compiler".

Diagnosis

A program that needs an optimizer has already lost information. Etch's fixes are in the language — elaboration, the instruction vocabulary, the proofs — not in a backend rewrite.

Measurement

On compiler-class workloads, the optimizer's -O1 to -O3 spread measures about zero — it has nothing to add there.

Performance comes from source instead: the instruction vocabulary (one word = one or two machine instructions) and the named costs, covered in section 柒. The only outside parts are the system linker and the debug format, named in note 1 of section 伍.

Where Etch sits

The chart plots two things: how directly you control the machine, and how much the compiler can check. The exact corner is empty. Etch sits beside it.

machine control (own code generation · no garbage collector · visible cost) compiler-checked guarantees (always stops · same result every time · proofs) proofs, no control both axes managed control, no guarantees historical trade-off PythonJS·TS JavaC# GoHaskell DafnyLean·Coq CC++Zig Rust safe, borrow-checked, LLVM backend Zerolang SPARK·Ada F*·Low* Wuffs (DSL) Jasmin (research) dashed = proofs added on top, code still from GCC/clang (examples simplified) no occupant Etch
managed language — runs on a garbage collector proof assistant — strong checks, but you don't control the generated code bare metal — full control, no automatic safety checks proofs on top, but the generated code still comes from GCC/clang Etch — the red square white circle with vermilion ring = Zerolang (a separate project aimed at AI authors)
  1. The top-right corner is empty. Etch sits next to it, not on it. Etch provides both axes — machine control and compiler-checked guarantees. The two reasons it is not on the exact corner: the system linker and the DWARF debug format come from the operating system's toolchain, and the proof checker is small enough to audit by hand, not formally verified. Together, the linker, the DWARF debug format, and the proof checker are the trusted computing base (TCB, see the glossary). Both reasons are listed openly.
  2. Most languages trade control against guarantees. C gives full control but no automatic checks. Proof assistants give strong checks, but you don't control the generated code. Rust goes partway toward both. Etch provides both axes — machine control and compiler-checked guarantees. The two caveats that keep it off the exact corner are in note 1.
  3. A separate project is also on the chart. zerolang (Vercel Labs) is aimed at AI authors and takes a different route: it stores the program as a graph and treats text as a view over it. Etch keeps text as the source of truth and adds compiler-checked guarantees.

Same bug, three outcomes. Reading one past the end of an array: C — undefined behavior; Rust — a runtime panic; Etch — a compile error naming the missing proof.

Where performance comes from

Regions

Memory uses region-based allocation: one forward sweep to take it, one bulk release to give it back — no per-object malloc/free.

Proven facts

When the compiler has already proved an index is safe, it emits no bounds check and no defensive copy. The proof comes from the language, not from a smart optimizer.

Effect rows → deterministic parallelism

Who reads and writes what is part of a function's type. Because the compiler knows that, it can run independent work on separate cores and still get the same result no matter how many cores are used — that is what "deterministic parallelism" means here. Hand-written parallelism in C or Rust doesn't give you that guarantee.

Performance depends on the workload. Code that allocates a lot, walks linked data, runs in parallel, or runs repeatedly benefits. A simple loop over integers does not. No performance claim is made for anything else. See RFC 007.

Performance

P1 — no hidden waits. Operations the hardware can't predict — random reads, branches that depend on data, indirect calls, atomic operations, calls into the operating system — must be named in the source. A named operation is a site you can point to in the code: for example, a table lookup written t.get(k) is a random read, and the compiler lists that exact site as a wait. Nothing in this class is added silently by the backend.

P2 — you can name machine instructions. A fixed set of source words, each mapping to one or two machine instructions per target — for example, popcount (count one-bits), fused multiply-add (multiply and add in one step), and charclass (classify a byte). Anything outside the set is emulated, and that emulation is listed in the tax bill.

The tax bill. The same five costs section 貳 names, itemized here, followed by the performance costs Etch keeps visible:

  • Crashing → one named, declared abort (running out of a declared budget).
  • Dynamic allocation → a declared budget (fn main budget 256MiB).
  • Effects → rows on each function, checked by the compiler.
  • Being slow → named waits: each operation that can stall is a site you can point to in the code.
  • Unproven code → a named fueled limit; running out becomes a value, not a crash.
  • Machine instructions outside the named set run as emulation.
  • The fixed emitter doesn't reorder instructions or hide register pressure (how many live values must fit in the CPU's registers at once) — those costs stay visible.

Semantics in one screen

Two examples: type-checked bounds and memoization. Each compiles and runs; the interpreter and the compiled version give the same result.

fn g(y: int(0..9)) -> int { return y; }
fn f(x: int(0..20)) -> int {
    if x < 10 { return g(x); }   // x < 10 refines x to int(0..9)
    return 0;
}
fn main() {
    println(f(9));    // 9
    println(f(15));   // 0
}

memo fn caches a pure function's result.

memo fn double(x: int) -> int { return x * 2; }
fn main() {
    println(double(21));   // 42
}

Reactive UI

UI building blocks work the same on every platform. A small core is built in — just the primitives that make those building blocks uniform across platforms. Everything else, such as widgets and application structure, is written in user code, and user code costs the same as the built-in parts.

UI targets: web DOM (wasm32), macOS / iOS, android, terminal (a text-based interface, TUI).

For AI authors

Every cost is written down, so an AI can reason about performance instead of measuring a black-box optimizer. Judge mode scores a code change (a diff between two versions of the program) against the cost model: it lists which named costs — waits, emulation, allocation, effects — were added or removed, so an agent can see whether the change made things cheaper or more expensive. Error messages, effect lists, and traces are machine-readable; that's the interface an agent uses.

拾壹Who it's for / not for

For

  • work that needs exact control (embedded devices, systems code)
  • infrastructure where a bug is expensive
  • tools and agents that generate code
  • systems where performance costs must be visible and arguable

Not for

  • people who need a huge package ecosystem
  • people who want everything they write to be fast
  • people who want a pre-built UI framework

拾贰Toolchain

拾叁RFC index

The RFC index below is generated by site/gen.etch — run site/gen-rfc.sh to regenerate it from docs/rfc-*.md.

  1. 001RFC 001: Etch Foundations
  2. 002RFC 002: Staged Structural Deltas
  3. 003RFC 003: Reactive Execution Model (Epochs, Boundaries, and Events)
  4. 004RFC 004: Modules, Module State, and the Boundary Row
  5. 005RFC 005: The Keyed Operator Algebra (Closed)
  6. 006RFC 006: Value Semantics and Copy-on-Write
  7. 007RFC 007: Performance
  8. 008RFC 008: Numeric Semantics: Integers and Deterministic Floats
  9. 009RFC 009: Stack Bounds
  10. 010RFC 010: IR Vocabulary

Glossary