KUJO THE COURSE

THE KUJO COURSE · VERIFIED 1.3.1

Collections, pipes, and null handling

Transform structured data without hiding missing-value errors.

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

Complete this stage’s build