kama keyword reference
Every type declaration begins with the type marker followed by one of six kinds: type value
(owns nothing, copies), type resource (owns/identity, move-only, RAII-dropped), type view (a
non-owning, stack-only borrow — a slice/span; C# ref struct), type enum (a sum type),
type contract (a public-only guarantee — an interface), or type intrinsic (gives a built-in
type a contract's methods). The kind words value/resource/view/contract/intrinsic are
contextual, not reserved — they mean a kind only right after type, and stay ordinary identifiers
everywhere else (int32 value = 5;, View v = a.viewMut();); enum is reserved. See the
type-model doc and the rows below.
Legend for the status column:
- ✅ implemented — parses, lowers, behaves correctly (every row below, the language surface being complete)
Status table#
| Keyword(s) | Status | Notes |
|---|---|---|
bool int8/16/32/64 uint8/16/32/64 float32 float64 isize usize void string |
✅ | core types. isize/usize (ptrdiff_t/size_t) are platform-varying, as are clong/culong (C's long/unsigned long, 4 bytes on Windows and 8 elsewhere — for an extern mirroring a C long); cchar is C's char as a raw-pointer element only (UnsafeConstPtr<cchar> = const char*, what string.cstr() returns), never a value — isize is the size type: every length()/count()/index is one, and usize is reserved for the C ABI (sizeof, an allocation, an extern fn). Bare int and double are NOT types — write int32/isize and float64. string is a fat value: a borrowed literal/view (no alloc) or a heap-owned RAII string (freed on drop); UTF-8 bytes throughout (see SPEC §Strings) |
char |
✅ | a Unicode scalar value (codepoint), a distinct primitive backed by uint32 (NOT a numeric type — no silent int mixing). Literals 'a' / '\n' / '\u{…}'; equality + ordering; char↔int32 via cast. Yielded by .chars() |
type |
✅ | the type-declaration marker — every type is type <kind> Name { … } (parallel to fn). The kind is value/resource/view/enum/contract/intrinsic (+ the virtual/abstract/final qualifiers after type). Greppable (grep '^type '). Contextual: it leads a declaration only where one can begin, and may name a field, local or parameter elsewhere (C headers call fields type) |
value resource view contract intrinsic |
✅ | contextual, not reserved. They name a kind only right after type; everywhere else they are ordinary identifiers (int32 value = 5;, a field/method named resource). type value = owns nothing, copies, sealed, fields private-or-public per field, no dtor. type resource = owns/identity, move-only, RAII-dropped, fields private-only. type view = a non-owning stack-only borrow (a slice/span — the flagship is View<T>; C# ref struct): codegens like a value (inline, copies, no dtor) but the escape check forbids it as a field, collection element, or an escaping return; owns nothing (no ~dtor, no owning fields), fields private-only. type contract = public-only guarantee (an interface): methods only, no bodies/fields/ctor/dtor; satisfied via implements; may refine another via implements (type contract A for value, resource implements B). A contract carries a mandatory for clause naming which kinds may implement it — a comma list of any of value / resource / view / enum / intrinsic (not contract: contract-to-contract is refinement). type intrinsic <int32, int64> implements C { … } = one block of methods that gives a set of built-in types a contract — how the prelude makes int32 Hashable in kama rather than in the compiler; it declares no type and no state |
enum |
✅ | a type enum is a plain set or a tagged union (variants carry payloads; may be generic), consumed by match. Like every kind it takes an implements clause and carries the methods that satisfy it, after a ; that separates them from the variants. enum is the one kind word that is a reserved keyword rather than a contextual one |
match case |
✅ | match is the one construct for branching on an enum — plain enums, tagged unions, and Optional/Result alike. Value-producing (statement or expression position), compile-time exhaustive, with a _ wildcard; case heads each arm and binds payloads (case Some(value: v): …). The subject may be a variable, method call, or free-function call. Arbitrary-integer branching uses if/else if — there is no switch |
operator |
✅ | operator overloading. Set: arithmetic + - * / %, bitwise & | ^ << >>, unary - ! ~, ++/--. Arity picks the form: 0 params = unary on this (Vec2 operator-()), 1 = binary method (Vec2 operator+(Vec2 rhs), this is the left operand), 2 = binary free/static form (Vec2 operator*(int32 s, Vec2 v) — enables scalar-on-the-left). Lowers to a direct call of the type's op_add function (&lhs, rhs). Type-based dispatch: a type may carry several operator* distinguished by operand type — mat*vec + mat*mat, v*s + s*v — like C++/C#/Rust; only the same symbol AND operand type collide. Chaining (a + b + c, in any position incl. a condition — nested rvalues wrap in a C99 compound-literal array); compound assignment pos += vel ≡ pos = pos + vel. The six COMPARISON operators are NOT declared here — they come from a contract: ==/!= lower to Equatable.equals, </>/<=/>= to Comparable.compareTo, and declaring operator== & co. is a hard error naming the implements form. (Operators a generic bound must NAME are contracts; the rest are concrete-type ergonomics.) Equality stays explicit — no auto structural equality — but @generate(Equatable, Hashable) synthesizes the memberwise walk on request. true/false conversion operators out of scope. In a contract, an operator is a bound for generic math |
extends implements this base Base |
✅ | base.m() reaches an inherited member non-virtually (named args; same visibility rules as this.). this.base = Base.<ctor>(…); is a derived ctor's mandatory FIRST statement — it installs the base part, built by the base's own ctor. Base names the base type contextually, as This names the enclosing one (a real type called Base wins); this.base may only be ASSIGNED |
ref out |
✅ | Two different promises, both passed by pointer. ref T x is a read-write borrow of an already-live value; its call-site marker is optional. out T x is a fill: the callee must assign it on every path before returning and may not read the incoming value, and the call site must say out (divmod(q: out quotient)) — both lower to the same T*, so without the marker neither a reader nor the caller's definite-assignment analysis could tell a borrow from a fill. out is what lets a slot be filled by a callee. "Every path" is a real flow merge: both arms of an if/else count, a lone if does not |
slot |
✅ | a declared HOLE — slot T x; is storage with no value in it yet: illegal to read until definitely assigned, and no destructor is emitted while it stays unassigned, so "drop only if live" is proven statically rather than checked at runtime. A local with no initializer and no slot is an error; so is slot with an initializer. It does not run the type's default ctor (spell T x = T.empty();). Filled by a whole assignment, a field write, an out argument, a method call on it, or addr(of: x). Assigned on only some paths → it drops (the storage is always valid). Not Optional<T>: no tag, no drop, nothing at runtime. See SPEC.md Uninitialized storage |
file |
✅ | the FILE GATE — file @compileFor(FLAG); as a unit's FIRST line (ahead of import) gates the WHOLE file the way @compileFor gates one declaration: a build the flags exclude never admits the file at all, so a native-only source costs a wasm build nothing. Excluded, not deactivated — its declarations, its export, its imports and its contribution to its module do not exist for that build — but it IS still parsed, so a syntax error in it is an error on every target. Takes @compileFor and nothing else. A file the CLI NAMES and whose gate excludes it is an error; one the build COLLECTED is skipped silently. Contextual: everywhere but that first position file is an ordinary name (File file = …). See SPEC.md Conditional compilation |
This |
✅ | the self-type (contextual, not a lexer keyword). Inside a type's own body it names that type — fn This clone(), implements Comparable<This> — and needs no declaration, because nothing is erased there. A contract may not name This in a signature; it declares the self-type as a pinned type parameter instead (type contract Comparable<T is This>) and writes T. The reason is erasure: a bound monomorphizes and can substitute This, a contract value has thrown the type away, so a vtable slot would have to bind it to the contract while the function behind the slot bound the implementing type. A pinned parameter is a real type argument that resolves the same on both sides. Distinct from the lowercase this value. Outside a type/contract it's a clean error |
generics <T> / bounds <K: A + B> / turbofish f::<T>() |
✅ | monomorphized (zero-cost, no boxing). Generic functions (type args inferred; a return-only generic uses turbofish f::<int32>() — the :: before < is unambiguous) and types (type value Pair<A,B>, generic resource), multi-param + nested (Box<Pair<int32,int32>> — the >> split means no space). Contract bounds <K: Hashable + Comparable<K>> (+ = AND) → the body may call the contracts' methods on a type-param, lowered to static direct calls; each concrete arg is checked to satisfy its bounds |
new |
✅ | the heap operator. Owned<Box> p = new Box.make(...) boxes the element type on the heap; a stack value drops new (Box b = Box.make(...)). Construction is always a named ctor called dot-on-type — the nameless Type(...) / new Type(...) form is a hard error. new into a plain value type is an error. A fallible ctor composes: new File.open(...) → Result<Owned<File>, E>. Infallible — panics on OOM (see try new). |
ctor |
✅ | the one kind of constructor — a named factory returning the type (or Result<This, E>), implicitly type-associated (no fn, no static). Called dot-on-type: Buffer.make(size: 8) constructs, while :: stays scope resolution (Vec3::dot, Enum::Variant) — so .make( greps for construction and catches nothing else. A generic ctor puts the turbofish on the TYPE (T::<Args>.make(…)). Nothing is constructible by default: no ctor and no of/zero opt-in means the type cannot be built. Every field must be assigned before it returns (or carry a field default T x = …;) — complete-by-delegation when the ctor ends in return Other.make(…). Reuse is an ordinary call: no init hook, no designated/final ctor, no funnel. A self-returning static fn and a class-named ctor are both rejected as disguised constructors. A contract may REQUIRE a ctor (ctor adopt(UnsafePtr<T> raw)), which is how generic code constructs through a type parameter — and why there is no privileged Default contract. deserialize and Copyable's copy are ordinary ctors. See SPEC.md Construction |
try new |
✅ | non-panic construction (MCU step 5). Optional<Owned<Box>> b = try new Box.make(...) yields None on OOM instead of panicking — the ONE fallible construction entry (new stays the infallible sugar; there is no tryAllocate). A typed local initializer or a return of an Optional<Owned<T>>; the placement form try new(allocator: a) Box.make(...) draws from a custom allocator. try also prefixes cast (try cast<T>(x), below). |
asm |
✅ | inline assembly (MCU step 6a). asm("wfi"); — a statement taking one string literal — lowers to __asm__ __volatile__("…" : : : "memory"). Requires an enclosing unsafe fn (the sole raw-operation seam; outside it is a hard build error). Always volatile (never elided/reordered) and always a full compiler memory barrier — so cpsid i/dsb/dmb order memory correctly by default. Multiple instructions go in one \n-separated string (asm("cpsid i\n\tdsb")). No interpolation; the text is target-specific (portability is the user's, like FFI). Named helpers (wfi(), disable_interrupts()) are an ordinary library built on this primitive. See SPEC.md Inline assembly |
virtual override |
✅ | an overridable method is written protected (never public/private), and its type must opt in as a type virtual(maxDepth: N) resource/type abstract(maxDepth: N) resource (extension is a resource concern — a value is sealed); override only re-seats a real base slot |
give copy |
✅ | hand-off markers. give = move (invalidates the source); copy = retain (Shared/Weak) or duplicate. Ride a named value (a fresh new/ctor/call result needs none) and work uniformly in initializer, assignment, argument, and return positions. Every owning kind is movable; a bare hand-off follows the kind's default, a marker overrides. Defaults by kind: Owned → move (copy is an error — unique), Shared/Weak → retain (bare = refcount++; give moves the handle, copy = explicit retain — implements Copyable<This>(bare: copy)), value/primitive → copy (a value's "move" is a copy), a plain resource (move-only) → move (bare hand-off moves; copy is an error until it opts into Copyable). A collection requires the marker: give = move (buffer), copy = deep copy — element-wise: a bitwise-copyable element copies memberwise, a Copyable-resource element deep-copies via its own copy ctor; a non-Copyable resource element is rejected. Copyable resource: a resource opts into copy nominally — implements Copyable<This>(bare: …) plus a public copy CTOR ctor copy(ref This source) (the contract declares it ctor copy(ref T source) on Copyable<T is This>; a type's own body still writes This) (a lone copy ctor without the implements does not opt in; it is a ctor because a copy IS a new object, and the source is borrowed); opting in must declare its bare default — Copyable<This>(bare: give) or Copyable<This>(bare: copy) (an implements Copyable<This> without (bare: …) is a compile error) — then a bare hand-off follows that default, copy x deep-copies via the copy ctor, give x moves. Since copy/give are only markers in expression position, they're contextual — usable as member names (so the opt-in ctor is literally copy). Full behavior matrix (every cell → fixture) in SPEC.md |
addr drop panic assert debugAssert |
✅ | the call-site intrinsics — the compiler lowers each at the call because it needs the call site (the source text and file:line, or the operand's storage): addr(of: x) (a raw pointer, unsafe fn only), drop(ptr: p), panic(msg:), assert(cond:, msg:), debugAssert(cond:, msg:) (stripped under --release). Reserved words, legal only in call position, and always bare (global::assert was retired with KR-87). They were floor NAMES until 0.9.425, so a local could take one and a user's fn void assert(…) was accepted and then never called. See FLOOR.md Diagnostics |
if else do while for foreach in break continue return |
✅ | full control flow (enum branching is match, not switch) |
true false null cast truncate bitcast unsafe extern |
✅ | The three conversion verbs, by what each PRESERVES. cast<T>(x) preserves the value and so TRAPS when the value does not fit T — in every build, a constant one at compile time; truncate<T>(x) preserves the low bits (target narrower or equal, and a provable widening is rejected — there are no bits to drop); bitcast<T>(x) preserves all the bits at the same width (the only one crossing int↔float — IEEE-754 bits, hashing, endianness) and rejects a width mismatch. The fallible form is try cast<T>(x) → Optional<T>: None where the plain cast would trap, and like try new it needs a declared Optional<T> destination. truncate is contextual — a keyword only where a conversion can start, so string's truncate(maxBytes:) and a method of that name still work. null is only for UnsafePtr<T> / UnsafeConstPtr<T> at the FFI boundary; unsafe fn is the sole raw-pointer memory seam. unsafe is a MODIFIER on a body-bearing declaration (method, ctor, destructor, operator, free fn) — C#'s meaning, not Rust's: it marks the BODY, so calling one is unrestricted and public unsafe fn is ordinary. Rejected where no body exists (a type, a field, an abstract method, a contract member) |
import as export |
✅ | the module system. One import { … }; block and one export { … }; block per file, both at the top. Every import entry names a SYMBOL — import { a::b::X, a::b::Y as Z, W }; — so a::b::X is symbol X of module a::b, as renames, and a scope-less W names a symbol of this file's OWN module. There is no whole-module import and no glob, and a module path is written only here (and in a friend grant) — every use is the bare name the import binds (KR-87). A module is a FOLDER, named by the project's kama.json; resolution goes through the manifest, then $KAMA_PATH, then the bundled stdlib (std/core/global are reserved roots; core is the runtime capabilities, embedded in the compiler). Visibility is per FILE: a name leaves its file only by being listed in that file's export, and reaches another file only by being listed in that file's import — export offers, import accepts. Declarations carry no visibility modifier (so type/fn syntax stays uniform). Two entries binding the same bare name is an error (use as). Replaces using (retired) |
static |
✅ | static methods AND module-level statics. (1) static fn has no implicit self and is called at type level: Vec2::dot(left:, right:). static + virtual/override/abstract is a contradiction (no vtable slot); a this/bare-field reference in a static body is a clean error. (2) static T name = const; at module scope (MCU step 1) — a module-level variable with deterministic zero/const init at reset. Per-isolate by construction (_Thread_local on native + wasm-pthreads, plain static on a single-core embedded target), so it's race-free under the threading model — cross-isolate sharing stays on the Atomic<T> seam. v1 restricts to value / UnsafePtr / InlineArray (owns nothing, no teardown) and compile-time-const initializers (literals / sizeof / const arithmetic; omitted = zero-init). This is firmware's home for ISR↔main flags, peripheral handles, and ring buffers — see SPEC.md Module statics |
public private protected |
✅ | Members are private by default; public/protected set it explicitly. protected = owner-or-subclass and is meaningful only inside an extensible resource (type virtual/abstract resource) — an error on a value, a plain resource, or a contract. Field visibility is per field on a type value (public field = a plain-old-data struct); a type resource keeps fields private; a type contract has no fields |
friend |
✅ | granular, owner-granted grants: friend <accessor>[members]; (or [...] = all privates). The accessor is a type, a free function, or a Type::method (a named ctor included), named bare or by qualified path across files and modules with no import; it may then reach the named private members. Granting an unknown or public member is an error; a grant into a module absent from the program is inert |
abstract |
✅ | type abstract(maxDepth: N) resource is non-instantiable (new rejected; also when a subclass leaves an inherited pure method un-overridden) — though its ctor IS callable in a derived type's base install; methods are protected abstract; the type must declare ≥1 overridable member and a ctor. A qualifier after type, on a resource only |
final |
✅ | type final resource is a sealed leaf (cannot be extends-ed) — it is a depth budget of 0, and the only spelling for one; final on a method seals its slot (no subclass override); only a type virtual/abstract resource may be extended at all (a value and a plain resource are already sealed) |
const |
✅ | a const binding is deeply immutable — no reassign, no write through it, no ++/--, no give out of it (a move mutates its source), and addr(of:) into it hands back a read-only UnsafeConstPtr<T>, never a writable pointer. const fn is non-mutating and the only kind callable on a const receiver; it may not return ref T (a place out of a const method launders const — the stdlib's get/getRef, iterator/iterMut split is the answer), and it is ABI-neutral (the emitted C is identical). Params take const T / const ref T (read-only borrow); a const field is write-once (constructor only); const UnsafePtr<T> lowers to const T* for const-correct FFI (a const binding; the read-only pointer type is UnsafeConstPtr<T>, T const*, which stores nothing and does not convert to UnsafePtr<T>). A contract member may be const fn / take const ref, and an implementation must honor both — unlike unsafe, which is rejected on a member because it marks a BODY while const constrains the CALLER's receiver; an override may not drop it either. Not available on a free function (no receiver), ctor, destructor, or operator. A const local whose initializer folds is opportunistically usable in a compile-time position (an array size) — comptime is the explicit, guaranteed form. Full rules: SPEC.md |
comptime |
✅ | a named compile-time constant (const-eval 6b-2) — the evaluation-time axis (const=immutable, static=associated storage, comptime=computed at compile time; comptime implies immutable + associated). Three scopes, one keyword: local/block comptime T N = <expr>; (explicit, errors at the decl if it can't fold — vs an opportunistic plain const), module comptime T NAME = <expr>; (a shared constant; no runtime init point, so folding is required), and type comptime T NAME = <expr>; read as Type::NAME (member-visibility-controlled, public/private like a field). Initializer must fold (a literal / sizeof / alignof / const arithmetic / another comptime). Lowers to a real static const symbol (addressable, @section/flash-placeable) and cross-constant refs bake to literals — so there is no C static-init-order dependency. Drives comptime sizes/fills (InlineArray<T>#(NAME)). It is also the compile-time PARAMETER marker in a generic parameter list — fn int32 shifted(int32 x) comptime(int32 S), type value InlineArray<T> comptime(int32 N) — supplied by its own trailing #(...) group (shifted(x: 2)#(3)) or inferred from an argument's type. That slot was spelled const until 0.9.119; it moved because const already meant immutability in three other places while comptime meant compile-time in every one of its own. comptime fn (const-eval 6b-3) extends this to compile-time functions — the compiler RUNS a bounded, pure function and bakes its scalar/table result into a static const (a CRC/gamma/trig LUT in .rodata/flash); comptime-only (never emitted as C), both top-level and type-associated (Type::name(), member-visibility-controlled, default private), purity + step-budget enforced. See SPEC.md Compile-time constants / Compile-time functions |
fn fnptr |
✅ | fn heads every function/method declaration. fnptr declares an explicit, named function-pointer type — zero-cost, non-null, signature-checked; a bare function name or Type::method binds it |
hardware |
✅ | the MMIO/ISR qualifier (renamed from C's volatile, which is no longer a keyword). hardware UnsafePtr<T> → volatile T* — a memory-mapped register, mirroring const UnsafePtr<T>; on a module static, hardware T → volatile T (a scalar ISR↔loop flag) and hardware UnsafePtr<T> → volatile T* (a peripheral handle). Composes with const (const hardware UnsafePtr<T> or hardware UnsafeConstPtr<T> → const volatile T*, a read-only status register). Single-core MMIO/ISR only — NOT a concurrency primitive (cross-isolate sharing is Atomic<T>) |
spawn |
✅ | starts an isolate — a real OS thread natively, a Web Worker on wasm, shared-nothing (its own stack, heap and module statics). Two forms: Isolate h = spawn f(args…) yields an owned RAII handle whose drop joins (explicit h.join() is the same thing said out loud); a bare spawn f(args…); is legal only inside a scope block, where it becomes a deferred-join child. Arguments cross under the ordinary ownership rules — a resource is moved with give, a value copies — and the bundle's type must declare implements Sendable, a claim the compiler verifies over every field (a type nobody annotated does not cross): a declaration over a non-atomic shared refcount — a Shared/Weak over a mutable payload — is rejected, naming the offending field. A view and a bare contract value cannot be fields at all, so they never reach a bundle; a raw UnsafePtr does cross — that is the unsafe seam, and joining the child is what orders the write. See SPEC.md Concurrency |
scope |
✅ | a structured-concurrency block — scope { … }. Every bare spawn inside it is joined at the closing brace, before any local destructor runs: RAII applied to tasks, so there are no orphans and a child may safely borrow from the enclosing scope (it is guaranteed alive until the join). Block-bodied |
borrow |
✅ | the lexical window a view lives in — borrow h.mint() as v, k.mint() as w { … }. A view LOCAL must root in one (or in a by-value view parameter): kama answers how long is this view valid with lexical scope rather than a lifetime annotation (GOALS.md §3e declines the machinery) or a programmer promise, so the mint must name its extent and the block IS that extent. The host is the mint call, and the mint is any nullary member of a @viewable contract — the grant names its own member, so borrow m.values() as vals and borrow buf.span() as s work as well as view(). The host place is frozen for the block — not unnameable: reads (a.length(), a[i], foreach (x in a)) stay legal, while growing it, reseating it, giveing it out, or passing it as a non-const ref/out do not. The frozen thing is a place — a base plus its chain of field names — so borrow this.buf.view() as b leaves this.hits fully mutable: two distinct fields cannot overlap in storage. Deriving (v.slice(…)) and passing a view to a callee stay free; storing one is still forbidden. foreach and parallel_for are the same window under a different spelling — they hold a borrowing iterator over their operand for the body and freeze it exactly the same way, naming the ELEMENTS instead of the view. Block-bodied; purely lexical, zero runtime cost. See SPEC.md Views |
parallel_for |
✅ | the disjoint-slice data-parallel loop — parallel_for (ref T e in coll, workers: N) { … } splits coll into K non-overlapping sub-Views, one per worker isolate, mutates each in place, and joins them all at its own closing brace (a self-joining barrier — no enclosing scope required, but it composes inside one). Safe by disjointness: two workers never touch the same element, so no lock and no borrow checker. ref is mandatory, and so is workers: (cpuCount() for one per core); the input is a View<T> or any contiguous container exposing .viewMut() (a Map has none and is rejected; a read-only ConstView<T> is too) |
parallel_spawn |
✅ | one long-lived isolate per element — parallel_spawn (ref Worker w in workers) { w.run(); } starts a worker for each element and joins them all at the closing brace: a fixed pool for work that runs for the life of a subsystem, where parallel_for is for one data-parallel pass. Same disjointness proof. See SPEC.md Worker pools |
when |
✅ | conditional conformance on a generic type — implements Copyable<This>(bare: give) when [T: Copyable<T>], or a member gated when [A: default]: the conformance or member exists only for instances whose arguments satisfy the bracket. A free function's bounds go in its type-parameter list instead; it has no "sometimes" |
sizeof alignof |
✅ | compile-time size and alignment of a type (sizeof(T), alignof(T)), folding to a constant where the layout is known — the verification half of @align/@packed via comptime assert. Reserved words |
default |
✅ | contextual. Names the default-allocator bound in when [A: default] and the default target in a manifest; otherwise an ordinary name |
immutable |
✅ | a type qualifier — type immutable value T / type immutable resource T — asserting deep immutability, verified by the compiler: every field, base and variant payload must be a primitive, string, enum, or another deeply-immutable type; a mutable member is a compile error naming it. Its purpose is cross-isolate sharing: a Shared/Weak over a deeply-immutable element is sendable, so isolates read one asset zero-copy, and only that case pays for an atomic refcount. Distinct from const, which constrains one binding and so cannot license sharing. A type qualifier and nothing smaller — on a field or method it is a hard error (it parses because the modifier list is shared, and it was silently dropped until 0.9.144); const is the per-field promise, const fn the per-method one |
expose |
✅ | marks a free function for the kama→host boundary: it gets an exported C-ABI symbol (KAMA_EXPORT) a host can resolve — dlsym on a kama build --shared .so/.dylib/.dll, or a .wasm module export. The symbol is the module path joined to the name with _ (game::sim::tick → game_sim_tick; a loose file qualifies by its file name), so modules keep exposed names apart in C too; @linkName fixes one verbatim. kama build writes the host's C header for them (<project>.h beside the output). Signature must be C-ABI-safe (no owned-by-value string/collection/Owned/Shared/Weak; use UnsafePtr<T> or an extern struct). Free functions only, not a member modifier — plus two type forms, type expose value (a C layout kama owns) and type expose enum (C constants kama owns, every value spelled), published by the generated header — the pairs of type extern value and type extern enum, whose C names are the header's own, verbatim. Distinct from export (module visibility) — three boundaries, three words. Full 2.0 expose adds richer wasm module exports + the scripting host. |
MCU codegen attributes (@interrupt, @section, @noheap)#
Not keywords — declaration attributes on the existing @name(args) mechanism (previously
serialization-only), extended in MCU step 4 to functions and module statics. @interrupt/@section emit a
C __attribute__((...)) only on the exact declaration they annotate; @noheap (step 5) emits nothing
— it is a checker flag. Un-annotated code is unchanged.
| Attribute | On | Lowers to | Rules |
|---|---|---|---|
@interrupt |
a function (@interrupt expose fn void h() { … }) |
__attribute__((interrupt, used)) — the Cortex-M / RISC-V / classic-ARM ISR calling convention; used survives --gc-sections |
Signature must be void h() (no params, no return); expose required so the handler has an exported symbol the vector table can name — usually fixed by the startup code, so spelled with @linkName("SysTick_Handler"). AVR's @interrupt("VECTOR") → ISR(VECTOR) macro is a later step |
@section(".name") |
a module static or a function | __attribute__((section(".name"))) |
one string-literal section name — vector table (.isr_vector), flash const table (.rodata), DMA RAM bank, .ramfunc. The board's linker script owns the actual addresses |
@noheap |
any declaration with a body — a free fn, and equally a method, ctor, destructor or operator (@noheap public fn int32 fill(…) { … }) |
nothing — a checker flag, not codegen | Makes every emitter-visible heap allocation a compile error (new/try new, parallel_for/spawn boxing, Owned<Error> boxing, string interpolation's Formatter). No args. Target-independent — an ISR / game frame-tick / real-time audio callback allocates nothing. Transitive: the body may not call anything that allocates, at any depth, including a local's destructor; GlobalAllocator is the leaf, so a container drawing from it is rejected while the same container over an arena is not. Where the callee is unknowable — a fnptr, or a contract/virtual slot — the call is rejected unless the CONTRACT member declares @noheap, which every implementation is then checked against. The whole-program equivalent is the --no-heap build flag |
Symbol-name attribute (@linkName)#
Not a keyword — a declaration attribute. The peer of Rust's #[link_name] / #[export_name], one
attribute for both directions of the C boundary; named for the concept rather than the backend, because
the 2.0 bytecode VM has no C names.
| Attribute | On | Lowers to | Rules |
|---|---|---|---|
@linkName("symbol") |
an extern fn (the C symbol it binds) or an expose fn (the symbol it exports) |
the symbol at every call site / as the definition's name — no __attribute__ |
exactly one string literal; a C symbol (letters, digits, _; not a C keyword — a kama keyword is fine, that is the point); one C symbol has ONE kama binding per program; exported symbols stay unique. On an expose fn it is the one override of the module-qualified symbol. Rejected on an ordinary fn, a member, a static, a fnptr or an extern "<h>"; — none has a symbol to rename. See SPEC.md FFI — calling C and Exposing to a host |
Layout-control attributes (@align, @packed)#
The same passthrough mechanism applied to a type rather than a declaration, and the companion to
layout verification (sizeof/alignof folding + comptime assert), which shipped first because
asserting a layout is what makes stating one safe. kama does not own layout — it emits C and the C
compiler lays the struct out — so these state a constraint and the assertions verify what the toolchain
actually did, rather than kama keeping a second model that could disagree per target.
| Attribute | On | Lowers to | Rules |
|---|---|---|---|
@align(N) |
a type with a struct (@align(16) type value Vec4 { … }) |
__attribute__((aligned(N))) on the emitted struct |
One literal number, a power of two from 1 to 4096. Held to a power of two rather than passed through because gcc/clang round a non-power-of-two up rather than refusing it, so @align(3) would compile and quietly mean 4 |
@packed |
a type with a struct (@packed type value Reg { … }) |
__attribute__((packed)) on the emitted struct |
No arguments — it removes padding, it does not set a width. A wire struct or an MMIO register block |
Both are refused on an enum (a payload-less one lowers to an integer, a tagged one to a tag plus a
per-variant union that an outer packed would not reach — an enum states its layout with
type enum E : IntType) and on a declaration (@align(64) static …), so there is one way to align an
object rather than two: put it on the type and declare the static with that type. Rust draws the same line
— #[repr(align(N))] is types-only. A type may carry both, and either may sit beside @generate(...).
Conditional-compilation attribute (@compileFor)#
Also on the @name(args) mechanism, but not MCU-specific and not codegen — a build-time keep/drop
gate. See SPEC.md Conditional compilation and ROADMAP_DETAIL.md §5.
| Attribute | On | Effect | Rules |
|---|---|---|---|
@compileFor(FLAG…) |
any top-level decl — fn, type, enum, module static |
The decl is kept iff its flag gate is active, else dropped before any collect/emit pass (its symbol never exists; no #ifdef reaches the emitted C). The attribute is stripped from kept decls |
Flags are derived from the target triple — ARCH_<arch>/OS_<os>/ABI_<abi> plus HOSTED and SIMD128 — with DEBUG/RELEASE (from --release), NOHEAP (from --no-heap), a project-declared target's own name, and user flags via --define/--undefine. ⚠️ A built-in target NAME is deliberately not a flag: NATIVE/WASM/EMBEDDED/WINDOWS are undeclared, and in a loose build an undeclared flag is inactive, so the decl is silently dropped. Logic is membership + leading ! + comma-AND (@compileFor(!RELEASE), @compileFor(OS_WINDOWS, DEBUG)) — not an expression language. Build-mode and platform are one primitive; platform = the gate on per-target type impls behind a contract. A kama.json manifest (a user-project file) declares the valid flag universe and makes an undeclared name a hard error |
Other attributes#
Each is described where its feature is; this is the index.
| Attribute | On | What it does | See |
|---|---|---|---|
@generate(...) |
a value, resource or enum |
derives Serializable, Deserializable, Formattable, Equatable, Hashable, or the of/zero constructors |
SPEC Derives |
@field / @field(name: "…") / @field(id: N) / @skip / @deprecated |
a field of a serialized type | marks what is written, under which name or number; @deprecated is read but never written |
SPEC Serialization |
@serializedGraphEdges |
a container's members | names the members an object graph routes through | SPEC Serialization |
@viewable |
a contract | its nullary members may mint a view inside a borrow window |
SPEC Views |
@foreignEntry / @callerThread |
a fnptr type handed to C |
says whether C calls it back on a thread kama did not create, which decides what it may touch | SPEC Foreign entry points |
@onPanic(recover: v) |
a function | a region that recovers from a panic by returning v instead of terminating |
SPEC Recoverable regions |
@heap |
an extern fn |
declares that the C function allocates, so @noheap and --no-heap see it |
SPEC No-heap |
@globalAllocator |
one resource implementing GlobalHeap |
replaces the allocator behind every heap block the program uses | SPEC Global allocator |
Reserved words#
Every keyword above is implemented, enforced, and exercised by the fixtures in ../tests/.
Every C keyword is reserved in kama too — int, double, long, volatile, goto, union and the
rest — because kama's names become C names and a binding called union would not compile. Using one
is an error that names the kama spelling where there is one: C's volatile is spelled hardware here
(see the hardware row), and cross-isolate sharing is Atomic<T>, never either.
SPEC.md C's reserved words are reserved in kama has the list.
(The minimal expose — a free-function C-ABI symbol for the native --shared reload boundary and wasm
exports — is implemented. The full 2.0 expose — richer wasm module exports and the scripting host
interface — remains future work, but the keyword is live today.)