Skip to content

Types, Bindings & Value Semantics

Sema is statically typed with local inference. This page covers the base types, the numeric model (which is stricter than Python’s on purpose), the prelude’s Option/Result (there is no null), the built-in collections, and how bindings, mutability, and value semantics work.

TypeMeaning
intArbitrary-precision integer (the default integer).
i8 i16 i32 i64 u8 u16 u32 u64Sized, fixed-width integers.
f16 f32 f64IEEE floating point (f64 is the default float).
booltrue / false.
strUnicode text; implements the Semantic trait natively.
bytesRaw byte string.

Static type checking happens at sema check time — before the program runs. It verifies call arity, struct construction (unknown field names), literal-argument base-type conflicts (add(1, "two") where add wants two ints), return-type conflicts, and trait-object conformance. The checker is conservative by design: it only flags a mismatch when it can resolve a concrete type on both sides, so generative outputs, ports, and untyped locals are left alone (typed any). It is the linter a type system would give you, with zero false positives.

This is one of the places Sema deliberately diverges from Python. Arithmetic on the default int/float is checked:

  • Overflow is a typed OverflowError, never a silent wrap.
  • Division and modulo by zero, and math-domain errors, raise (DivisionByZero, ValueError) instead of returning NaN/inf.
  • Conversions are explicit constructor calls; there is no implicit widening between the integer and float families.
def half(x: int) -> int !{}:
return x / 2 # DivisionByZero is impossible here; x / 0 would raise
# Mixing int and float requires an explicit conversion — no silent promotion.
ratio = f64(hits) / f64(total)

A sized type is observable, not cosmetic — casts round x through the actual IEEE / two’s-complement representation, which is exactly the quantization behavior ML code needs, native:

q = f16(0.1) # != 0.1 — rounded through IEEE binary16
sat = f8(1000.0) # == 448.0 — FP8-E4M3 saturates
r = bf16(x) # keeps f32's range but only 7 mantissa bits
w = i8(200) # == -56 — two's-complement wrap, like a systems `as iN`
b = u8(300) # == 44

Float cast forms: f32(x), f16(x), bf16(x), f8(x). Integer cast forms: i8/i16/i32/u8/u16/u32 are explicit narrowing conversions with wrap; int/i64/u64 are the full-width forms.

There is no null and no None reference in Sema. Absence and fallibility are ordinary sum types from the prelude:

  • Option[T]Some(x) or None. Use it when a value may be absent.
  • Result[T, E]Ok(x) or Err(e). Use it when an operation may fail.

None is the empty variant of a sum type — you consume it with match, the combinators, or ?/unwrap, never with an identity test (x == None is not how you check absence — there is no null reference to compare against).

def find_user(id: int) -> Option[User] !{db.read}:
rows = db.query("select * from users where id = ?", [id])
return Some(User.from_row(rows[0])) if len(rows) > 0 else None
# Consume it by matching — the only exhaustive way.
match find_user(42):
case Some(u): greet(u)
case None: prompt_signup()

Full error handling — ?, unwrap, expect/except, and the Result/Option combinators — is on the Error Handling page.

Collections — homogeneous and value-semantic

Section titled “Collections — homogeneous and value-semantic”

list[T], dict[K, V], and set[T] are built in, homogeneous (one element type), and value-semantic (see below). Literals and comprehensions are Pythonic:

xs = [1, 2, 3]
kept = [x for x in xs if x > 1] # comprehension
by_id = {u.id: u for u in users} # dict comprehension
tags = {t for t in raw if len(t) > 0} # set comprehension
point: tuple[int, int] = (3, 4) # tuple, literal (a, b)
x, y = point # destructured by irrefutable pattern
  • Slices on list/str/bytes use Python syntax (xs[1:]) and return copies.
  • dict keys and set elements must be hashable with total ==.
  • Heterogeneous collections are excluded (Codon divergence). Dynamic JSON-shaped data enters through the prelude JsonValue sum and leaves it at a typed boundary (parse[T] against a struct schema), never via stringly subscripting.

A comprehension is the sequential base case of the parallel [...] form — same scoping and typing rules.

List methods include append/extend/insert/pop/sort/reverse/index/ count/slice/first/last/contains/join; dict methods include get/set/keys/values/items/update/pop/setdefault/contains/len; plus the free builtins enumerate/zip/map/filter/sorted/reversed/ sum/min/max/mean.

str carries a method surface:

upper lower strip lstrip rstrip
split join replace
startswith endswith contains
find rfind count # find/rfind return a char index or -1
slice substring
capitalize title
isdigit isalpha isalnum isspace
repeat len
name = " Ada Lovelace ".strip()
slug = name.lower().replace(" ", "-")
if slug.startswith("ada") and slug.count("-") == 1:
log.info("slug", value=slug)

Bindings are immutable by default. mut x = … declares a rebindable binding whose aggregate contents may also be mutated in place. Assigning to a plain binding is a compile error. Every binding is monomorphic — one type for its lifetime; rebinding cannot change type.

let base = 10 # immutable (the `let` keyword is optional for bindings)
count = 10 # also immutable — a plain binding is not rebindable
mut total = 0 # rebindable
for x in xs:
total = total + x # ok — `total` is `mut`
# count = 11 # compile error: assignment to an immutable binding

Structs and collections are value-semantic: assignment and argument passing denote the value, not a shared alias. The compiler is free to copy-on-write. There is no observable aliasing of mutable data outside the explicit shared-state types Atomic[T] and Mutex[T]. This is what makes Sema’s parallelism sound without a GIL.

mut a = [1, 2, 3]
b = a # b is the value, not an alias
a.append(4) # mutating a does NOT change b
# a == [1,2,3,4], b == [1,2,3]

Mutation interacts with the rest of the language in three fixed ways:

  1. Contracts. A struct invariant is re-checked at every mutation of a guarded field through a mut binding, and at every boundary crossing — an aggregate can never be observed with a violated invariant.
  2. Trust. Writing a field re-labels the aggregate with the meet of its old trust label and the written value’s label — mutation can only lower trust, never launder it. See /governance/provenance/.
  3. Constants. Module-level plain bindings of literal or pure-!{} initializers are compile-time constants.

then if cond else else_ is an expression — only the taken branch is evaluated. It sits below every binary operator and above lambda/=> in precedence, and its else branch chains right-associatively:

label = "hot" if score > 0.9 else ("warm" if score > 0.5 else "cold")

This is the concise form default trait methods lean on; the statement if is on the Control Flow page.

Functions and lambdas are first-class values. The function type is written (T, U) -> R !{row} — the effect row is part of the type, so a parameter of function type declares the effects its callee may perform. A higher-order function cannot smuggle effects its own row does not admit. See Functions & Effects.

These types are language-adjacent and committed in the prelude, not user code: Path, Duration (written as quoted duration literals — "2s", "50ms"), Instant, JsonValue, Tensor[T], Atomic[T], Mutex[T], Task[T], Stream[T]/Window[T], DebugSnapshot, and the structured logging surface (log.*, print, alert). Their full APIs live in the stdlib reference.