Kagari
Type to search documentation.

Syntax

Values, References and Closures

Tentative. This chapter fixes the memory model. It is simpler than Rust’s on purpose: no lifetimes, no borrow checker and no moves.

Value semantics

Every type is a value type. Assignment, passing an argument and returning a value copy it; changing the copy never changes the original. The implementation may share storage and copy on write, but a program cannot observe it.

kis
struct Point { x: i32, y: i32 }
fn demo() -> i32 {
let a = Point { x: 1, y: 2 };
let mut b = a; // a copy
b.x = 10;
a.x // still 1: `a` is unchanged and still usable
}

There are no moves and no destructors. A value is gone when nothing holds it.

Places and mutability

A place is something that holds a value: a local, a parameter, a story variable, a field or element of one of these, or the target of a reference (*r). Assignment, compound assignment, &mut and a method with a &mut self receiver need a mutable place:

  • a let mut local or a mut parameter;
  • a story variable (static mut), in pushdown fn code;
  • a field or element of a mutable place;
  • a place reached through &mut.

A place reached through &T, a local or parameter without mut, a constant and a handle are read-only. Changing them is E3017.

References

&T and &mut T let code read or change a value without copying it. &x and &mut x take a reference to the place x, and *r is the place a reference points to. Field access and method calls look through references automatically.

kis
fn bump(counter: &mut u32) {
*counter += 1;
}
fn demo() -> u32 {
let mut n: u32 = 0;
bump(&mut n);
let r = &mut n;
let s = r; // copies the pointer, not the value
*s += 1;
n // 2
}

A reference is a shared pointer to a place. A reference keeps its place alive, and copying a reference copies the pointer, not the value.

  • In fn and const fn code, references are ordinary values: they may be bound to locals, stored in fields and collections, returned, and captured by closures.
  • Writes through one reference are visible through every other reference to the same place. There is no borrow checker: two &mut to one place may coexist. The game runs in a VM, so this can only surprise the program, never corrupt memory.
  • &mut T converts implicitly to &T.
  • == on references compares the values they point to.
  • The type of a const item, a story variable or a handle cannot contain a reference (E3012).

Render functions show the common pattern: the screen is passed in by value, changed through methods that take &mut self, and returned.

kis
const HANAKO: EntityId = EntityId(216);
const fn hanako_appears(mut screen: Screen) -> Screen {
screen.entity(HANAKO).opacity().change(1.0).apply_animation(tween(ms(300)));
screen
}

References in the story

In a pushdown fn, references are second-class: they can be passed down, never kept.

  • A reference type may appear only as the type of a parameter, including a method receiver (&self, &mut self). A local, a field or a captured variable cannot hold one (E3014).
  • A function may return a reference only if its receiver is one; the result lives until the end of the statement that produced it (E3014).
  • Exclusivity. In one call, a place passed as &mut may not also be passed as any other argument, so two parameters never alias when one of them can change (E3016).
kis
static mut TRUST: Int<0, 5> = 0;
static mut DOUBT: Int<0, 5> = 0;
pushdown fn shift(from: &mut Int<0, 5>, to: &mut Int<0, 5>) {
*from -= 1;
*to += 1;
}
pushdown fn reassure() {
shift(&mut DOUBT, &mut TRUST); // fine
shift(&mut TRUST, &mut TRUST); // E3016: the same place twice
}

Because a reference never outlives the call it was passed to and never aliases another argument, a pushdown fn that takes &mut x behaves exactly as if it received a copy of x and the caller stored the result back on return. A &mut argument of a pushdown fn is an in-out parameter of a finite type, and story state never holds a pointer. This keeps references harmless for the analyses of the story.

Closures

kis
fn scaled(values: [i32; 3], factor: i32) -> [i32; 3] {
let scale = |v: i32| v * factor; // captures a copy of `factor`
let clamp = |v: i32| -> i32 { if v > 100 { 100 } else { v } };
[clamp(scale(values[0])), clamp(scale(values[1])), clamp(scale(values[2]))]
}

A closure is written |parameters| body. Parameter types may be omitted when the context gives them. With an explicit return type -> R, the body must be a block. There is no move.

A closure captures by value: it copies the local variables it uses when it is created. Changing a captured copy inside the closure does not change the original; shared mutable state goes through references, story variables or signals. Capturing a reference copies the pointer, so writes through it reach the original place.

A closure has a kind, like a function. Its kind is the kind of the closure type it is passed to or stored as; without one, it is the kind of the function that contains it. The body is checked with the rules of that kind. What may be done with a closure depends on the kind of the code that creates it:

KindClosures areMay be
fnfirst-classstored, returned, put into UI values (handlers)
const fnfirst-classstored, returned
pushdown fnsecond-classpassed as an argument and called; never stored, returned or captured by another closure (E3015)

A closure in the story is an instant procedure: it cannot run a step (E3002) or call a scene (E3003), and it has no loop or while (E3004).

kix
static mut AFFECTION_MIO: Int<0, 5> = 0;
pushdown fn twice(f: pushdown Fn()) {
f();
f();
}
#[scene]
pushdown fn Compliment() {
twice(|| AFFECTION_MIO += 1);
s!(t!["Mio smiles."]);
}

Closures are never Eq (E2017).

Why closures in the story are second-class

A first-class closure in the story would let story state contain closures that contain closures, without bound. That is a higher-order pushdown system: still decidable, but the cost of analysis grows by an exponential with each order (Ong, On model-checking trees generated by higher-order recursion schemes, LICS 2006). A second-class closure is a function plus finite captured values that disappear when the call returns, so the story stays an ordinary pushdown system.

Closure and pointer types

Function pointer types and closure types carry the kind of the code they call:

TypeHolds
fn(A) -> Ra named fn
const fn(A) -> Ra named const fn, or a tuple struct or tuple variant constructor
pushdown fn(A) -> Ra named instant procedure
scene fn(A) -> Ra named scene (tentative syntax; scene is a keyword only here)
Fn(A) -> R, const Fn(A) -> R, pushdown Fn(A) -> Ra closure or function of that kind (tentative syntax)

The kind must match exactly: a const fn is not a value of type fn(A) -> R, and a scene is not a value of type pushdown fn(A) -> R. These conversions apply implicitly:

  • A named function or constructor converts to a function pointer of its kind.
  • A named function or a function pointer converts to the closure type of the same kind. Scenes have no closure type.
  • A closure that captures nothing converts to a function pointer of the same kind; this is how a render is written inline (see Inline renders). A closure that captures a variable cannot (E2036).
kis
const fn double(x: u32) -> u32 { x * 2 }
fn demo() -> Option<u32> {
let f: const fn(u32) -> u32 = double;
let wrap: const fn(u32) -> Option<u32> = Some; // a variant constructor
let g: const Fn(u32) -> u32 = f; // pointer to closure type
wrap(g(f(1)))
}

A function pointer is a value of a finite set, the named functions of the program, so pushdown fn, scene fn and const fn pointers are finite and may be held by the story. Function pointers compare by identity.

Calls through pointers and closures follow the same call rules as direct calls, because the type tells the compiler the kind of the callee.

Last updated Oct 29, 2026