This page is generated from
spec.llm.md— edit it there.
ArchLang in one prompt
ArchLang is a tiny declarative language that compiles a .arch source file into a professional floor-plan drawing (SVG/PNG/PDF/DXF). It is built for AI agents: deterministic (same source → identical output), pure (no runtime/IO), and self-correcting (every error carries a machine code and a fix). This page is everything you need to author it. Print it any time with arch spec.
The 7 rules that matter
- Units are millimetres. A 4-metre wall is
4000, not4. Optional metric suffixes fold to mm:4m=4000,3.5m=3500,40cm=400,20mm=20. - Origin is top-left; +x goes right, +y goes DOWN (screen/SVG convention — not math y-up).
- Coordinates are
(x, y)tuples; sizes areWxH(e.g.4000x3000) or<expr> x <expr>with spaces. - Doors and windows must lie ON a wall segment (on its centerline), or you get a
W_DOOR_OFF_WALL/W_WINDOW_OFF_WALLwarning. - String interpolation is
"{expr}"inside double quotes (e.g.label "Unit {i}"). - Ids must be unique. Omit
id=to auto-generate one; give anidonly when you reference it. - Everything is expand-time and pure —
let/for/if/functions all evaluate during compile.
Structure
plan "Title" {
units mm # required-ish settings come first
grid 50 # snap grid in mm
scale 1:50 # drawing scale (annotation only)
north up # up | down | left | right
# … elements and scripting …
title { project "…" drawn_by "…" date "…" }
}Elements
wall <category> thickness <mm> [material <name>] { (x,y) (x,y) … [close] } # category e.g. exterior/partition; `close` makes a loop
room [id=<name>] at (x,y) size <W>x<H> [label "…"] [uses living|kitchen|dining|bedroom|bath|wc|hall|circulation|storage|utility|office|entry …] # OR relational: room [id=…] (right-of|left-of|below|above) <roomId> [align top|middle|bottom|left|right] [gap <mm>] size <W>x<H> [label "…"]
door [id=<name>] (at (x,y) | on <wall> at <pos>) width <mm> [wall <id|category>] [hinge left|right|near start|near end] [swing in|out|into <roomId>] # `at (x,y)` must sit on a wall; `on <wall> at <pos>` pins it BY CONSTRUCTION (<pos> = `40%` | mm from the wall's start | `center`) and can never be reported off-wall — prefer it
window [id=<name>] (at (x,y) | on <wall> at <pos>) width <mm> [wall <id|category>] # same two placement forms as door
opening [id=<name>] (at (x,y) | on <wall> at <pos>) width <mm> [wall <id|category>] # a leaf-less cased opening (gap in a wall) that still connects the two spaces in the access graph
furniture <category> [id=<name>] (at (x,y) | against wall <id> [segment <n>] [offset <mm>] [side left|right] | in <roomId> centered | in <roomId> anchor <a> [inset <mm>]) [size <W>x<H>] [label "…"] [rotate 0|90|180|270] [in <roomId>] # `at` size is plan W×H; `against` size is wall-relative along×depth and derives position+rotation, with `side` inferred from `in <roomId>` when omitted; a known fixture (wc/basin/shower/bathtub/kitchen_sink/counter/stove/fridge…) `against wall` may omit `size` to use its catalogued footprint. `anchor <a>` is top-left|top|top-right|left|center|right|bottom-left|bottom|bottom-right; `inset` (default 0) pulls it in from that edge
dim (x,y)->(x,y) offset <mm> [text "…"] # a dimension line
column [id=<name>] at (x,y) size <W>x<H>
strip <right|left|down|up> at (x,y) gap <mm> [height|width <mm>] { room [id=<id>] size <main>[x<cross>] [label "…"] [uses …] … } # a row/column of rooms laid end to end: each room's offset is the running sum of the previous extents + gap, and the shared cross dimension is the strip's height (right/left) or width (down/up). Pure sugar — expands to absolute rooms. Plan-level block onlyScripting (all expand-time, deterministic)
let NAME = expr— bind a constant.NAME = expr— reassign an existing binding.let f(a, b) = expr— a pure value-function. Built-ins:min max abs sqrt floor ceil round len str.for i in lo..hi { … }— loop over a half-open integer range (0..3→ 0,1,2).if cond { … } else { … }·while cond { … }.set <element>(attr: value)— scoped default for following elements (e.g.set door(swing: out)).- Arrays:
[a, b, c], indexedarr[i]. Operators:+ - * / %,== != < > <= >=,&& ||. Comments:# …. import "lib/x.arch": nameandcomponent name(args) { … }for reuse.
Keyword reference
(Elements are fully specced above; these are the rest.)
- Settings / control:
plan,component,let,theme,title,style,import,for,if,while,else,set,strip - Attributes:
units,grid,scale,north,dims,accTitle,accDescr,material,angle,at,size,width,thickness,label,hinge,swing,offset,text,close,id,project,drawn_by,date,from,as,right-of,left-of,below,above,align,gap,uses,rotate,against,segment,side,on,into,near,anchor,inset,height - Enums / values:
up,down,left,right,in,out,mm,true,false,top,middle,bottom,center,centered,start,end,top-left,top-right,bottom-left,bottom-right,auto,living,kitchen,dining,bedroom,bath,wc,hall,circulation,storage,utility,office,entry
CLI loop (how an agent drives it)
Every command takes --json (structured result on stdout, human messages on stderr) and reads source from a file or stdin (-). Exit codes: 0 ok · 1 internal / IO error · 2 user-source error (deterministic — fix it, don't blindly retry) · 3 bad usage.
arch compile # render a plan to SVG/DXF/TXT/PDF/PNG
arch batch # render many .arch files in one call, concurrently
arch md # render every ```arch block in a Markdown file and rewrite to image links
arch preview # render a PNG you can look at (zero-install where the optional binary is present)
arch watch # recompile on save (interactive)
arch validate # parse + resolve + lint, no render (is it valid & sound?)
arch describe # semantic facts: rooms, areas, adjacency, what doors connect
arch score # continuous intent satisfaction (satisfied/total) as data — the refine-loop reward
arch lint # architectural soundness warnings
arch ast # parse only (no resolve/render) and print the span-bearing AST as JSON
arch complete # completion items in scope at a source byte offset (the LSP completion() core)
arch fmt # canonical formatting
arch repair # explicit source-to-source corrector (furniture out of walls) + change log
arch fix # apply the machine-applicable fix suggestions on a plan's diagnostics (bounded fixpoint)
arch suggest # advisory topology suggestions as data (door/window statements that resolve reachability/window faults)
arch manifest # this document: the whole CLI API as structured data
arch spec # print the one-prompt language spec (spec.llm.md)
arch context # print the full bundled agent context (spec + workflow + CLI + errors)
arch new # scaffold a starter .arch
arch explain # look up an error code (cause / fix / example)The flags that matter (the verb list above covers the rest):
arch compile plan.arch -o out.svg --json # JSON: { ok, diagnostics, summary }. -f txt = zero-dep ASCII plan
echo '<source>' | arch compile - --json # stdin, no temp file
arch validate plan.arch --strict --json # ship-gate: --strict fails on warnings too
arch fix plan.arch --dry-run --json # preview/apply the machine-applicable diagnostics[].fixes
arch validate plan.arch --intent brief.json --feedback --json # gate on a brief's intent contract (miss → exit 2)
arch score plan.arch --brief brief.json --json # satisfied/total — measures, never gatesSelf-correction loop: compile/validate → if ok is false, read each diagnostics[].fix (and line/col/span), edit the source, recompile. Exit code 2 means a deterministic user-source error (fix it; don't blindly retry). Then describe --json to confirm the plan matches intent (right room count, areas, adjacency) without rendering an image. Before shipping, gate with arch validate --strict --json — it fails on advisory warnings too, so a plan that lint flags (furniture through a wall, a fixture blocking a doorway, a room you can't step into, an unreachable room, a walk that squeezes too narrow — W_PATH_TOO_NARROW — or wanders the long way round — W_CIRCUITOUS_PATH) cannot pass silently.
Place furniture so it's physically sound: keep every piece inside its room and off the walls (don't cross a wall centerline); back plumbing/kitchen fixtures onto a wall with against wall <id> (+ in <roomId>) rather than guessing an at; give every room a door/opening; and leave the doorway approach and the door's swing clear.
Fix topology from facts, not guesses. arch repair corrects furniture but never adds a door or window (that is a design choice). When lint reports W_ROOM_UNREACHABLE, W_NO_ENTRANCE, W_BEDROOM_NO_WINDOW, or W_BATH_VIA_BEDROOM, run arch suggest --json — it returns ready-to-paste door/window statements (furniture-aware: a door candidate never opens onto a wardrobe) that reference a wall only by a stable ref (an authored id or a unique category) or absolute coordinates — never a re-bindable positional auto-id — with a rationale; pick one and insert it. If nothing fits, read describe --json (access.rooms[].reachable, room bbox/adjacent, building extent = min/max of room boxes) and attach the opening yourself — an exterior entrance into a cut-off living space beats routing a bath through a bedroom — then re-repair and validate --strict. See SKILL.md for the full recipe.
Common mistakes
| Mistake | Fix |
|---|---|
Using metres (size 4x3) | Use millimetres (size 4000x3000). |
| Expecting +y to go up | +y goes down; a room below another has a larger y. |
| Door/window floating off its wall | Attach it: door on <wall> at <pos> — hosted by construction, it can never be off-wall. |
| Hand-summing room offsets | Lay the row with strip — each room's at is computed for you. |
Furniture floated at a guessed (or copy-pasted) at | Place it in <room> anchor <9-point> [inset] or against wall <id> — closed-form, never floats or penetrates. |
size 4000 (no height) | Sizes are WxH: size 4000x3000 (or W x H with spaces). |
Reusing an id | Ids are unique; omit id= to auto-generate. |
| String math without interpolation | Use "{expr}", e.g. label "{aream2(W,H)} m²". |
Worked examples
examples/attached.arch
# A one-bedroom flat authored with the v1.13 placement sugar — no hand-computed
# coordinates for openings or furniture. It exercises, together:
# • `strip` — a row of rooms laid out end to end
# • openings attached to a wall by position (`door|window … on <wall> at <pos>`)
# • a door that opens toward a named room (`swing into`) hinged at a wall end
# • furniture placed relative to a room (`in <room> anchor …`)
# Compiles clean and passes `arch lint`.
plan "Attached 1BR" {
units mm
grid 100
north up
# Living + bedroom laid left-to-right by a strip, sharing a 4 m depth.
strip right at (0,0) gap 0 height 4000 {
room id=r_living size 4000 label "Living" uses living
room id=r_bed size 3000 label "Bedroom" uses bedroom
}
# Exterior shell as four straight walls (each a clean start→end to attach onto)
# plus the partition between the two rooms.
wall id=w_north exterior thickness 200 { (0,0) (7000,0) }
wall id=w_south exterior thickness 200 { (0,4000) (7000,4000) }
wall id=w_west exterior thickness 200 { (0,0) (0,4000) }
wall id=w_east exterior thickness 200 { (7000,0) (7000,4000) }
wall id=w_part partition thickness 100 { (4000,0) (4000,4000) }
# Entrance 2000 mm along the south wall, hinged at that wall's start end and
# opening into the living room. The bedroom door on the partition opens inward.
door id=d_main on w_south at 2000 width 1000 hinge near start swing into r_living
door id=d_bed on w_part at 2000 width 900 swing into r_bed
# A window centred on each room's exterior wall.
window on w_west at 50% width 1400
window on w_east at 50% width 1200
# Furniture placed by anchor inside each room (never off a coordinate).
furniture sofa in r_living anchor top-left inset 300 size 2000x900 label "Sofa"
furniture bed in r_bed anchor right inset 300 size 1500x2000 label "Bed"
}examples/parametric.arch
# Parametric plan (v0.8 scripting): a row of studio units generated with a
# `for` loop over a range, a value-function, an array indexed per unit, a scoped
# `set` rule, an `if`, and string-interpolated labels. Everything is derived
# from the constants — change COUNT and the whole row regenerates.
plan "Parametric — Studio Row" {
units mm
grid 50
scale 1:100
north up
# Plan-level constants (visible everywhere below — plan scope is global).
let WALL = 200
let W = 4000 # unit width
let H = 5000 # unit depth
let DOOR = 900
let WIN = 1600
let COUNT = 3 # number of units
# A value-function (pure closure) — area in square metres.
let aream2(w, h) = w * h / 1000000
# Per-unit names, indexed by the loop variable.
let names = ["Studio A", "Studio B", "Studio C"]
# Entrance doors swing outward throughout this plan (scoped default).
set door(swing: out)
for i in 0..COUNT {
let x = i * W
wall exterior thickness WALL { (x, 0) (x + W, 0) (x + W, H) (x, H) close }
room at (x, 0) size W x H label "{names[i]}"
furniture bed at (x + 300, 300) size 1500x2000 label "Bed"
furniture kitch at (x + W - 1900, 300) size 1600x600 label "Kitchen"
door at (x + W / 2, H) width DOOR wall exterior hinge left
window at (x + W / 2, 0) width WIN wall exterior
# The end unit carries a per-unit area dimension (computed by the function).
# Referenced to the outer face (y = H + WALL/2) so the extension lines start at
# the wall and read downward, away from the building.
if i == COUNT - 1 {
dim (x, H + WALL / 2)->(x + W, H + WALL / 2) offset 600 text "{aream2(W, H)} m² each"
}
}
# Overall run, dimensioned above the building: right-to-left so the offset lands
# ABOVE the row (outside), and referenced to the outer top face (y = -WALL/2).
dim (W * COUNT, 0 - WALL / 2)->(0, 0 - WALL / 2) offset 1300 text "{COUNT} units"
}