Skip to content
hikari
RenderingEditorAIToolchainDocumentation
GitHub
hikari

© 2026 Flying Rat Studio.
All rights reserved.

Explore the engineDocumentationContributorsLicenseBack to top
Documentation / Language reference
Browse docs
Overview
Tutorials16
OverviewFirst game projectFirst entityAuthored user_data and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesPlay, Edit, and scenesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides16
OverviewDevelopment guideGameplay APIChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / UnrealActor and component lifecycle
Systems28
OverviewArchitectureApplication lifecycleFrontends and driversPlatforms and supportSession services and cross-scene stateScenes and gameplayActor communication (hi.actors)Game-facing refsSave / replication wire versionCoordinate space and camera conventionsRenderingRenderer architecture mapFrame governorGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)UI and editorEditor asset hot reloadEditor Project Selector
Language reference2
OverviewAkari language referenceKawa language reference
Engine overview
Start exploring
  • No matching sections. Try fewer words or another topic.
NavigateEnter Openesc Close
Overview
Tutorials16
OverviewFirst game projectFirst entityAuthored user_data and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesPlay, Edit, and scenesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides16
OverviewDevelopment guideGameplay APIChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / UnrealActor and component lifecycle
Systems28
OverviewArchitectureApplication lifecycleFrontends and driversPlatforms and supportSession services and cross-scene stateScenes and gameplayActor communication (hi.actors)Game-facing refsSave / replication wire versionCoordinate space and camera conventionsRenderingRenderer architecture mapFrame governorGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)UI and editorEditor asset hot reloadEditor Project Selector
Language reference2
OverviewAkari language referenceKawa language reference
Engine overview
Language reference12 min read

Kawa language reference

On this page
On this pageLexicalTypesStrict modeProgram and statementsDeclarationsControl flowif / elif / elsewhilefor (in)break / continueAssignmentExpressionsexpr is typeCastsLiteralsRecordsArraysFunctions as valuesSwizzlesStandard libraryConsole / runtimeMathVecMatMatrix (array namespace)QuatArrayEventsC embeddingOffline compiler (kawac)VM script slots (embedding) Back to top

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.

Editor highlighting: tools/vscode-extensions/kawa (VS Code / Cursor); tools/nvim-plugins/kawa (Neovim / Vim). Concrete syntax: 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 strips whitespace and // line comments. There are no block comments. Strings may contain newlines.

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.

FamilyNames
Signed inti8 i16 i32 i64
Unsignedu8 u16 u32 u64
Floatf32 f64
Numeric (unspecified width)number
Other scalarsbool 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
break;
continue;

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:

FormExample
identifierhp = 10;
ident.fieldplayer.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):

PrecOperators
ternary?:
oror ||
andand &&
equality== != is type
comparison< > <= >=
term+ -
factor* / %
unary! - (no unary +)
postfixcall 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];
{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

CallAliasArgs
Console.printprintany number of values; writes a line
haltoptional 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
addmath_add
subtractmath_subtract
multiplymath_multiply
dividemath_divide
modulomath_modulo
powermath_power
sqrtmath_sqrt
absmath_abs
floormath_floor
ceilmath_ceil
roundmath_round
sinmath_sin
cosmath_cos
tanmath_tan
minmath_min
maxmath_max
randomrandom (not math_random)
vecvec
matmat
quatquat

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.*AliasArgs
addvec_add(a, b)
subvec_sub(a, b)
dotvec_dot(a, b) → number
distancevec_distance(a, b) → number
lenvec_len(v) → number
normalizevec_normalize(v)
mul_scalarvec_mul_scalar(v, s)
crossvec_cross(a, b) 3-wide
clampvec_clamp(v, min, max)
lerpvec_lerp(a, b, t)
reflectvec_reflect(v, n)
projectvec_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.*AliasArgs
mul_vecmat_mul_vec(m, v4)
mulmat_mul(a, b)
transposemat_transpose(m)
inverse_affinemat_inverse_affine(m)
make_translationmat_make_translation(x, y, z) or (vec3)
make_scalemat_make_scale(x, y, z) or (vec3)
make_rotation_xmat_make_rotation_x(radians)
make_rotation_ymat_make_rotation_y(radians)
make_rotation_zmat_make_rotation_z(radians)
lookAtmat_lookAt(eye, target, up)
perspectivemat_perspective(fovy, aspect, znear, zfar)
orthomat_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
lenmatrix_len
is_emptymatrix_is_empty
pushmatrix_push
popmatrix_pop
clonematrix_clone
clearmatrix_clear
compactmatrix_compact
reservematrix_reserve

Quat

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

Quat.*AliasArgs
mulquat_mul(a, b) Hamilton product
normalizequat_normalize(q)
slerpquat_slerp(a, b, t)
from_axis_anglequat_from_axis_angle(axis, angle) — axis is a vec, array, or {x,y,z}
from_eulerquat_from_euler(rx, ry, rz) XYZ intrinsic, radians
to_mat4quat_to_mat4(q) → 4×4 row array

Array

Array.*AliasArgs / notes
lenarray_len(arr) → count
is_emptyarray_is_empty(arr) → bool
pusharray_push(arr, value) — typed arrays reject a mismatched value (nil)
poparray_pop(arr) → last element, or nil if empty
clonearray_clone(arr)
cleararray_clear(arr) — count to 0
compactarray_compact(arr) — drop nil holes
reservearray_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.*AliasArgs
onevents_on(name, fn) or (name, fn, owner)
onceevents_oncesame; listener removes itself after one delivery
offevents_off(name, fn) — first matching listener
postevents_post(name, arg) — deliver now
post_deferredevents_post_deferred(name, arg) — queue until flush
flushevents_flush() — deliver deferred events
remove_by_scriptevents_remove_by_script(script_name: str)
remove_by_ownerevents_remove_by_owner(owner)
clearevents_clear(name) — drop every listener on that event
countevents_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 may rebuild the VM; game registerScriptNatives bindings are re-applied on the fresh VM.
PreviousAkari language reference

Documentation follows the current engine checkout.

Snapshot cc148c75Source docs/reference/kawa-language.md
On this pageLexicalTypesStrict modeProgram and statementsDeclarationsControl flowif / elif / elsewhilefor (in)break / continueAssignmentExpressionsexpr is typeCastsLiteralsRecordsArraysFunctions as valuesSwizzlesStandard libraryConsole / runtimeMathVecMatMatrix (array namespace)QuatArrayEventsC embeddingOffline compiler (kawac)VM script slots (embedding) Back to top