STAGE 1 / Kujo foundations · LESSON 07
Language contract
Arrays preserve insertion order. Indexing outside their bounds fails. Dictionary indexing with a missing key also fails; it does not produce null. If absence is normal, use has_key, get, or get_default according to their documented contracts.
Spread syntax copies elements into an array literal or entries into a dictionary literal. A later dictionary entry wins when keys overlap. Do not use dictionary display order as an application sorting policy; serialize explicitly and sort where presentation requires it.
push returns a new array value. Reassign that return when you intend to retain it. Mutation through an immutable binding remains forbidden even when replacing an element inside a nested collection.
Pipes and fallback
A pipe sends a value into a following function call. It is useful for small, readable transformations. A pipeline should still make failure visible; chaining more operations is not a substitute for validation.
?? selects a fallback for null. It is distinct from truthiness. Zero, false, and an empty string can be valid values and should not be silently replaced. record["missing"] ?? fallback cannot rescue a missing-key indexing error: the left expression fails before it becomes a value.
Professional pattern
At an input boundary, first decide whether a field is required, optional, or allowed to be null. Required fields should reject missing input. Optional fields can use a deliberate default. A default that silently converts broken input into plausible data is worse than a clear error.
The example copies defaults, replaces a field, builds a new list, and applies a simple transformation. These are the ingredients of the Stage 1 report. The stage project adds validation and meaningful failure behavior without requiring AI or external services.
Working example
func main() {
func double(x) { return x * 2 }
let config := {"limit": 2, "label": "jobs"}
let adjusted := {...config, "limit": 3}
mut values := [1, 2]
values := push(values, 3)
let all := [0, ...values]
assert_equal(adjusted["limit"], 3)
assert_equal(null ?? 9, 9)
assert_equal(0 ?? 9, 0)
print(4 |> double)
print(to_json(all))
}
main()
Run it
From the course repository root, use the pinned Kujo 1.3.1 runtime.
kujo check examples/07.kujo
kujo run --untrusted examples/07.kujo
Captured output
8
[0,1,2,3]
Break it and diagnose it
The missing dictionary key throws before null coalescing can select a fallback.
let record := {"name": "job"}
print(record["missing"] ?? "fallback")
kujo run --untrusted examples/07-break.kujo
Exit status: 4. Captured diagnostic:
[KUJOVM001] [vm] Runtime Error: Missing map key: "missing"
--> 0:0
Exercise
Implement a reporting function over dictionaries with required name and optional count. Preserve a count of zero. Reject a missing required name. Add one out-of-bounds array drill and compare it with a missing-key drill.
Checkpoint
- I distinguish missing keys from null values.
- I retain the result of push.
- I use right-biased spread and null fallback deliberately.
Builtin names and declaration scope
The names values, items, and input already identify builtins in global scope. Redeclaring them at top level is rejected in both runtimes. Use a distinct name such as course_rows; ordinary mutable collection reassignment passes. Local parameters and intentionally nested names have different scope. The evidence ledger includes both the collision and a passing control probe.
Verified 2026-09-06 · Official Kujo 1.3.1 release · Source contract · Download example