RatMinimal web framework

Guards and loops

Rat has no if/else keyword. Conditions are written as parenthesized guards followed by an action: (cond) action. Adjacent guards form a first-match chain. (else) closes one. Loops use [item] in coll on the body line.

One guard, one action

No else needed when not used

A lone guard runs its action only when the condition is true and falls through otherwise. Use it for inline gates: render an admin link only when authed, show an error only when one exists.

> server
score: 78

> page
<p> score: [score]
(score >= 50)
    <p class['pass']> passing
Result

score: 78

passing

First-match chain

Sibling guards short-circuit at the first hit

Stacked guards at the same indent form a chain; only the first matching branch runs. Use (else) as the final catch-all when one is required.

> page
(score >= 90)
    <p> excellent
(score >= 50)
    <p> passing
(else)
    <p> needs work

Two independent decisions

A statement between guards starts a new chain

Chaining is positional: a chain ends at the first non-guard line, and a blank line is NOT a separator, so two guard groups with only whitespace between them are still one chain. When two conditions are independent questions rather than branches of one decision, put a real statement between them. In markup any tag works; here the <hr> keeps the score ladder and the lucky-number check separate. Without it, all four branches fuse into one chain and a passing score would silently skip the second question.

> page
(score >= 50)
    <p> passing
(else)
    <p> needs work
<hr>
(score == 78)
    <p> the lucky demo number
Result

passing


the lucky demo number

Chains in function bodies

A matching branch that does not return still consumes the chain

The same first-match rule drives function logic, and it is where the chain can surprise you: when an early branch matches and merely mutates without returning, the rest of the chain is still skipped. Below, the team-normalization pair and the capacity check are independent questions. The [count] line between them starts a new chain; written adjacent, a coral join would match the first branch and the capacity check would never run. As in markup, a blank line alone does not break the chain.

> join[team, seats]
    (team == 'coral')
        team << 'a'
    (team == 'teal')
        team << 'b'
    [count] << len(seats)
    (count >= 8)
        << 'room is full'
    << 'joined crew ' + team

Loop with in coll

Iterate over arrays

Place the binder on the body line just like a tag. The framework rebinds the loop variable on each pass, so any handlers attached to the elements capture the right value. Empty collections render nothing.

Loops over page-tier state are reactive: a flat name ([item] in items) and a dotted path rooted at one ([e] in board.entries) both re-render their rows in place when a handler mutates the collection, with no server round-trip. Loops over server-tier collections render once on the server, dotted or not.

> server
items: ['apples', 'pears', 'figs']

> page
<ul>
    [item] in items
        <li> [item]
Result
  • apples
  • pears
  • figs

Guards inside a loop

Compose freely

Guards and loops nest in either direction. Filter the rendered output inline rather than precomputing a separate array.

> page
<ul>
    [item] in items
        (len(item) >= 5)
            <li> [item]
Result
  • apples
  • pears

break / continue

Loop control inside function and handler bodies

In a function or handler body, break stops the innermost loop and continue skips to the next iteration. Use them as a guard action ((cond) break) or on their own line. They unwind only the nearest loop, so an inner break leaves an outer loop running. They also work in a static markup loop ([x] in coll over a server-tier or local collection), stopping or skipping the emitted elements. A page-tier (reactive) loop re-renders its whole collection on the client, so loop control there is left to the SSR pass.

> first_big[xs, limit]
  [i] in len(xs)
    (xs.at(i) <= limit) continue
    << xs.at(i)
  << -1

> count_until_zero[xs]
  n << 0
  [i] in len(xs)
    (xs.at(i) == 0) break
    n << n + 1
  << n

while loops

Condition-driven, with an optional pass counter

When you don't know the trip count up front - a convergence loop, a queue that drains, a retry - use while (cond). The condition reads as prose, like a guard, and is re-checked each pass. The gcd below also shows a destructuring swap: [a, b] << [b, a % b] updates both names at once. Add an optional bracket counter, while (cond), and i is a free, read-only pass index (0, 1, 2, …) usable in the body for an indexing or "give up after N" guard; the loop still exits on the condition, never on i. Every loop is bounded by a hard iteration ceiling, so a runaway while (true) fails loudly instead of hanging.

> gcd[a, b]
  while (b > 0)
    [a, b] << [b, a % b]
  << a

> collatz_steps[n]
  x << n
  steps << 0
  while (x > 1)
    (x % 2 == 0) x << x / 2
    (x % 2 == 1) x << 3 * x + 1
    steps << steps + 1
  << steps

Infinite loops and ranges

in inf, and the a..b shorthand

inf is a real value (positive infinity), so in inf is the infinite loop - iterate i = 0, 1, 2, … and leave with break or a return. inf also seeds a min-search (best << inf, since every value is below it); -inf does the max. For a plain numeric span, a..b is shorthand for range(a, b) - in 2..20.

> first_factor[n]
  [d] in inf
    (d < 2) continue
    (n % d == 0) << d
  << n

> smallest[xs]
  best << inf
  [x] in xs
    (x < best) best << x
  << best

See also Functions · Reactivity · Collections