Introduction
Romit is a declarative language for describing state, events, rules, and tests. Programs use the .rmt file extension and the romit command-line tool.
Romit’s surface syntax is brace-free: no { }, [ ], or ( ). Blocks are shown only by indentation (two spaces per level; tabs are rejected with P001).
on Run
rule say_it
when true
emit Greet with message "Hello, world!"
The compiler in this repository implements Stage 0–1: specs, checks, tests, and a small interpreter for beginner examples. UI app / screen syntax is documented under planned examples.
Install Romit
Pre-built binaries ship on GitHub Releases for Linux x86_64-unknown-linux-gnu and aarch64-unknown-linux-gnu, macOS x86_64-apple-darwin and aarch64-apple-darwin, and Windows x86_64-pc-windows-msvc. Linux builds use glibc dynamic linking on the official GNU triples.
One-line install
macOS and Linux
curl -fsSL https://romitlang.org/install.sh | sh
Windows
irm https://romitlang.org/install.ps1 | iex
Set ROMIT_MODIFY_PATH=1 before piping if you want the installer to append ~/.romit/bin to your shell config or Windows user PATH automatically.
macOS
Download the .tar.gz for your CPU:
| Hardware | Asset triple |
|---|---|
| Apple Silicon | aarch64-apple-darwin |
| Intel Mac | x86_64-apple-darwin |
export ROMIT_VERSION=0.1.0
curl -fsSL -O "https://github.com/romit07/romitlang/releases/download/v${ROMIT_VERSION}/romit-${ROMIT_VERSION}-aarch64-apple-darwin.tar.gz"
curl -fsSL -O "https://github.com/romit07/romitlang/releases/download/v${ROMIT_VERSION}/SHA256SUMS"
sha256sum -c SHA256SUMS --ignore-missing
tar -xzf "romit-${ROMIT_VERSION}-aarch64-apple-darwin.tar.gz"
cp romit-*/romit ~/.romit/bin/romit
chmod +x ~/.romit/bin/romit
PATH — bash / zsh — add to ~/.zshrc or ~/.bash_profile:
export PATH="$HOME/.romit/bin:$PATH"
PATH — fish — in ~/.config/fish/config.fish:
fish_add_path $HOME/.romit/bin
Linux
| Architecture | Asset triple |
|---|---|
| amd64 | x86_64-unknown-linux-gnu |
| arm64 | aarch64-unknown-linux-gnu |
export ROMIT_VERSION=0.1.0
curl -fsSL -O "https://github.com/romit07/romitlang/releases/download/v${ROMIT_VERSION}/romit-${ROMIT_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
curl -fsSL -O "https://github.com/romit07/romitlang/releases/download/v${ROMIT_VERSION}/SHA256SUMS"
sha256sum -c SHA256SUMS --ignore-missing
mkdir -p ~/.romit/bin
tar -xzf "romit-${ROMIT_VERSION}-x86_64-unknown-linux-gnu.tar.gz" -C /tmp
cp /tmp/romit-*/romit ~/.romit/bin/
chmod +x ~/.romit/bin/romit
Use the same PATH snippets as macOS.
Windows
Download romit-${ROMIT_VERSION}-x86_64-pc-windows-msvc.zip from Releases, verify against SHA256SUMS, and extract romit.exe to %USERPROFILE%\.romit\bin.
Add that folder to the User PATH in Settings → System → Environment variables, or:
$env:ROMIT_MODIFY_PATH = '1'
irm https://romitlang.org/install.ps1 | iex
Verify checksums
Every release includes SHA256SUMS. After downloading an archive and the sums file:
sha256sum -c SHA256SUMS --ignore-missing
On Windows PowerShell:
Get-FileHash .\romit-0.1.0-x86_64-pc-windows-msvc.zip -Algorithm SHA256
# Compare with the matching line in SHA256SUMS
Update
Re-run the one-line installer, or download the newer release asset and replace the binary in ~/.romit/bin.
Pin a version:
ROMIT_VERSION=0.1.0 curl -fsSL https://romitlang.org/install.sh | sh
Uninstall
curl -fsSL https://romitlang.org/install.sh | sh -s -- --uninstall
Remove any PATH lines you added manually. On Windows:
irm https://romitlang.org/install.ps1 | iex; install.ps1 -Uninstall
Or delete %USERPROFILE%\.romit\bin\romit.exe and remove the directory from user PATH.
Build from source
Requires a Rust toolchain:
git clone https://github.com/romit07/romitlang.git
cd romitlang
cargo build --release
./target/release/romit --version
Copy target/release/romit anywhere on your PATH.
Troubleshooting
| Problem | What to try |
|---|---|
curl: 404 on install | No GitHub Release yet, or set ROMIT_VERSION to a published tag. |
| Checksum mismatch | Re-download both the archive and SHA256SUMS; check for partial downloads. |
romit: command not found | Ensure ~/.romit/bin is on PATH and start a new shell. |
| Wrong Linux arch | Pick the GNU triple that matches uname -m. |
| Corporate proxy | Set HTTPS_PROXY for curl / Invoke-WebRequest. |
For local installer development, point at a directory served over HTTP:
ROMIT_BASE_URL=http://127.0.0.1:8765 ROMIT_VERSION=0.1.0-test sh install.sh
Getting started on Linux
After installing Romit:
romit --version
Create hello.rmt:
spec HelloWorld version 1
output Greet
message text
event Run
on Run
rule say_it
when true
emit Greet with message "Hello, world!"
test says_hello
send Run
expect emitted Greet with message "Hello, world!"
Check and test:
romit check hello.rmt
romit test hello.rmt
Try the bundled beginner examples:
git clone https://github.com/romit07/romitlang.git
cd romitlang
romit test examples/beginner/hello_world.rmt
Official Linux releases target glibc GNU triples. Very old distributions may need to build from source with a current Rust toolchain.
Getting started on macOS
After installing Romit:
romit --version
Create hello.rmt in a new folder:
spec HelloWorld version 1
output Greet
message text
event Run
on Run
rule say_it
when true
emit Greet with message "Hello, world!"
test says_hello
send Run
expect emitted Greet with message "Hello, world!"
Type-check it:
romit check hello.rmt
Run tests in the file:
romit test hello.rmt
The repository example is the same program:
romit test examples/beginner/hello_world.rmt
Use romit check --no-coverage <file> on large specs when you want to skip rule-coverage diagnostics.
Getting started on Windows
After installing Romit, open a new terminal so PATH updates apply:
romit --version
Create hello.rmt with your editor:
spec HelloWorld version 1
output Greet
message text
event Run
on Run
rule say_it
when true
emit Greet with message "Hello, world!"
test says_hello
send Run
expect emitted Greet with message "Hello, world!"
From PowerShell in that folder:
romit check hello.rmt
romit test hello.rmt
Clone the repo to run the same file from examples:
git clone https://github.com/romit07/romitlang.git
cd romitlang
romit test examples\beginner\hello_world.rmt
The CLI commands today are romit check and romit test — there is no separate romit run subcommand yet.
Hello world
spec HelloWorld version 1
output Greet
message text
event Run
on Run
rule say_it
when true
emit Greet with message "Hello, world!"
test says_hello
send Run
expect emitted Greet with message "Hello, world!"
Run:
romit check examples/beginner/hello_world.rmt
romit test examples/beginner/hello_world.rmt
Calculator tutorial
Open examples/beginner/calculator.rmt. The handler runs when Evaluate is sent and sets answer using words instead of symbols:
set answer to 3 times 4 plus 2
Run romit test examples/beginner/calculator.rmt.
Todo list tutorial
examples/beginner/todo_list.rmt keeps task_text and task_done, emits outputs on add and complete, and uses ordered send / expect steps inside one test.
Traffic light tutorial
examples/beginner/traffic_light.rmt combines a machine, multiple conditional on Tick handlers, and tests that send ticks between expectations.
UI framework
Planned — not implemented yet.
Stage 1 of theromitcompiler does not parse or compile UI specs. This page is a design draft only. Treatexamples/planned/*.rmtas syntax sketches for a future Stage 4 toolchain.
What we are aiming for
Romit’s UI layer is a declarative, component-based framework in the spirit of SwiftUI and React:
- You describe what the screen should look like and what happens on events.
- You do not manually wire DOM nodes or imperatively repaint the page.
- Reactive
statedrives the UI: when state changes, the visible screen updates automatically (same mental model as@Statein SwiftUI oruseStatein React).
The compiler pipeline we plan (later) is:
- Read the UI section of a
.rmtfile (views, layout, modifiers, events). - Emit static HTML for structure and accessibility.
- Attach a small reactive JavaScript runtime that re-renders only what changed when Romit state updates.
None of that compilation exists in this repository today.
Core ideas
view components and parameters
A view is a named piece of UI. Views can take parameters (like SwiftUI’s struct properties or React props) using the same brace-free phrase style as the rest of Romit:
view Badge label text small bool is false
text label
size 14
muted small
Parameters are declared on the header line; the indented block is the view body. Composition works by nesting views inside layout containers.
Reactive state
Screen-local or app-wide state is declared next to the screen (or in a dedicated state block). When a rule or handler runs set count to count plus 1, the UI layer should re-bind the text that reads count—no manual “refresh” call.
screen Counter at /counter
state
count int is 0
This parallels:
| Framework | Reactive state |
|---|---|
| SwiftUI | @State, @Observable |
| React | useState, useReducer |
| Romit (planned) | state fields + automatic view updates |
Layout containers
Nested layout is indentation-only, same as the declarative spec layer:
| Container | Role |
|---|---|
column | Vertical stack (like VStack / a flex column) |
row | Horizontal stack (like HStack / a flex row) |
list | Scrollable list of items |
column
text "Title" bold
row
button "Cancel" outline
button "OK"
Elements
Basic building blocks:
| Element | Purpose |
|---|---|
text | Labels and copy; can show a string or a state field |
button | Tap target with a label |
input | Text field bound to state (planned) |
Strings use "...". State references use plain names: text count shows the current value of count.
Modifiers
Modifiers are trailing words on a line (SwiftUI-style), not chained punctuation:
text "Hello" size 16 bold
text count color accent padding 12
Common modifiers in the draft: padding, color, size, bold, muted, outline, gap on containers.
You do not have to set every visual property by hand. Anything you leave out is filled in from the theme and defaults cascade (below). A plain button "Save" with no modifiers should still look like a proper button.
Events
User gestures use phrase events—no parentheses:
button "+"
on click increment
on click increment
set count to count plus 1
Navigation stays phrase-based too: on click go to /settings (see examples/planned/landing-dark.rmt).
App theme and built-in defaults
Romit UI is designed so beginners can skip styling until they care about branding.
An app block names the program’s UI shell:
home— whichscreenis the entry route when the app loadstheme— app-wide visual tokens and control stylesdefaults— default screen settings applied to every screen unless a screen overrides them
The theme holds colours, type, spacing, corner radius, and interactive states for controls (for example button hover, button pressed, button focus, button disabled). If you do not specify something on a text, button, or container, the runtime looks it up here.
Built-in defaults sit underneath the theme: readable contrast, minimum tap targets, and sensible fonts if the app does not define a theme at all.
Cascade (who wins?)
When the compiler resolves how something should look, it walks from most specific to least:
- Element override — modifiers on one line, e.g.
text "Hi" color red - Screen
defaults— thedefaultsblock inside thatscreen - App
defaults— thedefaultsblock directly underapp(shared by all screens) - App
theme— tokens and per-control theme sections (button,input, …) - Built-in defaults — what Romit ships when nothing else applies
Think of it like CSS inheritance with a design system: local tweak beats page layout beats design tokens beats the browser baseline.
app CounterDemo
home Counter
theme default
accent #7c3aed
button
background accent
text white
defaults
padding 20
background #f4f4f5
A button "+" with no colour or padding lines still gets the themed button background and label colour.
Screen defaults
Each screen can rely on app-level defaults, or override them with its own defaults block. Planned screen-level keys include:
| Key | Purpose |
|---|---|
background | Page background (often a theme token name like background) |
padding | Inner padding around the main content |
safe | Respect safe areas (top, bottom, or top bottom) |
text style | Default typography for body copy |
title style | Style for the screen title |
scroll | Scrolling behaviour (vertical, horizontal, none) |
align | Default alignment for the main column |
layout | Responsive hint (stack, split, fluid — draft) |
empty | Message when there is no content |
loading | Message while data is loading |
error | Message when something failed |
Only the keys you list need to appear; everything else falls through the cascade.
screen About at /about
defaults
padding 32
background white
column
text "About"
Here padding and background override the app defaults; button colours still come from theme.
Planned example: theme + two screens
See examples/planned/theme_cascade.rmt:
Counter—button "+"has no styling modifiers; it usesthemeaccent colours and interaction states.About— overridesdefaults(padding,background) only; buttons would still use the app theme.
Example: counter (Romit draft)
Full planned example: examples/planned/counter_ui.rmt.
note PLANNED — UI counter demo. Not parsed by Stage 1 romit.
app CounterDemo
home Counter
theme default
accent #2563eb
button
background accent
text white
defaults
padding 24
screen Counter at /
state
count int is 0
column
gap 16
text count
size 48
button "+"
on click increment
on click increment
set count to count plus 1
state:countstarts at0.text count: displays the live value (you may still usesizeon the label; the button does not need colours).button "+": nobackgroundorcolorlines—thethemesupplies them;on click incrementraises the event.- Handler: one line updates state; the UI runtime (future) redraws the text.
Same counter in SwiftUI and React
These snippets are the same app in today’s frameworks—useful when you learn Romit’s planned syntax.
SwiftUI
import SwiftUI
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack(spacing: 16) {
Text("\(count)")
.font(.largeTitle)
.bold()
Button("+") {
count += 1
}
}
.padding(24)
}
}
SwiftUI keeps count in @State and rebuilds body when it changes. Romit’s draft uses an explicit state block and rules instead of a closure on the button.
React
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return (
<div style={{ padding: 24, textAlign: "center" }}>
<p style={{ fontSize: 48, fontWeight: "bold" }}>{count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
React stores count in component state and re-renders on setCount. Romit’s planned model moves the increment into a declarative on click handler that sets spec state, closer to the event / rule style you already use in beginner .rmt files.
How compilation might work (future)
This is not built; it explains the target so tooling can align later.
flowchart LR
subgraph source [Romit source]
UI[app / screen / view]
Rules[on click / state rules]
end
subgraph out [Output]
HTML[HTML skeleton]
JS[Reactive JS bundle]
end
UI --> HTML
Rules --> JS
state[(state)] --> JS
JS --> HTML
- HTML: semantic tags from
column,row,text,button,input; modifiers become classes or inline data attributes. - JS: a tiny runtime subscribes to state keys (
count, etc.) and updates text nodes and attributes when handlers fire. - Shared rules: the same
romit test/ interpreter ideas from Stage 1 can eventually drive both server logic and UI events.
Where to go next
- Theme cascade + two screens:
examples/planned/theme_cascade.rmt - Landing page draft:
examples/planned/landing-dark.rmt - Shorter planned overview: Planned UI syntax
- Roadmap: Roadmap (Stage 4)
When the UI compiler lands, this page will move out of “planned” and gain a tutorial that runs romit build (or similar) on counter_ui.rmt. Until then, romit check will not accept UI-only files as valid Stage 1 programs.
Brace-free surface
Romit source must not use braces, brackets, or parentheses in the surface syntax. If the lexer sees (, ), [, ], {, }, or a stray :, it reports P013:
Romit has no braces. Use indentation instead.
Allowed non-word symbols in code are limited to:
- Operators
+ - * / < >and==/!=(or words such asis,is not,above,below) - String quotes
" - Hex colours
#RRGGBB - Comments
//ornote
Calls use space-separated words, for example emit Greet with message "Hi" or set answer to 3 times 4 plus 2.
Types and collections use words instead of punctuation: List of Number, items at 0, go to /signup.
Blocks and indentation
Each block is introduced by a header line, then one or more lines indented two spaces deeper.
spec MySpec version 1
state
count int is 0
on Tick
rule bump
when true
set count to count plus 1
Validator codes
| Code | Meaning |
|---|---|
| P001 | Tab character used for indentation |
| P002 | Odd number of spaces (use multiples of two) |
| P003 | Dedent past the outermost block |
| P004 | Indentation does not match any enclosing block |
| P020 | Parser expected an indented block |
Tabs are never allowed. Mixed dedent levels must match a previous indent level on the stack.
Calls and phrases
Romit expresses calls and control flow as space-separated words instead of punctuation-heavy syntax.
Calls and assignments
Emit events and pass fields with phrases:
emit Greet with message "Hi"
set answer to 3 times 4 plus 2
send Compute with n 10
Comparisons and arithmetic can use words (is, is not, times, plus) or the allowed symbols (+, -, *, /, ==, !=).
Event headers and parameters
Handlers name their event and parameters in the same phrase style:
on Compute with n if n is 10
on Add with title
on Tick if light is red
Types and access
Types read as phrases: List of Number, items at 0, task_text is not "".
Navigation and UI (planned)
Planned UI examples use navigation phrases such as go to /signup and on click go to … instead of arrows or braces. See UI framework and Planned UI syntax.
Strings and interpolation
String interpolation does not use { or }. Use adjacent words after a string literal (the value is concatenated in order):
note documented join form (v0.3):
say "Hello" name
equivalent to building the message from "Hello " and the value of name. The longer spelling is:
"Hello " join name
Only one of these forms is required in source; this book uses say "Hello" name in examples.
Declarative layer
Stage 1 specs use the same brace-free style:
| Keyword | Role |
|---|---|
spec | Name and version |
state / config | Fields with name type is default |
entity | entity Name keyed by id and indented fields |
event | event E with arg type |
output | Declared outputs with indented parameters |
on | Handlers: on E with args if condition |
rule | when / otherwise and set / emit / reject |
machine | from to to transitions |
invariant / limit / test | Properties and tests |
Tests interleave send and expect lines in order:
test adds_one
send Tick
expect count is 1
send Tick
expect count is 2
Beginner programs
These programs live in the repository under examples/beginner/. Run any file with:
romit test examples/beginner/<name>.rmt
hello_world.rmt
spec HelloWorld version 1
about "Say hello — the smallest Romit program."
output Greet
message text
event Run
on Run
rule say_it
when true
emit Greet with message "Hello, world!"
test says_hello
send Run
expect emitted Greet with message "Hello, world!"
calculator.rmt
See the Calculator tutorial.
spec Calculator version 1
about "Multiply and add in one line — no buttons required."
state
answer int is 0
event Evaluate
on Evaluate
rule sample
when true
set answer to 3 times 4 plus 2
test two_times_three_plus_two
send Evaluate
expect answer is 14
fibonacci.rmt
spec Fibonacci version 1
about "Small Fibonacci answers without recursion — Stage 2 adds real functions."
state
result int is 0
event Compute with n int
on Compute with n if n is 0
rule zero
when true
set result to 0
on Compute with n if n is 1
rule one
when true
set result to 1
on Compute with n if n is 10
rule ten
when true
set result to 55
test fib_of_10
send Compute with n 10
expect result is 55
test fib_of_0
send Compute with n 0
expect result is 0
test fib_of_1
send Compute with n 1
expect result is 1
todo_list.rmt
Walkthrough: Todo list tutorial.
spec TodoList version 1
about "One task at a time: add text, then mark it done."
state
task_text text is ""
task_done bool is false
event Add
event MarkDone
output TaskAdded
title text
output TaskCompleted
title text
on Add with title
rule create
when not task_done or task_text is ""
set task_text to title
set task_done to false
emit TaskAdded with title title
on MarkDone
rule finish
when task_text is not "" and not task_done
set task_done to true
emit TaskCompleted with title task_text
test add_then_done
send Add with title "Buy milk"
expect task_text is "Buy milk"
expect emitted TaskAdded with title "Buy milk"
send MarkDone
expect task_done is true
expect emitted TaskCompleted with title "Buy milk"
traffic_light.rmt
Walkthrough: Traffic light tutorial.
spec TrafficLight version 1
about "A traffic light that cycles red → green → yellow."
enum Color red green yellow
state
light Color is red
event Tick
output ShowLight
color text
machine light
red to green
green to yellow
yellow to red
on Tick if light is red
rule advance
when true
set light to green
emit ShowLight with color green
on Tick if light is green
rule go_yellow
when true
set light to yellow
emit ShowLight with color yellow
on Tick if light is yellow
rule go_red
when true
set light to red
emit ShowLight with color red
invariant only_known_colors
light is red or light is green or light is yellow
test red_to_green
send Tick
expect light is green
expect emitted ShowLight with color green
test full_cycle
given light is green
send Tick
expect light is yellow
send Tick
expect light is red
Advanced programs
| File | Topic |
|---|---|
refund_desk.rmt | Entity state machine and refunds |
garden_valve.rmt | Timers and config-driven behaviour |
leg_manager.rmt | Many entities and limits |
Advanced examples are converted to the brace-free syntax. Some tests still depend on entity runtime features that are being extended beyond the beginner interpreter.
Use romit check --no-coverage while iterating on large specs.
Planned UI syntax
See the full design draft: UI framework (planned, not implemented).
| File | What it shows |
|---|---|
counter_ui.rmt | Minimal counter; plain "+" button uses app theme |
theme_cascade.rmt | App theme, defaults, two screens (one overrides screen defaults) |
landing-dark.rmt | Marketing layout and navigation phrases |
examples/planned/landing-dark.rmt (excerpt— not parsed by the Stage 1 compiler):
app Landing
theme dark
background #0a0a0a
text #f5f5f5
screen Home at /
hero
text "Build faster with Acme" size 48 bold
row
button "Get started"
on click go to /signup
Navigation uses phrases such as go to /signup and on click go to … instead of arrows or braces.
Error codes
Parser (P0xx)
| Code | Summary |
|---|---|
| P001 | Tabs in indentation |
| P002 | Indent not a multiple of two spaces |
| P003 | Dedent beyond outermost block |
| P004 | Indent does not match a prior level |
| P013 | Braces, brackets, parens, or : |
| P020 | Expected indented block |
| P021 | Expected a word |
Type and rules (E1xx, E2xx)
| Code | Summary |
|---|---|
| E199 | Test expectation failed at runtime |
| E210 | Type or field error (checker) |
| E402 | Rule never exercised by tests (coverage) |
Warnings (W4xx)
| Code | Summary |
|---|---|
| W410 | Suspicious but legal construct |
Negative fixtures live under examples/negative/.
Roadmap
- Stage 1 (current) — Specs, lexer, parser, checker, tests, mdBook docs, release binaries and install scripts.
- Stage 2 — Functions, richer expressions, replay tooling.
- Stage 3 — Entities and machines at full fidelity in the interpreter.
- Stage 4 — UI
app/screen/viewlayer (UI framework,examples/planned/).
Distribution (planned)
- Homebrew tap — third-party or official
brew install romitlang/tap/romitonce binaries are published; not implemented yet. - npm wrapper — global
npm install -g @romitlang/clishim that downloads the matching release; not implemented yet.