# Rat - a guide for AI models
You are reading the canonical AI-facing guide to **Rat**, a small language +
web framework where one `.rat` file is a server, a page, and its styles.
Rat is not in your training data. Do not guess syntax from HTML, JS, Python,
or any template language - several constructs look familiar but behave
differently. When this guide and your intuition disagree, this guide wins.
Human docs with live examples: https://rat-lang.com/documentation
Full extracted docs for models: https://rat-lang.com/llms-full.txt
Last verified against the runtime: 2026-06-10 (rat 0.2.x).
---
## 1. Mental model
- A Rat app is a directory of `.rat` files served by a single Go binary
(`rat start`). No npm, no build step, no JS to write.
- **One dispatch primitive**: every callable is `name.method(args)` or
`name(args)`. Builtins, user functions, services, database handles,
JSON API namespaces, Python modules (Tail), and JS libraries (Inverse
Tail) all resolve through the same lookup. Code never imports anything.
- **SSR-first**: `> server` state is computed per request and baked into
the HTML snapshot. Client-side reactivity patches the rendered spans in
place; explicit round-trips happen through event handlers.
- A `.rat` file is a sequence of **sections** opened by `> name` headers.
Markup lines, guard lines, and loop lines live inside `> page`.
Indentation is structural (4 spaces per level).
- `#` starts a comment (full-line or trailing).
The smallest complete app (one file, `pages/index.rat` or shown standalone):
```rat
> server
name: 'Angelo'
> page
count: 0
hello [name]
you've clicked [count] times
click me
```
That renders server-side, ships to the browser, and the button updates the
`[count]` span client-side with no network call.
---
## 2. Project layout and routing
```
mysite/
pages/ # routed pages: pages/about/page.rat -> /about
pups/ # components: pups/greeting.rat -> tag
styles/ # global CSS: styles/global.styles.rat
api/ # JSON endpoints: api/user.rat -> /api/user
data/ # models + seed services: data/user.rat
lang/python/ # Tail modules: text.py -> text.* namespace
lang/js/ # Inverse Tail / auto-mount JS modules
public/ # static assets, served at /
main.rat # root layout + site-wide services/functions
settings.rat # port, host, db, build modes
```
- Every directory is auto-discovered; there is no manifest and no import
statement anywhere in the language.
- `pages/blog/[slug].rat` is a **dynamic route**: matches `/blog/anything`,
exposes the capture as `args.slug` AND as bare top-level `slug` (same
value, use either).
- Multiple captures stack: `pages/users/[id]/posts/[post_id].rat` gives
`args.id`, `args.post_id`.
- `main.rat` wraps every page (the shell). Site-wide services and top-level
functions are conventionally declared there.
---
## 3. Sections and the five state tiers
A section header is `> name` at column 0. State blocks hold one `key: value`
binding per line. The tier you declare a name in decides its lifetime:
| Tier | Where it lives | Per-user? | Survives |
|--------------|-----------------------|---------------|---------------------|
| `> server` | server, per request | computed each request | just this request |
| `> global` | server memory, keyed by URL | shared by all viewers | until server restart |
| `> page` | browser JS heap | yes | until navigation |
| `> session` | browser sessionStorage| yes (per tab) | until tab close |
| `> local` | browser localStorage | yes | until cleared |
- `> server` is the home for DB reads and derived data: `todos: main_db.todo.all()`.
- `> page` state mutates client-side with **no** round-trip.
- Mutating `> server` / `> global` state from a handler round-trips
automatically through the framework's call endpoint.
- `route` is a **reserved name** - never declare a state var called `route`.
- Other reserved identifiers in handler scope: `event` (the DOM event),
`args` (route captures), `nav` (`nav.goto('/path')`).
Literals: `'single-quoted strings'`, numbers, `true` / `false` / `null`,
arrays `[1, 2, 3]`, objects `{a: 1, b: 2}`. Trailing commas are accepted.
Multi-line array/object literals are allowed **in state blocks only** (the
parser folds continuation lines while a bracket is open) - inside function
bodies a literal must stay on one line.
Objects support **derived fields** with `>>`:
```rat
person: {
weight: 80,
height: 1.80,
bmi >> weight / (height * height) # recomputes when inputs change
}
```
---
## 4. Interpolation, operators, expressions
- `[expr]` interpolates anywhere a value goes: text, attributes, strings.
` [name] is [age] years old, born in [2026 - age]`
- **`[...]` inside string literals is also evaluated.** `'hello [name]'` is
interpolation, not a literal. To keep literal bracket text, escape it:
`'pages/blog/\[slug].rat'`. Unescaped unknown names interpolate to empty
string silently - a classic source of vanishing text.
- `<<` is **assignment / write**: `count << count + 1`, and **return** in a
function body: `<< 'zero'`.
- **Compound assignment**: `+= -= *= /= %=` and postfix `++` / `--` are sugar
for `lhs << lhs OP rhs` - they work everywhere `<<` does (function bodies,
service methods, handlers) on any lvalue: `n += 1`, `total *= 2`, `i--`.
- **Index / member assignment**: `m[k] << v` and `m.field << v` write a map
key or object field; `xs[i] << v` and `grid[r][c] << v` write array
elements. Compound forms apply too: `m[k] += 1`, `xs[i]++`. A `+=` on a
missing key reads it as 0 (`m[k] += 1` on an absent `k` yields 1) - same
forgiveness as any null-in-arithmetic. `m[k]` is pure sugar for
`get`/`put`/`at`/`set`.
- **Multi-return + destructure**: `<< a, b, c` returns `[a, b, c]`; the call
site unpacks with `[x, y, z] << f()` (fresh locals, positional). A null
return binds every name null; short arrays null-fill, long ignore extras.
Top-level commas only - `<< f(a, b)` is a single call, not a pair.
- `>>` declares a **derived field** (`double >> count * 2` - recomputes
reactively) and is the **one-liner function body** marker.
- `++` increments: `on_click[count++]`.
- Comparison: `== != < <= > >=`. Null checks read as prose:
`(user is not null)`, `(x is null)`.
- Logic: `&` (and), `|` (or), parenthesize compound conditions:
`((a == b) & (i > 0))`.
- Arithmetic: `+ - * / %`. String concat is `+`:
`greeting >> 'Hello, ' + name + '!'`.
- Statement separator inside a handler bracket is `;`:
`on_click[items.add(draft); draft << '']`.
- Side-effect tap: `total << sum(xs) >> log('total: [total]')` - the `>>`
tail runs for effect without changing the assigned value.
---
## 5. Markup rules (where HTML intuition betrays you)
- Tags look like HTML but **prose must stay on the tag's own line**:
`
all the text here`. Indented text on the next line is parsed as
child *statements* - words become identifiers and you get a confusing
parse error pointing at the wrong line. There is no multi-line paragraph.
- **Attributes use `attr[value]`, not `attr="value"`.**
``. A bare `attr='x'` form does NOT do what
you expect (it becomes a boolean-ish flag). Attribute values:
- `class['btn']` - quoted literal.
- `href['/todo/[t]']` - interpolation inside the quoted literal.
- `value[hue]` - **the literal string "hue"**, NOT a state read. To bind
state into an attribute write `value['[hue]']`. Interpolated attribute
segments are reactive - they re-patch when the state changes.
- Kebab-case names work both ways: `data-copy['/x']` passes through
verbatim, and underscores lower to hyphens (`aria_label` →
`aria-label`). Any attribute name reaches the DOM.
- All attributes must be on the same line as the tag opener. No multi-line
tags.
- Tags auto-close at dedent; explicit closing tags are not written.
- Event handlers are attributes holding Rat statements:
`on_click[count++]`, `on_input[name << event.target.value]`,
`on_click[svcwrap(x)]`. Any `on_*` DOM event works.
- Inside prose, escape literal bracket text with `\[`: `write \[expr] here`.
- `#` after whitespace inside text is stripped as a comment - `
#[o.id]`
renders empty. Write `#` or rephrase.
- `*` in prose is markdown-style emphasis and will mangle lines containing
multiplication. Never put `x * y` in prose text; route it through a
derived field and interpolate that.
- `` pills must open and close on the same line.
---
## 6. Control flow: guards and loops
There is **no `if` / `else` keyword** and no `for` keyword.
```rat
# guard: (condition) then an indented action
(score >= 90)
excellent
(score >= 50)
passing
(else)
needs work
```
- Adjacent guards at the same indent form a **first-match chain** - only
the first true branch runs. `(else)` is the catch-all.
- A lone guard is fine (no else required).
- **Chaining is positional and blank lines do NOT break a chain.** The
chain ends at the first non-guard line. Two independent decisions
written as adjacent guard groups silently fuse into one chain - when
the first matches, the second never runs. Separate them with a real
statement: any tag in markup (a wrapper `
`, an `
`), any
statement in a fn body (e.g. `[count] << len(xs)`).
- **A matching branch that doesn't return still consumes the chain.**
In fn bodies a normalization guard (`(t == 'coral') t << 'a'`) placed
adjacent to an independent validation guard swallows it - the
validation never runs on the normalized path. Audit every chain for
"matches, doesn't return, next guard still needed"; break the chain
with a statement between the two questions.
- Loops bind with `[item] in collection` on its own line; the body indents:
```rat
[item] in items
(len(item) >= 5)
[item]
```
- Guards and loops nest freely in both directions. Empty collections render
nothing. Loop bodies are reactive (`[u.name]` inside a loop patches per
element), but **nested reactive loops are not supported** - keep loop
bodies flat.
- A loop is reactive when its collection is page-tier state: a flat name
(`[s] in items`) or a dotted path rooted at one (`[e] in board.entries`).
Both re-render on mutation (`items.add(x)`, `board.entries.add(x)`).
Server-tier collections render once server-side, flat or dotted.
- In functions, the same forms drive logic (see next section).
- **`break` / `continue`** work inside a loop in a function or handler body:
`break` stops the innermost loop, `continue` skips to the next iteration.
Use as a guard action (`(cond) break`) or on their own line. They unwind
only the nearest loop. (Page-template loops render every element - loop
control is for computational bodies, not markup.)
- **`while (cond)`** is the condition loop (re-checked each pass);
**`while [i] (cond)`** adds a free read-only pass counter `i` (0,1,2,…),
loop-scoped, exit still on `(cond)`. **`inf`** is a real +Inf value, so
**`[i] in inf`** is the infinite loop (exit via `break`/`<<`) and
`best << inf` seeds a min-search (`-inf` for max). Every count/while loop
is bounded by a hard iteration ceiling - a runaway loop errors loudly,
never hangs SSR.
---
## 7. Functions and lambdas
```rat
# multi-line: body indents, locals via [name] <<, return via <<
> add[a, b]
[sum] << a + b
<< sum
# one-liner: >> is the whole body
> double[x] >> x * 2
# first-match guard chain as function logic
> sign[n]
(n > 0) << 'positive'
(n < 0) << 'negative'
<< 'zero'
```
- Declared at top level with `> name[params]`. No imports, no registration -
callable from any page, pup, service, or handler.
- Missing args arrive as `null`. Both `[x] << ...` and a plain `x << ...`
for a NEW name are function-local - a helper's locals never leak into or
clobber the caller's. Writing a name that already exists as server/page
state still reaches that state (the local rule is for new names only).
- Lambdas for collection builtins: `filter([1,2,3,4], fn[n] >> n % 2 == 0)`.
- Loop + guard inside a body:
```rat
> first_admin[users]
[u] in users
(u.admin) << u
<< null
```
- **Do not** put `fn[...]` lambdas inside `> server` state initializers -
`sum(map(arr, fn[x] >> x.field))` in a server line does not evaluate.
Write a named helper function with an explicit loop instead.
- Dotted function names (`> channel.on_connect[...]`) are reserved for
framework hooks; don't invent your own dotted declarations.
---
## 8. Pups (components)
Any `.rat` file under `pups/` becomes a tag named after the file.
```rat
# pups/greeting.rat
> server
tagline: 'hello' # default, overridable per call site
> page
[tagline], friend.
```
```rat
# usage in any page
```
- Call-site attributes become the pup's variables.
- `` splices caller markup; the children evaluate in the
**caller's** scope.
- Pups can hold their own `> page` state + handlers; each instance is
independent.
- Don't name a pup prop `title` - it collides with the page-level `title`
server var. Use `heading` / `caption`.
---
## 9. Services
A service is a named singleton with state and methods, conventionally
declared in `main.rat`. It survives across requests (server frame).
```rat
> service [auth]
user: null
token: null
login[u, p]
user << u
token << 'tok-' + p
<< true
logout[]
user << null
is_authed[] >> token is not null
```
- Call from anywhere: `auth.login(name, pw)`, read state: `[auth.user]`.
Reads participate in the reactive graph.
- Methods inside the block do NOT take the `>` prefix; one-liners use `>>`.
- **Never name service methods** `add`, `push`, `pop`, `clear`, or `remove`
- the handler compiler rewrites `name.add(...)`-shaped calls as array
mutations and your service method will never be called.
- **Service-method calls don't work directly in event-handler attributes
in all cases.** `on_click[svc.method(x)]` works for simple cases, but the
reliable pattern is a top-level wrapper function:
```rat
> run_analyze[s]
text_demo.analyze(s)
# in the page
analyze
```
- Middleware: a service with a `match: '/admin/**'` line plus `before[req]`
/ `after[req]` methods gates requests (redirect/abort via magic names).
See https://rat-lang.com/documentation/advanced/middleware.
---
## 10. Data: models and queries
Models are declarative table definitions under `data/`:
```rat
> model [user]
id: text [pk]
email: text [unique]
name: text [length: 128]
pw: text [hashed]
created_at: date
```
- **Field lines need the colon**: `id: text [pk]`. Types: `text`, `number`,
`date`, `bool`. Attrs: `[pk]`, `[unique]`, `[index]`, `[length: N]`,
`[hashed]`, `[encrypted]`.
- Database connection lives in `settings.rat` (`> database` block, sqlite
default, `auto_migrate: true`). Migrations are SQL files under
`migrations/`.
The CRUD surface - exact and complete (there is no `list`/`get`/`delete`):
```rat
main_db.user.add({name: 'x'}) # insert, returns rowid
main_db.user.find({name: 'x'}) # rows matching where (array)
main_db.user.first({email: e}) # first match or null
main_db.user.all() # every row
main_db.user.update({id: 1}, {name: 'y'}) # returns affected count; set must be non-empty
main_db.user.remove({id: 1}) # delete matching, returns count
main_db.user.verify('pw', candidate, {email: e}) # bcrypt check on a [hashed] field
```
- `[hashed]` / `[encrypted]` transforms apply on write through `add`/`update`.
- `main_db.transaction(fn)` exists but **does not actually execute the
function body in the current version** - do not rely on it; loop inserts
serially instead.
- Guard single-row reads: `(user is not null)` before member access.
---
## 11. API files
`.rat` files under `api/` are JSON endpoints:
```rat
# api/user.rat -> /api/user
> server
user_cap: 1000 # bare GET /api/user serializes this block as JSON
> get_users[] [api_public: true]
<< main_db.user.all()
> add_user[name, email]
<< main_db.user.add({name: name, email: email})
```
- Functions become subpaths: `POST /api/user/get_users` (JSON array for
positional args, JSON object for a single arg).
- **Only `[api_public: true]` functions are reachable over HTTP**; private
ones return 403. In-process, the same functions are callable without HTTP
as `user.get_users()` from any page or service.
---
## 12. Styles
```rat
# co-located in any .rat file: > [selector]
> [.todo]
padding: 1rem 1.25rem
border-radius: 6px
> [.btn:hover]
background: var(--accent)
# global: styles/global.styles.rat - same blocks, applied to every page
> [body]
font-family: ui-sans-serif, system-ui
# media queries take the selector slot
> [@media (max-width: 600px)]
...
```
One declaration per line. Output is plain CSS bundled into the page - no
preprocessor, no scoping/hashing.
The bundle is **pruned per page** by default: rules whose class selectors
never appear on the rendered page are dropped before the `