Skip to main content

bitloom_hir/
lib.rs

1//! RHDL HIR: hierarchical FrozenHir and diagnostics.
2//! Unfrozen circuit state is private to this crate.
3
4use std::fmt;
5
6/// Source span threaded from builder / macros (opaque for now).
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8pub struct Span {
9    pub start: u32,
10    pub end: u32,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct Diagnostic {
15    pub span: Span,
16    pub code: String,
17    pub en: String,
18    pub zh: String,
19}
20
21impl fmt::Display for Diagnostic {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        write!(f, "{}: {} ({})", self.code, self.en, self.zh)
24    }
25}
26
27#[derive(Debug, Clone, Default, PartialEq, Eq)]
28pub struct Diagnostics(pub Vec<Diagnostic>);
29
30impl Diagnostics {
31    pub fn push(&mut self, d: Diagnostic) {
32        self.0.push(d);
33    }
34
35    pub fn is_empty(&self) -> bool {
36        self.0.is_empty()
37    }
38}
39
40impl fmt::Display for Diagnostics {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        for d in &self.0 {
43            writeln!(f, "{d}")?;
44        }
45        Ok(())
46    }
47}
48
49impl std::error::Error for Diagnostics {}
50
51/// Ground types for phase-1 HIR (AD-12).
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub enum GroundType {
54    UInt {
55        width: u32,
56    },
57    SInt {
58        width: u32,
59    },
60    Clock,
61    Reset,
62    Bool,
63    /// Top-level only (FR27).
64    Analog,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum PortDirection {
69    Input,
70    Output,
71    /// Top-level only (FR27).
72    InOut,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct Port {
77    pub name: String,
78    pub direction: PortDirection,
79    pub ty: GroundType,
80    pub span: Span,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum ProcessKind {
85    Combinational,
86    Sequential,
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum SignalKind {
91    Wire,
92    Reg,
93    Output,
94    Input,
95}
96
97/// Assignment target in a process.
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub enum AssignTarget {
100    /// Combinational net / output / wire.
101    Net(String),
102    /// Sequential next-state: `Reg.d`.
103    RegD(String),
104    /// Sequential memory write: `mem[addr] <= data` (addr is a net/reg name).
105    /// When `we` is `Some`, write only if that signal is non-zero.
106    MemWrite {
107        mem: String,
108        addr: String,
109        we: Option<String>,
110    },
111}
112
113/// RHS of an assignment (phase-1 subset for emit + tick).
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub enum AssignExpr {
116    /// Copy from a named port, wire, or reg.
117    Ref(String),
118    /// Integer literal.
119    Lit(u64),
120    /// `ref + 1` (wrapping), used for counters until full ALU lands.
121    Inc(String),
122    /// Same-width binary add of two refs.
123    Add(String, String),
124    /// Same-width binary subtract of two refs.
125    Sub(String, String),
126    /// Bitwise AND of two refs.
127    And(String, String),
128    /// Bitwise OR of two refs.
129    Or(String, String),
130    /// Bitwise XOR of two refs.
131    Xor(String, String),
132    /// Logical left shift: `a << (b & 63)`.
133    Shl(String, String),
134    /// Logical right shift: `a >> (b & 63)`.
135    Shr(String, String),
136    /// Equality compare of two refs → 0/1 (Bool).
137    Eq(String, String),
138    /// 2:1 mux: `sel != 0 ? t : f`.
139    Mux { sel: String, t: String, f: String },
140    /// Memory read `mem[addr]` (SyncReadMem: one-cycle latency when assigned in seq).
141    MemRead { mem: String, addr: String },
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct Assign {
146    pub target: AssignTarget,
147    pub expr: AssignExpr,
148    pub span: Span,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct Process {
153    pub kind: ProcessKind,
154    pub assigns: Vec<Assign>,
155    pub span: Span,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct PortConnect {
160    pub child_port: String,
161    pub parent_net: String,
162    pub span: Span,
163    /// Explicit dangling / unused input opt-out (FR9).
164    pub dangling: bool,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct Instance {
169    pub name: String,
170    pub module: String,
171    pub connects: Vec<PortConnect>,
172    pub params: Vec<(String, u32)>,
173    pub span: Span,
174}
175
176#[derive(Debug, Clone, PartialEq, Eq)]
177pub enum Stmt {
178    WireDecl {
179        name: String,
180        ty: GroundType,
181        span: Span,
182    },
183    RegDecl {
184        name: String,
185        ty: GroundType,
186        /// Bound clock port name (must be the module's Clock input).
187        clock: String,
188        /// Bound sync active-high reset port name.
189        reset: String,
190        /// AD-23: async reset envelope.
191        async_reset: bool,
192        /// AD-23: clock enable present.
193        has_enable: bool,
194        span: Span,
195    },
196    Process(Process),
197    Instance(Instance),
198    /// Single-clock memory (AD-21). `sync_read` distinguishes SyncReadMem vs Mem.
199    MemDecl {
200        name: String,
201        depth: u32,
202        width: u32,
203        /// true => SyncReadMem (read latency 1); false => Mem (async read / reg file).
204        sync_read: bool,
205        /// Elaborate-time constant init (FR73). Plain words only — no closure residue.
206        /// `None` ⇒ uninitialized (sim zeros); `Some` length must equal `depth`.
207        init: Option<Vec<u64>>,
208        span: Span,
209    },
210}
211
212/// Explicit width-changing / arithmetic nodes (AD-18).
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub enum Expr {
215    /// Same-width binary add; both operands must match `width`.
216    Add { width: u32, span: Span },
217    /// Same-width connect / drive.
218    Connect { width: u32, span: Span },
219    Pad {
220        from_width: u32,
221        to_width: u32,
222        span: Span,
223    },
224    Trunc {
225        from_width: u32,
226        to_width: u32,
227        span: Span,
228    },
229}
230
231#[derive(Debug, Clone, PartialEq, Eq)]
232pub struct Module {
233    pub name: String,
234    pub ports: Vec<Port>,
235    pub body: Vec<Stmt>,
236    pub span: Span,
237}
238
239#[derive(Debug, Clone, PartialEq, Eq)]
240pub struct Circuit {
241    pub name: String,
242    pub modules: Vec<Module>,
243}
244
245/// Immutable circuit after freeze (AD-7).
246#[derive(Debug, Clone, PartialEq, Eq)]
247pub struct FrozenHir {
248    circuit: Circuit,
249    pub abi_name: String,
250}
251
252impl FrozenHir {
253    pub fn circuit(&self) -> &Circuit {
254        &self.circuit
255    }
256}
257
258/// Emitted backend file (AD-16).
259#[derive(Debug, Clone, PartialEq, Eq)]
260pub struct EmittedFile {
261    pub path: String,
262    pub contents: String,
263}
264
265/// Backend output bundle (AD-16).
266#[derive(Debug, Clone, PartialEq, Eq)]
267pub struct Artifact {
268    pub files: Vec<EmittedFile>,
269    pub filelist: Vec<String>,
270}
271
272/// Port-level values shared by tick and functional models (AD-17).
273#[derive(Debug, Clone, Default, PartialEq, Eq)]
274pub struct PortValues {
275    pub values: std::collections::BTreeMap<String, u64>,
276}
277
278impl PortValues {
279    pub fn get(&self, name: &str) -> Option<u64> {
280        self.values.get(name).copied()
281    }
282
283    pub fn set(&mut self, name: impl Into<String>, value: u64) {
284        self.values.insert(name.into(), value);
285    }
286}
287
288/// Mutable circuit owned only inside this crate during elaborate.
289#[derive(Debug)]
290pub(crate) struct Hir {
291    pub circuit: Circuit,
292}
293
294impl Hir {
295    pub(crate) fn new(name: impl Into<String>) -> Self {
296        Self {
297            circuit: Circuit {
298                name: name.into(),
299                modules: Vec::new(),
300            },
301        }
302    }
303}
304
305/// Private freeze: only called from elaborate/import paths (AD-1, AD-7).
306pub(crate) fn freeze(hir: Hir) -> Result<FrozenHir, Diagnostics> {
307    if hir.circuit.modules.is_empty() {
308        return Err(Diagnostics(vec![Diagnostic {
309            span: Span::default(),
310            code: "rhdl::E0001".into(),
311            en: "circuit has no modules".into(),
312            zh: "电路没有任何模块".into(),
313        }]));
314    }
315    let mut diags = Diagnostics::default();
316    for m in &hir.circuit.modules {
317        validate_clock_reset(m, &mut diags);
318        validate_unique_drivers(m, &mut diags);
319        validate_instances(&hir.circuit, m, &mut diags);
320        validate_special_io(&hir.circuit, m, &mut diags);
321    }
322    if !diags.is_empty() {
323        return Err(diags);
324    }
325    let top = hir
326        .circuit
327        .modules
328        .first()
329        .map(|m| m.name.clone())
330        .unwrap_or_else(|| hir.circuit.name.clone());
331    Ok(FrozenHir {
332        circuit: hir.circuit,
333        abi_name: top,
334    })
335}
336
337fn validate_special_io(circuit: &Circuit, m: &Module, diags: &mut Diagnostics) {
338    let is_top = m.name == circuit.name;
339    for p in &m.ports {
340        let special =
341            matches!(p.direction, PortDirection::InOut) || matches!(p.ty, GroundType::Analog);
342        if special && !is_top {
343            diags.push(Diagnostic {
344                span: p.span,
345                code: "rhdl::E0270".into(),
346                en: format!(
347                    "Analog/InOut port `{}` only allowed on top module `{}`",
348                    p.name, circuit.name
349                ),
350                zh: format!(
351                    "Analog/InOut 端口 `{}` 仅允许在顶层模块 `{}`",
352                    p.name, circuit.name
353                ),
354            });
355        }
356    }
357}
358
359fn validate_clock_reset(m: &Module, diags: &mut Diagnostics) {
360    let clocks: Vec<_> = m
361        .ports
362        .iter()
363        .filter(|p| p.direction == PortDirection::Input && matches!(p.ty, GroundType::Clock))
364        .collect();
365    let resets: Vec<_> = m
366        .ports
367        .iter()
368        .filter(|p| p.direction == PortDirection::Input && matches!(p.ty, GroundType::Reset))
369        .collect();
370    if clocks.len() != 1 {
371        diags.push(Diagnostic {
372            span: m.span,
373            code: "rhdl::E0120".into(),
374            en: format!(
375                "module '{}' must have exactly one Clock input (found {})",
376                m.name,
377                clocks.len()
378            ),
379            zh: format!(
380                "模块 '{}' 必须恰好有一个 Clock 输入(找到 {} 个)",
381                m.name,
382                clocks.len()
383            ),
384        });
385    }
386    if resets.len() != 1 {
387        diags.push(Diagnostic {
388            span: m.span,
389            code: "rhdl::E0121".into(),
390            en: format!(
391                "module '{}' must have exactly one Reset input (found {})",
392                m.name,
393                resets.len()
394            ),
395            zh: format!(
396                "模块 '{}' 必须恰好有一个 Reset 输入(找到 {} 个)",
397                m.name,
398                resets.len()
399            ),
400        });
401    }
402    if clocks.len() == 1 && resets.len() == 1 {
403        let clk = &clocks[0].name;
404        let rst = &resets[0].name;
405        for stmt in &m.body {
406            if let Stmt::RegDecl {
407                name,
408                clock,
409                reset,
410                span,
411                ..
412            } = stmt
413            {
414                if clock != clk {
415                    diags.push(Diagnostic {
416                        span: *span,
417                        code: "rhdl::E0122".into(),
418                        en: format!(
419                            "Reg '{name}' must bind to module Clock '{clk}', not '{clock}'"
420                        ),
421                        zh: format!("寄存器 '{name}' 必须绑定模块时钟 '{clk}',而不是 '{clock}'"),
422                    });
423                }
424                if reset != rst {
425                    diags.push(Diagnostic {
426                        span: *span,
427                        code: "rhdl::E0123".into(),
428                        en: format!(
429                            "Reg '{name}' must bind to module Reset '{rst}', not '{reset}'"
430                        ),
431                        zh: format!("寄存器 '{name}' 必须绑定模块复位 '{rst}',而不是 '{reset}'"),
432                    });
433                }
434            }
435        }
436    }
437}
438
439fn validate_unique_drivers(m: &Module, diags: &mut Diagnostics) {
440    use std::collections::HashMap;
441    // Multi-drive is across processes; within one process, if/else may assign the same net.
442    let mut drivers: HashMap<String, Vec<Span>> = HashMap::new();
443    for stmt in &m.body {
444        if let Stmt::Process(p) = stmt {
445            let mut seen_in_process = std::collections::HashSet::new();
446            for a in &p.assigns {
447                let key = match &a.target {
448                    AssignTarget::Net(n) => n.clone(),
449                    AssignTarget::RegD(n) => format!("{n}.d"),
450                    AssignTarget::MemWrite { mem, addr, we } => match we {
451                        Some(en) => format!("{mem}[{addr}] (we={en})"),
452                        None => format!("{mem}[{addr}]"),
453                    },
454                };
455                if seen_in_process.insert(key.clone()) {
456                    drivers.entry(key).or_default().push(a.span);
457                }
458            }
459        }
460    }
461    for (net, spans) in drivers {
462        if spans.len() > 1 {
463            diags.push(Diagnostic {
464                span: spans[1],
465                code: "rhdl::E0140".into(),
466                en: format!("multiple drivers for '{net}' ({} drivers)", spans.len()),
467                zh: format!("'{net}' 有多个驱动({} 个)", spans.len()),
468            });
469        }
470    }
471}
472
473fn validate_instances(circuit: &Circuit, parent: &Module, diags: &mut Diagnostics) {
474    use std::collections::HashMap;
475    let modules: HashMap<&str, &Module> = circuit
476        .modules
477        .iter()
478        .map(|m| (m.name.as_str(), m))
479        .collect();
480    for stmt in &parent.body {
481        let Stmt::Instance(inst) = stmt else {
482            continue;
483        };
484        let Some(child) = modules.get(inst.module.as_str()) else {
485            diags.push(Diagnostic {
486                span: inst.span,
487                code: "rhdl::E0201".into(),
488                en: format!("unknown child module '{}'", inst.module),
489                zh: format!("未知子模块 '{}'", inst.module),
490            });
491            continue;
492        };
493        let connected: HashMap<&str, &PortConnect> = inst
494            .connects
495            .iter()
496            .map(|c| (c.child_port.as_str(), c))
497            .collect();
498        for port in &child.ports {
499            match connected.get(port.name.as_str()) {
500                None if port.direction == PortDirection::Input => {
501                    diags.push(Diagnostic {
502                        span: inst.span,
503                        code: "rhdl::E0202".into(),
504                        en: format!(
505                            "undriven child input '{}.{}' (mark dangling if intentional)",
506                            inst.name, port.name
507                        ),
508                        zh: format!(
509                            "子模块输入 '{}.{}' 未驱动(若故意悬空请标记 dangling)",
510                            inst.name, port.name
511                        ),
512                    });
513                }
514                Some(c) if c.dangling && port.direction == PortDirection::Input => {}
515                Some(c) => {
516                    // Width check against parent net if present in parent ports/wires.
517                    let parent_ty = parent
518                        .ports
519                        .iter()
520                        .find(|p| p.name == c.parent_net)
521                        .map(|p| &p.ty)
522                        .or_else(|| {
523                            parent.body.iter().find_map(|s| match s {
524                                Stmt::WireDecl { name, ty, .. }
525                                | Stmt::RegDecl { name, ty, .. }
526                                    if name == &c.parent_net =>
527                                {
528                                    Some(ty)
529                                }
530                                _ => None,
531                            })
532                        });
533                    if let Some(pty) = parent_ty {
534                        let pw = width_of(pty);
535                        let cw = width_of(&port.ty);
536                        if pw != cw {
537                            diags.push(Diagnostic {
538                                span: c.span,
539                                code: "rhdl::E0203".into(),
540                                en: format!(
541                                    "width mismatch connecting '{}' (parent {pw}) to '{}.{}' (child {cw})",
542                                    c.parent_net, inst.name, port.name
543                                ),
544                                zh: format!(
545                                    "连接位宽不匹配:'{}'(父 {pw})→ '{}.{}'(子 {cw})",
546                                    c.parent_net, inst.name, port.name
547                                ),
548                            });
549                        }
550                    } else if !c.dangling {
551                        diags.push(Diagnostic {
552                            span: c.span,
553                            code: "rhdl::E0204".into(),
554                            en: format!(
555                                "cannot resolve parent net '{}' when connecting to '{}.{}'",
556                                c.parent_net, inst.name, port.name
557                            ),
558                            zh: format!(
559                                "连接 '{}.{}' 时无法解析父网 '{}'",
560                                inst.name, port.name, c.parent_net
561                            ),
562                        });
563                    }
564                }
565                None => {}
566            }
567        }
568    }
569}
570
571fn width_of(ty: &GroundType) -> u32 {
572    match ty {
573        GroundType::UInt { width } | GroundType::SInt { width } => *width,
574        GroundType::Clock | GroundType::Reset | GroundType::Bool | GroundType::Analog => 1,
575    }
576}
577
578/// Crate-internal entry used by builder to finish elaborate.
579pub fn seal_from_builder(hir: BuilderOwnedHir) -> Result<FrozenHir, Diagnostics> {
580    freeze(hir.0)
581}
582
583/// Opaque wrapper so builder can hold Hir without re-exporting the type.
584pub struct BuilderOwnedHir(pub(crate) Hir);
585
586impl BuilderOwnedHir {
587    pub fn new(name: impl Into<String>) -> Self {
588        Self(Hir::new(name))
589    }
590
591    pub fn add_module(&mut self, module: Module) {
592        self.0.circuit.modules.push(module);
593    }
594
595    pub fn circuit_mut(&mut self) -> &mut Circuit {
596        &mut self.0.circuit
597    }
598}
599
600#[cfg(test)]
601mod tests {
602    use super::*;
603
604    #[test]
605    fn freeze_requires_module() {
606        let hir = BuilderOwnedHir::new("empty");
607        assert!(seal_from_builder(hir).is_err());
608    }
609
610    #[test]
611    fn freeze_simple_module() {
612        let mut hir = BuilderOwnedHir::new("Top");
613        hir.add_module(Module {
614            name: "Top".into(),
615            ports: vec![
616                Port {
617                    name: "clk".into(),
618                    direction: PortDirection::Input,
619                    ty: GroundType::Clock,
620                    span: Span::default(),
621                },
622                Port {
623                    name: "rst".into(),
624                    direction: PortDirection::Input,
625                    ty: GroundType::Reset,
626                    span: Span::default(),
627                },
628                Port {
629                    name: "data_in".into(),
630                    direction: PortDirection::Input,
631                    ty: GroundType::UInt { width: 8 },
632                    span: Span::default(),
633                },
634                Port {
635                    name: "data_out".into(),
636                    direction: PortDirection::Output,
637                    ty: GroundType::UInt { width: 8 },
638                    span: Span::default(),
639                },
640            ],
641            body: vec![],
642            span: Span::default(),
643        });
644        let frozen = seal_from_builder(hir).unwrap();
645        assert_eq!(frozen.abi_name, "Top");
646        assert_eq!(frozen.circuit().modules[0].ports.len(), 4);
647    }
648
649    #[test]
650    fn analog_on_top_ok() {
651        let mut hir = BuilderOwnedHir::new("PadTop");
652        hir.add_module(Module {
653            name: "PadTop".into(),
654            ports: vec![
655                Port {
656                    name: "clk".into(),
657                    direction: PortDirection::Input,
658                    ty: GroundType::Clock,
659                    span: Span::default(),
660                },
661                Port {
662                    name: "rst".into(),
663                    direction: PortDirection::Input,
664                    ty: GroundType::Reset,
665                    span: Span::default(),
666                },
667                Port {
668                    name: "pad".into(),
669                    direction: PortDirection::InOut,
670                    ty: GroundType::Analog,
671                    span: Span::default(),
672                },
673            ],
674            body: vec![],
675            span: Span::default(),
676        });
677        assert!(seal_from_builder(hir).is_ok());
678    }
679
680    #[test]
681    fn analog_on_non_top_rejected() {
682        let mut hir = BuilderOwnedHir::new("Top");
683        hir.add_module(Module {
684            name: "Child".into(),
685            ports: vec![
686                Port {
687                    name: "clk".into(),
688                    direction: PortDirection::Input,
689                    ty: GroundType::Clock,
690                    span: Span::default(),
691                },
692                Port {
693                    name: "rst".into(),
694                    direction: PortDirection::Input,
695                    ty: GroundType::Reset,
696                    span: Span::default(),
697                },
698                Port {
699                    name: "pad".into(),
700                    direction: PortDirection::InOut,
701                    ty: GroundType::Analog,
702                    span: Span::default(),
703                },
704            ],
705            body: vec![],
706            span: Span::default(),
707        });
708        let err = seal_from_builder(hir).unwrap_err();
709        assert!(err.0.iter().any(|d| d.code == "rhdl::E0270"));
710    }
711}