etch

Etch Programming Language

Write once, script fast, ship native.

Etch is a statically-typed scripting language built specifically for game development. Run your scripts instantly in a fast VM during development with hot-reloading and debugging, then compile the same code to C for production performance. Get the iteration speed of Lua with compile-time safety guarantees and native-level performance when you need it.

The Etch Philosophy

Game development requires two conflicting things: fast iteration during development and maximum performance in production. Most languages make you choose one or compromise on both. Etch offers dual execution modes: a fast VM for prototyping with instant compilation and hot-reloading, plus a C backend for production that generates performance competitive with hand-written C—all from the same source code.

Safety Without Runtime Cost

Before your program compiles, Etch’s prover verifies it’s free from entire classes of bugs:

If the prover can’t verify these properties, your code doesn’t compile. If it compiles, these bugs literally cannot happen.

Key Features

1. The Prover: Your Compile-Time Safety Net

Etch’s prover uses range analysis and control flow analysis to track value ranges through your entire program. When you add two variables, the prover knows the range of possible results. When you access an array, it knows whether the index is in bounds. When you divide, it knows if the divisor could be zero.

fn process(arr: array[int]) -> int {
    var sum = 0;
    for i in 0 ..< #arr {
        sum = sum + arr[i];  // ✅ Index proven in bounds
    }
    return sum;  // ✅ No overflow if inputs are bounded
}

The prover traces ranges through loops, conditionals, and function calls. If it can’t prove safety, you get a clear error message explaining why—often with suggestions for how to fix it.

2. Type Inference That Just Works

You don’t need to annotate every variable. Etch infers types from how you use them, giving you the conciseness of dynamic languages with the safety of static types:

let x = 42;                    // int, inferred from literal
let items = [1, 2, 3];         // array[int], inferred from elements
let doubled = items.map(|n| n * 2);  // Type flows through functions

Function signatures are explicit (for clarity and documentation), but local variables infer naturally.

3. Null Safety Through Monads and Reference Types

Etch provides multiple mechanisms to prevent null pointer dereferences:

Monads for Optional Values Functions that may or may not return a value use option[T] or result[T]. Pattern matching forces you to handle both cases:

fn divide(a: int, b: int) -> result[int] {
    if b == 0 {
        return error("division by zero");
    }
    return ok(a / b);
}

match divide(10, 2) {
    ok(value) => print(value),
    error(msg) => print("Error: " + msg),
}

Reference Types with Compile-Time Verification For heap-allocated objects, use ref[T] (strong reference) or weak[T] (weak reference). The prover tracks nil states and enforces checking:

fn processRef(obj: ref[GameObject]) -> void {
    // ref[T] can be nil - prover tracks when checking is needed
    if obj != nil {
        obj.update();  // Safe: prover verified non-nil
    }
}

fn processWeak(weakObj: weak[GameObject]) -> void {
    // Weak references must be checked before use or promotion to ref
    if weakObj != nil {
        let strongObj: ref[GameObject] = weakObj;  // Weak-to-ref promotion
        strongObj.update();  // Safe: prover knows it's non-nil here
    }
}

fn guaranteed(obj: ref[GameObject]) -> void {
    // If prover knows obj cannot be nil (e.g., freshly created), no check needed
    obj.update();  // Safe: prover verified through data flow analysis
}

Key Semantics:

The compiler won’t let you ignore errors, forget to check for none, dereference potentially nil references without checking, or promote weak references without nil checks. Null becomes a compile-time concern, not a runtime crash.

4. UFCS: Functions That Read Like Methods

Uniform Function Call Syntax lets you call any function as if it were a method, turning nested function calls into readable left-to-right pipelines:

// Without UFCS: nested, inside-out
process(filter(transform(getData())));

// With UFCS: natural, left-to-right
getData().transform().filter().process();

Any function whose first parameter matches can be called this way. It’s a simple syntactic transformation that makes a huge difference in readability.

5. Performance Without Compromise

When the prover verifies your code is safe, the compiler generates code with zero safety overhead. No bounds checks, no overflow checks, no null checks—because they’ve already been proven unnecessary.

The result:

6. Seamless C Interop

Etch’s FFI makes calling C libraries straightforward—just declare the signatures and call them like normal functions:

import ffi cmath {
    fn sin(x: float) -> float;
    fn sqrt(x: float) -> float;
}

let result = sin(0.5).sqrt();  // C functions, Etch syntax

No wrapper code, no build complexity. Etch handles the calling conventions and type marshaling.

7. Compile-Time Superpowers

Execute functions during compilation to pre-compute expensive calculations, embed files directly into binaries, or generate code based on build-time configuration:

// Pre-compute at compile time
let fib_10: int = comptime(fibonacci(10));  // No runtime calculation!

// Embed files directly
let template: string = comptime(readFile("page.html"));

// Conditional compilation
comptime {
    if readFile(".mode") == "debug" {
        inject("logging", "int", 1);
    }
}

Anything you can compute at build time becomes a constant with zero runtime cost.

8. Simple, Consistent Syntax

Etch’s syntax is small and orthogonal. Learn the basics in an afternoon, then apply them everywhere:

// Variables: let = immutable, var = mutable
let x = 42;
var count = 0;

// Functions with explicit signatures
fn greet(name: string) -> void {
    print("Hello, " + name);
}

// Pattern matching for branching
match parseNumber(input) {
    ok(n) => print("Got " + string(n)),
    error(msg) => print("Error: " + msg),
}

No special cases, no hidden complexity. The language gets out of your way.

When to Use Etch

Etch is designed specifically for game scripting where you need:

Primary Use Case: Game Development

Other Use Cases

If you’re building a game and want scripting that’s easier than C++, safer than Lua, and can hot-reload during development, Etch is for you.

How Etch Compares to Game Scripting Languages

Understanding Etch’s position in the game scripting ecosystem:

  Etch (VM) Etch (C) Lua Python C++
Iteration speed Instant Slow Instant Instant Slow
Hot-reload ✅ Yes ❌ No ✅ Yes ✅ Yes ❌ No
Type safety ✅ Static ✅ Static ❌ Dynamic ❌ Dynamic ✅ Static
Safety checks Compile-time Compile-time Runtime Runtime None
Performance ~Lua speed ~C speed Fast Slow Fastest
Debugging Full (VSCode) C debugger Limited Full Full
Compound debug ✅ Yes ✅ Yes ❌ No ❌ No N/A
Learning curve Gentle Gentle Gentle Gentle Steep
Memory model Ref-counted Ref-counted GC GC Manual
Embedding Easy (C API) Native Easy Medium Native

Etch’s sweet spot: Lua’s ease of use + compile-time safety + optional C-level performance, all with full debugging support.

Quick Start

Get Etch Running

# Clone and build
git clone https://github.com/your/etch
cd etch
just build

# Try an example
./etch --run examples/hello_world.etch

Your First Program

Create hello.etch:

fn main() {
    print("Hello, Etch!");
}

Run it:

./etch --run hello.etch

That’s it. No complex build systems, no dependency management. Just write code and run it.

Dive Deeper

Ready to learn more? The documentation covers everything:

Core Language

Safety System

Advanced Topics

Explore Examples

The examples/ directory demonstrates real Etch code in action:

Run any example: ./etch --run examples/arrays_test.etch

The Etch Way

Etch is built on three principles:

Safety first - If the prover can’t verify it’s safe, it doesn’t compile. This feels restrictive at first, but it eliminates entire bug categories.

Simplicity matters - Small feature set, orthogonal design. Learn the fundamentals once, apply them everywhere.

Performance by default - Zero-cost abstractions. Safety checks happen at compile time; the generated code is as fast as hand-written C.

Community

Etch is actively developed and we welcome contributions:


Ready to dive in? Start with Type System & Inference to learn how Etch’s types work.