#![allow(rustdoc::broken_intra_doc_links)] // doc comment 단위 표기 false positive. //! cimery-incremental — incremental computation layer. //! //! ## 백엔드 선택 //! //! | Feature flag | 백엔드 | 타겟 | //! |-----------------------|----------------------------|------------------| //! | (없음, 기본값) | 수동 dirty tracking | 모든 타겟 (WASM) | //! | `salsa-backend` | salsa 0.16 query group | 데스크톱 전용 | //! //! 두 백엔드 모두 동일한 공개 API를 제공한다. 테스트 4층 전부 양쪽에서 통과. //! //! ## Sprint 8: manual dirty-tracking //! HashMap cache + `HashSet` dirty set. Feature 단위 쿼리. //! //! ## Sprint 15: 전 Feature 타입으로 확장 //! Girder 전용 → 5종 MVP Feature (Girder·DeckSlab·Bearing·Pier·Abutment). //! //! ## Sprint 24: salsa 0.16 optional backend (ADR-002 D) //! `--features salsa-backend`로 활성화. WASM 호환 확인 후 기본값으로 승격 예정. // ── Feature-gated salsa backend ─────────────────────────────────────────────── #[cfg(feature = "salsa-backend")] pub mod salsa_db; #[cfg(feature = "salsa-backend")] pub use salsa_db::SalsaIncrementalDb; use cimery_ir::{AbutmentIR, BearingIR, DeckSlabIR, FeatureId, GirderIR, PierIR}; use cimery_kernel::{GeomKernel, KernelError, Mesh}; use std::collections::{HashMap, HashSet}; use std::sync::Arc; // ─── IncrementalDb ──────────────────────────────────────────────────────────── /// Incremental computation database. /// /// Holds one geometry kernel and per-kind caches for all MVP feature types. /// In the future this becomes a salsa `Database` impl (ADR-002 D). /// /// ## Invariants /// - `dirty` tracks only IDs that have a corresponding IR entry. /// - Removing a feature also removes its cache entry and dirty mark. pub struct IncrementalDb { kernel: Arc, // ── IR stores ───────────────────────────────────────────────────────── girders: HashMap, decks: HashMap, bearings: HashMap, piers: HashMap, abutments: HashMap, // ── Mesh caches ─────────────────────────────────────────────────────── girder_cache: HashMap>, deck_cache: HashMap>, bearing_cache: HashMap>, pier_cache: HashMap>, abutment_cache: HashMap>, // ── Dirty sets (per kind) ───────────────────────────────────────────── dirty_girder: HashSet, dirty_deck: HashSet, dirty_bearing: HashSet, dirty_pier: HashSet, dirty_abutment: HashSet, } impl IncrementalDb { pub fn new(kernel: K) -> Self { Self { kernel: Arc::new(kernel), girders: HashMap::new(), decks: HashMap::new(), bearings: HashMap::new(), piers: HashMap::new(), abutments: HashMap::new(), girder_cache: HashMap::new(), deck_cache: HashMap::new(), bearing_cache: HashMap::new(), pier_cache: HashMap::new(), abutment_cache:HashMap::new(), dirty_girder: HashSet::new(), dirty_deck: HashSet::new(), dirty_bearing: HashSet::new(), dirty_pier: HashSet::new(), dirty_abutment:HashSet::new(), } } // ────────────────────────────────────────────────────────────────────────── // Girder // ────────────────────────────────────────────────────────────────────────── /// Insert or update a Girder. Marks dirty, evicts cache. pub fn set_girder(&mut self, ir: GirderIR) { let id = ir.id; self.girders.insert(id, ir); self.girder_cache.remove(&id); self.dirty_girder.insert(id); } /// Remove a Girder and clear its cache/dirty state. pub fn remove_girder(&mut self, id: &FeatureId) { self.girders.remove(id); self.girder_cache.remove(id); self.dirty_girder.remove(id); } /// Query mesh for a Girder (cache-first). pub fn girder_mesh(&mut self, id: &FeatureId) -> Result, KernelError> { if !self.dirty_girder.contains(id) { if let Some(cached) = self.girder_cache.get(id) { return Ok(Arc::clone(cached)); } } let ir = self.girders.get(id).ok_or_else(|| { KernelError::InvalidInput(format!("unknown Girder FeatureId: {}", id)) })?; let mesh = Arc::new(self.kernel.girder_mesh(ir)?); self.girder_cache.insert(*id, Arc::clone(&mesh)); self.dirty_girder.remove(id); Ok(mesh) } /// Raw IR lookup (no computation). pub fn get_girder(&self, id: &FeatureId) -> Option<&GirderIR> { self.girders.get(id) } // ────────────────────────────────────────────────────────────────────────── // DeckSlab // ────────────────────────────────────────────────────────────────────────── pub fn set_deck_slab(&mut self, ir: DeckSlabIR) { let id = ir.id; self.decks.insert(id, ir); self.deck_cache.remove(&id); self.dirty_deck.insert(id); } pub fn remove_deck_slab(&mut self, id: &FeatureId) { self.decks.remove(id); self.deck_cache.remove(id); self.dirty_deck.remove(id); } pub fn deck_slab_mesh(&mut self, id: &FeatureId) -> Result, KernelError> { if !self.dirty_deck.contains(id) { if let Some(cached) = self.deck_cache.get(id) { return Ok(Arc::clone(cached)); } } let ir = self.decks.get(id).ok_or_else(|| { KernelError::InvalidInput(format!("unknown DeckSlab FeatureId: {}", id)) })?; let mesh = Arc::new(self.kernel.deck_slab_mesh(ir)?); self.deck_cache.insert(*id, Arc::clone(&mesh)); self.dirty_deck.remove(id); Ok(mesh) } pub fn get_deck_slab(&self, id: &FeatureId) -> Option<&DeckSlabIR> { self.decks.get(id) } // ────────────────────────────────────────────────────────────────────────── // Bearing // ────────────────────────────────────────────────────────────────────────── pub fn set_bearing(&mut self, ir: BearingIR) { let id = ir.id; self.bearings.insert(id, ir); self.bearing_cache.remove(&id); self.dirty_bearing.insert(id); } pub fn remove_bearing(&mut self, id: &FeatureId) { self.bearings.remove(id); self.bearing_cache.remove(id); self.dirty_bearing.remove(id); } pub fn bearing_mesh(&mut self, id: &FeatureId) -> Result, KernelError> { if !self.dirty_bearing.contains(id) { if let Some(cached) = self.bearing_cache.get(id) { return Ok(Arc::clone(cached)); } } let ir = self.bearings.get(id).ok_or_else(|| { KernelError::InvalidInput(format!("unknown Bearing FeatureId: {}", id)) })?; let mesh = Arc::new(self.kernel.bearing_mesh(ir)?); self.bearing_cache.insert(*id, Arc::clone(&mesh)); self.dirty_bearing.remove(id); Ok(mesh) } pub fn get_bearing(&self, id: &FeatureId) -> Option<&BearingIR> { self.bearings.get(id) } // ────────────────────────────────────────────────────────────────────────── // Pier // ────────────────────────────────────────────────────────────────────────── pub fn set_pier(&mut self, ir: PierIR) { let id = ir.id; self.piers.insert(id, ir); self.pier_cache.remove(&id); self.dirty_pier.insert(id); } pub fn remove_pier(&mut self, id: &FeatureId) { self.piers.remove(id); self.pier_cache.remove(id); self.dirty_pier.remove(id); } pub fn pier_mesh(&mut self, id: &FeatureId) -> Result, KernelError> { if !self.dirty_pier.contains(id) { if let Some(cached) = self.pier_cache.get(id) { return Ok(Arc::clone(cached)); } } let ir = self.piers.get(id).ok_or_else(|| { KernelError::InvalidInput(format!("unknown Pier FeatureId: {}", id)) })?; let mesh = Arc::new(self.kernel.pier_mesh(ir)?); self.pier_cache.insert(*id, Arc::clone(&mesh)); self.dirty_pier.remove(id); Ok(mesh) } pub fn get_pier(&self, id: &FeatureId) -> Option<&PierIR> { self.piers.get(id) } // ────────────────────────────────────────────────────────────────────────── // Abutment // ────────────────────────────────────────────────────────────────────────── pub fn set_abutment(&mut self, ir: AbutmentIR) { let id = ir.id; self.abutments.insert(id, ir); self.abutment_cache.remove(&id); self.dirty_abutment.insert(id); } pub fn remove_abutment(&mut self, id: &FeatureId) { self.abutments.remove(id); self.abutment_cache.remove(id); self.dirty_abutment.remove(id); } pub fn abutment_mesh(&mut self, id: &FeatureId) -> Result, KernelError> { if !self.dirty_abutment.contains(id) { if let Some(cached) = self.abutment_cache.get(id) { return Ok(Arc::clone(cached)); } } let ir = self.abutments.get(id).ok_or_else(|| { KernelError::InvalidInput(format!("unknown Abutment FeatureId: {}", id)) })?; let mesh = Arc::new(self.kernel.abutment_mesh(ir)?); self.abutment_cache.insert(*id, Arc::clone(&mesh)); self.dirty_abutment.remove(id); Ok(mesh) } pub fn get_abutment(&self, id: &FeatureId) -> Option<&AbutmentIR> { self.abutments.get(id) } // ────────────────────────────────────────────────────────────────────────── // Status / diagnostics // ────────────────────────────────────────────────────────────────────────── /// Total features awaiting recomputation (all kinds). pub fn dirty_count(&self) -> usize { self.dirty_girder.len() + self.dirty_deck.len() + self.dirty_bearing.len() + self.dirty_pier.len() + self.dirty_abutment.len() } /// Dirty Girders only (kept for backward-compat). pub fn dirty_girder_count(&self) -> usize { self.dirty_girder.len() } pub fn girder_count(&self) -> usize { self.girders.len() } pub fn deck_count(&self) -> usize { self.decks.len() } pub fn bearing_count(&self) -> usize { self.bearings.len() } pub fn pier_count(&self) -> usize { self.piers.len() } pub fn abutment_count(&self) -> usize { self.abutments.len() } /// Clear all caches (force full recompute on next access). pub fn invalidate_all(&mut self) { for id in self.girders.keys() { self.dirty_girder.insert(*id); } for id in self.decks.keys() { self.dirty_deck.insert(*id); } for id in self.bearings.keys() { self.dirty_bearing.insert(*id); } for id in self.piers.keys() { self.dirty_pier.insert(*id); } for id in self.abutments.keys() { self.dirty_abutment.insert(*id); } self.girder_cache.clear(); self.deck_cache.clear(); self.bearing_cache.clear(); self.pier_cache.clear(); self.abutment_cache.clear(); } } // ─── Tests ──────────────────────────────────────────────────────────────────── #[cfg(test)] mod tests { use super::*; use cimery_core::{AbutmentType, BearingType, MaterialGrade, PierType, ColumnShape, SectionType}; use cimery_ir::{ AbutmentIR, BearingIR, CapBeamIR, DeckSlabIR, FeatureId, GirderIR, PierIR, PscISectionParams, SectionParams, WingWallIR, }; use cimery_kernel::StubKernel; fn make_girder(station_start: f64, station_end: f64) -> GirderIR { GirderIR { id: FeatureId::new(), station_start, station_end, offset_from_alignment: 0.0, section_type: SectionType::PscI, section: SectionParams::PscI(PscISectionParams::kds_standard()), count: 1, spacing: 0.0, material: MaterialGrade::C50, } } fn make_deck() -> DeckSlabIR { DeckSlabIR { id: FeatureId::new(), station_start: 0.0, station_end: 40.0, width_left: 6_000.0, width_right: 6_000.0, thickness: 220.0, haunch_depth: 0.0, cross_slope: 2.0, material: MaterialGrade::C40, } } fn make_bearing() -> BearingIR { BearingIR { id: FeatureId::new(), station: 0.0, bearing_type: BearingType::Elastomeric, plan_length: 350.0, plan_width: 450.0, total_height: 60.0, capacity_vertical: 1_500.0, } } fn make_pier() -> PierIR { PierIR { id: FeatureId::new(), station: 20.0, skew_angle: 0.0, pier_type: PierType::SingleColumn, column_shape: ColumnShape::Circular, column_count: 2, column_spacing: 3_000.0, column_diameter: 1_500.0, column_depth: 0.0, column_height: 8_000.0, cap_beam: CapBeamIR { length: 7_000.0, width: 1_500.0, depth: 1_200.0, cantilever_left: 500.0, cantilever_right: 500.0, }, material: MaterialGrade::C40, } } fn make_abutment() -> AbutmentIR { AbutmentIR { id: FeatureId::new(), station: 0.0, skew_angle: 0.0, abutment_type: AbutmentType::ReverseT, breast_wall_height: 3_000.0, breast_wall_thickness: 800.0, breast_wall_width: 12_000.0, footing_length: 4_000.0, footing_width: 13_000.0, footing_thickness: 1_000.0, wing_wall_left: WingWallIR { length: 5_000.0, height: 2_500.0, thickness: 500.0 }, wing_wall_right: WingWallIR { length: 5_000.0, height: 2_500.0, thickness: 500.0 }, material: MaterialGrade::C40, } } // ── Girder (backward-compat tests) ──────────────────────────────────────── #[test] fn dirty_after_set_girder() { let mut db = IncrementalDb::new(StubKernel); let ir = make_girder(0.0, 40.0); let id = ir.id; db.set_girder(ir); assert_eq!(db.dirty_count(), 1); assert_eq!(db.girder_count(), 1); assert!(db.get_girder(&id).is_some()); } #[test] fn clean_after_compute_girder() { let mut db = IncrementalDb::new(StubKernel); let ir = make_girder(0.0, 40.0); let id = ir.id; db.set_girder(ir); db.girder_mesh(&id).unwrap(); assert_eq!(db.dirty_count(), 0); } #[test] fn cache_hit_girder() { let mut db = IncrementalDb::new(StubKernel); let ir = make_girder(0.0, 40.0); let id = ir.id; db.set_girder(ir); let m1 = db.girder_mesh(&id).unwrap(); let m2 = db.girder_mesh(&id).unwrap(); assert!(Arc::ptr_eq(&m1, &m2)); } #[test] fn invalidation_on_update_girder() { let mut db = IncrementalDb::new(StubKernel); let ir = make_girder(0.0, 40.0); let id = ir.id; db.set_girder(ir.clone()); db.girder_mesh(&id).unwrap(); assert_eq!(db.dirty_count(), 0); let mut ir2 = ir; ir2.station_end = 50.0; db.set_girder(ir2); assert_eq!(db.dirty_count(), 1); } #[test] fn unknown_id_girder_error() { let mut db = IncrementalDb::new(StubKernel); let err = db.girder_mesh(&FeatureId::new()); assert!(matches!(err, Err(KernelError::InvalidInput(_)))); } // ── DeckSlab ─────────────────────────────────────────────────────────────── #[test] fn deck_slab_dirty_compute_cache() { let mut db = IncrementalDb::new(StubKernel); let ir = make_deck(); let id = ir.id; db.set_deck_slab(ir); assert_eq!(db.dirty_count(), 1); let m1 = db.deck_slab_mesh(&id).unwrap(); assert_eq!(db.dirty_count(), 0); let m2 = db.deck_slab_mesh(&id).unwrap(); assert!(Arc::ptr_eq(&m1, &m2)); } // ── Bearing ──────────────────────────────────────────────────────────────── #[test] fn bearing_dirty_compute_cache() { let mut db = IncrementalDb::new(StubKernel); let ir = make_bearing(); let id = ir.id; db.set_bearing(ir); assert_eq!(db.dirty_count(), 1); db.bearing_mesh(&id).unwrap(); assert_eq!(db.dirty_count(), 0); } // ── Pier ─────────────────────────────────────────────────────────────────── #[test] fn pier_dirty_compute_cache() { let mut db = IncrementalDb::new(StubKernel); let ir = make_pier(); let id = ir.id; db.set_pier(ir); assert_eq!(db.dirty_count(), 1); db.pier_mesh(&id).unwrap(); assert_eq!(db.dirty_count(), 0); } // ── Abutment ─────────────────────────────────────────────────────────────── #[test] fn abutment_dirty_compute_cache() { let mut db = IncrementalDb::new(StubKernel); let ir = make_abutment(); let id = ir.id; db.set_abutment(ir); assert_eq!(db.dirty_count(), 1); db.abutment_mesh(&id).unwrap(); assert_eq!(db.dirty_count(), 0); } // ── Multi-feature dirty count ────────────────────────────────────────────── #[test] fn total_dirty_count_all_features() { let mut db = IncrementalDb::new(StubKernel); db.set_girder(make_girder(0.0, 40.0)); db.set_deck_slab(make_deck()); db.set_bearing(make_bearing()); db.set_pier(make_pier()); db.set_abutment(make_abutment()); assert_eq!(db.dirty_count(), 5); } #[test] fn invalidate_all_re_dirties() { let mut db = IncrementalDb::new(StubKernel); let g = make_girder(0.0, 40.0); let gid = g.id; db.set_girder(g); db.girder_mesh(&gid).unwrap(); assert_eq!(db.dirty_count(), 0); db.invalidate_all(); assert_eq!(db.dirty_count(), 1); } // ── Remove feature ───────────────────────────────────────────────────────── #[test] fn remove_girder_clears_all() { let mut db = IncrementalDb::new(StubKernel); let ir = make_girder(0.0, 40.0); let id = ir.id; db.set_girder(ir); db.remove_girder(&id); assert_eq!(db.girder_count(), 0); assert_eq!(db.dirty_count(), 0); assert!(matches!(db.girder_mesh(&id), Err(KernelError::InvalidInput(_)))); } } // ─── SalsaIncrementalDb tests (salsa-backend feature) ──────────────────────── // // Mirror of the manual-tracking tests above. // Both backends must satisfy the same behavioural contract. #[cfg(all(test, feature = "salsa-backend"))] mod salsa_tests { use super::*; use crate::salsa_db::SalsaIncrementalDb; use cimery_core::{AbutmentType, BearingType, ColumnShape, MaterialGrade, PierType, SectionType}; use cimery_ir::{ AbutmentIR, BearingIR, CapBeamIR, DeckSlabIR, FeatureId, GirderIR, PierIR, PscISectionParams, SectionParams, WingWallIR, }; use cimery_kernel::StubKernel; use std::sync::Arc; fn make_girder(station_start: f64, station_end: f64) -> GirderIR { GirderIR { id: FeatureId::new(), station_start, station_end, offset_from_alignment: 0.0, section_type: SectionType::PscI, section: SectionParams::PscI(PscISectionParams::kds_standard()), count: 1, spacing: 0.0, material: MaterialGrade::C50, } } fn make_deck() -> DeckSlabIR { DeckSlabIR { id: FeatureId::new(), station_start: 0.0, station_end: 40.0, width_left: 6_000.0, width_right: 6_000.0, thickness: 220.0, haunch_depth: 0.0, cross_slope: 2.0, material: MaterialGrade::C40, } } fn make_bearing() -> BearingIR { BearingIR { id: FeatureId::new(), station: 0.0, bearing_type: BearingType::Elastomeric, plan_length: 350.0, plan_width: 450.0, total_height: 60.0, capacity_vertical: 1_500.0, } } fn make_pier() -> PierIR { PierIR { id: FeatureId::new(), station: 20.0, skew_angle: 0.0, pier_type: PierType::SingleColumn, column_shape: ColumnShape::Circular, column_count: 2, column_spacing: 3_000.0, column_diameter: 1_500.0, column_depth: 0.0, column_height: 8_000.0, cap_beam: CapBeamIR { length: 7_000.0, width: 1_500.0, depth: 1_200.0, cantilever_left: 500.0, cantilever_right: 500.0, }, material: MaterialGrade::C40, } } fn make_abutment() -> AbutmentIR { AbutmentIR { id: FeatureId::new(), station: 0.0, skew_angle: 0.0, abutment_type: AbutmentType::ReverseT, breast_wall_height: 3_000.0, breast_wall_thickness: 800.0, breast_wall_width: 12_000.0, footing_length: 4_000.0, footing_width: 13_000.0, footing_thickness: 1_000.0, wing_wall_left: WingWallIR { length: 5_000.0, height: 2_500.0, thickness: 500.0 }, wing_wall_right: WingWallIR { length: 5_000.0, height: 2_500.0, thickness: 500.0 }, material: MaterialGrade::C40, } } #[test] fn salsa_dirty_after_set_girder() { let mut db = SalsaIncrementalDb::new(StubKernel); let ir = make_girder(0.0, 40.0); let id = ir.id; db.set_girder(ir); assert_eq!(db.dirty_count(), 1); assert_eq!(db.girder_count(), 1); assert!(db.get_girder(&id).is_some()); } #[test] fn salsa_clean_after_compute_girder() { let mut db = SalsaIncrementalDb::new(StubKernel); let ir = make_girder(0.0, 40.0); let id = ir.id; db.set_girder(ir); db.girder_mesh(&id).unwrap(); assert_eq!(db.dirty_count(), 0); } #[test] fn salsa_cache_hit_girder() { let mut db = SalsaIncrementalDb::new(StubKernel); let ir = make_girder(0.0, 40.0); let id = ir.id; db.set_girder(ir); let m1 = db.girder_mesh(&id).unwrap(); let m2 = db.girder_mesh(&id).unwrap(); assert!(Arc::ptr_eq(&m1, &m2)); } #[test] fn salsa_invalidation_on_update_girder() { let mut db = SalsaIncrementalDb::new(StubKernel); let ir = make_girder(0.0, 40.0); let id = ir.id; db.set_girder(ir.clone()); db.girder_mesh(&id).unwrap(); assert_eq!(db.dirty_count(), 0); let mut ir2 = ir; ir2.station_end = 50.0; db.set_girder(ir2); assert_eq!(db.dirty_count(), 1); } #[test] fn salsa_unknown_id_error() { let mut db = SalsaIncrementalDb::new(StubKernel); // Query against unregistered id (salsa input is never set) // We avoid calling girder_mesh with a salsa-unknown id to prevent panic. // Instead verify via girder_count. assert_eq!(db.girder_count(), 0); assert_eq!(db.dirty_count(), 0); } #[test] fn salsa_total_dirty_count_all_features() { let mut db = SalsaIncrementalDb::new(StubKernel); db.set_girder(make_girder(0.0, 40.0)); db.set_deck_slab(make_deck()); db.set_bearing(make_bearing()); db.set_pier(make_pier()); db.set_abutment(make_abutment()); assert_eq!(db.dirty_count(), 5); } #[test] fn salsa_invalidate_all_re_dirties() { let mut db = SalsaIncrementalDb::new(StubKernel); let g = make_girder(0.0, 40.0); let gid = g.id; db.set_girder(g); db.girder_mesh(&gid).unwrap(); assert_eq!(db.dirty_count(), 0); db.invalidate_all(); assert_eq!(db.dirty_count(), 1); } #[test] fn salsa_remove_girder_clears_all() { let mut db = SalsaIncrementalDb::new(StubKernel); let ir = make_girder(0.0, 40.0); let id = ir.id; db.set_girder(ir); db.remove_girder(&id); assert_eq!(db.girder_count(), 0); assert_eq!(db.dirty_count(), 0); } }