# Surface language specification, v0

- **Status:** draft
- **Edition:** none yet. Version 0 has no stability guarantee.
- **Related documents:** [PLAN.md](https://git.solenovis.com/henning.liemant/surface/src/branch/main/PLAN.md), [ADRs](https://git.solenovis.com/henning.liemant/surface/src/branch/main/docs/adr), [GLOSSARY.md](https://git.solenovis.com/henning.liemant/surface/src/branch/main/GLOSSARY.md), [component reference](https://surface.solenovis.com/docs/components.md)
- **Changes:** 2026-11-06: the texts of the runtime errors are sentences in Simplified Technical English (15.2, P6.29). 2026-11-06: the formatter writes 4 spaces for each indent level, and the key `indent` of `[fmt]` in `surf.toml` can choose another indent (2.1, ADR-0162). 2026-11-06: the compiler has sequences on all targets: `sequence`, `cue` and the utility `cue-<name>` (3.2, 7.5a, 16, ADR-0152). `with` takes the name of an earlier cue, as in the grammar. 2026-11-06: the compiler has `Video` and `Track` (9.6, P6.25). 2026-11-06: the states `active:` and `focus-visible:` of ADR-0161 in 12. 2026-11-06: the compiler has `Locale` with the ICU of the system, and `sortWith` (4.4, 4.5, 7.3, 10.2, 10.4, 14, P6.6). It reads the key `locales` of `surf.toml`, and it gives its errors. The three sorts have one merge sort on each target, and section 4.5 gives its exact form. 2026-11-05: the compiler has `dyn` types and the error of `==` on a `dyn` value (5.1, 7.9, P6.3). It has generic components (3.2, 7.9, 9.1, P6.4), and `Comparable` includes `Equatable` (7.8). 2026-11-05: `Video`, `Audio`, `Source` and `Track` in the list of built-in components (9.6, ADR-0156, ADR-0157). 2026-11-05: sequences, the timeline form of version 1, with the contextual keywords `sequence` and `cue` (3.2, 7.1, 7.5a, 16, ADR-0152). The spec marks them "Not in version 0". 2026-11-05: the compiler has bounds with `+` and `impl` types (7.3, 7.9, P6.2). It also has impls and methods with type parameters, and `T.zero()` and `T.Item` in a generic function (7.8, 7.9). `==` on a type parameter needs `T: Equatable` (7.3). A generic function is a value where the expected type gives its types, as in Rust (7.3). 2026-11-05: the compiler has the rules of file names (2.2), the order of UTF-16 code units on the web (4.4), only the names of utilities in `ease` (7.5), and the import of a trait (7.2, 7.8). 2026-11-05: the answers of the project owner for version 1 (18). File names in camelCase and PascalCase (2.2, ADR-0140). Each keyword is valid as a field name (3.2, ADR-0134). The web compares strings by UTF-16 code units (4.4, ADR-0113). `ease` of `motion` takes only the name of a utility (7.5, ADR-0104). A file calls a method of a trait only when it imports the trait (7.2, 7.8, ADR-0134). The spec marks the new rules "Not in version 0". 2026-11-05: the compiler has traits and impl blocks (7.8, P6.1). The six words of traits are keywords (3.2). `sort`, map keys, `==`, `{x}` and the operators use the impls (4.5, 5.1, 5.4, 6.2).

  2026-11-04: the details of `Locale` (ADR-0132). A module constant cannot read `Locale` (7.3, 14). A call of a function prop reads `Locale` when a function value of its type reads it (10.2). `Locale.set` is a write (10.4, 14). `locales = []` is an error, the lookup of RFC 4647 finds the locale, and a request after `Locale.set` has the header `x-wave-locale` (14). The three sorts use one stable merge sort on each target (4.5). 2026-11-04: `Locale` is valid in each function (7.3, 10.2, 14, ADR-0132). A project with `Locale` needs the key `locales`. A request of a server has its own locale, and `Locale.set` is not saved (14, ADR-0132). `sortWith` (4.4, 4.5, 6.4, ADR-0132). `==` on a `dyn` value is a compile error, and `==` on a type parameter needs `Equatable` (5.1, 7.3, 7.8, 7.9, ADR-0135). A module constant gets its value at its first read (7.3, ADR-0131). 2026-11-04: the decisions for version 1 (18). Traits and impl blocks (7.8, ADR-0134). Type parameters with bounds, generic components and trait objects (7.9, ADR-0135). The changes for both are also in 3.2, 4.1, 4.5, 5.1, 5.4, 6.2, 7.1, 7.3, 9.1, 10.2 and 16. No function types, traits or type parameters in extern modules (13.1, ADR-0133). The module `Locale` (7.2, 14, ADR-0132). The second section 13.3 (Calls) is now 13.4. 2026-11-03: a runtime error in a mount and in `on error` shows in a development build (15.2, ADR-0122). 2026-11-03: a module constant has one value (7.3, ADR-0118). 2026-11-03: a name cannot contain `__` (3.1, ADR-0115). 2026-11-03: `Span`, a `Link` in a `Text`, and one text block for each `Text` (9.3, 9.6, ADR-0111). 2026-11-03: `Window` (14, ADR-0106), the module `surf:ui` (7.2, ADR-0102), `Focus.to` and `Focus.newId` (9.7, ADR-0102), the high contrast modes of `ThemeMode` (ADR-0103), and `motion` (7.5, ADR-0104). 2026-11-02: the syntax review: `;` and match commas (3.5, 6.3, ADR-0094), `.surfs` for modules (2.2, ADR-0093), `$` only for tokens (3.1, ADR-0095), conditions in style text (9.2, 12, ADR-0096), number marks and enum attributes (3.3, 4.7, ADR-0099). 2026-11-01: generic records and enums, and tuples (7.3, ADR-0087). 2026-11-01: `Theme.setScale` (ADR-0085). 2026-11-01: `Set<T>` and `{...value}` (ADR-0083), and `Int64` (ADR-0084). 2026-11-01: generic functions (7.3, ADR-0080). 2026-11-01: the functions of dates (ADR-0079). 2026-11-01: `File.pick` (ADR-0078). 2026-11-01: `Http.event` (ADR-0077). 2026-11-01: names in other scripts (3.1, ADR-0076). 2026-11-01: `Storage` (ADR-0074), and `on key` (7.5, ADR-0075). 2026-11-01: `#try` (9.5, 15.2, ADR-0073). 2026-11-01: packages and `pkg:` imports (7.2, ADR-0071), `surf lint` (3.1, 9.5, ADR-0070), and the module `surf:test` (7.2, ADR-0069). 2026-10-31: `Json.embed`, `Theme.designWidth`, and events and calls for each app in a process (13, ADR-0056). 2026-10-31: the variant form `style:{value}={{ ... }}`, and style text can have line ends (2, 9.2, ADR-0054). 2026-10-30: a function value can be each kind of value, and it only reads (6.4, 10.3, ADR-0039). 2026-10-29: `style:` directives and computed values in `style` on an instance of a user component (9.2, Phase 3, P3.11).

  2026-10-29: `on error(message)` in the root component (15.2, Phase 3, P3.11).

  2026-10-29: imports from subfolders, and a unique name for each component (7.2, Phase 3, P3.11).

  2026-10-29: the pointer events `pointerdown`, `pointermove`, `pointerup` and `wheel` with the built-in record `Pointer` (component reference 3.1, Phase 3, P3.2).

  2026-10-29: the standard library for apps (Phase 3, P3.1): math, `format`, `replace`, `indexOf`, `join`, `any`, `all`, `sum`, `Float.pi`, `Time.now()` and `Random`. The [standard library](https://surface.solenovis.com/docs/standard-library.md) lists the rules.

  2026-10-29: `surf extern stubs` writes the stubs of the implementation files (13.2).

  2026-10-29: an initial value cannot call a function of the component (7.5). Before, the compiler accepted the call, and the generated code failed.

  2026-10-29: `List` is virtualized, has the list role, and takes `rowHeight` (9.6, ADR-0027).

  2026-10-29: the `Timer` module is removed. The built-in `interval(ms)` ends at the next multiple of `ms` on the clock, so a loop with it does not drift (7.5, 8, ADR-0026).

  2026-10-29: `Int.parse` and `Float.parse` read a number from a string (4.2).

  2026-10-29: the bounds of `slice`, `get` outside a list, and `split("")` are the same on all targets (4.4, 4.5). Before, the web and the native targets gave different results.

  2026-10-29: the features that the compiler of version 0 does not have yet are marked "Not in version 0" (3.1, 6.4, 7.2, 7.5, 9.2, 9.5, 9.6, 13.2, 15.2). [Limits of version 0](https://surface.solenovis.com/docs/limits.md) lists them on one page. `panic` ends a path like `return` (7.3). The lambda of a list method can read a `let` variable (6.4). Text can go into each slot that is inside a component that accepts text, not only the `default` slot (9.3).

  2026-10-29: the rules of `chars`, `remove`, `sort` and `sortBy`. `enumerate` is removed: version 0 has no tuples, and a `for` loop gives the index (4.4, 4.5).

  2026-10-28: a component file has no `<script>` tag. A line `---` separates its declarations from its markup (2.3). Comments are `//`, `/* */` and `///` everywhere (2.4). An `on:` attribute takes statements (6.4, 9.2). Blocks are `#if`, `#for`, `#match`, `#await` and `#fill` with a body in braces, and tags are `@const` and `@slot` (9.5). ADR-0021 gives the reasons.

  2026-10-28: utilities replace the `<style>` section, the style attributes and the class names (ADR-0020). `testid` is new. `Box`, the `variant` and `tone` of `Text`, and `#platform` blocks are removed (2.3, 9.2, 9.5, 9.6, 12, 14, 16).

  2026-10-25: clarifications from Spike A ([SPIKE-A.md](https://git.solenovis.com/henning.liemant/surface/src/branch/main/spike/SPIKE-A.md), sections 4 and 7.4). They define these rules:
  - the rounding of `Float.round()` (4.2)
  - `for` loops over a changing list (6.1)
  - lambdas with an assignment body (6.4). An `on:` attribute takes statements now (ADR-0021).
  - return paths (7.3)
  - list blocks over computed lists (9.5)

  2026-10-26: P1.7 sets the web implementation file of Phase 1 to `.web.js` and gives the JavaScript values at the boundary (13.2, 13.4).

  2026-10-26: P1.6 adds the rules of section 8 for the position of `await`, `spawn`, `sleep` and the start of a task. `#await` takes a `Task<T>?` (9.5).

  2026-10-26: P1.9 names the theme tokens (12.2). The web stylesheet is in the layout spec, section 10.

  2026-10-26: P1.8 adds the [component reference](https://surface.solenovis.com/docs/components.md) (9.2, 9.6).

  2026-10-25: P1.5 adds the map methods and the equality of maps (4.5).

  2026-10-25: P1.4 adds three rules. The first rule tells where `cleanup` is valid and what it can use (7.5). The others are about events from a component that leaves the tree (7.6), and writes that are not reads (10.2).

  2026-10-25: the slot example of 7.7 declares `row` with a `bindable` parameter, and the parent binds `items`. Its fill binds `todo.done`, and a fill parameter without `bindable` is a constant. P1.3 found the conflict.

This document specifies the `.surf` language: the file structure, Surf Script, the markup, the reactivity model, the styles and the extern modules. The Surface Layout Spec is a separate document. It has the utilities. Section 12 gives only a summary of them.

---

## 1. Conventions

- **must** and **must not** state a rule. A violation of a rule is a compile error, unless the rule says "runtime error".
- **can** states a permission.
- A **runtime error** has the same cause and the same result on all targets (section 15).
- Grammar fragments use this notation: `?` is optional, `*` is zero or more, `+` is one or more, `|` separates alternatives, and `"x"` is literal text.

The design rule behind every decision in this spec: **one source, one meaning, on every target.** If a feature cannot have the same meaning in JavaScript and in native code, the feature is not in the language.

---

## 2. Source files

### 2.1 Encoding

Source files are UTF-8. Line ends can be LF or CRLF. The formatter writes LF.

The formatter writes 4 spaces for each indent level. A project can set another indent in `surf.toml` (ADR-0162):

```toml
[fmt]
indent = 2
```

- The value is a number of spaces from 1 to 8.
- A file uses the `surf.toml` of the nearest folder above it. Thus a package in `src/pkg` keeps its own indent. A file with no `surf.toml` above it gets 4 spaces.
- `surf fmt`, `surf fmt --check` and the format action of the language server use the indent of the project. The indent of the project wins over the settings of the editor.
- The indent does not change the meaning of a file. The formatter does not use tabs.

### 2.2 File kinds

| Kind | File name | Content |
|---|---|---|
| Component file | Has the extension `.surf`. A name in PascalCase, for example `Counter.surf` or `TodoRow.surf` | Declarations in Surf Script, then markup |
| Module file | Has the extension `.surfs`. A name in camelCase, for example `model.surfs` or `todoRow.surfs` | Declarations in Surf Script only |

The extension gives the kind of a file (ADR-0093). The name of a component is its file name without the extension. A component file must contain markup. A module file must not contain markup.

**File names** (ADR-0140):

- A component file starts with an uppercase letter, and a module file starts with a lowercase letter. A capital letter marks each new word. A name has no `_` and no `-`.
- A wrong name is a compile error, and the message gives the correct name. For example, the message for `todo_row.surfs` gives `todoRow.surfs`, and the message for `todo-row.surf` gives `TodoRow.surf`.
- Two files with paths that differ only in case are a compile error, for example `todoRow.surfs` and `todorow.surfs`. macOS and Windows read the two paths as one file.
- The rule applies to the part before the first dot. The parts after a dot keep their rule, for example `.server` in `hooks.server.surfs` and `.remote` in `todos.remote.surfs` (wave spec 1). The `@` of `Page@.surf` stays valid.
- Two files cannot have one module name on a native target, for example `TodoRow.surf` and `todoRow.surfs` (ADR-0115).
- The names of folders and of assets have no such rule.

### 2.3 Sections of a component file

A component file has two sections, in this order:

1. The **declarations** of the component, in Surf Script. They are also called the script of the component.
2. The **markup**. It is required.

A line that has only `---` separates them. A component without declarations has no `---`: its file is markup only.

```surf
prop label: String = "Clicks";
state count = 0;

---
<Button on:press={count += 1}>{label}: {count}</Button>
```

A component file is a module file with markup at its end. It has no `<script>` tag and no `<style>` section. The styles are utilities in the markup (section 12). A `<script>` tag is a compile error with a help text (ADR-0021).

### 2.4 Comments

The comments are the same in the declarations and in the markup:

| Comment | Meaning |
|---|---|
| `// text` | A comment to the end of the line |
| `/* text */` | A comment over one or more lines. Block comments do not nest. |
| `/// text` | Documentation of the declaration on the next line. At the top of a component file, documentation of the component. The language server shows it. |

In the markup, a comment starts at the start of a line or after a tag: `<Text>http://example.com</Text>` is text. `<!-- -->` is a compile error with a help text. A comment between the attributes of a tag is a compile error: write it on the line above the tag.

---

## 3. Lexical structure

### 3.1 Identifiers

An identifier starts with a letter of any script or `_`. Its other characters are letters, digits, combining marks and `_` (ADR-0076). `$` marks a theme token, and a name cannot start with `$` (ADR-0095).

A name cannot contain `__` (two `_` in a row). The compiler uses these names, so a name of a program never meets a name of the compiler (ADR-0115). This rule is also true for a field, so a JSON key such as `__typename` cannot be a field. `Json.decode` ignores a key that the record does not have.

- Component names, type names and enum names must start with an uppercase letter.
- Variables, functions, fields, props and enum cases start with a lowercase letter. `surf lint` warns about other names (ADR-0070).

### 3.2 Keywords

```
as async await bindable break case const continue derived dyn effect else
emit enum event extern false fn for if impl import in let match module mut
none on prop pub return self Self slot spawn state trait true type var while
```

`var` is reserved. It is not a declaration: use `let` for a variable (ADR-0013).

`case` and `as` are reserved for later versions.

`dyn`, `impl`, `mut`, `self`, `Self` and `trait` are the keywords of traits (section 7.8, ADR-0134) and of trait objects (section 7.9, ADR-0135), as in Rust.

Each keyword is valid as a field name, also the six keywords of traits (ADR-0134). So a record reads the JSON keys `type`, `for` and `self`. A field name is the name in a record type, in a record literal and after `.`. A local variable, a function and a type cannot have a keyword name.

```surf
pub type Entry = { type: String, for: String, self: Bool }

pub fn mine(e: Entry) -> Bool = e.self && e.type == "user";
```

These words are **contextual keywords**. They have a special meaning only in one position. In all other positions they are valid identifiers:

- `component` at the start of a declaration in a component script (section 7.9)
- `motion` at the start of a declaration in a component script
- `sequence` at the start of a declaration in a component script (section 7.5a)
- `cue` at the start of a line in the body of a `sequence` (section 7.5a)
- `mount`, `destroy` and `error` after `on`
- `cleanup` in an effect
- `fallback` in an extern module
- `key` in the head of a `#for` block
- `else`, `empty`, `then` and `catch` after the body of a block in the markup

### 3.3 Literals

| Type | Examples | Rules |
|---|---|---|
| `Int` | `42`, `1_000_000`, `0xFF`, `0b1010` | `_` can separate digits. |
| `Float` | `1.5`, `0.25`, `2e10`, `1.5e-3`, `1f64` | A Float literal must have digits before the point. `.5` is an error. The mark `f64` makes a number a `Float`. |
| `Int64` | `1i64`, `9007199254740993i64` | The mark `i64` makes a number an `Int64` (ADR-0099). A hex or a binary literal has no mark. |
| `String` | `"hello"`, `"{count} items"` | See section 3.4. |
| `Bool` | `true`, `false` | |
| Optional | `none` | The empty value of an optional type. |

### 3.4 Strings

- A string literal is in double quotes.
- `{expression}` inside a string inserts the value of the expression (section 5.4 gives the conversion rules).
- Escapes: `\n`, `\t`, `\r`, `\"`, `\\`, `\{`, `\}`, `\u{1F600}`.
- A string literal cannot contain a raw line end. Use `\n`. Style text is the exception: in `style`, a line end is white space between two items (section 12).

### 3.5 Statement ends

A `;` ends each statement and each declaration on one line (ADR-0094). A line end has no meaning in script, so a statement can use more than one line.

- A form that ends with `}` has no `;`: `fn` with a block, `if`, `for`, `while`, `match`, `effect`, `cleanup`, `on ...`, `type`, `enum` and `extern module`.
- A declaration or a statement that ends with `}` for another reason needs its `;`, for example `const a = { x: 1 };` and `const b = match c { ... };`.
- The last statement of an `on:` value has no need for `;`: `on:press={n += 1}`, `on:press={save(); close()}`.
- `@const` has no `;`. The line end ends a tag.

---

## 4. Types

### 4.1 Overview

| Type | Meaning |
|---|---|
| `Int` | Integer in the range −(2^53 − 1) to 2^53 − 1 (section 4.2, ADR-0010) |
| `Float` | IEEE 754 binary64 number |
| `Bool` | `true` or `false` |
| `String` | Immutable sequence of Unicode scalar values |
| `Bytes` | Immutable sequence of numbers from 0 to 255 (ADR-0037) |
| `Color` | A color that the compiler, `Color.parse`, `Color.rgb` or `Color.mix` checked. Text in quotes is a `Color` where the type says so (ADR-0050, ADR-0160) |
| `Scalar` | Built-in enum with the cases `text(String)`, `number(Float)` and `bool(Bool)`, for a JSON value whose kind only the data gives (section 4.7, ADR-0045) |
| `Void` | The return type of a function that returns no value |
| `List<T>` | Ordered sequence of values of type `T` |
| `Map<K, V>` | Map from keys of type `K` to values of type `V` |
| `T?` | Optional value: a `T` or `none` |
| `Result<T, E>` | Built-in enum with the cases `ok(T)` and `err(E)` |
| `Task<T>` | The result of an `async fn` |
| `Ordering` | Built-in enum with the cases `less`, `equal` and `greater`. The result of `compare` of the trait `Comparable` (section 7.8, ADR-0134) |
| `(A, B) -> R` | Function type |
| Record types | Declared with `type` (section 4.6) |
| Enum types | Declared with `enum` (section 4.7) |
| `dyn Trait` | A trait object: a value of each type with an impl of the trait (section 7.9, ADR-0135) |
| `impl Trait` | The short form of a type parameter with a bound (section 7.9, ADR-0135) |

There is no `any` type, no `null` other than `none`, and no implicit conversion between types.

### 4.2 Int

- The range of `Int` is the same on all targets: −(2^53 − 1) to 2^53 − 1. JavaScript can represent every value in this range exactly.
- A result outside the range is a runtime error ("the result is outside the integer range"). The compiler removes the check where it can prove that the result is in the range.
- `a / b` with two `Int` values gives an `Int`. The result is truncated toward zero.
- `a % b` gives the remainder. The sign of the remainder is the sign of `a`.
- Division or remainder by zero is a runtime error.
- Conversion: `i.toFloat()`, `f.toInt()` (truncation), `f.round()`, `f.floor()`, `f.ceil()`. Each conversion from `Float` gives a runtime error for NaN and for a result outside the `Int` range.
- `Int.parse(s)` gives `Int?`: the value of `s`, or `none`. `s` must be an optional `+` or `-` and the digits `0` to `9`, with no other characters. A value outside the `Int` range gives `none`.
- `Float.parse(s)` gives `Float?`. `s` must be an optional sign, digits with an optional `.` and fraction, and an optional exponent (`e` or `E`, an optional sign, digits). It needs at least one digit before the exponent. White space, `inf`, `NaN` and hexadecimal numbers give `none`, and so does a value that is too large for a `Float`. Use `s.trim()` first for the text of an `Input`.
- `f.round()` rounds a value that is exactly halfway between two integers away from zero: `2.5.round()` is `3`, and `(-2.5).round()` is `-3`. JavaScript `Math.round` rounds `-2.5` to `-2`, so web builds use their own function for `round`.

### 4.3 Float

- `Float` follows IEEE 754 binary64 on all targets. Division by zero gives infinity or NaN, as IEEE 754 specifies.
- Conversion to `String` uses the shortest text that converts back to the same value. The format is the format of ECMAScript `Number.prototype.toString`. Native builds implement the same algorithm, so `"{x}"` gives the same text on all targets.
- An `Int` and a `Float` cannot be mixed in one operation. `1 + 1.5` is a compile error. Write `1.toFloat() + 1.5` or `1.0 + 1.5`.

### 4.4 String

- A `String` is a sequence of Unicode scalar values. Native builds store UTF-8. Web builds store JavaScript strings.
- There is no index operator, because the index of a character is different in UTF-8 and UTF-16.
- Operations count and slice by Unicode scalar values on all targets:
  - `s.count()`, `s.isEmpty()`, `s.slice(from, to)`, `s.chars()`. `chars()` gives a `List<String>` with one string for each scalar value.
  - `s.contains(t)`, `s.startsWith(t)`, `s.endsWith(t)`, `s.split(t)`, `s.trim()`
  - `s.upper()` and `s.lower()` use the Unicode default case mapping, independent of the locale.
- `s.slice(from, to)` keeps each bound between 0 and `s.count()`, and `to` is at least `from`. So `"abc".slice(-1, 9)` is `"abc"`, and `"abc".slice(2, 1)` is `""`.
- `s.split("")` gives one string for each scalar value, as `chars()` does.
- `a + b` joins two strings.
- `<`, `<=`, `>`, `>=` compare by scalar values on the native targets and in the interpreter. The web compares by UTF-16 code units, as JavaScript does, because it is faster (ADR-0113). The same rule applies to `compare` of `String` (section 7.8), `sort` and `sortBy`. The two orders differ only in one case. At the first place where the two strings differ, one character is above U+FFFF and the other is from U+E000 to U+FFFF. Most emoji are above U+FFFF. `docs/native-differences.md` lists this difference.
- `==` and `!=` compare the scalar values on all targets.
- The order of a language comes from `Locale.compare` (section 14), for example in `sortWith` (section 4.5).
- On the web, the runtime replaces each lone surrogate that comes from a browser API with U+FFFD.

### 4.5 List and Map

- A list literal is `[1, 2, 3]`. An empty list needs a known type: `state todos: List<Todo> = []`.
- `xs[i]` reads element `i`. An index outside the list is a runtime error. `xs.get(i)` gives `T?`: `none` for an index outside the list, also for a negative index.
- `xs.slice(from, to)` keeps its bounds in the list, as `String.slice` does (section 4.4).
- Methods that change the list need a place (section 5.2): `push`, `insert`, `remove`, `removeAt`, `clear`, `sort`, `sortBy`, `sortWith`. Each is a statement of its own.
  - `xs.remove(v)` removes the first element that is equal to `v`. Without such an element, the list does not change.
  - `xs.sort()` orders a list by `<`. `xs.sortBy(x => key)` orders any list by a key, for example `todos.sortBy(t => t.title)`. The type of the elements of `sort` and the type of the key must have the trait `Comparable` (section 7.8, ADR-0134).
  - `xs.sortWith((a, b) => order)` orders any list with a compare function of the type `(T, T) -> Ordering` (ADR-0132). The result `.less` puts `a` before `b`. An order that no key gives needs this method, for example the order of a language: `names.sortWith((a, b) => Locale.compare(a, b))` or `users.sortWith((a, b) => Locale.compare(a.name, b.name))`. The argument is a lambda with two parameters, or another function value of this type.
  - The three sorts are stable: equal elements keep their order. For `sortWith`, two elements are equal when the compare function gives `.equal`.
  - Each runtime uses the same stable merge sort for the three sorts, also the web (ADR-0132). So each target gives the same order, also when the order is not consistent. An example is a compare function or an impl of `Comparable` that gives `.less` for `(a, b)` and also for `(b, a)`.
  - The merge sort has this exact form, so each target calls the compare function with the same pairs, in the same order:
    1. `sortBy` gets the key of each element one time, in the order of the list, before the sort starts.
    2. The sort starts with runs of one element. A pass merges each two adjacent runs into one run, from the start of the list to its end. A run at the end with no partner stays as it is. The next pass has runs of double the length. The sort stops when one run holds the whole list.
    3. A merge compares two elements: `a`, the next element of the left run, and `b`, the next element of the right run. It takes `b` only when `b` must come before `a`. Else it takes `a`. When one run has no more elements, the merge takes the rest of the other run.
    4. `b` must come before `a` in these cases. For `sortWith`, the compare function gives `.greater` for `(a, b)`. For `sort` and `sortBy` of a type with an impl of `Comparable`, `compare` gives `.greater` for `(a, b)`, or for their keys. For the other types, `b < a`, or the key of `b` `<` the key of `a`. So `NaN` never comes before another value in a merge.
- Methods that return a new value: `map`, `filter`, `find`, `count`, `isEmpty`, `contains`, `first`, `last`, `get`, `slice`. A loop gets the index of each element with `for item, index in xs` (section 6.1).
- `remove`, `contains` and `indexOf` compare with `==`. A list of `dyn` values cannot use them, because a `dyn` value has no `==` (section 5.1, ADR-0135).
- A map literal is `Map { "a": 1, "b": 2 }`. The key type must have the trait `Hashable` (section 7.8, ADR-0134): `Int`, `String`, `Bool`, or a record or an enum whose fields all have `Hashable`. A `dyn` type cannot be a key (section 7.9).
- `m[k]` reads a value of type `V?`. `m[k] = v` adds or replaces a value. A new key goes to the end. A replaced value keeps the position of its key.
- Map methods: `count`, `isEmpty`, `contains(k)`, `keys()` and `values()` return values. `remove(k)` and `clear()` change the map and need a place.
- Two maps are equal when they have the same keys with equal values. The order of the keys does not count.
- Iteration over a map follows the order in which the keys were first added.

### 4.6 Records

```surf
type Todo = { id: Int, title: String, done: Bool = false }
```

- A record type is nominal. Two record types with the same fields are different types.
- A field can have a default value.
- Two fields with one name are a compile error.
- A record cannot hold a value of its own type, also through an optional value, a tuple or another record or enum: `type Node = { next: Node? }` is a compile error. A `List` or a `Map` can hold it: `type Node = { children: List<Node> }`. A native build gives each field a fixed size.
- Literal: `Todo { id: 1, title: "Buy milk" }`. When the expected type is known, the type name can be left out: `{ id: 1, title: "Buy milk" }`.
- Copy with changes: `{ ...todo, done: true }`.

### 4.7 Enums

```surf
enum Tone { info, warning, error }

enum Shape { circle(radius: Float), rect(width: Float, height: Float) }
```

- Cases start with a lowercase letter. Two cases with one name, and two fields of a case with one name, are compile errors.
- The field of a case cannot hold its own enum outside a `List` or a `Map`, as for a record (4.6).
- A case is written `Shape.circle(radius: 2.0)`, or `.circle(radius: 2.0)` when the expected type is known.
- A prop with an enum type takes a case: `tone={.warning}`. The prop gives the enum, so the parent needs no import of it. Text is a compile error (ADR-0099).
- `Scalar` is a built-in enum (ADR-0045). `Json.decode` takes its case from the value, and `value.text()` gives its text.

### 4.8 Optional values

- `T?` is a `T` or `none`.
- An optional of an optional is a compile error. The web target has one `none`, so it cannot keep the two levels apart. `xs.get(i)`, `xs.first()`, `xs.last()`, `xs.find(f)` and `m[k]` of a list or a map of optional values are compile errors: check the index or the key first, or use a list without `none`.
- `a?.b` gives `none` if `a` is `none`. `a?.b(c)` calls a method only when `a` has a value (ADR-0052).
- `a ?? b` gives `b` if `a` is `none`. With an optional `b`, the result is an optional: `m[k] ?? none`.
- `if let x = opt { ... }` runs the block only when `opt` has a value.

### 4.9 Result and errors

- `Result<T, E>` is a built-in enum with the cases `ok(T)` and `err(E)`.
- A `Result` that nothing reads is a compile error. `const _ = save()` drops it (ADR-0052).
- In a function that returns `Result<_, E>`, the postfix operator `?` returns the `err` value at once, or gives the `ok` value.
- There are no exceptions. A bug that cannot continue uses `panic("message")`, which is a runtime error.

### 4.10 Type inference

- The compiler infers the types of `const`, `let`, `state` and `derived` declarations.
- These must have explicit types: `prop` declarations, function parameters, the return types of `pub` functions, record fields and extern declarations.

---

## 5. Values

### 5.1 Value semantics

Every value in Surf Script is a value, not a reference (ADR-0009).

- An assignment, a function argument and a prop give the receiver its own copy.
- A change to a copy does not change the original.
- `==` compares two values field by field and element by element. Function values cannot be compared. `f == g` is a compile error.
- An impl of the trait `Equatable` replaces this comparison for its type (section 7.8, ADR-0134).
- A `dyn` value cannot be compared. `a == b` on two `dyn Shape` values is a compile error, because `Equatable` cannot be `dyn` (section 7.9, ADR-0135).
- There is no reference identity in the language.

Implementations do not copy at every assignment. Native builds use reference counting with copy-on-write. Web builds insert copies only where a change through one name could be visible through another name. Because there are no references, values cannot form cycles, and reference counting never leaks memory.

### 5.2 Places

A **place** is an expression that can receive a value:

- a `let` variable
- a `state` or a `bindable prop` (section 7)
- a field of a place: `todo.title`
- an element of a list place: `todos[i]`
- an entry of a map place: `scores["ana"]`
- the variable of a `#for` block over a place (section 9.5)

Assignments (`=`, `+=`, `-=`, `*=`, `/=`, `%=`) and the changing methods of lists and maps need a place on the left side.

### 5.3 Equality of primitive writes

A write of an `Int`, `Float`, `Bool` or `String` value that is equal to the current value does not count as a change (section 10.4). A write of a list, map, record or enum value always counts as a change.

### 5.4 Conversion to text

`{expression}` in a string or in markup text accepts each type with the trait `Text` (section 7.8, ADR-0134). `String`, `Int`, `Float` and `Bool` have it. For another type, `{x}` gives the result of `x.text()`. Other types are a compile error.

---

## 6. Statements and expressions

### 6.1 Statements

| Statement | Example |
|---|---|
| Constant | `const total = a + b` |
| Variable | `let i = 0` |
| Assignment | `count += 1` |
| Condition | `if x > 0 { ... } else if x < 0 { ... } else { ... }` |
| Optional test | `if let user = maybeUser { ... }` |
| List loop | `for todo in todos { ... }` |
| Loop with index | `for todo, i in todos { ... }` |
| Map loop | `for key, value in scores { ... }` |
| Range loop | `for i in 0..10 { ... }` (excludes 10), `for i in 0..=10 { ... }` (includes 10) |
| Condition loop | `while queue.count() > 0 { ... }` |
| Loop control | `break`, `continue` |
| Return | `return value` |
| Event | `emit submit(draft)` (section 7.6) |
| Background task | `spawn load()` (section 7.5) |

An assignment is a statement, not an expression. `a = b = 0` is a compile error.

A `for` loop over a list iterates over the list as it was when the loop started (section 5.1):

- The loop variable is a copy of the element. A write to the list in the body does not change the loop variable.
- A write to the list in the body does not change the number of iterations or the elements that the loop sees. Examples of such writes are `todos.push(x)` and `todos[0].done = true`. The next statement after the loop sees all the writes.
- A `for` loop over a map follows the same rule.

```surf
for todo in todos {
    todos.push({ ...todo, id: nextId() }); // runs once for each element of the original list
}
```

### 6.2 Operators

From the highest to the lowest precedence:

| Level | Operators | Notes |
|---|---|---|
| 1 | `.` `?.` `()` `[]` postfix `?` | Member, call, index, error propagation |
| 2 | prefix `-` `!` | |
| 3 | `*` `/` `%` | |
| 4 | `+` `-` | `+` also joins strings |
| 5 | `..` `..=` | Ranges of `Int` |
| 6 | `<` `<=` `>` `>=` | |
| 7 | `==` `!=` | Value equality (section 5.1) |
| 8 | `&&` | |
| 9 | `\|\|` | |
| 10 | `??` | |
| 11 | `cond ? a : b` | |
| 12 | `=>` | Lambda |

Binary operators at the same level group from left to right. `??`, `? :` and `=>` group from right to left.

A type with an impl of an operator trait can use the operator of the trait (section 7.8, ADR-0134). For example, a record `Vector` can use `a + b`. The operators of levels 6 and 7 come from `Comparable` and `Equatable`.

### 6.3 Match

```surf
const area = match shape {
    .circle(r) => 3.14159 * r * r,
    .rect(w, h) => w * h,
};
```

- `match` is an expression.
- Each arm ends with `,`. The last arm can leave it out (ADR-0094).
- Patterns: literals, enum cases with bindings, `none`, and `_` for any value.
- A `match` must cover every case. A missing case is a compile error.

### 6.4 Lambdas

- `x => x * 2`
- `(a, b) => a + b`
- `(g) => { ...statements... }`

A lambda captures `const` values and the reactive sources of its component (section 10.1). A lambda cannot capture a `let` variable. This rule prevents shared mutable state between a function and its creator. The lambda of `filter`, `find`, `map`, `sortBy` and `sortWith` is an exception: it runs before the call returns, so it can read a `let` variable.

The body of a lambda cannot be an assignment: `x => total += x` is a compile error. Use a block: `x => { total += x; }`. An `on:` attribute is different: its value is statements (section 9.2).

A function value is a value (ADR-0039). It can be a local constant, a parameter, a result, a field of a record, an item of a list or a map. It can also be a `state`, a `derived` or a prop. A module function is a value by its name. Each expression of a function type can be called: `f(1)`, `action.run(1)`, `fs[0](1)`, `adder(1)(2)`.

- A function value only reads. It cannot write to a `state`, call a function of a component or use `await`. Its block can write its own `let` variables. A child tells its parent with an event (section 9.2).
- A lambda with a block needs a known type: `const f: (Int) -> Int = x => { return x + 1; }`. Each path of a block with a result must return.
- A function value is not a key of a `Map`. JSON and extern modules refuse it. An `async fn` is not a function value.

### 6.5 Calls and named arguments

```surf
fn circle(x: Float, y: Float, r: Float, fill: Color = "#007aff", stroke: Color? = none) {
    // ...
}

fn dot(x: Float, y: Float) {
    circle(x, y, 3.0, fill: "#ff9500");
}
```

- Parameters without a default value are positional.
- Parameters with a default value must be named in the call.

---

## 7. Declarations

### 7.1 Where each declaration is valid

| Declaration | Module file | Component script | Inside a function |
|---|---|---|---|
| `import` | yes | yes | no |
| `const` | yes (module constant) | yes (instance constant, which can read the props. `pub const` is a module constant) | yes (local constant) |
| `type`, `enum` | yes | yes | no |
| `fn`, `async fn` | yes | yes | no |
| `trait`, `impl` (section 7.8) | yes | no | no |
| `component` (section 7.9) | no | yes | no |
| `extern module` | yes | no | no |
| `prop`, `bindable prop` | no | yes | no |
| `state`, `derived`, `motion`, `effect` | no | yes | no |
| `sequence` (section 7.5a) | no | yes | no |
| `event`, `slot` | no | yes | no |
| `on mount`, `on destroy` | no | yes | no |
| `let` | no | no | yes |

A component script has no `let`. Mutable component data must be `state`, so every change is visible to the reactivity model.

### 7.2 Imports and visibility

```surf
import Counter from "./Counter.surf";
import { Todo, nextId } from "./model.surfs";
import { Haptics } from "pkg:haptics";
import close from "./icons/close.svg";
```

- A path is relative to the importing file and must end with `.surf`, `.surfs` or `.svg`. A `pkg:` path names a package. A `~/` path starts at the folder `src`, for example `~/model.surfs`. `surf:wave`, `surf:test` and `surf:ui` are built-in modules.
- An import of an `.svg` file gives a constant of the built-in type `Icon` with the name of the import (ADR-0030). The compiler converts the file: a missing file and a part of the file that an icon cannot have are compile errors.
- The files of a project can be in subfolders of its folder. A path starts with `./` (the folder of the importing file) or `../` (the folder above it), for example `./parts/Row.surf` or `../model.surfs`. A path that leaves the folder of the project is a compile error.
- A component has the name of its file, without the folders. Two components with the same name in one project are a compile error, also in different folders.
- A value of a record type of another file needs an import of the type. This is also true for the value of a prop of a child component.
- `pkg:name` imports `lib.surfs` of a package, and `pkg:name/x.surfs` imports another file of it. `surf.toml` names the packages, and `surf install` copies each one to `src/pkg/<name>` (ADR-0071). A `pkg:` path to a package that is not there is a compile error.
- The module `surf:test` has the functions of `surf test` (ADR-0069). `surf test` adds this module, so a build has no `surf:test`.
- The module `surf:ui` has the standard components (component reference, section 5, ADR-0102). A named import takes each component and each type: `import { Dialog, Tabs, UiItem } from "surf:ui";`. Only the components of the import go into a build.
- A component file exports only its component. The other declarations in a component script are private.
- A module file exports only the declarations that start with `pub`.
- Built-in components (section 9.6), `Platform`, `Focus`, `Result`, `Task`, `Pointer`, `Icon`, `ThemeMode` and the theme tokens need no import.
- The built-in traits and `Ordering` (section 7.8), and `Locale` and its enums (section 14), need no import.
- A call of a method of a trait of another file needs an import of the trait (section 7.8, ADR-0134). A method of a plain impl needs no import.
- An import cycle is a compile error in version 0.

### 7.3 Constants and functions

```surf
const MAX_ITEMS = 100;

pub fn plural(n: Int, word: String) -> String {
    return n == 1 ? "1 {word}" : "{n} {word}s";
}

fn double(x: Int) -> Int = x * 2;
```

- A `const` value is an expression of literals, other constants and calls of functions.
- A module constant has one value. Each read gives the same value, also when the value comes from a call such as `Random.float()` (ADR-0118).
- A constant with a value that is not a literal gets its value at its first read, on all targets (ADR-0131). A server has one value for the process. Not in version 0: the web computes the value when the page loads, and a server has one value for each thread (see [native differences](https://surface.solenovis.com/docs/targets.md)).
- `fn name(...) -> T = expression` is the short form of a function with one expression.
- In a function with a return type other than `Void`, every path through the body must end with `return`. A path that reaches the end of the body without `return` is a compile error. For this check:
  - An `if` with an `else` returns when both branches return. An `if` without an `else` does not return.
  - A loop does not return, because its body can run zero times.
  - A call of `panic(message)` as a statement returns, because it stops the batch (section 15.2).
- A `Void` function can end without `return`.
- A function in a module file cannot read the reactive sources of a component. It gets the data of a component through its parameters. It can read `Locale` (section 14, ADR-0132).
- A module constant cannot read `Locale`, also through a call (ADR-0132). A module constant has one value, so it cannot follow `Locale.set`. This is a compile error. The message says to use a function in place of the constant, for example `fn title() -> String`. An instance constant of a component that reads `Locale` is also a compile error, and its message says to use a `derived` value.

**Generic functions**

```surf
pub fn first<T>(items: List<T>) -> T? {
    return items.get(0);
}
```

- A function in a module file can have type parameters. A component file cannot have such a function (ADR-0080). A generic component has type parameters of its own (section 7.9).
- A call has no list of types. The values of the call give the types, so each type parameter must appear in a parameter.
- The body can copy a value of a type parameter and put it in a list. Other operations on it are compile errors.
- A type parameter can have a bound: `<T: Comparable>`. The body can then use the methods and the operators of the traits of the bound (section 7.9, ADR-0135).
- `==` and `!=` on a value of a type parameter need the bound `T: Equatable`. `contains`, `indexOf` and `remove` on a list of such values need it too (section 4.5). A function type and a `dyn` type have no `Equatable`, so a call cannot give them to such a type parameter (section 7.9, ADR-0135).
- A generic function is a value where the expected type gives each type parameter, as a function of Rust: `const pick: (List<Int>) -> Int = largest;`. The type of a parameter of a call can also give them: `apply([1, 3], largest)`. Without such a type, a generic function as a value is a compile error. An `async fn` is not a value.

**Generic records and enums, and tuples**

```surf
pub type Box<T> = { value: T }
pub enum Either<L, R> { left(value: L), right(value: R) }

fn sample() {
    const boxed: Box<Int> = { value: 7 };
    const pair = (1, "one");
    const (n, word) = pair;
}
```

- A record and an enum can have type parameters (ADR-0087). A type parameter of a record or an enum can have a bound too (section 7.9). Each use of the type must give a type with the traits of the bound.
- `(Int, String)` is a tuple type with two to four values. `(a, b)` makes a tuple, and `t.0` reads a value.
- `const (a, b) = value` reads the values of a tuple.

### 7.4 Props

```surf
prop label: String = "Clicks";
prop items: List<Todo>;
bindable prop value: String = "";
```

- A prop without a default value is required. A missing required prop is a compile error in the parent.
- A component cannot assign to a normal prop.
- A component can assign to a `bindable prop`. If the parent used `bind:value={place}`, the assignment also changes the parent place. Without `bind:`, the assignment changes only the local value until the parent sends a new value.

### 7.5 State, derived values and effects

```surf
state count = 0;
state todos: List<Todo> = [];
derived remaining = todos.filter(t => !t.done).count();

effect {
    const r = room;
    Chat.join(r);
    cleanup {
        Chat.leave(r);
    }
}

on change(filter) {
    page = 0;
}
on mount {
    focusFirstField();
}
on destroy {
    saveDraft();
}
```

- A `state` initializer runs once, when the component instance is created. It can read props, but it does not follow later changes of the props.
- The initial values of `state`, of an instance `const` and of a prop cannot call a function of the component. The state of the component does not exist yet at that time. A module function can get the props as arguments: `state items = makeItems(count)`. A `derived` value can call a function of the component.
- An initial value can read a `derived` value that comes before it. It then reads the expression of that value, so the rule above applies to that expression too (ADR-0104).
- A `derived` value is an expression. It must not write a reactive source. A write is a compile error.
- A `derived` value is calculated when something reads it, and it stays cached until one of its dependencies changes.
- An `effect` body runs after the first render and again after every batch that changes one of its dependencies (section 10.5).
- `cleanup { ... }` inside an effect runs before the next run of the effect and when the component is destroyed. It is valid only at the top level of the effect body, at most once. It can use the locals that the effect declares before it, with their values at the time when the effect reached `cleanup`.
- An effect body is synchronous. It can start an async function with `spawn f()`. The runtime cancels the task when the effect runs again or when the component is destroyed. A task that `on change` starts goes on after the next change, as a task of an event handler does.
- `on mount` runs once, after the first frame that shows the component. `on destroy` runs once, when the component leaves the tree.

**Values that move in time**

```surf
state open = false;
motion x(ms: 400, ease: "ease-spring") = open ? 240.0 : 0.0;

---
<Column style="translate-x-{x}px" />
```

`motion` declares a value that moves to its target in time (ADR-0104). The expression after `=` is the target, as the expression of a `derived` value is. An app sets a target with a write to a `state` or with a new prop.

- The value is a `Float`, and each consumer reads it as it reads a `state`: a computed style, a `derived` value, an effect or a text. The target must be a `Float`.
- `ms` is the time in milliseconds. `ease` is the name of an easing utility of the layout spec, section 12.4: `"ease"`, `"ease-linear"`, `"ease-out"`, `"ease-out-cubic"`, `"ease-spring"` or `"ease-spring-0.4"` (ADR-0104). A reader can then copy the name from a `style`. Both are optional and both are fixed in the source. The defaults are 150 and `"ease"`.
- A name without `ease-`, for example `"spring"` or `"linear"`, is a compile error. The message gives the correct name, for example `"ease-spring"`. The prop `ease` of `Transition` has the same rule (component reference 4.15).
- The first value is the first target, with no movement. The target follows the rule of an initial value of a `state`: it cannot call a function of the component.
- When the target changes, the value starts at its shown value. With a spring, it keeps its speed. With another easing, it starts with a speed of 0 and takes the whole time.
- At the end of the time, the value is exactly the target. During the time, the value has at most 4 digits after the point, so that all targets show the same numbers.
- A `motion` value cannot take a write. Write the state that its target reads.
- The value changes once for each frame while it moves. Each change is a batch (section 10.5). A value that does not move uses no frames.
- With the setting of reduced motion on, the value goes to a new target at once (layout spec 5.10).
- The runtime stops a moving value when its component leaves the tree, as it cancels a task.

### 7.5a Sequences

```surf
state ready = false;

sequence intro(play: ready) {
    cue logo(ms: 400) = "fade";
    cue title(gap: -200, stagger: 30) = "rise";
    cue more(gap: -100) = "fade";
}

on mount {
    ready = true;
}

---
<Column style="gap-4 items-center">
    <Image src="assets/logo.svg" alt="Surface" style="cue-logo" />
    <Text style="text-48 split-letters cue-title">One universal best option</Text>
    <Button style="cue-more">Read more</Button>
</Column>
```

A `sequence` orders the animations of several elements on one timeline (ADR-0152). It is the timeline form of version 1. Each `cue` is one animation of the sequence. An element plays a cue with the utility `cue-<name>` (layout spec 5.10).

- `sequence` and `cue` are contextual keywords. `sequence` starts a declaration in a component script, and `cue` starts a line in the body of a sequence. Sections 3.2, 7.1 and 16 have these words.
- A sequence is valid only in a component script. Its name is unique among the declarations of the component.
- `cue <name>(<arguments>) = "<keyframes>";` declares a cue. The text after `=` is a name of `keyframes` of `style.toml`, or of a built-in animation (layout spec 2.1). An unknown name is a compile error.
- The arguments of a cue have fixed values in the source. Each argument is optional:

| Argument | Meaning | Default |
|---|---|---|
| `ms` | The time of the cue for one element, in ms | The `ms` of the keyframes |
| `ease` | The name of an easing utility, as `ease` of `motion` (section 7.5) | The `ease` of the keyframes |
| `gap` | The time from the end of the cue before, in ms. A negative value starts the cue earlier, so the two cues overlap | `0` |
| `with` | The name of an earlier cue of the sequence, for example `with: logo`. The cue then starts at the start of that cue, plus `gap` | none |
| `stagger` | The extra time for each element of the cue, in ms | `0` |
| `from` | The first element of the stagger: `"start"`, `"end"` or `"center"` | `"start"` |

**The elements of a cue.**

- The elements of a cue are the elements of the component with its utility `cue-<name>`, in tree order. The `style` of an instance gives the utility to the root elements of the instance (section 12).
- The parts of a split text are elements of the cue, in the order of the text (layout spec 5.10).
- The names of the cues are unique in a component, also across two sequences. A `cue-<name>` that names no cue of the component is a compile error.

**The places.**

- The first cue starts at its `gap`. Each other cue starts at the end of the cue before, plus its `gap`. With `with`, it starts at the start of that cue, plus its `gap`. A start below 0 is 0.
- Element `i` of `n` starts at the start of its cue, plus an extra time. The extra time follows the table of `Transition` (component reference 4.15). It is `i * stagger` from the start and `(n - 1 - i) * stagger` from the end. From the center, it is `floor(abs(2 * i - (n - 1)) / 2) * stagger`.
- The end of a cue is its start, plus `ms`, plus the largest extra time of its elements. A cue with no elements keeps its `ms`. The length of the sequence is the latest end of its cues.
- When the count of the elements of a cue changes, the places change with it. A new element takes the current time of its sequence.
- Before its start, an element of a cue shows the first stop of its keyframes. After its end, it shows the last stop (layout spec 5.10, ADR-0150).

**The timeline of a sequence.** A sequence has one of two timelines:

- `play: <Bool>` runs the sequence in time. The value has the dependencies of a `derived` value. While it is true, the sequence runs to its end. While it is false, the sequence runs back to its start. A change during a run turns the sequence at its current time.
- At the mount, a sequence with `play` true starts from its start, as a CSS animation that applies at the start does. With `play` false, it waits at its start.
- `timeline: "<name>"` runs the sequence with the scroll. The name is a scroll timeline or a view timeline of the same file (layout spec 5.10). The optional `range: "<range>"` has the text of `animate-range-`, for example `"contain"`. Without it, the sequence uses the whole timeline.
- With a `timeline`, the start of the range is the time 0, and the end of the range is the length of the sequence. The range uses one range name at both ends, or offsets alone. So each cue is a part of one range. Another range is a compile error.
- A sequence with both `play` and `timeline`, or with neither, is a compile error.

**Other rules.**

- With the setting of reduced motion on, a sequence with `play` goes to its end or to its start at once. A `motion` value does the same with its target. A sequence with a `timeline` does not run, and each element shows its own style.
- The runtime stops a sequence when its component leaves the tree.
- Each feature of this section works on the web, on the native targets and in the interpreter.

How a sequence relates to the other forms:

| Form | Task |
|---|---|
| `transition` | A change of the style of one element |
| `Transition` | An element that a block adds or removes. Its `stagger` is for a batch of elements that come in one frame |
| `animate-<name>` | The keyframes of one element on its own timeline, with `animate-delay-` (layout spec 5.10) |
| `motion` | One number that moves to a target (section 7.5) |
| `sequence` | An order of several animations on one timeline, with gaps, overlaps and a stagger |

A row that knows its index uses `delay-{i * 40}` or `animate-delay-{i * 40}`. The stagger of a cue is for elements that have no index in the source, for example the parts of a split text.

### 7.6 Events

```surf
event submit(text: String);

fn send() {
    emit submit(draft);
}
```

The parent listens with `on:submit={handler}`. An event without a listener does nothing. Event handlers can be `async` functions. The runtime cancels a running handler task when its component is destroyed.

An event that a component sends while it leaves the tree, in `on destroy` or in a cleanup, does not reach the parent. The parent is in step 3 of a batch then (section 10.5), and its handlers cannot run.

### 7.7 Slots

A slot is a part of a component that the parent fills with markup.

Declaration in the child:

```surf
slot default;
slot header?;
slot row(bindable item: Todo);
```

Use in the child markup:

```surf
<Column>
    @slot header
    #for item in items key item.id {
        @slot row(item)
    }
    @slot default
</Column>
```

Fill in the parent:

```surf
<TodoTable bind:items={todos}>
    #fill header { <Text style="text-20 font-600">Tasks</Text> }
    #fill row(todo) { <Toggle bind:checked={todo.done}>{todo.title}</Toggle> }
    <Text>Markup outside a fill goes into the default slot.</Text>
</TodoTable>
```

- `TodoTable` declares `bindable prop items: List<Todo>`, and the parent binds it. A write in the fill then changes `todos` in the parent.
- A slot name with `?` is optional. An unfilled optional slot renders nothing.
- A required slot without a fill is a compile error in the parent.
- A slot can have parameters. The fill receives the values from the child.
- A slot parameter with `bindable` (`slot row(bindable item: Todo)`) must receive a place from the child. In the fill, the parameter is then also a place, and a write to it changes the original value. Without `bindable`, a fill parameter is a constant.
- The built-in `List` declares `slot row(bindable item: T)`. When `items` is a place, each row can write to its item.

### 7.8 Traits and impl blocks

A **trait** names methods that a type can have. An **impl block** gives the methods to a type (ADR-0134). Traits have the form of Rust.

```surf
// shapes.surfs
pub trait Shape {
    fn area(self) -> Float;
    fn describe(self) -> String = "a shape with the area {self.area()}";
}

pub type Circle = { r: Float }
pub type Square = { side: Float }

impl Shape for Circle {
    fn area(self) -> Float = 3.14 * self.r * self.r;
}

impl Shape for Square {
    fn area(self) -> Float = self.side * self.side;
}

impl Circle {
    pub fn new(r: Float) -> Circle = { r: r };

    pub fn grow(mut self, k: Float) {
        self.r *= k;
    }
}
```

**Methods and associated items**

- A method has `self` as its first parameter. `self` is the value, and `Self` is its type. A call is `c.area()`.
- A method with `mut self` changes its value. Its call needs a place (section 5.2), for example `shape.grow(2.0)` on a state. The call is a statement of its own, as `xs.push(1)` is. A method without `mut` cannot write to `self`.
- A function with no `self` is an **associated function**. A call names the type, for example `Circle.new(2.0)`. For a trait with `fn zero() -> Self`, the calls are `Int.zero()`, and `T.zero()` in a generic function. A plain impl can have associated functions too, for example a constructor.
- A method of a trait can have a default body. An impl can replace it.
- A trait can have an **associated type**: `type Item;`. Each impl gives it a type: `type Item = Int;`. A signature names it `Self.Item`, and a generic function names it `T.Item`.
- An impl of a trait must have each method of the trait that has no default body, with the same signature. It cannot have other items. Two impls of one trait for one type are a compile error.

**Where an impl is valid**

- Traits and impl blocks are valid only in a module file (section 7.1). A method is a function of a module file, so it cannot read the reactive sources of a component (section 7.3). It can read `Locale`, as each function can (section 10.2).
- An impl of a trait is valid only in the module of the trait or in the module of the type. A plain impl is valid only in the module of the type. This is the orphan rule of Rust. Rust applies it to a crate, and Surf applies it to a module.
- A module exports a trait that starts with `pub`. A method of a plain impl is visible outside its module only when it starts with `pub`. A method of an impl of a trait has the visibility of the trait.
- An impl can have type parameters: `impl<T: Text> Text for Page<T> { ... }` (section 7.9).

**The import of a trait**

A file calls a method of a trait only when it imports the trait, as in Rust (ADR-0134). A plain impl needs no import.

```surf
// App.surf
import { Circle, Shape } from "./shapes.surfs";

derived area = Circle.new(2.0).area();
```

- `Circle.new` is a function of a plain impl, so it needs no import. `area` is a method of `Shape`, so the file imports `Shape`.
- The file that declares the trait needs no import.
- An associated function of a trait needs the import too, for example `Int.zero()` for a trait `Zero` of another file.
- A call through a bound (`T: Shape`) or on a `dyn Shape` value needs no import, as in Rust. The bound or the type names the trait.
- The built-in traits need no import (section 7.2).
- A call without the import is a compile error. The message names the trait, and its help gives the import with the path from this file:

  ```text
  error: `area` is a method of the trait `Shape`, and this file does not import the trait
     = help: add `import { Shape } from "./shapes.surfs";` to this file
  ```

  For an associated function, the message says "function" in place of "method".

**Built-in traits**

| Trait | Method | Gives | Built-in types with an impl |
|---|---|---|---|
| `Equatable` | `equals(self, other: Self) -> Bool` | `==` and `!=` | each type that `==` takes (section 5.1) |
| `Comparable` | `compare(self, other: Self) -> Ordering` | `<`, `<=`, `>`, `>=`, `sort` and the key of `sortBy` | `Int`, `Int64`, `Float` and `String` |
| `Hashable` | `hash(self) -> Int` | the key of a `Map` (section 4.5) | `Int`, `String` and `Bool` |
| `Text` | `text(self) -> String` | `{x}` in a string and in the markup (section 5.4) | `String`, `Int`, `Float` and `Bool` |
| `Number` | none | the operators of numbers | `Int` and `Float` |
| `Token` | none | the value of `Theme.setToken` (ADR-0082, ADR-0130) | `Color` and `Float` |

- `Equatable` and `Hashable` are automatic for records and enums. The automatic `==` compares each field (section 5.1). A record or an enum has the automatic `Hashable` when each of its fields has `Hashable`, as `#[derive(Hash)]` in Rust.
- A record or an enum with a field whose type holds a `dyn` type gets no automatic `Equatable` and no automatic `Hashable` (ADR-0135). Examples of such fields are `shape: dyn Shape` and `shapes: List<dyn Shape>`. A program writes an impl when it needs one.
- An impl of `Equatable` replaces the automatic comparison. A type with an impl of `Equatable` must also have an impl of `Hashable` to be a map key. The compiler checks this rule.
- `Comparable` and `Text` need an impl. A record or an enum has no automatic impl of them.
- `Comparable` includes `Equatable` (ADR-0135). A type with an impl of `Comparable` must have `Equatable`, from an impl or from the automatic `==`. A bound `T: Comparable` gives `==` and `!=` too.
- `<`, `<=`, `>` and `>=` use the result of `compare`. `Ordering` is a built-in enum with the cases `less`, `equal` and `greater`.
- A bound `T: Number` gives `+`, `-`, `*`, `/`, `%` and prefix `-` on values of `T`, and the traits `Comparable` (with `Equatable`) and `Text`.
- A program cannot write an impl of `Number` or of `Token`.

**Operator traits**

| Trait | Operator | Method |
|---|---|---|
| `Add` | `a + b` | `add(self, other: Rhs) -> Self.Output` |
| `Sub` | `a - b` | `sub(self, other: Rhs) -> Self.Output` |
| `Mul` | `a * b` | `mul(self, other: Rhs) -> Self.Output` |
| `Div` | `a / b` | `div(self, other: Rhs) -> Self.Output` |
| `Rem` | `a % b` | `rem(self, other: Rhs) -> Self.Output` |
| `Neg` | `-a` | `neg(self) -> Self.Output` |

- Each operator trait has the form of its Rust trait. `Rhs` is the type of the right side. It is `Self`, unless the impl names another type: `impl Mul<Float> for Vector`. The associated type `Output` is the type of the result.
- Each built-in type with an operator in section 6.2 has an impl of the trait of that operator, for example `Add` for `Int` and for `String`.
- `a += b` is `a = a + b`, and the result must have the type of the place. The other assignment operators of section 5.2 follow the same rule.

```surf
pub type Vector = { x: Float, y: Float }

impl Add for Vector {
    type Output = Vector;
    fn add(self, other: Vector) -> Vector = { x: self.x + other.x, y: self.y + other.y };
}

impl Mul<Float> for Vector {
    type Output = Vector;
    fn mul(self, k: Float) -> Vector = { x: self.x * k, y: self.y * k };
}

impl Text for Vector {
    fn text(self) -> String = "({self.x}, {self.y})";
}
```

Each feature of this section works on the web, on the native targets and in the interpreter.

A method cannot be an `async fn`.

### 7.9 Type parameters with bounds, and trait objects

**Bounds**

```surf
pub fn largest<T: Comparable>(xs: List<T>) -> T {
    let best = xs[0];
    for x in xs {
        if x > best {
            best = x;
        }
    }
    return best;
}

pub fn label(value: impl Text) -> String = "Value: {value}";

pub type Page<T> = { items: List<T>, next: String? }
```

- A function, a record, an enum and a component can have type parameters with bounds (ADR-0135). A bound names one or more traits: `<T: Comparable + Text>`.
- The type of a type parameter must have an impl of each trait of its bound. Another type is a compile error at the call or at the use.
- `x: impl Token` is the short form of a type parameter with a bound. It is valid for a parameter and for a return type. Each `impl` in a parameter is a type parameter of its own.
- Use the long form when two parameters, or a parameter and the return type, have the same type: `fn larger<T: Comparable>(a: T, b: T) -> T`.
- A return type `impl Shape` is one type that the function chooses. Each `return` of the body must give that type. The caller sees only the methods of the trait.
- An `impl` return type has only the traits of its bound. Each `impl` return type is a type of its own, also when two functions have the same bound. A generic function with an `impl` return type gives one type for each list of types of its calls. A method of a trait cannot return an `impl` type.
- Dispatch is static. The compiler makes one copy of a generic function for each list of types that the app uses (ADR-0080). So an app pays only for what it uses.
- A call through a bound needs no import, because the bound names the trait (section 7.8). A file that writes the bound `T: Shape` names the trait, so it declares or imports `Shape`.
- In a generic function, `T.zero()` calls an associated function of a trait of the bound of `T`, and `T.Item` names an associated type of such a trait (section 7.8). `T: Mul<Float>` gives `a * k` with `k: Float`, and the result has the type `T.Output`.

**Impls and methods with type parameters**

```surf
pub type Page<T> = { items: List<T> }

impl<T: Text> Text for Page<T> {
    fn text(self) -> String = "{self.items.count()} items";
}

impl<T> Page<T> {
    pub fn first(self) -> T? = self.items.get(0);
    pub fn map<U>(self, f: (T) -> U) -> Page<U> = { items: self.items.map(f) };
}
```

- An impl with type parameters is for a generic record or enum. Each type parameter of the impl is a type argument of the type, once: `impl<T> Page<T>`.
- A type has the methods of such an impl when its type arguments have the traits of the bounds of the impl. `Page<User>` has the method `text` only when `User` has `Text`.
- Two impls of one trait for one generic type are a compile error, also with different bounds.
- A method can have type parameters of its own, for example `U` of `map`. A method of a trait with type parameters has the same type parameters and bounds in each impl, in their order. Their names can be different.
- The compiler makes one copy of each method of such an impl for each list of types that the app uses.
- A call of an associated function of such an impl names the generic type without its types: `Page.empty()`. The expected type of the result and the values of the call give the types, as for a generic function (section 7.3): `const p: Page<Int> = Page.empty();`.

**Generic components**

```surf
// Picker.surf
component Picker<T: Text>;

prop items: List<T>;
bindable prop selected: T? = none;

---
<Column style="gap-1">
    #for item in items {
        <Button on:press={selected = item}>{item}</Button>
    }
</Column>
```

```surf
<Picker items={users} bind:selected={chosen} />
<Picker<User> items={users} />
```

- `component Name<...>;` declares the type parameters of a component. It is valid only in the script of a component file, and its name must be the name of the file. A component without type parameters has no such declaration.
- The root component, `App.surf` or a page in `routes`, cannot have type parameters, because no parent gives their types.
- Each declaration of the script can use the type parameters, for example a prop, a state or a function.
- The checker infers the types of each use from its props. A use can also name them after the name of the component: `<Picker<User> items={users} />`. The end tag has no types: `</Picker>`.
- A prop gives a type when its value has a type without the help of the prop. A lambda without the types of its parameters and an empty list give no type. Then the use names the types.
- The types of a use have the traits of the bounds. The props, the events and the slots of the use have these types.
- The compiler makes one copy of a generic component for each list of types that the app uses, as for a generic function. The copies have the style rules of the component once.

**Trait objects**

```surf
import { Shape, Circle, Square } from "./shapes.surfs";

state shapes: List<dyn Shape> = [Circle.new(1.0), Square { side: 2.0 }];
derived total = shapes.map(s => s.area()).sum();
```

- A type can be `dyn Trait` directly, for example `List<dyn Shape>` or `prop shape: dyn Shape`. There is no `Box`. The runtime holds the value, and a table of functions finds each method at run time (ADR-0135).
- A value of a type with an impl of the trait becomes a `dyn` value where the expected type is `dyn Trait`.
- A `dyn` value has the value semantics of section 5.1. An assignment gives the receiver its own copy.
- A `dyn` value has only the methods of its trait.
- `==` and `!=` on a `dyn` value are a compile error, because `Equatable` cannot be `dyn` (ADR-0135). A `dyn` type cannot be the key type of a `Map`, because a key needs `==`. A record or an enum with a `dyn` field gets no automatic `Equatable` and no automatic `Hashable` (section 7.8).
- A trait can be `dyn` only when it follows the object rules of Rust:
  - Each function of the trait has `self`. A trait with an associated function cannot be `dyn`.
  - No method returns `Self`, and no method has a parameter of the type `Self`. So `Equatable` and `Comparable` cannot be `dyn`.
  - A `dyn` type names each associated type of its trait, for example `dyn Source<Item = Int>`.
  - A `dyn` type of an operator trait names the type of the right side, because `Self` has no meaning there: `dyn Mul<Float, Output = Vector>`. This is error E0393 of Rust.
  - `Number` and `Token` cannot be `dyn`. `Number` includes `Equatable`. A `dyn` type of `Token` is not in version 1.
- A value of a `dyn` type has the trait of its type, as `dyn Shape: Shape` in Rust. So a generic function `fn total<T: Shape>(xs: List<T>)` takes a `List<dyn Shape>`.
- A value becomes a `dyn` value with no import of the trait, as in Rust. A `List<Circle>` does not become a `List<dyn Shape>`, and a `Circle?` does not become a `dyn Shape?`. A value of `Circle` becomes a `dyn Shape?`.
- A method with `mut self` of a `dyn` value changes the value. The `dyn` value keeps its table.
- A type at the boundary of an extern module cannot be `dyn` or `impl` (section 13.1, ADR-0133).

Each feature of this section works on the web, on the native targets and in the interpreter.

---

## 8. Async code

- `async fn load() -> Result<Data, Error>` returns a `Task<Result<Data, Error>>`. A call starts the task at once. The task runs until its first `await` before the call returns.
- `await task` waits for the task and gives its value. `await` is valid in an `async fn` and in an event handler.
- In version 0, `await` must be at the start of a statement value: `const x = await load()`, `let x = await load()`, `x = await load()`, `await load()` and `return await load()`. Other positions are a compile error, for example `1 + await load()`.
- `spawn task` starts a task and does not wait for it. A call of an `async fn` as a statement does the same.
- A task can start only in a component: in a component function, an effect, `on mount`, `on destroy` or an event handler. In a module function and in an initial value, a call of an `async fn` must be a direct `await` in an `async fn`.
- `sleep(ms: Int)` is a built-in `async fn`. Its task ends after `ms` milliseconds.
- `interval(ms: Int)` is a built-in `async fn`. Its task ends at the next time that is a multiple of `ms` on the clock of the app. This clock counts from the start of the app. A loop with `await interval(ms)` does not drift: the time of its work does not move the next end. The first end can come before `ms`. For both functions, a time of 0 or less ends the task at once.

```surf
state seconds = 0;

async fn tick() {
    while true {
        await interval(1000);
        seconds += 1;
    }
}

on mount {
    tick();
}
```

The runtime cancels the task of `tick` when the component leaves the tree, so the loop needs no `cleanup`. ADR-0026 gives the reasons for this form in place of a `Timer` module with callbacks.
- An event handler that contains `await` runs as a task.
- All code runs on the UI thread. An `await` lets other events run until the task continues.
- Each `await` ends the current batch (section 10.5). The UI shows the changes before the await. The end of a task also ends a batch.
- A task belongs to the component that started it. The runtime cancels the task when the component is destroyed. A cancelled task does not continue after its current `await`.
- Two tasks are equal only if they are the same task.

---

## 9. Markup

### 9.1 Elements

```text
<Name attribute=value>children</Name>
<Name attribute=value />
```

- A name must be a built-in component or an imported component.
- A lowercase name such as `<div>` is a compile error. There is no raw HTML, because raw HTML has no meaning on native targets.
- A component file can have more than one root element. The parent lays out each root as a separate child.
- The start tag of a generic component can name its types: `<Picker<User> items={users} />` (section 7.9, ADR-0135).

### 9.2 Attributes

| Form | Meaning |
|---|---|
| `name="text"` | String value. `{...}` inside it inserts values. |
| `name={expression}` | Value of any type |
| `name` | Short form of `name={true}`. Valid only for `Bool` props. |
| `{name}` | Short form of `name={name}` |
| `on:event={handler}` | Event listener: a function name, statements, or a lambda whose body is statements |
| `bind:prop={place}` | Two-way binding. `prop` must be a `bindable prop`. The value must be a place. |
| `style="p-2 gap-1"` | Utilities: the styles of the element (section 12) |
| `{...value}` | One attribute for each field of a record. An attribute on the element wins (ADR-0083). |
| `testid="name"` | A name for tests. It has no effect on the app. Valid only on built-in components. |

- Two attributes with the same name on one element are a compile error.
- On an instance of a user component, the `style` of the instance applies to the root elements of the component, with its computed values and its conditions. This is the same as on an element (layout spec 12).
- On a built-in component, `on:`, `bind:` and the attributes follow the [component reference](https://surface.solenovis.com/docs/components.md).
- The value of an `on:` attribute is one of these:
  - a function name: `on:submit={add}`. The function gets the value of the event, if it takes a parameter.
  - statements, which run at each event: `on:press={count += 1}`, `on:press={remove(todo.id)}`, `on:press={save(); close()}`.
  - a lambda whose body is statements, to use the value of the event: `on:change={done => setDone(todo.id, done)}`, `on:keydown={key => last = key}`.

### 9.3 Text

- Literal text and `{expression}` are valid only inside a component that accepts text. The built-in components that accept text are `Text`, `Span`, `Button`, `Link`, `Toggle` and `Option`. A user component accepts text in the fill of each of its slots that is inside one of these. For the `default` slot, the fill is the children of the instance.
- Text in any other place is a compile error.
- Each run of white space (spaces, tabs, line ends) becomes one space. White space at the start and at the end of a text block is removed.
- Text has no HTML entities. `&amp;` stays the five letters `&amp;`. Write `&` for an ampersand. Write `{"<"}` for a `<`, and `{"\{"}` for a `{`, because these characters start a tag and an expression.
- A `Text` and its parts are one text (ADR-0111):
  - A `Span` is valid only in a `Text` or in a `Span`. A `Link` in a `Text` is a part of the text. Other elements in a `Text` are a compile error. The fill of a slot in a `Text` takes a `Span` and a `Link` too.
  - The content of a `Text` is one text block. A `Span` and a `Link` do not end it, so a space next to them stays: `Run <Span>surf dev</Span>, then` has a space before the `Span` and after the comma. A run of white space across the edge of a `Span` is one space.
  - A block or a tag ends a text block, and the body of a block is a text block of its own. So a space next to a block goes: `#for t in tokens { <Span>{t.text}</Span> }` puts no space between the tokens. Write `{" "}` for a space there.

### 9.4 Interpolation

`{expression}` in text follows section 5.4. The value updates when its dependencies change.

### 9.5 Blocks and tags

The markup has two kinds of control words:

| Sigil | Kind | Words |
|---|---|---|
| `#` | A **block**: a head, then a body in `{ ... }`. Some blocks continue after their `}`. | `#if` (`else if`, `else`), `#for` (`empty`), `#match`, `#await` (`then`, `catch`), `#fill`, `#try` (`catch`) |
| `@` | A **tag**: one line, without a body. | `@const`, `@slot` |

- The head of a block uses the syntax of the script. `#if` is the `if` statement, `#for` is the `for` loop, and `#match` is the `match` expression with markup in its arms.
- A `#` or an `@` is a control word only when one of the words of the table follows it. It must also be at the start of a line, or directly after a tag or a `}`. Everywhere else it is text: `<Text>#1 of {n}</Text>`.
- A `}` in markup ends the body of a block. For a `}` in text, write `{"}"}`.

**Condition**

```surf
#if count > 10 {
    ...
} else if count > 5 {
    ...
} else {
    ...
}
```

**List**

```surf
#for todo, index in todos key todo.id {
    ...
} empty {
    <Text>No tasks.</Text>
}
```

- The words and the order are the words and the order of the `for` loop of the script (section 6.1): `#for item in list { ... }`, `#for item, index in list { ... }`.
- The value must be a `List<T>` or an `Int` range.
- `key <expression>` is optional. A key must be an `Int` or a `String`. The runtime uses the key to keep the identity of an item when the list changes.
- Two equal keys in one list are a runtime error.
- Without a key, the position in the list is the identity. `surf lint` warns when a list of records has no key (ADR-0070).
- If the list is a place, `todo` is also a place, and a write to `todo` changes the element in the list.
- The list can also be a computed value, for example `#for todo in todos.filter(t => !t.done) key todo.id { ... }`. Then `todo` is a constant: a write to it or `bind:` with it is a compile error. To change the element, write to the original list, for example through a function that finds the element by its key.

**Match**

```surf
#match shape {
    .circle(r) => <Text>Circle, radius {r}</Text>
    .rect(w, h) => {
        <Text>Rectangle</Text>
        <Text>{w} by {h}</Text>
    }
}
```

- Each arm is `pattern => markup`. The markup is one element, block or tag, or nodes in `{ ... }`.
- The arms must cover every value (section 6.3).

**Await**

```surf
#await profile {
    <Text>Loading</Text>
} then user {
    <Text>{user.name}</Text>
} catch error {
    <Text style="text-$error">{error.message}</Text>
}
```

- The value must be a `Task<T>` or a `Task<T>?`. While the value is `none`, the block shows nothing.
- If `T` is `Result<V, E>`, `then` receives the `ok` value and `catch` receives the `err` value. Otherwise `catch` is not valid.
- A new task in the value shows the first branch again, until the new task ends.

**Slots and local constants**

- `#fill name(parameters) { ... }` fills a slot of a child (section 7.7). It is valid only as a direct child of a component element.
- `@slot name(arguments)` renders a slot in the component that declares it (section 7.7).
- `@const x = expression` declares a constant for the rest of the enclosing block. The expression ends at the end of the line.

```surf
#for item, i in items key item.id {
    @const label = "{i}: {item.name}"
    <Button alt={label}>{label}</Button>
}
```

**Error boundary**

```surf
#try {
    <Chart data={points} />
} catch message {
    <Text>The chart failed: {message}</Text>
}
```

- The `catch` body shows when the mount or an update of the `#try` body has a runtime error (section 15.2). This includes the child components of the body.
- `catch` is necessary. Its name is optional, and it is a `String` that does not change.
- The `#try` body stays gone after the error, and the rest of the app goes on.
- An error in an event handler, in an effect or in a task goes to `on error`.
- An error in the `catch` body goes to `on error`.

### 9.6 Built-in components (version 0)

| Component | Function | Accepts text | Web element |
|---|---|---|---|
| `Row` | Horizontal flex container | no | `<div>` |
| `Column` | Vertical flex container | no | `<div>` |
| `Stack` | Children on top of each other | no | `<div>` |
| `Grid` | Grid container | no | `<div>` |
| `Scroll` | Scrollable area | no | `<div>` |
| `List` | Virtualized list, renders only the rows near its visible area (ADR-0027) | no | `<div role="list">` |
| `Text` | Text | yes | `<span>` |
| `Span` | A part of a `Text` with a style of its own (ADR-0111) | yes | `<span>` |
| `Link` | A link. In a `Text`, a part of the text | yes | `<a>` |
| `Image` | Image | no | `<img>` |
| `Video` | A video with its sound, captions and controls (ADR-0156) | no: it takes `Source` and `Track` elements | `<video>` |
| `Audio` | A sound, with controls or with no box (ADR-0156) | no: it takes `Source` and `Track` elements | `<audio>` |
| `Source` | One file of a `Video` or an `Audio`, with its format | no | `<source>` |
| `Track` | A WebVTT file of captions or subtitles (ADR-0157) | no | `<track>` |
| `Icon` | Icon from an SVG file (ADR-0030) | no | `<svg>` with `<path>` elements |
| `Button` | Button | yes | `<button>` |
| `Input` | One-line text input. `kind="date"` or `kind="number"` gives the `type` of the element on the web. A native field stays a text field. | no | `<input>` |
| `TextArea` | Multi-line text input | no | `<textarea>` |
| `Toggle` | Switch or check box | yes | `<label>` with `<input type="checkbox">` (layout spec, section 4) |
| `Slider` | Value slider | no | `<input type="range">` |
| `Select` | Choice from a list | no: it takes `Option` elements | a `<button>` and a list in a `<div>`, or with `system` a `<select>` in a `<div>` |
| `OptionList` | The box of the open list of a `Select`, for its styles | no: it takes `Option` elements | `<div>` |
| `Option` | One choice of a `Select` | yes | `<option>` |
| `Overlay` | An element above all others, next to its parent (ADR-0032) | no | an element with `popover` |
| `Transition` | Animates the elements that its blocks add and remove (ADR-0036) | no | no element of its own |
| `Drawing` | A box with its own coordinates for shapes (ADR-0029) | shapes and `Group` | `<svg viewBox>` |
| `Line`, `Polyline`, `Polygon`, `Rect`, `Circle`, `Ellipse`, `Path`, `Group` | Shapes in a `Drawing` | `Group`: shapes | the SVG element of the name |
| `Canvas` | Immediate-mode drawing | no | `<canvas>` |
| `PlatformView` | Native view (native targets only) | no | not available |

The [component reference](https://surface.solenovis.com/docs/components.md) gives the attributes, the bindings and the events of each component. An attribute or an event that is not in the reference is a compile error.

Not in version 0:

- `Canvas` and `PlatformView`. Each of them is a compile error (component reference, section 4.16).

### 9.7 The keyboard focus

- `Focus.to(id)` gives the keyboard focus to the element with this `id` (ADR-0102). It takes effect at the end of the batch, so the element can come into the tree in the same batch. A row of a `List` that shows again can come in the next frames, so the web target tries again in the next three frames (ADR-0110). An `id` that no element has does nothing.
- `Focus.newId()` gives a `String` that no other call gives in this run of the app. A component uses it for `id`, `controls` and `labelledBy`: `state uid = Focus.newId();`.
- On the native targets, only a control can get the focus.

---

## 10. Reactivity

### 10.1 Sources and consumers

- **Reactive sources:** `prop`, `bindable prop`, `state`, `derived` and `motion` declarations.
- **Reactive consumers:** markup expressions, `derived` expressions and `effect` bodies.

### 10.2 Dependencies

The compiler finds the dependencies of each consumer at compile time. The dependencies of a consumer are:

1. the sources that the consumer reads directly
2. the sources that the component functions read, for each component function that the consumer calls, transitively

Functions from module files cannot read the sources of a component (section 7.3), so they add no dependencies on them. This is also true for the methods of an impl (section 7.8) and for a call through a `dyn` value (section 7.9).

`Locale` is the exception (section 14, ADR-0132). Each function can read it, also a function of a module file and a method. The checker follows each call, transitively. A consumer that calls a function that reads `Locale` depends on `Locale`, so it updates after `Locale.set`. A call through a `dyn` value depends on `Locale` when an impl of the method in the app reads it.

A call of a function value depends on `Locale` when a function value of the same type in the app reads `Locale` (ADR-0132). This applies to each function value: a prop (section 10.3), a state, a field, an item of a list or a map, and a parameter. For example, with `prop format: (Int) -> String`, the call `format(count)` depends on `Locale` when a lambda or a module function of the type `(Int) -> String` reads it. This is the conservative rule of a call through a `dyn` value. So the child updates after `Locale.set`.

A write to a place is not a read of the place. `total = v` and `log.push(v)` read `v` and the indexes in the place, but not `total` or `log`. An operator such as `+=` reads its target.

The analysis is conservative. A read inside a branch counts as a dependency, also when the branch does not run. The result can be more updates than necessary, but never a missed update, and the result is the same on every target.

### 10.3 Lambdas as props

A lambda that reads reactive sources is itself a consumer in the parent. When one of its dependencies changes, the parent sends the lambda again, and the child sees a changed prop. This keeps the dependency analysis static across component boundaries.

A function prop has a function type: `prop format: (Int) -> String`. Its value is a function value (section 6.4), for example:

- a lambda: `format={(n) => "{n} {unit}"}`,
- a module function, by its name: `format={formatWeight}`,
- a function prop of the parent, by its name.

The child calls it like a function: `{format(count)}`. These rules apply:

- The lambda keeps copies of the values that it reads when the parent makes it. The parent makes it again when one of these values changes.
- The lambda cannot call a function of the parent component, because the child runs the lambda. Use a module function.
- An `async fn` cannot be a function prop.
- The default value of a function prop cannot read the sources of its component.
- A function prop cannot be `bindable`. Its type cannot take or give a task.
- Two function values are equal only when they are the same value. So a new lambda from the parent is always a change.

### 10.4 Writes

- A write to a source, or to a place inside a source, marks the source as changed (with the exception in section 5.3).
- Writes are valid in functions, event handlers, effects, `on mount` and `on destroy`.
- A write in a `derived` expression or in a markup expression is a compile error.
- `Locale.set` is a write (section 14, ADR-0132). In a `derived` expression and in a markup expression, it follows the rules of a write of a state. So it is a compile error there. An `on:` attribute can call it, because its value is statements.

### 10.5 Batches

Writes do not change the UI at once. The runtime collects writes into a **batch**. A batch ends when the current event handler, effect, `on mount` block or task step (up to the next `await`) returns.

At the end of a batch, the runtime does these steps in this order:

1. It sends changed props from parents to children.
2. It marks the affected `derived` values as stale. A stale value is calculated again when something reads it.
3. It updates the markup of each affected component, parent before child.
4. It lays out and paints the changes in the next frame.
5. It runs the affected effects, in tree order (parent before child), and in declaration order inside one component.

Writes from step 5 start a new batch. After 100 batches in a row from effects, without an event from outside, the runtime stops with a runtime error ("the effects started 100 batches in a row").

### 10.6 Guarantees

- **No partial states:** each consumer in a batch sees the values after all writes of the batch.
- **Deterministic order:** the order of updates and effects depends only on the source code and the events, not on the target.

---

## 11. Component lifecycle

1. The parent calculates the props.
2. The `const` and `state` initializers run in declaration order.
3. The runtime creates the markup. Child components go through the same steps.
4. The first frame shows the component.
5. The effects run for the first time, then `on mount` runs. Children run these steps before their parent.
6. Each batch updates the component (section 10.5).
7. When the component leaves the tree, the runtime cancels its tasks, runs the effect cleanups, then runs `on destroy`. Children run these steps before their parent.

---

## 12. Styles (summary)

The styles of an element are **utilities** in its `style` attribute. The [Surface Layout Spec v0](https://surface.solenovis.com/docs/layout.md), section 12, lists every utility. This section gives a summary.

```surf
<Column style="p-6 gap-3 bg-$surface rounded max-600:p-4">
    <Text style="text-20 font-600 text-$text-strong">Title</Text>
    <Button
        style="px-4 h-9 bg-$accent text-$on-accent hover:opacity-0.85 active:scale-0.97 {saved}:bg-$success"
    >
        Save
    </Button>
    <Column style="h-2 bg-$accent w-{progress}%" />
</Column>
```

- Each utility sets one style property of the layout spec, or one part of it: `p-2` sets all sides of `padding`, `pt-2` the top side.
- For spacing and sizes, a number is a number of steps (ADR-0098). One step is `0.25rem`, so `p-2` is 8 logical px at the default root size. `p-2px` is exact, and a value can have a unit: `w-50vw`, `p-1rem` (ADR-0067). Other utilities keep exact numbers: `border-2`, `text-14`, `rounded-8`.
- `$name` is a theme token (layout spec, section 2.1). A token always has `$`. An unknown token is a compile error. A bare name is a constant: `gap-inset`, `text-xs`.
- A value with spaces has `_` for each space: `shadow-0_2_8_rgb(0_0_0_/_0.15)`.
- Variants come before a utility. A variant is a state (`hover:`, `active:`, `focus-visible:`, `disabled:`) or a theme (`dark:`, `light:`). It can also be a platform (`ios:`, section 14), a named breakpoint (`md:`, `max-md:`) or a window width (`min-600:`, `max-600:`).
- A computed value goes in braces after the prefix: `w-{width}px`. A computed transform runs no layout: `translate-x-{x}px`, `translate-y-{y}px`, `scale-{s}`, `rotate-{r}`.
- `{condition}:` is a variant (ADR-0096): `{saved}:bg-$success`. The value must be a `Bool`. It can come with other variants: `{!open}:max-md:hidden`. A condition wins over the same utility with no condition.
- `(...)` groups utilities under one variant: `{saved}:(bg-$success text-$on-accent)`, `hover:(underline opacity-0.8)`.
- `{value}:{ case: utilities, }` is a case list (ADR-0096). The value must be an enum. The compiler checks each utility.
  - The list must name each case. A case with fields is a compile error. Write `case:,` for a case with no style.
  - A `String` value uses one condition for each value: `{status == "open"}:text-$success`.
  - The utilities of one case cannot set the same part of a property twice. Two cases can set the same property, because only one case is on.
  - The utilities are fixed text. A computed value goes outside the list.
- `@name` uses a preset from `style-presets.toml` (ADR-0091, layout spec, section 2.2). A utility on the element wins over a preset.
- A word that is not a utility is a compile error. Two utilities of one element can set the same part of a property with the same variants. This is a compile error, so the order of the words has no effect.
- The `style` of a user component applies to the root elements of that component.
- A style property as an attribute, for example `gap={12}`, is a compile error. The help text of the error gives the utility.

The layout spec, section 12.3, gives the precedence of the declarations.

---

## 13. Extern modules

An extern module declares functions that each target implements in its own language.

```surf
// haptics.surfs
pub enum Strength { light, medium, heavy }

pub extern module Haptics {
    fn impact(strength: Strength);
    async fn isAvailable() -> Bool;

    fallback {
        fn impact(strength: Strength) {}
        async fn isAvailable() -> Bool = false;
    }
}
```

### 13.1 Types at the boundary

Parameters and return values must use only these types:

- `Int`, `Float`, `Bool`, `String`, `Void`
- `List<T>`, `Map<K, V>`, `T?`, `Result<T, E>`
- records and enums whose fields use only these types

Function types are not valid at the boundary (ADR-0133). Events (section 13.3) and `async fn` calls (section 13.4) do the work of a callback.

A function or an event of an extern module has no type parameters. A type at the boundary cannot be a `dyn` type or an `impl` type (ADR-0133).

### 13.2 Implementation files

Implementation files are in the same folder as the module file. The name is `<module file>.<target>.<extension>`:

| File | Used for |
|---|---|
| `haptics.ios.swift`, `haptics.macos.swift` | One Apple target |
| `haptics.apple.swift` | Apple targets without a more specific file |
| `haptics.android.kt` | Android |
| `haptics.macos.rs`, `haptics.windows.rs`, `haptics.linux.rs` | One desktop target |
| `haptics.native.rs` | Native targets without a more specific file |
| `haptics.web.js` | Web (Phase 1). `haptics.web.ts` is a decision for later. |

- The most specific file wins: target, then group, then `native`.
- If a target has no implementation file, the target uses the `fallback` block. If there is no `fallback` block either, the build for that target fails with a compile error.
- `surf extern stubs [folder]` writes a stub of each implementation file that does not exist yet: `haptics.native.rs` and `haptics.web.js`. A stub has each function of the module with its types, and a body that panics or throws. A stub replaces the `fallback` block on its targets. The command also writes `haptics.web.d.ts`, the TypeScript declarations of the web implementation file, as `surf build` does.
- The desktop targets and a SurfWave server accept Rust implementation files. A `String` parameter is `&String` in Rust, and `surf extern stubs` writes the exact types. The file can use the standard library and the runtime, and no other crate. Swift and Kotlin files are for the mobile targets.
- The `fallback` block must have each function of the module, with the same signature, and no other function.

An app that embeds in a host shares its process with other apps (ADR-0056). `surf_core::task::current_view()` gives the number of the app that calls a function. `emit.to(view)` sends an event to that app only. `emit` alone sends to all apps.

A web implementation file is an ES module. It exports one function for each function of the module, with the same name. The values cross the boundary as JavaScript values:

| Surf type | JavaScript value |
|---|---|
| `Int`, `Float` | number |
| `Bool`, `String` | boolean, string |
| `List<T>`, `Map<K, V>` | array, `Map` |
| `T?` | the value, or `null` |
| `Result<T, E>` | `{ $: "ok", value }` or `{ $: "err", value }` |
| record | object with the fields |
| enum | the case name for a case without fields, else `{ $: "case", ...fields }` |

An `async fn` returns a promise.

### 13.3 Events

An extern module can send events to the app (ADR-0031). It declares each event, and a component listens with a declaration:

```surf
// plugin.surfs
pub extern module Plugin {
    fn setParam(id: Int, value: Float);
    event report(params: List<Float>, peak: Float);

    fallback {
        fn setParam(id: Int, value: Float) {}
    }
}
```

```surf
import { Plugin } from "./plugin.surfs";

state level = 0.0;

on Plugin.report(params, peak) {
    level = peak;
}
```

- The values of an event follow the rules of section 13.1. A listener has one name for each value, and the names take the types of the event.
- A listener can write the state of its component, and it runs as one batch. It lives as long as the instance of the component. All mounted instances with a listener get each event, in the order in which they mounted.
- An event waits until the current batch ends. Events keep their order, and no event is dropped or merged. An event without a listener is lost.
- An implementation file of a module with events has one more function, `start`, which takes `emit`. The runtime calls it once, when the first listener mounts. `emit` has a method for each event: `emit.state(params, peak)`. A web file exports `function start(emit)`. A Rust file has `pub fn start(emit: Emit)`. `Emit` is `Copy` and `Send`, so any thread can send, and the send wakes the event loop.
- `start` is not a valid name for a function of a module with events.
- A Rust file names the items of the module by their Surf names. A name that Rust or the generated code uses gets `__` at the end: a Rust keyword that cannot be a raw identifier (`self`), a name of the Rust prelude (`Vec`, `Some`), a name of `surf_core::prelude` (`Event`, `Tree`) and the names `Emit`, `imp`, `STYLESHEET` and `surf_bridge`. Another keyword is a raw identifier (`r#type`). An event `to` is the method `to__` of `Emit`, because `to` sends to one app (ADR-0115).
- Without an implementation file, the module uses its `fallback` block and no event comes. A `fallback` block cannot send events in version 0.
- The interpreter (`surf dev --target desktop`) delivers no events in version 0.

A Rust implementation file has the items of a Rust module. The compiler includes the file in the module of the extern module. So the file can name the types of the module file and of its imports. The file has a `pub fn` for each function of the module, with the same name, and a `pub async fn` for each `async fn`. The file cannot have inner attributes (`#![...]` or `//!`). The values cross the boundary as Rust values:

| Surf type | Rust value |
|---|---|
| `Int`, `Float`, `Bool` | `i64`, `f64`, `bool` |
| `String` | `String` |
| `List<T>`, `Map<K, V>` | `Vec<T>`, `surf_core::Map<K, V>` |
| `T?` | `Option<T>` |
| `Result<T, E>` | `Result<T, E>` |
| record | a struct with the fields |
| enum | an enum with the cases |

A parameter of a type other than `Int`, `Float` and `Bool` is a shared reference, for example `&String` or `&Vec<Todo>`. The native executor has no wakers: a future of a Rust implementation calls `surf_core::task::progress()` when it becomes ready. `surf_core::data::dir()` gives the data folder of the app, where an implementation can keep files.

### 13.4 Calls

- A synchronous call runs on the UI thread. The implementation must return quickly.
- An `async fn` call can run on any thread. The result comes back on the UI thread and starts a new batch.
- A call names the module: `Haptics.impact(.light)`. A call of an `async fn` starts a task, as other `async fn` calls do (section 8).
- The compiler generates direct bindings. These are a Rust call for Rust, the C ABI for Swift, JNI for Kotlin, and an ES module import for the web. There is no message serialization.

---

## 14. Platform information

- The platform names are `macos`, `windows`, `linux`, `android`, `ios` and `web`. The groups are `apple` (macos, ios), `mobile` (ios, android), `desktop` (macos, windows, linux) and `native` (all targets except web). A platform variant of a utility (`ios:p-8`, section 12) takes these names.
- `Platform.os` has the type `enum Os { macos, windows, linux, android, ios, web }`.
- `Platform.os` is a compile-time constant. `if Platform.os == .ios { ... }` keeps the block only in iOS builds.
- The compiler type-checks code for all targets, also code that a target removes.
- `Window.width` and `Window.height` are the size of the window, as `Float` values in logical px (ADR-0106). A read has the dependencies of a state: a `derived` value, an effect and the markup update after a resize.
- `Window` is valid only in a component, and a program cannot write it. A function in a `.surfs` file takes the size as a parameter. A declaration or an import with the name `Window` comes first.
- A server has no window, so both values are 0 there.

**Locale**

The module `Locale` formats values for a person in the locale of the app (ADR-0132). A **locale** is a language with an optional region, as a BCP 47 tag, for example `en`, `de` or `pt-BR`.

```surf not-surf
state count = 3;
state price = 1234.5;

derived total = Locale.number(price, style: .currency(code: "EUR"));
derived files = match Locale.plural(count) {
  .one => "{count} file",
  _ => "{count} files",
};

---
<Column style="gap-2">
  <Text>{total}</Text>
  <Text>{files}</Text>
  <Button on:press={Locale.set("de")}>Deutsch</Button>
</Column>
```

- `surf.toml` lists the locales of the project, for example `locales = ["en", "de"]`. At the start, the app takes the locale of the system that matches the list best, else the first item of the list. The rule of the match follows below.
- If a project uses `Locale` and its `surf.toml` has no key `locales`, the compiler gives an error. An empty list, `locales = []`, gives the same error (ADR-0132). The message gives an example line, for example `locales = ["en"]`.
- The MCP server and the compiler in the browser give this error too, because they check source text with no `surf.toml` (ADR-0132). A sample that uses `Locale` needs a project with a `surf.toml`.
- `Locale.set(tag)` changes the locale while the app runs, and each format follows. A tag that fits no item of the list gives the first item.
- `Locale.set` is a write (section 10.4, ADR-0132). It follows each rule of a write of a state, in each place. So a `derived` value and a markup expression cannot call it.
- `Locale.set` does not save the locale. At the next start, the app takes the locale of the system again. This is the rule of `Theme.setToken` (ADR-0082). An app can save the choice with `Storage`.
- `Locale.current` is the tag of the current locale, as a `String`. It is always an item of the list.
- A read of `Locale.current` and a call of a function of `Locale` have the dependencies of a state, as a read of `Window.width` has. A `derived` value, an effect and the markup update after `Locale.set`.
- `Locale` is valid in each function, also in a function of a `.surfs` file and in a method. For example, a module file can have `fn due(t: Todo) -> String = Locale.date(t.due);`. The checker follows each call (section 10.2). So a component that calls a function that reads `Locale` updates after `Locale.set`.
- A module constant cannot read `Locale`, also through a call (section 7.3, ADR-0132). This is a compile error, and the message says to use a function. An instance constant of a component cannot read `Locale` either, and its message says to use a `derived` value. A call of a function value reads `Locale` when a function value of its type reads it (section 10.2).
- Only `Locale.set` changes the locale. A declaration or an import with the name `Locale` comes first.
- On a SurfWave server, each request takes the best match of its `Accept-Language` header in the list, else the first item of the list. `Locale.current` is the locale of the request. The page hydrates with the same locale ([SurfWave](https://surface.solenovis.com/docs/routes.md), section 5).

```surf not-surf
// labels.surfs
const PRICE = Locale.number(9.99);              // compile error: a module constant reads Locale
pub fn price() -> String = Locale.number(9.99); // a function reads Locale at each call
```

**The match.** The app uses the lookup of RFC 4647 (ADR-0132). It takes the wanted tags in their order. For each wanted tag, it tries the tag, and then the tag without its last part, until a try fits an item of the list. So `de-CH` fits `de`, and `zh-Hant-TW` tries `zh-Hant` and then `zh`. A part of one letter, for example `x` in `de-x-private`, goes away together with the part after it. The lookup never adds a part, so `de` does not fit `de-CH`. A try fits an item when the two tags are equal apart from the case of the letters. BCP 47 gives the case no meaning. The first fit gives the locale. These are the wanted tags:

- At the start of an app, the wanted tags are the languages of the system, in the order of the system. On the web, these are the languages of the browser.
- On a SurfWave server, the wanted tags are the tags of the `Accept-Language` header of the request, in the order of their weights. Two tags with the same weight keep their order in the header. A tag with the weight 0 is not a wanted tag, because RFC 9110 gives it the meaning "not acceptable".
- After `Locale.set` in the browser, each data request, each action and each remote call has the header `x-wave-locale` with the tag of `Locale.current`. The server uses this tag as the one wanted tag, in place of `Accept-Language` ([SurfWave](https://surface.solenovis.com/docs/routes.md), section 5).
- `Locale.set(tag)` has one wanted tag.

If no try fits, the app takes the first item of the list. Before 2026-11-04 the rule was exact tags only. The project owner changed it, because macOS often gives tags with a region, for example `de-DE`, and a German Mac then got English from the list `["en", "de"]`.

| Function | Result | Rule |
|---|---|---|
| `Locale.number(x, style: NumberStyle = .decimal)` | `String` | `x` is an `Int` or a `Float` (`impl Number`). The text has the decimal separator and the grouping of the locale |
| `Locale.date(ms, style: DateStyle = .medium)` | `String` | The date of `ms` in the time zone of the machine, as `Time.local` gives it, with the names of months and weekdays of the locale |
| `Locale.time(ms, style: DateStyle = .medium)` | `String` | The time of day of `ms` in the time zone of the machine |
| `Locale.relative(ms)` | `String` | The text of a difference of `ms` milliseconds, for example "3 days ago" or "in 2 hours". A negative value is in the past |
| `Locale.list(items)` | `String` | The strings of a `List<String>` with the separators and the words of the locale, for example "a, b, and c" in `en` |
| `Locale.plural(n)` | `PluralCategory` | The plural category of `n` in the locale. `n` is an `Int` or a `Float` |
| `Locale.compare(a, b)` | `Ordering` | The order of two strings in the rules of the locale. `.less` when `a` comes before `b`. `xs.sortWith((a, b) => Locale.compare(a, b))` sorts a list in this order (section 4.5) |
| `Locale.set(tag)` | nothing | Changes the locale. The tag is a `String`. The change is not saved |
| `Locale.current` | `String` | The tag of the current locale |

- `NumberStyle` is a built-in enum with the cases `decimal`, `percent` and `currency(code: String)`. `code` is a currency code of ISO 4217, for example `"EUR"`. A percent shows `x` times 100: `0.25` gives "25%" in `en`.
- A decimal number has at most 3 digits after the separator, and a percent has none. A currency has the digits of the currency, for example 2 for `EUR`. The value rounds half away from zero, as `round` does (section 4.2).
- `DateStyle` is a built-in enum with the cases `short`, `medium` and `long`. ICU and `Intl` both have these lengths. The locale gives the pattern of each length.
- `Locale.relative` uses the largest unit in which the amount is at least 1. The units are year (365 days), month (30 days), week, day, hour and minute. Else it uses seconds. The amount drops its fraction. A negative value with the amount 0 stays in the past: `Locale.relative(-500)` is "0 seconds ago" in `en`.
- `PluralCategory` is a built-in enum with the categories of CLDR: `zero`, `one`, `two`, `few`, `many` and `other`. A locale uses only some of them: English has `one` and `other`.
- `Locale.plural` rounds a `Float` to 3 digits after the separator first, as the decimal format of `Locale.number` and `Intl.PluralRules` do. So `Locale.plural(1.0001)` is `.one` in `en`, and the word fits the number that `Locale.number` shows.
- The web uses `Intl` of the browser. Its data costs no bytes.
- The native targets and the interpreter use the ICU of the system: `libicucore` on macOS, `icu.dll` on Windows 10 version 1903 and later, and `libicu` on Linux (ADR-0132). An app carries no locale data. An app that does not use `Locale` does not load ICU.
- An app that uses `Locale` loads ICU when it starts. When the system has no ICU, the app stops at its start with an error that names the package of ICU (ADR-0132).
- The browser and the system can have different versions of ICU and of CLDR, so a few results can differ between the targets (see [native differences](https://surface.solenovis.com/docs/targets.md)).

---

## 15. Errors

### 15.1 Compile errors

Each compile error gives the file, the line, the column, a message and, where possible, a suggested fix.

### 15.2 Runtime errors

The text of a runtime error is `runtime error: ` and a text of this table. The text is the same on each target.

| Text | Cause |
|---|---|
| the result is outside the integer range | An `Int` or `Int64` result outside the range (section 4.2) |
| the divisor is zero | An integer division or remainder by zero |
| the index is outside the list | `xs[i]` or `xs.insert(i, x)` with an index outside the list |
| the Float is NaN or outside the Int range | `f.toInt()` with NaN or a value outside the range |
| two rows have the same key | Two equal keys in one `#for` block or `List` |
| the effects started 100 batches in a row | 100 batches in a row from effects (section 10.5) |
| the message of the call | A call to `panic(message)` |

- A runtime error stops the current batch. The writes of the batch before the error stay in effect.
- In a development build, the app shows the error and its source location on top of the UI. This covers an error in the mount of the app and in the `on error` handler. An `on error` handler takes an error first, and the error then shows only when the handler fails. `surf dev` does this on the web and in the dev host of the desktop (ADR-0122).
- In a release build, the runtime calls the `on error` handler of the app. The default handler logs the error and ends the app.
- A `#try` block catches an error of the mount or of an update of its body (section 9.5). This error does not reach `on error`, and the batch goes on.
- `on error(message) { ... }` is valid only in the root component, `App.surf`. `message` is the text of the error, for example `"runtime error: the divisor is zero"`. The handler runs as a new batch after the batch with the error, and the app goes on. The parameter is optional: `on error { ... }`.
- An error in the handler ends the app. On the web, an app that ended takes no more events.
- On the native targets, a release build of an app with `on error` unwinds its panics, and a release build without it aborts. Unwinding makes the binary larger: about 100 KB on macOS.
- The behavior is the same on all targets.

---

## 16. Grammar summary

```ebnf
component     = ( componentItem* "---" )? markup ;
module        = moduleItem* ;

moduleItem    = import | const | typeDecl | enumDecl | fnDecl | externDecl
              | traitDecl | implDecl ;
componentItem = import | const | typeDecl | enumDecl | fnDecl | componentDecl
              | propDecl | stateDecl | derivedDecl | motionDecl | sequenceDecl
              | effectDecl | eventDecl | slotDecl | lifecycle ;

import        = "import" ( Name | "{" ident ( "," ident )* "}" ) "from" string ";" ;
const         = "pub"? "const" ident ( ":" type )? "=" expr ";" ;
typeDecl      = "pub"? "type" Name typeParams? "=" "{" field ( "," field )* ","? "}" ;
field         = ident ":" type ( "=" expr )? ;
enumDecl      = "pub"? "enum" Name typeParams? "{" case ( "," case )* ","? "}" ;
case          = ident ( "(" field ( "," field )* ")" )? ;
fnDecl        = "pub"? "async"? "fn" ident typeParams? "(" ( selfParam ( "," params )? | params? ) ")"
                ( "->" type )? ( block | "=" expr ";" ) ;
params        = param ( "," param )* ;
param         = ident ":" type ( "=" expr )? ;
externDecl    = "pub"? "extern" "module" Name "{" ( externFn | externEvent )* fallback? "}" ;
externEvent   = "event" ident ( "(" params? ")" )? ";" ;
externFn      = "async"? "fn" ident "(" params? ")" ( "->" type )? ";" ;
fallback      = "fallback" "{" fnDecl* "}" ;

typeParams    = "<" typeParam ( "," typeParam )* ">" ;
typeParam     = Name ( ":" bound )? ;
bound         = traitRef ( "+" traitRef )* ;
traitRef      = Name ( "<" type ">" )? ;
traitDecl     = "pub"? "trait" Name "{" traitItem* "}" ;
traitItem     = "type" Name ";" | fnSig ";" | fnDecl ;
implDecl      = "impl" typeParams? ( type "for" )? type "{" implItem* "}" ;
implItem      = "type" Name "=" type ";" | fnDecl ;
fnSig         = "fn" ident typeParams? "(" ( selfParam ( "," params )? | params? ) ")"
                ( "->" type )? ;
selfParam     = "mut"? "self" ;
componentDecl = "component" Name typeParams ";" ;

propDecl      = "bindable"? "prop" ident ":" type ( "=" expr )? ";" ;
stateDecl     = "state" ident ( ":" type )? "=" expr ";" ;
derivedDecl   = "derived" ident ( ":" type )? "=" expr ";" ;
motionDecl    = "motion" ident ( "(" motionArg ( "," motionArg )* ")" )? "=" expr ";" ;
motionArg     = "ms" ":" integer | "ease" ":" string ;
sequenceDecl  = "sequence" ident "(" sequenceArg ( "," sequenceArg )* ")" "{" cueDecl* "}" ;
sequenceArg   = "play" ":" expr | "timeline" ":" string | "range" ":" string ;
cueDecl       = "cue" ident ( "(" cueArg ( "," cueArg )* ")" )? "=" string ";" ;
cueArg        = ( "ms" | "gap" | "stagger" ) ":" "-"? integer | ( "ease" | "from" ) ":" string
              | "with" ":" ident ;
effectDecl    = "effect" block ;
eventDecl     = "event" ident "(" params? ")" ";" ;
slotDecl      = "slot" ident "?"? ( "(" slotParam ( "," slotParam )* ")" )? ";" ;
slotParam     = "bindable"? ident ":" type ;
lifecycle     = "on" ( "mount" | "destroy" | "error" ) block
              | "on" "change" "(" args ")" block
              | "on" "key" "(" ident ( "," ident )? ")" block
              | "on" ident "." ident [ "(" [ ident { "," ident } ] ")" ] block ;

type          = ( Name ( "<" type ( "," type )* ">" )?
              | "(" ( type ( "," type )* )? ")" "->" type
              | "dyn" Name ( "<" dynArg ( "," dynArg )* ">" )?
              | "impl" bound ) "?"? ;
dynArg        = type | Name "=" type ;

markup        = node+ ;
node          = element | text | interp | block | tag ;
element       = "<" Name ( "<" type ( "," type )* ">" )? attr* ( "/>" | ">" node* "</" Name ">" ) ;
attr          = ident ( "=" ( string | "{" expr "}" ) )?
              | "{" ident "}"
              | ( "on" | "bind" ) ":" ident "=" "{" expr "}"
              | "style" "=" '"' styleItem* '"'
              | "{" "..." expr "}" ;
styleItem     = ( styleMark ":" )* ( utility | "@" ident | "(" styleItem* ")" | caseList ) ;
styleMark     = variant | "{" expr "}" ;
caseList      = "{" ( ident ":" styleItem* "," )* "}" ;
utility       = "-"? word ;   (* layout spec, section 12 *)
interp        = "{" expr "}" ;
block         = ifBlock | forBlock | matchBlock | awaitBlock | tryBlock | fillBlock ;
ifBlock       = "#if" expr body ( "else" "if" expr body )* ( "else" body )? ;
forBlock      = "#for" ident ( "," ident )? "in" expr ( "key" expr )? body ( "empty" body )? ;
matchBlock    = "#match" expr "{" ( pattern "=>" ( body | element | block | tag ) )* "}" ;
awaitBlock    = "#await" expr body ( "then" ident? body )? ( "catch" ident? body )? ;
tryBlock      = "#try" body "catch" ident? body ;
fillBlock     = "#fill" ident ( "(" ident ( "," ident )* ")" )? body ;
body          = "{" node* "}" ;
tag           = "@slot" ident ( "(" args ")" )?
              | "@const" ident "=" expr ;   (* to the end of the line *)

```

The grammar is a summary. It leaves out the statements, which end with `;` (section 3.5). The sections above are the normative text where the two disagree. `on error` in `lifecycle` is valid only in the root component of an app.

Only a function in an impl block or in a trait can have `selfParam`. In a `dyn` type, each `type` of `dynArg` comes before each `Name "=" type`.

---

## 17. Complete examples

### 17.1 Counter

```surf
prop label: String = "Clicks";

state count = 0;
derived doubled = count * 2;

---
<Column style="p-6 gap-3 items-center bg-$surface">
    <Text style="text-20 font-600">{label}: {count}</Text>
    <Text>Doubled: {doubled}</Text>
    <Button on:press={count += 1}>Add one</Button>
    #if count > 10 {
        <Text style="text-$warning">That is a lot of clicks</Text>
    }
</Column>
```

### 17.2 Todo list

Module files have no mutable data, so the next id is a `state` in the component.

```surf
// model.surfs
pub type Todo = { id: Int, title: String, done: Bool = false }

pub fn remainingText(todos: List<Todo>) -> String {
    const n = todos.filter(t => !t.done).count();
    return n == 1 ? "1 task left" : "{n} tasks left";
}
```

`TodoApp.surf`:

```surf
import { Todo, remainingText } from "./model.surfs";

state todos: List<Todo> = [];
state draft = "";
state nextId = 1;

fn add() {
    if draft.trim().isEmpty() {
        return;
    }
    todos.push({ id: nextId, title: draft.trim() });
    nextId += 1;
    draft = "";
}

---
<Column style="gap-2">
    <Row style="gap-2">
        <Input style="grow" bind:value={draft} placeholder="New task" on:submit={add} />
        <Button on:press={add} disabled={draft.trim().isEmpty()}>Add</Button>
    </Row>
    <List items={todos} key={todo => todo.id}>
        #fill row(todo) { <Toggle bind:checked={todo.done}>{todo.title}</Toggle> }
    </List>
    <Text style="text-$text-secondary">{remainingText(todos)}</Text>
</Column>
```

---

## 18. Decisions for version 1

On 2026-11-04, the project owner decided the open questions of version 1:

- The formats of a locale: the module `Locale`, valid in each function, and the sort `sortWith` (sections 4.5 and 14, ADR-0132). The details are in sections 4.5, 7.3, 10.2, 10.4 and 14: no `Locale` in a module constant, one stable merge sort, and the lookup of RFC 4647.
- No function types in extern modules. Events and `async fn` calls do the work of a callback (section 13.1, ADR-0133).
- Traits and impl blocks in the form of Rust (section 7.8, ADR-0134).
- Type parameters with bounds, generic components and trait objects. `==` on a `dyn` value is a compile error (section 7.9, ADR-0135).

On 2026-11-05, the project owner answered the open questions of the language for version 1:

- A module file has a name in camelCase, and a component file has a name in PascalCase. A wrong name and two paths that differ only in case are compile errors (section 2.2, ADR-0140).
- Each keyword is valid as a field name, also the six keywords of traits (section 3.2, ADR-0134).
- The web compares strings by UTF-16 code units, as JavaScript does. The native targets keep the order of scalar values (section 4.4, ADR-0113).
- `ease` of `motion` takes only the name of a utility, for example `"ease-spring"` (section 7.5, ADR-0104).
- A file calls a method of a trait only when it imports the trait (sections 7.2 and 7.8, ADR-0134).
- The other rules of ADR-0113 stay (points 1, 2, 5 and 6), and so do points 1 to 8 of ADR-0115.

These items come after version 1:

- Function types in extern modules. They need a real case and a new ADR (ADR-0133).
