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, with documented VM/interpreter coverage; do not assume every generator or metaprogramming pattern is supported inside a struct.
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.3.1 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
The release VM silently left a struct field unchanged after an assignment that the interpreter applied, and the method retained the original field value. The successful example uses construction and reads only. Struct mutation and custom qualified-enum matching need a runtime fix or a deliberately labeled interpreter fallback before relying on them. See the evidence ledger for exact probes and runtime results.
Verified 2026-09-06 · Official Kujo 1.3.1 release · Source contract · Download example