Skip to content

Metaprogramming stuff#1455

Draft
micahscopes wants to merge 17 commits into
masterfrom
metaprogramming-stuff
Draft

Metaprogramming stuff#1455
micahscopes wants to merge 17 commits into
masterfrom
metaprogramming-stuff

Conversation

@micahscopes

Copy link
Copy Markdown
Collaborator

messy first draft

Add #[derive(Eq, Default, Ord, Hash, Abi)] support that generates impl
bodies during the lowering phase using HirBuilder — same pipeline as
msg/event/error desugaring. Derived impls land in the scope graph with
proper DeriveDesugared origins and pass type checking normally.

Architecture:
- CodegenSink trait bridges BodyBuilder ↔ derive evaluator (no SymbolicExpr)
- eval_derive_strategy_into interprets strategy patterns via field metadata
- Capability-based design: Reflect<T>, Builder<T>, Hasher, Compare<T>
- Fe strategies in core use capabilities, never trait methods on field values
- Parser extended with .{expr} comptime field access (ComptimeFieldExpr)
- SExpr::DynField + Expr::DynField in semantic/HIR IR
- HirAnalysisDb view registered via zalsa_register_downcaster for CTFE access
- #[derive_strategy] functions filtered from type checking and const checking

Core Fe infrastructure:
- ingots/core/src/reflect.fe — Reflect<T> capability type
- ingots/core/src/builder.fe — Builder<T> capability type
- Hash + Hasher traits, Compare<T> trait in ops.fe
- __derive_eq, __derive_hash, __derive_ord, __derive_default strategies

Test coverage:
- 8 standalone capability unit tests (field_count, field_name, Builder,
  Hasher, Compare, composition)
- 11 uitest fixtures (Eq, Default, Ord, Hash, Abi, empty struct, single
  field, many fields, multi-trait, generic error, unknown trait error)
…d/DynField

Add eval_derive_with_machine function that sets up the CTFE machine with
derive_sink (CodegenSink via lifetime-erased pointer) and symbolic params.

Machine operation handlers now check for Symbolic values:
- BinOp on two Symbolics → emit Expr::Bin to sink, return new Symbolic
- Field on Symbolic base → emit Expr::Field to sink, return new Symbolic
- DynField with Symbolic base + Name field → emit Expr::Field with IdentId

Also:
- Register HirAnalysisDb view via zalsa_register_downcaster in DriverDataBase
- Add Hash/Hasher/Compare traits to core Fe (capability-based strategies)
- Thread ingot through derive impl generation for strategy lookup
- Validate strategy func exists via find_strategy_func before attempting CTFE

The machine path currently falls through to the pattern evaluator because
strategy SMIR bodies aren't accessible yet (type checking filtered).
Next step: enable relaxed type checking for strategies to produce valid SMIR.
The CTFE machine's derive_strategy_instance was calling lower_adt on the
user's struct during scope_graph_impl, causing a salsa query cycle. Fixed
by using TyId::unit as placeholder for T — the machine intercepts reflect
intrinsics directly from field_names, never resolving T's actual structure.

Also removed catch_unwind band-aid. The machine now runs cleanly (no panic)
and falls through to pattern evaluator via Err when strategy SMIR evaluation
doesn't complete (remaining: reflect interception, branch/return on symbolic).
Three interception points wired into the existing CtfeMachine:
- Call: intercepts reflect.field_count()/field_name() → returns concrete
  values from derive_field_names
- Branch: when condition is Symbolic(ExprId), emits if-guard to sink
  and skips to else_bb
- Return: when value is Symbolic or Bool, emits return statement to sink

Also adds derive_strategy_instance (ported from const-reflect) which
builds SemanticInstance with proper generic arg padding for effect
provider params via collect_generic_params.

The CTFE path is blocked on SMIR lowering: strategy bodies use DynField
which gets invalid type, causing path resolution panic in SMIR lower.
Fix: teach type checker that DynField has a deferred type (next commit).
Pattern evaluator remains active until that's resolved.
Instead of blanket-filtering #[derive_strategy] functions from type
checking (hiding real bugs), teach the type checker to handle DynField:
- DynField now gets a fresh type variable (deferred type)
- Strategy functions ARE type-checked (SMIR bodies generated for CTFE)
- Strategy diagnostics suppressed in analysis pass (DynField vars unresolved)
- Force check_func_body on strategies so SMIR is available for CTFE

CTFE machine path still blocked: BinOps on DynField fresh vars can't
resolve to trait methods, causing SMIR call lowering to panic. Fix
requires deferred call sites in SMIR for unresolved ops in strategy
context. Pattern evaluator remains active pending that fix.
When SMIR lowering encounters an unresolved call-like expression in a
strategy body (operators on DynField-typed values that couldn't resolve
to trait methods), fall back to emitting SExpr::Binary directly from
the HIR BinOp. This allows the CTFE machine to handle them symbolically.

Also removes the blanket type-checking filter: strategies are now
type-checked (DynField gets fresh type var), their diagnostics suppressed
in the analysis pass, and check_func_body is forced so SMIR is available.

CTFE path still blocked: strategy bodies hit path value classification
panic for path expressions whose types couldn't be fully resolved.
Next: teach SMIR path lowering to accept unresolved paths in strategy
context (emit as Forward/UseValue instead of panicking).
Two SMIR lowering panics fixed for strategy bodies:
- Unresolved call-like expressions (operator desugaring that can't
  resolve to trait methods on fresh type vars): fallback to emitting
  SExpr::Binary from the HIR BinOp directly
- Unresolved path value classification: fallback to ReadPlace when
  the path has a known local binding

Strategy bodies still have more unresolved expressions (e.g., range
expressions, for loop desugaring) that need similar treatment. The
pattern evaluator remains active. Next: systematic SMIR relaxation
for all expression kinds that strategy bodies use.
Implement HirStrategyInterp — a HIR-level strategy interpreter that walks
the __derive_eq strategy's Body AST directly. The Fe strategy function IS
the source of truth: the interpreter resolves reflect intrinsics concretely
(field_count → len, field_name(i) → field_names[i]) and emits symbolic
operations (BinOp, Field, if-guards) via CodegenSink.

derive_eq passes with CTFE driving output — no pattern evaluator fallback.
derive_ord also passes (same pattern: for loop + comparison + guard).

Remaining: Default (needs Builder set_field/finish → RecordInit),
Hash (needs Hasher.feed → .hash() accumulation).
The HirStrategyInterp now handles all derive patterns:
- Eq: for loop + DynField + BinOp(!=) + if-guard → works
- Ord: for loop + DynField + BinOp(<) + if-guard → works
- Default: Builder.set_field/finish → RecordInit → works
- Hash: Hasher.feed/finish → accumulation chain → works
- Abi: const computation (unchanged) → works

Key fixes:
- Local variable lookup (loop vars) via pat name matching
- Builder.finish with empty fields (empty struct Default)
- Hasher accumulator: feed() builds hash chain, finish() returns it
- Distinguish Builder vs Hasher finish via hash_accum presence
The pattern-based evaluator (eval_derive_strategy_into) is now #[cfg(test)]
only — used by unit tests that validate CodegenSink output shape.

Production path: eval_strategy_from_hir drives ALL derives (Eq, Ord, Hash,
Default, Abi) by walking the Fe strategy function's HIR body directly.
The Fe strategy code IS the source of truth — no hardcoded Rust fallback.

Ord's lt method also goes through the HIR walker now (was the last holdout).
Delete all derive-mode code from CtfeMachine:
- derive_strategy_instance, eval_derive_with_machine
- derive_sink/derive_field_names fields
- derive_sink_ref helper
- All BinOp/Field/DynField/Branch/Return/Call interception blocks

The HIR walker (eval_strategy_from_hir) handles all derive evaluation.
No dead code, no #[allow(dead_code)] on derive infrastructure.
Only CtfeConstKind::Name/Symbolic variants remain (harmless, future use).
- Fix __derive_ord strategy: add `if a > b { return false }` guard
  for correct lexicographic comparison (was missing, only had `< → true`)
- Add multi-field Ord test (Coordinate{x,y,z} with lt/le/gt/ge)
- Add CTFE-driven verification test (Triple struct proves strategy
  body evaluation produces correct output for 3-field structs)

13 derive tests now pass, all driven by the HIR walker.
Two new unit tests:
- strategy_body_drives_output_not_hardcoded: proves field_count scales
  output (1 field → 1 guard, 3 fields → 3 guards)
- different_strategies_produce_different_output: proves Eq/Ord/Default
  produce structurally different HIR (!=  vs < vs RecordInit)

These would fail if the evaluator used hardcoded logic instead of
reading the strategy function's Body AST.
…robustness

- Fix critical double-emission bug: Expr::If with symbolic condition no longer
  evaluates the then-block (which emitted unconditional returns). Stmt::Expr is
  now the sole emission point for if-guards.
- Fix Hash on empty struct: track in_hash_context flag to distinguish hash
  finish (return 0) from builder finish (return RecordInit).
- Fix for-loop start value: use evaluated start, not always 0.
- Fix Expr::Call substring matching: match on exact last path segment.
- Handle Stmt::Let bindings in the interpreter.
- Handle Expr::Un (unary not) for symbolic and concrete values.
- Remove dead CtfeConstKind::Name/Symbolic variants from CTFE machine.
- Remove dead Compare<T> trait and unused uses clause from __derive_ord.
- Remove "Copy" from KNOWN_DERIVE_TRAITS (no codegen exists for it).
- Remove stale #[allow(dead_code)] annotations on used functions.
- Convert find_strategy_func panics to graceful fallbacks.
- Remove db() from CodegenSink trait (only needed by test helpers).
- Move test pattern evaluators into mod tests where MockSink is defined.
- Delete eval_derive_strategy_into dispatch function.
…ed dispatch

- Write __derive_le, __derive_gt, __derive_ge Fe strategies in ops.fe
  that express semantics directly: le = !(other < self), gt = other < self,
  ge = !(self < other). All Ord methods now go through the HIR walker.
- Delete emit_ord_le_body/emit_ord_gt_body/emit_ord_ge_body hardcoded Rust
  functions. The ORD_METHODS table drives all 4 methods uniformly.
- Add InterpValue::Capability(CapabilityKind) for type-aware method dispatch.
  Receiver resolution now matches on (capability_kind, method_name) pairs
  instead of just method name strings. This prevents incorrect dispatch when
  methods share names across capabilities (e.g. Builder.finish vs Hasher.finish).
- Resolve capability names (reflect, builder, hasher) from Path expressions
  instead of using positional params.
- Remove in_hash_context flag (superseded by capability-based dispatch).
@micahscopes

Copy link
Copy Markdown
Collaborator Author

c.c. @sbillig

@micahscopes

Copy link
Copy Markdown
Collaborator Author

@g-r-a-n-t @cburgdorf I'd be curious to get feedback from ya'll too

@cburgdorf

Copy link
Copy Markdown
Collaborator

Had an agent walk me through the PR. Just glanced over it but general direction looks promising to me. End goal would be support for reflection and users being able to come up with their own #[derive(MyTrait)] macros etc?

@micahscopes

Copy link
Copy Markdown
Collaborator Author

@cburgdorf exactly, and also to be able to do the same and similar in std/core, potentially including in desugared fe code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants