A progressive guide to the FORMA programming language.
FORMA is an AI-optimized systems programming language that delivers memory safety without lifetimes, concise syntax that costs fewer tokens, and grammar-constrained generation for correct-by-construction code.
- Getting Started
- Basics
- Variables & Mutability
- Types
- Control Flow
- Functions
- Data Types in Depth
- Structs
- Enums
- Pattern Matching
- Error Handling
- Closures & Higher-Order Functions
- Generics
- Traits & Implementations
- References
- Contracts
- Modules & Imports
- Async & Concurrency
- Standard Library Overview
- Tooling
- For AI Developers
git clone https://github.com/sfw/forma.git
cd forma
cargo build --releaseThe compiler binary is at ./target/release/forma.
Create hello.forma:
f main()
print("Hello, World!")
Run it:
forma run hello.formaEvery FORMA program starts at main. By default, main returns nothing (unit). If you need an explicit exit code, you can declare f main() -> Int and return an integer (e.g., 0 for success).
Why short keywords? Every token costs money in LLM API calls. f instead of fn or function, s instead of struct, m instead of match. FORMA reduces token usage by ~38% compared to Rust.
Why indentation-based? Braces are a leading source of syntax errors in AI-generated code. Indentation is unambiguous and reduces the grammar's complexity.
Why no lifetimes? 94.8% of AI failures targeting Rust are compilation errors, with dependency resolution and type complexity as the top causes. Lifetimes add further friction. FORMA uses second-class references that eliminate lifetime annotations entirely while preserving memory safety.
# This is a line comment
# FORMA uses # for comments (like Python)
f main()
print("Hello!") # Print any value
print(42) # Integers
print(3.14) # Floats
print(true) # Booleans
eprintln("error message") # Print to stderr
FORMA supports f-strings for embedding expressions in strings:
f main()
name := "World"
age := 25
print(f"Hello, {name}!")
print(f"{name} is {age} years old")
print(f"2 + 3 = {2 + 3}")
Variables are declared with := (walrus operator). All variables are mutable by default:
f main()
x := 42 # Type inferred as Int
name := "Alice" # Type inferred as Str
pi := 3.14159 # Type inferred as Float
# Reassignment uses :=
x := x + 1
print(x) # 43
You can add explicit type annotations:
f main()
x: Int = 42
name: Str = "Alice"
| Type | Description | Example |
|---|---|---|
Int |
Integer (default) | 42, -7 |
Float |
Float (default) | 3.14, -0.5 |
Bool |
Boolean | true, false |
Char |
Unicode character | 'a', 'Z' |
Str |
String | "hello" |
() |
Unit (void) | () |
| Signed | Unsigned |
|---|---|
i8, i16, i32, i64, i128 |
u8, u16, u32, u64, u128 |
isize |
usize |
| Type | Syntax | Example |
|---|---|---|
| List | [T] |
[1, 2, 3] |
| Array (fixed) | [T; N] |
[0; 10] |
| Map | {K: V} |
{"a": 1, "b": 2} |
| Set | {T} |
— |
| Tuple | (T, U) |
(1, "hello") |
| Type | Syntax | Meaning |
|---|---|---|
| Option | T? |
Value or nothing |
| Result | T!E |
Value or error |
| Function | (A, B) -> C |
Function type |
| Never | ! |
Never returns |
if expressions always produce a value. The then keyword is required for single-expression bodies:
# Single-expression form
result := if x > 0 then "positive" else "non-positive"
# Block form
if x > 0
print("positive")
else if x == 0
print("zero")
else
print("negative")
i := 0
wh i < 10
print(i)
i := i + 1
for item in [1, 2, 3, 4, 5]
print(item)
lp
line := read_line()
if line == "quit" then br
print(f"You said: {line}")
| Keyword | Long Form | Meaning |
|---|---|---|
br |
break |
Break out of loop |
ct |
continue |
Skip to next iteration |
ret |
return |
Return from function |
'outer: for i in [1, 2, 3]
for j in [10, 20, 30]
if i * j > 40 then br 'outer
print(f"{i} * {j} = {i * j}")
Functions are declared with f. The last expression is the return value:
f add(a: Int, b: Int) -> Int
a + b
f greet(name: Str) -> Str
f"Hello, {name}!"
For one-liners, use =:
f double(x: Int) -> Int = x * 2
f is_even(n: Int) -> Bool = n % 2 == 0
Functions that don't return a meaningful value use Unit (or omit the return type):
f say_hello(name: Str) -> Unit
print(f"Hello, {name}!")
Use ret (or return) to return early:
f is_prime(n: Int) -> Bool
if n < 2 then return false
if n == 2 then return true
if n % 2 == 0 then return false
i := 3
wh i * i <= n
if n % i == 0 then return false
i := i + 2
true
Lists are dynamically-sized sequences:
f main()
numbers := [1, 2, 3, 4, 5]
print(len(numbers)) # 5
print(numbers[0]) # 1 (zero-indexed)
# Mutation
numbers[0] := 10
vec_push(numbers, 6)
print(numbers) # [10, 2, 3, 4, 5, 6]
Tuples group values of different types. Access fields with .0, .1, etc.:
f divide(a: Int, b: Int) -> (Int, Int)
quotient := a / b
remainder := a % b
(quotient, remainder)
f main()
result := divide(17, 5)
print(f"{result.0} remainder {result.1}") # 3 remainder 2
Maps are key-value collections created with map_new():
f main()
scores := map_new()
map_insert(scores, "Alice", 95)
map_insert(scores, "Bob", 87)
print(map_keys(scores))
Strings support many builtin operations:
f main()
s := "Hello, World!"
print(str_len(s)) # 13
print(str_contains(s, "World")) # true
print(str_slice(s, 0, 5)) # "Hello"
parts := str_split("a,b,c", ",")
print(parts) # ["a", "b", "c"]
Structs are declared with s:
s Point
x: Float
y: Float
f main()
p := Point { x: 3.0, y: 4.0 }
print(f"({p.x}, {p.y})")
s Color(u8, u8, u8)
f main()
red := Color(255, 0, 0)
print(red.0) # 255
Add methods to structs with i (impl):
s Point
x: Float
y: Float
i Point
f distance(&self) -> Float
sqrt(self.x * self.x + self.y * self.y)
f translate(&self, dx: Float, dy: Float) -> Point
Point { x: self.x + dx, y: self.y + dy }
f main()
p := Point { x: 3.0, y: 4.0 }
print(p.distance()) # 5.0
s Pair[T]
first: T
second: T
f main()
pair := Pair { first: 1, second: 2 }
print(pair.first)
Enums define types with multiple variants:
e Direction
North
South
East
West
e Shape
Circle(Float)
Rectangle(Float, Float)
Triangle(Float, Float, Float)
For simple enums, use the pipe-separated syntax:
e Color = Red | Green | Blue
Enum variants with data are primarily used through pattern matching. The built-in constructors Some(), Ok(), and Err() can be called directly:
f main()
opt := Some(42)
m opt
Some(v) -> print(f"Got: {v}")
None -> print("Nothing")
result := Ok("success")
m result
Ok(msg) -> print(msg)
Err(e) -> print(f"Error: {e}")
User-defined enums with unit variants are constructed by name:
e Direction
North
South
East
West
f main()
# Unit variants are referenced by name
dir := North
Pattern matching with m is exhaustive — the compiler ensures all cases are covered:
f describe(n: Int) -> Str
m n
0 -> "zero"
1 -> "one"
_ -> "many"
f point_info(p: (Int, Int)) -> Str
m p
(0, 0) -> "origin"
(x, 0) -> f"on x-axis at {x}"
(0, y) -> f"on y-axis at {y}"
(x, y) -> f"at ({x}, {y})"
Add conditions to match arms with if:
f classify(n: Int) -> Str
m n
n if n < 0 -> "negative"
0 -> "zero"
n if n <= 10 -> "small positive"
_ -> "large positive"
f handle(result: Int!Str) -> Int
m result
Ok(value) -> value
Err(msg) ->
print(f"Error: {msg}")
0
f main()
print(handle(Ok(42))) # 42
print(handle(Err("no"))) # 0
s Point
x: Int
y: Int
f describe_point(p: Point) -> Str
m p
Point { x: 0, y: 0 } -> "origin"
Point { x, y } -> f"({x}, {y})"
FORMA uses Result and Option types instead of exceptions. No try/catch — errors are values.
T? is shorthand for Option[T]. A value is either Some(value) or None:
f find_index(arr: [Int], target: Int) -> Int?
i := 0
wh i < len(arr)
if arr[i] == target then return Some(i)
i := i + 1
None
T!E is shorthand for Result[T, E]. A value is either Ok(value) or Err(error):
f parse_positive(s: Str) -> Int!Str
n := str_to_int(s)
m n
Some(v) if v > 0 -> Ok(v)
Some(_) -> Err("not positive")
None -> Err("not a number")
The ? operator propagates errors — if the value is Err, return it immediately:
f read_config(path: Str) -> Str!Str
content := file_read(path)? # Returns Err if file_read fails
Ok(content)
The ! operator unwraps a Result, panicking on error:
f main()
db := db_open("app.db")! # Panic if database can't be opened
print("Database opened")
The ?? operator provides a default value for None/Err:
f main()
name := env_get("USER") ?? "unknown"
print(f"Hello, {name}")
| Function | Signature | Description |
|---|---|---|
is_some |
Option[T] -> Bool |
true if Some |
is_none |
Option[T] -> Bool |
true if None |
is_ok |
Result[T, E] -> Bool |
true if Ok |
is_err |
Result[T, E] -> Bool |
true if Err |
unwrap |
Option[T] -> T |
Extract value, panic on None |
unwrap_or |
(Option[T], T) -> T |
Extract value, or return default on None |
expect |
(Option[T], Str) -> T |
Extract value, panic with message on None |
map_opt |
(Option[T], (T) -> U) -> Option[U] |
Apply function to Some value |
flatten |
(Option[Option[T]]) -> Option[T] |
Flatten nested Option |
and_then |
(Option[T], (T) -> Option[U]) -> Option[U] |
Chain Option-returning function |
# Check status
is_some(Some(42)) # true
is_none(None) # true
is_ok(Ok(42)) # true
is_err(Err("bad")) # true
# Unwrapping (Option only)
unwrap(Some(42)) # 42
unwrap_or(Some(42), 0) # 42
unwrap_or(None, 0) # 0
expect(Some(42), "missing") # 42
# Option chaining
map_opt(Some(5), |x: Int| x * 2) # Some(10)
map_opt(None, |x: Int| x * 2) # None
flatten(Some(Some(42))) # Some(42)
flatten(Some(None)) # None
and_then(Some(5), |x: Int| if x > 0 then Some(x * 10) else None) # Some(50)
and_then(None, |x: Int| Some(x * 10)) # None
Use m (match) to handle both variants with value binding:
f describe(opt: Int?) -> Str
m opt
Some(v) -> f"got {v}"
None -> "nothing"
f handle(res: Int!Str) -> Str
m res
Ok(v) -> f"success: {v}"
Err(e) -> f"error: {e}"
Match guards add conditions after the pattern:
f classify(opt: Int?) -> Str
m opt
Some(n) if n > 0 -> "positive"
Some(n) if n < 0 -> "negative"
Some(_) -> "zero"
None -> "absent"
Safe division:
f safe_div(a: Int, b: Int) -> Int?
if b == 0 then None
else Some(a / b)
f main()
result := safe_div(10, 0) ?? 0 # 0
Chaining ? for error propagation:
f parse_positive(s: Str) -> Int!Str
m str_to_int(s)
Some(n) if n > 0 -> Ok(n)
Some(_) -> Err("not positive")
None -> Err("not a number")
f add_parsed(a: Str, b: Str) -> Int!Str
x := parse_positive(a)? # Returns Err early if parsing fails
y := parse_positive(b)?
Ok(x + y)
unwrap_or vs ??:
Both provide a default value for None. Use ?? for concise inline expressions:
f main()
# These are equivalent for Option values:
a := unwrap_or(str_to_int("abc"), 0)
b := str_to_int("abc") ?? 0
Functions that return Option:
# str_to_int: Str -> Int?
m str_to_int("42")
Some(n) -> print(n) # 42
None -> print("failed")
# vec_get: ([T], Int) -> T?
arr := [10, 20, 30]
m vec_get(arr, 1)
Some(v) -> print(v) # 20
None -> print("out of bounds")
Closures are anonymous functions enclosed in |...|. Parameters require type annotations:
f main()
add := |a: Int, b: Int| a + b
print(add(3, 4)) # 7
square := |x: Int| -> Int x * x
print(square(5)) # 25
f apply(x: Int, func: (Int) -> Int) -> Int
func(x)
f main()
result := apply(5, |x: Int| x * 2)
print(result) # 10
Functions and types can be parameterized with type variables using [T]:
f identity[T](x: T) -> T
x
f first[T](list: [T]) -> T?
if len(list) > 0 then Some(list[0])
else None
s Stack[T]
items: [T]
size: Int
Note: FORMA uses [T] for generics (not <T> like Rust or Java). This avoids ambiguity with comparison operators, making AI generation more reliable.
Traits define shared behavior:
t Display
f display(&self) -> Str
t Area
f area(&self) -> Float
Implement traits with i TraitName for Type:
s Circle
radius: Float
i Area for Circle
f area(&self) -> Float
3.14159 * self.radius * self.radius
i Display for Circle
f display(&self) -> Str
f"Circle(r={self.radius})"
Use where clauses to constrain generic types:
f print_area[T](shape: T) -> Unit where T: Area + Display
print(f"{shape.display()}: area = {shape.area()}")
FORMA uses second-class references — references exist only as function parameters, never in structs or return types. This eliminates lifetime annotations entirely.
Use ref for shared (read-only) access, ref mut for mutable access:
f swap(ref mut arr: [Int], i: Int, j: Int) -> Unit
temp := arr[i]
arr[i] := arr[j]
arr[j] := temp
f sort(ref mut arr: [Int]) -> Unit
n := len(arr)
if n > 1 then
quicksort(ref mut arr, 0, n - 1)
Pass arguments with the matching ref or ref mut:
f main()
data := [3, 1, 2]
swap(ref mut data, 0, 2)
print(data) # [2, 1, 3]
Like Rust, FORMA enforces borrow safety:
- Multiple
refborrows allowed simultaneously - Only one
ref mutborrow at a time - Cannot have
refandref mutborrows simultaneously
The difference: since references are second-class, these rules are trivially checkable without lifetime analysis.
Design-by-contract with @pre and @post annotations:
@pre(n >= 0, "n must be non-negative")
@post(result >= 1, "factorial is always positive")
f factorial(n: Int) -> Int
if n <= 1 then 1
else n * factorial(n - 1)
Contracts are checked at runtime by default:
forma run myfile.formaDisable runtime contract checks explicitly with:
forma run --no-check-contracts myfile.formaContracts support rich expression syntax:
old(expr)in@post— captures the value ofexprbefore the function body executesforall x in xs: predicate— universal quantifier over a collection or rangeexists x in xs: predicate— existential quantifierx in collection— membership testA => B— implication (if A then B)result— refers to the return value in@postconditions
FORMA provides 35 named patterns that expand to contract expressions:
| Pattern | Context | Meaning |
|---|---|---|
@positive(x) |
Both | x > 0 |
@nonnegative(x) |
Both | x >= 0 |
@nonzero(x) |
Both | x != 0 |
@even(x) |
Both | x % 2 == 0 |
@odd(x) |
Both | x % 2 != 0 |
@divisible(x, n) |
Both | x % n == 0 |
@bounded(x, lo, hi) |
Both | lo <= x <= hi (inclusive) |
@in_range(x, lo, hi) |
Both | lo < x < hi (exclusive) |
| Pattern | Context | Meaning |
|---|---|---|
@nonempty(x) |
Pre-only | x.len() > 0 |
@contains(arr, elem) |
Both | elem is in arr |
@all_positive(arr) |
Both | all elements > 0 |
@all_nonnegative(arr) |
Both | all elements >= 0 |
@all_nonzero(arr) |
Both | all elements != 0 |
@valid_index(arr, i) |
Both | 0 <= i < arr.len() |
@valid_range(arr, lo, hi) |
Both | valid slice bounds |
| Pattern | Context | Meaning |
|---|---|---|
@subset(a, b) |
Both | all of a in b |
@superset(a, b) |
Both | all of b in a |
@disjoint(a, b) |
Both | no overlap between a and b |
@equals(a, b) |
Both | same elements (set equality) |
@same_length(a, b) |
Both | a.len() == b.len() |
@permutation(a, b) |
Both | same elements with same multiplicities |
| Pattern | Context | Meaning |
|---|---|---|
@prefix(a, b) |
Both | a starts b |
@suffix(a, b) |
Both | a ends b |
@reversed(a, b) |
Both | a is reverse of b |
@rotated(a, b, k) |
Both | a is b rotated by k (k is normalized via modular arithmetic; empty arrays are trivially rotated) |
@unique(x) |
Both | no duplicate elements |
| Pattern | Context | Meaning |
|---|---|---|
@sorted(x) |
Both | ascending order (allows duplicates) |
@sorted_desc(x) |
Both | descending order (allows duplicates) |
@strictly_sorted(x) |
Both | ascending, no duplicates |
@strictly_sorted_desc(x) |
Both | descending, no duplicates |
@sorted_by(arr, field) |
Both | sorted by struct field |
@partitioned(arr, pivot) |
Both | partitioned at pivot index |
@stable(in, out, field) |
Post-only | stable sort: output is sorted by field, is a permutation of input, and equal-field elements keep original relative order (field is an identifier, e.g. @stable(items, result, priority)) |
| Pattern | Context | Meaning |
|---|---|---|
@unchanged(x) |
Post-only | x == old(x) |
@pure |
Post-only | no side effects marker |
Postcondition inference rule: Patterns default to preconditions. A pattern automatically becomes a postcondition when any of its arguments is the identifier result (the function return value). For example, @sorted(result) is a postcondition, while @sorted(items) is a precondition. Use @pre(...) or @post(...) to override this behavior explicitly.
The forma explain command translates contract expressions into readable English:
# Human-readable output with box-drawing
forma explain myfile.forma --format human
# Machine-readable JSON
forma explain myfile.forma --format json
# With deterministic example generation
forma explain myfile.forma --format markdown --examples=3 --seed 42Example output:
┌─ verified_sort(items: [Int]) -> [Int]
│ Requires:
│ - items is not empty
│ Guarantees:
│ - [@sorted] for every i in 0..result.len() - 1, result[i] is at most result[i + 1]
│ - [@permutation] permutation(items, result)
└─ Examples:
[valid] ([3]) -> [3]
[valid] ([0, -6]) -> [-6, 0]
[invalid] ([]) -> []
The forma verify command runs contract verification over files or directories:
# Generate a trust report
forma verify src --report --format json --examples 20 --seed 42
# Human-readable report
forma verify src --report --format humanVerification runs with capabilities revoked by default (no file/network/exec side effects) unless --allow-side-effects is explicitly set. This makes it CI-safe and reproducible.
Example:
@pre(values.len() > 0)
@post(forall i in 0..result.len()-1: result[i] <= result[i+1])
@post(permutation(values, result))
f sort(values: [Int]) -> [Int]
sort_ints(values)
Use us to import modules:
us std.io
us std.collections
us std.math
Import specific items:
us collections.{Vec, Map}
Import with alias:
us std.fs -> filesystem
pub md utils
pub f helper() -> Int
42
Prefix a function with as to make it async:
as f fetch_data(url: Str) -> Str!Str
response := http_get(url)?
Ok(response)
Use sp to spawn concurrent tasks and aw to await results:
as f main()
# Spawn concurrent work
task1 := sp fetch_data("https://api.example.com/a")
task2 := sp fetch_data("https://api.example.com/b")
# Await results
result1 := aw task1
result2 := aw task2
print("Both complete!")
Send messages between concurrent tasks:
as f main()
ch := channel_new()
sender := ch.0
receiver := ch.1
sp producer(sender)
msg := channel_recv(receiver)
print(msg)
Shared mutable state between tasks:
mtx := mutex_new(0)
mutex_lock(mtx)
mutex_set(mtx, 42)
mutex_unlock(mtx)
FORMA includes 316+ builtin functions. Here are the key categories:
| Function | Description |
|---|---|
print(value) |
Print to stdout |
eprintln(value) |
Print to stderr |
str(value) |
Convert any value to string |
| Function | Description |
|---|---|
abs(n) |
Absolute value |
sqrt(x) |
Square root |
pow(base, exp) |
Exponentiation |
sin(x), cos(x), tan(x) |
Trigonometry |
asin(x), acos(x), atan2(y, x) |
Inverse trigonometry |
log(x), exp(x) |
Logarithm, exponential |
log2(x) |
Base-2 logarithm |
floor(x), ceil(x), round(x) |
Rounding |
| Function | Description |
|---|---|
str_len(s) |
String length |
str_contains(s, sub) |
Check substring |
str_starts_with(s, prefix) |
Check prefix |
str_ends_with(s, suffix) |
Check suffix |
str_split(s, delim) |
Split string |
str_trim(s) |
Remove whitespace |
str_replace_all(s, old, new) |
Replace all occurrences |
str_replace(s, old, new) |
Replace all occurrences (alias for str_replace_all) |
str_slice(s, start, end) |
Get substring |
str_to_int(s) |
Parse integer (returns Option[Int]) |
str_to_float(s) |
Parse float (returns Option[Float]) |
| Function | Description |
|---|---|
len(collection) |
Get length |
vec_new() |
Create empty list |
vec_push(list, item) |
Append item |
vec_pop(list) |
Remove last item |
vec_slice(list, start, end) |
Get sublist |
vec_concat(a, b) |
Concatenate lists |
vec_reverse(list) |
Reverse list |
vec_sort(list) |
Sort list (generic: Int, Float, Str, Char) |
vec_index_of(list, item) |
Find first index of item (returns Option[Int]) |
sort_ints(list) |
Sort integers |
map(list, fn) |
Apply function to each element |
filter(list, fn) |
Keep elements matching predicate |
reduce(list, init, fn) |
Fold list with accumulator |
any(list, fn) |
True if any element matches predicate |
all(list, fn) |
True if all elements match predicate |
map_new() |
Create empty map |
map_get(map, key) |
Get value by key |
map_insert(map, key, value) |
Insert entry |
map_keys(map) |
Get all keys |
Requires --allow-read and/or --allow-write capability flags:
| Function | Description |
|---|---|
file_read(path) |
Read file to string |
file_write(path, content) |
Write content to file |
file_exists(path) |
Check if file exists |
file_append(path, content) |
Append to file |
file_read_bytes(path) |
Read file as byte array (Result[[Int], Str]) |
file_write_bytes(path, bytes) |
Write byte array to file (Result[(), Str]) |
file_read_lines(path) |
Read file as line array (stdlib: us std.io) |
file_write_lines(path, lines) |
Write line array to file (stdlib: us std.io) |
file_remove(path) |
Delete file |
dir_list(path) |
List directory contents |
dir_create(path) |
Create directory |
path_join(a, b) |
Join path segments |
| Function | Description |
|---|---|
json_parse(s) |
Parse JSON string |
json_stringify(v) |
Convert to JSON |
json_stringify_pretty(v) |
Pretty-print JSON |
json_get(obj, key) |
Get field |
json_get_str(obj, key) |
Get string field |
json_get_int(obj, key) |
Get integer field |
Requires --allow-network capability flag:
| Function | Description |
|---|---|
http_get(url) |
HTTP GET request |
http_post(url, body) |
HTTP POST request |
http_post_json(url, json) |
POST with JSON |
http_serve(port, handler) |
Start HTTP server |
tcp_connect(host, port) |
TCP connection |
tcp_read(conn) |
Read from TCP |
tcp_write(conn, data) |
Write to TCP |
tls_connect(host, port) |
TLS connection |
Capability-gated groups:
--allow-exec: process execution builtins--allow-env: environment variable builtins (env_get,env_set,env_remove,env_vars)--allow-unsafe: pointer/memory allocation and low-level unsafe builtins
| Function | Description |
|---|---|
db_open(path) |
Open SQLite database |
db_open_memory() |
In-memory database |
db_execute(db, sql) |
Execute SQL |
db_query(db, sql) |
Query rows |
db_close(db) |
Close database |
row_get_int(row, col) |
Get integer column |
row_get_str(row, col) |
Get string column |
| Function | Description |
|---|---|
random() |
Random float [0, 1) |
random_int(min, max) |
Random integer in range |
random_bool() |
Random boolean |
random_choice(list) |
Random element |
random_shuffle(list) |
Shuffled copy of list |
| Function | Description |
|---|---|
time_now() |
Unix timestamp (seconds) |
time_now_ms() |
Unix timestamp (milliseconds) |
time_sleep(seconds) |
Sleep |
time_format(ts, fmt) |
Format timestamp |
| Function | Description |
|---|---|
regex_match(pattern, s) |
Test match |
regex_find(pattern, s) |
Find first match |
regex_find_all(pattern, s) |
Find all matches |
regex_replace(pattern, s, repl) |
Replace first |
regex_replace_all(pattern, s, repl) |
Replace all |
regex_split(pattern, s) |
Split by pattern |
| Function | Description |
|---|---|
assert(condition) |
Assert or panic |
panic(message) |
Panic with message |
exit(code) |
Exit process |
FORMA includes stdlib modules written in FORMA that compose builtins. Import with us:
| Module | Import | Key Functions |
|---|---|---|
std.core |
us std.core |
clamp(v, lo, hi) (Int), clamp_float(v, lo, hi) (Float), abs, min, max, pow, gcd, lcm |
std.io |
us std.io |
file_read_lines(path), file_write_lines(path, lines), puts(s), file_read_or(path, default) |
std.string |
us std.string |
str_join(arr, sep), str_replace_first(s, old, new), str_index_of(s, sub), str_pad_left, str_pad_right |
std.vec |
us std.vec |
int_vec_index_of(arr, x), int_vec_sum(arr), int_vec_max(arr), int_vec_min(arr) |
std.iter |
us std.iter |
range(start, end), enumerate(arr) |
std.map |
us std.map |
map_get_or(m, key, default), map_update(m, key, fn) |
forma run <file> # Run a FORMA program
forma run <file> --dump-mir # Run with MIR dump
forma run <file> --no-check-contracts # Disable runtime contracts
forma run <file> --no-optimize # Disable MIR optimization pass
forma run <file> --allow-read # Allow file reads
forma run <file> --allow-write # Allow file writes
forma run <file> --allow-network # Allow networking
forma run <file> --allow-exec # Allow process execution
forma run <file> --allow-env # Allow env var access
forma run <file> --allow-unsafe # Allow pointer/unsafe builtins
forma run <file> --allow-all # Allow all capabilities (see warning below)
forma check <file> # Type check without running
forma check <file> --partial # Partial checking
forma build <file> # Build native executable (LLVM feature)
forma build <file> --no-optimize # Build without MIR optimization
forma explain <file> # Explain contracts in plain English
forma explain <file> --examples=3 --seed 42 --format json
forma explain <file> --max-examples 3 --seed 42 --format json
forma verify <file-or-dir> --report --format human
forma verify <file-or-dir> --report --format json --examples 20 --seed 42
forma verify <file-or-dir> --report --max-steps 10000 --timeout 1000
forma lex <file> # Dump tokens
forma parse <file> # Dump AST
forma grammar --format ebnf # Export grammar as EBNF
forma grammar --format json # Export grammar as JSON
forma fmt <file> # Format source code
forma repl # Interactive REPL
forma new <name> # Create new project
forma init # Initialize project in current dir
forma typeof <file> --position 5:10 # Query type at position
forma complete <file> --position 5:10 # Get completionsDo not run untrusted FORMA code with --allow-all. This flag enables file I/O, networking, process execution, environment variable access, and unsafe memory operations — equivalent to giving the program full host access.
Prefer least-privilege flags:
forma run myfile.forma --allow-read --allow-write # file I/O only
forma run myfile.forma --allow-network # networking onlyThe --allow-exec flag is particularly sensitive — it permits shell command execution via the exec builtin and should be treated as equivalent to full shell access.
When running forma verify, capabilities are revoked by default. Only use --allow-side-effects when you trust the code being verified.
forma explainconverts function contracts into readable intent (human,json, ormarkdown).--examplesgenerates deterministic I/O samples when paired with--seed.- Use
--examples=Nor--max-examples Nto request an explicit explain-example count. explain --examplesincludes both satisfying examples and counterexamples that violate detected preconditions.forma verify --reportexecutes generated examples and emits per-function status:PASS: all generated examples satisfied contract checksSKIP: function could not be safely/meaningfully executed (for example, capability-restricted paths)WARN: no contracts definedFAIL: one or more generated examples violated checks or errored
- Generated examples run side-effect safe by default;
--allow-side-effectsopts into full capabilities. - Resource controls for verify:
--max-stepslimits interpreter steps per generated example--timeoutsets per-example timeout budget in milliseconds
Human-readable errors (default):
error[type_mismatch]: expected Int, found Str
--> main.forma:5:12
JSON errors for AI consumption:
forma check --error-format json main.forma{
"success": false,
"errors": [{
"file": "main.forma",
"line": 5,
"column": 12,
"severity": "error",
"code": "type_mismatch",
"message": "expected Int, found Str"
}]
}$ forma repl
forma> 2 + 3
5
forma> f greet(name: Str) -> Str = f"Hello, {name}!"
forma> greet("World")
"Hello, World!"FORMA exports its grammar in EBNF and JSON formats, enabling constrained decoding. This means LLMs can generate only syntactically valid FORMA code:
forma grammar --format ebnf > forma.ebnf
forma grammar --format json > forma.jsonUse these grammars with constrained decoding toolkits (Outlines, guidance, llama.cpp grammars) to eliminate syntax errors at generation time.
When AI generates code that doesn't type-check, FORMA returns machine-readable errors:
forma check --error-format json broken.formaFeed the JSON errors back to the LLM for automatic correction. Each error includes:
- Exact file, line, column location
- Error code (parseable category)
- Human-readable message
- Optional fix suggestion
Query the type of any expression at a given position:
forma typeof myfile.forma --position "5:10"This enables LLMs to make type-aware completions.
Get context-aware suggestions at a position:
forma complete myfile.forma --position "5:10"FORMA's short keywords and clean syntax mean AI-generated code costs fewer tokens:
| Concept | Rust | FORMA |
|---|---|---|
| Function | fn |
f |
| Struct | struct |
s |
| Enum | enum |
e |
| Match | match |
m |
| Trait | trait |
t |
| Impl | impl |
i |
| While | while |
wh |
| Loop | loop |
lp |
| Break | break |
br |
| Continue | continue |
ct |
| Return | return |
ret |
| Async | async |
as |
| Await | .await |
aw |
| Spawn | tokio::spawn |
sp |
| Use | use |
us |
| Module | mod |
md |
| Unsafe | unsafe |
un |
| Move | move |
mv |
For a dense, token-efficient reference designed to fit in LLM system prompts, see AI Reference.
| Keyword | Long Form | Purpose |
|---|---|---|
f |
— | Function declaration |
s |
— | Struct declaration |
e |
— | Enum declaration |
t |
— | Trait declaration |
i |
— | Impl block |
m |
— | Match expression |
if |
— | Conditional |
then |
— | Then branch (single-expr if) |
else |
— | Else branch |
for |
— | For loop |
in |
— | Iterator binding |
wh |
while |
While loop |
lp |
loop |
Infinite loop |
br |
break |
Break |
ct |
continue |
Continue |
ret |
return |
Return |
as |
async |
Async modifier |
aw |
await |
Await expression |
sp |
spawn |
Spawn task |
us |
use |
Import |
md |
module |
Module declaration |
pub |
— | Public visibility |
mut |
— | Mutable modifier |
ref |
— | Reference parameter |
mv |
move |
Move semantics |
un |
unsafe |
Unsafe block |
type |
— | Type alias |
where |
— | Trait bounds |
linear |
— | Linear type qualifier |
affine |
— | Affine type qualifier |
true / T |
— | Boolean true |
false / F |
— | Boolean false |
none / N |
— | None/null value |
Some |
— | Option::Some constructor |
Ok / ok |
— | Result::Ok constructor |
Err / err |
— | Result::Err constructor |
| Operator | Meaning |
|---|---|
+, -, *, /, % |
Arithmetic |
==, !=, <, <=, >, >= |
Comparison |
&&, ||, ! |
Logical |
&, |, ^, <<, >> |
Bitwise |
= |
Assignment (struct fields) |
:= |
Variable binding / reassignment |
+=, -=, *=, /=, %= |
Compound assignment |
? |
Error propagation |
?? |
Null coalescing |
-> |
Return type / match arm |
=> |
Fat arrow |
.. |
Range (exclusive) |
..= |
Range (inclusive) |
:: |
Path separator |
. |
Field access |
@ |
Pattern binding |