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 — read the program’s shape
Section titled “reflect — read the program’s shape”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.
Code[T] — staged code as typed data
Section titled “Code[T] — staged code as typed data”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 admissionranked = 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 entersuntrustedand exits at mostvalidated— nevertrusted. - Execution.
c.run(args)has row{code.exec(<sandbox>)} ∪ row(T)and demands an explicit policy grant naming the sandbox — an effect outsiderow(T)is a typedEffectViolationat the site, andT’s boundary contracts run at entry and exit exactly as at a bridge. - Honest guarantees. A
runsite types at mostcheckedfor structure andbest_effort/statistical(α)for behavior — no gauntlet ran over the staged body, soassuretreatsrunlike an FFI edge. EveryCode[T]value carries provenance and is content-hash-addressed; staged execution journals like static code, so replay is exact.
trace — errors as repair packets
Section titled “trace — errors as repair packets”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 modelA 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.
How they compose
Section titled “How they compose”The three constructs form a self-repair loop:
- A function fails and raises a typed error.
trace(e).markdownreflects the involved interfaces into a repair packet.- A
simulate def … -> Code[T]proposes a fix as staged, typed code. compileadmits 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.
Run and verify
Section titled “Run and verify”Reflection is !{} and staged execution journals like static code, so a project
using them checks and replays like any other:
sema check <your-project>SEMA_STRICT=1 sema run <your-project>sema assure <your-project> --grade silverassure 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.
Variations
Section titled “Variations”- Schema-in-prompt. Splice
reflect(T)into anysimulate defprompt so the model always sees the target shape and meaning — see Schemas & Typed Decode. - Explain your constraints.
reflecta contract-bearing function to show a model the rules it must respect before it proposes a change. - Mid-flight self-heal. Obtain
trace(e).markdownwithout an uncaught error and feed it to a repairsimulate— the program fixes itself before aborting.