STAGE 2 / Language semantics · LESSON 09
Why this exists
Dictionaries are flexible input and serialization values. A struct gives an internal record named fields and associated behavior. An enum expresses alternatives, making states easier to distinguish than unrelated string conventions scattered through the code.
Runtime contract
Declare a struct with fields, optional field annotations, and supported defaults. Construct it using its name and a field initializer. Access fields through dot syntax. A field update still requires a mutable binding. Methods are supported, and Kujo 1.8 adds generator methods declared with func* in both engines. Those generator methods preserve receiver snapshots, shared alias progress, and cached terminal failures.
The example's method reads a declared field. Kujo's method implementation is not JavaScript prototype inheritance or Rust ownership. Keep the domain model small and test each operation's observable result.
Enums use named variants and tagged values. The next lesson combines them with match and Result/Option. A tagged state lets a consumer distinguish successful completion from failure without inspecting a free-form sentence.
Serialization boundary
A struct is a runtime value, not automatically a JSON object. Convert selected fields into a dictionary for machine output. Choose a stable external schema instead of exposing every internal field. Native handles and functions likewise need deliberate representations.
Professional pattern
Use structs for internal invariants and methods, dictionaries for explicit external contracts, and enums for mutually exclusive states. Do not add a field just because some downstream tool might want it later. Start with the data your calculation actually needs.
Common mistakes
Optional type annotations do not make the VM a static type gate. A field label is not a promise that every future assignment is statically verified. Validate external data before constructing a domain value, and test invalid values at that boundary.
Working example
struct Job {
name: string,
units: int,
func doubled() { return units * 2 }
}
let job := Job { name: "report", units: 3 }
assert_equal(job.doubled(), 6)
print(to_json({"name": job.name, "units": job.units}))
Run it
From the course repository root, use the pinned Kujo 1.9.0 runtime.
kujo check examples/09.kujo
kujo run --untrusted examples/09.kujo
Captured output
{"name":"report","units":3}
Break it and diagnose it
Runtime structs are not directly JSON-serializable; select a dictionary of fields.
struct Job { units: int }
let job := Job { units: 3 }
print(to_json(job))
kujo run --untrusted examples/09-break.kujo
Exit status: 4. Captured diagnostic:
[KUJOVM001] [vm] Runtime Error: Cannot convert Struct { name: "Job", fields: {"units": Int(3)} } to JSON
--> 0:0
Exercise
Define a Report struct with a method that computes a derived value. Create a separate dictionary serializer. Add an enum describing pending, passed, and failed states, then use it in the next lesson’s match exercise.
Checkpoint
- I can construct and inspect a struct.
- I serialize a deliberate dictionary contract.
- I distinguish record fields from tagged alternatives.
Contract versus release behavior
Kujo 1.9.0 fixes the tested struct-field assignment: both runtimes update the field from 3 to 4. The course example still uses construction and reads. Qualified custom-enum matching remains a VM/interpreter discrepancy; use the verified Result/Option patterns or an explicitly labeled interpreter fallback. See the evidence ledger for exact probes and runtime results.
Verified 2026-10-22 · Official Kujo 1.9.0 release · Source contract · Download example