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.
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.
Before your program compiles, Etch’s prover verifies it’s free from entire classes of bugs:
option[T] and result[T] monads make optional values explicit and force handlingref[T], weak[T]) are analyzed by the prover for potential nil statesIf the prover can’t verify these properties, your code doesn’t compile. If it compiles, these bugs literally cannot happen.
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.
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.
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:
ref[T] - Strong reference, can be nil, prover tracks when nil checking is requiredweak[T] - Weak reference, can be nil, must be checked before use or promotion to ref[T]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.
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.
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:
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.
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.
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.
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.
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.
# Clone and build
git clone https://github.com/your/etch
cd etch
just build
# Try an example
./etch --run examples/hello_world.etch
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.
Ready to learn more? The documentation covers everything:
Core Language
Safety System
Advanced Topics
The examples/ directory demonstrates real Etch code in action:
arrays_test.etch - Array operations with compile-time bounds checkingmatch_pattern_test.etch - Pattern matching with option and resultufcs_advanced_test.etch - Beautiful method chains with UFCScffi_math_test.etch - Calling C library functionsRun any example: ./etch --run examples/arrays_test.etch
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.
Etch is actively developed and we welcome contributions:
Ready to dive in? Start with Type System & Inference to learn how Etch’s types work.