Skip to content

Reflection & Staged Code

AI-native programs need to reason about themselves: hand a model the exact shape and meaning of a type, generate a new function at runtime, and turn a failure into context a model can fix. Python does this with getattr/setattr/eval and a stringly stack trace — unbounded, invisible to types, ungoverned. Sema does it with three governed constructs: reflect (read-only introspection), Code[T] (staged code as typed data), and trace (a first-class, model-ready error packet).

This guide covers all three. They are the substrate the documentation generator and the debugger share (see Documents & Reports).

reflect(T) and reflect(f) return TypeInfo / CallableInfo values: fields with their types and sem descriptors, contracts, effect rows, and the wire schema — a runtime API over the same artifacts the compiler already sealed into the binary. Reflection is read-only and pure (!{}): it reads compile-time constants, and there is no mutating reflection — no setattr, no dynamic member addition, no monkey-patching. A program cannot observe a different shape of itself than the compiler proved.

The point is that reflection is prompt-ready by construction. TypeInfo and CallableInfo carry a canonical, build-stable rendering, so handing a model the shape and meaning of anything is one splice into a prompt template:

template extraction_prompt(note: str) -> Prompt[Patient]:
role system:
text "Extract a structured record. The target schema, with field meanings:"
text f"{reflect(Patient)}" # name, field types, sem descriptors, ranges
role user:
text f"{note}"

simulate def already does this implicitly — the meaning IR is reflected context — and reflect hands the same artifact to your own templates and context slots. Contracts and policy summaries reflect the same way, which is how an agentic program can explain its own constraints to a model mid-flight.

Runtime-generated code in Sema is native, typed data — never ambient text fed to an eval. The type parameter T is a function type, and function types carry effect rows, so the row statically bounds everything the staged code could ever do:

simulate def synthesize_scorer(spec: str) -> Code[(Candidate) -> f32 !{model.embed}]
by coder:
sem "Generate a Sema scoring function for the described ranking policy"
scorer = compile(synthesize_scorer(spec))? # resident-compiler admission
ranked = parallel candidates map c => scorer.run(c) # !{code.exec("scoring-sandbox"), model.embed}

Three stages make this safe:

  • Admission. compile(c) runs the resident incremental compiler over the candidate: parse, types, effects ⊆ T’s row, trust and policy well-formedness, contract attachment. Admission is pure analysis (!{}). A model-produced candidate enters untrusted and exits at most validated — never trusted.
  • Execution. c.run(args) has row {code.exec(<sandbox>)} ∪ row(T) and demands an explicit policy grant naming the sandbox — an effect outside row(T) is a typed EffectViolation at the site, and T’s boundary contracts run at entry and exit exactly as at a bridge.
  • Honest guarantees. A run site types at most checked for structure and best_effort/statistical(α) for behavior — no gauntlet ran over the staged body, so assure treats run like an FFI edge. Every Code[T] value carries provenance and is content-hash-addressed; staged execution journals like static code, so replay is exact.

Debugging is first-class too. When an error is caught with except or reaches the top level, the runtime captures it with its call frames; the trace keyword then assembles a self-describing packet by reflecting the functions involved — their signatures, effect rows, and docstrings — so a human and a model have everything needed to self-fix behind one word:

expect port = connect(raw):
use(port)
except ContractViolation as e:
t = trace(e) # or bare `trace()` for the most recent error
heal(t.markdown) # hand the repair packet to a model

A Trace exposes .kind, .message, .frames, .interfaces (reflected signatures), .report (human-readable), and .markdown (the LLM-ready repair packet: the error and location, the call chain, the reflected interfaces with their docstrings, any evidence values, and the repair task).

An uncaught error prints the same packet automatically — the stack trace a user sees is already the context an agent needs, closing the self-repair loop. This is the payoff of reflection: the doc reflector and the debugger share one mechanism, so an error report carries the exact interfaces and intent, not just a line number.

The three constructs form a self-repair loop:

  1. A function fails and raises a typed error.
  2. trace(e).markdown reflects the involved interfaces into a repair packet.
  3. A simulate def … -> Code[T] proposes a fix as staged, typed code.
  4. compile admits it (untrusted → validated, effects ⊆ T), and it runs only inside a granted sandbox.

Persistent adaptation stays the business of the governed paths — heal under its gauntlet, descriptor-space regeneration at simulate sites — so dynamic staging composes with them rather than bypassing them.

Reflection is !{} and staged execution journals like static code, so a project using them checks and replays like any other:

Terminal window
sema check <your-project>
SEMA_STRICT=1 sema run <your-project>
sema assure <your-project> --grade silver

assure treats a Code[T].run site as an FFI edge — a proved region can never contain one, and the guarantee ceiling is in the type, not in a run history.

  • Schema-in-prompt. Splice reflect(T) into any simulate def prompt so the model always sees the target shape and meaning — see Schemas & Typed Decode.
  • Explain your constraints. reflect a contract-bearing function to show a model the rules it must respect before it proposes a change.
  • Mid-flight self-heal. Obtain trace(e).markdown without an uncaught error and feed it to a repair simulate — the program fixes itself before aborting.