<!-- Sema documentation — Types, Bindings & Value Semantics
     Sema is a new AI-native, neurosymbolic programming language most models
     have not been trained on. Full corpus: https://sema.49.12.246.95.sslip.io/llms-full.txt
     Install language support: https://sema.49.12.246.95.sslip.io/install-skill/ -->

# Types, Bindings & Value Semantics

> Scalars, sized numerics, no-null Option/Result, homogeneous collections, and Sema's immutable-by-default 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.

## Scalars

| Type | Meaning |
|---|---|
| `int` | Arbitrary-precision integer (the default integer). |
| `i8` `i16` `i32` `i64` `u8` `u16` `u32` `u64` | Sized, fixed-width integers. |
| `f16` `f32` `f64` | IEEE floating point (`f64` is the default float). |
| `bool` | `true` / `false`. |
| `str` | Unicode text; implements the `Semantic` trait natively. |
| `bytes` | Raw 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 `int`s), 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.

## Numerics — checked, never silent

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.

```sema
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)
```

### Width casts round through the real format

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:

```sema
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.

:::caution[Wrap is opt-in, not the default]
Wrapping only happens through an explicit sized cast. Ordinary `int`/`float`
arithmetic never wraps — it raises. If you want the systems-style wrap, you ask
for it by name.
:::

## No `null` — `Option[T]` and `Result[T, E]`

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).

```sema
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](/language/error-handling/) page.

## 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:

```sema
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`.

## String methods

`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
```

```sema
name = "  Ada Lovelace  ".strip()
slug = name.lower().replace(" ", "-")
if slug.startswith("ada") and slug.count("-") == 1:
    log.info("slug", value=slug)
```

## Bindings and mutability

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.

```sema
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
```

:::note[Blocks introduce no new scope]
Following Python's binding rules, `if`/`expect` arms do **not** open a new scope —
a binding made inside such a block is visible after it. The one exception is `with
<expr> as x:` (see [Control Flow](/language/control-flow/)): its `as`-binding is
affine and scoped to the block, released at scope exit, and cannot be referenced
afterwards.
:::

## Value semantics

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.

```sema
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/](/governance/provenance/).
3. **Constants.** Module-level plain bindings of literal or pure-`!{}`
   initializers are compile-time constants.

## Conditional expression

`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:

```sema
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](/language/control-flow/) page.

## Function types

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](/language/functions-and-effects/).

## Prelude type commitments

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](/stdlib/overview/).

## What's next

- **Sum types with behavior** → [Traits, Enums & Generics](/language/traits-enums-generics/)
- **Absence and failure in depth** → [Error Handling](/language/error-handling/)
- **The `~=` graded operator** → [Operators](/language/operators/) and [Similarity](/neurosymbolic/similarity/)
