Tutorial¶
This tutorial walks through building a crossroad traffic light controller step by step, introducing each language feature as it becomes needed.
Before we start writing specifications, let’s get familiar with the symbols Caelum uses. If you’ve never worked with formal logic notation, this section will get you up to speed.
Notation Primer¶
Caelum specifications use symbols from logic and set theory. Every symbol has an ASCII alternative you can type on a regular keyboard — you never need to type Unicode by hand.
Logical Connectives¶
These work like the boolean operators you already know from programming.
Conjunction ∧ (ASCII: /\ or keyword: and) — logical AND.
Both sides must be true.
In JavaScript:
// Caelum: x = 0 ∧ y = 1
// means:
if (x === 0 && y === 1) { /* ... */ }
Disjunction ∨ (ASCII: \/ or keyword: or) — logical OR.
At least one side must be true.
// Caelum: x = 0 ∨ y = 1
// means:
if (x === 0 || y === 1) { /* ... */ }
Negation ¬ (ASCII: ~ or keyword: not) — logical NOT.
Flips true to false and vice versa.
// Caelum: ¬ (x = 0)
// means:
if (!(x === 0)) { /* ... */ }
Implication → (ASCII: ->) — “if … then …”.
A → B means whenever A is true, B must also be true. It is only false
when A is true and B is false.
// Caelum: x = 0 → y = 1
// means:
if (x === 0) {
assert(y === 1);
}
Not equal ≠ (ASCII: !=) — same as != in most languages.
Less or equal ≤ (ASCII: <=), greater or equal ≥
(ASCII: >=) — same as in most languages.
Set Membership¶
Element of ∈ (ASCII: :).
In Caelum, let x ∈ 0..3 means “x is a variable whose value is
an element of the set {0, 1, 2, 3}.” You can read it as “x is in 0 to 3.”
The ASCII alternative is a plain colon: let x : 0..3.
Temporal Operators¶
These are specific to model checking — they describe properties that hold across time (sequences of states), not just a single state.
Always □ (ASCII: [] or keyword: always).
□ P means P is true in every reachable state. Think of it as
a loop that checks every state your system can ever reach:
// Caelum: □ (x >= 0)
// Conceptually:
for (const state of allReachableStates) {
assert(state.x >= 0);
}
Eventually ◇ (ASCII: <> or keyword: eventually).
◇ P means P becomes true at some point in the future. No matter
where you are, you can always reach a state where P holds:
// Caelum: ◇ (x = 0)
// Conceptually:
// Starting from any state, there exists a future state where x === 0
Always eventually □ ◇ is a common pattern. □ ◇ P means P
keeps happening forever — the system never permanently stops reaching P.
Next ◯ (ASCII: () or keyword: next).
◯ P means P is true in the next state (one transition step ahead).
Quick Reference¶
Meaning |
Unicode |
ASCII |
Keyword |
|---|---|---|---|
and |
|
|
|
or |
|
|
|
not |
|
|
|
implies |
|
|
|
always |
|
|
|
eventually |
|
|
|
next |
|
|
|
element of |
|
|
|
not equal |
|
|
|
less or equal |
|
|
|
greater or equal |
|
|
You can freely mix Unicode and ASCII in the same file. The formatter
(caelum fmt) can convert between styles.
Step 1: A Single Traffic Light¶
We are going to specify a crossroad traffic light system with two traffic
lights: traf1 and traf2. The full specification is available at
examples/crossroad_traffic_light.lum.
Let’s start simple with just one light that cycles through red, green, and yellow. Declare a named type so the colours are self-documenting:
type Color = enum { red, green, yellow }
let light ∈ Color
let timer ∈ 0..5
init {
light = red ∧ timer = 5
}
type Color = enum { ... } declares a named set of values.
Variables declared with let light ∈ Color (read: “light is in Color”)
can hold any value from that set.
The timer is an integer in the range 0 to 5 (inclusive).
The init block says the system starts with light = red and
timer = 5. The ∧ is just “and” — you could also write
light = red and timer = 5.
Step 2: Adding Transitions¶
Transitions describe how the system evolves. Each transition has a guard
(when it can fire) and an effect (what changes). Primed variables like
light' refer to the value in the next state:
transition tick {
timer > 0 ∧ timer' = timer - 1 ∧ light' = light
}
transition go_green {
light = red ∧ timer = 0 ∧ light' = green ∧ timer' = 5
}
transition go_yellow {
light = green ∧ timer = 0 ∧ light' = yellow ∧ timer' = 2
}
transition go_red {
light = yellow ∧ timer = 0 ∧ light' = red ∧ timer' = 5
}
Read timer > 0 ∧ timer' = timer - 1 as: “if timer is greater than 0,
then in the next state timer equals timer minus 1.” The ∧ chains
multiple conditions together (just like && in JavaScript).
The tick transition counts the timer down while keeping the light colour.
The other transitions fire when the timer expires, changing colour and
resetting the timer. Yellow gets a shorter timer (2 instead of 5).
Step 3: Safety Properties¶
Safety properties assert that something bad never happens.
The □ (“always”) operator means “in every reachable state”:
property timer_bounded {
□ (timer ≥ 0 ∧ timer ≤ 5)
}
Read this as: “it is always the case that timer is between 0 and 5.”
You could write it with ASCII as [] (timer >= 0 /\ timer <= 5)
or with keywords as always (timer >= 0 and timer <= 5).
The implication operator → (“if … then”) is useful for conditional
safety properties:
property yellow_is_short {
□ (light = yellow → timer ≤ 2)
}
Read this as: “it is always the case that if the light is yellow, then the timer is at most 2.” This ensures yellow phases are always short.
Step 4: Liveness Properties¶
Liveness properties assert that something good eventually happens.
The □ ◇ (“always eventually”) pattern means “this keeps happening
forever”:
property always_cycles {
□ ◇ (light = red)
}
Read this as: “it is always the case that the light eventually becomes red.” In other words, the light never gets permanently stuck — no matter what state you’re in, red will come around again.
Step 5: Two Lights at an Intersection¶
Now the payoff of named types: declare two variables with the same Color
type. Both traf1 and traf2 share the same enum variants (red,
green, yellow):
type Color = enum { red, green, yellow }
let traf1 ∈ Color
let traf2 ∈ Color
let timer ∈ 0..5
init {
traf1 = green ∧ traf2 = red ∧ timer = 5
}
Without the type keyword, you would need globally unique variant names
for each light (e.g. t1_red, t2_red), or fall back to integer encoding.
Named types keep the specification readable.
Add transitions that cycle between the two lights:
transition tick {
timer > 0 ∧ timer' = timer - 1 ∧ traf1' = traf1 ∧ traf2' = traf2
}
transition traf1_to_yellow {
traf1 = green ∧ timer = 0 ∧ traf1' = yellow ∧ traf2' = red ∧ timer' = 2
}
transition swap_to_traf2 {
traf1 = yellow ∧ timer = 0 ∧ traf1' = red ∧ traf2' = green ∧ timer' = 5
}
transition traf2_to_yellow {
traf2 = green ∧ timer = 0 ∧ traf2' = yellow ∧ traf1' = red ∧ timer' = 2
}
transition swap_to_traf1 {
traf2 = yellow ∧ timer = 0 ∧ traf2' = red ∧ traf1' = green ∧ timer' = 5
}
Step 6: Mutual Exclusion¶
The critical safety property: both lights must never be green at the same time.
property mutual_exclusion {
□ ¬ (traf1 = green ∧ traf2 = green)
}
Read this as: “it is always the case that it is not true that both traf1 and traf2 are green.” Or more naturally: “both lights are never green at the same time.”
property one_moving_at_most {
□ (traf1 ≠ red → traf2 = red)
}
one_moving_at_most is stronger: “if traf1 is not equal to red,
then traf2 must be red.” In other words, at most one light can be
non-red at any time.
Step 7: Fairness Properties¶
Fairness ensures both directions get a turn. The → combined with ◇
lets us say “if this happens, then that will eventually happen”:
property traf1_green_implies_traf2_next {
□ (traf1 = green → ◇ (traf2 = green))
}
property traf2_green_implies_traf1_next {
□ (traf2 = green → ◇ (traf1 = green))
}
Read the first one as: “it is always the case that if traf1 is green, then eventually traf2 will be green too.” This guarantees neither direction is starved.
Step 8: Invalid Properties¶
The invalid keyword marks properties that should not hold. Caelum
reports PASS when the property fails (as expected) and FAIL when it
unexpectedly holds:
invalid both_green {
◇ (traf1 = green ∧ traf2 = green)
}
This claims “eventually both lights are green simultaneously.” Since the system prevents that, the property fails — and Caelum reports PASS.
Running the Full Specification¶
Save the complete specification as crossroad.lum and run:
caelum crossroad.lum
You should see all properties pass. The full specification is available at
examples/crossroad_traffic_light.lum in the repository.
Summary of Language Features Used¶
Feature |
Syntax |
Purpose |
|---|---|---|
Named type |
|
Reusable domain shared by multiple variables |
Variable |
|
State variable with a finite domain |
Init block |
|
Constrains the initial state |
Transition |
|
Defines state evolution with primed variables |
Safety property |
|
Asserts an invariant over all reachable states |
Liveness property |
|
Asserts something keeps happening |
Invalid property |
|
Asserts a property does not hold |