Static copy
Copied from docs/en/manual/control-flow.md in https://github.com/poppy-team/aipo-lang (MIT). Pinned to revision ce47c4e070404fc41722ccd8e2b7b808b99062a8, blob 05bd3c867c925cfcd0d1d673fb893d92ffd91c30. The source repository remains canonical; this copy is refreshed through a sync pull request, not live.
Aipo offers clean, deterministic, and highly readable control flow constructs: blocks enclosed in curly braces { ... } without redundant parentheses around conditions, combined with a transactional error handling model with automatic atomic rollback.
if ... elif ... else Blocks Conditions require no parentheses, and blocks open and close with braces { ... }. The syntax provides immediate visual clarity, with native support for rainbow brackets and automatic code folding in any IDE:
var score = 85
var status = ""
if score >= 90 {
status = "Excellent"
} elif score >= 70 {
status = "Passing"
} else {
status = "Remedial"
}
io.println(status) # "Passing"if condition then a else b Aipo also supports single-line ternary conditional value expressions using then:
let active = true
let message = if active then "Online" else "Offline"
io.println(message) # "Online"match ... when) The match construct branches execution by comparing an expression against literals, struct destructuring patterns, and enum variants:
Each branch begins with when (never case). It supports optional boolean guards using if <condition>:
match user {
when { name, age } if age >= 65 {
io.println(f"Senior: {name}")
}
when { name, age } if age >= 18 {
io.println(f"Adult: {name}")
}
else {
io.println("Minor")
}
}When matching over an enum, the compiler strictly enforces exhaustive checking. If no else branch is supplied, all variants must be explicitly covered:
match state {
when ConnectionState.Disconnected {
io.println("Disconnected")
}
when ConnectionState.Connecting(a) {
io.println(f"Connecting attempt #{a}")
}
when ConnectionState.Connected { ip, ping_ms } {
io.println(f"Connected to {ip}")
}
when ConnectionState.Error(r) {
io.println(f"Error: {r}")
}
}All loop constructs in Aipo use delimited { ... } blocks and require no parentheses or connecting words like do.
while Executes the loop body as long as the boolean condition is true:
var i = 0
while i < 3 {
io.println(f"Step: {i}")
i += 1
}loop Canonical infinite loop, designed for repetitions that terminate via explicit break:
var attempts = 0
loop {
attempts += 1
if attempts >= 3 {
break
}
}
io.println(f"Total attempts: {attempts}")repeat Repeats the block a fixed number of times with an optional iteration counter (repeat count as index):
# Executes 3 times (with indices 0, 1, and 2)
repeat 3 as idx {
io.println(f"Iteration number: {idx}")
}each Canonical iteration over collections (List, Dict, Set, Sequence):
# Simple iteration over a list
let fruits = ["Apple", "Banana", "Orange"]
each fruit in fruits {
io.println(fruit)
}
# Iteration with index and element
each idx, fruit in fruits {
io.println(f"{idx}: {fruit}")
}
# Iteration over dictionary (key and value in insertion order)
let config = {
"host": "127.0.0.1",
"port": 5432,
}
each key, val in config {
io.println(f"{key} => {val}")
}attempt ... failed) In Aipo, errors are neither uncontrolled stack-unwinding exceptions nor easily ignored status codes. Failures are raised explicitly with fail (or returned with return fail(...)) and handled in transactional blocks with journaling and automatic atomic rollback:
struct Vault {
var balance = 0.0
}
impl Vault {
fn init(initial_balance = 0.0) {
self.balance = initial_balance
}
invariant {
self.balance >= 0.0
}
}
let v = Vault{ balance: 100.0 }
attempt {
# This mutation temporarily drops balance to -100.0
v.balance -= 200.0
} failed err {
# Violating the invariant triggers automatic rollback restoring self.balance to 100.0!
io.println(f"Caught failure: {err.message}")
}
# The balance remains untouched at its pre-attempt state!
io.println(f"Preserved balance: {v.balance}") # 100.0If any operation inside an attempt block executes fail or violates a structural invariant, all mutations to journaled objects are atomically reverted to their original state.
or_else For expressions where you simply want to provide a fallback recovery value without the boilerplate of an attempt block, use the canonical or_else operator:
fn read_file(path) {
# If the file does not exist or fails, return fail
return fail("file not found")
}
# If read_file raises fail, or_else evaluates and yields the fallback alternative:
let content = read_file("config.toml") or_else "host = 127.0.0.1"
io.println(content) # "host = 127.0.0.1"