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