Skip to main content

bitloom_builder/
lib.rs

1//! Builder session: the only public mutation path into HIR (AD-13).
2
3use std::collections::{HashMap, HashSet};
4
5use bitloom_hir::{
6    Assign, AssignExpr, AssignTarget, BuilderOwnedHir, Module, Port, PortDirection, Process,
7    ProcessKind, Stmt,
8};
9
10pub use bitloom_hir::{
11    Diagnostic, Diagnostics, Diagnostics as HirDiagnostics, FrozenHir, FrozenHir as Frozen,
12    GroundType, SignalKind, Span,
13};
14
15mod closures;
16pub use closures::*;
17
18#[derive(Debug)]
19enum ProcessState {
20    Combinational {
21        assigns: Vec<Assign>,
22        /// Assignment sets along the current path stack for latch analysis.
23        path_assigned: Vec<HashSet<String>>,
24        /// Open branches: then-set collected so far, optional else placeholder.
25        pending_branches: Vec<(HashSet<String>, bool)>,
26        span: Span,
27    },
28    Sequential {
29        assigns: Vec<Assign>,
30        span: Span,
31    },
32}
33
34/// Session token holding the unfrozen circuit (AD-13).
35pub struct ElaborateSession {
36    hir: BuilderOwnedHir,
37    current: Option<Module>,
38    /// name -> kind for the current module
39    signals: HashMap<String, SignalKind>,
40    /// name -> bit width for UInt/SInt (Clock/Reset/Bool use 1)
41    widths: HashMap<String, u32>,
42    /// Phantom clock-domain id per signal (AD-22); default 0.
43    domains: HashMap<String, u32>,
44    /// Signals that may legally cross domains (DoubleFlop/SyncFIFO bridges).
45    cdc_bridges: HashSet<String>,
46    clock_port: Option<String>,
47    reset_port: Option<String>,
48    process: Option<ProcessState>,
49    errors: Diagnostics,
50}
51
52impl ElaborateSession {
53    pub fn new(circuit_name: impl Into<String>) -> Self {
54        Self {
55            hir: BuilderOwnedHir::new(circuit_name),
56            current: None,
57            signals: HashMap::new(),
58            widths: HashMap::new(),
59            domains: HashMap::new(),
60            cdc_bridges: HashSet::new(),
61            clock_port: None,
62            reset_port: None,
63            process: None,
64            errors: Diagnostics::default(),
65        }
66    }
67
68    fn push_err(&mut self, d: Diagnostic) {
69        self.errors.push(d);
70    }
71
72    pub fn begin_module(&mut self, name: impl Into<String>, span: Span) {
73        self.signals.clear();
74        self.widths.clear();
75        self.domains.clear();
76        self.cdc_bridges.clear();
77        self.clock_port = None;
78        self.reset_port = None;
79        self.process = None;
80        self.current = Some(Module {
81            name: name.into(),
82            ports: Vec::new(),
83            body: Vec::new(),
84            span,
85        });
86    }
87
88    /// Bind a phantom clock-domain id to a signal (AD-22).
89    pub fn bind_domain(&mut self, name: impl Into<String>, domain: u32) {
90        self.domains.insert(name.into(), domain);
91    }
92
93    /// Mark a CDC bridge signal that may legally cross domains.
94    pub fn mark_cdc_bridge(&mut self, name: impl Into<String>) {
95        self.cdc_bridges.insert(name.into());
96    }
97
98    /// Declare a synthesizable 2-stage CDC synchronizer register pair (FR79 / AD-29).
99    ///
100    /// Creates `{stem}_ff0` and `{stem}_ff1`, binds both to `dst_domain`, and marks
101    /// `{stem}_ff0` as a CDC bridge so [`Self::assign_reg_d_from`] / [`Self::assign_net`]
102    /// may sample a source-domain `din`. Call [`Self::connect_double_flop`] inside a
103    /// sequential process to wire `ff0 ← din`, `ff1 ← ff0`.
104    ///
105    /// **Stage count:** 2. **Latency:** 2 destination-domain ticks to `ff1` (see
106    /// `DoubleFlop::LATENCY_DST_TICKS` in prelude).
107    pub fn declare_double_flop_stages(
108        &mut self,
109        stem: impl Into<String>,
110        ty: GroundType,
111        dst_domain: u32,
112        span: Span,
113    ) -> (String, String) {
114        let stem = stem.into();
115        let ff0 = format!("{stem}_ff0");
116        let ff1 = format!("{stem}_ff1");
117        self.declare_reg(ff0.clone(), ty.clone(), span);
118        self.declare_reg(ff1.clone(), ty, span);
119        self.bind_domain(&ff0, dst_domain);
120        self.bind_domain(&ff1, dst_domain);
121        self.mark_cdc_bridge(&ff0);
122        (ff0, ff1)
123    }
124
125    /// Wire a previously declared DoubleFlop pair: `ff0 ← din`, `ff1 ← ff0`.
126    ///
127    /// Must be called inside a sequential process. `ff0`/`ff1` should come from
128    /// [`Self::declare_double_flop_stages`].
129    pub fn connect_double_flop(
130        &mut self,
131        ff0: impl Into<String>,
132        ff1: impl Into<String>,
133        din: impl Into<String>,
134        span: Span,
135    ) {
136        let ff0 = ff0.into();
137        let ff1 = ff1.into();
138        let din = din.into();
139        self.assign_reg_d_from(&ff0, &din, span);
140        self.assign_reg_d_from(&ff1, &ff0, span);
141    }
142
143    fn reject_illegal_cdc(&mut self, from: &str, to: &str, span: Span) -> bool {
144        let src_dom = self.domains.get(from).copied().unwrap_or(0);
145        let dst_dom = self.domains.get(to).copied().unwrap_or(0);
146        if src_dom != dst_dom && !self.cdc_bridges.contains(to) && !self.cdc_bridges.contains(from)
147        {
148            self.push_err(Diagnostic {
149                span,
150                code: "rhdl::E0220".into(),
151                en: format!(
152                    "illegal clock-domain crossing '{from}'(D{src_dom}) → '{to}'(D{dst_dom}); use DoubleFlop/SyncFIFO"
153                ),
154                zh: format!(
155                    "非法跨时钟域:'{from}'(D{src_dom}) → '{to}'(D{dst_dom});请用 DoubleFlop/SyncFIFO"
156                ),
157            });
158            return true;
159        }
160        false
161    }
162
163    fn record_width(&mut self, name: &str, ty: &GroundType) {
164        let w = match ty {
165            GroundType::UInt { width } | GroundType::SInt { width } => *width,
166            GroundType::Clock | GroundType::Reset | GroundType::Bool | GroundType::Analog => 1,
167        };
168        self.widths.insert(name.to_string(), w);
169    }
170
171    /// Fail-before-emit when a flattened leaf / port name collides (FR51).
172    fn ensure_fresh_signal_name(&mut self, name: &str, span: Span) -> bool {
173        if self.signals.contains_key(name) {
174            self.push_err(Diagnostic {
175                span,
176                code: "rhdl::E0152".into(),
177                en: format!(
178                    "flattened leaf/port name '{name}' collides with an existing signal (rename Bundle members or fields so `{{field}}_{{member}}` / `{{field}}_{{i}}` stay unique)"
179                ),
180                zh: format!(
181                    "展平叶/端口名 '{name}' 与已有信号冲突(请调整 Bundle 成员或字段名,保证 `{{field}}_{{member}}` / `{{field}}_{{i}}` 唯一)"
182                ),
183            });
184            return false;
185        }
186        true
187    }
188
189    pub fn add_input(&mut self, name: impl Into<String>, ty: GroundType, span: Span) {
190        let name = name.into();
191        if !self.ensure_fresh_signal_name(&name, span) {
192            return;
193        }
194        if matches!(ty, GroundType::Clock) {
195            self.clock_port = Some(name.clone());
196        }
197        if matches!(ty, GroundType::Reset) {
198            self.reset_port = Some(name.clone());
199        }
200        self.signals.insert(name.clone(), SignalKind::Input);
201        self.record_width(&name, &ty);
202        if let Some(m) = self.current.as_mut() {
203            m.ports.push(Port {
204                name,
205                direction: PortDirection::Input,
206                ty,
207                span,
208            });
209        }
210    }
211
212    pub fn add_output(&mut self, name: impl Into<String>, ty: GroundType, span: Span) {
213        let name = name.into();
214        if !self.ensure_fresh_signal_name(&name, span) {
215            return;
216        }
217        self.signals.insert(name.clone(), SignalKind::Output);
218        self.record_width(&name, &ty);
219        if let Some(m) = self.current.as_mut() {
220            m.ports.push(Port {
221                name,
222                direction: PortDirection::Output,
223                ty,
224                span,
225            });
226        }
227    }
228
229    /// Top-level InOut / Analog IO (FR27). Non-top uses are rejected at freeze.
230    pub fn add_inout(&mut self, name: impl Into<String>, ty: GroundType, span: Span) {
231        let name = name.into();
232        if !self.ensure_fresh_signal_name(&name, span) {
233            return;
234        }
235        self.signals.insert(name.clone(), SignalKind::Wire);
236        self.record_width(&name, &ty);
237        if let Some(m) = self.current.as_mut() {
238            m.ports.push(Port {
239                name,
240                direction: PortDirection::InOut,
241                ty,
242                span,
243            });
244        }
245    }
246
247    pub fn declare_wire(&mut self, name: impl Into<String>, ty: GroundType, span: Span) {
248        let name = name.into();
249        self.signals.insert(name.clone(), SignalKind::Wire);
250        self.record_width(&name, &ty);
251        if let Some(m) = self.current.as_mut() {
252            m.body.push(Stmt::WireDecl { name, ty, span });
253        }
254    }
255
256    pub fn declare_reg(&mut self, name: impl Into<String>, ty: GroundType, span: Span) {
257        let name = name.into();
258        let (Some(clock), Some(reset)) = (self.clock_port.clone(), self.reset_port.clone()) else {
259            self.push_err(Diagnostic {
260                span,
261                code: "rhdl::E0124".into(),
262                en: "cannot declare Reg before Clock and Reset ports are declared".into(),
263                zh: "声明寄存器前必须先有 Clock 与 Reset 端口".into(),
264            });
265            return;
266        };
267        self.signals.insert(name.clone(), SignalKind::Reg);
268        self.record_width(&name, &ty);
269        if let Some(m) = self.current.as_mut() {
270            m.body.push(Stmt::RegDecl {
271                name,
272                ty,
273                clock,
274                reset,
275                async_reset: false,
276                has_enable: false,
277                span,
278            });
279        }
280    }
281
282    /// Declare a register with optional async reset / clock enable (AD-23).
283    pub fn declare_reg_ex(
284        &mut self,
285        name: impl Into<String>,
286        ty: GroundType,
287        async_reset: bool,
288        has_enable: bool,
289        span: Span,
290    ) {
291        let name = name.into();
292        let (Some(clock), Some(reset)) = (self.clock_port.clone(), self.reset_port.clone()) else {
293            self.push_err(Diagnostic {
294                span,
295                code: "rhdl::E0124".into(),
296                en: "cannot declare Reg before Clock and Reset ports are declared".into(),
297                zh: "声明寄存器前必须先有 Clock 与 Reset 端口".into(),
298            });
299            return;
300        };
301        self.signals.insert(name.clone(), SignalKind::Reg);
302        self.record_width(&name, &ty);
303        if let Some(m) = self.current.as_mut() {
304            m.body.push(Stmt::RegDecl {
305                name,
306                ty,
307                clock,
308                reset,
309                async_reset,
310                has_enable,
311                span,
312            });
313        }
314    }
315
316    /// Declare SyncReadMem (CHIRRTL-friendly; sync_read=true).
317    pub fn declare_sync_read_mem(
318        &mut self,
319        name: impl Into<String>,
320        depth: u32,
321        width: u32,
322        span: Span,
323    ) {
324        self.declare_mem_inner(name, depth, width, true, None, span);
325    }
326
327    /// Declare Mem (async-read / reg-file style; sync_read=false).
328    pub fn declare_mem(&mut self, name: impl Into<String>, depth: u32, width: u32, span: Span) {
329        self.declare_mem_inner(name, depth, width, false, None, span);
330    }
331
332    /// Declare Mem with elaborate-time init words (FR73). Prefer
333    /// [`Self::declare_mem_with_init_fn`] for generator closures.
334    pub fn declare_mem_with_init(
335        &mut self,
336        name: impl Into<String>,
337        depth: u32,
338        width: u32,
339        init: Vec<u64>,
340        span: Span,
341    ) {
342        self.declare_mem_inner(name, depth, width, false, Some(init), span);
343    }
344
345    /// Declare SyncReadMem with elaborate-time init words (FR73).
346    pub fn declare_sync_read_mem_with_init(
347        &mut self,
348        name: impl Into<String>,
349        depth: u32,
350        width: u32,
351        init: Vec<u64>,
352        span: Span,
353    ) {
354        self.declare_mem_inner(name, depth, width, true, Some(init), span);
355    }
356
357    /// Elaborate-time Mem init generator (FR73 / AD-18).
358    ///
359    /// Runs `f(addr)` for each address inside the session, masks to `width` bits,
360    /// and stores plain `Vec<u64>` on the HIR mem node. The closure does **not**
361    /// enter FrozenHir (NFR36). Only **non-capturing** `Fn` is legal; capturing
362    /// Wire/Reg/signal refs must be rejected via
363    /// [`Self::assert_no_hw_capture`] (`rhdl::E0142`).
364    pub fn declare_mem_with_init_fn<F>(
365        &mut self,
366        name: impl Into<String>,
367        depth: u32,
368        width: u32,
369        f: F,
370        span: Span,
371    ) where
372        F: Fn(usize) -> u64,
373    {
374        let init = generate_mem_init_words(depth, width, f);
375        self.declare_mem_inner(name, depth, width, false, Some(init), span);
376    }
377
378    /// SyncReadMem variant of [`Self::declare_mem_with_init_fn`] (FR73).
379    pub fn declare_sync_read_mem_with_init_fn<F>(
380        &mut self,
381        name: impl Into<String>,
382        depth: u32,
383        width: u32,
384        f: F,
385        span: Span,
386    ) where
387        F: Fn(usize) -> u64,
388    {
389        let init = generate_mem_init_words(depth, width, f);
390        self.declare_mem_inner(name, depth, width, true, Some(init), span);
391    }
392
393    fn declare_mem_inner(
394        &mut self,
395        name: impl Into<String>,
396        depth: u32,
397        width: u32,
398        sync_read: bool,
399        init: Option<Vec<u64>>,
400        span: Span,
401    ) {
402        let name = name.into();
403        if depth == 0 || width == 0 {
404            self.push_err(Diagnostic {
405                span,
406                code: "rhdl::E0210".into(),
407                en: "Mem depth and width must be non-zero".into(),
408                zh: "Mem 的 depth 与 width 必须非零".into(),
409            });
410            return;
411        }
412        let init = match init {
413            None => None,
414            Some(words) => {
415                if width > 64 {
416                    self.push_err(Diagnostic {
417                        span,
418                        code: "rhdl::E0211".into(),
419                        en: "Mem init path supports width ≤ 64 for this MVP".into(),
420                        zh: "本 MVP 的 Mem 初值路径仅支持 width ≤ 64".into(),
421                    });
422                    return;
423                }
424                if words.len() != depth as usize {
425                    self.push_err(Diagnostic {
426                        span,
427                        code: "rhdl::E0212".into(),
428                        en: format!(
429                            "Mem init length {} does not match depth {depth}",
430                            words.len()
431                        ),
432                        zh: format!("Mem 初值长度 {} 与 depth {depth} 不一致", words.len()),
433                    });
434                    return;
435                }
436                Some(words.into_iter().map(|w| mask_mem_word(w, width)).collect())
437            }
438        };
439        self.signals.insert(name.clone(), SignalKind::Wire);
440        self.widths.insert(name.clone(), width);
441        if let Some(m) = self.current.as_mut() {
442            m.body.push(Stmt::MemDecl {
443                name,
444                depth,
445                width,
446                sync_read,
447                init,
448                span,
449            });
450        }
451    }
452
453    /// Same-width binary add. Returns Err diagnostic via session if widths differ.
454    pub fn check_add(&mut self, lhs: &str, rhs: &str, span: Span) -> Option<u32> {
455        let lw = self.widths.get(lhs).copied();
456        let rw = self.widths.get(rhs).copied();
457        match (lw, rw) {
458            (Some(a), Some(b)) if a == b => Some(a),
459            (Some(a), Some(b)) => {
460                self.push_err(Diagnostic {
461                    span,
462                    code: "rhdl::E0130".into(),
463                    en: format!(
464                        "add requires same width; '{lhs}' is {a}, '{rhs}' is {b} (use pad/trunc)"
465                    ),
466                    zh: format!("加法要求同位宽;'{lhs}' 为 {a},'{rhs}' 为 {b}(请用 pad/trunc)"),
467                });
468                None
469            }
470            _ => {
471                self.push_err(Diagnostic {
472                    span,
473                    code: "rhdl::E0113".into(),
474                    en: format!("unknown signal in add ('{lhs}', '{rhs}')"),
475                    zh: format!("加法中有未知信号('{lhs}', '{rhs}')"),
476                });
477                None
478            }
479        }
480    }
481
482    pub fn check_connect(&mut self, lhs: &str, rhs: &str, span: Span) -> Option<u32> {
483        let lw = self.widths.get(lhs).copied();
484        let rw = self.widths.get(rhs).copied();
485        match (lw, rw) {
486            (Some(a), Some(b)) if a == b => Some(a),
487            (Some(a), Some(b)) => {
488                self.push_err(Diagnostic {
489                    span,
490                    code: "rhdl::E0131".into(),
491                    en: format!(
492                        "connect requires same width; '{lhs}' is {a}, '{rhs}' is {b} (use pad/trunc)"
493                    ),
494                    zh: format!(
495                        "连接要求同位宽;'{lhs}' 为 {a},'{rhs}' 为 {b}(请用 pad/trunc)"
496                    ),
497                });
498                None
499            }
500            _ => {
501                self.push_err(Diagnostic {
502                    span,
503                    code: "rhdl::E0113".into(),
504                    en: format!("unknown signal in connect ('{lhs}', '{rhs}')"),
505                    zh: format!("连接中有未知信号('{lhs}', '{rhs}')"),
506                });
507                None
508            }
509        }
510    }
511
512    /// Explicit zero-extend / sign-pad to a wider width; records result as a temp wire name.
513    pub fn pad_to(
514        &mut self,
515        src: &str,
516        to_width: u32,
517        dest: impl Into<String>,
518        span: Span,
519    ) -> bool {
520        let Some(from) = self.widths.get(src).copied() else {
521            self.push_err(Diagnostic {
522                span,
523                code: "rhdl::E0113".into(),
524                en: format!("unknown signal '{src}' in pad"),
525                zh: format!("pad 中未知信号 '{src}'"),
526            });
527            return false;
528        };
529        if to_width <= from {
530            self.push_err(Diagnostic {
531                span,
532                code: "rhdl::E0132".into(),
533                en: format!("pad requires to_width > from_width ({to_width} <= {from})"),
534                zh: format!("pad 要求目标位宽大于源位宽({to_width} <= {from})"),
535            });
536            return false;
537        }
538        let dest = dest.into();
539        self.declare_wire(dest, GroundType::UInt { width: to_width }, span);
540        let _ = bitloom_hir::Expr::Pad {
541            from_width: from,
542            to_width,
543            span,
544        };
545        true
546    }
547
548    pub fn trunc_to(
549        &mut self,
550        src: &str,
551        to_width: u32,
552        dest: impl Into<String>,
553        span: Span,
554    ) -> bool {
555        let Some(from) = self.widths.get(src).copied() else {
556            self.push_err(Diagnostic {
557                span,
558                code: "rhdl::E0113".into(),
559                en: format!("unknown signal '{src}' in trunc"),
560                zh: format!("trunc 中未知信号 '{src}'"),
561            });
562            return false;
563        };
564        if to_width >= from {
565            self.push_err(Diagnostic {
566                span,
567                code: "rhdl::E0133".into(),
568                en: format!("trunc requires to_width < from_width ({to_width} >= {from})"),
569                zh: format!("trunc 要求目标位宽小于源位宽({to_width} >= {from})"),
570            });
571            return false;
572        }
573        let dest = dest.into();
574        self.declare_wire(dest, GroundType::UInt { width: to_width }, span);
575        let _ = bitloom_hir::Expr::Trunc {
576            from_width: from,
577            to_width,
578            span,
579        };
580        true
581    }
582
583    pub fn begin_combinational(&mut self, span: Span) {
584        if self.process.is_some() {
585            self.push_err(Diagnostic {
586                span,
587                code: "rhdl::E0101".into(),
588                en: "nested processes are not allowed".into(),
589                zh: "不允许嵌套硬件过程".into(),
590            });
591            return;
592        }
593        self.process = Some(ProcessState::Combinational {
594            assigns: Vec::new(),
595            path_assigned: vec![HashSet::new()],
596            pending_branches: Vec::new(),
597            span,
598        });
599    }
600
601    pub fn begin_sequential(&mut self, span: Span) {
602        if self.process.is_some() {
603            self.push_err(Diagnostic {
604                span,
605                code: "rhdl::E0101".into(),
606                en: "nested processes are not allowed".into(),
607                zh: "不允许嵌套硬件过程".into(),
608            });
609            return;
610        }
611        self.process = Some(ProcessState::Sequential {
612            assigns: Vec::new(),
613            span,
614        });
615    }
616
617    /// Start an if-branch inside a combinational process (latch analysis).
618    pub fn begin_then(&mut self, span: Span) {
619        let err = match self.process.as_mut() {
620            Some(ProcessState::Combinational {
621                path_assigned,
622                pending_branches,
623                ..
624            }) => {
625                pending_branches.push((HashSet::new(), false));
626                path_assigned.push(HashSet::new());
627                None
628            }
629            Some(ProcessState::Sequential { .. }) => Some(Diagnostic {
630                span,
631                code: "rhdl::E0102".into(),
632                en: "branch tracking for latch checks is only valid in combinational processes"
633                    .into(),
634                zh: "仅组合过程支持 if/else 赋值完整性检查".into(),
635            }),
636            None => Some(Diagnostic {
637                span,
638                code: "rhdl::E0103".into(),
639                en: "assignment control outside a marked combinational/sequential process".into(),
640                zh: "在未标注的 comb/seq 过程外使用分支".into(),
641            }),
642        };
643        if let Some(d) = err {
644            self.push_err(d);
645        }
646    }
647
648    pub fn begin_else(&mut self, span: Span) {
649        let err = match self.process.as_mut() {
650            Some(ProcessState::Combinational {
651                path_assigned,
652                pending_branches,
653                ..
654            }) => {
655                let then_set = path_assigned.pop().unwrap_or_default();
656                if let Some(last) = pending_branches.last_mut() {
657                    last.0 = then_set;
658                    last.1 = true;
659                    path_assigned.push(HashSet::new());
660                    None
661                } else {
662                    Some(Diagnostic {
663                        span,
664                        code: "rhdl::E0102".into(),
665                        en: "else without an open combinational then-branch".into(),
666                        zh: "else 没有对应的组合 then 分支".into(),
667                    })
668                }
669            }
670            _ => Some(Diagnostic {
671                span,
672                code: "rhdl::E0102".into(),
673                en: "else without an open combinational then-branch".into(),
674                zh: "else 没有对应的组合 then 分支".into(),
675            }),
676        };
677        if let Some(d) = err {
678            self.push_err(d);
679        }
680    }
681
682    pub fn end_if(&mut self, span: Span) {
683        let mut latch_errs = Vec::new();
684        let err = match self.process.as_mut() {
685            Some(ProcessState::Combinational {
686                path_assigned,
687                pending_branches,
688                ..
689            }) => {
690                let current = path_assigned.pop().unwrap_or_default();
691                let Some((stored_then, had_else)) = pending_branches.pop() else {
692                    latch_errs.push(Diagnostic {
693                        span,
694                        code: "rhdl::E0102".into(),
695                        en: "end_if without begin_then".into(),
696                        zh: "end_if 缺少 begin_then".into(),
697                    });
698                    for d in latch_errs {
699                        self.push_err(d);
700                    }
701                    return;
702                };
703
704                let (then_set, else_set) = if had_else {
705                    (stored_then, current)
706                } else {
707                    (current, HashSet::new())
708                };
709
710                let union: HashSet<_> = then_set.union(&else_set).cloned().collect();
711                let inter: HashSet<_> = then_set.intersection(&else_set).cloned().collect();
712                for name in union.difference(&inter) {
713                    latch_errs.push(Diagnostic {
714                        span,
715                        code: "rhdl::E0110".into(),
716                        en: format!(
717                            "incomplete combinational assignment to '{name}' (would infer a latch)"
718                        ),
719                        zh: format!("组合赋值不完整:'{name}'(会推断成 latch)"),
720                    });
721                }
722
723                if let Some(parent) = path_assigned.last_mut() {
724                    for n in inter {
725                        parent.insert(n);
726                    }
727                }
728                None
729            }
730            _ => Some(Diagnostic {
731                span,
732                code: "rhdl::E0102".into(),
733                en: "end_if outside combinational process".into(),
734                zh: "end_if 不在组合过程中".into(),
735            }),
736        };
737        for d in latch_errs {
738            self.push_err(d);
739        }
740        if let Some(d) = err {
741            self.push_err(d);
742        }
743    }
744
745    /// Combinational assign `dst = lhs + rhs` (same-width required).
746    pub fn assign_add(
747        &mut self,
748        dst: impl Into<String>,
749        lhs: impl Into<String>,
750        rhs: impl Into<String>,
751        span: Span,
752    ) {
753        let dst = dst.into();
754        let lhs = lhs.into();
755        let rhs = rhs.into();
756        if self.check_add(&lhs, &rhs, span).is_none() {
757            return;
758        }
759        // Reuse assign_net permission checks by temporarily pushing Add.
760        let kind = self.signals.get(&dst).copied();
761        let process_kind = match &self.process {
762            Some(ProcessState::Combinational { .. }) => Some(ProcessKind::Combinational),
763            Some(ProcessState::Sequential { .. }) => Some(ProcessKind::Sequential),
764            None => None,
765        };
766        match process_kind {
767            Some(ProcessKind::Combinational) => {
768                match kind {
769                    Some(SignalKind::Wire | SignalKind::Output) => {}
770                    Some(SignalKind::Reg) => {
771                        self.push_err(Diagnostic {
772                            span,
773                            code: "rhdl::E0111".into(),
774                            en: format!("combinational process must not drive Reg '{dst}'"),
775                            zh: format!("组合过程不能驱动寄存器 '{dst}'"),
776                        });
777                        return;
778                    }
779                    Some(SignalKind::Input) => {
780                        self.push_err(Diagnostic {
781                            span,
782                            code: "rhdl::E0112".into(),
783                            en: format!("cannot assign to input port '{dst}'"),
784                            zh: format!("不能给输入端口 '{dst}' 赋值"),
785                        });
786                        return;
787                    }
788                    None => {
789                        self.push_err(Diagnostic {
790                            span,
791                            code: "rhdl::E0113".into(),
792                            en: format!("unknown signal '{dst}'"),
793                            zh: format!("未知信号 '{dst}'"),
794                        });
795                        return;
796                    }
797                }
798                if let Some(ProcessState::Combinational {
799                    assigns,
800                    path_assigned,
801                    ..
802                }) = self.process.as_mut()
803                {
804                    assigns.push(Assign {
805                        target: AssignTarget::Net(dst.clone()),
806                        expr: AssignExpr::Add(lhs, rhs),
807                        span,
808                    });
809                    if let Some(path) = path_assigned.last_mut() {
810                        path.insert(dst);
811                    }
812                }
813            }
814            Some(ProcessKind::Sequential) => {
815                self.push_err(Diagnostic {
816                    span,
817                    code: "rhdl::E0114".into(),
818                    en: format!("sequential process must not drive combinational net '{dst}'"),
819                    zh: format!("时序过程不能驱动组合网 '{dst}'"),
820                });
821            }
822            None => {
823                self.push_err(Diagnostic {
824                    span,
825                    code: "rhdl::E0103".into(),
826                    en: "assignment outside a marked combinational/sequential process".into(),
827                    zh: "在未标注的 comb/seq 过程外赋值".into(),
828                });
829            }
830        }
831    }
832
833    /// Combinational assign `dst = lit`.
834    pub fn assign_lit(&mut self, dst: impl Into<String>, lit: u64, span: Span) {
835        self.push_comb_net_expr(dst.into(), AssignExpr::Lit(lit), span);
836    }
837
838    /// Combinational assign `dst = (lhs == rhs)` (0/1).
839    pub fn assign_eq(
840        &mut self,
841        dst: impl Into<String>,
842        lhs: impl Into<String>,
843        rhs: impl Into<String>,
844        span: Span,
845    ) {
846        self.push_comb_net_expr(dst.into(), AssignExpr::Eq(lhs.into(), rhs.into()), span);
847    }
848
849    /// Combinational assign `dst = sel ? t : f` (`sel != 0` is true).
850    pub fn assign_mux(
851        &mut self,
852        dst: impl Into<String>,
853        sel: impl Into<String>,
854        t: impl Into<String>,
855        f: impl Into<String>,
856        span: Span,
857    ) {
858        self.push_comb_net_expr(
859            dst.into(),
860            AssignExpr::Mux {
861                sel: sel.into(),
862                t: t.into(),
863                f: f.into(),
864            },
865            span,
866        );
867    }
868
869    /// Combinational `dst = lhs - rhs`.
870    pub fn assign_sub(
871        &mut self,
872        dst: impl Into<String>,
873        lhs: impl Into<String>,
874        rhs: impl Into<String>,
875        span: Span,
876    ) {
877        self.push_comb_net_expr(dst.into(), AssignExpr::Sub(lhs.into(), rhs.into()), span);
878    }
879
880    /// Combinational `dst = lhs & rhs`.
881    pub fn assign_and(
882        &mut self,
883        dst: impl Into<String>,
884        lhs: impl Into<String>,
885        rhs: impl Into<String>,
886        span: Span,
887    ) {
888        self.push_comb_net_expr(dst.into(), AssignExpr::And(lhs.into(), rhs.into()), span);
889    }
890
891    /// Combinational `dst = lhs | rhs`.
892    pub fn assign_or(
893        &mut self,
894        dst: impl Into<String>,
895        lhs: impl Into<String>,
896        rhs: impl Into<String>,
897        span: Span,
898    ) {
899        self.push_comb_net_expr(dst.into(), AssignExpr::Or(lhs.into(), rhs.into()), span);
900    }
901
902    /// Combinational `dst = lhs ^ rhs`.
903    pub fn assign_xor(
904        &mut self,
905        dst: impl Into<String>,
906        lhs: impl Into<String>,
907        rhs: impl Into<String>,
908        span: Span,
909    ) {
910        self.push_comb_net_expr(dst.into(), AssignExpr::Xor(lhs.into(), rhs.into()), span);
911    }
912
913    /// Combinational `dst = lhs << (rhs & 63)`.
914    pub fn assign_shl(
915        &mut self,
916        dst: impl Into<String>,
917        lhs: impl Into<String>,
918        rhs: impl Into<String>,
919        span: Span,
920    ) {
921        self.push_comb_net_expr(dst.into(), AssignExpr::Shl(lhs.into(), rhs.into()), span);
922    }
923
924    /// Combinational `dst = lhs >> (rhs & 63)` (logical).
925    pub fn assign_shr(
926        &mut self,
927        dst: impl Into<String>,
928        lhs: impl Into<String>,
929        rhs: impl Into<String>,
930        span: Span,
931    ) {
932        self.push_comb_net_expr(dst.into(), AssignExpr::Shr(lhs.into(), rhs.into()), span);
933    }
934
935    fn push_comb_net_expr(&mut self, dst: String, expr: AssignExpr, span: Span) {
936        let kind = self.signals.get(&dst).copied();
937        let process_kind = match &self.process {
938            Some(ProcessState::Combinational { .. }) => Some(ProcessKind::Combinational),
939            Some(ProcessState::Sequential { .. }) => Some(ProcessKind::Sequential),
940            None => None,
941        };
942        match process_kind {
943            Some(ProcessKind::Combinational) => {
944                match kind {
945                    Some(SignalKind::Wire | SignalKind::Output) => {}
946                    Some(SignalKind::Reg) => {
947                        self.push_err(Diagnostic {
948                            span,
949                            code: "rhdl::E0111".into(),
950                            en: format!("combinational process must not drive Reg '{dst}'"),
951                            zh: format!("组合过程不能驱动寄存器 '{dst}'"),
952                        });
953                        return;
954                    }
955                    Some(SignalKind::Input) => {
956                        self.push_err(Diagnostic {
957                            span,
958                            code: "rhdl::E0112".into(),
959                            en: format!("cannot assign to input port '{dst}'"),
960                            zh: format!("不能给输入端口 '{dst}' 赋值"),
961                        });
962                        return;
963                    }
964                    None => {
965                        self.push_err(Diagnostic {
966                            span,
967                            code: "rhdl::E0113".into(),
968                            en: format!("unknown signal '{dst}'"),
969                            zh: format!("未知信号 '{dst}'"),
970                        });
971                        return;
972                    }
973                }
974                if let Some(ProcessState::Combinational {
975                    assigns,
976                    path_assigned,
977                    ..
978                }) = self.process.as_mut()
979                {
980                    assigns.push(Assign {
981                        target: AssignTarget::Net(dst.clone()),
982                        expr,
983                        span,
984                    });
985                    if let Some(path) = path_assigned.last_mut() {
986                        path.insert(dst);
987                    }
988                }
989            }
990            Some(ProcessKind::Sequential) => {
991                self.push_err(Diagnostic {
992                    span,
993                    code: "rhdl::E0114".into(),
994                    en: format!("sequential process must not drive combinational net '{dst}'"),
995                    zh: format!("时序过程不能驱动组合网 '{dst}'"),
996                });
997            }
998            None => {
999                self.push_err(Diagnostic {
1000                    span,
1001                    code: "rhdl::E0103".into(),
1002                    en: "assignment outside a marked combinational/sequential process".into(),
1003                    zh: "在未标注的 comb/seq 过程外赋值".into(),
1004                });
1005            }
1006        }
1007    }
1008
1009    /// Assign a combinational net / output / wire from `from`.
1010    pub fn assign_net(&mut self, name: impl Into<String>, from: impl Into<String>, span: Span) {
1011        let name = name.into();
1012        let from = from.into();
1013        if self.reject_illegal_cdc(&from, &name, span) {
1014            return;
1015        }
1016        // Width gate (E0131) before emit — FR51 / FR22 same-width connects.
1017        if self.check_connect(&name, &from, span).is_none() {
1018            return;
1019        }
1020        let kind = self.signals.get(&name).copied();
1021        let process_kind = match &self.process {
1022            Some(ProcessState::Combinational { .. }) => Some(ProcessKind::Combinational),
1023            Some(ProcessState::Sequential { .. }) => Some(ProcessKind::Sequential),
1024            None => None,
1025        };
1026
1027        match process_kind {
1028            Some(ProcessKind::Combinational) => {
1029                match kind {
1030                    Some(SignalKind::Reg) => {
1031                        self.push_err(Diagnostic {
1032                            span,
1033                            code: "rhdl::E0111".into(),
1034                            en: format!(
1035                                "combinational process must not drive Reg '{name}' (use Reg.d in sequential)"
1036                            ),
1037                            zh: format!(
1038                                "组合过程不能驱动寄存器 '{name}'(请在时序过程写 Reg.d)"
1039                            ),
1040                        });
1041                        return;
1042                    }
1043                    Some(SignalKind::Input) => {
1044                        self.push_err(Diagnostic {
1045                            span,
1046                            code: "rhdl::E0112".into(),
1047                            en: format!("cannot assign to input port '{name}'"),
1048                            zh: format!("不能给输入端口 '{name}' 赋值"),
1049                        });
1050                        return;
1051                    }
1052                    Some(SignalKind::Wire | SignalKind::Output) => {}
1053                    None => {
1054                        self.push_err(Diagnostic {
1055                            span,
1056                            code: "rhdl::E0113".into(),
1057                            en: format!("unknown signal '{name}'"),
1058                            zh: format!("未知信号 '{name}'"),
1059                        });
1060                        return;
1061                    }
1062                }
1063                if let Some(ProcessState::Combinational {
1064                    assigns,
1065                    path_assigned,
1066                    ..
1067                }) = self.process.as_mut()
1068                {
1069                    assigns.push(Assign {
1070                        target: AssignTarget::Net(name.clone()),
1071                        expr: AssignExpr::Ref(from.clone()),
1072                        span,
1073                    });
1074                    if let Some(path) = path_assigned.last_mut() {
1075                        path.insert(name);
1076                    }
1077                }
1078            }
1079            Some(ProcessKind::Sequential) => {
1080                self.push_err(Diagnostic {
1081                    span,
1082                    code: "rhdl::E0114".into(),
1083                    en: format!("sequential process must not drive combinational net '{name}'"),
1084                    zh: format!("时序过程不能驱动组合网 '{name}'"),
1085                });
1086            }
1087            None => {
1088                self.push_err(Diagnostic {
1089                    span,
1090                    code: "rhdl::E0103".into(),
1091                    en: "assignment outside a marked combinational/sequential process".into(),
1092                    zh: "在未标注的 comb/seq 过程外赋值".into(),
1093                });
1094            }
1095        }
1096    }
1097
1098    /// Assign `Reg.d` next-state as `name + 1` (wrapping).
1099    pub fn assign_reg_d_inc(&mut self, name: impl Into<String>, span: Span) {
1100        self.assign_reg_d_expr(name, None, span);
1101    }
1102
1103    /// Assign `Reg.d` from another signal.
1104    pub fn assign_reg_d_from(
1105        &mut self,
1106        name: impl Into<String>,
1107        from: impl Into<String>,
1108        span: Span,
1109    ) {
1110        self.assign_reg_d_expr(name, Some(from.into()), span);
1111    }
1112
1113    /// Assign `Reg.d` as a 2:1 mux (same-width `t`/`f` as the register).
1114    ///
1115    /// Used for handwritten seq goldens alongside [`Self::inline_seq_fn`].
1116    pub fn assign_reg_d_mux(
1117        &mut self,
1118        name: impl Into<String>,
1119        sel: impl Into<String>,
1120        t: impl Into<String>,
1121        f: impl Into<String>,
1122        span: Span,
1123    ) {
1124        let name = name.into();
1125        let t = t.into();
1126        let f = f.into();
1127        if self.check_connect(&name, &t, span).is_none()
1128            || self.check_connect(&name, &f, span).is_none()
1129        {
1130            return;
1131        }
1132        self.push_reg_d_assign(
1133            name,
1134            AssignExpr::Mux {
1135                sel: sel.into(),
1136                t,
1137                f,
1138            },
1139            span,
1140        );
1141    }
1142
1143    /// Sequential SyncReadMem / Mem write: `mem[addr] <= data` (always enabled).
1144    pub fn assign_mem_write(
1145        &mut self,
1146        mem: impl Into<String>,
1147        addr: impl Into<String>,
1148        data: impl Into<String>,
1149        span: Span,
1150    ) {
1151        self.assign_mem_write_inner(mem.into(), addr.into(), data.into(), None, span);
1152    }
1153
1154    /// Sequential mem write gated by `we` (`we != 0`).
1155    pub fn assign_mem_write_en(
1156        &mut self,
1157        mem: impl Into<String>,
1158        addr: impl Into<String>,
1159        data: impl Into<String>,
1160        we: impl Into<String>,
1161        span: Span,
1162    ) {
1163        self.assign_mem_write_inner(mem.into(), addr.into(), data.into(), Some(we.into()), span);
1164    }
1165
1166    fn assign_mem_write_inner(
1167        &mut self,
1168        mem: String,
1169        addr: String,
1170        data: String,
1171        we: Option<String>,
1172        span: Span,
1173    ) {
1174        match &self.process {
1175            Some(ProcessState::Sequential { .. }) => {
1176                if let Some(ProcessState::Sequential { assigns, .. }) = self.process.as_mut() {
1177                    assigns.push(Assign {
1178                        target: AssignTarget::MemWrite { mem, addr, we },
1179                        expr: AssignExpr::Ref(data),
1180                        span,
1181                    });
1182                }
1183            }
1184            _ => {
1185                self.push_err(Diagnostic {
1186                    span,
1187                    code: "rhdl::E0211".into(),
1188                    en: "mem write must be inside a sequential process".into(),
1189                    zh: "mem 写必须在 sequential 过程内".into(),
1190                });
1191            }
1192        }
1193    }
1194
1195    /// Sequential SyncReadMem read into a register (latency 1 on tick).
1196    pub fn assign_reg_d_mem_read(
1197        &mut self,
1198        reg: impl Into<String>,
1199        mem: impl Into<String>,
1200        addr: impl Into<String>,
1201        span: Span,
1202    ) {
1203        let reg = reg.into();
1204        let mem = mem.into();
1205        let addr = addr.into();
1206        match &self.process {
1207            Some(ProcessState::Sequential { .. }) => {
1208                if let Some(ProcessState::Sequential { assigns, .. }) = self.process.as_mut() {
1209                    assigns.push(Assign {
1210                        target: AssignTarget::RegD(reg),
1211                        expr: AssignExpr::MemRead { mem, addr },
1212                        span,
1213                    });
1214                }
1215            }
1216            _ => {
1217                self.push_err(Diagnostic {
1218                    span,
1219                    code: "rhdl::E0212".into(),
1220                    en: "sync mem read into Reg must be inside a sequential process".into(),
1221                    zh: "SyncReadMem 读入寄存器必须在 sequential 过程内".into(),
1222                });
1223            }
1224        }
1225    }
1226
1227    fn assign_reg_d_expr(&mut self, name: impl Into<String>, from: Option<String>, span: Span) {
1228        let name = name.into();
1229        if let Some(ref src) = from {
1230            if self.reject_illegal_cdc(src, &name, span) {
1231                return;
1232            }
1233            if self.check_connect(&name, src, span).is_none() {
1234                return;
1235            }
1236        }
1237        let expr = match from {
1238            Some(src) => AssignExpr::Ref(src),
1239            None => AssignExpr::Inc(name.clone()),
1240        };
1241        self.push_reg_d_assign(name, expr, span);
1242    }
1243
1244    pub fn end_process(&mut self) {
1245        let Some(state) = self.process.take() else {
1246            return;
1247        };
1248        match state {
1249            ProcessState::Combinational {
1250                assigns,
1251                pending_branches,
1252                span,
1253                ..
1254            } => {
1255                if !pending_branches.is_empty() {
1256                    self.push_err(Diagnostic {
1257                        span,
1258                        code: "rhdl::E0102".into(),
1259                        en: "unclosed if/else in combinational process".into(),
1260                        zh: "组合过程中有未关闭的 if/else".into(),
1261                    });
1262                }
1263                if let Some(m) = self.current.as_mut() {
1264                    m.body.push(Stmt::Process(Process {
1265                        kind: ProcessKind::Combinational,
1266                        assigns,
1267                        span,
1268                    }));
1269                }
1270            }
1271            ProcessState::Sequential { assigns, span } => {
1272                if let Some(m) = self.current.as_mut() {
1273                    m.body.push(Stmt::Process(Process {
1274                        kind: ProcessKind::Sequential,
1275                        assigns,
1276                        span,
1277                    }));
1278                }
1279            }
1280        }
1281    }
1282
1283    pub fn end_module(&mut self) {
1284        if self.process.is_some() {
1285            self.end_process();
1286        }
1287        if let Some(m) = self.current.take() {
1288            self.hir.add_module(m);
1289        }
1290        self.signals.clear();
1291        self.widths.clear();
1292        self.clock_port = None;
1293        self.reset_port = None;
1294    }
1295
1296    pub fn finish(self) -> Result<FrozenHir, Diagnostics> {
1297        if !self.errors.is_empty() {
1298            return Err(self.errors);
1299        }
1300        bitloom_hir::seal_from_builder(self.hir)
1301    }
1302
1303    /// Record a synthesizable-path violation (heap, threads, f64, capturing closure, …)
1304    /// as a structured diagnostic (`rhdl::E0141` / FR16).
1305    pub fn reject_unsynthesizable(&mut self, construct: &str, span: Span) {
1306        self.push_err(Diagnostic {
1307            span,
1308            code: "rhdl::E0141".into(),
1309            en: format!(
1310                "unsynthesizable construct '{construct}' is not allowed on the cycle-accurate path"
1311            ),
1312            zh: format!("周期精确路径不允许不可综合构造 '{construct}'"),
1313        });
1314    }
1315
1316    /// Reject capturing a hardware Wire/Reg/signal ref into an elaborate-time
1317    /// generator or factory closure (FR73 / NFR35 / AD-18).
1318    ///
1319    /// Stable code: **`rhdl::E0142`**. Distinct from FR16 cycle-accurate
1320    /// [`Self::reject_unsynthesizable`] (`rhdl::E0141`).
1321    pub fn reject_hw_capture(&mut self, capture: &HwCaptureRef, span: Span) {
1322        let kind = capture.kind_label();
1323        self.push_err(Diagnostic {
1324            span,
1325            code: "rhdl::E0142".into(),
1326            en: format!(
1327                "illegal capture of hardware {kind} '{}' into elaborate-time generator closure; \
1328                 only non-capturing Fn that dissolves before freeze is allowed (FR73 / NFR35 / AD-18)",
1329                capture.name
1330            ),
1331            zh: format!(
1332                "不允许将硬件 {kind} '{}' 捕获进 elaborate-time 生成器闭包;\
1333                 仅允许冻前消解的非捕获 Fn(FR73 / NFR35 / AD-18)",
1334                capture.name
1335            ),
1336        });
1337    }
1338
1339    /// Assert that no hardware Wire/Reg/signal refs were captured into a
1340    /// generator / factory context. Empty slice is a no-op (legal non-capturing path).
1341    ///
1342    /// Call when a documented illegal capture is present (ATDD / macros / typed
1343    /// handles). Any token → `rhdl::E0142` at [`Self::finish`].
1344    pub fn assert_no_hw_capture(&mut self, captures: &[HwCaptureRef], span: Span) {
1345        for c in captures {
1346            self.reject_hw_capture(c, span);
1347        }
1348    }
1349
1350    /// Reject a documented SynthesizableClosure constraint breach (FR74 / Cap-R-60).
1351    ///
1352    /// Stable codes: **`rhdl::E0143`** (heap), **`rhdl::E0144`** (runtime capture
1353    /// state), **`rhdl::E0145`** (impure). Does **not** emit closure IR into HIR
1354    /// (NFR36) — diagnostics only. Distinct from E0141 / E0142.
1355    pub fn reject_unsynthesizable_closure(
1356        &mut self,
1357        violation: &SynthesizableClosureViolation,
1358        span: Span,
1359    ) {
1360        for d in diagnose_synthesizable_closure_violations(std::slice::from_ref(violation), span).0
1361        {
1362            self.push_err(d);
1363        }
1364    }
1365
1366    /// Cap-R-60 check hook: record all documented SynthesizableClosure violations.
1367    ///
1368    /// Empty `violations` is a no-op (legal empty/simple closure — paves 28.2).
1369    /// Reachable from design crates via prelude and from future
1370    /// `cargo bitloom check` (or equivalent) wrappers.
1371    pub fn check_synthesizable_closure(
1372        &mut self,
1373        violations: &[SynthesizableClosureViolation],
1374        span: Span,
1375    ) {
1376        for v in violations {
1377            self.reject_unsynthesizable_closure(v, span);
1378        }
1379    }
1380
1381    /// Cap-R-60 helper: run [`SynthesizableClosure::synthesizable_closure_violations`]
1382    /// and record any tokens (empty = pass).
1383    pub fn check_synthesizable_closure_marker<C: SynthesizableClosure>(
1384        &mut self,
1385        closure: &C,
1386        span: Span,
1387    ) {
1388        let vs = closure.synthesizable_closure_violations();
1389        self.check_synthesizable_closure(&vs, span);
1390    }
1391
1392    /// Inline a synthesizable combinational transform (FR75 / Cap-R-55).
1393    ///
1394    /// Must be called inside an open [`Self::begin_combinational`] process
1395    /// (same rules as `assign_*`). Sequence:
1396    /// 1. Cap-R-60 [`Self::check_synthesizable_closure`] on `violations`
1397    /// 2. If any violation token was supplied, **do not** expand (finish fails)
1398    /// 3. Otherwise invoke `f(args)` once and lower [`CombInline`] through
1399    ///    existing Wire/`assign_*` paths — FrozenHir holds only ordinary
1400    ///    [`AssignExpr`] (NFR36; no `Fn` objects after freeze)
1401    ///
1402    /// Incomplete-assign / latch analysis (AD-18) still applies to `dst`.
1403    /// Sequential-block inline: [`Self::inline_seq_fn`] (Cap-R-56 / Cap-R-70).
1404    ///
1405    /// ```ignore
1406    /// session.begin_combinational(span);
1407    /// session.inline_comb_fn("y", &["a", "b"], &[], span, |args| {
1408    ///     CombInline::Add(args[0].into(), args[1].into())
1409    /// });
1410    /// session.end_process();
1411    /// ```
1412    pub fn inline_comb_fn<F>(
1413        &mut self,
1414        dst: impl Into<String>,
1415        args: &[&str],
1416        violations: &[SynthesizableClosureViolation],
1417        span: Span,
1418        f: F,
1419    ) where
1420        F: FnOnce(&[&str]) -> CombInline,
1421    {
1422        self.check_synthesizable_closure(violations, span);
1423        if !violations.is_empty() {
1424            return;
1425        }
1426        let inline = f(args);
1427        self.apply_comb_inline(dst.into(), inline, span);
1428    }
1429
1430    /// Cap-R-55 helper: run marker [`SynthesizableClosure`] check then inline.
1431    pub fn inline_comb_fn_marker<C, F>(
1432        &mut self,
1433        dst: impl Into<String>,
1434        args: &[&str],
1435        marker: &C,
1436        span: Span,
1437        f: F,
1438    ) where
1439        C: SynthesizableClosure,
1440        F: FnOnce(&[&str]) -> CombInline,
1441    {
1442        let vs = marker.synthesizable_closure_violations();
1443        self.inline_comb_fn(dst, args, &vs, span, f);
1444    }
1445
1446    fn apply_comb_inline(&mut self, dst: String, inline: CombInline, span: Span) {
1447        match inline {
1448            CombInline::Ref(src) => self.assign_net(dst, src, span),
1449            CombInline::Lit(v) => self.assign_lit(dst, v, span),
1450            CombInline::Add(l, r) => self.assign_add(dst, l, r, span),
1451            CombInline::Sub(l, r) => self.assign_sub(dst, l, r, span),
1452            CombInline::And(l, r) => self.assign_and(dst, l, r, span),
1453            CombInline::Or(l, r) => self.assign_or(dst, l, r, span),
1454            CombInline::Xor(l, r) => self.assign_xor(dst, l, r, span),
1455            CombInline::Eq(l, r) => self.assign_eq(dst, l, r, span),
1456            CombInline::Mux { sel, t, f } => self.assign_mux(dst, sel, t, f, span),
1457        }
1458    }
1459
1460    /// Cap-R-70: record documented seq-ownership violations (`rhdl::E0146`).
1461    pub fn reject_seq_ownership_violation(
1462        &mut self,
1463        violation: &SeqOwnershipViolation,
1464        span: Span,
1465    ) {
1466        for d in diagnose_seq_ownership_violations(std::slice::from_ref(violation), span).0 {
1467            self.push_err(d);
1468        }
1469    }
1470
1471    /// Cap-R-70 check hook: record all documented seq-ownership violations.
1472    /// Empty `violations` is a no-op.
1473    pub fn check_seq_ownership(&mut self, violations: &[SeqOwnershipViolation], span: Span) {
1474        for v in violations {
1475            self.reject_seq_ownership_violation(v, span);
1476        }
1477    }
1478
1479    /// Inline a synthesizable sequential transform onto `Reg.d` (FR75 / Cap-R-56).
1480    ///
1481    /// Must be called inside an open [`Self::begin_sequential`] process.
1482    /// Sequence:
1483    /// 1. Cap-R-60 [`Self::check_synthesizable_closure`] on `synth_violations`
1484    /// 2. Cap-R-70 [`Self::check_seq_ownership`] on `ownership_violations`
1485    /// 3. Cap-R-70 auto-detect: if `dst_reg.d` is already assigned in this
1486    ///    sequential process, diagnose `rhdl::E0146` and **do not** expand
1487    /// 4. If any Cap-R-60 / Cap-R-70 token was supplied, **do not** expand
1488    /// 5. Otherwise invoke `f(args)` once and lower [`SeqInline`] to ordinary
1489    ///    sequential [`AssignExpr`] / `Reg.d` — FrozenHir holds no `Fn` (NFR36)
1490    ///
1491    /// Cross-process multi-drive / undriven after expand still use AD-4 freeze
1492    /// checks (`rhdl::E0140`, …).
1493    ///
1494    /// ```ignore
1495    /// session.begin_sequential(span);
1496    /// session.inline_seq_fn("count", &[], &[], &[], span, |_args| SeqInline::Inc);
1497    /// session.end_process();
1498    /// ```
1499    pub fn inline_seq_fn<F>(
1500        &mut self,
1501        dst_reg: impl Into<String>,
1502        args: &[&str],
1503        synth_violations: &[SynthesizableClosureViolation],
1504        ownership_violations: &[SeqOwnershipViolation],
1505        span: Span,
1506        f: F,
1507    ) where
1508        F: FnOnce(&[&str]) -> SeqInline,
1509    {
1510        self.check_synthesizable_closure(synth_violations, span);
1511        self.check_seq_ownership(ownership_violations, span);
1512        let dst = dst_reg.into();
1513        let already = self.seq_reg_d_already_assigned(&dst);
1514        if already {
1515            self.reject_seq_ownership_violation(
1516                &SeqOwnershipViolation::illegal_mutable_borrow(format!(
1517                    "Reg.d '{dst}' already assigned in this sequential process"
1518                )),
1519                span,
1520            );
1521        }
1522        if !synth_violations.is_empty() || !ownership_violations.is_empty() || already {
1523            return;
1524        }
1525        let inline = f(args);
1526        self.apply_seq_inline(dst, inline, span);
1527    }
1528
1529    /// Cap-R-56 helper: marker Cap-R-60 check then seq inline (Cap-R-70 ownership args).
1530    pub fn inline_seq_fn_marker<C, F>(
1531        &mut self,
1532        dst_reg: impl Into<String>,
1533        args: &[&str],
1534        marker: &C,
1535        ownership_violations: &[SeqOwnershipViolation],
1536        span: Span,
1537        f: F,
1538    ) where
1539        C: SynthesizableClosure,
1540        F: FnOnce(&[&str]) -> SeqInline,
1541    {
1542        let vs = marker.synthesizable_closure_violations();
1543        self.inline_seq_fn(dst_reg, args, &vs, ownership_violations, span, f);
1544    }
1545
1546    fn seq_reg_d_already_assigned(&self, name: &str) -> bool {
1547        match &self.process {
1548            Some(ProcessState::Sequential { assigns, .. }) => assigns
1549                .iter()
1550                .any(|a| matches!(&a.target, AssignTarget::RegD(n) if n == name)),
1551            _ => false,
1552        }
1553    }
1554
1555    fn apply_seq_inline(&mut self, dst: String, inline: SeqInline, span: Span) {
1556        let expr = match inline {
1557            SeqInline::Inc => AssignExpr::Inc(dst.clone()),
1558            SeqInline::Comb(CombInline::Ref(src)) => {
1559                if self.check_connect(&dst, &src, span).is_none() {
1560                    return;
1561                }
1562                AssignExpr::Ref(src)
1563            }
1564            SeqInline::Comb(CombInline::Lit(v)) => AssignExpr::Lit(v),
1565            SeqInline::Comb(CombInline::Add(l, r)) => {
1566                if self.check_add(&l, &r, span).is_none() {
1567                    return;
1568                }
1569                AssignExpr::Add(l, r)
1570            }
1571            SeqInline::Comb(CombInline::Sub(l, r)) => {
1572                if self.check_add(&l, &r, span).is_none() {
1573                    return;
1574                }
1575                AssignExpr::Sub(l, r)
1576            }
1577            SeqInline::Comb(CombInline::And(l, r)) => {
1578                if self.check_add(&l, &r, span).is_none() {
1579                    return;
1580                }
1581                AssignExpr::And(l, r)
1582            }
1583            SeqInline::Comb(CombInline::Or(l, r)) => {
1584                if self.check_add(&l, &r, span).is_none() {
1585                    return;
1586                }
1587                AssignExpr::Or(l, r)
1588            }
1589            SeqInline::Comb(CombInline::Xor(l, r)) => {
1590                if self.check_add(&l, &r, span).is_none() {
1591                    return;
1592                }
1593                AssignExpr::Xor(l, r)
1594            }
1595            SeqInline::Comb(CombInline::Eq(l, r)) => {
1596                if self.check_add(&l, &r, span).is_none() {
1597                    return;
1598                }
1599                AssignExpr::Eq(l, r)
1600            }
1601            SeqInline::Comb(CombInline::Mux { sel, t, f }) => {
1602                if self.check_connect(&dst, &t, span).is_none()
1603                    || self.check_connect(&dst, &f, span).is_none()
1604                {
1605                    return;
1606                }
1607                AssignExpr::Mux { sel, t, f }
1608            }
1609        };
1610        self.push_reg_d_assign(dst, expr, span);
1611    }
1612
1613    /// Push a sequential `Reg.d` assign (shared by `assign_reg_d_*` and seq inline).
1614    fn push_reg_d_assign(&mut self, name: String, expr: AssignExpr, span: Span) {
1615        let kind = self.signals.get(&name).copied();
1616        let process_kind = match &self.process {
1617            Some(ProcessState::Combinational { .. }) => Some(ProcessKind::Combinational),
1618            Some(ProcessState::Sequential { .. }) => Some(ProcessKind::Sequential),
1619            None => None,
1620        };
1621
1622        match process_kind {
1623            Some(ProcessKind::Sequential) => match kind {
1624                Some(SignalKind::Reg) => {
1625                    if let Some(ProcessState::Sequential { assigns, .. }) = self.process.as_mut() {
1626                        assigns.push(Assign {
1627                            target: AssignTarget::RegD(name),
1628                            expr,
1629                            span,
1630                        });
1631                    }
1632                }
1633                Some(_) => {
1634                    self.push_err(Diagnostic {
1635                        span,
1636                        code: "rhdl::E0115".into(),
1637                        en: format!("'{name}' is not a Reg; Reg.d requires a register"),
1638                        zh: format!("'{name}' 不是寄存器,不能写 Reg.d"),
1639                    });
1640                }
1641                None => {
1642                    self.push_err(Diagnostic {
1643                        span,
1644                        code: "rhdl::E0113".into(),
1645                        en: format!("unknown signal '{name}'"),
1646                        zh: format!("未知信号 '{name}'"),
1647                    });
1648                }
1649            },
1650            Some(ProcessKind::Combinational) => {
1651                self.push_err(Diagnostic {
1652                    span,
1653                    code: "rhdl::E0116".into(),
1654                    en: format!("combinational process must not write Reg.d for '{name}'"),
1655                    zh: format!("组合过程不能写 '{name}' 的 Reg.d"),
1656                });
1657            }
1658            None => {
1659                self.push_err(Diagnostic {
1660                    span,
1661                    code: "rhdl::E0103".into(),
1662                    en: "assignment outside a marked combinational/sequential process".into(),
1663                    zh: "在未标注的 comb/seq 过程外赋值".into(),
1664                });
1665            }
1666        }
1667    }
1668
1669    /// Hierarchical instance (Story 2.2); not flattened at elaborate.
1670    pub fn add_instance(
1671        &mut self,
1672        name: impl Into<String>,
1673        module: impl Into<String>,
1674        connects: Vec<(String, String)>,
1675        params: Vec<(String, u32)>,
1676        span: Span,
1677    ) {
1678        use bitloom_hir::{Instance, PortConnect};
1679        let connects = connects
1680            .into_iter()
1681            .map(|(child_port, parent_net)| PortConnect {
1682                child_port,
1683                parent_net,
1684                span,
1685                dangling: false,
1686            })
1687            .collect();
1688        if let Some(m) = self.current.as_mut() {
1689            m.body.push(Stmt::Instance(Instance {
1690                name: name.into(),
1691                module: module.into(),
1692                connects,
1693                params,
1694                span,
1695            }));
1696        }
1697    }
1698
1699    /// Elaborate-time module factory (FR73 / Cap-R-53).
1700    ///
1701    /// Invokes `f(i, session)` for each `i` in `0..count`. The factory should
1702    /// call [`Self::add_instance`] (and optional wire decls) so each iteration
1703    /// records a child instance plus type-safe port connects. Only ordinary
1704    /// `Stmt::Instance` / `PortConnect` remain after the loop — the closure
1705    /// does **not** enter FrozenHir (NFR36). Width/dir checks still run at
1706    /// `finish` (FR8 / existing instance validation). Capturing Wire/Reg into
1707    /// the factory is illegal — use [`Self::assert_no_hw_capture`] (`rhdl::E0142`).
1708    ///
1709    /// ```ignore
1710    /// session.generate_instances(4, |i, s| {
1711    ///     s.add_instance(
1712    ///         format!("u{i}"),
1713    ///         "Lane",
1714    ///         vec![
1715    ///             ("clk".into(), "clk".into()),
1716    ///             ("rst".into(), "rst".into()),
1717    ///             ("x".into(), format!("x{i}")),
1718    ///             ("y".into(), format!("y{i}")),
1719    ///         ],
1720    ///         vec![],
1721    ///         Span::default(),
1722    ///     );
1723    /// });
1724    /// ```
1725    pub fn generate_instances<F>(&mut self, count: usize, mut f: F)
1726    where
1727        F: FnMut(usize, &mut Self),
1728    {
1729        for i in 0..count {
1730            f(i, self);
1731        }
1732    }
1733
1734    /// Pure-return factory variant (FR73 / Cap-R-53): `f(i)` returns an
1735    /// [`GeneratedInstance`] that is immediately recorded as HIR via
1736    /// [`Self::add_instance`]. Prefer when the factory needs no extra session
1737    /// side effects (wires, dangling marks).
1738    pub fn generate_instances_from<F>(&mut self, count: usize, f: F, span: Span)
1739    where
1740        F: Fn(usize) -> GeneratedInstance,
1741    {
1742        for i in 0..count {
1743            let g = f(i);
1744            self.add_instance(g.name, g.module, g.connects, g.params, span);
1745        }
1746    }
1747
1748    pub fn add_dangling_input(
1749        &mut self,
1750        instance: &str,
1751        child_port: impl Into<String>,
1752        span: Span,
1753    ) {
1754        use bitloom_hir::PortConnect;
1755        if let Some(m) = self.current.as_mut() {
1756            for stmt in &mut m.body {
1757                if let Stmt::Instance(inst) = stmt {
1758                    if inst.name == instance {
1759                        inst.connects.push(PortConnect {
1760                            child_port: child_port.into(),
1761                            parent_net: String::new(),
1762                            span,
1763                            dangling: true,
1764                        });
1765                        return;
1766                    }
1767                }
1768            }
1769        }
1770        self.push_err(Diagnostic {
1771            span,
1772            code: "rhdl::E0201".into(),
1773            en: format!("unknown instance '{instance}' for dangling mark"),
1774            zh: format!("悬空标记找不到实例 '{instance}'"),
1775        });
1776    }
1777}
1778
1779/// Trait implemented by `#[rhdl::top]` / design modules (AD-19 partial for 1.1).
1780pub trait Elaboratable {
1781    fn elaborate() -> Result<FrozenHir, Diagnostics>;
1782}
1783
1784#[cfg(test)]
1785mod tests {
1786    use super::*;
1787
1788    fn base_ports(s: &mut ElaborateSession) {
1789        s.begin_module("M", Span::default());
1790        s.add_input("clk", GroundType::Clock, Span::default());
1791        s.add_input("rst", GroundType::Reset, Span::default());
1792        s.add_input("data_in", GroundType::UInt { width: 8 }, Span::default());
1793        s.add_output("data_out", GroundType::UInt { width: 8 }, Span::default());
1794    }
1795
1796    #[test]
1797    fn complete_comb_assign_ok() {
1798        let mut s = ElaborateSession::new("t");
1799        base_ports(&mut s);
1800        s.begin_combinational(Span::default());
1801        s.assign_net("data_out", "data_in", Span::default());
1802        s.end_process();
1803        s.end_module();
1804        assert!(s.finish().is_ok());
1805    }
1806
1807    #[test]
1808    fn incomplete_branch_is_latch_error() {
1809        let mut s = ElaborateSession::new("t");
1810        base_ports(&mut s);
1811        s.begin_combinational(Span::default());
1812        s.begin_then(Span::default());
1813        s.assign_net("data_out", "data_in", Span::default());
1814        s.begin_else(Span::default());
1815        // else does not assign data_out
1816        s.end_if(Span::default());
1817        s.end_process();
1818        s.end_module();
1819        let err = s.finish().unwrap_err();
1820        assert!(
1821            err.0.iter().any(|d| d.code == "rhdl::E0110"),
1822            "expected latch diagnostic, got {err}"
1823        );
1824    }
1825
1826    #[test]
1827    fn both_branches_assign_ok() {
1828        let mut s = ElaborateSession::new("t");
1829        base_ports(&mut s);
1830        s.begin_combinational(Span::default());
1831        s.begin_then(Span::default());
1832        s.assign_net("data_out", "data_in", Span::default());
1833        s.begin_else(Span::default());
1834        s.assign_net("data_out", "data_in", Span::default());
1835        s.end_if(Span::default());
1836        s.end_process();
1837        s.end_module();
1838        let r = s.finish();
1839        assert!(r.is_ok(), "{:?}", r.err());
1840    }
1841
1842    #[test]
1843    fn comb_cannot_write_reg_d() {
1844        let mut s = ElaborateSession::new("t");
1845        base_ports(&mut s);
1846        s.declare_reg("count", GroundType::UInt { width: 8 }, Span::default());
1847        s.begin_combinational(Span::default());
1848        s.assign_reg_d_inc("count", Span::default());
1849        s.end_process();
1850        s.end_module();
1851        let err = s.finish().unwrap_err();
1852        assert!(err.0.iter().any(|d| d.code == "rhdl::E0116"));
1853    }
1854
1855    #[test]
1856    fn seq_cannot_drive_comb_net() {
1857        let mut s = ElaborateSession::new("t");
1858        base_ports(&mut s);
1859        s.begin_sequential(Span::default());
1860        s.assign_net("data_out", "data_in", Span::default());
1861        s.end_process();
1862        s.end_module();
1863        let err = s.finish().unwrap_err();
1864        assert!(err.0.iter().any(|d| d.code == "rhdl::E0114"));
1865    }
1866
1867    #[test]
1868    fn assign_outside_process_rejected() {
1869        let mut s = ElaborateSession::new("t");
1870        base_ports(&mut s);
1871        s.assign_net("data_out", "data_in", Span::default());
1872        s.end_module();
1873        let err = s.finish().unwrap_err();
1874        assert!(err.0.iter().any(|d| d.code == "rhdl::E0103"));
1875    }
1876
1877    #[test]
1878    fn seq_reg_d_ok() {
1879        let mut s = ElaborateSession::new("t");
1880        base_ports(&mut s);
1881        s.declare_reg("count", GroundType::UInt { width: 8 }, Span::default());
1882        s.begin_combinational(Span::default());
1883        s.assign_net("data_out", "count", Span::default());
1884        s.end_process();
1885        s.begin_sequential(Span::default());
1886        s.assign_reg_d_inc("count", Span::default());
1887        s.end_process();
1888        s.end_module();
1889        assert!(s.finish().is_ok());
1890    }
1891
1892    #[test]
1893    fn missing_clock_rejected() {
1894        let mut s = ElaborateSession::new("t");
1895        s.begin_module("M", Span::default());
1896        s.add_input("rst", GroundType::Reset, Span::default());
1897        s.add_output("data_out", GroundType::UInt { width: 8 }, Span::default());
1898        s.end_module();
1899        let err = s.finish().unwrap_err();
1900        assert!(err.0.iter().any(|d| d.code == "rhdl::E0120"));
1901    }
1902
1903    #[test]
1904    fn missing_reset_rejected() {
1905        let mut s = ElaborateSession::new("t");
1906        s.begin_module("M", Span::default());
1907        s.add_input("clk", GroundType::Clock, Span::default());
1908        s.add_output("data_out", GroundType::UInt { width: 8 }, Span::default());
1909        s.end_module();
1910        let err = s.finish().unwrap_err();
1911        assert!(err.0.iter().any(|d| d.code == "rhdl::E0121"));
1912    }
1913
1914    #[test]
1915    fn mismatched_add_width_rejected() {
1916        let mut s = ElaborateSession::new("t");
1917        base_ports(&mut s);
1918        s.declare_wire("a", GroundType::UInt { width: 8 }, Span::default());
1919        s.declare_wire("b", GroundType::UInt { width: 16 }, Span::default());
1920        assert!(s.check_add("a", "b", Span::default()).is_none());
1921        s.end_module();
1922        let err = s.finish().unwrap_err();
1923        assert!(err.0.iter().any(|d| d.code == "rhdl::E0130"));
1924    }
1925
1926    #[test]
1927    fn mismatched_assign_net_width_rejected() {
1928        let mut s = ElaborateSession::new("t");
1929        base_ports(&mut s);
1930        s.add_output("narrow", GroundType::UInt { width: 4 }, Span::default());
1931        s.begin_combinational(Span::default());
1932        s.assign_net("narrow", "data_in", Span::default());
1933        s.end_process();
1934        s.end_module();
1935        let err = s.finish().unwrap_err();
1936        assert!(
1937            err.0.iter().any(|d| d.code == "rhdl::E0131"),
1938            "expected E0131, got {err}"
1939        );
1940    }
1941
1942    #[test]
1943    fn mismatched_assign_reg_d_width_rejected() {
1944        let mut s = ElaborateSession::new("t");
1945        base_ports(&mut s);
1946        s.declare_reg("q_narrow", GroundType::UInt { width: 4 }, Span::default());
1947        s.begin_sequential(Span::default());
1948        s.assign_reg_d_from("q_narrow", "data_in", Span::default());
1949        s.end_process();
1950        s.end_module();
1951        let err = s.finish().unwrap_err();
1952        assert!(
1953            err.0.iter().any(|d| d.code == "rhdl::E0131"),
1954            "expected E0131 on Reg.d path, got {err}"
1955        );
1956    }
1957
1958    #[test]
1959    fn pad_then_add_ok() {
1960        let mut s = ElaborateSession::new("t");
1961        base_ports(&mut s);
1962        s.declare_wire("a", GroundType::UInt { width: 8 }, Span::default());
1963        s.declare_wire("b", GroundType::UInt { width: 16 }, Span::default());
1964        assert!(s.pad_to("a", 16, "a_pad", Span::default()));
1965        assert_eq!(s.check_add("a_pad", "b", Span::default()), Some(16));
1966        s.begin_combinational(Span::default());
1967        s.assign_net("data_out", "data_in", Span::default());
1968        s.end_process();
1969        s.end_module();
1970        assert!(s.finish().is_ok());
1971    }
1972
1973    #[test]
1974    fn multi_drive_rejected() {
1975        let mut s = ElaborateSession::new("t");
1976        base_ports(&mut s);
1977        s.begin_combinational(Span::default());
1978        s.assign_net("data_out", "data_in", Span::default());
1979        s.end_process();
1980        s.begin_combinational(Span::default());
1981        s.assign_net("data_out", "data_in", Span::default());
1982        s.end_process();
1983        s.end_module();
1984        let err = s.finish().unwrap_err();
1985        assert!(err.0.iter().any(|d| d.code == "rhdl::E0140"));
1986    }
1987
1988    #[test]
1989    fn parameterized_widths_w8_and_w16() {
1990        fn elaborate_w(w: u32) -> bitloom_hir::FrozenHir {
1991            let mut s = ElaborateSession::new("t");
1992            s.begin_module(format!("Add{w}"), Span::default());
1993            s.add_input("clk", GroundType::Clock, Span::default());
1994            s.add_input("rst", GroundType::Reset, Span::default());
1995            s.add_input("a", GroundType::UInt { width: w }, Span::default());
1996            s.add_input("b", GroundType::UInt { width: w }, Span::default());
1997            s.add_output("y", GroundType::UInt { width: w }, Span::default());
1998            s.begin_combinational(Span::default());
1999            s.assign_net("y", "a", Span::default());
2000            s.end_process();
2001            s.end_module();
2002            s.finish().unwrap()
2003        }
2004        let h8 = elaborate_w(8);
2005        let h16 = elaborate_w(16);
2006        assert!(matches!(
2007            h8.circuit().modules[0].ports[2].ty,
2008            GroundType::UInt { width: 8 }
2009        ));
2010        assert!(matches!(
2011            h16.circuit().modules[0].ports[2].ty,
2012            GroundType::UInt { width: 16 }
2013        ));
2014    }
2015
2016    #[test]
2017    fn hierarchy_instance_preserved() {
2018        let mut s = ElaborateSession::new("t");
2019        s.begin_module("Child", Span::default());
2020        s.add_input("clk", GroundType::Clock, Span::default());
2021        s.add_input("rst", GroundType::Reset, Span::default());
2022        s.add_input("x", GroundType::UInt { width: 8 }, Span::default());
2023        s.add_output("y", GroundType::UInt { width: 8 }, Span::default());
2024        s.begin_combinational(Span::default());
2025        s.assign_net("y", "x", Span::default());
2026        s.end_process();
2027        s.end_module();
2028
2029        s.begin_module("Parent", Span::default());
2030        s.add_input("clk", GroundType::Clock, Span::default());
2031        s.add_input("rst", GroundType::Reset, Span::default());
2032        s.add_input("x", GroundType::UInt { width: 8 }, Span::default());
2033        s.add_output("y", GroundType::UInt { width: 8 }, Span::default());
2034        s.add_instance(
2035            "u0",
2036            "Child",
2037            vec![
2038                ("clk".into(), "clk".into()),
2039                ("rst".into(), "rst".into()),
2040                ("x".into(), "x".into()),
2041                ("y".into(), "y".into()),
2042            ],
2043            vec![("W".into(), 8)],
2044            Span::default(),
2045        );
2046        s.end_module();
2047        let frozen = s.finish().unwrap();
2048        assert_eq!(frozen.circuit().modules.len(), 2);
2049        assert!(frozen.circuit().modules[1].body.iter().any(|st| matches!(
2050            st,
2051            bitloom_hir::Stmt::Instance(i) if i.name == "u0" && i.module == "Child"
2052        )));
2053    }
2054
2055    #[test]
2056    fn generate_instances_factory_batches_children() {
2057        let mut s = ElaborateSession::new("t");
2058        s.begin_module("Lane", Span::default());
2059        s.add_input("clk", GroundType::Clock, Span::default());
2060        s.add_input("rst", GroundType::Reset, Span::default());
2061        s.add_input("x", GroundType::UInt { width: 8 }, Span::default());
2062        s.add_output("y", GroundType::UInt { width: 8 }, Span::default());
2063        s.begin_combinational(Span::default());
2064        s.assign_net("y", "x", Span::default());
2065        s.end_process();
2066        s.end_module();
2067
2068        s.begin_module("Parent", Span::default());
2069        s.add_input("clk", GroundType::Clock, Span::default());
2070        s.add_input("rst", GroundType::Reset, Span::default());
2071        for i in 0..3 {
2072            s.add_input(
2073                format!("x{i}"),
2074                GroundType::UInt { width: 8 },
2075                Span::default(),
2076            );
2077            s.add_output(
2078                format!("y{i}"),
2079                GroundType::UInt { width: 8 },
2080                Span::default(),
2081            );
2082        }
2083        s.generate_instances(3, |i, sess| {
2084            sess.add_instance(
2085                format!("u{i}"),
2086                "Lane",
2087                vec![
2088                    ("clk".into(), "clk".into()),
2089                    ("rst".into(), "rst".into()),
2090                    ("x".into(), format!("x{i}")),
2091                    ("y".into(), format!("y{i}")),
2092                ],
2093                vec![],
2094                Span::default(),
2095            );
2096        });
2097        s.end_module();
2098        let frozen = s.finish().unwrap();
2099        let parent = frozen
2100            .circuit()
2101            .modules
2102            .iter()
2103            .find(|m| m.name == "Parent")
2104            .unwrap();
2105        let instances: Vec<_> = parent
2106            .body
2107            .iter()
2108            .filter_map(|st| match st {
2109                bitloom_hir::Stmt::Instance(i) => Some(i.name.as_str()),
2110                _ => None,
2111            })
2112            .collect();
2113        assert_eq!(instances, ["u0", "u1", "u2"]);
2114    }
2115
2116    #[test]
2117    fn generate_instances_from_returns_plain_specs() {
2118        let mut s = ElaborateSession::new("t");
2119        s.begin_module("Lane", Span::default());
2120        s.add_input("clk", GroundType::Clock, Span::default());
2121        s.add_input("rst", GroundType::Reset, Span::default());
2122        s.add_input("x", GroundType::UInt { width: 8 }, Span::default());
2123        s.add_output("y", GroundType::UInt { width: 8 }, Span::default());
2124        s.end_module();
2125        s.begin_module("Parent", Span::default());
2126        s.add_input("clk", GroundType::Clock, Span::default());
2127        s.add_input("rst", GroundType::Reset, Span::default());
2128        s.add_input("x0", GroundType::UInt { width: 8 }, Span::default());
2129        s.add_output("y0", GroundType::UInt { width: 8 }, Span::default());
2130        s.generate_instances_from(
2131            1,
2132            |_| {
2133                GeneratedInstance::new(
2134                    "u0",
2135                    "Lane",
2136                    vec![
2137                        ("clk".into(), "clk".into()),
2138                        ("rst".into(), "rst".into()),
2139                        ("x".into(), "x0".into()),
2140                        ("y".into(), "y0".into()),
2141                    ],
2142                    vec![],
2143                )
2144            },
2145            Span::default(),
2146        );
2147        s.end_module();
2148        assert!(s.finish().is_ok());
2149    }
2150
2151    #[test]
2152    fn undriven_child_input_rejected() {
2153        let mut s = ElaborateSession::new("t");
2154        s.begin_module("Child", Span::default());
2155        s.add_input("clk", GroundType::Clock, Span::default());
2156        s.add_input("rst", GroundType::Reset, Span::default());
2157        s.add_input("x", GroundType::UInt { width: 8 }, Span::default());
2158        s.add_output("y", GroundType::UInt { width: 8 }, Span::default());
2159        s.end_module();
2160        s.begin_module("Parent", Span::default());
2161        s.add_input("clk", GroundType::Clock, Span::default());
2162        s.add_input("rst", GroundType::Reset, Span::default());
2163        s.add_output("y", GroundType::UInt { width: 8 }, Span::default());
2164        s.add_instance(
2165            "u0",
2166            "Child",
2167            vec![
2168                ("clk".into(), "clk".into()),
2169                ("rst".into(), "rst".into()),
2170                ("y".into(), "y".into()),
2171            ],
2172            vec![],
2173            Span::default(),
2174        );
2175        s.end_module();
2176        let err = s.finish().unwrap_err();
2177        assert!(err.0.iter().any(|d| d.code == "rhdl::E0202"));
2178    }
2179
2180    #[test]
2181    fn sync_read_mem_declares_and_emits() {
2182        let mut s = ElaborateSession::new("t");
2183        s.begin_module("MemTop", Span::default());
2184        s.add_input("clk", GroundType::Clock, Span::default());
2185        s.add_input("rst", GroundType::Reset, Span::default());
2186        s.add_output("y", GroundType::UInt { width: 8 }, Span::default());
2187        s.declare_sync_read_mem("ram", 16, 8, Span::default());
2188        s.begin_combinational(Span::default());
2189        s.assign_net("y", "ram", Span::default());
2190        s.end_process();
2191        s.end_module();
2192        let frozen = s.finish().unwrap();
2193        assert!(frozen.circuit().modules[0].body.iter().any(|st| matches!(
2194            st,
2195            bitloom_hir::Stmt::MemDecl {
2196                sync_read: true,
2197                ..
2198            }
2199        )));
2200    }
2201
2202    #[test]
2203    fn mem_with_init_fn_stores_plain_words() {
2204        let mut s = ElaborateSession::new("t");
2205        s.begin_module("Lut", Span::default());
2206        s.add_input("clk", GroundType::Clock, Span::default());
2207        s.add_input("rst", GroundType::Reset, Span::default());
2208        s.add_output("y", GroundType::UInt { width: 8 }, Span::default());
2209        s.declare_mem_with_init_fn("rom", 4, 8, |i| ((i * i) & 0xff) as u64, Span::default());
2210        s.begin_combinational(Span::default());
2211        s.assign_net("y", "rom", Span::default());
2212        s.end_process();
2213        s.end_module();
2214        let frozen = s.finish().unwrap();
2215        let init = frozen.circuit().modules[0]
2216            .body
2217            .iter()
2218            .find_map(|st| match st {
2219                bitloom_hir::Stmt::MemDecl {
2220                    name,
2221                    init: Some(words),
2222                    ..
2223                } if name == "rom" => Some(words.clone()),
2224                _ => None,
2225            })
2226            .expect("rom init present");
2227        assert_eq!(init, vec![0, 1, 4, 9]);
2228    }
2229
2230    #[test]
2231    fn mem_init_len_mismatch_fails() {
2232        let mut s = ElaborateSession::new("t");
2233        s.begin_module("Bad", Span::default());
2234        s.add_input("clk", GroundType::Clock, Span::default());
2235        s.add_input("rst", GroundType::Reset, Span::default());
2236        s.declare_mem_with_init("rom", 4, 8, vec![1, 2], Span::default());
2237        s.end_module();
2238        let err = s.finish().unwrap_err();
2239        assert!(err.0.iter().any(|d| d.code == "rhdl::E0212"));
2240    }
2241
2242    #[test]
2243    fn async_reset_and_enable_flags() {
2244        let mut s = ElaborateSession::new("t");
2245        s.begin_module("M", Span::default());
2246        s.add_input("clk", GroundType::Clock, Span::default());
2247        s.add_input("rst", GroundType::Reset, Span::default());
2248        s.add_output("y", GroundType::UInt { width: 8 }, Span::default());
2249        s.declare_reg_ex(
2250            "q",
2251            GroundType::UInt { width: 8 },
2252            true,
2253            true,
2254            Span::default(),
2255        );
2256        s.begin_combinational(Span::default());
2257        s.assign_net("y", "q", Span::default());
2258        s.end_process();
2259        s.begin_sequential(Span::default());
2260        s.assign_reg_d_inc("q", Span::default());
2261        s.end_process();
2262        s.end_module();
2263        let frozen = s.finish().unwrap();
2264        assert!(frozen.circuit().modules[0].body.iter().any(|st| matches!(
2265            st,
2266            bitloom_hir::Stmt::RegDecl {
2267                async_reset: true,
2268                has_enable: true,
2269                ..
2270            }
2271        )));
2272    }
2273
2274    #[test]
2275    fn illegal_domain_crossing_rejected() {
2276        let mut s = ElaborateSession::new("t");
2277        s.begin_module("Cdc", Span::default());
2278        s.add_input("clk", GroundType::Clock, Span::default());
2279        s.add_input("rst", GroundType::Reset, Span::default());
2280        s.add_input("a", GroundType::UInt { width: 8 }, Span::default());
2281        s.add_output("y", GroundType::UInt { width: 8 }, Span::default());
2282        s.bind_domain("a", 0);
2283        s.bind_domain("y", 1);
2284        s.begin_combinational(Span::default());
2285        s.assign_net("y", "a", Span::default());
2286        s.end_process();
2287        s.end_module();
2288        let err = s.finish().unwrap_err();
2289        assert!(err.0.iter().any(|d| d.code == "rhdl::E0220"), "{err}");
2290    }
2291
2292    #[test]
2293    fn cdc_bridge_allows_crossing() {
2294        let mut s = ElaborateSession::new("t");
2295        s.begin_module("CdcOk", Span::default());
2296        s.add_input("clk", GroundType::Clock, Span::default());
2297        s.add_input("rst", GroundType::Reset, Span::default());
2298        s.add_input("a", GroundType::UInt { width: 8 }, Span::default());
2299        s.add_output("y", GroundType::UInt { width: 8 }, Span::default());
2300        s.bind_domain("a", 0);
2301        s.bind_domain("y", 1);
2302        s.mark_cdc_bridge("y");
2303        s.begin_combinational(Span::default());
2304        s.assign_net("y", "a", Span::default());
2305        s.end_process();
2306        s.end_module();
2307        assert!(s.finish().is_ok());
2308    }
2309
2310    #[test]
2311    fn double_flop_stages_allow_reg_d_crossing() {
2312        let mut s = ElaborateSession::new("t");
2313        s.begin_module("Df", Span::default());
2314        s.add_input("clk", GroundType::Clock, Span::default());
2315        s.add_input("rst", GroundType::Reset, Span::default());
2316        s.add_input("din", GroundType::UInt { width: 1 }, Span::default());
2317        s.add_output("dout", GroundType::UInt { width: 1 }, Span::default());
2318        s.bind_domain("din", 0);
2319        s.bind_domain("dout", 1);
2320        let (ff0, ff1) =
2321            s.declare_double_flop_stages("sync", GroundType::UInt { width: 1 }, 1, Span::default());
2322        s.begin_combinational(Span::default());
2323        s.assign_net("dout", &ff1, Span::default());
2324        s.end_process();
2325        s.begin_sequential(Span::default());
2326        s.connect_double_flop(&ff0, &ff1, "din", Span::default());
2327        s.end_process();
2328        s.end_module();
2329        assert!(s.finish().is_ok());
2330    }
2331
2332    #[test]
2333    fn assign_reg_d_cross_domain_without_bridge_rejected() {
2334        let mut s = ElaborateSession::new("t");
2335        s.begin_module("Bad", Span::default());
2336        s.add_input("clk", GroundType::Clock, Span::default());
2337        s.add_input("rst", GroundType::Reset, Span::default());
2338        s.add_input("din", GroundType::UInt { width: 1 }, Span::default());
2339        s.declare_reg("q", GroundType::UInt { width: 1 }, Span::default());
2340        s.bind_domain("din", 0);
2341        s.bind_domain("q", 1);
2342        s.begin_sequential(Span::default());
2343        s.assign_reg_d_from("q", "din", Span::default());
2344        s.end_process();
2345        s.end_module();
2346        let err = s.finish().unwrap_err();
2347        assert!(err.0.iter().any(|d| d.code == "rhdl::E0220"), "{err}");
2348    }
2349
2350    #[test]
2351    fn unknown_parent_net_rejected() {
2352        let mut s = ElaborateSession::new("t");
2353        s.begin_module("Child", Span::default());
2354        s.add_input("clk", GroundType::Clock, Span::default());
2355        s.add_input("rst", GroundType::Reset, Span::default());
2356        s.add_input("x", GroundType::UInt { width: 8 }, Span::default());
2357        s.add_output("y", GroundType::UInt { width: 8 }, Span::default());
2358        s.end_module();
2359        s.begin_module("Parent", Span::default());
2360        s.add_input("clk", GroundType::Clock, Span::default());
2361        s.add_input("rst", GroundType::Reset, Span::default());
2362        s.add_output("y", GroundType::UInt { width: 8 }, Span::default());
2363        s.add_instance(
2364            "u0",
2365            "Child",
2366            vec![
2367                ("clk".into(), "clk".into()),
2368                ("rst".into(), "rst".into()),
2369                ("x".into(), "no_such_net".into()),
2370                ("y".into(), "y".into()),
2371            ],
2372            vec![],
2373            Span::default(),
2374        );
2375        s.end_module();
2376        let err = s.finish().unwrap_err();
2377        assert!(err.0.iter().any(|d| d.code == "rhdl::E0204"), "{err}");
2378    }
2379
2380    #[test]
2381    fn hw_capture_wire_rejected_e0142() {
2382        let mut s = ElaborateSession::new("t");
2383        base_ports(&mut s);
2384        s.declare_wire("w", GroundType::UInt { width: 8 }, Span::default());
2385        // Documented illegal capture of Wire into generator context.
2386        s.assert_no_hw_capture(&[HwCaptureRef::wire("w")], Span::default());
2387        s.begin_combinational(Span::default());
2388        s.assign_net("data_out", "data_in", Span::default());
2389        s.end_process();
2390        s.end_module();
2391        let err = s.finish().unwrap_err();
2392        assert!(
2393            err.0.iter().any(|d| d.code == "rhdl::E0142"),
2394            "expected E0142, got {err}"
2395        );
2396        assert!(
2397            err.0
2398                .iter()
2399                .any(|d| d.en.contains("Wire") && d.en.contains("w")),
2400            "diagnostic should name Wire 'w': {err}"
2401        );
2402    }
2403
2404    #[test]
2405    fn hw_capture_reg_rejected_e0142() {
2406        let mut s = ElaborateSession::new("t");
2407        base_ports(&mut s);
2408        s.declare_reg("r", GroundType::UInt { width: 8 }, Span::default());
2409        s.reject_hw_capture(&HwCaptureRef::reg("r"), Span::default());
2410        s.begin_combinational(Span::default());
2411        s.assign_net("data_out", "data_in", Span::default());
2412        s.end_process();
2413        s.end_module();
2414        let err = s.finish().unwrap_err();
2415        assert!(
2416            err.0
2417                .iter()
2418                .any(|d| d.code == "rhdl::E0142" && d.en.contains("Reg")),
2419            "expected E0142 Reg, got {err}"
2420        );
2421    }
2422
2423    #[test]
2424    fn assert_no_hw_capture_empty_ok() {
2425        let mut s = ElaborateSession::new("t");
2426        base_ports(&mut s);
2427        s.assert_no_hw_capture(&[], Span::default());
2428        s.declare_mem_with_init_fn("rom", 2, 8, |i| i as u64, Span::default());
2429        s.begin_combinational(Span::default());
2430        s.assign_net("data_out", "data_in", Span::default());
2431        s.end_process();
2432        s.end_module();
2433        assert!(s.finish().is_ok());
2434    }
2435
2436    #[test]
2437    fn fr16_capturing_closure_still_e0141() {
2438        let mut s = ElaborateSession::new("t");
2439        base_ports(&mut s);
2440        s.reject_unsynthesizable("capturing closure", Span::default());
2441        s.begin_combinational(Span::default());
2442        s.assign_net("data_out", "data_in", Span::default());
2443        s.end_process();
2444        s.end_module();
2445        let err = s.finish().unwrap_err();
2446        assert!(
2447            err.0.iter().any(|d| d.code == "rhdl::E0141"),
2448            "FR16 capturing closure must stay E0141, got {err}"
2449        );
2450    }
2451
2452    #[test]
2453    fn synthesizable_closure_heap_e0143() {
2454        let mut s = ElaborateSession::new("t");
2455        base_ports(&mut s);
2456        s.reject_unsynthesizable_closure(
2457            &SynthesizableClosureViolation::heap("Box<u8> in body"),
2458            Span::default(),
2459        );
2460        s.begin_combinational(Span::default());
2461        s.assign_net("data_out", "data_in", Span::default());
2462        s.end_process();
2463        s.end_module();
2464        let err = s.finish().unwrap_err();
2465        assert!(
2466            err.0.iter().any(|d| d.code == "rhdl::E0143"),
2467            "expected E0143, got {err}"
2468        );
2469    }
2470
2471    #[test]
2472    fn synthesizable_closure_capture_state_e0144() {
2473        let mut s = ElaborateSession::new("t");
2474        base_ports(&mut s);
2475        s.check_synthesizable_closure(
2476            &[SynthesizableClosureViolation::runtime_capture_state(
2477                "captures local threshold",
2478            )],
2479            Span::default(),
2480        );
2481        s.begin_combinational(Span::default());
2482        s.assign_net("data_out", "data_in", Span::default());
2483        s.end_process();
2484        s.end_module();
2485        let err = s.finish().unwrap_err();
2486        assert!(
2487            err.0.iter().any(|d| d.code == "rhdl::E0144"),
2488            "expected E0144, got {err}"
2489        );
2490    }
2491
2492    #[test]
2493    fn synthesizable_closure_impure_e0145() {
2494        let mut s = ElaborateSession::new("t");
2495        base_ports(&mut s);
2496        s.reject_unsynthesizable_closure(
2497            &SynthesizableClosureViolation::impure("file I/O"),
2498            Span::default(),
2499        );
2500        s.begin_combinational(Span::default());
2501        s.assign_net("data_out", "data_in", Span::default());
2502        s.end_process();
2503        s.end_module();
2504        let err = s.finish().unwrap_err();
2505        assert!(
2506            err.0.iter().any(|d| d.code == "rhdl::E0145"),
2507            "expected E0145, got {err}"
2508        );
2509    }
2510
2511    #[test]
2512    fn legal_empty_and_simple_synthesizable_closure_pass() {
2513        let mut s = ElaborateSession::new("t");
2514        base_ports(&mut s);
2515        s.check_synthesizable_closure(&[], Span::default());
2516        s.check_synthesizable_closure_marker(&LegalEmptyClosure, Span::default());
2517        s.check_synthesizable_closure_marker(&LegalSimpleClosure, Span::default());
2518        s.begin_combinational(Span::default());
2519        s.assign_net("data_out", "data_in", Span::default());
2520        s.end_process();
2521        s.end_module();
2522        assert!(s.finish().is_ok(), "legal empty/simple must pass");
2523    }
2524
2525    #[test]
2526    fn diagnose_free_fn_cap_r60() {
2527        let diags = diagnose_synthesizable_closure_violations(
2528            &[SynthesizableClosureViolation::heap("String")],
2529            Span::default(),
2530        );
2531        assert!(diags.0.iter().any(|d| d.code == "rhdl::E0143"));
2532    }
2533
2534    #[test]
2535    fn inline_comb_fn_expands_to_ordinary_assign() {
2536        let mut s = ElaborateSession::new("t");
2537        base_ports(&mut s);
2538        s.add_input("b", GroundType::UInt { width: 8 }, Span::default());
2539        s.declare_wire("sum", GroundType::UInt { width: 8 }, Span::default());
2540        s.begin_combinational(Span::default());
2541        s.inline_comb_fn("sum", &["data_in", "b"], &[], Span::default(), |args| {
2542            CombInline::Add(args[0].into(), args[1].into())
2543        });
2544        s.inline_comb_fn_marker(
2545            "data_out",
2546            &["sum"],
2547            &LegalSimpleClosure,
2548            Span::default(),
2549            |args| CombInline::Ref(args[0].into()),
2550        );
2551        s.end_process();
2552        s.end_module();
2553        let hir = s.finish().expect("legal inline must finish");
2554        let body = &hir.circuit().modules[0].body;
2555        let procs: Vec<_> = body
2556            .iter()
2557            .filter_map(|st| match st {
2558                Stmt::Process(p) => Some(p),
2559                _ => None,
2560            })
2561            .collect();
2562        assert_eq!(procs.len(), 1);
2563        assert_eq!(procs[0].assigns.len(), 2);
2564        assert!(matches!(
2565            &procs[0].assigns[0].expr,
2566            AssignExpr::Add(l, r) if l == "data_in" && r == "b"
2567        ));
2568        assert!(matches!(
2569            &procs[0].assigns[1].expr,
2570            AssignExpr::Ref(n) if n == "sum"
2571        ));
2572        // NFR36: no Fn / closure types in Debug of frozen module body.
2573        let dump = format!("{body:?}");
2574        assert!(!dump.contains("CombInline"));
2575        assert!(!dump.to_lowercase().contains("closure"));
2576    }
2577
2578    #[test]
2579    fn inline_comb_fn_violation_skips_expand() {
2580        let mut s = ElaborateSession::new("t");
2581        base_ports(&mut s);
2582        s.begin_combinational(Span::default());
2583        s.inline_comb_fn(
2584            "data_out",
2585            &["data_in"],
2586            &[SynthesizableClosureViolation::heap("Box in transform")],
2587            Span::default(),
2588            |_args| CombInline::Ref("data_in".into()),
2589        );
2590        s.end_process();
2591        s.end_module();
2592        let err = s.finish().expect_err("heap must fail");
2593        assert!(err.0.iter().any(|d| d.code == "rhdl::E0143"));
2594    }
2595
2596    #[test]
2597    fn inline_comb_fn_incomplete_branch_still_latch() {
2598        let mut s = ElaborateSession::new("t");
2599        base_ports(&mut s);
2600        s.begin_combinational(Span::default());
2601        s.begin_then(Span::default());
2602        s.inline_comb_fn("data_out", &["data_in"], &[], Span::default(), |args| {
2603            CombInline::Ref(args[0].into())
2604        });
2605        s.begin_else(Span::default());
2606        // else does not assign data_out — AD-18 latch
2607        s.end_if(Span::default());
2608        s.end_process();
2609        s.end_module();
2610        let err = s.finish().unwrap_err();
2611        assert!(
2612            err.0.iter().any(|d| d.code == "rhdl::E0110"),
2613            "expected latch diagnostic after inline, got {err}"
2614        );
2615    }
2616
2617    #[test]
2618    fn inline_seq_fn_expands_to_ordinary_reg_d() {
2619        let mut s = ElaborateSession::new("t");
2620        base_ports(&mut s);
2621        s.declare_reg("count", GroundType::UInt { width: 8 }, Span::default());
2622        s.begin_sequential(Span::default());
2623        s.inline_seq_fn("count", &[], &[], &[], Span::default(), |_args| {
2624            SeqInline::Inc
2625        });
2626        s.end_process();
2627        s.begin_combinational(Span::default());
2628        s.assign_net("data_out", "count", Span::default());
2629        s.end_process();
2630        s.end_module();
2631        let hir = s.finish().expect("legal seq inline must finish");
2632        let body = &hir.circuit().modules[0].body;
2633        let seq = body.iter().find_map(|st| match st {
2634            Stmt::Process(p) if matches!(p.kind, ProcessKind::Sequential) => Some(p),
2635            _ => None,
2636        });
2637        let seq = seq.expect("sequential process");
2638        assert_eq!(seq.assigns.len(), 1);
2639        assert!(matches!(
2640            &seq.assigns[0],
2641            Assign {
2642                target: AssignTarget::RegD(n),
2643                expr: AssignExpr::Inc(i),
2644                ..
2645            } if n == "count" && i == "count"
2646        ));
2647        let dump = format!("{body:?}");
2648        assert!(!dump.contains("SeqInline"));
2649        assert!(!dump.to_lowercase().contains("closure"));
2650    }
2651
2652    #[test]
2653    fn inline_seq_fn_cap_r70_blocks_second_reg_d() {
2654        let mut s = ElaborateSession::new("t");
2655        base_ports(&mut s);
2656        s.declare_reg("count", GroundType::UInt { width: 8 }, Span::default());
2657        s.begin_sequential(Span::default());
2658        s.assign_reg_d_inc("count", Span::default());
2659        s.inline_seq_fn("count", &["data_in"], &[], &[], Span::default(), |args| {
2660            CombInline::Ref(args[0].into()).into()
2661        });
2662        s.end_process();
2663        s.begin_combinational(Span::default());
2664        s.assign_net("data_out", "count", Span::default());
2665        s.end_process();
2666        s.end_module();
2667        let err = s.finish().expect_err("second Reg.d must fail Cap-R-70");
2668        assert!(
2669            err.0.iter().any(|d| d.code == "rhdl::E0146"),
2670            "expected E0146, got {err}"
2671        );
2672    }
2673
2674    #[test]
2675    fn inline_seq_fn_ownership_token_skips_expand() {
2676        let mut s = ElaborateSession::new("t");
2677        base_ports(&mut s);
2678        s.declare_reg("count", GroundType::UInt { width: 8 }, Span::default());
2679        s.begin_sequential(Span::default());
2680        s.inline_seq_fn(
2681            "count",
2682            &[],
2683            &[],
2684            &[SeqOwnershipViolation::illegal_mutable_borrow(
2685                "&mut count captured",
2686            )],
2687            Span::default(),
2688            |_args| SeqInline::Inc,
2689        );
2690        s.end_process();
2691        s.begin_combinational(Span::default());
2692        s.assign_net("data_out", "count", Span::default());
2693        s.end_process();
2694        s.end_module();
2695        let err = s.finish().expect_err("ownership token must fail");
2696        assert!(err.0.iter().any(|d| d.code == "rhdl::E0146"));
2697    }
2698
2699    #[test]
2700    fn inline_seq_fn_multi_drive_still_e0140() {
2701        let mut s = ElaborateSession::new("t");
2702        base_ports(&mut s);
2703        s.declare_reg("count", GroundType::UInt { width: 8 }, Span::default());
2704        s.begin_sequential(Span::default());
2705        s.inline_seq_fn("count", &[], &[], &[], Span::default(), |_args| {
2706            SeqInline::Inc
2707        });
2708        s.end_process();
2709        s.begin_sequential(Span::default());
2710        s.assign_reg_d_from("count", "data_in", Span::default());
2711        s.end_process();
2712        s.begin_combinational(Span::default());
2713        s.assign_net("data_out", "count", Span::default());
2714        s.end_process();
2715        s.end_module();
2716        let err = s.finish().expect_err("cross-process multi-drive");
2717        assert!(
2718            err.0.iter().any(|d| d.code == "rhdl::E0140"),
2719            "expected E0140, got {err}"
2720        );
2721    }
2722
2723    #[test]
2724    fn diagnose_seq_ownership_free_fn() {
2725        let diags = diagnose_seq_ownership_violations(
2726            &[SeqOwnershipViolation::illegal_mutable_borrow("x")],
2727            Span::default(),
2728        );
2729        assert!(diags.0.iter().any(|d| d.code == "rhdl::E0146"));
2730    }
2731}