# Kawa language reference

Kawa is the C scripting runtime used by the engine's current scripting adapter. This page is the **language surface**: syntax, types, and the stdlib binds the compiler/VM actually expose. Engine attach (entity and component `.script`, tick hooks, natives) lives in [Scripting with Kawa](/docs/guides/scripting-kawa.md).

Editor highlighting: [`tools/vscode-extensions/kawa`](https://github.com/Flying-Rat/hikari/blob/develop/tools/vscode-extensions/kawa) (VS Code / Cursor); [`tools/nvim-plugins/kawa`](https://github.com/Flying-Rat/hikari/blob/develop/tools/nvim-plugins/kawa) (Neovim / Vim). Concrete syntax: [`src/kawa/syntax.ebnf`](https://github.com/Flying-Rat/hikari/blob/develop/src/kawa/syntax.ebnf) (W3C EBNF aligned with the C parser/scanner). Do not invent forms that are not in that grammar.

```kawa
let hp: i32 = 100;
let name: str = "Player";
fn add(a: i32, b: i32) -> i32 { return a + b; }
if (hp > 0) { print(name); } else { print("dead"); }
let values: i32[] = [1, 2, 3];
for (value: i32 in values) { print(value); }
```

---

## Lexical

The scanner ignores whitespace and comments between tokens:

- `//` and `#` start line comments, ending at a newline or end of file.
- `/* … */` encloses a block comment, which may span lines. Block comments do not nest: the first `*/` closes the comment. An unclosed block comment is a compile error.
- Comment markers inside strings are ordinary text. Strings may contain newlines.

```kawa
# A full-line comment.
let count = 4; // A trailing comment.
/* A block comment
   spanning two lines. */
let doubled = count /* comments separate tokens */ * 2;
let markers = "# and /* */ are text here";
```

**Keywords** (not identifiers): `and` `break` `continue` `elif` `else` `false` `fn` `for` `if` `in` `is` `let` `nil` `or` `return` `true` `while`.

Identifiers: `[A-Za-z_][A-Za-z0-9_]*`. Type names (`i32`, `str`, …) are ordinary identifiers in expression position unless they parse as a cast.

**Numbers** are decimal only: `[0-9]+` or `[0-9]+ "." [0-9]+`. A trailing dot is a number then `.` (`1.` is not a float). A leading dot is `.` (`.5` is not a number). No sign, hex, or exponent; unary minus is a separate token.

**Strings** are double-quoted. `\"` does not terminate the token. Unescapes: `\n` `\t` `\r` `\\` `\"`.

**No bitwise `&` `|`.** Those characters are only valid as `&&` and `||`. A lone `&` or `|` is a scanner error (`Unexpected character`).

**No trailing commas** in argument lists, parameter lists, type lists, array literals, or record field lists.

---

## Types

Closed set of base names, then zero or more `[]`, then an optional function-type suffix.

| Family | Names |
|--------|--------|
| Signed int | `i8` `i16` `i32` `i64` |
| Unsigned | `u8` `u16` `u32` `u64` |
| Float | `f32` `f64` |
| Numeric (unspecified width) | `number` |
| Other scalars | `bool` `str` `any` `void` |

Arrays: `i32[]`, `i32[][]`, … Function types write the return type first: `i64(i32, i32)` is a function returning `i64` and taking two `i32` parameters. `void` is a type (often a return annotation); the `nil` literal is the empty value.

Unknown type names are a compile error. `number`, `any`, and `void` are types but **not** cast prefixes.

Runtime values are still tagged coarsely (`number` / `bool` / `str` / array / record / function / `nil`). Width names (`i32`, `f32`, …) are declaration and cast annotations, not distinct runtime tags.

---

## Strict mode

`KAWA_COMPILE_ENFORCE_TYPE_NOTATIONS` requires type annotations on:

- `let` bindings
- function parameters
- function return types (`-> type`)
- record fields
- `for`-loop variables

Without the flag, those annotations are optional. Archives may set the flag so load matches compile-time behavior.

---

## Program and statements

A program is a sequence of statements. `{` at the **start of a statement** is a block. In expression position `{` is a record literal.

```text
statement  ::=  ;  |  block  |  let  |  if  |  while  |  for  |  fn
             |  return  |  break  |  continue  |  assignment  |  expression ;
```

`fn` is allowed in any statement position, including inside blocks. Nested `fn` inside another function is rejected. `return` is only valid inside a function. `break` / `continue` are only valid inside a loop.

A function that falls off the end returns `nil`. `return;` also yields `nil`.

---

## Declarations

```kawa
let hp: i32 = 100;
let name = "Player";          // annotation optional unless strict mode

fn add(a: i32, b: i32) -> i32 {
    return a + b;
}

fn log_only(msg: str) -> void {
    print(msg);
    return;
}
```

- `let` always takes `=` and a terminating `;`. There is no declaration without an initializer.
- Functions are **named only**. `fn` has no prefix (expression) rule, so `fn (...) { }` is not an expression.
- Parameter and return annotations are optional unless strict mode.
- Function names must not already exist in the current scope.

---

## Control flow

Conditions and loop headers take **parentheses**. Bodies are **blocks** (`{ … }`).

### `if` / `elif` / `else`

```kawa
if (hp > 0) {
    print("alive");
} elif (hp == 0) {
    print("down");
} else {
    print("dead");
}
```

**`else if` is rejected.** After `else` the parser requires `{`. Chain extra conditions with `elif`.

### `while`

```kawa
while (hp > 0) {
    hp -= 1;
}
```

### `for` (`in`)

```kawa
for (value: i32 in values) {
    print(value);
}
```

The iterated expression is an array. The loop variable may carry a type annotation (required in strict mode). There is no C-style `for (init; cond; step)`.

### `break` / `continue`

```kawa
for (value in [1, 2, 3, 4]) {
    if (value == 2) { continue; }
    if (value == 4) { break; }
    print(value);
}
```

Both require a trailing `;`. Illegal outside a loop. Nesting is capped (`KAWA_PARSER_MAX_LOOP_DEPTH` is 64).

---

## Assignment

Assignment is a **statement**, not an expression. `=` / `+=` / `-=` / `*=` / `/=` have no infix expression rule. You cannot write `a = b = 1` or `if ((x = 1))`.

Compound operators are only `+=` `-=` `*=` `/=`. There is no `%=` (or bitwise assigns).

Legal lvalues:

| Form | Example |
|------|---------|
| identifier | `hp = 10;` |
| `ident.field` | `player.name = "Bob";` |
| `ident[i]` (one or more indices) | `grid[0] = 1;` `tilemap[1][0] = 4;` |

Not assignment forms: chained fields (`a.b.c = …`) and mixed lvalues (`a.b[i] = …`).

A leading `ident.field` that is **not** followed by an assign operator is rewound and parsed as an expression statement. A leading `ident[…]` that is **not** followed by an assign operator is a **compile error** (it is not rewritten as an expression statement). Index reads belong in expressions: `let x = values[1];` or `print(values[1]);`.

```kawa
hp = 50;
hp += 10;
player.name = "Ada";
values[1] = 4;
tilemap[0][1] *= 2;
```

---

## Expressions

Precedence, weakest to strongest (Pratt table in `kawa_parser_state.c`):

| Prec | Operators |
|------|-----------|
| ternary | `?:` |
| or | `or` `\|\|` |
| and | `and` `&&` |
| equality | `==` `!=`  `is type` |
| comparison | `<` `>` `<=` `>=` |
| term | `+` `-` |
| factor | `*` `/` `%` |
| unary | `!` `-`  (no unary `+`) |
| postfix | call `f(…)`  index `[i]`  field `.name` |

`and` / `&&` and `or` / `||` short-circuit and coerce the result to `bool`. Falsy values are `nil` and `false` only (a `0` number is truthy).

`+` adds numbers, or concatenates when either operand is a `str` (the other side is stringified).

Ternary: `cond ? then_expr : else_expr`.

### `expr is type`

```kawa
let typed: i32 = 1;
print(typed is i32);     // true — declared type of a simple identifier
print(typed is number);  // true — any numeric declaration matches `number`
print(typed is any);     // true
print((1 + 2) is i32);   // false — width names are not runtime tags
print(nil is void);      // true
print("x" is str);       // true
```

`is` binds at equality precedence. The right-hand side is a **type**, not an expression (`x is i32[]`, `cb is i64(i32, i32)`).

- Simple identifier: compares the variable's **declared** type when present (`any` always matches; `number` matches any numeric declaration; otherwise the type strings must be equal). Untyped identifiers fall back to the runtime tag.
- Other expressions: runtime tag only — `any` (always), `void` (`nil`), `bool`, `str`, `number`, array types, function types. Specific numeric names (`i32`, `f32`, …) do **not** match a bare number value.

### Casts

Cast prefixes are `i8` `i16` `i32` `i64` `u8` `u16` `u32` `u64` `f32` `f64` `bool` `str`. Rank suffixes select array casts.

```kawa
let n: i32 = i32(wide);     // safe: error if the conversion would lose data
let t: i32 = i32!(wide);    // forced: may truncate
let row: i32[] = i32[](raw);
```

`number`, `any`, and `void` are not cast prefixes.

### Literals

```kawa
true; false; nil;
42; 3.5;
"line\n";
[1, 2, 3];
let person = {name: str = "Ada", age: i32 = 1};
```

Array and record literals are expressions. Empty `[]` and `{}` are legal. Field type annotations on records are optional unless strict mode.

---

## Records

```kawa
let player = {name: str = "Alice", age: i32 = 25};
print(player.name);
player.name = "Bob";
```

Fields are `identifier (":" type)? "=" expression`, comma-separated, **no** trailing comma. There is no `struct` keyword; the literal *is* the record.

Reading a missing field yields `nil` unless the compile flag `KAWA_COMPILE_ERROR_ON_STRUCT_READ_UNDEFINED` is set. Writing a missing field creates it unless `KAWA_COMPILE_ERROR_ON_STRUCT_WRITE_UNDEFINED` is set.

---

## Arrays

```kawa
let values: i32[] = [1, 2, 3];
print(values[1]);
values[1] = 4;
print(values.len);
print(values.empty);
print(values.first);
print(values.last);
```

Index and `.len` / `.empty` / `.first` / `.last` are postfix on the value. `Array.*` (and `Matrix.*` for nested arrays) are the mutating/query binds below. Strings expose the same `.len` / `.empty` / `.first` / `.last` properties and `s[i]` (character as a one-char string).

---

## Functions as values

Named functions are values. Function types appear in annotations and `is`:

```kawa
fn double(value: i32) -> i32 { return value * 2; }
fn map(input: i32[], callback: i32(i32)) -> i32[] {
    let output: i32[] = [];
    for (item: i32 in input) {
        Array.push(output, callback(item));
    }
    return output;
}
let cb: i32(i32) = double;
```

Argument, parameter, and field counts are capped at 255 (`KAWA_PARSER_MAX_ARG_COUNT`).

---

## Swizzles

Arrays and records support `.x` `.y` `.z` `.w` and `.r` `.g` `.b` `.a`, one to four components long (`v.xy`, `v.rgba`, `v.xwzw`). A one-component swizzle is a number; two or more build a record with `x`/`y`/`z`/`w` fields. **Exact record fields take precedence** over a swizzle of the same name.

---

## Standard library

Binds registered by `kawa_init_std_lib`. Each row is an actual bind: namespace name and the free-function alias (when one exists). Wrong arity or type typically returns `nil` rather than throwing.

### Console / runtime

| Call | Alias | Args |
|------|-------|------|
| `Console.print` | `print` | any number of values; writes a line |
| | `halt` | optional number exit code |

There is no `Console.halt`.

### Math

Numeric helpers take `number` values. `min` / `max` take one or more numbers. `random` is `()` → `[0,1)`, `(max)`, or `(min, max)`. `vec` is 2–4 numbers, or one existing vec. `mat` is 16 numbers, four vec4s, or one existing mat4. `quat` is four numbers or one existing quat.

| `Math.*` | Alias |
|----------|--------|
| `add` | `math_add` |
| `subtract` | `math_subtract` |
| `multiply` | `math_multiply` |
| `divide` | `math_divide` |
| `modulo` | `math_modulo` |
| `power` | `math_power` |
| `sqrt` | `math_sqrt` |
| `abs` | `math_abs` |
| `floor` | `math_floor` |
| `ceil` | `math_ceil` |
| `round` | `math_round` |
| `sin` | `math_sin` |
| `cos` | `math_cos` |
| `tan` | `math_tan` |
| `min` | `math_min` |
| `max` | `math_max` |
| `random` | `random` (not `math_random`) |
| `vec` | `vec` |
| `mat` | `mat` |
| `quat` | `quat` |

`Math.quat` is registered with the quaternion module; it is still `Math.quat` / `quat`.

### Vec

Vectors are records `{x, y, …}` (2–4 components) or numeric arrays of length 2–4. Color fields `r/g/b/a` are accepted when reading. Operations require matching length; `cross` is 3-wide only. `clamp` / `lerp` accept a scalar or a same-length vector for the extra arguments.

| `Vec.*` | Alias | Args |
|---------|--------|------|
| `add` | `vec_add` | `(a, b)` |
| `sub` | `vec_sub` | `(a, b)` |
| `dot` | `vec_dot` | `(a, b)` → number |
| `distance` | `vec_distance` | `(a, b)` → number |
| `len` | `vec_len` | `(v)` → number |
| `normalize` | `vec_normalize` | `(v)` |
| `mul_scalar` | `vec_mul_scalar` | `(v, s)` |
| `cross` | `vec_cross` | `(a, b)` 3-wide |
| `clamp` | `vec_clamp` | `(v, min, max)` |
| `lerp` | `vec_lerp` | `(a, b, t)` |
| `reflect` | `vec_reflect` | `(v, n)` |
| `project` | `vec_project` | `(v, n)` |

### Mat

4×4 matrices are row-major arrays of four rows (each a 4-wide array or vec4), or a flat 16-number array. Builders return the 4×4 row form.

| `Mat.*` | Alias | Args |
|---------|--------|------|
| `mul_vec` | `mat_mul_vec` | `(m, v4)` |
| `mul` | `mat_mul` | `(a, b)` |
| `transpose` | `mat_transpose` | `(m)` |
| `inverse_affine` | `mat_inverse_affine` | `(m)` |
| `make_translation` | `mat_make_translation` | `(x, y, z)` or `(vec3)` |
| `make_scale` | `mat_make_scale` | `(x, y, z)` or `(vec3)` |
| `make_rotation_x` | `mat_make_rotation_x` | `(radians)` |
| `make_rotation_y` | `mat_make_rotation_y` | `(radians)` |
| `make_rotation_z` | `mat_make_rotation_z` | `(radians)` |
| `lookAt` | `mat_lookAt` | `(eye, target, up)` |
| `perspective` | `mat_perspective` | `(fovy, aspect, znear, zfar)` |
| `ortho` | `mat_ortho` | `(left, right, bottom, top, znear, zfar)` |

`lookAt` is camelCase in the bind list.

### Matrix (array namespace)

Same implementations as `Array.*`, bound for nested arrays (`i32[][]` and the like):

| `Matrix.*` | Alias |
|------------|--------|
| `len` | `matrix_len` |
| `is_empty` | `matrix_is_empty` |
| `push` | `matrix_push` |
| `pop` | `matrix_pop` |
| `clone` | `matrix_clone` |
| `clear` | `matrix_clear` |
| `compact` | `matrix_compact` |
| `reserve` | `matrix_reserve` |

### Quat

Quaternions are `{x, y, z, w}` records (a 4-wide numeric array is also accepted when reading).

| `Quat.*` | Alias | Args |
|----------|--------|------|
| `mul` | `quat_mul` | `(a, b)` Hamilton product |
| `normalize` | `quat_normalize` | `(q)` |
| `slerp` | `quat_slerp` | `(a, b, t)` |
| `from_axis_angle` | `quat_from_axis_angle` | `(axis, angle)` — `axis` is a vec, array, or `{x,y,z}` |
| `from_euler` | `quat_from_euler` | `(rx, ry, rz)` XYZ intrinsic, radians |
| `to_mat4` | `quat_to_mat4` | `(q)` → 4×4 row array |

### Array

| `Array.*` | Alias | Args / notes |
|-----------|--------|----------------|
| `len` | `array_len` | `(arr)` → count |
| `is_empty` | `array_is_empty` | `(arr)` → bool |
| `push` | `array_push` | `(arr, value)` — typed arrays reject a mismatched value (`nil`) |
| `pop` | `array_pop` | `(arr)` → last element, or `nil` if empty |
| `clone` | `array_clone` | `(arr)` |
| `clear` | `array_clear` | `(arr)` — count to 0 |
| `compact` | `array_compact` | `(arr)` — drop `nil` holes |
| `reserve` | `array_reserve` | `(arr, n)` — grow **count** to `n`, filling new slots with `nil` |

`push` / `clear` / `compact` / `reserve` return the array. `pop` returns the element.

### Events

| `Events.*` | Alias | Args |
|------------|--------|------|
| `on` | `events_on` | `(name, fn)` or `(name, fn, owner)` |
| `once` | `events_once` | same; listener removes itself after one delivery |
| `off` | `events_off` | `(name, fn)` — first matching listener |
| `post` | `events_post` | `(name, arg)` — deliver now |
| `post_deferred` | `events_post_deferred` | `(name, arg)` — queue until flush |
| `flush` | `events_flush` | `()` — deliver deferred events |
| `remove_by_script` | `events_remove_by_script` | `(script_name: str)` |
| `remove_by_owner` | `events_remove_by_owner` | `(owner)` |
| `clear` | `events_clear` | `(name)` — drop every listener on that event |
| `count` | `events_count` | `(name)` → listener count |

Events are enabled by default in standalone Kawa and shared across scripts in the same VM. Hikari disables this standard-library bus at VM creation, so neither `Events.*` nor the `events_*` aliases are registered there. Hikari gameplay uses `Actors.send` / `Actors.emit` through the World dispatcher.

Event names are strings. `owner` is a string or a number. One-shot listeners remove themselves inline. Deferred events run when flushed (`Events.flush` or the host `kawa_events_flush`). Destroying a script removes listeners whose user-functions came from that script; `Events.remove_by_script` does the same by source name.

```kawa
fn on_hit(payload: any) { hp -= payload.damage; }
Events.on("Enemy.hit", on_hit, "player");
Events.once("Enemy.alert", on_alert);
Events.post("Enemy.hit", {damage: i32 = 10});
Events.post_deferred("tick", 1);
Events.flush();
Events.off("Enemy.hit", on_hit);
Events.remove_by_owner("player");
Events.remove_by_script("player.kawa");
```

---

## C embedding

```c
#include "kawa.h"
kawa_handle_t vm = kawa_create(NULL);
kawa_init_std_lib(vm);
kawa_compilation_options_t options = {0};
options.emit_symbols = true;
kawa_script_t script = kawa_script_compile(vm, source, "main", options);
kawa_scope_t scope = kawa_scope_create(vm, NULL);
kawa_result_t result = kawa_script_run(script, scope);
kawa_events_flush(vm, scope);
kawa_scope_destroy(scope);
kawa_script_destroy(script);
kawa_destroy(vm);
```

`kawa_create(NULL)` selects default VM options, including `enable_events = true`.
To customize them, start from the defaults:

```c
kawa_vm_options_t vm_options = kawa_vm_options_default();
vm_options.enable_events = false;
kawa_handle_t vm = kawa_create(&vm_options);
kawa_init_std_lib(vm);
```

Options are copied at creation and stay fixed for that VM's lifetime. Disabling
Events omits both its namespace and free-function aliases; math, arrays, and the
other standard libraries remain available. Repeating `kawa_init_std_lib` cannot
re-enable Events. This is an embedding option, separate from script compilation
flags. Hikari explicitly disables Events on every VM creation, including resets.

Bind native callbacks with `kawa_bind_function` or `kawa_bind_namespace_function`. Engine code should use `src/hikari/src/scripting/kawa/public.zig`, not this raw C layer.

---

## Offline compiler (`kawac`)

Shinra and CI cook scripts outside the game process:

```text
src/kawa/tools/kawac  →  bin/kawa/<platform-arch>/kawac  →  bin/game/tools/kawac (product stage)
kawac input.kawa output.kawabc
```

Compile flags must match engine scene scripts (`emit_symbols`, `KAWA_COMPILE_ENABLE_SIMD_TIER1`; archives may set `KAWA_COMPILE_ENFORCE_TYPE_NOTATIONS` for strict annotations).

---

## VM script slots (embedding)

- Each distinct compiled script occupies one of `KAWA_MAX_SCRIPTS` (256) VM slots until destroyed.
- The engine **caches** compiled bytecode across scene unload; actor instances drop, session scripts stay.
- Destroying a compiled script while live user-function meta strings still borrow its chunk table can **poison** the VM — the engine promotes meta strings on destroy when refs remain; avoid ad hoc destroy/reload loops in embedders.
- Editor **Stop** retires the disposable Play world and returns to the retained Edit world. Register native bindings for every world attachment; do not assume one VM is shared by both worlds.
