Metaprogramming stuff#1455
Draft
micahscopes wants to merge 17 commits into
Draft
Conversation
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).
Collaborator
Author
|
c.c. @sbillig |
Collaborator
Author
|
@g-r-a-n-t @cburgdorf I'd be curious to get feedback from ya'll too |
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 |
Collaborator
Author
|
@cburgdorf exactly, and also to be able to do the same and similar in std/core, potentially including in desugared fe code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
messy first draft