Section 20: Compile-Time Reflection
Status: Not Started
Goal: Compile-time structural reflection works end-to-end: fields_of(T) returns field metadata, $for expands heterogeneously during monomorphization, $if eliminates dead branches, and value.[field] resolves to direct field access — all with zero runtime overhead and identical codegen to hand-written code.
Context: The approved compile-time-reflection-proposal.md (2026-03-26) supersedes the runtime reflection model (Reflect trait, TypeInfo, Unknown). Compile-time reflection is zero-cost, requires no opt-in (#derive), and works on any type. The flagship target is a pure Ori JSON parser using $for + fields_of(T) + SIMD intrinsics.
Proposal: proposals/approved/compile-time-reflection-proposal.md
Supersedes: proposals/superseded/reflection-api-proposal.md (runtime reflection), Section 20 (old runtime reflection)
Reference implementations:
- Zig
src/Sema.zig,src/InternPool.zig:@typeInfo+inline for+@field— zero-cost, type-safe, comptime field iteration - C++26 P2996:
^^Treflect,template forexpansion,[:expr:]splice — zero overhead, fully typed
Depends on: Section 18 (Const Generics — specifically 18.0 const eval bridge), Section 2 (Type Inference), Section 3 (Traits).
Architecture
Parse ──→ Type Check ──→ Monomorphize ──→ ARC Lower ──→ LLVM Codegen
↑
$for/$if expansion here
fields_of(T) evaluated here
splice resolved to field access
each expanded copy type-checked
Key insight: Expansion happens during monomorphization — after type inference resolves T to a concrete type, but early enough for expanded code to be type-checked. This is architecturally identical to Zig’s inline for and C++26’s template for.
Data flow:
- Parser produces
ExprKind::CompFor,ExprKind::CompIf,ExprKind::SpliceAST nodes - Type checker validates const iterables/conditions; defers expansion to monomorphization
- Monomorphizer encounters
$for/$ifwith concreteT, evaluatesfields_of(T), expands/branches - Each expanded copy is independently type-checked with concrete field types
- ARC lowerer and LLVM codegen see only plain
ExprKind::Fieldaccess — no reflection constructs remain
20.1 Compile-Time Metadata Types and Intrinsics
Verified not-started (2026-03-29): No
$FieldMeta,$VariantMeta,Tag::FieldMeta,Tag::VariantMetain type pool. Nofields_of/variants_of/name_ofintrinsic registration. No test files ortests/spec/reflection/directory.
File(s): compiler/ori_ir/src/ast/expr.rs, compiler/ori_types/src/output/mod.rs, compiler/ori_types/src/infer/expr/identifiers.rs, compiler/ori_types/src/check/registration/mod.rs
Goal: Register fields_of(T), variants_of(T), name_of(T) as compile-time intrinsics and define $FieldMeta, $VariantMeta as compiler-internal types.
Depends on: Section 18.0 (const eval bridge — provides ori_types → ori_eval invocation path). If 18.0 is not complete, intrinsics can still be registered but cannot be evaluated until the bridge exists.
Metadata Types
-
Add
$FieldMetarepresentation to the type pool (compiler/ori_types/src/pool/)- Two fields:
name: str,index: int - Compile-time-only flag — cannot appear in runtime types
- Follow
Tagpattern in pool: addTag::FieldMeta,Tag::VariantMeta
- Two fields:
-
Add
$VariantMetarepresentation- Three fields:
name: str,index: int,fields: [$FieldMeta] - Same compile-time-only flag
- Three fields:
Intrinsic Registration
-
Register
fields_ofintrinsic inori_types/src/check/registration/mod.rs- Type signature:
<T>(T) -> [$FieldMeta](or type-parameter-only form) - Returns empty
[]for non-struct types (primitives, sum types, collections, tuples) - Returns
[$FieldMeta { name: "inner", index: 0 }]for newtypes - Only public fields (excludes
::-prefixed) - Warning W0461 when called on known non-struct literal type
- Type signature:
-
Register
variants_ofintrinsic- Type signature:
<T>(T) -> [$VariantMeta] - Returns empty
[]for non-sum types
- Type signature:
-
Register
name_ofintrinsic- Type signature:
<T>(T) -> str - Returns unqualified type name without type arguments
- Type signature:
Metadata Extraction
- Implement field metadata extraction from type pool
- Given a concrete struct
Idx, extract ordered field list from pool - Build
$FieldMetavalues: name fromNameinterner, index from declaration order - Handle newtypes: single field named “inner”
- Handle private fields: skip
::-prefixed names
- Given a concrete struct
Tests
-
Matrix tests (types): struct (multiple fields), struct (single field), struct (no public fields), newtype, sum type, primitive, collection, tuple, generic struct (monomorphized), nested struct
-
Semantic pin:
fields_of(NewType)returns[{name: "inner", index: 0}]— only passes with newtype support -
TDD: Write failing tests first → implement → verify pass in debug and release
-
/tpr-reviewpassed — independent review found no critical or major issues (or all findings triaged) -
/impl-hygiene-reviewpassed — hygiene review clean. MUST run AFTER/tpr-reviewis clean. -
Subsection close-out (20.1) — MANDATORY before starting the next subsection. Run
/improve-toolingretrospectively on THIS subsection’s debugging journey (per.claude/skills/improve-tooling/SKILL.md“Per-Subsection Workflow”): whichdiagnostics/scripts you ran, where you addeddbg!/tracingcalls, where output was hard to interpret, where test failures gave unhelpful messages, where you ran the same command sequence repeatedly. Forward-look: what tool/log/diagnostic would shorten the next regression in this code path by 10 minutes? Implement improvements NOW (zero deferral) and commit each via SEPARATE/commit-pushusing a valid conventional-commit type (build(diagnostics): ... — surfaced by section-20.1 retrospective—build/test/chore/ci/docsare valid;tools(...)is rejected by the lefthook commit-msg hook). Mandatory even when nothing felt painful. If genuinely no gaps, document briefly: “Retrospective 20.1: no tooling gaps”. Update this subsection’sstatusin section frontmatter tocomplete. -
/sync-claudesection-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW. -
Repo hygiene check — run
diagnostics/repo-hygiene.sh --checkand clean any detected temp files.
20.2 Parser: $for, $if, and Splice Syntax
Verified not-started (2026-03-29): No
ExprKind::CompFor,CompIf, orSplicevariants in AST. Noparse_comp_for/parse_comp_iffunctions. Parser$dispatch only producesExprKind::Const(name). No parse tests for reflection syntax.
File(s): compiler/ori_parse/src/grammar/expr/primary/control_flow.rs, compiler/ori_parse/src/grammar/expr/postfix.rs, compiler/ori_ir/src/ast/expr.rs, compiler/ori_ir/src/token/kind.rs
Goal: Parse $for, $if, and value.[field] splice syntax, producing new AST nodes.
Parser: $for
-
Add
ExprKind::CompForto IR (compiler/ori_ir/src/ast/expr.rs)ExprKind::CompFor { pattern: BindingPatternId, iter: ExprId, guard: ExprId, // ExprId::INVALID = no guard body: ExprId, is_yield: bool, }Note: No label —
$foris expansion, not a loop.break/continueare invalid. -
Add
parse_comp_forin parser (control_flow.rs)- Detect
$forviaTokenKind::Dollarfollowed byforkeyword (adjacency check) - Reuse
forgrammar:$for pattern in const_iterable [if const_guard] do|yield body - Do NOT set
IN_LOOPcontext (break/continue are invalid) - Follow pattern of
parse_for_loop()at line 211
- Detect
Parser: $if
-
Add
ExprKind::CompIfto IRExprKind::CompIf { cond: ExprId, then_branch: ExprId, else_branch: ExprId, // ExprId::INVALID = no else } -
Add
parse_comp_ifin parser (control_flow.rs)- Detect
$ifviaTokenKind::Dollarfollowed byifkeyword - Reuse
ifgrammar:$if const_cond then expr [else expr] - Follow pattern of
parse_if_expr()at line 153
- Detect
Parser: Splice Access
-
Add
ExprKind::Spliceto IRExprKind::Splice { receiver: ExprId, field_expr: ExprId, // compile-time $FieldMeta expression } -
Add splice parsing in postfix dispatch (
postfix.rs)- Detect
.[after dot token —value.[field] - In
parse_postfix_dot()(line 178): if next token after.is[, parse splice - Parse
field_expras expression, expect] - Follow existing index parsing pattern from
apply_postfix_ops()
- Detect
Primary Expression Dispatch
- Wire
$forand$ifinto primary expression dispatch- In
parse_primary(): when encounteringTokenKind::Dollar, peek next token $+for→parse_comp_for()$+if→parse_comp_if()$+ identifier → existingExprKind::Const(name)path
- In
Tests
-
Parse tests:
$for x in items yield x,$for x in items do body,$for x in items if guard yield body,$if cond then a else b,$if cond then a(no else),value.[field],value.[field] = x(assignment), nested$for/$if -
Error tests:
$forwithbreak(error),$forwithcontinue(error),$ifwithoutthen(error),.[without matching](error) -
Semantic pin:
$forparses toCompFornotFor— only passes with new AST variant -
TDD: Write failing parse tests first
-
/tpr-reviewpassed — independent review found no critical or major issues (or all findings triaged) -
/impl-hygiene-reviewpassed — hygiene review clean. MUST run AFTER/tpr-reviewis clean. -
Subsection close-out (20.2) — MANDATORY before starting the next subsection. Run
/improve-toolingretrospectively on THIS subsection’s debugging journey (per.claude/skills/improve-tooling/SKILL.md“Per-Subsection Workflow”): whichdiagnostics/scripts you ran, where you addeddbg!/tracingcalls, where output was hard to interpret, where test failures gave unhelpful messages, where you ran the same command sequence repeatedly. Forward-look: what tool/log/diagnostic would shorten the next regression in this code path by 10 minutes? Implement improvements NOW (zero deferral) and commit each via SEPARATE/commit-pushusing a valid conventional-commit type (build(diagnostics): ... — surfaced by section-20.2 retrospective—build/test/chore/ci/docsare valid;tools(...)is rejected by the lefthook commit-msg hook). Mandatory even when nothing felt painful. If genuinely no gaps, document briefly: “Retrospective 20.2: no tooling gaps”. Update this subsection’sstatusin section frontmatter tocomplete. -
/sync-claudesection-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW. -
Repo hygiene check — run
diagnostics/repo-hygiene.sh --checkand clean any detected temp files.
20.3 $for Expansion During Monomorphization
Verified not-started (2026-03-29): No expansion logic in monomorphization. No
CompForhandling. BLOCKED on Section 18.0 (const eval bridge, also not-started).
File(s): compiler/ori_types/src/infer/expr/calls/monomorphization.rs, compiler/ori_types/src/infer/mod.rs, compiler/ori_types/src/pool/substitute/mod.rs
Goal: When monomorphizer encounters $for with concrete T, expand the loop body N times (once per field/variant), producing independently type-checked expressions.
Depends on: 20.1 (metadata intrinsics), 20.2 (parser), Section 18.0 (const eval bridge for evaluating const iterables).
Expansion Logic
-
Add expansion sub-phase to monomorphization
- Location: after
maybe_record_mono_instance()resolves all type variables - When encountering
ExprKind::CompForin a monomorphized function body:- Evaluate const iterable (e.g.,
fields_of(User)→ concrete[$FieldMeta]) - For each element, duplicate the body with the iteration variable bound to that element
- If
is_yield: collect results into a list expression - If
do: sequence as void expressions - Type-check each expanded copy independently (field types differ per iteration)
- Evaluate const iterable (e.g.,
- Location: after
-
Homogeneous expansion (const lists)
- Iterable is a compile-time list of uniform type (e.g.,
[1, 2, 3]) - Each body has the same type — result is
[T] - Straightforward unrolling
- Iterable is a compile-time list of uniform type (e.g.,
-
Heterogeneous expansion (fields_of/variants_of)
- Each iteration may produce differently-typed expressions
value.[field]has typestrfor one field,intfor another- Result type of
$for...yieldis[T]whereTis the unified type of all expanded bodies - Compile error E0464 if types are incompatible
-
Guard evaluation
$for field in fields_of(T) if const_guard yield body- Guard must be compile-time evaluable (e.g.,
is_option(value.[field])) - Skip iterations where guard is false
-
Empty iteration
$for...yieldover empty list →[]$for...doover empty list →void
Integration with MonoInstance
- Expansion produces new AST nodes in the expression arena
- Expanded
$forbecomes a list literal (yield) or block sequence (do) - Each expanded body references concrete field types via
body_type_map - The
MonoInstance::body_type_mapmust include types from expanded copies
- Expanded
Tests
-
Matrix tests (expansion patterns): homogeneous
$for(const list), heterogeneous$for(fields_of), nested$for(variants_of → fields),$forwith guard, empty iteration, single-field struct, multi-field struct, generic struct monomorphized -
Matrix tests (yield vs do):
$for...yield(list result),$for...do(void),$for...yieldwith type unification -
Semantic pin:
$for field in fields_of(User) yield field.nameproduces["name", "age"]— only passes with heterogeneous expansion -
TDD: Write failing expansion tests first
-
/tpr-reviewpassed — independent review found no critical or major issues (or all findings triaged) -
/impl-hygiene-reviewpassed — hygiene review clean. MUST run AFTER/tpr-reviewis clean. -
Subsection close-out (20.3) — MANDATORY before starting the next subsection. Run
/improve-toolingretrospectively on THIS subsection’s debugging journey (per.claude/skills/improve-tooling/SKILL.md“Per-Subsection Workflow”): whichdiagnostics/scripts you ran, where you addeddbg!/tracingcalls, where output was hard to interpret, where test failures gave unhelpful messages, where you ran the same command sequence repeatedly. Forward-look: what tool/log/diagnostic would shorten the next regression in this code path by 10 minutes? Implement improvements NOW (zero deferral) and commit each via SEPARATE/commit-pushusing a valid conventional-commit type (build(diagnostics): ... — surfaced by section-20.3 retrospective—build/test/chore/ci/docsare valid;tools(...)is rejected by the lefthook commit-msg hook). Mandatory even when nothing felt painful. If genuinely no gaps, document briefly: “Retrospective 20.3: no tooling gaps”. Update this subsection’sstatusin section frontmatter tocomplete. -
/sync-claudesection-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW. -
Repo hygiene check — run
diagnostics/repo-hygiene.sh --checkand clean any detected temp files.
20.4 $if Dead Branch Elimination and Splice Resolution
Verified not-started (2026-03-29): No compile-time branch elimination, no splice resolution logic. None of E0462/E0463/E0464 error codes registered. BLOCKED on Section 18.0. ERROR CODE DRIFT: E0462-E0464 are type checker errors but assigned to E0xxx (lexer) range — should be E2xxx per diagnostic conventions.
File(s): compiler/ori_types/src/infer/expr/, compiler/ori_types/src/infer/expr/calls/monomorphization.rs
Goal: $if conditions resolve at monomorphization time; dead branches are not type-checked. Splice value.[field] resolves to direct ExprKind::Field access.
Depends on: 20.1 (metadata types), 20.2 (parser), 20.3 (expansion context).
$if Resolution
-
Evaluate $if condition at monomorphization time
- Condition must be compile-time evaluable boolean
- If true: emit only the then-branch (type-check only then-branch)
- If false: emit only the else-branch (type-check only else-branch)
- Dead branch is parsed but NOT type-checked (syntax errors still reported)
- Compile error E0463 if condition is not compile-time determinable
-
Support nested $if and $if inside $for
- Common pattern:
$if is_struct(T) then { $for field in fields_of(T)... } else { value.to_str() } - Dead branch elimination prevents type errors from irrelevant branch
- Common pattern:
Splice Resolution
-
Resolve
ExprKind::SplicetoExprKind::Fieldduring expansion- Inside
$for field in fields_of(T),value.[field]resolves tovalue.name,value.age, etc. - The
field_exprmust evaluate to a$FieldMetavalue at compile time - Extract
field.name→ look up field in struct type → produceExprKind::Field { receiver, field: name } - Result type is the concrete field type (resolved from pool)
- Inside
-
Splice on left side of assignment
value.[field] = x→value.name = x(follows existing index/field assignment rules)
-
Nested splice
value.[outer_field].[inner_field]— outer field is itself a struct
Error Messages
- E0462: Splice on non-struct value —
items.[field]whereitemsis not a struct - E0463: $if with non-const condition — runtime value in $if condition
- E0464: Heterogeneous yield type mismatch —
$foryield produces incompatible types
Tests
-
Matrix tests ($if): true branch only, false branch only, dead branch with type error (should not trigger), nested $if, $if inside $for, $if with is_struct/is_enum/is_option
-
Matrix tests (splice): single field access, multiple fields in $for, nested struct splice, splice assignment, splice with different field types
-
Semantic pin:
$if is_struct(int) then compile_error("oops") else 42produces42— dead branch not type-checked -
TDD: Write failing tests first
-
/tpr-reviewpassed — independent review found no critical or major issues (or all findings triaged) -
/impl-hygiene-reviewpassed — hygiene review clean. MUST run AFTER/tpr-reviewis clean. -
Subsection close-out (20.4) — MANDATORY before starting the next subsection. Run
/improve-toolingretrospectively on THIS subsection’s debugging journey (per.claude/skills/improve-tooling/SKILL.md“Per-Subsection Workflow”): whichdiagnostics/scripts you ran, where you addeddbg!/tracingcalls, where output was hard to interpret, where test failures gave unhelpful messages, where you ran the same command sequence repeatedly. Forward-look: what tool/log/diagnostic would shorten the next regression in this code path by 10 minutes? Implement improvements NOW (zero deferral) and commit each via SEPARATE/commit-pushusing a valid conventional-commit type (build(diagnostics): ... — surfaced by section-20.4 retrospective—build/test/chore/ci/docsare valid;tools(...)is rejected by the lefthook commit-msg hook). Mandatory even when nothing felt painful. If genuinely no gaps, document briefly: “Retrospective 20.4: no tooling gaps”. Update this subsection’sstatusin section frontmatter tocomplete. -
/sync-claudesection-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW. -
Repo hygiene check — run
diagnostics/repo-hygiene.sh --checkand clean any detected temp files.
20.5 Type Classification Intrinsics
Verified not-started (2026-03-29): No Ori-level
is_struct/is_enum/is_primitive/is_collection/is_option/is_result/is_tupleintrinsic registration. Existing Rust-internalTag::is_primitive()etc. are unrelated (operator dispatch, not Ori compile-time predicates).
File(s): compiler/ori_types/src/check/registration/mod.rs, compiler/ori_types/src/infer/expr/identifiers.rs
Goal: is_struct(T), is_enum(T), is_primitive(T), is_collection(T), is_option(T), is_result(T), is_tuple(T) — compile-time predicates for type classification, usable in $if conditions.
Depends on: 20.1 (intrinsic registration pattern), 20.4 ($if for consumption).
Intrinsic Registration
- Register 7 type classification intrinsics
- Each takes a type parameter OR an expression (checks static type of expression, does not evaluate)
- Type parameter form:
is_struct(T) -> bool - Expression form:
is_option(value.[field]) -> bool— inspects compile-time type - Returns compile-time boolean (usable in
$ifconditions)
Implementation
-
Type parameter form — trivial: check
Tagof resolved type in poolis_struct:Tag::Structis_enum:Tag::Sum/Tag::Variantis_primitive: check againstTag::Int,Tag::Float,Tag::Bool,Tag::Str,Tag::Char,Tag::Byte,Tag::Voidis_collection:Tag::List,Tag::Map,Tag::Setis_option:Tag::Optionis_result:Tag::Resultis_tuple:Tag::Tuple
-
Expression form — resolve expression’s static type, then check tag
- Inside
$for,value.[field]has a concrete static type at each iteration - The intrinsic inspects this type without evaluating the expression
- Inside
Tests
-
Matrix tests (type classification): struct, sum type, newtype, primitive (int, float, bool, str, char, byte, void), collection ([T], {K:V}, Set
), Option, Result, tuple, function type, generic (monomorphized) -
Matrix tests (expression form):
is_option(value.[field])inside $for,is_struct(value.[field])for nested struct,is_primitive(value.[field])for mixed-type struct -
Semantic pin:
is_struct(int)returnsfalse,is_struct(User)returnstrue -
TDD: Write failing tests first
-
/tpr-reviewpassed — independent review found no critical or major issues (or all findings triaged) -
/impl-hygiene-reviewpassed — hygiene review clean. MUST run AFTER/tpr-reviewis clean. -
Subsection close-out (20.5) — MANDATORY before starting the next subsection. Run
/improve-toolingretrospectively on THIS subsection’s debugging journey (per.claude/skills/improve-tooling/SKILL.md“Per-Subsection Workflow”): whichdiagnostics/scripts you ran, where you addeddbg!/tracingcalls, where output was hard to interpret, where test failures gave unhelpful messages, where you ran the same command sequence repeatedly. Forward-look: what tool/log/diagnostic would shorten the next regression in this code path by 10 minutes? Implement improvements NOW (zero deferral) and commit each via SEPARATE/commit-pushusing a valid conventional-commit type (build(diagnostics): ... — surfaced by section-20.5 retrospective—build/test/chore/ci/docsare valid;tools(...)is rejected by the lefthook commit-msg hook). Mandatory even when nothing felt painful. If genuinely no gaps, document briefly: “Retrospective 20.5: no tooling gaps”. Update this subsection’sstatusin section frontmatter tocomplete. -
/sync-claudesection-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW. -
Repo hygiene check — run
diagnostics/repo-hygiene.sh --checkand clean any detected temp files.
20.6 Integration and Verification
Verified not-started (2026-03-29): No evaluator or LLVM handling for reflection constructs (AST variants do not exist). Spec Clause 27 still contains OLD runtime Reflect model — body not rewritten despite SUPERSEDED header. ori-syntax.md update is done (design target documentation).
tests/spec/reflection/directory does not exist. None of E0460/W0461 error codes registered. ERROR CODE DRIFT: E0460/W0461 should be E2xxx range.
File(s): tests/spec/reflection/, compiler/ori_eval/src/, compiler/ori_llvm/src/
Goal: End-to-end verification that compile-time reflection produces correct, zero-cost code across both eval and LLVM paths.
Depends on: All prior subsections (20.1-20.5).
Evaluator Support
- Implement $for expansion in interpreter (
compiler/ori_eval/src/interpreter/)- Evaluator must handle
ExprKind::CompFor,ExprKind::CompIf,ExprKind::Splice - Either expand during const-eval or interpret directly (expand inline)
- Must produce identical results to LLVM path
- Evaluator must handle
LLVM Codegen
- Verify $for expansion produces no LLVM overhead
- After monomorphization, no
CompFor/CompIf/Splicenodes reach codegen - All expansion happens before codegen — LLVM sees only plain field access
ExprKind::Spliceis already resolved toExprKind::Fieldbefore codegen
- After monomorphization, no
Spec and Documentation
- Rewrite spec Clause 27 for compile-time reflection model
- Replace runtime
Reflect/TypeInfo/Unknownwithfields_of/variants_of/$for/$if/splice - Run
/sync-specand/sync-grammar
- Replace runtime
- Update
.claude/rules/ori-syntax.md— already done (2026-03-26 propagation audit)
End-to-End Tests
- Flagship test: structural debug —
$for field in fields_of(T) yield "{field.name}: {value.[field]}"for User struct - Flagship test: structural equality —
$for field in fields_of(T) yield a.[field] == b.[field]with.all() - Recursive reflection test —
deep_debug<T>that recurses into nested structs - Default impl test —
pub def impl ToJsonusing$for+fields_of(Self) - Eval-vs-LLVM equivalence — run
/code-journeyto verify both paths match
Error Message Quality
-
E0460: $for over non-const iterable — clear message with
$vs non-$hint -
W0461: fields_of on known non-struct — warning, not error
-
E0462: splice on non-struct value — show actual type
-
E0463: $if with non-const condition — suggest
ifinstead -
E0464: heterogeneous yield mismatch — show per-iteration types
-
/tpr-reviewpassed — independent review found no critical or major issues (or all findings triaged) -
/impl-hygiene-reviewpassed — hygiene review clean. MUST run AFTER/tpr-reviewis clean. -
Subsection close-out (20.6) — MANDATORY before starting the next subsection. Run
/improve-toolingretrospectively on THIS subsection’s debugging journey (per.claude/skills/improve-tooling/SKILL.md“Per-Subsection Workflow”): whichdiagnostics/scripts you ran, where you addeddbg!/tracingcalls, where output was hard to interpret, where test failures gave unhelpful messages, where you ran the same command sequence repeatedly. Forward-look: what tool/log/diagnostic would shorten the next regression in this code path by 10 minutes? Implement improvements NOW (zero deferral) and commit each via SEPARATE/commit-pushusing a valid conventional-commit type (build(diagnostics): ... — surfaced by section-20.6 retrospective—build/test/chore/ci/docsare valid;tools(...)is rejected by the lefthook commit-msg hook). Mandatory even when nothing felt painful. If genuinely no gaps, document briefly: “Retrospective 20.6: no tooling gaps”. Update this subsection’sstatusin section frontmatter tocomplete. -
/sync-claudesection-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW. -
Repo hygiene check — run
diagnostics/repo-hygiene.sh --checkand clean any detected temp files.
20.R Third Party Review Findings
Verified done (2026-03-29): “None” is correct — no implementation exists to review.
- None.
20.7 Completion Checklist
Verified not-started (2026-03-29): All 15 items unimplemented. No reflection functionality exists in any compiler phase.
-
fields_of(T)returns correct metadata for structs, newtypes, and empty for non-struct types -
variants_of(T)returns correct metadata for sum types -
name_of(T)returns unqualified type name -
$for...yieldproduces correct list from heterogeneous expansion -
$for...doproduces correct void sequence -
$ifeliminates dead branch — dead branch with type error does NOT trigger error -
value.[field]resolves to direct field access — zero overhead in LLVM IR - Type classification intrinsics work in both type-param and expression form
- All error messages (E0460-E0464, W0461) are clear and actionable
- Eval and LLVM paths produce identical results for all reflection tests
-
./test-all.shgreen -
./clippy-all.shgreen - Spec Clause 27 rewritten for compile-time model
-
grammar.ebnfupdated with$for,$if, splice productions -
/tpr-reviewpassed — independent Codex review found no critical or major issues (or all findings triaged) -
/impl-hygiene-reviewpassed — implementation hygiene review clean (phase boundaries, SSOT, algorithmic DRY, naming). MUST run AFTER/tpr-reviewis clean. -
/improve-toolingretrospective completed — MANDATORY at section close, after both reviews are clean. Reflect on the section’s debugging journey (whichdiagnostics/scripts you ran, which command sequences you repeated, where you added ad-hocdbg!/tracingcalls, where output was hard to interpret) and identify any tool/log/diagnostic improvement that would have made this section materially easier OR that would help the next section touching this area. Implement every accepted improvement NOW (zero deferral) and commit each via SEPARATE/commit-push. The retrospective is mandatory even when nothing felt painful — that is exactly when blind spots accumulate. See.claude/skills/improve-tooling/SKILL.md“Retrospective Mode” for the full protocol.
Exit Criteria: $for field in fields_of(User) yield field.name evaluated at compile time produces ["name", "age", "email"] with zero runtime overhead. LLVM IR for reflection-using code is identical to hand-written field-by-field code. All 5 error codes produce clear, actionable diagnostics. Eval and LLVM paths match for all tests. ./test-all.sh and ./clippy-all.sh green.
Verification Notes (2026-03-29)
ERROR CODE DRIFT
Error codes E0460-E0464 and W0461 are assigned in the E0xxx range (lexer errors per diagnostic conventions), but all reflection errors are type checker/monomorphization errors and belong in the E2xxx range. Current highest E2xxx code is E2041. When implementing, either:
- Reassign to E2042-E2046 (consistent with type checker convention), or
- Document E04xx as a deliberate “compile-time expansion” sub-range in
.claude/rules/diagnostic.md.
SPEC CLAUSE 27 STALE
docs/ori_lang/v2026/spec/27-reflection.md contains the OLD runtime Reflect model (Reflect trait, TypeInfo, Unknown, FieldInfo, VariantInfo, field_by_index/field_by_name, #derive(Reflect)). Has a SUPERSEDED (2026-03-26) header but the body has not been rewritten. The grammar.ebnf has no $for, $if, or splice productions. This is tracked in 20.6 but is a significant documentation gap.
DEPENDENCY: SECTION 18.0 NOT STARTED
The const eval bridge (Section 18.0) is entirely unchecked. This blocks 20.3 (expansion) and 20.4 (resolution) but does NOT block 20.1 (registration) or 20.2 (parsing).
PLAN QUALITY GAPS (from verification)
The plan is well-structured but has these gaps to address when implementing:
- Missing negative test specifications: Positive test matrices lack explicit negative pins (e.g.,
$forwith runtime iterable must fail). - Missing interaction testing: No cross-feature interaction tests specified (reflection + closures, reflection + generics, reflection + traits).
- Missing ARC/memory testing: No
ORI_CHECK_LEAKS=1verification mentioned for expanded code. - Missing cleanup section: No final annotation cleanup section per CLAUDE.md requirements.
- Missing dual-execution verification mechanism: Completion checklist mentions eval/LLVM parity but does not specify the tool (
/code-journeyordual-exec-verify.sh). - Heterogeneous yield type unification: Plan does not specify what “unified type” means for
$for...yieldwhen field types differ (Ori has no implicit conversions).
Inspired By
- Zig
@typeInfo+inline for+@field(src/Sema.zig,src/InternPool.zig) - C++26 P2996 static reflection (
template for,[: :]splice)
Verification Notes (2026-03-29)
- ERROR CODE DRIFT: E0460-E0464/W0461 are in the E0xxx (lexer) range but these are type checker/monomorphization errors — should be E2xxx per diagnostic conventions. When implementing, use E2042+ or document E04xx as a deliberate compile-time expansion sub-range.
- SPEC STALE: spec/27-reflection.md contains the OLD runtime Reflect model (Reflect trait, TypeInfo, Unknown, #derive(Reflect)). Has SUPERSEDED header but body not rewritten. grammar.ebnf has no $for/$if/splice productions.
- DEPENDENCY BLOCKED: Section 18.0 (const eval bridge) is not-started. Registration (20.1) and parsing (20.2) can proceed independently, but expansion (20.3) and resolution (20.4) are blocked on it.
- ori-syntax.md documents reflection features that have zero implementation — correct as design target documentation, not a hygiene violation.