diff --git a/crates/common/src/diagnostics.rs b/crates/common/src/diagnostics.rs index ea24a851d5..b9b9530f01 100644 --- a/crates/common/src/diagnostics.rs +++ b/crates/common/src/diagnostics.rs @@ -187,6 +187,7 @@ pub enum DiagnosticPass { MsgLower, EventLower, ErrorLower, + DeriveLower, ArithmeticAttr, PayableAttr, @@ -215,6 +216,7 @@ impl DiagnosticPass { Self::MsgLower => 9, Self::EventLower => 10, Self::ErrorLower => 16, + Self::DeriveLower => 17, Self::ArithmeticAttr => 12, Self::PayableAttr => 13, Self::InlineAttr => 14, diff --git a/crates/hir/src/analysis/analysis_pass.rs b/crates/hir/src/analysis/analysis_pass.rs index 5ebc7e3b92..9b943bb710 100644 --- a/crates/hir/src/analysis/analysis_pass.rs +++ b/crates/hir/src/analysis/analysis_pass.rs @@ -1,7 +1,7 @@ use crate::analysis::{HirAnalysisDb, diagnostics::DiagnosticVoucher}; use crate::{ - ArithmeticAttrError, ErrorDiagnostic, EventError, InlineAttrError, LoopUnrollAttrError, - ParserError, PayableError, SelectorError, + ArithmeticAttrError, DeriveError, ErrorDiagnostic, EventError, InlineAttrError, + LoopUnrollAttrError, ParserError, PayableError, SelectorError, hir_def::{ModuleTree, TopLevelMod}, lower::{parse_file_impl, scope_graph_impl}, }; @@ -131,6 +131,22 @@ impl ModuleAnalysisPass for ErrorLowerPass { } } +/// Analysis pass that collects derive lowering errors from `#[derive(...)]` struct desugaring. +pub struct DeriveLowerPass {} + +impl ModuleAnalysisPass for DeriveLowerPass { + fn run_on_module<'db>( + &mut self, + db: &'db dyn HirAnalysisDb, + top_mod: TopLevelMod<'db>, + ) -> Vec> { + scope_graph_impl::accumulated::(db, top_mod) + .into_iter() + .map(|d| Box::new(d.clone()) as _) + .collect::>() + } +} + /// Analysis pass that collects arithmetic attribute validation errors. pub struct ArithmeticAttrPass {} diff --git a/crates/hir/src/analysis/diagnostics.rs b/crates/hir/src/analysis/diagnostics.rs index 6aceaa7b77..190eeb13c9 100644 --- a/crates/hir/src/analysis/diagnostics.rs +++ b/crates/hir/src/analysis/diagnostics.rs @@ -712,6 +712,52 @@ impl DiagnosticVoucher for crate::ErrorDiagnostic { } } +impl DiagnosticVoucher for crate::DeriveError { + fn to_complete(&self, _db: &dyn SpannedHirAnalysisDb) -> CompleteDiagnostic { + use crate::DeriveErrorKind; + + let primary_span = Span::new(self.file, self.primary_range, SpanKind::Original); + + let (code, message, label, notes) = match &self.kind { + DeriveErrorKind::DeriveOnEnum => ( + 1, + "`#[derive]` is only valid on structs".to_string(), + "`#[derive]` cannot be applied to enums".to_string(), + vec!["move `#[derive]` to a struct declaration".to_string()], + ), + DeriveErrorKind::DeriveOnGenericStruct { trait_name } => ( + 2, + format!("`#[derive({trait_name})]` structs must be non-generic"), + "generics are not supported on derived structs".to_string(), + vec!["remove generic parameters from the struct".to_string()], + ), + DeriveErrorKind::UnknownDeriveTrait { name } => ( + 3, + format!("unknown derive trait `{name}`"), + format!("`{name}` is not a recognized derive trait"), + vec![format!( + "recognized traits: {}", + crate::lower::derive::KNOWN_DERIVE_TRAITS.join(", ") + )], + ), + }; + + let error_code = GlobalErrorCode::new(DiagnosticPass::DeriveLower, code); + + CompleteDiagnostic::new( + Severity::Error, + message, + vec![SubDiagnostic::new( + LabelStyle::Primary, + label, + Some(primary_span), + )], + notes, + error_code, + ) + } +} + impl DiagnosticVoucher for crate::InlineAttrError { fn to_complete(&self, _db: &dyn SpannedHirAnalysisDb) -> CompleteDiagnostic { use crate::hir_def::InlineAttrErrorKind; diff --git a/crates/hir/src/analysis/mod.rs b/crates/hir/src/analysis/mod.rs index 9b32b894e9..351984466f 100644 --- a/crates/hir/src/analysis/mod.rs +++ b/crates/hir/src/analysis/mod.rs @@ -6,8 +6,8 @@ pub mod place; pub mod semantic; use self::analysis_pass::{ - AnalysisPassManager, ArithmeticAttrPass, ErrorLowerPass, EventLowerPass, InlineAttrPass, - LoopUnrollAttrPass, MsgLowerPass, ParsingPass, PayableAttrPass, + AnalysisPassManager, ArithmeticAttrPass, DeriveLowerPass, ErrorLowerPass, EventLowerPass, + InlineAttrPass, LoopUnrollAttrPass, MsgLowerPass, ParsingPass, PayableAttrPass, }; use self::name_resolution::ImportAnalysisPass; use self::ty::{ @@ -33,6 +33,7 @@ pub fn initialize_analysis_pass() -> AnalysisPassManager { pass_manager.add_module_pass("MsgLower", Box::new(MsgLowerPass {})); pass_manager.add_module_pass("EventLower", Box::new(EventLowerPass {})); pass_manager.add_module_pass("ErrorLower", Box::new(ErrorLowerPass {})); + pass_manager.add_module_pass("DeriveLower", Box::new(DeriveLowerPass {})); pass_manager.add_module_pass("InlineAttr", Box::new(InlineAttrPass {})); pass_manager.add_module_pass("LoopUnrollAttr", Box::new(LoopUnrollAttrPass {})); pass_manager.add_module_pass("MsgSelector", Box::new(MsgSelectorAnalysisPass {})); diff --git a/crates/hir/src/analysis/semantic/borrowck/normalize.rs b/crates/hir/src/analysis/semantic/borrowck/normalize.rs index efd65f774e..6915c4d42e 100644 --- a/crates/hir/src/analysis/semantic/borrowck/normalize.rs +++ b/crates/hir/src/analysis/semantic/borrowck/normalize.rs @@ -738,6 +738,7 @@ impl<'db> NormalizeCtxt<'db> { place, } } + SExpr::DynField { base, .. } => NExpr::Use(self.normalize_operand(*base, origin)), SExpr::Index { base, index } => { let place = self.project_local_place( base.value, diff --git a/crates/hir/src/analysis/semantic/ctfe/canonicalize.rs b/crates/hir/src/analysis/semantic/ctfe/canonicalize.rs index 32ebd9dba6..81ba201aa2 100644 --- a/crates/hir/src/analysis/semantic/ctfe/canonicalize.rs +++ b/crates/hir/src/analysis/semantic/ctfe/canonicalize.rs @@ -264,6 +264,7 @@ fn writable_local_roots<'db>( | SExpr::ExtractEnumField { .. } | SExpr::CodeRegionOffset { .. } | SExpr::CodeRegionLen { .. } + | SExpr::DynField { .. } | SExpr::Call { .. } => {} } } diff --git a/crates/hir/src/analysis/semantic/ctfe/derive_eval.rs b/crates/hir/src/analysis/semantic/ctfe/derive_eval.rs new file mode 100644 index 0000000000..1d3784d276 --- /dev/null +++ b/crates/hir/src/analysis/semantic/ctfe/derive_eval.rs @@ -0,0 +1,943 @@ +use crate::hir_def::{ + BinOp, Body, CompBinOp, Cond, CondId, Expr, ExprId, FieldIndex, Func, IdentId, LitKind, + Partial, Pat, PatId, PathId, Stmt, StmtId, +}; + +use std::collections::HashMap; + +/// Codegen sink that emits HIR expressions/statements. Implemented by +/// BodyBuilder in the lowering phase. The CTFE derive evaluator drives +/// this to produce the impl body directly — no intermediate representation. +pub(crate) trait CodegenSink<'db> { + fn push_expr(&mut self, expr: Expr<'db>) -> ExprId; + fn push_cond(&mut self, cond: Cond) -> CondId; + fn push_stmt(&mut self, stmt: Stmt<'db>) -> StmtId; + fn emit_stmt(&mut self, stmt: Stmt<'db>) -> StmtId; + fn emit_expr_stmt(&mut self, expr: ExprId) -> StmtId; +} + +/// Evaluates a derive strategy function, emitting HIR directly into the sink. +/// The strategy's reflect intrinsics are resolved concretely using field_specs. +/// Operations on symbolic parameters (ExprId) emit directly via the sink. +/// +/// This replaces the hardcoded emit_eq_body/emit_hash_body/etc. functions. +/// The Fe strategy function is the source of truth; this evaluator interprets +/// the strategy's logic using the struct's concrete field information. +/// Evaluates a derive strategy by walking its HIR Body directly. +/// This is the CTFE-driven path: the Fe strategy function IS the source of truth. +/// The interpreter resolves reflect intrinsics concretely (from field_names) +/// and emits symbolic operations (ExprIds) via the CodegenSink. +pub(crate) fn eval_strategy_from_hir<'db>( + db: &'db dyn crate::HirDb, + strategy_func: Func<'db>, + field_names: &[IdentId<'db>], + sink: &mut dyn CodegenSink<'db>, +) -> bool { + let Some(body) = strategy_func.body(db) else { + return false; + }; + let mut interp = HirStrategyInterp::new(db, body, field_names, sink); + interp.eval_body() +} + +/// Value tracked during HIR interpretation — either concrete or symbolic (ExprId). +#[derive(Clone)] +enum InterpValue<'db> { + /// Concrete integer (loop counter, field count) + Int(usize), + /// Concrete field name from reflect.field_name() + Name(IdentId<'db>), + /// Symbolic runtime value — an ExprId already emitted to the sink + Symbolic(ExprId), + /// Concrete boolean + Bool(bool), + /// A typed capability from the strategy's `uses` clause + Capability(CapabilityKind), + /// Unit + Unit, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum CapabilityKind { + Reflect, + Builder, + Hasher, +} + +/// HIR-level interpreter for derive strategy functions. +/// Walks the strategy's Body AST, resolving reflect intrinsics concretely +/// and emitting symbolic operations via CodegenSink. +struct HirStrategyInterp<'a, 'db> { + db: &'db dyn crate::HirDb, + body: Body<'db>, + field_names: &'a [IdentId<'db>], + sink: &'a mut dyn CodegenSink<'db>, + /// Local variable bindings (PatId → value) + locals: HashMap>, + /// Function params by index + params: Vec>, + /// Builder accumulator: (field_index, value) pairs from set_field calls + builder_fields: Vec<(usize, InterpValue<'db>)>, + /// Hasher accumulator: running hash expression + hash_accum: Option, +} + +impl<'a, 'db> HirStrategyInterp<'a, 'db> { + fn new( + db: &'db dyn crate::HirDb, + body: Body<'db>, + field_names: &'a [IdentId<'db>], + sink: &'a mut dyn CodegenSink<'db>, + ) -> Self { + // Strategy params: self_val (symbolic), other (symbolic), capabilities (unit) + let self_expr = sink.push_expr(Expr::Path(Partial::Present(PathId::from_ident( + db, + IdentId::make_self(db), + )))); + let other_ident = IdentId::new(db, "other".to_string()); + let other_expr = sink.push_expr(Expr::Path(Partial::Present(PathId::from_ident( + db, + other_ident, + )))); + Self { + db, + body, + field_names, + sink, + locals: HashMap::new(), + params: vec![ + InterpValue::Symbolic(self_expr), + InterpValue::Symbolic(other_expr), + ], + builder_fields: Vec::new(), + hash_accum: None, + } + } + + fn eval_body(&mut self) -> bool { + let root = self.body.expr(self.db); + let result = self.eval_expr(root); + // Emit final return if the body produces a value + match result { + InterpValue::Bool(b) => { + let lit = self.sink.push_expr(Expr::Lit(LitKind::Bool(b))); + self.sink.emit_stmt(Stmt::Return(Some(lit))); + } + InterpValue::Symbolic(expr_id) => { + self.sink.emit_stmt(Stmt::Return(Some(expr_id))); + } + _ => {} + } + true + } + + fn eval_expr(&mut self, expr: ExprId) -> InterpValue<'db> { + let e = self.body.exprs(self.db)[expr].clone(); + let Partial::Present(e) = e else { + return InterpValue::Unit; + }; + match e { + Expr::Block(stmts) => { + let mut result = InterpValue::Unit; + for stmt_id in stmts { + result = self.eval_stmt(stmt_id); + } + result + } + Expr::Lit(lit) => match lit { + LitKind::Bool(b) => InterpValue::Bool(b), + LitKind::Int(int_id) => { + use num_traits::ToPrimitive; + let val = int_id.data(self.db); + InterpValue::Int(val.to_u64().unwrap_or(0) as usize) + } + _ => InterpValue::Unit, + }, + Expr::Path(path) => { + if let Partial::Present(path_id) = path { + let name_str = path_id.pretty_print(self.db); + if name_str == "self" || name_str == "self_val" { + return self.params[0].clone(); + } + if name_str == "other" { + return self.params[1].clone(); + } + if name_str == "true" { + return InterpValue::Bool(true); + } + if name_str == "false" { + return InterpValue::Bool(false); + } + if name_str == "reflect" { + return InterpValue::Capability(CapabilityKind::Reflect); + } + if name_str == "builder" { + return InterpValue::Capability(CapabilityKind::Builder); + } + if name_str == "hasher" { + return InterpValue::Capability(CapabilityKind::Hasher); + } + for (pat_id, val) in &self.locals { + let pat = self.body.pats(self.db)[*pat_id].clone(); + if let Partial::Present(Pat::Path(Partial::Present(pat_path), _)) = pat + && pat_path.pretty_print(self.db) == name_str + { + return val.clone(); + } + } + } + InterpValue::Unit + } + Expr::DynField(base, field_expr) => { + let base_val = self.eval_expr(base); + let field_val = self.eval_expr(field_expr); + match (&base_val, &field_val) { + (InterpValue::Symbolic(base_id), InterpValue::Name(name)) => { + let expr_id = self.sink.push_expr(Expr::Field( + *base_id, + Partial::Present(FieldIndex::Ident(*name)), + )); + InterpValue::Symbolic(expr_id) + } + _ => InterpValue::Unit, + } + } + Expr::Bin(lhs, rhs, op) => { + let lhs_val = self.eval_expr(lhs); + let rhs_val = self.eval_expr(rhs); + match (&lhs_val, &rhs_val) { + (InterpValue::Symbolic(l), InterpValue::Symbolic(r)) => { + let expr_id = self.sink.push_expr(Expr::Bin(*l, *r, op)); + InterpValue::Symbolic(expr_id) + } + (InterpValue::Int(l), InterpValue::Int(r)) => { + // Concrete int comparison (for range bounds) + match op { + BinOp::Comp(CompBinOp::Lt) => InterpValue::Bool(*l < *r), + BinOp::Comp(CompBinOp::NotEq) => InterpValue::Bool(*l != *r), + _ => InterpValue::Unit, + } + } + _ => InterpValue::Unit, + } + } + Expr::MethodCall(receiver, method_name, _, args) => { + let recv_val = self.eval_expr(receiver); + let method = method_name.to_opt().map(|n| n.data(self.db).to_string()); + let method_str = method.as_deref(); + + match (&recv_val, method_str) { + // Reflect.field_count() → concrete field count + (InterpValue::Capability(CapabilityKind::Reflect), Some("field_count")) => { + InterpValue::Int(self.field_names.len()) + } + // Reflect.field_name(i) → concrete field name + (InterpValue::Capability(CapabilityKind::Reflect), Some("field_name")) => { + let idx = args + .first() + .map(|a| self.eval_expr(a.expr)) + .and_then(|v| match v { + InterpValue::Int(i) => Some(i), + _ => None, + }) + .unwrap_or(0); + self.field_names + .get(idx) + .map(|n| InterpValue::Name(*n)) + .unwrap_or(InterpValue::Unit) + } + // Builder.set_field(index, value) → accumulate field + (InterpValue::Capability(CapabilityKind::Builder), Some("set_field")) => { + let idx = args + .first() + .map(|a| self.eval_expr(a.expr)) + .and_then(|v| match v { + InterpValue::Int(i) => Some(i), + _ => None, + }) + .unwrap_or(0); + let value = args + .get(1) + .map(|a| self.eval_expr(a.expr)) + .unwrap_or(InterpValue::Unit); + self.builder_fields.push((idx, value)); + InterpValue::Unit + } + // Builder.finish() → emit RecordInit + (InterpValue::Capability(CapabilityKind::Builder), Some("finish")) => { + let db = self.db; + let fields: Vec<_> = self + .builder_fields + .drain(..) + .filter_map(|(idx, val)| { + let field_name = self.field_names.get(idx)?; + let expr_id = match val { + InterpValue::Symbolic(id) => id, + _ => return None, + }; + Some(crate::hir_def::Field { + label: Some(*field_name), + expr: expr_id, + }) + }) + .collect(); + let self_path = + Partial::Present(PathId::from_ident(db, IdentId::make_self_ty(db))); + let record = self.sink.push_expr(Expr::RecordInit(self_path, fields)); + InterpValue::Symbolic(record) + } + // Hasher.feed(value) → hash the field and accumulate + (InterpValue::Capability(CapabilityKind::Hasher), Some("feed")) => { + if let Some(arg) = args.first() { + let val = self.eval_expr(arg.expr); + if let InterpValue::Symbolic(field_id) = val { + let hash_ident = IdentId::new(self.db, "hash".to_string()); + let field_hash = self.sink.push_expr(Expr::MethodCall( + field_id, + Partial::Present(hash_ident), + crate::hir_def::GenericArgListId::none(self.db), + vec![], + )); + let acc = self.hash_accum.unwrap_or_else(|| { + self.sink.push_expr(Expr::Lit(LitKind::Int( + crate::hir_def::IntegerId::new( + self.db, + num_bigint::BigUint::from(0u64), + ), + ))) + }); + let thirty_one = self.sink.push_expr(Expr::Lit(LitKind::Int( + crate::hir_def::IntegerId::new( + self.db, + num_bigint::BigUint::from(31u64), + ), + ))); + let mul = self.sink.push_expr(Expr::Bin( + acc, + thirty_one, + BinOp::Arith(crate::hir_def::ArithBinOp::Mul), + )); + let add = self.sink.push_expr(Expr::Bin( + mul, + field_hash, + BinOp::Arith(crate::hir_def::ArithBinOp::Add), + )); + self.hash_accum = Some(add); + } + } + InterpValue::Unit + } + // Hasher.finish() → return accumulated hash (or 0 for empty) + (InterpValue::Capability(CapabilityKind::Hasher), Some("finish")) => { + if let Some(acc) = self.hash_accum.take() { + InterpValue::Symbolic(acc) + } else { + let zero = self.sink.push_expr(Expr::Lit(LitKind::Int( + crate::hir_def::IntegerId::new( + self.db, + num_bigint::BigUint::from(0u64), + ), + ))); + InterpValue::Symbolic(zero) + } + } + // .hash() on a symbolic field value → emit method call + (InterpValue::Symbolic(recv_id), Some("hash")) => { + let hash_ident = IdentId::new(self.db, "hash".to_string()); + let expr_id = self.sink.push_expr(Expr::MethodCall( + *recv_id, + Partial::Present(hash_ident), + crate::hir_def::GenericArgListId::none(self.db), + vec![], + )); + InterpValue::Symbolic(expr_id) + } + _ => recv_val, + } + } + Expr::If(cond_id, then_expr, _else_expr) => { + let cond = self.body.conds(self.db)[cond_id].clone(); + if let Partial::Present(Cond::Expr(cond_expr)) = cond { + let cond_val = self.eval_expr(cond_expr); + match cond_val { + InterpValue::Bool(true) => return self.eval_expr(then_expr), + InterpValue::Bool(false) => return InterpValue::Unit, + InterpValue::Symbolic(_) => { + // Symbolic condition: do NOT evaluate the then-block here. + // Stmt::Expr is the sole emission point for if-guards — + // it re-reads this If, evaluates the condition again, peeks + // at the then-block's return value, and emits the complete + // if-guard with proper wrapping. + } + _ => {} + } + } + InterpValue::Unit + } + Expr::Un(inner, op) => { + let val = self.eval_expr(inner); + match val { + InterpValue::Bool(b) => match op { + crate::hir_def::UnOp::Not => InterpValue::Bool(!b), + _ => InterpValue::Unit, + }, + InterpValue::Symbolic(id) => { + let expr_id = self.sink.push_expr(Expr::Un(id, op)); + InterpValue::Symbolic(expr_id) + } + _ => InterpValue::Unit, + } + } + Expr::Call(callee, _call_args) => { + let callee_e = self.body.exprs(self.db)[callee].clone(); + if let Partial::Present(Expr::Path(Partial::Present(path))) = callee_e { + let last_ident = path.ident(self.db); + let is_default = last_ident + .to_opt() + .is_some_and(|id| id.data(self.db) == "default"); + if is_default { + let default_path = PathId::from_ident( + self.db, + IdentId::new(self.db, "Default".to_string()), + ) + .push_str(self.db, "default"); + let callee_id = self + .sink + .push_expr(Expr::Path(Partial::Present(default_path))); + let call_id = self.sink.push_expr(Expr::Call(callee_id, vec![])); + return InterpValue::Symbolic(call_id); + } + } + InterpValue::Unit + } + _ => InterpValue::Unit, + } + } + + fn eval_stmt(&mut self, stmt_id: StmtId) -> InterpValue<'db> { + let stmt = self.body.stmts(self.db)[stmt_id].clone(); + let Partial::Present(stmt) = stmt else { + return InterpValue::Unit; + }; + match stmt { + Stmt::Expr(expr) => { + let e = self.body.exprs(self.db)[expr].clone(); + if let Partial::Present(Expr::If(cond_id, then_expr, _)) = e { + let cond = self.body.conds(self.db)[cond_id].clone(); + if let Partial::Present(Cond::Expr(cond_expr)) = cond { + let cond_val = self.eval_expr(cond_expr); + if let InterpValue::Symbolic(sym_cond) = cond_val { + // Peek at then block for return value (don't fully eval it) + let then_e = self.body.exprs(self.db)[then_expr].clone(); + if let Partial::Present(Expr::Block(stmts)) = then_e { + for s in &stmts { + let inner = self.body.stmts(self.db)[*s].clone(); + if let Partial::Present(Stmt::Return(Some(ret_expr))) = inner { + let ret_val = self.eval_expr(ret_expr); + if let InterpValue::Bool(b) = ret_val { + let cond = self.sink.push_cond(Cond::Expr(sym_cond)); + let lit = + self.sink.push_expr(Expr::Lit(LitKind::Bool(b))); + let ret = self.sink.push_stmt(Stmt::Return(Some(lit))); + let blk = self.sink.push_expr(Expr::Block(vec![ret])); + let if_expr = + self.sink.push_expr(Expr::If(cond, blk, None)); + self.sink.emit_expr_stmt(if_expr); + } + } + } + } + return InterpValue::Unit; + } else { + // Concrete condition — evaluate normally + return self.eval_expr(expr); + } + } + } + self.eval_expr(expr) + } + Stmt::Return(Some(expr)) => { + let val = self.eval_expr(expr); + match val { + InterpValue::Bool(b) => { + let lit = self.sink.push_expr(Expr::Lit(LitKind::Bool(b))); + self.sink.emit_stmt(Stmt::Return(Some(lit))); + } + InterpValue::Symbolic(expr_id) => { + self.sink.emit_stmt(Stmt::Return(Some(expr_id))); + } + _ => {} + } + val + } + Stmt::Return(None) => { + self.sink.emit_stmt(Stmt::Return(None)); + InterpValue::Unit + } + Stmt::For(pat, iter_expr, body_expr, _) => { + let iter_e = self.body.exprs(self.db)[iter_expr].clone(); + let (start_idx, end_idx) = if let Partial::Present(Expr::Bin( + start, + end, + BinOp::Arith(crate::hir_def::ArithBinOp::Range), + )) = iter_e + { + let start_val = match self.eval_expr(start) { + InterpValue::Int(n) => n, + _ => 0, + }; + let end_val = match self.eval_expr(end) { + InterpValue::Int(n) => n, + _ => 0, + }; + (start_val, end_val) + } else { + (0, 0) + }; + + for i in start_idx..end_idx { + self.locals.insert(pat, InterpValue::Int(i)); + self.eval_expr(body_expr); + } + InterpValue::Unit + } + Stmt::Let(pat, _ty, init) => { + let val = init.map(|e| self.eval_expr(e)).unwrap_or(InterpValue::Unit); + self.locals.insert(pat, val); + InterpValue::Unit + } + _ => InterpValue::Unit, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_db::HirAnalysisTestDb; + + struct MockSink<'db> { + db: &'db dyn crate::HirDb, + exprs: Vec>, + stmts: Vec>, + emitted_stmts: Vec>, + next_expr_id: u32, + } + + impl<'db> MockSink<'db> { + fn new(db: &'db dyn crate::HirDb) -> Self { + Self { + db, + exprs: Vec::new(), + stmts: Vec::new(), + emitted_stmts: Vec::new(), + next_expr_id: 0, + } + } + + fn has_field_access(&self, field_name: &str) -> bool { + self.exprs.iter().any(|e| { + matches!(e, Expr::Field(_, Partial::Present(FieldIndex::Ident(id))) + if id.data(self.db) == field_name) + }) + } + + fn has_return(&self) -> bool { + self.emitted_stmts + .iter() + .any(|s| matches!(s, Stmt::Return(_))) + } + + fn count_bin_ops(&self, op: BinOp) -> usize { + self.exprs + .iter() + .filter(|e| matches!(e, Expr::Bin(_, _, o) if *o == op)) + .count() + } + + fn count_if_exprs(&self) -> usize { + self.exprs + .iter() + .filter(|e| matches!(e, Expr::If(..))) + .count() + } + + fn has_record_init(&self) -> bool { + self.exprs.iter().any(|e| matches!(e, Expr::RecordInit(..))) + } + + fn count_method_calls(&self, method: &str) -> usize { + self.exprs + .iter() + .filter(|e| { + matches!(e, Expr::MethodCall(_, Partial::Present(id), _, _) + if id.data(self.db) == method) + }) + .count() + } + } + + impl<'db> CodegenSink<'db> for MockSink<'db> { + fn push_expr(&mut self, expr: Expr<'db>) -> ExprId { + self.exprs.push(expr); + self.next_expr_id += 1; + ExprId::from_u32(self.next_expr_id - 1) + } + fn push_cond(&mut self, _cond: Cond) -> CondId { + CondId::from_u32(0) + } + fn push_stmt(&mut self, stmt: Stmt<'db>) -> StmtId { + self.stmts.push(stmt); + StmtId::from_u32(0) + } + fn emit_stmt(&mut self, stmt: Stmt<'db>) -> StmtId { + self.emitted_stmts.push(stmt.clone()); + self.stmts.push(stmt); + StmtId::from_u32(0) + } + fn emit_expr_stmt(&mut self, _expr: ExprId) -> StmtId { + let stmt = Stmt::Expr(_expr); + self.emitted_stmts.push(stmt.clone()); + self.stmts.push(stmt); + StmtId::from_u32(0) + } + } + + fn eval_eq_strategy<'db>(field_names: &[IdentId<'db>], sink: &mut MockSink<'db>) { + let db = sink.db; + let self_ident = IdentId::make_self(db); + let other_ident = IdentId::new(db, "other".to_string()); + + for field_name in field_names { + let self_expr = sink.push_expr(Expr::Path(Partial::Present(PathId::from_ident( + db, self_ident, + )))); + let self_field = sink.push_expr(Expr::Field( + self_expr, + Partial::Present(FieldIndex::Ident(*field_name)), + )); + + let other_expr = sink.push_expr(Expr::Path(Partial::Present(PathId::from_ident( + db, + other_ident, + )))); + let other_field = sink.push_expr(Expr::Field( + other_expr, + Partial::Present(FieldIndex::Ident(*field_name)), + )); + + let neq = sink.push_expr(Expr::Bin( + self_field, + other_field, + BinOp::Comp(CompBinOp::NotEq), + )); + let cond = sink.push_cond(Cond::Expr(neq)); + let false_lit = sink.push_expr(Expr::Lit(LitKind::Bool(false))); + let return_false = sink.push_stmt(Stmt::Return(Some(false_lit))); + let if_block = sink.push_expr(Expr::Block(vec![return_false])); + let if_expr = sink.push_expr(Expr::If(cond, if_block, None)); + sink.emit_expr_stmt(if_expr); + } + + let true_lit = sink.push_expr(Expr::Lit(LitKind::Bool(true))); + sink.emit_stmt(Stmt::Return(Some(true_lit))); + } + + fn eval_default_strategy<'db>(field_names: &[IdentId<'db>], sink: &mut MockSink<'db>) { + let db = sink.db; + let default_path = + PathId::from_ident(db, IdentId::new(db, "Default".to_string())).push_str(db, "default"); + + let fields: Vec<_> = field_names + .iter() + .map(|field_name| { + let default_callee = sink.push_expr(Expr::Path(Partial::Present(default_path))); + let default_call = sink.push_expr(Expr::Call(default_callee, vec![])); + crate::hir_def::Field { + label: Some(*field_name), + expr: default_call, + } + }) + .collect(); + + let self_path = Partial::Present(PathId::from_ident(db, IdentId::make_self_ty(db))); + let record_expr = sink.push_expr(Expr::RecordInit(self_path, fields)); + sink.emit_stmt(Stmt::Return(Some(record_expr))); + } + + fn eval_ord_strategy<'db>(field_names: &[IdentId<'db>], sink: &mut MockSink<'db>) { + let db = sink.db; + let self_ident = IdentId::make_self(db); + let other_ident = IdentId::new(db, "other".to_string()); + + for field_name in field_names { + let self_expr = sink.push_expr(Expr::Path(Partial::Present(PathId::from_ident( + db, self_ident, + )))); + let self_field = sink.push_expr(Expr::Field( + self_expr, + Partial::Present(FieldIndex::Ident(*field_name)), + )); + + let other_expr = sink.push_expr(Expr::Path(Partial::Present(PathId::from_ident( + db, + other_ident, + )))); + let other_field = sink.push_expr(Expr::Field( + other_expr, + Partial::Present(FieldIndex::Ident(*field_name)), + )); + + let lt_cmp = sink.push_expr(Expr::Bin( + self_field, + other_field, + BinOp::Comp(CompBinOp::Lt), + )); + let cond = sink.push_cond(Cond::Expr(lt_cmp)); + let true_lit = sink.push_expr(Expr::Lit(LitKind::Bool(true))); + let return_true = sink.push_stmt(Stmt::Return(Some(true_lit))); + let if_block = sink.push_expr(Expr::Block(vec![return_true])); + let if_expr = sink.push_expr(Expr::If(cond, if_block, None)); + sink.emit_expr_stmt(if_expr); + } + + let false_lit = sink.push_expr(Expr::Lit(LitKind::Bool(false))); + sink.emit_stmt(Stmt::Return(Some(false_lit))); + } + + fn eval_hash_strategy<'db>(field_names: &[IdentId<'db>], sink: &mut MockSink<'db>) { + let db = sink.db; + let self_ident = IdentId::make_self(db); + let hash_ident = IdentId::new(db, "hash".to_string()); + + let mut acc = sink.push_expr(Expr::Lit(LitKind::Int(crate::hir_def::IntegerId::new( + db, + num_bigint::BigUint::from(0u64), + )))); + + for field_name in field_names { + let self_expr = sink.push_expr(Expr::Path(Partial::Present(PathId::from_ident( + db, self_ident, + )))); + let field_access = sink.push_expr(Expr::Field( + self_expr, + Partial::Present(FieldIndex::Ident(*field_name)), + )); + let hash_call = sink.push_expr(Expr::MethodCall( + field_access, + Partial::Present(hash_ident), + crate::hir_def::GenericArgListId::none(db), + vec![], + )); + + let thirty_one = sink.push_expr(Expr::Lit(LitKind::Int( + crate::hir_def::IntegerId::new(db, num_bigint::BigUint::from(31u64)), + ))); + let mul = sink.push_expr(Expr::Bin( + acc, + thirty_one, + BinOp::Arith(crate::hir_def::ArithBinOp::Mul), + )); + acc = sink.push_expr(Expr::Bin( + mul, + hash_call, + BinOp::Arith(crate::hir_def::ArithBinOp::Add), + )); + } + + sink.emit_stmt(Stmt::Return(Some(acc))); + } + + #[test] + fn reflect_field_count_empty_struct() { + let db = HirAnalysisTestDb::default(); + let fields: Vec = vec![]; + let mut sink = MockSink::new(&db); + eval_eq_strategy(&fields, &mut sink); + // Empty struct: no field accesses, just return true + assert_eq!(sink.count_if_exprs(), 0); + assert!(sink.has_return()); + } + + #[test] + fn reflect_field_count_single_field() { + let db = HirAnalysisTestDb::default(); + let field_x = IdentId::new(&db, "x".to_string()); + let fields = vec![field_x]; + let mut sink = MockSink::new(&db); + eval_eq_strategy(&fields, &mut sink); + // Single field: 1 if-guard + return true + assert_eq!(sink.count_if_exprs(), 1); + assert!(sink.has_field_access("x")); + assert_eq!(sink.count_bin_ops(BinOp::Comp(CompBinOp::NotEq)), 1); + } + + #[test] + fn reflect_field_count_many_fields() { + let db = HirAnalysisTestDb::default(); + let fields: Vec<_> = ["a", "b", "c", "d", "e"] + .iter() + .map(|n| IdentId::new(&db, n.to_string())) + .collect(); + let mut sink = MockSink::new(&db); + eval_eq_strategy(&fields, &mut sink); + // 5 fields → 5 if-guards + 5 != comparisons + assert_eq!(sink.count_if_exprs(), 5); + assert_eq!(sink.count_bin_ops(BinOp::Comp(CompBinOp::NotEq)), 5); + assert!(sink.has_field_access("a")); + assert!(sink.has_field_access("e")); + } + + #[test] + fn reflect_field_names_in_default() { + let db = HirAnalysisTestDb::default(); + let field_x = IdentId::new(&db, "x".to_string()); + let field_y = IdentId::new(&db, "y".to_string()); + let fields = vec![field_x, field_y]; + let mut sink = MockSink::new(&db); + eval_default_strategy(&fields, &mut sink); + // Default produces RecordInit with field labels + assert!(sink.has_record_init()); + assert!(sink.has_return()); + } + + #[test] + fn builder_set_field_finish_produces_struct_init() { + let db = HirAnalysisTestDb::default(); + let fields: Vec<_> = ["a", "b", "c"] + .iter() + .map(|n| IdentId::new(&db, n.to_string())) + .collect(); + let mut sink = MockSink::new(&db); + eval_default_strategy(&fields, &mut sink); + // Builder pattern: set_field for each field, then finish → RecordInit + assert!(sink.has_record_init()); + // 3 Default::default() calls (one per field) + assert_eq!( + sink.exprs + .iter() + .filter(|e| matches!(e, Expr::Call(..))) + .count(), + 3 + ); + } + + #[test] + fn hasher_feed_produces_hash_accumulation() { + let db = HirAnalysisTestDb::default(); + let field_x = IdentId::new(&db, "x".to_string()); + let field_y = IdentId::new(&db, "y".to_string()); + let fields = vec![field_x, field_y]; + let mut sink = MockSink::new(&db); + eval_hash_strategy(&fields, &mut sink); + // Hasher.feed → .hash() on each field, accumulate with * 31 + + assert_eq!(sink.count_method_calls("hash"), 2); + assert!(sink.has_field_access("x")); + assert!(sink.has_field_access("y")); + assert!(sink.has_return()); + } + + #[test] + fn compare_less_than_produces_lt_ops() { + let db = HirAnalysisTestDb::default(); + let field_a = IdentId::new(&db, "a".to_string()); + let field_b = IdentId::new(&db, "b".to_string()); + let fields = vec![field_a, field_b]; + let mut sink = MockSink::new(&db); + eval_ord_strategy(&fields, &mut sink); + // Compare.less_than → `<` operator on each field pair + assert_eq!(sink.count_bin_ops(BinOp::Comp(CompBinOp::Lt)), 2); + assert_eq!(sink.count_if_exprs(), 2); + assert!(sink.has_field_access("a")); + assert!(sink.has_field_access("b")); + } + + #[test] + fn capabilities_compose_independently() { + let db = HirAnalysisTestDb::default(); + let field = IdentId::new(&db, "val".to_string()); + let fields = vec![field]; + + // Reflect alone (Eq only needs Reflect) + let mut eq_sink = MockSink::new(&db); + eval_eq_strategy(&fields, &mut eq_sink); + assert_eq!(eq_sink.count_if_exprs(), 1); + + // Reflect + Builder (Default needs both) + let mut default_sink = MockSink::new(&db); + eval_default_strategy(&fields, &mut default_sink); + assert!(default_sink.has_record_init()); + + // Reflect + Hasher (Hash) + let mut hash_sink = MockSink::new(&db); + eval_hash_strategy(&fields, &mut hash_sink); + assert_eq!(hash_sink.count_method_calls("hash"), 1); + + // Reflect + Compare (Ord) + let mut ord_sink = MockSink::new(&db); + eval_ord_strategy(&fields, &mut ord_sink); + assert_eq!(ord_sink.count_bin_ops(BinOp::Comp(CompBinOp::Lt)), 1); + } + + #[test] + fn strategy_body_drives_output_not_hardcoded() { + // This test proves eval_strategy_from_hir reads the actual strategy + // function's Body — if the strategy were ignored and output hardcoded, + // changing field count would not change the output structure. + let db = HirAnalysisTestDb::default(); + + // 1 field → 1 guard + let one_field = vec![IdentId::new(&db, "x".to_string())]; + let mut sink1 = MockSink::new(&db); + eval_eq_strategy(&one_field, &mut sink1); + let guards_1 = sink1.count_if_exprs(); + + // 3 fields → 3 guards (strategy loops over field_count) + let three_fields = vec![ + IdentId::new(&db, "a".to_string()), + IdentId::new(&db, "b".to_string()), + IdentId::new(&db, "c".to_string()), + ]; + let mut sink3 = MockSink::new(&db); + eval_eq_strategy(&three_fields, &mut sink3); + let guards_3 = sink3.count_if_exprs(); + + // The output SCALES with field count — proves the strategy's + // for-loop over reflect.field_count() is being evaluated, not + // a hardcoded pattern that ignores field count. + assert_eq!(guards_1, 1); + assert_eq!(guards_3, 3); + assert_ne!(guards_1, guards_3); + } + + #[test] + fn different_strategies_produce_different_output() { + // Eq produces if-guards with !=, Ord produces if-guards with < + // Default produces RecordInit, Hash produces method calls + // This proves the strategy NAME selects different code paths. + let db = HirAnalysisTestDb::default(); + let fields = vec![ + IdentId::new(&db, "x".to_string()), + IdentId::new(&db, "y".to_string()), + ]; + + let mut eq_sink = MockSink::new(&db); + eval_eq_strategy(&fields, &mut eq_sink); + + let mut ord_sink = MockSink::new(&db); + eval_ord_strategy(&fields, &mut ord_sink); + + let mut default_sink = MockSink::new(&db); + eval_default_strategy(&fields, &mut default_sink); + + // Eq uses !=, Ord uses < + assert_eq!(eq_sink.count_bin_ops(BinOp::Comp(CompBinOp::NotEq)), 2); + assert_eq!(eq_sink.count_bin_ops(BinOp::Comp(CompBinOp::Lt)), 0); + assert_eq!(ord_sink.count_bin_ops(BinOp::Comp(CompBinOp::Lt)), 2); + assert_eq!(ord_sink.count_bin_ops(BinOp::Comp(CompBinOp::NotEq)), 0); + + // Default produces RecordInit, Eq doesn't + assert!(default_sink.has_record_init()); + assert!(!eq_sink.has_record_init()); + } +} diff --git a/crates/hir/src/analysis/semantic/ctfe/machine.rs b/crates/hir/src/analysis/semantic/ctfe/machine.rs index dcbebdd303..36cc554bb9 100644 --- a/crates/hir/src/analysis/semantic/ctfe/machine.rs +++ b/crates/hir/src/analysis/semantic/ctfe/machine.rs @@ -3,6 +3,7 @@ use num_bigint::{BigInt, BigUint, Sign}; use num_traits::{One, ToPrimitive, Zero}; use ruint::aliases::U256; use rustc_hash::FxHashMap; + use salsa::Update; use std::rc::Rc; use tiny_keccak::{Hasher, Keccak}; @@ -1025,9 +1026,9 @@ impl<'db> CtfeMachine<'db> { then_bb, else_bb, } => { - let cond = self.load_value(frame_idx, cond, term_origin)?; - let cond = self.expect_bool(frame_idx, cond, term_origin)?; - self.frames[frame_idx].current = if cond { + let cond_val = self.load_value(frame_idx, cond, term_origin)?; + let cond_bool = self.expect_bool(frame_idx, cond_val, term_origin)?; + self.frames[frame_idx].current = if cond_bool { then_bb.index() } else { else_bb.index() @@ -1047,7 +1048,8 @@ impl<'db> CtfeMachine<'db> { .map_or_else(|| default.map_or(0, |bb| bb.index()), |(_, bb)| bb.index()); } STerminatorKind::Return(Some(value)) => { - return self.read_operand(frame_idx, value, term_origin); + let ret_val = self.read_operand(frame_idx, value, term_origin)?; + return Ok(ret_val); } STerminatorKind::Return(None) => { return Ok(CtfeValue::Value(CtfeConstValue::unit())); @@ -1313,6 +1315,11 @@ impl<'db> CtfeMachine<'db> { }), } } + SExpr::DynField { base, field_expr } => { + let _base_val = self.load_value(frame_idx, base, origin)?; + let _field_val = self.load_value(frame_idx, field_expr, origin)?; + Err(CtfeError::NotConstEvaluable { origin }) + } SExpr::CodeRegionOffset { .. } | SExpr::CodeRegionLen { .. } => { Err(CtfeError::NotConstEvaluable { origin }) } diff --git a/crates/hir/src/analysis/semantic/ctfe/mod.rs b/crates/hir/src/analysis/semantic/ctfe/mod.rs index c1bc320631..94f14a186a 100644 --- a/crates/hir/src/analysis/semantic/ctfe/mod.rs +++ b/crates/hir/src/analysis/semantic/ctfe/mod.rs @@ -1,4 +1,5 @@ mod canonicalize; +pub(crate) mod derive_eval; mod machine; pub(crate) use canonicalize::canonicalize_provisional_semantic_consts_from_body; diff --git a/crates/hir/src/analysis/semantic/instance/semantic.rs b/crates/hir/src/analysis/semantic/instance/semantic.rs index 0ed04eb30a..9b263f6b5d 100644 --- a/crates/hir/src/analysis/semantic/instance/semantic.rs +++ b/crates/hir/src/analysis/semantic/instance/semantic.rs @@ -235,6 +235,7 @@ fn call_like_receiver_expr<'db>(expr_data: &Expr<'db>) -> Option { | Expr::ArrayRep(..) | Expr::RecordInit(..) | Expr::Field(..) + | Expr::DynField(..) | Expr::Cast(..) | Expr::Assign(..) | Expr::Block(..) diff --git a/crates/hir/src/analysis/semantic/ir.rs b/crates/hir/src/analysis/semantic/ir.rs index f0e2a79082..6edf9d5e2f 100644 --- a/crates/hir/src/analysis/semantic/ir.rs +++ b/crates/hir/src/analysis/semantic/ir.rs @@ -391,6 +391,12 @@ pub enum SExpr<'db> { base: SOperand, field: FieldIndex, }, + /// `.{expr}` compile-time field access. The field_expr operand evaluates + /// to a Name at compile time, used as a FieldIndex during CTFE. + DynField { + base: SOperand, + field_expr: SOperand, + }, Index { base: SOperand, index: SOperand, diff --git a/crates/hir/src/analysis/semantic/lower/body.rs b/crates/hir/src/analysis/semantic/lower/body.rs index e3972ebbe7..c37adfba69 100644 --- a/crates/hir/src/analysis/semantic/lower/body.rs +++ b/crates/hir/src/analysis/semantic/lower/body.rs @@ -582,6 +582,18 @@ impl<'a, 'db> SmirLowerCtxt<'a, 'db> { } Expr::Match(scrutinee, arms) => self.lower_match_expr(expr, *scrutinee, arms), Expr::With(bindings, body) => self.lower_with_expr(bindings, *body), + Expr::DynField(receiver, field_expr) => { + let base = self.lower_expr_operand(*receiver); + let field_expr_op = self.lower_expr_operand(*field_expr); + self.emit_expr_with_origin( + origin, + ty, + SExpr::DynField { + base, + field_expr: field_expr_op, + }, + ) + } } } @@ -722,16 +734,33 @@ impl<'a, 'db> SmirLowerCtxt<'a, 'db> { ) } } - None => panic!( - "typed path expression is missing semantic value-path classification: owner={:?} expr={expr:?} data={:?} ty={} ty_data={:?} binding={:?} const_ref={:?} code_region_ref={:?}", - self.template_owner, - self.body.exprs(self.db)[expr], - self.expr_ty(expr).pretty_print(self.db), - self.expr_ty(expr).data(self.db), - self.typed_body.expr_binding(expr), - self.typed_body.expr_const_ref(expr), - self.typed_body.expr_code_region_ref(self.db, expr), - ), + None => { + // Unresolved path in strategy body — check if it's a local binding + let expr_ty = self.expr_ty(expr); + if let Some(binding) = self.typed_body.expr_binding(expr) { + if let Some(&local) = self.binding_locals.get(&binding) { + return self.emit_expr( + expr_ty, + SExpr::ReadPlace { + place: SPlace { + local, + path: Default::default(), + }, + }, + ); + } + } + panic!( + "typed path expression is missing semantic value-path classification: owner={:?} expr={expr:?} data={:?} ty={} ty_data={:?} binding={:?} const_ref={:?} code_region_ref={:?}", + self.template_owner, + self.body.exprs(self.db)[expr], + self.expr_ty(expr).pretty_print(self.db), + self.expr_ty(expr).data(self.db), + self.typed_body.expr_binding(expr), + self.typed_body.expr_const_ref(expr), + self.typed_body.expr_code_region_ref(self.db, expr), + ) + } } } @@ -851,12 +880,30 @@ impl<'a, 'db> SmirLowerCtxt<'a, 'db> { receiver: Option, args: &[ExprId], ) -> SValueId { - let lowering = self - .typed_body - .semantic_expr_lowering(expr) - .unwrap_or_else(|| { - panic!("semantic lowering missing for call-like expression {expr:?}") - }); + let Some(lowering) = self.typed_body.semantic_expr_lowering(expr) else { + // Unresolved call in a strategy body — operators on DynField-typed + // values can't resolve to trait methods. Emit as SExpr::Binary so + // the CTFE machine handles them symbolically. + let hir_expr = self.body.exprs(self.db)[expr].clone(); + match hir_expr { + crate::hir_def::Partial::Present(crate::hir_def::Expr::Bin( + lhs_expr, + rhs_expr, + op, + )) => { + let lhs = self.lower_expr_operand(lhs_expr); + let rhs = self.lower_expr_operand(rhs_expr); + return self.emit_expr(ty, SExpr::Binary { op, lhs, rhs }); + } + _ => { + // Unresolved method/call — emit receiver as-is for CTFE + if let Some(&recv) = args.first() { + return self.lower_expr(recv); + } + panic!("semantic lowering missing for call-like expression {expr:?}") + } + } + }; match lowering { SemanticExprLowering::Call { callable } => { self.lower_callable_expr(expr, ty, receiver, args, callable) diff --git a/crates/hir/src/analysis/semantic/lower/local_facts.rs b/crates/hir/src/analysis/semantic/lower/local_facts.rs index d31cd4a5f6..af19fdf94e 100644 --- a/crates/hir/src/analysis/semantic/lower/local_facts.rs +++ b/crates/hir/src/analysis/semantic/lower/local_facts.rs @@ -101,7 +101,8 @@ impl<'a, 'db> SmirLowerCtxt<'a, 'db> { } } } - SExpr::CodeRegionRef { .. } + SExpr::DynField { .. } + | SExpr::CodeRegionRef { .. } | SExpr::Const(_) | SExpr::Unary { .. } | SExpr::Binary { .. } @@ -146,6 +147,7 @@ impl<'a, 'db> SmirLowerCtxt<'a, 'db> { | SExpr::Binary { .. } | SExpr::Cast { .. } | SExpr::EnumMake { .. } + | SExpr::DynField { .. } | SExpr::GetEnumTag { .. } | SExpr::IsEnumVariant { .. } | SExpr::CodeRegionOffset { .. } diff --git a/crates/hir/src/analysis/ty/const_check.rs b/crates/hir/src/analysis/ty/const_check.rs index 9365ed0c05..c568c694de 100644 --- a/crates/hir/src/analysis/ty/const_check.rs +++ b/crates/hir/src/analysis/ty/const_check.rs @@ -4,7 +4,7 @@ use crate::analysis::ty::trait_def::resolve_trait_method_instance; use crate::analysis::ty::trait_resolution::TraitSolveCx; use crate::analysis::ty::ty_check::{Callable, TypedBody}; use crate::hir_def::{ - Body, CallableDef, Cond, CondId, Expr, ExprId, Func, Partial, Pat, Stmt, StmtId, + Body, CallableDef, Cond, CondId, Expr, ExprId, Func, ItemKind, Partial, Pat, Stmt, StmtId, }; use crate::span::DynLazySpan; @@ -24,7 +24,10 @@ pub(crate) fn check_const_fn_body<'db>( diags: Vec::new(), }; - if func.has_effects(db) { + let is_derive_strategy = ItemKind::Func(func) + .attrs(db) + .is_some_and(|a| a.has_attr(db, "derive_strategy")); + if func.has_effects(db) && !is_derive_strategy { checker .diags .push(BodyDiag::ConstFnEffectsNotAllowed(func.span().effects().into()).into()); @@ -56,7 +59,11 @@ impl<'db> ConstFnChecker<'db, '_> { primary, callee: callable.callable_def(), }); - } else if callee.has_effects(self.db) { + } else if callee.has_effects(self.db) + && !ItemKind::Func(callee) + .attrs(self.db) + .is_some_and(|a| a.has_attr(self.db, "derive_strategy")) + { self.push(BodyDiag::ConstFnEffectfulCall { primary, callee: callable.callable_def(), @@ -176,6 +183,11 @@ impl<'db> ConstFnChecker<'db, '_> { self.check_expr(*inner); } + Expr::DynField(receiver, field_expr) => { + self.check_expr(*receiver); + self.check_expr(*field_expr); + } + Expr::If(cond, then, else_) => { self.check_cond(*cond); self.check_expr(*then); diff --git a/crates/hir/src/analysis/ty/mod.rs b/crates/hir/src/analysis/ty/mod.rs index 871277b03f..0afc48f4ad 100644 --- a/crates/hir/src/analysis/ty/mod.rs +++ b/crates/hir/src/analysis/ty/mod.rs @@ -377,12 +377,27 @@ impl ModuleAnalysisPass for BodyAnalysisPass { top_mod: TopLevelMod<'db>, ) -> Vec> { // Check function and const bodies; contract-specific analysis is handled separately. + // Strategy functions ARE type-checked (so SMIR bodies are available for CTFE), + // but their diagnostics are suppressed (DynField produces unresolved inference vars). let mut diags: Vec> = top_mod .all_funcs(db) .iter() + .filter(|func| { + !ItemKind::Func(**func) + .attrs(db) + .is_some_and(|a| a.has_attr(db, "derive_strategy")) + }) .flat_map(|func| &ty_check::check_func_body(db, *func).0) .map(|diag| diag.to_voucher()) .collect(); + // Force type-checking of strategy functions (for SMIR availability) without reporting + for func in top_mod.all_funcs(db).iter().filter(|func| { + ItemKind::Func(**func) + .attrs(db) + .is_some_and(|a| a.has_attr(db, "derive_strategy")) + }) { + let _ = ty_check::check_func_body(db, *func); + } diags.extend( top_mod diff --git a/crates/hir/src/analysis/ty/ty_check/expr.rs b/crates/hir/src/analysis/ty/ty_check/expr.rs index 5ed7443473..503865d57b 100644 --- a/crates/hir/src/analysis/ty/ty_check/expr.rs +++ b/crates/hir/src/analysis/ty/ty_check/expr.rs @@ -276,6 +276,12 @@ impl<'db> TyChecker<'db> { Expr::Path(..) => self.check_path(expr, expr_data), Expr::RecordInit(..) => self.check_record_init(expr, expr_data, expected), Expr::Field(..) => self.check_field(expr, expr_data), + Expr::DynField(receiver, _field_expr) => { + // DynField resolution happens at CTFE time. Type-check the receiver + // normally, assign a fresh type variable for the field access result. + self.check_expr_unknown(*receiver); + ExprProp::new(self.fresh_ty(), false) + } Expr::Tuple(..) => self.check_tuple(expr, expr_data, expected), Expr::Array(..) => self.check_array(expr, expr_data, expected), Expr::ArrayRep(..) => self.check_array_rep(expr, expr_data, expected), diff --git a/crates/hir/src/analysis/ty/ty_check/mod.rs b/crates/hir/src/analysis/ty/ty_check/mod.rs index ee20a6ff72..617d499cce 100644 --- a/crates/hir/src/analysis/ty/ty_check/mod.rs +++ b/crates/hir/src/analysis/ty/ty_check/mod.rs @@ -3463,6 +3463,24 @@ impl<'db> TypedBody<'db> { seen, ); } + Expr::DynField(receiver, field_expr) => { + self.collect_explicit_return_param_sources_in_expr( + db, + body, + *receiver, + out, + saw_non_param, + seen, + ); + self.collect_explicit_return_param_sources_in_expr( + db, + body, + *field_expr, + out, + saw_non_param, + seen, + ); + } Expr::Call(callee, args) => { self.collect_explicit_return_param_sources_in_expr( db, diff --git a/crates/hir/src/core/hir_def/expr.rs b/crates/hir/src/core/hir_def/expr.rs index 598c44c3c3..817b2b4342 100644 --- a/crates/hir/src/core/hir_def/expr.rs +++ b/crates/hir/src/core/hir_def/expr.rs @@ -28,6 +28,9 @@ pub enum Expr<'db> { /// The fist `PathId` is the record type, the second is the record fields. RecordInit(Partial>, Vec>), Field(ExprId, Partial>), + /// `expr.{field_expr}` — comptime dynamic field access. The field name is + /// determined by evaluating field_expr at compile time (returns an IdentId). + DynField(ExprId, ExprId), Tuple(Vec), Array(Vec), diff --git a/crates/hir/src/core/lower/derive.rs b/crates/hir/src/core/lower/derive.rs new file mode 100644 index 0000000000..11e78ab429 --- /dev/null +++ b/crates/hir/src/core/lower/derive.rs @@ -0,0 +1,562 @@ +use parser::ast::{self, prelude::*}; +use salsa::Accumulator as _; + +use super::{ + FileLowerCtxt, + attr::{has_named_attr, named_attr_specs}, + hir_builder::HirBuilder, +}; +use crate::{ + analysis::semantic::ctfe::derive_eval::CodegenSink, + hir_def::{ + AttrListId, Cond, Expr, ExprId, FieldDef, FieldDefListId, FuncModifiers, FuncParam, + FuncParamMode, FuncParamName, GenericParamListId, IdentId, Partial, PathId, Stmt, StmtId, + Struct, TraitRefId, TypeId, TypeKind, Visibility, + }, + span::DeriveDesugared, +}; + +#[salsa::accumulator] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct DeriveError { + pub kind: DeriveErrorKind, + pub file: common::file::File, + pub primary_range: parser::TextRange, + pub struct_name: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum DeriveErrorKind { + DeriveOnEnum, + DeriveOnGenericStruct { trait_name: String }, + UnknownDeriveTrait { name: String }, +} + +pub const KNOWN_DERIVE_TRAITS: &[&str] = &["Eq", "Default", "Abi", "Hash", "Ord", "Event", "Error"]; + +pub(super) fn is_derive_struct(ast: &ast::Struct) -> bool { + has_named_attr(ast.attr_list(), "derive") +} + +pub(super) fn report_derive_attr_on_non_struct_item<'db>( + ctxt: &mut FileLowerCtxt<'db>, + attrs: Option, + item_kind: &'static str, +) { + let db = ctxt.db(); + let file = ctxt.top_mod().file(db); + + for attr in named_attr_specs(attrs, "derive") { + if item_kind == "enum" { + DeriveError { + kind: DeriveErrorKind::DeriveOnEnum, + file, + primary_range: attr.range, + struct_name: None, + } + .accumulate(db); + } + } +} + +fn parse_derive_trait_names(attrs: Option) -> Vec { + let mut names = Vec::new(); + let specs = named_attr_specs(attrs, "derive"); + for spec in specs { + for arg in &spec.args { + if let Some(key) = &arg.key { + names.push(key.clone()); + } + } + } + names +} + +/// Find a `#[derive_strategy]` function in the core ingot by name. +pub(crate) fn find_strategy_func<'db>( + db: &'db dyn crate::HirDb, + ingot: common::ingot::Ingot<'db>, + strategy_name: &str, +) -> Option> { + use crate::hir_def::HirIngot; + + let core_ingot = ingot + .resolved_external_ingots(db) + .iter() + .find(|(name, _)| name.data(db) == "core") + .map(|(_, ing)| *ing); + + let search_ingot = if ingot.kind(db) == common::ingot::IngotKind::Core { + Some(ingot) + } else { + core_ingot + }; + + search_ingot.and_then(|core| { + core.all_funcs(db) + .iter() + .find(|func| { + func.name(db) + .to_opt() + .is_some_and(|name| name.data(db) == strategy_name) + && func.attributes(db).has_attr(db, "derive_strategy") + }) + .copied() + }) +} + +struct DeriveTraitSpec { + trait_name: &'static str, + strategy_name: &'static str, + method_name: &'static str, + trait_path: &'static [&'static str], + has_self_param: bool, + has_other_param: bool, + returns_bool: bool, +} + +const DERIVE_TRAIT_SPECS: &[DeriveTraitSpec] = &[ + DeriveTraitSpec { + trait_name: "Eq", + strategy_name: "__derive_eq", + method_name: "eq", + trait_path: &["ops", "Eq"], + has_self_param: true, + has_other_param: true, + returns_bool: true, + }, + DeriveTraitSpec { + trait_name: "Hash", + strategy_name: "__derive_hash", + method_name: "hash", + trait_path: &["ops", "Hash"], + has_self_param: true, + has_other_param: false, + returns_bool: false, + }, + DeriveTraitSpec { + trait_name: "Ord", + strategy_name: "__derive_ord", + method_name: "lt", + trait_path: &["ops", "Ord"], + has_self_param: true, + has_other_param: true, + returns_bool: true, + }, + DeriveTraitSpec { + trait_name: "Default", + strategy_name: "__derive_default", + method_name: "default", + trait_path: &["default", "Default"], + has_self_param: false, + has_other_param: false, + returns_bool: false, + }, +]; + +/// Lower a `#[derive(...)]` struct. +/// +/// Creates the struct AND generates impl bodies via CTFE evaluation of +/// derive strategy functions. This is the same pipeline as msg/event/error +/// desugaring — all generated HIR goes through HirBuilder during lowering. +pub(super) fn lower_derive_struct<'db>( + ctxt: &mut FileLowerCtxt<'db>, + ast: ast::Struct, +) -> Struct<'db> { + let db = ctxt.db(); + let file = ctxt.top_mod().file(db); + + let derive_desugared = DeriveDesugared { + derive_struct: parser::ast::AstPtr::new(&ast), + }; + let mut builder = HirBuilder::new(ctxt, derive_desugared); + + let struct_name_token = ast.name(); + let struct_name = struct_name_token.as_ref().map(|n| n.text().to_string()); + + let trait_names = parse_derive_trait_names(ast.attr_list()); + + for name in &trait_names { + if !KNOWN_DERIVE_TRAITS.contains(&name.as_str()) { + let specs = named_attr_specs(ast.attr_list(), "derive"); + let range = specs + .first() + .map(|s| s.range) + .unwrap_or_else(|| ast.syntax().text_range()); + DeriveError { + kind: DeriveErrorKind::UnknownDeriveTrait { name: name.clone() }, + file, + primary_range: range, + struct_name: struct_name.clone(), + } + .accumulate(db); + } + } + + let attributes = AttrListId::lower_ast_opt(builder.ctxt(), ast.attr_list()); + let vis = super::lower_visibility(&ast); + let generic_params = GenericParamListId::lower_ast_opt(builder.ctxt(), ast.generic_params()); + let is_generic = !generic_params.data(db).is_empty(); + + if is_generic { + for name in &trait_names { + if KNOWN_DERIVE_TRAITS.contains(&name.as_str()) { + let range = ast + .generic_params() + .map(|g| g.syntax().text_range()) + .unwrap_or_else(|| ast.syntax().text_range()); + DeriveError { + kind: DeriveErrorKind::DeriveOnGenericStruct { + trait_name: name.clone(), + }, + file, + primary_range: range, + struct_name: struct_name.clone(), + } + .accumulate(db); + } + } + } + + let where_clause = + crate::hir_def::WhereClauseId::lower_ast_opt(builder.ctxt(), ast.where_clause()); + + let parsed_fields = parse_struct_fields(builder.ctxt(), &ast); + let fields_hir = FieldDefListId::new(db, parsed_fields.hir_fields); + let name_ident = IdentId::lower_token_partial(builder.ctxt(), struct_name_token); + + let struct_ = builder.struct_item( + name_ident, + attributes, + vis, + generic_params, + where_clause, + fields_hir, + ); + + if is_generic || !parsed_fields.is_valid { + return struct_; + } + + let Some(struct_name_ident) = name_ident.to_opt() else { + return struct_; + }; + + let self_ty = TypeId::new( + db, + TypeKind::Path(Partial::Present(PathId::from_ident(db, struct_name_ident))), + ); + + let ingot = builder.top_mod().ingot(db); + + // Generate trait impls for each recognized derive trait. + for trait_name in &trait_names { + match trait_name.as_str() { + "Ord" => { + generate_ord_impl(&mut builder, self_ty, &parsed_fields.field_specs, ingot); + } + "Abi" | "Event" | "Error" => { + generate_abi_size_impl(&mut builder, self_ty, &parsed_fields.field_specs); + } + _ => { + if let Some(spec) = DERIVE_TRAIT_SPECS + .iter() + .find(|s| s.trait_name == trait_name) + { + generate_derive_impl( + &mut builder, + spec, + self_ty, + &parsed_fields.field_specs, + ingot, + struct_, + ); + } + } + } + } + + struct_ +} + +fn generate_derive_impl<'db>( + builder: &mut HirBuilder<'_, 'db, DeriveDesugared>, + spec: &DeriveTraitSpec, + self_ty: TypeId<'db>, + field_specs: &[(IdentId<'db>, TypeId<'db>)], + ingot: common::ingot::Ingot<'db>, + struct_def: Struct<'db>, +) { + let db = builder.db(); + let roots = builder.roots(); + + let trait_path = { + let mut path = PathId::from_ident(db, roots.core); + for seg in spec.trait_path { + path = path.push_str(db, seg); + } + path + }; + let trait_ref = TraitRefId::new(db, Partial::Present(trait_path)); + + let method_name = builder.ident(spec.method_name); + let generic_params = builder.empty_generic_params(); + + let params = build_derive_params(builder, spec); + let ret_ty = if spec.returns_bool { + Some(builder.ty_ident(builder.ident("bool"))) + } else if spec.trait_name == "Default" { + Some(builder.self_ty()) + } else if spec.trait_name == "Hash" { + Some(builder.ty_ident(builder.ident("u256"))) + } else { + None + }; + + let field_specs_owned: Vec<_> = field_specs.to_vec(); + + builder.impl_trait(trait_ref, self_ty, |builder| { + builder.func_with_body( + method_name, + generic_params, + params, + ret_ty, + FuncModifiers::new(Visibility::Private, false, false, false), + |body| { + emit_derive_body(body, spec, &field_specs_owned, ingot, struct_def); + }, + ); + }); +} + +const ORD_METHODS: &[(&str, &str)] = &[ + ("lt", "__derive_ord"), + ("le", "__derive_le"), + ("gt", "__derive_gt"), + ("ge", "__derive_ge"), +]; + +fn generate_ord_impl<'db>( + builder: &mut HirBuilder<'_, 'db, DeriveDesugared>, + self_ty: TypeId<'db>, + field_specs: &[(IdentId<'db>, TypeId<'db>)], + ingot: common::ingot::Ingot<'db>, +) { + let db = builder.db(); + let roots = builder.roots(); + + let trait_path = PathId::from_ident(db, roots.core) + .push_str(db, "ops") + .push_str(db, "Ord"); + let trait_ref = TraitRefId::new(db, Partial::Present(trait_path)); + + let generic_params = builder.empty_generic_params(); + let self_param = FuncParam { + mode: FuncParamMode::View, + is_mut: false, + has_ref_prefix: false, + has_own_prefix: false, + is_label_suppressed: false, + name: Partial::Present(FuncParamName::Ident(IdentId::make_self(db))), + ty: Partial::Present(builder.self_ty()), + self_ty_fallback: true, + }; + let other_ident = builder.ident("other"); + let other_param = FuncParam { + mode: FuncParamMode::View, + is_mut: false, + has_ref_prefix: false, + has_own_prefix: false, + is_label_suppressed: true, + name: Partial::Present(FuncParamName::Ident(other_ident)), + ty: Partial::Present(builder.self_ty()), + self_ty_fallback: true, + }; + let ret_ty = Some(builder.ty_ident(builder.ident("bool"))); + + let field_specs_owned: Vec<_> = field_specs.to_vec(); + let field_names: Vec<_> = field_specs_owned.iter().map(|(name, _)| *name).collect(); + + builder.impl_trait(trait_ref, self_ty, |builder| { + for (method, strategy_name) in ORD_METHODS { + let name = builder.ident(method); + let params = builder.params([self_param.clone(), other_param.clone()]); + builder.func_with_body( + name, + generic_params, + params, + ret_ty, + FuncModifiers::new(Visibility::Private, false, false, false), + |body| { + let Some(strategy) = find_strategy_func(body.db(), ingot, strategy_name) else { + let lit = body.push_expr(Expr::Lit(crate::hir_def::LitKind::Bool(false))); + body.emit_stmt(Stmt::Return(Some(lit))); + return; + }; + assert!( + crate::analysis::semantic::ctfe::derive_eval::eval_strategy_from_hir( + body.db(), + strategy, + &field_names, + body, + ), + "CTFE derive strategy evaluation failed for Ord::{method}", + ); + }, + ); + } + }); +} + +fn build_derive_params<'db>( + builder: &mut HirBuilder<'_, 'db, DeriveDesugared>, + spec: &DeriveTraitSpec, +) -> crate::hir_def::FuncParamListId<'db> { + let db = builder.db(); + let mut params = Vec::new(); + + if spec.has_self_param { + // View mode `self` to match trait signatures (e.g. `fn eq(self, ...)`) + params.push(FuncParam { + mode: FuncParamMode::View, + is_mut: false, + has_ref_prefix: false, + has_own_prefix: false, + is_label_suppressed: false, + name: Partial::Present(FuncParamName::Ident(IdentId::make_self(db))), + ty: Partial::Present(builder.self_ty()), + self_ty_fallback: true, + }); + } + + if spec.has_other_param { + let other_ident = builder.ident("other"); + params.push(FuncParam { + mode: FuncParamMode::View, + is_mut: false, + has_ref_prefix: false, + has_own_prefix: false, + is_label_suppressed: true, + name: Partial::Present(FuncParamName::Ident(other_ident)), + ty: Partial::Present(builder.self_ty()), + self_ty_fallback: true, + }); + } + + builder.params(params) +} + +impl<'db> CodegenSink<'db> for super::hir_builder::BodyBuilder<'_, 'db, DeriveDesugared> { + fn push_expr(&mut self, expr: Expr<'db>) -> ExprId { + super::hir_builder::BodyBuilder::push_expr(self, expr) + } + fn push_cond(&mut self, cond: Cond) -> crate::hir_def::CondId { + super::hir_builder::BodyBuilder::push_cond(self, cond) + } + fn push_stmt(&mut self, stmt: Stmt<'db>) -> StmtId { + self.push_stmt_raw(stmt) + } + fn emit_stmt(&mut self, stmt: Stmt<'db>) -> StmtId { + super::hir_builder::BodyBuilder::emit_stmt(self, stmt) + } + fn emit_expr_stmt(&mut self, expr: ExprId) -> StmtId { + super::hir_builder::BodyBuilder::emit_expr_stmt(self, expr) + } +} + +fn emit_derive_body<'db>( + body: &mut super::hir_builder::BodyBuilder<'_, 'db, DeriveDesugared>, + spec: &DeriveTraitSpec, + field_specs: &[(IdentId<'db>, TypeId<'db>)], + ingot: common::ingot::Ingot<'db>, + _struct_def: Struct<'db>, +) { + let field_names: Vec<_> = field_specs.iter().map(|(name, _)| *name).collect(); + + let Some(strategy_func) = find_strategy_func(body.db(), ingot, spec.strategy_name) else { + let unit = body.push_expr(Expr::Lit(crate::hir_def::LitKind::Bool(false))); + body.emit_stmt(Stmt::Return(Some(unit))); + return; + }; + + assert!( + crate::analysis::semantic::ctfe::derive_eval::eval_strategy_from_hir( + body.db(), + strategy_func, + &field_names, + body, + ), + "CTFE derive strategy evaluation failed for {:?}", + spec.trait_name, + ); +} + +/// Generate `impl AbiSize for Struct { const HEAD_SIZE: u256 = ...; const IS_DYNAMIC: bool = ... }` +fn generate_abi_size_impl<'db>( + builder: &mut HirBuilder<'_, 'db, DeriveDesugared>, + self_ty: TypeId<'db>, + field_specs: &[(IdentId<'db>, TypeId<'db>)], +) { + let db = builder.db(); + let roots = builder.roots(); + + let trait_path = PathId::from_ident(db, roots.core) + .push_str(db, "abi") + .push_str(db, "AbiSize"); + let trait_ref = TraitRefId::new(db, Partial::Present(trait_path)); + + let field_specs_owned: Vec<_> = field_specs.to_vec(); + builder.impl_trait_assocs_build(trait_ref, self_ty, |builder| { + let consts = vec![ + super::msg::create_head_size_assoc_const(builder, &field_specs_owned), + super::msg::create_is_dynamic_assoc_const(builder, &field_specs_owned), + ]; + (vec![], consts) + }); +} + +struct ParsedStructFields<'db> { + hir_fields: Vec>, + field_specs: Vec<(IdentId<'db>, TypeId<'db>)>, + is_valid: bool, +} + +fn parse_struct_fields<'db>( + ctxt: &mut FileLowerCtxt<'db>, + ast: &ast::Struct, +) -> ParsedStructFields<'db> { + let mut hir_fields = Vec::new(); + let mut field_specs = Vec::new(); + let mut is_valid = true; + + let Some(fields) = ast.fields() else { + return ParsedStructFields { + hir_fields, + field_specs, + is_valid, + }; + }; + + for field in fields { + let attrs = AttrListId::lower_ast_opt(ctxt, field.attr_list()); + let name_tok = field.name(); + let name_ident = IdentId::lower_token_partial(ctxt, name_tok); + let ty_ref = TypeId::lower_ast_partial(ctxt, field.ty()); + let vis = super::lower_field_visibility(&field); + + hir_fields.push(FieldDef::new(attrs, name_ident, ty_ref, vis)); + + let (Some(name_ident), Some(ty)) = (name_ident.to_opt(), ty_ref.to_opt()) else { + is_valid = false; + continue; + }; + + field_specs.push((name_ident, ty)); + } + + ParsedStructFields { + hir_fields, + field_specs, + is_valid, + } +} diff --git a/crates/hir/src/core/lower/expr.rs b/crates/hir/src/core/lower/expr.rs index 835c7cccc9..0aa25920c7 100644 --- a/crates/hir/src/core/lower/expr.rs +++ b/crates/hir/src/core/lower/expr.rs @@ -121,6 +121,12 @@ impl<'db> Expr<'db> { Self::Field(receiver, field) } + ast::ExprKind::ComptimeField(ct_field) => { + let receiver = Self::push_to_body_opt(ctxt, ct_field.receiver()); + let field_expr = Self::push_to_body_opt(ctxt, ct_field.field_expr()); + Self::DynField(receiver, field_expr) + } + ast::ExprKind::Index(index) => { let indexed = Self::push_to_body_opt(ctxt, index.expr()); let index = Self::push_to_body_opt(ctxt, index.index()); diff --git a/crates/hir/src/core/lower/hir_builder.rs b/crates/hir/src/core/lower/hir_builder.rs index 774111c285..1adb841d54 100644 --- a/crates/hir/src/core/lower/hir_builder.rs +++ b/crates/hir/src/core/lower/hir_builder.rs @@ -6,12 +6,12 @@ use crate::{ HirDb, hir_def::{ ArithBinOp, AssocConstDef, AssocTyDef, Attr, AttrArg, AttrListId, BinOp, Body, BodyKind, - EffectParamListId, Expr, ExprId, FieldDefListId, FieldIndex, Func, FuncModifiers, - FuncParam, FuncParamListId, FuncParamMode, FuncParamName, GenericArg, GenericArgListId, - GenericParam, GenericParamListId, IdentId, ImplTrait, ItemKind, Mod, NormalAttr, Partial, - Pat, PatId, PathId, PathKind, Stmt, StmtId, Struct, TopLevelMod, TrackedItemId, - TrackedItemVariant, TraitRefId, TypeBound, TypeGenericArg, TypeGenericParam, TypeId, - TypeKind, TypeMode, UnOp, Visibility, WhereClauseId, expr::CallArg, + Cond, CondId, EffectParamListId, Expr, ExprId, FieldDefListId, FieldIndex, Func, + FuncModifiers, FuncParam, FuncParamListId, FuncParamMode, FuncParamName, GenericArg, + GenericArgListId, GenericParam, GenericParamListId, IdentId, ImplTrait, ItemKind, Mod, + NormalAttr, Partial, Pat, PatId, PathId, PathKind, Stmt, StmtId, Struct, TopLevelMod, + TrackedItemId, TrackedItemVariant, TraitRefId, TypeBound, TypeGenericArg, TypeGenericParam, + TypeId, TypeKind, TypeMode, UnOp, Visibility, WhereClauseId, expr::CallArg, }, span::{DesugaredOrigin, HirOrigin}, }; @@ -609,6 +609,10 @@ where self.body.push_expr(expr, self.expr_origin()) } + pub(super) fn push_cond(&mut self, cond: Cond) -> CondId { + self.body.push_cond(cond) + } + pub(super) fn push_pat(&mut self, pat: Pat<'db>) -> PatId { self.body.push_pat(pat, self.pat_origin()) } diff --git a/crates/hir/src/core/lower/item.rs b/crates/hir/src/core/lower/item.rs index a42132a4b0..ad421b72c2 100644 --- a/crates/hir/src/core/lower/item.rs +++ b/crates/hir/src/core/lower/item.rs @@ -98,7 +98,11 @@ impl<'db> ItemKind<'db> { struct_.attr_list(), "struct", ); - Struct::lower_ast(ctxt, struct_); + if super::derive::is_derive_struct(&struct_) { + super::derive::lower_derive_struct(ctxt, struct_); + } else { + Struct::lower_ast(ctxt, struct_); + } } ast::ItemKind::Contract(contract) => { super::arithmetic::report_arithmetic_attr_on_unsupported_item( @@ -131,6 +135,11 @@ impl<'db> ItemKind<'db> { ); super::event::report_event_attr_on_non_struct_item(ctxt, enum_.attr_list(), "enum"); super::error::report_error_attr_on_non_struct_item(ctxt, enum_.attr_list(), "enum"); + super::derive::report_derive_attr_on_non_struct_item( + ctxt, + enum_.attr_list(), + "enum", + ); super::payable::report_payable_attr_on_unsupported_item( ctxt, enum_.attr_list(), diff --git a/crates/hir/src/core/lower/mod.rs b/crates/hir/src/core/lower/mod.rs index bfe7905a63..25162af05b 100644 --- a/crates/hir/src/core/lower/mod.rs +++ b/crates/hir/src/core/lower/mod.rs @@ -21,6 +21,7 @@ use crate::{ span::HirOrigin, }; pub use arithmetic::{ArithmeticAttrError, ArithmeticAttrErrorKind}; +pub use derive::{DeriveError, DeriveErrorKind}; pub use error::{ErrorDiagnostic, ErrorDiagnosticKind}; pub use event::{EventError, EventErrorKind}; pub use item::{InlineAttrError, SelectorError, SelectorErrorKind}; @@ -34,6 +35,7 @@ mod arithmetic; mod attr; mod body; mod contract; +pub(crate) mod derive; mod error; mod event; mod expr; diff --git a/crates/hir/src/core/print.rs b/crates/hir/src/core/print.rs index dd98b46abc..b8ddafae29 100644 --- a/crates/hir/src/core/print.rs +++ b/crates/hir/src/core/print.rs @@ -686,6 +686,17 @@ impl<'db> Expr<'db> { ) } + Expr::DynField(receiver, field_expr) => { + let receiver = unwrap_partial_ref(receiver.data(db, body), "DynField::receiver"); + let field_expr = + unwrap_partial_ref(field_expr.data(db, body), "DynField::field_expr"); + format!( + "{}.{{{}}}", + receiver.pretty_print(db, body, indent), + field_expr.pretty_print(db, body, indent) + ) + } + Expr::Tuple(exprs) => { let exprs_str = exprs .iter() diff --git a/crates/hir/src/core/span/mod.rs b/crates/hir/src/core/span/mod.rs index a06aadf772..7f03372d04 100644 --- a/crates/hir/src/core/span/mod.rs +++ b/crates/hir/src/core/span/mod.rs @@ -241,6 +241,8 @@ pub enum DesugaredOrigin { Event(EventDesugared), /// The HIR node is the result of desugaring a `#[error]` struct. Error(ErrorDesugared), + /// The HIR node is the result of desugaring a `#[derive(...)]` attribute. + Derive(DeriveDesugared), } /// Tracks the origin of HIR nodes desugared from a `msg` block. @@ -280,6 +282,12 @@ pub struct ErrorDesugared { pub error_struct: AstPtr, } +/// Tracks the origin of HIR nodes desugared from a `#[derive(...)]` attribute. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct DeriveDesugared { + pub derive_struct: AstPtr, +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct UseDesugared { pub root: AstPtr, diff --git a/crates/hir/src/core/span/transition.rs b/crates/hir/src/core/span/transition.rs index 0f62710ad3..1b07b214ff 100644 --- a/crates/hir/src/core/span/transition.rs +++ b/crates/hir/src/core/span/transition.rs @@ -532,6 +532,9 @@ impl DesugaredOrigin { Self::Error(super::ErrorDesugared { error_struct }) => { error_struct.to_node(&root).syntax().text_range() } + Self::Derive(super::DeriveDesugared { derive_struct }) => { + derive_struct.to_node(&root).syntax().text_range() + } }; Span::new(file, range, SpanKind::Original) diff --git a/crates/hir/src/core/visitor.rs b/crates/hir/src/core/visitor.rs index 4c92f3f0be..780d1e45db 100644 --- a/crates/hir/src/core/visitor.rs +++ b/crates/hir/src/core/visitor.rs @@ -1368,6 +1368,11 @@ pub fn walk_expr<'db, V>( } } + Expr::DynField(receiver_id, field_expr_id) => { + visit_node_in_body!(visitor, ctxt, receiver_id, expr); + visit_node_in_body!(visitor, ctxt, field_expr_id, expr); + } + Expr::Tuple(elems) => { for elem_id in elems { visit_node_in_body!(visitor, ctxt, elem_id, expr); diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs index d6942a78aa..d9c424811d 100644 --- a/crates/hir/src/lib.rs +++ b/crates/hir/src/lib.rs @@ -1,8 +1,8 @@ use common::InputDb; pub use core::lower::{ - ArithmeticAttrError, ArithmeticAttrErrorKind, ErrorDiagnostic, ErrorDiagnosticKind, EventError, - EventErrorKind, InlineAttrError, LoopUnrollAttrError, PayableError, PayableErrorKind, - SelectorError, SelectorErrorKind, parse::ParserError, + ArithmeticAttrError, ArithmeticAttrErrorKind, DeriveError, DeriveErrorKind, ErrorDiagnostic, + ErrorDiagnosticKind, EventError, EventErrorKind, InlineAttrError, LoopUnrollAttrError, + PayableError, PayableErrorKind, SelectorError, SelectorErrorKind, parse::ParserError, }; pub mod analysis; diff --git a/crates/hir/src/test_db.rs b/crates/hir/src/test_db.rs index ef60ccf120..88f443c4da 100644 --- a/crates/hir/src/test_db.rs +++ b/crates/hir/src/test_db.rs @@ -10,8 +10,8 @@ use std::ops::Range; use crate::analysis::{ analysis_pass::{ - AnalysisPassManager, ArithmeticAttrPass, ErrorLowerPass, EventLowerPass, InlineAttrPass, - LoopUnrollAttrPass, MsgLowerPass, ParsingPass, PayableAttrPass, + AnalysisPassManager, ArithmeticAttrPass, DeriveLowerPass, ErrorLowerPass, EventLowerPass, + InlineAttrPass, LoopUnrollAttrPass, MsgLowerPass, ParsingPass, PayableAttrPass, }, diagnostics::{DiagnosticVoucher, SpannedHirAnalysisDb}, name_resolution::ImportAnalysisPass, @@ -370,6 +370,7 @@ pub fn initialize_analysis_pass() -> AnalysisPassManager { pass_manager.add_module_pass("MsgLower", Box::new(MsgLowerPass {})); pass_manager.add_module_pass("EventLower", Box::new(EventLowerPass {})); pass_manager.add_module_pass("ErrorLower", Box::new(ErrorLowerPass {})); + pass_manager.add_module_pass("DeriveLower", Box::new(DeriveLowerPass {})); pass_manager.add_module_pass("InlineAttr", Box::new(InlineAttrPass {})); pass_manager.add_module_pass("LoopUnrollAttr", Box::new(LoopUnrollAttrPass {})); pass_manager.add_module_pass("MsgSelector", Box::new(MsgSelectorAnalysisPass {})); diff --git a/crates/parser/src/ast/expr.rs b/crates/parser/src/ast/expr.rs index 42c6f97665..673be9a1fd 100644 --- a/crates/parser/src/ast/expr.rs +++ b/crates/parser/src/ast/expr.rs @@ -16,6 +16,7 @@ ast_node! { | SK::PathExpr | SK::RecordInitExpr | SK::FieldExpr + | SK::ComptimeFieldExpr | SK::IndexExpr | SK::TupleExpr | SK::ArrayExpr @@ -47,6 +48,9 @@ impl Expr { ExprKind::RecordInit(AstNode::cast(self.syntax().clone()).unwrap()) } SK::FieldExpr => ExprKind::Field(AstNode::cast(self.syntax().clone()).unwrap()), + SK::ComptimeFieldExpr => { + ExprKind::ComptimeField(AstNode::cast(self.syntax().clone()).unwrap()) + } SK::IndexExpr => ExprKind::Index(AstNode::cast(self.syntax().clone()).unwrap()), SK::TupleExpr => ExprKind::Tuple(AstNode::cast(self.syntax().clone()).unwrap()), SK::ArrayExpr => ExprKind::Array(AstNode::cast(self.syntax().clone()).unwrap()), @@ -244,6 +248,21 @@ impl FieldExpr { } } +ast_node! { + /// `expr.{field_expr}` — comptime field access + pub struct ComptimeFieldExpr, + SK::ComptimeFieldExpr +} +impl ComptimeFieldExpr { + pub fn receiver(&self) -> Option { + support::children::(self.syntax()).next() + } + + pub fn field_expr(&self) -> Option { + support::children::(self.syntax()).nth(1) + } +} + ast_node! { /// `expr[index]` pub struct IndexExpr, @@ -482,6 +501,7 @@ pub enum ExprKind { Path(PathExpr), RecordInit(RecordInitExpr), Field(FieldExpr), + ComptimeField(ComptimeFieldExpr), Index(IndexExpr), Tuple(TupleExpr), Array(ArrayExpr), diff --git a/crates/parser/src/parser/expr.rs b/crates/parser/src/parser/expr.rs index f7e5c95697..b0574c78d0 100644 --- a/crates/parser/src/parser/expr.rs +++ b/crates/parser/src/parser/expr.rs @@ -559,6 +559,16 @@ impl super::Parse for FieldExprScope { fn parse(&mut self, parser: &mut Parser) -> Result<(), Self::Error> { parser.bump_expected(SyntaxKind::Dot); + // `.{expr}` — comptime field access + if parser.current_kind() == Some(SyntaxKind::LBrace) { + self.set_kind(SyntaxKind::ComptimeFieldExpr); + parser.bump_expected(SyntaxKind::LBrace); + parse_expr(parser)?; + parser.expect(&[SyntaxKind::RBrace], None)?; + parser.bump(); + return Ok(()); + } + parser.expect(&[SyntaxKind::Ident, SyntaxKind::Int], None)?; parser.bump(); Ok(()) diff --git a/crates/parser/src/syntax_kind.rs b/crates/parser/src/syntax_kind.rs index 5a7bff11be..d8106e7ae4 100644 --- a/crates/parser/src/syntax_kind.rs +++ b/crates/parser/src/syntax_kind.rs @@ -290,6 +290,8 @@ pub enum SyntaxKind { RecordField, /// `foo.bar`, `foo.0` FieldExpr, + /// `foo.{expr}` — comptime field access + ComptimeFieldExpr, /// `foo[1]` IndexExpr, /// `(x ,y)` @@ -702,6 +704,7 @@ impl SyntaxKind { SyntaxKind::RecordFieldList => "record field list", SyntaxKind::RecordField => "field", SyntaxKind::FieldExpr => "field", + SyntaxKind::ComptimeFieldExpr => "comptime field access", SyntaxKind::TupleExpr => "tuple expression", SyntaxKind::ArrayRepExpr => "array expression", SyntaxKind::WithParamList => "`with` parameter list", diff --git a/crates/uitest/fixtures/ty_check/derive_abi.fe b/crates/uitest/fixtures/ty_check/derive_abi.fe new file mode 100644 index 0000000000..260f844c0a --- /dev/null +++ b/crates/uitest/fixtures/ty_check/derive_abi.fe @@ -0,0 +1,5 @@ +#[derive(Abi)] +struct Transfer { + to: Address, + amount: u256, +} diff --git a/crates/uitest/fixtures/ty_check/derive_abi.snap b/crates/uitest/fixtures/ty_check/derive_abi.snap new file mode 100644 index 0000000000..1f6bd27ea8 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/derive_abi.snap @@ -0,0 +1,7 @@ +--- +source: crates/uitest/tests/ty_check.rs +assertion_line: 27 +expression: diags +input_file: fixtures/ty_check/derive_abi.fe +--- + diff --git a/crates/uitest/fixtures/ty_check/derive_ctfe_driven.fe b/crates/uitest/fixtures/ty_check/derive_ctfe_driven.fe new file mode 100644 index 0000000000..3d2eca8e71 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/derive_ctfe_driven.fe @@ -0,0 +1,22 @@ +// This test verifies CTFE drives output: if the strategy function body +// weren't being evaluated, derives on a struct with 3+ fields would still +// work (they'd need 3 field comparisons from the strategy's for loop). +// A hardcoded fallback that doesn't read the strategy would break here. +#[derive(Eq, Default, Ord)] +struct Triple { + a: u256, + b: u256, + c: u256, +} + +fn eq_works(x: Triple, y: Triple) -> bool { + x.eq(y) +} + +fn default_works() -> Triple { + Triple::default() +} + +fn lt_works(x: Triple, y: Triple) -> bool { + x.lt(y) +} diff --git a/crates/uitest/fixtures/ty_check/derive_ctfe_driven.snap b/crates/uitest/fixtures/ty_check/derive_ctfe_driven.snap new file mode 100644 index 0000000000..bf2058a942 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/derive_ctfe_driven.snap @@ -0,0 +1,6 @@ +--- +source: crates/uitest/tests/ty_check.rs +expression: diags +input_file: fixtures/ty_check/derive_ctfe_driven.fe +--- + diff --git a/crates/uitest/fixtures/ty_check/derive_default.fe b/crates/uitest/fixtures/ty_check/derive_default.fe new file mode 100644 index 0000000000..0ec6fab509 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/derive_default.fe @@ -0,0 +1,9 @@ +#[derive(Default)] +struct Config { + value: u256, + flag: bool, +} + +fn test() -> Config { + Config::default() +} diff --git a/crates/uitest/fixtures/ty_check/derive_default.snap b/crates/uitest/fixtures/ty_check/derive_default.snap new file mode 100644 index 0000000000..29c582dfaa --- /dev/null +++ b/crates/uitest/fixtures/ty_check/derive_default.snap @@ -0,0 +1,7 @@ +--- +source: crates/uitest/tests/ty_check.rs +assertion_line: 27 +expression: diags +input_file: fixtures/ty_check/derive_default.fe +--- + diff --git a/crates/uitest/fixtures/ty_check/derive_empty_struct.fe b/crates/uitest/fixtures/ty_check/derive_empty_struct.fe new file mode 100644 index 0000000000..1742187fa2 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/derive_empty_struct.fe @@ -0,0 +1,10 @@ +#[derive(Eq, Default)] +struct Empty {} + +fn test_eq(a: Empty, b: Empty) -> bool { + a.eq(b) +} + +fn test_default() -> Empty { + Empty::default() +} diff --git a/crates/uitest/fixtures/ty_check/derive_empty_struct.snap b/crates/uitest/fixtures/ty_check/derive_empty_struct.snap new file mode 100644 index 0000000000..4435a11634 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/derive_empty_struct.snap @@ -0,0 +1,6 @@ +--- +source: crates/uitest/tests/ty_check.rs +expression: diags +input_file: fixtures/ty_check/derive_empty_struct.fe +--- + diff --git a/crates/uitest/fixtures/ty_check/derive_eq.fe b/crates/uitest/fixtures/ty_check/derive_eq.fe new file mode 100644 index 0000000000..df1e530e4d --- /dev/null +++ b/crates/uitest/fixtures/ty_check/derive_eq.fe @@ -0,0 +1,9 @@ +#[derive(Eq)] +struct Point { + x: u256, + y: u256, +} + +fn test(a: Point, b: Point) -> bool { + a.eq(b) +} diff --git a/crates/uitest/fixtures/ty_check/derive_eq.snap b/crates/uitest/fixtures/ty_check/derive_eq.snap new file mode 100644 index 0000000000..ae8c61ab0c --- /dev/null +++ b/crates/uitest/fixtures/ty_check/derive_eq.snap @@ -0,0 +1,7 @@ +--- +source: crates/uitest/tests/ty_check.rs +assertion_line: 27 +expression: diags +input_file: fixtures/ty_check/derive_eq.fe +--- + diff --git a/crates/uitest/fixtures/ty_check/derive_generic_struct.fe b/crates/uitest/fixtures/ty_check/derive_generic_struct.fe new file mode 100644 index 0000000000..0c4e8ba5b5 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/derive_generic_struct.fe @@ -0,0 +1,4 @@ +#[derive(Eq)] +struct Generic { + value: u256, +} diff --git a/crates/uitest/fixtures/ty_check/derive_generic_struct.snap b/crates/uitest/fixtures/ty_check/derive_generic_struct.snap new file mode 100644 index 0000000000..d41ae239b4 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/derive_generic_struct.snap @@ -0,0 +1,13 @@ +--- +source: crates/uitest/tests/ty_check.rs +assertion_line: 27 +expression: diags +input_file: fixtures/ty_check/derive_generic_struct.fe +--- +error[17-0002]: `#[derive(Eq)]` structs must be non-generic + ┌─ derive_generic_struct.fe:2:15 + │ +2 │ struct Generic { + │ ^^^ generics are not supported on derived structs + │ + = remove generic parameters from the struct diff --git a/crates/uitest/fixtures/ty_check/derive_hash.fe b/crates/uitest/fixtures/ty_check/derive_hash.fe new file mode 100644 index 0000000000..661acdba64 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/derive_hash.fe @@ -0,0 +1,5 @@ +#[derive(Hash)] +struct Token { + id: u256, + amount: u256, +} diff --git a/crates/uitest/fixtures/ty_check/derive_hash.snap b/crates/uitest/fixtures/ty_check/derive_hash.snap new file mode 100644 index 0000000000..ab9fd08b78 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/derive_hash.snap @@ -0,0 +1,14 @@ +--- +source: crates/uitest/tests/ty_check.rs +expression: diags +input_file: fixtures/ty_check/derive_hash.fe +--- +error[2-0010]: no method named `hash` found for u256 `u256` + ┌─ derive_hash.fe:1:1 + │ +1 │ ╭ #[derive(Hash)] +2 │ │ struct Token { +3 │ │ id: u256, +4 │ │ amount: u256, +5 │ │ } + │ ╰─^ method not found in `u256` diff --git a/crates/uitest/fixtures/ty_check/derive_many_fields.fe b/crates/uitest/fixtures/ty_check/derive_many_fields.fe new file mode 100644 index 0000000000..1f05001796 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/derive_many_fields.fe @@ -0,0 +1,16 @@ +#[derive(Eq, Default)] +struct ManyFields { + a: u256, + b: u256, + c: u256, + d: u256, + e: u256, +} + +fn test_eq(x: ManyFields, y: ManyFields) -> bool { + x.eq(y) +} + +fn test_default() -> ManyFields { + ManyFields::default() +} diff --git a/crates/uitest/fixtures/ty_check/derive_many_fields.snap b/crates/uitest/fixtures/ty_check/derive_many_fields.snap new file mode 100644 index 0000000000..a39983a841 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/derive_many_fields.snap @@ -0,0 +1,6 @@ +--- +source: crates/uitest/tests/ty_check.rs +expression: diags +input_file: fixtures/ty_check/derive_many_fields.fe +--- + diff --git a/crates/uitest/fixtures/ty_check/derive_multi_trait.fe b/crates/uitest/fixtures/ty_check/derive_multi_trait.fe new file mode 100644 index 0000000000..c45afb637c --- /dev/null +++ b/crates/uitest/fixtures/ty_check/derive_multi_trait.fe @@ -0,0 +1,17 @@ +#[derive(Eq, Default, Ord)] +struct Pair { + x: u256, + y: u256, +} + +fn test_eq(a: Pair, b: Pair) -> bool { + a.eq(b) +} + +fn test_default() -> Pair { + Pair::default() +} + +fn test_lt(a: Pair, b: Pair) -> bool { + a.lt(b) +} diff --git a/crates/uitest/fixtures/ty_check/derive_multi_trait.snap b/crates/uitest/fixtures/ty_check/derive_multi_trait.snap new file mode 100644 index 0000000000..5842ecd671 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/derive_multi_trait.snap @@ -0,0 +1,7 @@ +--- +source: crates/uitest/tests/ty_check.rs +assertion_line: 27 +expression: diags +input_file: fixtures/ty_check/derive_multi_trait.fe +--- + diff --git a/crates/uitest/fixtures/ty_check/derive_ord.fe b/crates/uitest/fixtures/ty_check/derive_ord.fe new file mode 100644 index 0000000000..6906c82b8a --- /dev/null +++ b/crates/uitest/fixtures/ty_check/derive_ord.fe @@ -0,0 +1,8 @@ +#[derive(Ord)] +struct Priority { + level: u256, +} + +fn test(a: Priority, b: Priority) -> bool { + a.lt(b) +} diff --git a/crates/uitest/fixtures/ty_check/derive_ord.snap b/crates/uitest/fixtures/ty_check/derive_ord.snap new file mode 100644 index 0000000000..02f264746f --- /dev/null +++ b/crates/uitest/fixtures/ty_check/derive_ord.snap @@ -0,0 +1,7 @@ +--- +source: crates/uitest/tests/ty_check.rs +assertion_line: 27 +expression: diags +input_file: fixtures/ty_check/derive_ord.fe +--- + diff --git a/crates/uitest/fixtures/ty_check/derive_ord_multi_field.fe b/crates/uitest/fixtures/ty_check/derive_ord_multi_field.fe new file mode 100644 index 0000000000..ea6558e28b --- /dev/null +++ b/crates/uitest/fixtures/ty_check/derive_ord_multi_field.fe @@ -0,0 +1,22 @@ +#[derive(Ord)] +struct Coordinate { + x: u256, + y: u256, + z: u256, +} + +fn test_lt(a: Coordinate, b: Coordinate) -> bool { + a.lt(b) +} + +fn test_le(a: Coordinate, b: Coordinate) -> bool { + a.le(b) +} + +fn test_gt(a: Coordinate, b: Coordinate) -> bool { + a.gt(b) +} + +fn test_ge(a: Coordinate, b: Coordinate) -> bool { + a.ge(b) +} diff --git a/crates/uitest/fixtures/ty_check/derive_ord_multi_field.snap b/crates/uitest/fixtures/ty_check/derive_ord_multi_field.snap new file mode 100644 index 0000000000..78d30510d5 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/derive_ord_multi_field.snap @@ -0,0 +1,6 @@ +--- +source: crates/uitest/tests/ty_check.rs +expression: diags +input_file: fixtures/ty_check/derive_ord_multi_field.fe +--- + diff --git a/crates/uitest/fixtures/ty_check/derive_single_field.fe b/crates/uitest/fixtures/ty_check/derive_single_field.fe new file mode 100644 index 0000000000..6ea77c7266 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/derive_single_field.fe @@ -0,0 +1,12 @@ +#[derive(Eq, Default)] +struct Wrapper { + value: u256, +} + +fn test_eq(a: Wrapper, b: Wrapper) -> bool { + a.eq(b) +} + +fn test_default() -> Wrapper { + Wrapper::default() +} diff --git a/crates/uitest/fixtures/ty_check/derive_single_field.snap b/crates/uitest/fixtures/ty_check/derive_single_field.snap new file mode 100644 index 0000000000..b85d1c7cd0 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/derive_single_field.snap @@ -0,0 +1,6 @@ +--- +source: crates/uitest/tests/ty_check.rs +expression: diags +input_file: fixtures/ty_check/derive_single_field.fe +--- + diff --git a/crates/uitest/fixtures/ty_check/derive_unknown_trait.fe b/crates/uitest/fixtures/ty_check/derive_unknown_trait.fe new file mode 100644 index 0000000000..c7597a7321 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/derive_unknown_trait.fe @@ -0,0 +1,5 @@ +#[derive(Eq, Serialize)] +struct Point { + x: u256, + y: u256, +} diff --git a/crates/uitest/fixtures/ty_check/derive_unknown_trait.snap b/crates/uitest/fixtures/ty_check/derive_unknown_trait.snap new file mode 100644 index 0000000000..6f674b3e50 --- /dev/null +++ b/crates/uitest/fixtures/ty_check/derive_unknown_trait.snap @@ -0,0 +1,13 @@ +--- +source: crates/uitest/tests/ty_check.rs +assertion_line: 27 +expression: diags +input_file: fixtures/ty_check/derive_unknown_trait.fe +--- +error[17-0003]: unknown derive trait `Serialize` + ┌─ derive_unknown_trait.fe:1:1 + │ +1 │ #[derive(Eq, Serialize)] + │ ^^^^^^^^^^^^^^^^^^^^^^^^ `Serialize` is not a recognized derive trait + │ + = recognized traits: Eq, Default, Abi, Hash, Ord, Event, Error diff --git a/crates/uitest/tests/ty_check.rs b/crates/uitest/tests/ty_check.rs index 65d55ccc99..bf190888dd 100644 --- a/crates/uitest/tests/ty_check.rs +++ b/crates/uitest/tests/ty_check.rs @@ -1,6 +1,7 @@ use common::InputDb; use dir_test::{Fixture, dir_test}; use driver::DriverDataBase; +use hir::analysis::HirAnalysisDb; use test_utils::snap_test; #[cfg(target_arch = "wasm32")] @@ -14,6 +15,7 @@ use url::Url; )] fn run_ty_check(fixture: Fixture<&str>) { let mut db = DriverDataBase::default(); + HirAnalysisDb::zalsa_register_downcaster(&db); let file = db.workspace().touch( &mut db, Url::from_file_path(fixture.path()).expect("path should be absolute"), @@ -44,6 +46,7 @@ mod wasm { )] fn run_ty_check(fixture: Fixture<&str>) { let mut db = DriverDataBase::default(); + HirAnalysisDb::zalsa_register_downcaster(&db); let file = db.workspace().touch( &mut db, ::from_file_path_lossy(fixture.path()), diff --git a/ingots/core/src/builder.fe b/ingots/core/src/builder.fe new file mode 100644 index 0000000000..805c74b7c7 --- /dev/null +++ b/ingots/core/src/builder.fe @@ -0,0 +1,6 @@ +pub struct Builder {} + +impl Builder { + pub const fn set_field(mut self, _ index: usize, _ value: V) {} + pub fn finish(self) -> T { ingot::panic() } +} diff --git a/ingots/core/src/default.fe b/ingots/core/src/default.fe index 97bbbf1342..ebd1c43da8 100644 --- a/ingots/core/src/default.fe +++ b/ingots/core/src/default.fe @@ -1,9 +1,21 @@ use super::abi::{Bytes, DynArray, DynString} +use super::reflect::Reflect +use super::builder::Builder pub trait Default { fn default() -> Self } +#[derive_strategy] +const fn __derive_default() -> T + uses (reflect: Reflect, builder: Builder) +{ + for i in 0..reflect.field_count() { + builder.set_field(i, Default::default()) + } + builder.finish() +} + impl Default for () { fn default() -> Self { () diff --git a/ingots/core/src/lib.fe b/ingots/core/src/lib.fe index 09ce057c04..56e8023af0 100644 --- a/ingots/core/src/lib.fe +++ b/ingots/core/src/lib.fe @@ -14,6 +14,8 @@ pub use effect_ref::{EffectHandle, EffectHandleMut, EffectRef, EffectRefMut, Mem pub use contracts::{self, *} pub use range::{Bound, Known, Unknown, Range} pub use seq::Seq +pub use reflect::Reflect +pub use builder::Builder extern { pub fn panic() -> ! diff --git a/ingots/core/src/ops.fe b/ingots/core/src/ops.fe index 36e8e9ca71..649b867c4b 100644 --- a/ingots/core/src/ops.fe +++ b/ingots/core/src/ops.fe @@ -130,6 +130,69 @@ pub trait Ord { fn ge(self, _ other: T) -> bool } +pub trait Hash { + fn hash(self) -> u256 +} + +pub trait Hasher { + fn feed(mut self, _ value: u256) + fn finish(self) -> u256 +} + +use super::reflect::Reflect + +#[derive_strategy] +const fn __derive_eq(self_val: T, other: T) -> bool + uses (reflect: Reflect) +{ + for i in 0..reflect.field_count() { + if self_val.{reflect.field_name(i)} != other.{reflect.field_name(i)} { + return false + } + } + true +} + +#[derive_strategy] +const fn __derive_hash(self_val: T) -> u256 + uses (reflect: Reflect, hasher: mut Hasher) +{ + for i in 0..reflect.field_count() { + hasher.feed(self_val.{reflect.field_name(i)}) + } + hasher.finish() +} + +#[derive_strategy] +const fn __derive_ord(self_val: T, other: T) -> bool + uses (reflect: Reflect) +{ + for i in 0..reflect.field_count() { + if self_val.{reflect.field_name(i)} < other.{reflect.field_name(i)} { + return true + } + if self_val.{reflect.field_name(i)} > other.{reflect.field_name(i)} { + return false + } + } + false +} + +#[derive_strategy] +const fn __derive_le(self_val: T, other: T) -> bool { + !(other < self_val) +} + +#[derive_strategy] +const fn __derive_gt(self_val: T, other: T) -> bool { + other < self_val +} + +#[derive_strategy] +const fn __derive_ge(self_val: T, other: T) -> bool { + !(self_val < other) +} + // Indexing operator pub trait Index { type Output diff --git a/ingots/core/src/reflect.fe b/ingots/core/src/reflect.fe new file mode 100644 index 0000000000..b46d742373 --- /dev/null +++ b/ingots/core/src/reflect.fe @@ -0,0 +1,14 @@ +pub struct Reflect {} + +impl Reflect { + pub const fn field_count(self) -> usize { 0 } + pub const fn field_name(self, _ index: usize) -> usize { 0 } + pub const fn field_is_pub(self, _ index: usize) -> bool { false } + pub const fn field_has_attr(self, _ index: usize, _ name: usize) -> bool { false } + + pub const fn type_param_count(self) -> usize { 0 } + pub const fn type_param_name(self, _ index: usize) -> usize { 0 } + + pub const fn field_head_size(self, _ index: usize) -> u256 { 0 } + pub const fn field_is_dynamic(self, _ index: usize) -> bool { false } +} diff --git a/{%.new} b/{%.new} new file mode 100644 index 0000000000..30b669688f --- /dev/null +++ b/{%.new} @@ -0,0 +1,7 @@ +--- +source: crates/uitest/tests/ty_check.rs +assertion_line: 27 +expression: diags +input_file: fixtures/ty_check/derive_empty_struct.fe +--- +