STAGE 2 / Language semantics · LESSON 12
Today
Kujo supports type annotations on bindings, parameters, and return positions. Type expressions include suffix forms such as nullable and array expressions. Parsing a type expression is not the same thing as enforcing a complete static safety system.
The v1 scope explicitly says interpreter mode may emit non-fatal type-check warnings while the default VM keeps dynamic execution without a static type gate. A successful check is useful feedback, but it is not proof that every future input satisfies your annotations.
Mental model
Annotations describe intended types to readers and tooling, but enforcement has limits. Validate untrusted input at runtime. This is neither Rust's compile-time model nor a reason to omit all useful type information.
The example annotates a simple function and then asserts its actual result. These are complementary pieces of evidence: the annotation describes intent; the assertion checks behavior. The breaking example supplies incompatible runtime values so the actual operation fails.
On the horizon
The current v1 scope lists optional-typing precision follow-ups, including destructuring inference, module existence checks, struct field lookup, Promise unwrap typing, and callable fallback policy. Generics, FFI, WASM targeting, and macros remain deferred candidates. They are not course prerequisites or release promises.
Professional pattern
Annotate public function boundaries where it helps readers and editors. Validate incoming JSON before passing it into domain functions. Keep tests for valid, absent, malformed, and boundary values. If a warning appears only in interpreter mode, record that runtime context instead of advertising it as a VM rejection.
Common mistakes
Do not claim annotations make an unsafe input safe. Do not write lessons using a deferred generic API just because its type expression parses. And do not infer that a dynamic language cannot have useful contracts: schemas, assertions, capability gates, and stable result shapes provide different kinds of guarantees.
Working example
func double(value: int) -> int { return value * 2 }
let answer: int := double(4)
assert_equal(answer, 8)
print(answer)
Run it
From the course repository root, use the pinned Kujo 1.3.1 runtime.
kujo check examples/12.kujo
kujo run --untrusted examples/12.kujo
Captured output
8
Break it and diagnose it
The runtime operation receives an incompatible dictionary. This demonstrates dynamic execution failure, not a static type-check guarantee.
func double(value: int) -> int { return value * 2 }
print(double({"bad": true}))
kujo run --untrusted examples/12-break.kujo
Exit status: 4. Captured diagnostic:
[KUJOVM001] [vm] Runtime Error: Invalid binary operation: value * int
--> 0:0
= help: Use operands whose types support this operator, or convert values before applying it.
Call stack:
0 at double
Exercise
Annotate a validator, then test wrong runtime types at its input boundary. Run check, VM, and interpreter modes and record which diagnostics each actually produces. Explain what each result does and does not prove.
Checkpoint
- I can describe the current typing boundary honestly.
- I validate external inputs at runtime.
- I keep deferred features out of baseline code.
Verified 2026-09-06 · Official Kujo 1.3.1 release · Source contract · Download example