Appearance
Signals
In most languages, you manage changing state with mutable variables and manual update logic. When the state grows, you spend time tracking who changed what, when, and why.
AIVI replaces mutable state with signals — reactive values in a dependency graph. A signal declares what it depends on, and the runtime handles the updates. You describe the relationships; the runtime does the work.
value → computed once, never changes
signal → recomputes when its dependencies changeThink of signals as cells in a spreadsheet. When you change one cell, every cell that references it recalculates automatically. You never manually propagate changes.
Declaring a signal
aivi
signal count = 21This declares a reactive value named count.
Deriving from another signal
Signals are often defined from earlier signals with pipes:
aivi
type Int -> Int
func double = n =>
n * 2
signal count = 21
signal doubledCount = count
|> doubleBoolean gating
Signals can branch just like ordinary values:
aivi
signal ready = True
signal statusText = ready
T|> "ready"
F|> "waiting"Filtering with ?|>
On signals, ?|> filters updates whose predicate fails while keeping the Signal A carrier:
aivi
type User = {
active: Bool,
email: Text
}
type Session = { user: User }
value seed : User = {
active: True,
email: "ada@example.com"
}
signal sessions : Session = {
user: seed
}
signal activeUsers : User = sessions
|> .user
?|> .activeFor ordinary non-signal values, the same operator returns Option A.
Reactive update clauses with when
You can also attach top-level reactive updates to an already declared signal.
The guarded form uses an ordinary boolean expression:
aivi
signal left = 20
signal right = 22
signal total = 0
signal ready = True
signal enabled = True
when ready => total <- left + right
when ready and enabled => total <-
result {
next <- Ok left
next + right
}There is also a source-pattern form for routing emissions from a named signal directly into another signal:
aivi
type Event = Tick | Turn Text
type Key =
| Key Text
signal event : Signal Event
signal tick : Signal Unit
signal keyDown : Signal Key
when tick _ => event <- Tick
when keyDown (Key "ArrowUp") => event <- Turn "up"You can also match a subject value directly and route each matching arm into an existing signal:
aivi
type Direction = Up | Down
type Event =
| Turn Direction
| Tick
signal event = Turn Down
signal heading = Up
signal tickSeen = False
when event
||> Turn dir => heading <- dir
||> Tick => tickSeen <- TrueThese forms mean:
- the guarded form uses an ordinary boolean expression
- the source-pattern form matches each emission from a previously declared signal name against one ordinary pattern
- the pattern-armed form matches each
||>arm against the subject expression - pattern binders introduced by the source-pattern form are only in scope for that body
- any binders introduced by an arm, like
dir, are only in scope for that arm body - the target must be a previously declared signal
- the source-pattern form also requires its source name to refer to a previously declared signal
- the right-hand side is an ordinary expression with direct signal references
- unlike a pipe, there is no ambient subject value inside the body
- if a guarded clause is false when it fires, the target keeps its previous committed value
- if a source-pattern clause does not match, the target keeps its previous committed value
- if multiple
whenclauses write the same signal in one tick, later clauses win by source order
Guards like status.done are fine too, but only when ordinary expression typing already proves that member access is a Bool.
Use when when you want event-shaped reactive commits into an existing signal. Use pipes when you want to transform the current subject flowing through one expression spine.
This surface now executes end to end in the linked runtime: guards and bodies are lowered as ordinary expressions, false guards keep the previous committed value, and later matching clauses still win by source order within a tick.
Reactive update self-reference rules are unchanged. A target signal still cannot read itself from its own when guard or body.
Previous and diff
The language has dedicated pipes for time-oriented signal transformations:
aivi
signal score = 10
signal previousScore = score
~|> 0
signal scoreDelta = score
-|> 0Shaping signal outputs
Signals can still produce richer values without leaving the ordinary expression model:
aivi
type NamePair = {
first: Text,
last: Text
}
signal firstName = "Ada"
signal lastName = "Lovelace"
signal namePair = {
first: firstName,
last: lastName
}Signals versus values
| Form | Meaning |
|---|---|
value answer = 42 | Fixed expression |
signal count = 21 | Reactive graph node |
Use value when something does not participate in reactive recomputation. Use signal when it should.