# EX Script — Type System

## Design goals

1. **Infer by default.** You write types only where the compiler can't know better
   (parameters, public API surface, `Any` boundaries).
2. **Safe by default.** No `null`; optionality is tracked through the whole program.
   No implicit `Any`. Exhaustiveness is proven by the compiler.
3. **Structural where useful.** A `contract` is satisfied by shape, not by
   declaration. No `implements`, no nominal ceremony.
4. **Small enough to learn in an hour.** There is no conditional-type Turing
   machine. What you see in the source is what the types mean.

## Primitive types

| Type | Meaning | JS representation |
|---|---|---|
| `Int` | 64-bit integer | `number` (safe integer range checked) |
| `Float` | 64-bit float | `number` |
| `Bool` | `true`/`false` | `boolean` |
| `String` | Unicode text | `string` |
| `Char` | single code point | `string` (length 1) |
| `Bytes` | byte sequence | `Uint8Array` |
| `Void` | no value | `undefined` |
| `Any` | dynamic (JS boundary) | anything |

## Compound types

```
List<T>        T[]            — ordered collection
Map<K, V>      Map            — key → value
Set<T>         Set            — unique values
(A, B, ...)    tuple          — fixed arity, heterogeneous
(A) -> B       function type  — (params) -> result
T?             optional       — may be absent; never null
T!             result         — may fail with an Err value
```

- `List`, `Map`, `Set` are written `List<Int>`, not `Int[]`. One syntax, no
  surprises.
- Optional `T?` and result `T!` compose: `fun f() -> User?!` is a fallible
  function returning an optional user.

## Structural typing

```
contract HasName { name: String }

struct Person { name: String, age: Int }

fun greet(x: HasName) -> String { "hi {x.name}" }

greet(Person("Ada", 36))   // ✓ — Person has a `name: String` field
```

Assignability is by shape everywhere: structs satisfy contracts, tuples satisfy
tuple types, functions satisfy function types by parameter/result comparison.

## Unions and narrowing

The `type` declaration creates a closed, discriminated union:

```
type Shape = Circle(radius: Float) | Square(side: Float) | Rect(w: Float, h: Float)
```

Each variant carries a runtime `tag`. The compiler:

- proves `match` coverage (exhaustiveness);
- narrows the payload inside each branch;
- rejects pattern sets that overlap ambiguously;
- treats the union as a first-class value (can be stored, returned, passed).

`Any` values can be narrowed with `is` tests and `match ... is` patterns; after
narrowing the value has a static type.

## Optionality tracking

`T?` values flow through the program and the checker tracks them:

- `let x: String? = maybe()` — declared optional
- `x ?? ...` — not valid; use `x or fallback` (lazy)
- `x.require("msg")` — checked unwrap, `String` result, runtime-checked
- `if let v = x { }` — narrowed inside
- `match x { Some(v) -> ..., None -> ... }` — pattern alternatives
- passing `x` where `String` is required — **compile error** with fixes (see
  diagnostics)

There is no `null`, no `undefined` type, no `Non-null assertion operator`.

## Results

`T!` is the type of a possibly-failing computation. `Result` has two shapes:

```
Ok(value)     — succeeded
Err(e)        — failed with an `Err` value
```

`Err` is a built-in error value carrying a message and a stack trace. The checker
enforces that a `Result` value is handled (via `?`, `match`, `or`, `if let`, or
`.require`) before it is dropped, and that `?` is only used inside a `!` function.
This turns the JavaScript "unhandled promise rejection" class of bugs into a
compile error.

## Generics

```
fun first<T>(items: List<T>) -> T? { ... }
fun map<T, U>(items: List<T>, f: (T) -> U) -> List<U> { ... }

contract Box<T> { get(): T }
```

- Type parameters are inferred from arguments; explicit arguments are rarely
  needed: `first<Int>(...)`.
- No variance annotations. Subtyping is structural; a `List<Circle>` is a
  `List<Shape>` because `Circle` is a `Shape`.
- Constraints are written `T: Contract` when needed.

## Inference rules

1. Literals: `5` → `Int`, `5.0` → `Float`, `"s"` → `String`, `'c'` → `Char`,
   `[1, 2]` → `List<Int>`, `{ "a": 1 }` → `Map<String, Int>`, `(1, "a")` →
   `(Int, String)`, `true` → `Bool`.
2. Function bodies: return type inferred from the last expression (or `Void`).
3. `if`/`match`: the common type of the branches.
4. Calls: from declared parameter types, with generic unification.
5. Struct/enum/schema literals: from the named type.
6. `let` without annotation: from the initializer. The declared type must be
   assignable from it (no silent widening, no silent narrowing).
7. Compound assignment (`+=`), `for` loops, and `await` all propagate types.
8. Method calls and property access are resolved from the receiver's type.

## Assignability

`A` is assignable to `B` when:

- `A` and `B` are the same type, or
- both are primitives and equal, or
- `A` is `T?` and `B` is `T?` and `T` is assignable (optionality is exact — a
  `String?` is **not** assignable to `String`), or
- `A` is a union variant and `B` is its union, or
- `A` and `B` are structs/contracts and every member of `B` exists in `A` with an
  assignable type (structural), or
- `A` is a tuple and `B` is a tuple of the same arity, memberwise assignable, or
- `A` is `List<T>`, `Set<T>` and `B` is `List<U>`, `Set<U>` with `T` assignable to
  `U` (covariant — safe because collections are read/write but element types are
  never unsafely widened at runtime: list *inserts* are checked against the
  declared element type), or
- function types: contravariant params, covariant result, or
- `Any` is assignable to/from anything (the dynamic boundary).

## Runtime validation (schema)

`schema` is the bridge between compile-time and runtime guarantees:

```
schema User {
    name: String (minLen: 1)
    age: Int (min: 0, max: 130)
    email: Email?                     // built-in format validators
    roles: List<String> (default: [])
}
```

From this one declaration the compiler generates:

1. a static type `User` (fields as declared);
2. `User.parse(String) -> User!` — parse + validate JSON text;
3. `User.from(Any) -> User!` — validate an `Any` value;
4. field-level checks (constraints above) with precise error messages.

Constraints: `min`, `max`, `minLen`, `maxLen`, `pattern`, `default`, format types
(`Email`, `Url`, `Uuid`, `Date`), `optional` via `?`.

## Utility types

| Type | Meaning |
|---|---|
| `List<T>` / `Set<T>` / `Map<K, V>` | collections |
| `T?` | optional |
| `T!` | result |
| `(A, B)` | tuple |
| `(A) -> B` | function |
| `Any` | dynamic boundary |
| `Err` | error value |

That is the whole utility vocabulary. No mapped/conditional/template-literal
types — the compiler generates what it generates, and that is a feature.

## What the compiler does NOT guarantee (honesty)

Static types are checked at compile time. The runtime makes no claims they
weren't checked. Therefore:

- JS imports are `Any` until validated/narrowed — the compiler never lies about
  what a JS function actually returns.
- Untrusted data (JSON, env vars, config, DB rows, user input) should cross a
  `schema` boundary. The compiler *encourages* this; tooling (lint) flags dynamic
  values that flow into static positions without a boundary.
- `Any` is the only place a lie can enter; `as` (narrowing) and `.require` check at
  runtime and fail loudly.

This split — compile-time guarantees vs. runtime guarantees — is explicit
language policy, documented and enforced.