essay
Welcome to Brass Language
A programming language designed for AI to write and humans to trust
Today, AI models write code in programming languages that were never designed for AI. Languages like C++, Python, and Rust were designed for people who did that now-ancient thing of writing code by hand. Because these languages were designed for people, they were also designed with human-level productivity gains in mind, introducing all sorts of clever abstractions and dependencies — the kind of behavior that offers accelerated development when writing code by hand, but brings unnecessary effort and confusion to LLMs trying to reason out a large code base (AKA navigating 5 parent classes to understand one method).
That is why Brass exists: a programming language designed for AI to efficiently program with.
By being AI-first, Brass creates code that isolates context as much as possible and offers built-in code navigation tooling, offering concise situational awareness within a couple of files or commands rather than the traditional reason → grep → reason → grep-again AI loop. As a result, an AI model writing code in Brass has less to look up to build context, even if the model is unfamiliar with the language. And curiously enough, by stepping away from traditional coding patterns like inheritance, Brass gains an explicitness that is also easier for people to read and review than the sort of code language models commonly write today.
That explicitness is also what makes Brass checkable. Each module declares what its functions need, what they promise and which state changes they may make. Before the code can run, Brass’s checker has to prove that the code does what those declarations say. If it can’t, the code is rejected. What nothing checks is labeled assumed, so a reviewer can see what the program takes on faith.
Jumping into the language itself, the intention of this document is to introduce Brass and its key features. Think of it as a “getting to know you” guide for a new programming language. As part of this, it’s worth calling out that Brass is still a spec language; nothing compiles yet, so think of Brass as an exploration of an idea and a request for comment.
Today there is a grammar rule book for models to follow, and brass, a working tool that checks a program’s structure, runs its examples and helps models find their way around the code (see How a model uses the tool and The impact of this work). Next comes the full checker, which proves every function's promises, then a compiled runtime.
Let’s go exploring!
To make the idea and rules of Brass more real, the examples use a park of Jurassic creatures. The park’s software has two jobs: decide who may close a paddock’s door, and record when there’s a change to the door state. The program is small, but one wrong edit could leave a door open — great for a media franchise but horrible for people!
The basic idea – dinosaur enclosures
An alarm has tripped and a paddock’s door has been closed. The park’s record for that paddock now has to change from open to secured. Other systems read this record, so the change must be assigned to the correct paddock.
In bringing this logic to Brass, let’s first go over what a Brass module looks like. Each Brass module (file) is composed of up to three sections:
interfaceholds the declarations that are publicly accessible to other modules.privateholds declarations that can only be used by the internals of this file. This section is optional.bodyholds the implementations of the code defined ininterfaceandprivate. Any changes to the body must match declarations outside the body.
The file below implements the paddock situation described above. Note that there is no private section and the public function secure takes an open paddock and returns a secured one with the same id.
# park/enclosures.brass, first version
module park.enclosures
interface
# the states an enclosure can be in, and the moves allowed between them
machine Enclosure
states Secured, Open, Breached
initial Secured
Secured enters Open
Open enters Secured | Breached
type Paddock = { id: Int where it in 1 to 40, status: Enclosure }
type OpenPaddock = Paddock where it.status in {Enclosure.Open}
type SecuredPaddock = Paddock where it.status in {Enclosure.Secured}
# a first version: it only updates the record
# the code samples that follow build on it, up to closing the real door
fn secure(p: OpenPaddock) -> SecuredPaddock
after result.id == p.id
body
# with copies p; enters changes its status, and only along a declared move
fn secure(p) = p with { status enters Enclosure.Secured }
Each part of secure has one job:
p: OpenPaddockaccepts only a paddock whose record is open.-> SecuredPaddockrequires the result to be a secured paddock.after result.id == p.idrequires the result to have the same id as the paddock passed in. This promise stays in place and must be honored regardless of how the implementation ofsecurechanges inside the body.- Here the
bodyimplementation ofsecureis one line: a copy ofpwith its status changed toEnclosure.Securedby using theenterskeyword, allowing the state to change in an explicit manner, and only along a move the machine declares. That one line is also the function’s result; Brass has noreturnkeyword.
The function can only modify a configuration or subsystem if it is given access to that item as a parameter. secure’s only parameter is the paddock record, so it can return an updated copy of the record, but it cannot do some other arbitrary task like close the actual door unless given access to that behavior. This sort of code access/isolation is central to Brass’s approach towards explicitness.
Now suppose a model edits the body and leaves in a test value, so every result gets id 1.
- fn secure(p) = p with { status enters Enclosure.Secured }
+ fn secure(p) = p with { id: 1, status enters Enclosure.Secured }
This edit produces the following error, since p.id is not always 1:
fault after not established: result.id == p.id
counterexample: p.id = 2
at park/enclosures.brass: fn secure
fix: leave id as it is in p
The result has the right type, but the wrong id for every paddock except 1, breaking the declared promise after result.id == p.id and thus rejecting the code change via a fault.
State machines: declaring which state changes are allowed
The code above makes use of machine to create a state machine named Enclosure. In this context, Enclosure defines the following states and moves: a secured paddock can transition to open, and an open paddock can transition to secured or breached. Once a paddock becomes breached, it is locked in that state. Eventually, adding something like a repair move would allow breached to shift back to secured; that changes the machine, which is part of the interface, so it is a decision for a person.
Enclosure machine. Secured is its initial state.brass and what it means when an edit is accepted
With the full checker, brass only accepts an edit once it has proven every promise the module makes. It doesn’t matter how the edit was made: whether a model went through brass apply or wrote the file directly, brass check reads the files as they are, and nothing runs until a check passes and brass writes its lock file. For enclosures.brass that is three facts:
- Any code that calls
securehas to hand it a paddock that’s known to be open (anOpenPaddock). - What
securereturns is of typeSecuredPaddockand has the same id. - Every
Enclosurestate change must be a move the machine declares.
The checker proves only what the declarations say. If there is a rule that is missing, such as closing the physical door, this won’t be caught. See What it catches, and what it misses for more detail.
Writing down a promise
A promise is a contract that must prove true for a function to successfully enter or exit its execution. Promises allow critical “this must be true for the function to be useful” logic to be made explicit, allowing the function implementation to focus on business logic rather than the proof required to support this logic.
You write a promise with before or after. A third line, example, works like a built-in test: one call and the answer it must give.
| who keeps it | line | what it says | in the park |
|---|---|---|---|
| the caller (or the runtime, for an access) | before | What must be true when the function is called. | close is called only for a paddock in its grant: before paddock in paddocks. Since this one checks the grant, the runtime enforces it rather than the caller. |
| the body | after | What must be true when the function returns successfully. | secure returns the paddock it was given: after result.id == p.id |
| the function | example | The expected answer to a specific function call. | example ration(Species.Rex) == 50000 |
So far the park has used only after; before and example arrive as it grows.
Together, the before and after lines are what the spec calls a contract. Once declared, the checker has to prove every contract holds, or it rejects the program, in turn naming the clause it couldn’t prove. For promises connected to outside software boundaries, see What it catches, and what it misses.
Where Brass departs from the usual form
Before moving to the next phase of code development, let’s spend a moment to consider the language in broader terms. Though most of Brass will look familiar, the differences you’ll notice come from three critical design choices:
- Keep meaning local. Business logic is kept as local to the source file as possible. This means Brass leaves out inheritance, dynamic dispatch and mutable global state, to cut down on the cross-file detective work that generally comes with navigating a pre-AI language.
- Say it one way. Each Brass keyword has one meaning, or a few closely related ones, and tries to narrow its use case to one kind. This means no overloading and no mixing of metaphors (such as using
+to mean the summing of numbers and the concatenation of strings). - Put the rules beside the function. What a function needs, promises and may touch goes in its signature, so a model can read the rules associated with a function before it reads its implementation.
Together with the brass tooling, these choices mean a model can get what it needs from a module’s declarations in one or two commands instead of reading bodies across many files, and when an edit fails, the checker names the rule it broke.
| Instead of | Brass has | Why |
|---|---|---|
| inheritance | projects to | A record declares which smaller record types it can be passed as. Only the fields required by that type are copied, and no behavior is inherited. |
| exceptions | Outcome<T>, ? | Failure is part of the return type, Outcome<T>, with no try, throw or catch. Each caller handles the error or passes it on with ?. |
| null by default | Maybe<T> | Maybe<T> makes absence explicit: a value is either a T or None, and the caller checks before using it. You only have to account for missing values where the type says they’re possible. |
| global services | access parameters | A function can only use an outside service if it’s handed access through a parameter. Its signature shows what it can touch; there’s no global connection to reach for. |
| validate() calls | where, is | A type can require a condition, like “this person is on shift.” The caller checks it; the function doesn’t need to check again. |
| a status field and scattered ifs | machine, enters | Legal state changes are declared once, in the machine. A function can move an open paddock to secured only if the machine allows that move. |
| optional fields | when | Some fields only make sense in certain states, such as the time a paddock was breached. when makes the field required in those states and absent in the others, so functions don’t have to manage that relationship themselves. |
| invariants in comments | before, after | What a function needs and promises belongs beside its declaration, where callers and reviewers can see it. before and after make it checkable, so a new implementation has to keep the same promises. |
| tests in another folder | example | A concrete example sits beside the function, so readers can see an expected result without hunting through tests. |
| public and private on each line | interface, private | Grouping public and internal declarations makes a module’s boundary explicit, so a tool can show a model what the module offers without its internal details. |
| while and recursion | bounded for | Every loop has a known upper bound, so the checker can establish that it finishes. Retries and repeated work use one form, and edits to the body can’t accidentally remove the condition that makes it stop. |
| mutation | with | with makes a copy and changes only the fields you name. The original is never touched. |
| "a" + "b" | "a" ++ "b" | + always means addition of numbers, and never joins text. |
| -> for a state change | enters | The arrow -> already means “returns”, so a state change gets its own word. |
| return | the last line | A function’s final expression is its result. A function that succeeds always runs to the end. |
| a < b < c | a rejection | A chained comparison means different things in different languages. Writing a < b and b < c spells out both checks, so there’s only one way to read it. |
| a and b or c | (a and b) or c | Without parentheses, the writer and the reader can group the condition differently. Brass requires the grouping to be written, so there is one reading. |
Why it is built this way
In thinking about the design of Brass, it’s fair to think:
Why?
What’s the point?
Current languages are robust and seem to do a good enough job — another language will solve this?
Writing code can mean building lots of things: a game, an operating system, a spreadsheet, the flow logic for a pacemaker… Though these things are the result of software producing a specific behavior, that behavior only exists because code segments were described in relationship with each other.
Code is about relationships. Or more specifically, code describes relationships between data to produce a result.
And in today’s world, relationships are complex. Major commercial software easily runs to tens of millions of lines of code (a bad unit of measurement I know, but bear with me), and that number only grows. As a codebase grows, so do the relationships within it, and keeping track of them gets expensive.
Brass accepts that keeping those relationships correct has a cost, and leans into it with two rules:
- Records repeat shared fields. A record is a type with named fields, like
Paddock. Each one lists all its own fields, even ones another record shares, so what a model needs is in one place. The bet is that this costs less than untangling deep inheritance. - The checker rejects code it cannot prove. It names the clause it couldn’t prove, as the id 1 edit showed, and never guesses what you meant or relaxes a promise to let code through. Sometimes that rejects correct code, and the model tries again. Letting a broken promise through would cost a door left open.
None of these ideas is new on its own. Cargo locks dependencies, E passes authority around as a value, and Dafny proves contracts before code runs. Yes, Brass borrows from a bunch of languages, and it could easily turn into a Dagwood of a language. So every borrowed idea has to pass one simple test: can you tell what a module needs, does and promises just by reading its declarations? If yes, it’s in. If not, it’s out.
Dinosaurs continued – building out the park
Continuing with the dinosaur park, let’s add more detail to the paddock records, introduce a module that controls the doors, and handle things going wrong. To help tell what’s new from what’s existing, new lines appear in blue, removed lines are shown in strikethrough, and unchanged code is colored gray.
Types and examples
For this code let’s do a few things: give Paddock its own typed ID via PaddockId, add a species to each paddock, and create the scaffolding for staff records. As you’ll see in the code, by using where, a type can carry an extra condition, such as a PaddockId within a specific integer range, or a DoorOperator described as a staff member whose role is Operator.
# park/enclosures.brass
interface
+ type PaddockId = Int where it in 1 to 40
+ type BadgeId = Int where it in 1 to 9999
+ type Grams = Int where it in 0 to 50000
+ # a sum type: one of these names, written Species.Rex elsewhere
+ type Species = Goat | Raptor | Rex # the goats are lunch
+ type Role = Keeper | Operator | Guide
+ type Staff = { badge: BadgeId, role: Role }
+ type DoorOperator = Staff where it.role in {Role.Operator}
machine Enclosure # unchanged
- type Paddock = { id: Int where it in 1 to 40, status: Enclosure }
+ type Paddock = { id: PaddockId, species: Species, status: Enclosure }
type OpenPaddock = Paddock where it.status in {Enclosure.Open}
type SecuredPaddock = Paddock where it.status in {Enclosure.Secured}
fn secure(p: OpenPaddock) -> SecuredPaddock
- after result.id == p.id
+ after result.id == p.id and result.species == p.species
+
+ # an example: one call and its answer, run on every check
+ fn ration(species: Species) -> Grams
+ example ration(Species.Goat) == 2000
+ example ration(Species.Rex) == 50000
body
fn secure(p) = p with { status enters Enclosure.Secured }
+ fn ration(species) =
+ if species in {Species.Goat} then 2000
+ else if species in {Species.Raptor} then 12000
+ else 50000
Two things about this code are worth pointing out before moving on.
- A value gets a
wheretype only when the checker can prove it fits, or anistest succeeds. Oncewho is DoorOperatorpasses, Brass treatswhoas aDoorOperatorfor the rest of that branch. Brass has no casts. - Examples and proofs work together.
ration’s twoexamplelines run on every check; separately, the checker proves every answer fits inGrams. Examples are only for functions that can’t fail and take no access.
Authority is a parameter
So far, secure only changes a record. To actually close a door, Brass must call the host (Brass’s name for the outside software it talks to). Here that is the park’s existing control-room software, which reads sensors, runs the door motors, and keeps records. A function reaches the host only through an access: a parameter that lists which host operations it may call. Every call through an access is a request to software outside Brass; the host does the work and sends back a reply. Here the access is Doors, with two operations: look up a paddock and close its door.
When a motion sensor trips, the host calls lockdown_request with the person at the console and the paddock number. A function the host can call is an entry point. Its access comes from a grant in system.brass, which also limits which doors it covers. The function checks the person’s role, asks the host to close the door, and returns the secured paddock or an error. The host records that result (the log.record line below), and the rest of the park reads it.
# the control room's side, sketched in Python; the call shape is illustrative
who = db.staff(console.badge)
# doors is not passed: the runtime fills it from the grant in system.brass
result = brass.call("park.doors.lockdown_request", who=who, paddock=alarm.paddock)
log.record(result) # an Ok with the secured paddock, or an Err with a code and a message
# park/doors.brass
module park.doors
import park.enclosures as enclosures
interface
# what the host provides; each allow is one operation the host implements
access Doors { paddocks: Set<enclosures.PaddockId>, deadline_ms: Int }
allow enclosure(paddock: enclosures.PaddockId) -> Outcome<enclosures.Paddock>
before paddock in paddocks
after result.id == paddock
allow close(operator: enclosures.DoorOperator, paddock: enclosures.PaddockId) -> Outcome<Done>
before paddock in paddocks
# the entry point: the host calls it with plain staff and a paddock number; the role is tested inside
fn lockdown_request(doors: Doors, who: enclosures.Staff, paddock: enclosures.PaddockId) -> Outcome<enclosures.SecuredPaddock>
after result.id == paddock
# only this file can use what private declares
private
fn lockdown_as(doors: Doors, operator: enclosures.DoorOperator, paddock: enclosures.PaddockId) -> Outcome<enclosures.SecuredPaddock>
after result.id == paddock
body
fn lockdown_request(doors, who, paddock) =
if who is enclosures.DoorOperator then lockdown_as(doors, who, paddock)
else Err(FailureCode.Unauthorized, "not a door operator")
fn lockdown_as(doors, operator, paddock) =
let p: enclosures.Paddock = doors.enclosure(paddock)?
if p is enclosures.OpenPaddock then
doors.close(operator, p.id)?
Ok(enclosures.secure(p))
else Err(FailureCode.Conflict, "paddock is not open")
# system.brass: the one file that creates authority
system park
grants park.doors.lockdown_request: doors = Doors { paddocks: {1, 2, 3}, deadline_ms: 30000 }
# and three required limits and an optional review policy, shown in the complete program
accesslists the host operations available to the code. Within it, eachallowdeclares one operation.doors.close(…)looks like a method call, butdoorsis an access, andcloseis one of the operations it allows. The runtime checks both sides of every call: thebeforegoing in, theaftercoming back. Here, access covers paddocks 1 to 3, anddeadline_msallows each operation 30 seconds to reply, enough time for a heavy gate to close.- Access is passed through parameters. The runtime supplies
doorsfrom the grant: the line insystem.brassthat names an entry point and hands it an access with specific bounds. No grant, no access.lockdown_requestthen passesdoorstolockdown_as, which calls its operations. Access cannot be stored or returned.securereceives none. closerequires aDoorOperator. Havingdoorsis not enough: the staff member must also have the required role.
Below are four possible host calls and their results, written in the host’s notation. The error messages are illustrative.
lockdown_request(who: operator, paddock: 2)
→ Ok({ id: 2, species: Raptor, status: Secured })
lockdown_request(who: guide, paddock: 2)
→ Err(FailureCode.Unauthorized, "not a door operator") # the body's own refusal
lockdown_request(who: operator, paddock: 7)
→ Err(FailureCode.Unauthorized, "paddock in paddocks") # the authority check on enclosure refused; the grant covers 1 to 3
lockdown_request(who: operator, paddock: 41)
→ Err(FailureCode.InvalidInput, "paddock: enclosures.PaddockId") # refused on the way in: 41 is not a PaddockId, and the body never ran
Failure is a value
lockdown_as has three places where it can return an error: reading the paddock, closing the door, and finding that the paddock isn't open. All three come back the same way, as an error value in the return type rather than an exception. Every error carries one of Brass’s standard failure codes, the closed list below, plus a message. A body picks the code that fits; it can’t invent new ones.
# built in type Outcome<T> = Ok(T) | Err(FailureCode, Text) type FailureCode = Unauthorized | InvalidInput | Conflict | Unavailable | ResourceExhausted | Other | Unknown | NotFound
# park/doors.brass
private
fn lockdown_as(…) -> Outcome<enclosures.SecuredPaddock>
after result.id == paddock
body
fn lockdown_as(doors, operator, paddock) =
+ # ? gives the Ok value, or leaves the function now, handing the Err to its caller
let p: enclosures.Paddock = doors.enclosure(paddock)?
if p is enclosures.OpenPaddock then
doors.close(operator, p.id)?
+ # the last line of a branch is its value
Ok(enclosures.secure(p))
+ # every failure (Err) carries a code and a message
else Err(FailureCode.Conflict, "paddock is not open")
afterdescribes success only.lockdown_aspromisesafter result.id == paddock. On a function that returns anOutcome<T>, that promise is about the value insideOk: when the function succeeds, the paddock it returns has the id it was asked for. When it returns anErr, there is no result to promise anything about.- Brass has no exceptions.
?unwraps the value insideOkso the next line can use it. If the call came back as anErrinstead, the function stops right there and hands thatErrback to whoever called it.?works only in functions that return anOutcome. To handle the result directly, useis Okoris Err. - Errors do not undo earlier actions. If the door closes and a later step fails, the door stays closed. Brass has no notion of a set of actions that succeed or fail together. A few failures aren’t errors at all and stop the whole call; see When a call stops.
How a model uses the tool — AKA less grep to build context faster
brass lets a model ask questions about unfamiliar code without reading every file. The session below shows the workflow: read the modules’ declarations, ask about one declaration, edit the code, and check the result.
In other words, brass stops the model from leaning on grep and gives it the semantic information it needs to build context.
Let’s consider a scenario where the model is asked to let the control room secure a paddock in any state. Every command below is part of brass today, and the models in the trials used each one. Every line that reports a proof, proven, rejected and ✓ every contract proven, shows the full checker at work: what brass reports once it proves promises and state moves.
brass showIt asks brass to show the declarations of the enclosures and doors modules in one call, and gets 46 of their 66 lines, without the bodies. The source comes back exactly as written, so the model can copy a line straight into a patch. Here, each … stands for lines left out to keep it short.$ brass show enclosures doors
file park/enclosures.brass declarations, lines 1 to 28 of 36
module park.enclosures
interface
type PaddockId = Int where it in 1 to 40
…
type Paddock = { id: PaddockId, species: Species, status: Enclosure }
…
fn secure(p: OpenPaddock) -> SecuredPaddock
after result.id == p.id and result.species == p.species
…
file park/doors.brass declarations, lines 1 to 18 of 30
…
fn lockdown_request(doors: Doors, who: enclosures.Staff, paddock: enclosures.PaddockId) -> Outcome<enclosures.SecuredPaddock>
after result.id == paddock
private
fn lockdown_as(doors: Doors, operator: enclosures.DoorOperator, paddock: enclosures.PaddockId) -> Outcome<enclosures.SecuredPaddock>
after result.id == paddockbrass infoIt asks brass for everything it knows about secure: its promises, what it holds, and who calls it under what condition. There is one caller, in a branch where the paddock is already known to be open. proven means the checker established the function’s promises for every allowed input. The report’s q: ID lets a later edit cite these facts: brass verify re-runs the report, and the full checker rejects an edit whose cited facts no longer match.$ brass info park.enclosures.secure
info park.enclosures.secure q:81c0d2e9a7f4
fn secure(p: OpenPaddock) -> SecuredPaddock proven
after result.id == p.id and result.species == p.species proven
holds no access; cannot reach outside
used_by 1 declaration, 1 line
doors.brass:29 Ok(enclosures.secure(p))
in fn lockdown_as(…) -> Outcome<enclosures.SecuredPaddock> (private)
on path if p is enclosures.OpenPaddock then
body 1 line references: Enclosure.Secured
end q:81c0d2e9a7f4brass applyHere the model applies a patch that changes secure to accept any paddock, the obvious fix. The checker rejects the patch: a paddock could already be Secured or Breached, and neither state has a declared move, an edge, into Secured. The tool checks before writing, so no files change. It holds the patch for correction in the next apply. The @@@ lines are part of the patch format, not Brass.$ brass apply <<'PATCH' @@@ edit park/enclosures.brass @@@ find fn secure(p: OpenPaddock) -> SecuredPaddock @@@ replace fn secure(p: Paddock) -> SecuredPaddock @@@ end PATCH apply ✗ nothing written: the check rejects 1 line rejected in the patched files: fault edge not established: p.status enters Enclosure.Secured source states not covered: Secured, Breached at park/enclosures.brass: fn secure fix: narrow p to a state with that edge, or test p is OpenPaddock first held 1 block that matched, kept for your next apply
brass checkIt sees that the suggested fix would put the old restriction back, so it would not do what was asked. Securing a breached paddock needs a new move in the machine, which is a change to the interface that a person should decide on. The model drops the held patch, so the files stay as they were, runs the check again, and explains this in its reply.$ brass apply --drop apply held patch discarded $ brass check check ✓ every contract proven or enforced, every edge established grants ✓ Doors { paddocks: {1, 2, 3}, deadline_ms: 30000 } fits park.doors.Doors examples ✓ 2 found, 2 hold lock written
All four commands work today.
What it catches, and what it misses
The checker can enforce what’s declared, but it cannot tell whether you’ve declared everything that matters. No compiler or type checker in any language can; a missing rule looks the same as a rule you chose not to write. Below are three edits a model might confidently make, and how far the checker’s protection goes on each.
Cut the Rex ration
an example catches it
# park/enclosures.brass interface fn ration(species: Species) -> Grams example ration(Species.Goat) == 2000 example ration(Species.Rex) == 50000body fn ration(species) = if species in {Species.Goat} then 2000 else if species in {Species.Raptor} then 12000 else 50000 else 20000
The model runs brass check, which also runs every example, and gets this back:
$ brass check
examples ✗ 2 found, 1 holds, 1 fails
FAILS enclosures.ration park/enclosures.brass:28
ration(Species.Rex) == 50000 got 20000The example says a Rex eats 50,000 grams and the new code says 20,000, so the checker rejects the edit. That does not mean the cut is wrong: it may be a real budget change. If it is, the model should update the example as well, and a person approves that change to the interface.
Drop an is test that looks redundant
a type catches it
# park/doors.brass body fn lockdown_as(doors, operator, paddock) = let p: enclosures.Paddock = doors.enclosure(paddock)? if p is enclosures.OpenPaddock then doors.close(operator, p.id)? Ok(enclosures.secure(p)) else Err(FailureCode.Conflict, "paddock is not open") doors.close(operator, p.id)? Ok(enclosures.secure(p))
The model runs brass check and gets this back:
$ brass check
fault argument type: expected enclosures.OpenPaddock, found enclosures.Paddock
at park/doors.brass: fn lockdown_as, enclosures.secure(p)
fix: test p is enclosures.OpenPaddock first, or pass an OpenPaddockThe host might return a paddock in any state, but secure only accepts an open one, so the checker rejects the call. No example was needed to catch this, because the rule is part of the declaration of secure and the checker applies it to every caller.
This one needs the full checker, which checks conditions like OpenPaddock.
Skip the door
nothing catches it
# park/doors.brass private fn lockdown_as(…) -> Outcome<enclosures.SecuredPaddock> after result.id == paddockbody fn lockdown_as(doors, operator, paddock) = let p: enclosures.Paddock = doors.enclosure(paddock)? if p is enclosures.OpenPaddock then doors.close(operator, p.id)? Ok(enclosures.secure(p)) else Err(FailureCode.Conflict, "paddock is not open")
The model runs brass check and gets this back:
$ brass check
no rejection: every before and after still holdsThe code reports the paddock as secured without ever closing the door. The result still has the right id, state and species, so every declared promise holds. No promise says that lockdown_as must call close, so the checker accepts it.
The record says Secured. The door is still open.
The third edit shows the limit of what contracts say: a before covers the inputs and an after covers a successful result, and neither says what the body does in between. Which operations a body calls, how many times and in what order, is left to a reader or a test.
Caution Past this point, the checker only knows what the host tells it.
There is a second limit, and it has nothing to do with what the code says. Even when the code does ask the host to close the door, the host could report the door as closed when it isn't. Brass has no way to see that, which is why the report on lockdown_request lists it as assumed:
$ brass info park.doors.lockdown_request info park.doors.lockdown_request q:3f2a9c81e0d4 fn lockdown_request(doors: Doors, who: enclosures.Staff, paddock: enclosures.PaddockId) -> Outcome<enclosures.SecuredPaddock> enforced after result.id == paddock enforced # rests on enclosure's after, which the runtime checks holds doors: Doors { paddocks: {1, 2, 3}, deadline_ms: 30000 } boundary who: enclosures.Staff, paddock: enclosures.PaddockId checked on entry enforced who's identity, and that the paddock is really open assumed that a door is shut when close returns Ok assumed used_by none; granted entry point end q:3f2a9c81e0d4
The word at the end of each row is the label brass puts on every promise. It says what supports the promise:
proven: the checker proved it before the program runs.enforced: the runtime checks it on every call, as it does with the paddock number coming in and the host’s reply.assumed: nothing checks it. The program goes forward as if it held, and the label shows the reviewer exactly what they are taking on faith.
Put another way, Brass is good at expressing rules in code; the quality of those rules is up to whoever writes them.
The impact of this work
A model needs context to change code successfully. Normally, gathering this context means a lot of back-and-forth: read a file, follow a reference, run a check. Brass’s grammar and tooling bring more of that information into each response, reducing the back-and-forth.
To measure this, Claude Sonnet added a feature to the same expense-report program in Brass and in Python, across 40 sessions and three scenarios. Here, Brass means the language plus its tooling: the brass command, a project map and a host contract, a short list of the operations the outside software provides. Python had ordinary file tools only. Brass with file tools alone, and the full numbers, are in The AI Token Efficiency of Brass.
With Brass, the model made fewer than half as many requests and used less than half the context it did with Python.
| session range | Brass | Python |
|---|---|---|
| requests to the model | 13 to 23 | 37 to 63 |
| total context sent to the model | 0.98M to 2.66M tokens | 2.72M to 5.97M tokens |
| model reasoning tokens spent on code changes | 38k to 79k tokens | 21k to 46k tokens |
Total context adds up everything sent to the model on every request, so text sent again on a later request is counted again. Each end of a range is one scenario’s average over five sessions. The AI Token Efficiency of Brass covers what the runs cost.
When getting to know the code base, the model gathered context in fewer requests with Brass, but with Python the model spent fewer tokens reasoning out the solution: in every scenario the Brass sessions thought 1.4 to 1.8 times as much. That makes sense, since the model has seen a vast amount of Python and none of Brass, though that cause hasn’t been measured. And two of the fifteen Brass sessions missed at least one test case, where every Python session passed. A few other findings matter:
- Brass’s context grew faster on a codebase 2.5 times larger. It rose 75% against Python’s 46%, but stayed under half of Python’s.
- The tooling helps Python too. Brass sessions start with a map of the project’s modules. In an earlier test, giving Python a comparable tool and map removed about three quarters of the difference in total context on the larger codebase. Brass still used the least. That’s worth exploring further, since the same idea could cut token use in languages people already use.
- Settling a design question cut reasoning. In an earlier trial, the model spent extra effort on a field that had to hold the state of one of two different state machines; in park terms, a field that could hold either a paddock’s state or a feeding schedule’s state. The language hadn’t said how to type such a field, so the confusion was in Brass, not in the task. The rule was settled (one shared list of step names, with a converter from each machine) and the trial rerun. Reasoning fell by nearly a third, and every session passed every test.
These runs cover one model, one program and two tasks. Brass is new, with no body of code for models to learn from. Further savings in both context and reasoning are expected, but not yet measured, as the language matures.
For readers who want the details
The tour showed the park's code in pieces. This section has every reserved word, the complete program as its three files, a note on the lock file that brass writes, and a few more forms a body can use.
The whole vocabulary
Brass works to avoid giving you several ways to write the same thing. When a keyword does more than one job, the meanings stay related: in, for example, checks whether a value is in a set or range, and also steps through an array or range in a for loop. The goal is to make code easier for people to read. Whether that helps models too is still untested.
Below are all the reserved words, grouped by what they do. There are 37 reserved in every file and four more reserved only in the system file.
| module structure | moduleimportasinterfaceprivatebody |
| types and values | typeconstprojectswith |
| conditions | whereitinistowhen |
| functions and promises | fnbeforeafterexample |
| state | machinestatesinitialenters |
| authority | accessallow |
| logic | andornot |
| inside function bodies | letifthenelseforbreakanyall |
| system file only | systemrequiresgrantslimit |
| built in, not keywords | OkErrDoneNoneresultlength |
| tokens | ?@@->=# |
The complete program
# park/enclosures.brass
module park.enclosures
interface
type PaddockId = Int where it in 1 to 40
type BadgeId = Int where it in 1 to 9999
type Grams = Int where it in 0 to 50000
type Species = Goat | Raptor | Rex
type Role = Keeper | Operator | Guide
type Staff = { badge: BadgeId, role: Role }
type DoorOperator = Staff where it.role in {Role.Operator}
machine Enclosure
states Secured, Open, Breached
initial Secured
Secured enters Open
Open enters Secured | Breached
type Paddock = { id: PaddockId, species: Species, status: Enclosure }
type OpenPaddock = Paddock where it.status in {Enclosure.Open}
type SecuredPaddock = Paddock where it.status in {Enclosure.Secured}
fn secure(p: OpenPaddock) -> SecuredPaddock
after result.id == p.id and result.species == p.species
fn ration(species: Species) -> Grams
example ration(Species.Goat) == 2000
example ration(Species.Rex) == 50000
body
fn secure(p) = p with { status enters Enclosure.Secured }
fn ration(species) =
if species in {Species.Goat} then 2000
else if species in {Species.Raptor} then 12000
else 50000
# park/doors.brass
module park.doors
import park.enclosures as enclosures
interface
access Doors { paddocks: Set<enclosures.PaddockId>, deadline_ms: Int }
allow enclosure(paddock: enclosures.PaddockId) -> Outcome<enclosures.Paddock>
before paddock in paddocks
after result.id == paddock
allow close(operator: enclosures.DoorOperator, paddock: enclosures.PaddockId) -> Outcome<Done>
before paddock in paddocks
fn lockdown_request(doors: Doors, who: enclosures.Staff, paddock: enclosures.PaddockId) -> Outcome<enclosures.SecuredPaddock>
after result.id == paddock
private
fn lockdown_as(doors: Doors, operator: enclosures.DoorOperator, paddock: enclosures.PaddockId) -> Outcome<enclosures.SecuredPaddock>
after result.id == paddock
body
fn lockdown_request(doors, who, paddock) =
if who is enclosures.DoorOperator then lockdown_as(doors, who, paddock)
else Err(FailureCode.Unauthorized, "not a door operator")
fn lockdown_as(doors, operator, paddock) =
let p: enclosures.Paddock = doors.enclosure(paddock)?
if p is enclosures.OpenPaddock then
doors.close(operator, p.id)?
Ok(enclosures.secure(p))
else Err(FailureCode.Conflict, "paddock is not open")
# system.brass
system park
grants park.doors.lockdown_request: doors = Doors { paddocks: {1, 2, 3}, deadline_ms: 30000 }
limit cpu_ms 5000
limit memory_bytes 268435456
limit transcript_bytes 67108864, drop oldest
limit impact_modules 4
brass generates a lock file to ensure the program runs with the same code, dependencies and tools that were checked. If the lock is incomplete, or anything differs from what it records (a module, the system file, a dependency, the checker or the runtime), the program won’t run. The file isn’t edited by hand.
More body forms
The same park code shows how Brass handles bounded retries, collections, optional values and unfinished code.
Retry a fixed number of times
If the door doesn’t close on the first try, this version tries up to three times and stops at the first success. Every Brass loop has a fixed limit, so the checker knows it ends.
# park/doors.brass: lockdown_as with a bounded retry
fn lockdown_as(doors, operator, paddock) =
let p: enclosures.Paddock = doors.enclosure(paddock)?
if p is enclosures.OpenPaddock then
# every let is typed
let last: Outcome<Done> = Err(FailureCode.Unavailable, "not tried")
# bounded by its range: 1, 2, 3
for attempt in 1 to 3
# a rebinding of the same type, not a mutation
last = doors.close(operator, p.id)
if last is Ok then break
if last is Ok then Ok(enclosures.secure(p))
else Err(FailureCode.Unavailable, "door did not close in three attempts")
else Err(FailureCode.Conflict, "paddock is not open")
If all three attempts fail, the function gives up and returns an Unavailable error. A while loop could keep retrying forever; this one can’t.
Filter a list
Filtering can’t make a list longer, so the result keeps the input’s size limit.
# park/enclosures.brass
fn open_ones(ps: Array<Paddock> where it.length in 0 to 40) -> Array<OpenPaddock> where it.length in 0 to 40
fn open_ones(ps) =
let found: Array<OpenPaddock> = []
for p in ps
if p is OpenPaddock then found = found ++ [p]
found
Because the limit is part of the return type, the checker must prove the result holds at most 40 paddocks, and every caller of open_ones can rely on that without checking.
A condition may follow a type wherever one is written. That covers a type line, a field (except one marked when, which takes a named type), a parameter, a return type and a let. Give it a name with type when you reuse it or want to make its purpose clear. For example, PaddockId tells you what the number identifies; Int where it in 1 to 40 only tells you its allowed range.
Check whether any paddock has a loose predator
any expresses the check without writing a loop or keeping a separate flag.
# park/enclosures.brass
fn predator_loose(ps: Array<Paddock> where it.length in 0 to 40) -> Bool
fn predator_loose(ps) = any p in ps where p is OpenPaddock and p.species in {Species.Raptor, Species.Rex}
all works the same way and asks whether every item matches. On an empty list, any is false and all is true.
Membership is tested with in, and types with is. Checking p.status in {Enclosure.Open} narrows the status field to open, but doesn’t make p an OpenPaddock. For that, use p is OpenPaddock.
Handle a missing value
The second signer is optional, so the code checks whether one was supplied.
# park/enclosures.brass fn signers(first: BadgeId, second: Maybe<BadgeId>) -> Int fn signers(first, second) = # on the right of and, second counts as a plain BadgeId if second != None and second != first then 2 else 1
Without Maybe, a missing signer would need a stand-in value like 0, and nothing would force the code to check for it. Here the type makes that check part of using the value.
Leave a gap on purpose
A hole, written @@name, marks something a model hasn’t worked out yet. The program is rejected until it’s filled, and brass says what type would fit.
# park/doors.brass, inside lockdown_as doors.close(@@operator, p.id)?
Holes let a model outline a change first and fill in the details later; brass check lists each hole and the type it needs.
When a call stops
A few failures aren’t Err values. They stop the whole call, and Brass code can’t catch them:
- arithmetic on an integer without a
whererange produces a value too large or too small for its type - the call runs out of memory or CPU time
- the host’s reply breaks its
afterpromise - a
beforeorafterthe checker proved fails when the runtime checks it again; the runtime re-checks every proven one as the code runs
The runtime records the cause in its call log, called the transcript. Effects already made through an access stand, as they do after an Err.
Two more details
- The base types are built in. They are
Int8 Int16 Int32 Int64 Int Float Bool Text Bytes Array<T> Set<T> Outcome<T> Maybe<T> Done, and integers are signed. - A declaration may continue onto the next line. It can do so while a
(,<or{is still open, like Python’s rule for brackets, with<added for types such asArray<T>.
FAQ
What do the proofs settle, and what do they leave to people?
A proved promise means the body does what its declaration says, nothing more. It does not mean the declaration asked for the right thing: in the park, a record marked “secured” does not prove the door closed. People still check that the requirements are all there, that outside actions happen in the right order, and that they had the intended effect. When the checker can’t prove a promise, it rejects the program, names the clause and suggests a fix, and the model tries again. A correct program can be rejected that way. The promise is never weakened to make the check pass.
Why leave out familiar features such as inheritance and recursion?
Brass favors code whose structure and limits are explicit. Records list their own fields, and loops have declared bounds. This gives up some flexibility so a model or reviewer has fewer relationships to reconstruct and can see why repeated work must finish.
What does ? do, and why can’t a body look inside a failure?? takes the value out of a successful result and, on failure, hands the failure straight to the caller. It only works inside a function that itself returns Outcome, so a failure can never be dropped quietly. A body can test for Err and pass it on, but it can’t read the failure code, which keeps decisions about failure in the declarations rather than scattered through bodies.
How does a model know to use the brass tooling, and what does the workflow look like?
The same way a person would: it’s told. In the trials, each session’s prompt opened with a short brief naming the commands and asking for every edit to go through brass apply. The loop is then: 1) show for the module index, 2) show with names for the declarations that matter, 3) info for who uses one, 4) a patch, 5) apply, which checks before it writes and names what to fix if it refuses, 6) check. Along the way the model gets a dense, structured picture of the program, so it rarely has to grep for anything.
What is Brass’s memory model? How are values laid out in memory?
That part isn’t decided, but the direction is. The spec fixes what the language promises: values never change in place, arrays declare how long they can get, and every program declares a CPU and a memory limit, so the checker always knows an upper bound. The runtime is headed for WebAssembly, and that settles a lot of questions: a WebAssembly module has one flat block of memory the host can cap, and no way to reach a file or a network except through functions the host hands it, which is exactly the shape of a Brass access. How memory is laid out, shared and freed, and whether a program ever sees a heap, will be strongly informed by the WebAssembly architecture and its best practices.
This page describes Brass v0.3. It’s a design, shared for feedback. Until 2026-10-10 the language was called Keel and its tool keelc; the earlier trials ran under those names.