Skip to content

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

  1. Units are millimetres. A 4-metre wall is 4000, not 4. Optional metric suffixes fold to mm: 4m=4000, 3.5m=3500, 40cm=400, 20mm=20.
  2. Origin is top-left; +x goes right, +y goes DOWN (screen/SVG convention — not math y-up).
  3. Coordinates are (x, y) tuples; sizes are WxH (e.g. 4000x3000) or <expr> x <expr> with spaces.
  4. Doors and windows must lie ON a wall segment (on its centerline), or you get a W_DOOR_OFF_WALL / W_WINDOW_OFF_WALL warning.
  5. String interpolation is "{expr}" inside double quotes (e.g. label "Unit {i}").
  6. Ids must be unique. Omit id= to auto-generate one; give an id only when you reference it.
  7. Everything is expand-time and purelet/for/if/functions all evaluate during compile.

Structure

arch
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

text
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 only

Scripting (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], indexed arr[i]. Operators: + - * / %, == != < > <= >=, && ||. Comments: # ….
  • import "lib/x.arch": name and component 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.

text
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):

bash
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 gates

Self-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

MistakeFix
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 wallAttach it: door on <wall> at <pos> — hosted by construction, it can never be off-wall.
Hand-summing room offsetsLay the row with strip — each room's at is computed for you.
Furniture floated at a guessed (or copy-pasted) atPlace 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 idIds are unique; omit id= to auto-generate.
String math without interpolationUse "{expr}", e.g. label "{aream2(W,H)} m²".

Worked examples

examples/attached.arch

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

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"
}