# EX Script — Syntax

This document is the normative syntax proposal. The compiler in this repository
implements exactly this. Every feature shown here works today.

## Conventions

- 2-space indentation, braces on the same line as the opening construct.
- `//` line comments and `/* */` block comments.
- Files end in `.xan`. Each file is a module.
- Strings use double quotes; single quotes are characters: `"hello"`, `'h'`.

## Hello world

```
// src/main.xan
import std.io

export fun main() {
    io.println("Hello, world!")
}
```

`ex init` creates this. `ex run` compiles and runs it.

## Variables

```
let name = "ex"                 // immutable — cannot be reassigned
let mut count = 0               // mutable — explicit opt-in
count += 1

let greeting = "Hi {name}"      // string interpolation
let pi: Float = 3.14            // annotation only where it adds info
let big = 1_000_000             // numeric separators
let empty: String? = undefined  // optional value: no null, just absence
```

Rules:

- `let` is immutable by default. `let mut` is required to reassign.
- Type inference is the default; annotations are for public APIs and `Any` boundaries.
- There is **no `null`**. Optional values use the `?` type suffix and the value
  `undefined`.

## Functions

```
fun add(a: Int, b: Int) -> Int {      // block body
    a + b                              // last expression is the return value
}

fun double(x: Int) = x * 2            // expression body

fun describe(p: Person) -> String {   // body with early return
    if p.age < 18 { return "minor" }
    "adult"
}

export fun main() {                   // entry point, params optional
    ...
}

async fun fetchUser(id: Int) -> User! { ... }   // async + fallible
```

Rules:

- Parameters require annotations (the one place types are always written).
- Return type is optional; it is inferred from the body.
- The last expression of a block is the value. `return` is for early exits.
- An `async` function returns a promise; it can also be `!` (fallible).

## Control flow

```
// if / else — an expression
let label = if score >= 60 { "pass" } else { "fail" }

// match — exhaustive pattern matching
match shape {
    Circle(r)   -> "circle with radius {r}"
    Square(s)   -> "square"
    Rect(w, h)  -> "rect {w}x{h}"
}

// for — over iterables and ranges
for item in items { io.println(item) }
for (i, item) in items.indexed() { io.println("{i}: {item}") }
for i in 0..10 { sum += i }            // 0..10 is exclusive: 0..9
for i in 0..=10 { ... }                // inclusive: 0..10

// while
let mut i = 0
while i < 10 { i += 1 }
```

Rules:

- `match` must be exhaustive over union and enum types; the `_` wildcard covers
  the rest. The compiler proves coverage.
- The type of `if`/`match` is the common type of all branches.

## Optionals

```
let home: String? = env.get("HOME")

let value = home or "/home"        // fallback if missing (lazy)
if let h = home { io.println(h) }  // narrow to the present value
let h = home.require("HOME must be set")  // checked unwrap, good error on fail

// optional chaining
let street = user?.address?.street
```

Rules:

- You can't use a `T?` where a `T` is required — the compiler proves presence.
- `if let` and `match` narrow the type inside the branch.
- `or` is lazy (evaluates the fallback only when needed).

## Errors

```
fun readConfig(path: String) -> Config! {      // `!` = may fail with Result
    let text = std.io.readFile(path)?          // `?` propagates the error
    Config.parse(text)
}

fun fallback() -> Config {
    let config = readConfig("config.json") or Config.default()
}

export fun main() {
    match readConfig("config.json") {
        Ok(config) -> io.println("loaded {config.name}")
        Err(e)     -> io.println("failed: {e}")
    }
}

// inside a `!` function, raise a failure explicitly:
fun validateAge(a: Int) -> Int {
    if a < 0 { raise "age must be non-negative, got {a}" }
    a
}
```

Rules:

- A function declared `-> T!` returns `Result<T, Err>` and may use `raise`.
- A `Result` must be handled before it is discarded: `?`, `match`, `or`,
  `if let`, or `.require(...)`. The compiler enforces this.
- `try { ... } catch e { ... }` exists for the JavaScript boundary only —
  catching exceptions thrown by JS code.

## Types and data shapes

```
struct Point { x: Float, y: Float }            // record

enum Color { Red, Green, Blue }                // plain enum
enum HttpMethod { Get, Post, Put, Delete }

type Shape =                                  // discriminated union (ADT)
    | Circle(radius: Float)
    | Square(side: Float)
    | Rect(width: Float, height: Float)

contract Logger {                             // structural interface
    log(msg: String): Void
}

schema Config {                               // type + runtime validator
    port: Int (min: 1, max: 65535)
    host: String (default: "localhost")
    debug: Bool (default: false)
}
```

Rules:

- `struct`: named record of fields. Constructed with `Point(1.5, 2.0)` or
  `Point { x: 1.5, y: 2.0 }`.
- `enum`: closed set of values. `Color.Red`.
- `type`: closed union of variants, each with optional payloads. Pattern matching
  is exhaustive.
- `contract`: anything with the right shape satisfies it automatically
  (structural typing). No `implements` keyword.
- `schema`: generates a static type, a runtime validator, `.parse(String) -> T!`,
  and `.from(Any) -> T!`. Field constraints and defaults live in the declaration.
- `class`: for stateful objects with private state. Used rarely; `struct` +
  functions are preferred.

## Collections

```
let list = [1, 2, 3]                 // List<Int>
let map = { "a": 1, "b": 2 }         // Map<String, Int>
let set = Set(1, 2, 3)               // Set<Int>
let tuple = (1, "one")               // (Int, String)

list[0]          // index access
map["a"]         // Map access returns Map<V>? — use or/require
set.contains(1)

list.map(fn)     // standard iteration methods on all collections
list.filter(fn)
list.sum()
list.count()
```

Map access returns an optional value because the key may be absent — the compiler
won't let you forget.

## Destructuring

```
let (x, y) = point                    // tuple / struct positional
let { name, age } = user              // struct by field name
let (first, rest) = list.splitFirst()!
let (a, b) = await all(task1(), task2())   // concurrency

// in patterns:
match tuple { (0, y) -> ... , (x, 0) -> ... , (x, y) -> ... }
```

## Concurrency

```
async fun main() {
    let (users, posts) = await all(fetchUsers(), fetchPosts())   // parallel
    let winner = await race(fetchA(), fetchB())                  // first to finish
    let slow = await timeout(1000, slowCall())                   // fails if too slow
    let data = await fetch("/api")!
}
```

Rules:

- `all` runs futures in parallel and fails fast: if any fails, the result is an
  error (the others are cancelled).
- `race` resolves with the first to complete (value or error).
- `timeout(ms, fut)` fails if the future doesn't finish in time.
- Cancellation is automatic: the compiler wires it up; you never write
  `AbortController`-style plumbing.

## Testing

```
test "addition works" {
    expect(add(2, 3)).eq(5)
}

test "parse rejects bad input" {
    expect(Config.parse("not json")).fails()
}

async test "api round-trip" {
    let res = await fetch("/ping")!
    expect(res.status).eq(200)
}
```

`ex test` discovers and runs every `test` block in the project. `expect` is
auto-imported in modules containing tests (`std.test` needs no explicit import).

## Modules and imports

```
import std.io                              // standard library
import std.json as json                    // aliased
from std.string import toUpper             // selective
import "src/helpers"                    // relative file (no extension needed)
from pkg:my-lib import flatten             // package import

export fun publicThing() { ... }           // exported — public API
fun privateThing() { ... }                 // everything else is private
```

Rules:

- Everything is private unless `export`ed.
- `std.*` is the standard library. `pkg:name` is a dependency from the manifest.
- Relative imports are resolved from the importing file, without the `.xan`
  extension.

## Type tests and narrowing

```
let value: Any = json.parse(text)         // dynamic boundary
if value is Int {                         // type test narrows inside
    let n: Int = value
}
match value {
    is String -> ...
    is Int    -> ...
    _         -> ...
}
```

`Any` is the dynamic escape hatch (JS interop, JSON). Everything else is statically
typed.

## Interop with JavaScript

```
import js:node/fs                       // Node built-in module
import js:lodash                        // npm package

let text = fs.readFileSync("x.txt", "utf8")   // Any result
let content: String = text.require("expected a string")
```

`js:` imports give you the module untyped (`Any`). Validate or narrow at the
boundary; the compiler never pretends a JS value has a static type it doesn't have.

## Other expressions

```
let f = fun (x: Int) -> Int { x * 2 }   // anonymous function (lambda)
list.map(fun (x) { x * 2 })             // params inferred where possible
f(3)

let up = "ex".toUpper()                 // method calls on primitives
let s = "  pad  ".trim()

// ranges and iteration
for i in 0..100 { ... }
let squares = (0..10).map(fun (i) { i * i })

// ternary-style: if as expression (see Control flow)
```

## Reserved words

`let mut fun async await if else match for in while return or raise try catch
struct enum type contract schema class import from export test is and not
true false undefined Ok Err as`

Note: `and`/`not` are aliases for `&&`/`!` provided for readability; `or` is the
default-value operator (boolean `||` is spelled `or` with boolean operands — it is
the same operator, safely typed).

## Formatting

`ex fmt` canonicalizes: 2-space indent, one statement per line, no trailing
whitespace, spaces inside braces `{ x }` in interpolation, aligned chained calls
as in the examples above. The formatter is deterministic: formatting an already
formatted file is a no-op.