Agents never need to learn Taal
An agent reviews, tests, profiles, and debugs the emitted Go with tools it already masters — go vet, the race detector, pprof, delve. Taal is upstream of the loop, never inside it.
Taal is a small, strict ML that compiles to gofmt-clean, idiomatic Go — and a framework that rebuilds the load-bearing ideas of the BEAM on the Go runtime. One binary. One database. Actors, supervision, live views, declarative resources, transactional jobs, durable execution. No externalities except Postgres.
Every node runs the same binary and carries the whole system's code. What differs is tuning: at boot, a node is told which roles to serve — web, live views, workers, analytics — and its supervision tree starts only those subtrees. Capacity is added by starting another binary and pointing it at the swarm. There is no orchestration layer, no message broker, no cache tier, no job service. The database is the only externality, and it doubles as the coordination fabric.
This is the architecture the BEAM was built for, transplanted onto a substrate chosen for a different virtue: Go's compiler, formatter, and runtime form the fastest, most unambiguous verification loop in mainstream software. Taal exists to close the gap between those two worlds — Erlang's shape, Go's feedback.
Most compile-to-X languages treat their output as exhaust. Taal treats it as a
contract: everything taalc
emits is gofmt-formatted, idiomatically named, and structured the way a careful Go
engineer would have written it by hand. That single decision does most of the work in
this system:
An agent reviews, tests, profiles, and debugs the emitted Go with tools it already masters — go vet, the race detector, pprof, delve. Taal is upstream of the loop, never inside it.
Any file can be ejected: delete the .taal source, keep the .go output, and continue by hand. Adopting Taal is never a one-way door.
Every change is checked twice — first by Taal's typechecker (ADTs, exhaustiveness, no nil), then by the Go compiler. Both are sub-second. Both produce pointed, one-sentence errors.
gofmt means there is exactly one way the output looks. A model trained or prompted on this corpus sees zero stylistic variance — the property that makes generation reliable.
Each layer is a small framework kernel with a Dutch name and a familiar ancestor. Every layer compiles into the same binary; a node's role tuning decides which ones wake up.
A Bron resource is the canonical Taal artifact: the domain model, its legal state transitions, and its derived surface in one screen of code. English keywords, always — the corpus should read natively to every model that touches it.
resource Invoice { field id Uuid @primary field total Money @min(0) field status Status = Draft type Status = Draft | Sent | Paid | Void action send(self) -> Result(Invoice, SendError) { match self.status { Draft -> Ok({ ..self, status: Sent }) _ -> Err(AlreadySent) } } derive api.rest // handlers + routes derive authz(owner) // policy checks derive migration // schema diff → sql }
// Code generated by taalc. // Readable by design — edit invoice.taal, or // delete it and own this file. type InvoiceStatus int const ( InvoiceDraft InvoiceStatus = iota InvoiceSent InvoicePaid InvoiceVoid ) func (i Invoice) Send() (Invoice, error) { switch i.Status { case InvoiceDraft: i.Status = InvoiceSent return i, nil default: return i, ErrAlreadySent } } // + invoice_http.go, invoice_authz.go, // 0007_invoice.sql — all inspectable.
Supervision reads like OTP because it is OTP's shape. A node's --role
flags select which supervisors boot:
supervisor Billing { strategy one_for_one roles [worker] // only wakes on worker-tuned nodes child Ledger { restart: permanent } child InvoiceMailer { restart: transient, pool: 8 } } actor Ledger { state { balances Map(AccountId, Money) } handle Post(entry Entry) -> Result(Money, LedgerError) { // state is reachable only here — the checker forbids leaking it ... } }
tx { invoice = Invoice.create(order)? Werk.enqueue(SendInvoice { id: invoice.id }) // commits with the row, or not at all }
view InvoiceBoard { state { invoices List(Invoice) = [] } mount(socket) { subscribe("invoices") // postgres LISTEN under the hood { ..state, invoices: Invoice.list()? } } event "mark_paid"(id Uuid) { Invoice.get(id)?.pay()? // NOTIFY fans the diff to every node } }
$ ./mono --role=web,zicht --join=10.0.0.12:4369 $ ./mono --role=worker --join=10.0.0.12:4369 # capacity is a process, not a platform
The surface area is the product. Every exclusion below is load-bearing — each one removes a way for two codebases to disagree, or for an error message to point somewhere far from the mistake.
An agent's effectiveness is a function of its edit-verify loop: how fast the signal arrives and how little it can be misread. Taal is designed backward from that loop.
Every taalc error states what's wrong, where, and one concrete fix — machine-stable format, human-legible prose. The checker is treated as an interface with an SLA, not a gatekeeper with opinions.
taalc is a small strict-ML compiler; go build is go build. Two full verifications per edit, both under a second on a workshop-scale codebase.
Because the framework and language are built together, the canonical corpus — resources, actors, views, jobs, with their emitted Go — is authored once and stays in lockstep. Agents learn one dialect because only one exists.
Stack traces, pprof flames, race reports, and delve sessions all land in the emitted Go — which was written to be read. The agent never hits a layer it can't see through.
The endgame is a division of labor with a clean seam: humans and agents author intent in a language with almost no surface, and agents verify consequence in a language with almost no ambiguity. The compiler is the whole framework; the framework is one binary; the binary needs nothing but Postgres and a peer to join.