Core Concepts
Function Kinds
Every function has one of three kinds. The kind is written before fn, and it is the only
place where the language restricts what code may do.
fn open_config() { nav::call(Config) } // Turing-complete, runtime#[scene] pushdown fn Confession() -> Answer { … } // the storyconst fn fade_in(mut screen: Screen) -> Screen { … } // pure, compile time| Kind | May use | Evaluated |
|---|---|---|
fn | Everything except story variables | At runtime |
pushdown fn | Finite types at runtime, story variables; steps only in a #[scene] | At runtime, inside the story |
const fn | Every type; no story variables, no settings values, no meta, no signals, no I/O | At compile time where needed; may also be called at runtime |
The compiler never infers a kind. A function is const only when it says so.
Call rules
| Caller ↓ / callee → | fn | pushdown fn | const fn |
|---|---|---|---|
fn | yes | no: it can only start an entry scene | yes |
pushdown fn | no | yes | yes, see below |
const fn | no | no | yes |
A call that breaks this table is E3001. Inside the story a further rule separates
scenes from instant procedures: only a #[scene]
may run steps (E3002), call another scene (E3003) or draw a random value (E3024).
The rules hold through function pointers and closures too, because their types carry the kind: a
value of type pushdown fn() -> R can only point to a pushdown fn, so the story cannot reach
Turing-complete code by calling through a pointer (see
Values, References and Closures).
The table per kind, as a summary:
| Kind | Calls | Holds at runtime | Steps | Story variables |
|---|---|---|---|---|
fn | fn, const fn; starts entry scenes | anything | no | no |
#[scene] pushdown fn | scenes, instant procedures, const fn (constant or finite arguments) | finite types | yes | read and write |
pushdown fn (instant procedure) | instant procedures, const fn; no recursion, only for loops | finite types | no | read and write |
const fn | const fn | anything | no | no |
pushdown fn
A pushdown fn is part of the story. It may:
- take, hold and return values of finite types only (
E3008); - read and write story variables;
- call other
pushdown fnandconst fn.
A pushdown fn marked #[scene] is a scene. Only a scene may run steps, which
suspend the story until the player gives input, and call other scenes, including itself. Every
loop and every cycle of calls in a scene must pass through a step (see
Termination). A pushdown fn without #[scene] is an instant
procedure: it never steps, never recurses and has only bounded loops, so it always finishes
without input. Either way, the code between two inputs always finishes.
Scenes call each other directly, like Kotlin’s suspend functions: there is no .await. A step
anywhere below a call suspends the whole story, and input resumes it at the same point.
const fn
A const fn is pure and deterministic: its result depends only on its arguments. It cannot read
story variables (E3007), settings values, meta (E3013) or signals, and it has no I/O. Inside,
it is Turing-complete: loops, recursion, Rust integers, Rat, Decimal, String, Vec.
The compiler evaluates a const fn when its result is needed at compile time: constants,
render functions, and calls from the story. Evaluation has a fixed budget of
1,000,000 evaluation steps (tentative: the budget may become a compiler setting); running out
is E4003 at the call that needed the value. A const fn called from fn code at runtime is an
ordinary function call.
Calling const fn from the story
A pushdown fn may call a const fn when every argument is either a
constant expression or of a finite type:
- All arguments constant. The compiler evaluates the call at compile time and replaces it by
its value (folding). The value may be of any type.
fade(ms(500))is such a call. - Some arguments finite and runtime. The compiler tabulates the function: it evaluates it
on every combination of argument values the call can receive. This is a compile-time proof that
the call finishes without error and gives a finite result on every possible input; during play
the function is still called normally with the actual arguments. The result must be of a finite
type (
E3008). A call with more than 65,536 argument combinations (tentative: may become a compiler setting) isE4004.
const fn mood(affection: Int<0, 5>, met: bool) -> Mood { match (affection, met) { (4..=5, true) => Mood::Warm, (_, true) => Mood::Polite, _ => Mood::Stranger, }}
#[scene]pushdown fn Greeting() { match mood(AFFECTION_MIO, MET_MIO) { // tabulated: 6 × 2 entries Mood::Warm => s!(t!["\"There you are!\""]), Mood::Polite => s!(t!["\"Hello.\""]), Mood::Stranger => s!(t!["\"…Who are you?\""]), }}For you as an author this means: write helper logic for the story as an ordinary const fn with
small finite parameters such as bool, enums and Int<lo, hi>. An error in the function (an
overflow, a division by zero, a runaway loop) is reported at compile time for the argument values
that trigger it, never during play.
fn
A plain fn is Turing-complete runtime code: the game system, its UI, saves, settings and meta.
It cannot call a pushdown fn and cannot read story variables. Its only way into the story is
starting an entry scene, and its only view of a running story is the
Narrative handle.
Finite types
The type of every local, parameter, return value and story variable of a pushdown fn must be
finite. A type is finite when it has finitely many values that the compiler can enumerate:
| Finite | Not finite |
|---|---|
bool, (), ! | Rust integers (u8, i32, …), Rat, Decimal |
Int<lo, hi> | String, Vec<T> |
| Enums whose variants carry only finite data | Screen and the other builtin handle types |
Structs, tuples and arrays [T; N] of finite types | First-class closures |
Option<T> of a finite T | Signals, settings and meta values |
Text (its values are the constant texts of the program) | Types that contain themselves (recursive types) |
File asset types ImageAsset, AudioAsset, VoiceAsset, VideoAsset, FontAsset (their values are the asset constants of the program) | EntityId, Part, Mark, Duration, Color (they hold Rust integers); shader types such as ColorShader<A> |
pushdown fn, scene fn and const fn pointers (their values are the named functions of the program) | |
&T, &mut T of a finite T, as parameters |
A constant expression of any type may still appear in a pushdown fn, because it is folded at
compile time. The full list for standard library types is in the STD Reference,
Compiler Rules for std Items.
Why this works
The engine needs two properties of the story: every step finishes, and questions such as “can
this step be reached with MET_B false?” can be answered by the compiler. Both hold for a
pushdown system over finite data, and the kinds make the story exactly that:
- The story is closed. A
pushdown fncan only callpushdown fnandconst fn, and aconst fncalled from the story is either folded to a constant or tabulated, that is, proven to finish with a finite result on every input it can receive. - The only door is the entry. Turing-complete code enters the story only by starting an entry scene, with finite arguments that the save records and the analyses hold for. It gets back only the entry scene’s return value, plus meta the story writes and never reads.
- Frames stay finite. A stack frame holds a return point and finite locals; references and closures in the story are second-class, so they never become part of the state.
- Function pointers are finite. There are finitely many named functions, so a pointer is a value of a finite set.
The system and the presentation may then compute freely: no story analysis depends on them. The background is in Philosophy: Analyzable by Construction.
Last updated Oct 29, 2026