Hale — a language for running systems

5 min read Original article ↗

a language for running systems

Describe the system. Hale compiles the mechanism.

Write the services, messages, ownership, placement, and recovery as one program. Hale turns that description into direct calls, queues, sockets, shared memory, or broker adapters — and checks the resulting system before it runs.

$ curl -fsSL https://hale-lang.org/install.sh | sh

room.hl

type Msg { room: String; user: String; text: String; }

topic Posted    { payload: Msg; keyed_by room; }
topic Broadcast { payload: Msg; }

locus Room {
    params { name: String = "lobby"; }

    bus {
        subscribe Posted as on_post where key == self.name;
        publish Broadcast;
    }

    fn on_post(m: Msg) {
        Broadcast <- m;
    }
}

a roomlocus Room each posted messagesubscribe Posted in that roomkeyed_by room relay it to everyonepublish Broadcast

why now

Code is getting cheaper. Consequences are not.

More implementations will be produced by more people and more tools. The scarce part is knowing what each part may touch, publish, block on, allocate, depend on, or destroy. Hale makes those facts part of the program instead of leaving them in review comments and deployment lore.

one typed edge, several mechanisms

Build a monolith. Deploy a distributed system.

The loci and their topic sends stay put. Change main to decide where they run and how each edge travels.

main.local.hl

main locus App {
    params {
        ingest: Ingest = Ingest { };
        rollup: Rollup = Rollup { };
    }
}

Dispatch
Direct call where provable; otherwise the in-process bus.

Boundary
One process, one ownership tree.

Domain code
Unchanged.

main.pinned.hl

main locus App {
    params {
        ingest: Ingest = Ingest { };
        rollup: Rollup = Rollup { };
    }

    placement {
        ingest: pinned(core = 1);
        rollup: cooperative(pool = compute);
    }
}

Execution
Dedicated core for ingest; cooperative pool for rollup.

Communication
The same typed topic, still in process.

Domain code
Unchanged.

main.unix.hl

main locus App {
    params {
        ingest: Ingest = Ingest { };
        rollup: Rollup = Rollup { };
    }

    bindings {
        Samples: unix("/run/samples.sock");
    }
}

Transport
Framed Unix socket with message boundaries preserved.

Failure
The transport is a child locus and enters supervision.

Domain code
Unchanged.

main.shm.hl

main locus App {
    params {
        ingest: Ingest = Ingest { };
        rollup: Rollup = Rollup { };
    }

    bindings {
        Samples: shm_ring("/samples",
            slot_count: 1024,
            on_overflow: drop)
            where intra_machine, zero_copy;
    }
}

Transport
Shared-memory ring for a high-rate same-machine edge.

Policy
Capacity and overflow behavior are visible at the seam.

Domain code
Unchanged.

main.adapter.hl

main locus App {
    params {
        ingest: Ingest = Ingest { };
        rollup: Rollup = Rollup { };
    }

    bindings {
        Samples: NatsAdapter {
            url: "nats://prod:4222"
        };
    }
}

Transport
A user-written locus supplies the external broker.

Contract
Ordering, retries, and durability belong to the adapter contract.

Domain code
Unchanged.

A successful send means the broker accepted the message under the selected binding's guarantee. A binding that cannot be opened fails the declaring locus at birth; it does not leave a route that claims success while dropping everything.

one recursive structure

The architecture carries the runtime semantics.

A locus is not just an actor or an object. It is where domain identity, owned state, memory lifetime, execution, children, and failure policy meet.

main locus Telemetry owns the system root deployment authority

typed broker plane subjects are above publishers and subscribers

SamplesSnapshot

locus Ingest owns socket + receive arena pinned · core 1

locus Rollup owns windows + counters pool · compute

locus Metrics owns HTTP server state pool · io

ownership and lifetime communication through a typed subject physical placement
ownership

Structure is the lifetime.

A locus owns its region and children. Dissolve it and the owned memory is reclaimed deterministically.

communication

Topics name the edge.

Publishers and subscribers declare intent. They do not hold references to one another.

execution

Placement is separate from logic.

The same locus can share a pool, own a thread, pin to a core, or sit across a process boundary.

failure

Recovery belongs to the owner.

Structural failures reach the parent with enough context to restart, quarantine, or let them bubble.

causality as a contract

The compiler follows the system past the function call.

A call graph stops at a publish. Hale continues through the typed bus graph, across republishers and into the loci that receive the result.

report.hl

@effects(depends: {PublicSummary, ConfigChanged})
locus Report {
    bus {
        subscribe PublicSummary as on_summary;
        subscribe ConfigChanged as configure;
    }
}

The declaration says exactly which subjects may transitively influence Report. It is a complete set, not a comment.

hale check

declared dependency set violated:
  Report can transitively depend on PrivateFeed

Path:
  subject PrivateFeed
  -> Relay
  -> subject PublicSummary
  -> Report

A violation names the architectural path, including the innocent- looking republished subject that hid the original dependency.

Operational effects

Block, syscall, time, entropy, FFI, publish, spawn, recursion, and allocation.

Causal reach

What a function may cause through calls and topic publication.

Dependency sets

What may transitively influence a locus through its subscriptions.

Budgets

Hard bounds such as zero allocations per call on a hot path.

one line of development

Declare it. Check it. Run it. Compare it with reality.

Hale's direction is not a pile of unrelated features. It is a progression from architecture as source to architecture that can be observed, replayed, and safely changed.

shipped Describe loci, topics, ownership

shipped Check effects, causality, budgets

shipped Deploy placement and bindings

shipped Observe runtime topology and events

RFC Replay recorded deterministic execution

RFC Adapt verified deployment transitions

Deterministic replay and verified deployment transitions are RFC work. They are shown here as direction, not as shipped commands.

the toolchain

One binary, the whole loop.

Build, run, check, test, format, document, benchmark, and verify. The LSP and MCP server expose the same structured diagnostics to editors, CI, and other tools.

supervision.hl

locus WorkerPool {
    accept(c: Worker) { }

    on_failure(c: Worker, err: ClosureViolation) {
        restart(c);
    }
}

built to be inspected

Claims should have somewhere to land.

the shortest useful introduction

Run a system, then change its deployment.

The playground tour starts with ordinary code and ends with typed communication, ownership, supervision, and placement.