From b2dc080a80807d09c1030bf024f92e235868e5fe Mon Sep 17 00:00:00 2001 From: rollrat Date: Sun, 24 May 2026 23:52:35 +0900 Subject: [PATCH 01/19] Document Verilog frontend plan --- AGENTS.md | 1 + .../plans/2026-05-24-verilog-frontend.md | 1372 +++++++++++++++++ 2 files changed, 1373 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-24-verilog-frontend.md diff --git a/AGENTS.md b/AGENTS.md index 11403fe..786012d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,6 +5,7 @@ - Local placer improvement context: `docs/local_placer_improvement.md` - Hierarchical placer design and SCC/primitive layout strategy: `docs/hierarchical_placer_design.md` - Sequential primitive and soft macro placement notes: `docs/sequential_primitives.md` +- Verilog frontend incremental implementation plan: `docs/superpowers/plans/2026-05-24-verilog-frontend.md` ## Documentation diff --git a/docs/superpowers/plans/2026-05-24-verilog-frontend.md b/docs/superpowers/plans/2026-05-24-verilog-frontend.md new file mode 100644 index 0000000..6e51134 --- /dev/null +++ b/docs/superpowers/plans/2026-05-24-verilog-frontend.md @@ -0,0 +1,1372 @@ +# Verilog Frontend Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a small, testable Verilog frontend that lowers easy `assign`-based combinational modules into the existing `LogicGraph` pipeline before adding broader RTL features. + +**Architecture:** Start with no external Verilog dependency: parse a deliberately small Verilog subset into a local AST, render expressions into fully parenthesized logic strings, and reuse `LogicGraph::from_stmt` plus a new multi-assignment helper. Keep parser, AST, and lowering in separate files so a real parser such as `sv-parser` can replace only the source parsing layer later. + +**Tech Stack:** Rust 2021, existing `LogicGraph`, `Graph::merge`, `eyre`, `cargo test --release` for local placer and place-and-route tests. + +--- + +## Scope + +Build this in increasing order of value: + +1. Multi-assignment combinational graph helper. +2. Verilog-friendly identifier support in the existing expression parser. +3. Tiny Verilog AST and source parser for scalar `module`, `input`, `output`, `wire`, and `assign`. +4. Verilog expression lowering for `~`, `&`, `^`, `|`, and parentheses with Verilog precedence. +5. CLI parse-and-prepare integration. +6. Small-gate NBT export path only after graph lowering is stable. +7. Bus flattening and module instances as follow-up work. + +This plan intentionally excludes `always`, procedural assignments, parameters, generate blocks, signed arithmetic, memories, delays, tasks/functions, tri-state, and full SystemVerilog syntax. + +## File Structure + +- Modify `src/graph/logic.rs`: add a small public helper for merging multiple output assignments into one `LogicGraph`; extend identifier tokenization enough for common Verilog scalar names. +- Replace `src/verilog/mod.rs`: expose the frontend API and keep the old commented `sv_parser` stub out of the active path. +- Create `src/verilog/ast.rs`: define `VerilogModule`, declarations, assignments, and expression AST. +- Create `src/verilog/lexer.rs`: tokenize the supported source subset and strip comments. +- Create `src/verilog/parser.rs`: parse the supported subset into `VerilogModule`. +- Create `src/verilog/lower.rs`: convert `VerilogModule` into `LogicGraph`. +- Modify `src/main.rs`: call the frontend for `.v` files and report parse/prepare success before adding NBT output. +- Optional parser replacement path: modify `Cargo.toml` to add `sv-parser` only when the handwritten subset is not enough. + +--- + +### Task 1: Add Multi-Assignment LogicGraph Helper + +**Files:** +- Modify: `src/graph/logic.rs` + +- [ ] **Step 1: Write the failing test** + +Add this test inside `#[cfg(test)] mod tests` in `src/graph/logic.rs`. + +```rust +#[test] +fn from_assignments_builds_half_adder() -> eyre::Result<()> { + let graph = LogicGraph::from_assignments([ + ("s".to_owned(), "a^b".to_owned()), + ("c".to_owned(), "a&b".to_owned()), + ])?; + let table = graph.truth_table()?; + + assert_eq!(table.input_names, vec!["a", "b"]); + assert_eq!(table.output_tables["s"], vec![false, true, true, false]); + assert_eq!(table.output_tables["c"], vec![false, false, false, true]); + + Ok(()) +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: + +```powershell +cargo test --release graph::logic::tests::from_assignments_builds_half_adder +``` + +Expected: fail to compile because `LogicGraph::from_assignments` does not exist. + +- [ ] **Step 3: Implement the helper** + +Add this method to `impl LogicGraph` in `src/graph/logic.rs`. + +```rust +pub fn from_assignments(assignments: I) -> eyre::Result +where + I: IntoIterator, +{ + let mut graphs = assignments + .into_iter() + .map(|(output, expr)| LogicGraph::from_stmt(&expr, &output)) + .collect::>>()? + .into_iter(); + + let Some(mut graph) = graphs.next() else { + eyre::bail!("expected at least one logic assignment"); + }; + + for next in graphs { + graph.graph.merge(next.graph); + } + + Ok(graph) +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: + +```powershell +cargo test --release graph::logic::tests::from_assignments_builds_half_adder +``` + +Expected: pass. + +- [ ] **Step 5: Commit** + +```powershell +git add src/graph/logic.rs +git commit -m "Add multi-assignment logic graph helper" +``` + +--- + +### Task 2: Accept Common Verilog Identifiers In Logic Expressions + +**Files:** +- Modify: `src/graph/logic.rs` + +- [ ] **Step 1: Write the failing test** + +Add this test inside `#[cfg(test)] mod tests` in `src/graph/logic.rs`. + +```rust +#[test] +fn logic_parser_accepts_verilog_style_identifiers() -> eyre::Result<()> { + let graph = LogicGraph::from_stmt("A_0&carry_in", "SUM_0")?; + let table = graph.truth_table()?; + + assert_eq!(table.input_names, vec!["A_0", "carry_in"]); + assert_eq!(table.output_tables["SUM_0"], vec![false, false, false, true]); + + Ok(()) +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: + +```powershell +cargo test --release graph::logic::tests::logic_parser_accepts_verilog_style_identifiers +``` + +Expected: panic or fail while lexing `A_0`. + +- [ ] **Step 3: Extend identifier scanning** + +Replace the identifier branch in `LogicGraphBuilder::next` with helper predicates. + +```rust +fn is_ident_start(ch: char) -> bool { + ch == '_' || ch.is_ascii_alphabetic() +} + +fn is_ident_continue(ch: char) -> bool { + ch == '_' || ch == '$' || ch.is_ascii_alphanumeric() +} +``` + +Then change the match arm from the lowercase-only branch to: + +```rust +ch if is_ident_start(ch) => { + let mut result = String::new(); + + while self.stmt.len() != next_ptr + && is_ident_continue(self.stmt.chars().nth(next_ptr).unwrap()) + { + result.push(self.stmt.chars().nth(next_ptr).unwrap()); + next_ptr = self.next_ptr(); + } + + self.ptr -= 1; + + LogicStringTokenType::Ident(result) +} +``` + +- [ ] **Step 4: Run focused tests** + +Run: + +```powershell +cargo test --release graph::logic::tests +``` + +Expected: both pass. + +- [ ] **Step 5: Commit** + +```powershell +git add src/graph/logic.rs +git commit -m "Accept Verilog-style logic identifiers" +``` + +--- + +### Task 3: Define Minimal Verilog AST + +**Files:** +- Create: `src/verilog/ast.rs` +- Modify: `src/verilog/mod.rs` + +- [ ] **Step 1: Write the AST file** + +Create `src/verilog/ast.rs`. + +```rust +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct VerilogModule { + pub name: String, + pub ports: Vec, + pub declarations: Vec, + pub assignments: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Declaration { + pub direction: Option, + pub names: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PortDirection { + Input, + Output, + Wire, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Assignment { + pub output: String, + pub expr: Expr, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Expr { + Ident(String), + Not(Box), + Binary { + op: BinaryOp, + left: Box, + right: Box, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BinaryOp { + And, + Xor, + Or, +} + +impl Expr { + pub fn to_logic_stmt(&self) -> String { + match self { + Expr::Ident(name) => name.clone(), + Expr::Not(expr) => format!("~({})", expr.to_logic_stmt()), + Expr::Binary { op, left, right } => { + let op = match op { + BinaryOp::And => "&", + BinaryOp::Xor => "^", + BinaryOp::Or => "|", + }; + format!("({}{}{})", left.to_logic_stmt(), op, right.to_logic_stmt()) + } + } + } +} +``` + +- [ ] **Step 2: Expose the module** + +Replace `src/verilog/mod.rs` with: + +```rust +pub mod ast; +``` + +- [ ] **Step 3: Add a unit test for expression rendering** + +Add this to `src/verilog/ast.rs`. + +```rust +#[cfg(test)] +mod tests { + use super::{BinaryOp, Expr}; + + #[test] + fn expr_renders_fully_parenthesized_logic_stmt() { + let expr = Expr::Binary { + op: BinaryOp::Xor, + left: Box::new(Expr::Ident("a".to_owned())), + right: Box::new(Expr::Binary { + op: BinaryOp::And, + left: Box::new(Expr::Ident("b".to_owned())), + right: Box::new(Expr::Ident("c".to_owned())), + }), + }; + + assert_eq!(expr.to_logic_stmt(), "(a^(b&c))"); + } +} +``` + +- [ ] **Step 4: Run AST test** + +Run: + +```powershell +cargo test --release verilog::ast::tests::expr_renders_fully_parenthesized_logic_stmt +``` + +Expected: pass. + +- [ ] **Step 5: Commit** + +```powershell +git add src/verilog/mod.rs src/verilog/ast.rs +git commit -m "Add minimal Verilog AST" +``` + +--- + +### Task 4: Add Lexer For Supported Verilog Subset + +**Files:** +- Create: `src/verilog/lexer.rs` +- Modify: `src/verilog/mod.rs` + +- [ ] **Step 1: Write failing lexer test** + +Create `src/verilog/lexer.rs` with token definitions and this test first. + +```rust +#[cfg(test)] +mod tests { + use super::{lex, Token}; + + #[test] + fn lexes_combinational_module_subset() -> eyre::Result<()> { + let tokens = lex( + r#" + module half_adder(a, b, s, c); + input a, b; + output s, c; + assign s = a ^ b; + assign c = a & b; + endmodule + "#, + )?; + + assert_eq!( + tokens, + vec![ + Token::Module, + Token::Ident("half_adder".to_owned()), + Token::LParen, + Token::Ident("a".to_owned()), + Token::Comma, + Token::Ident("b".to_owned()), + Token::Comma, + Token::Ident("s".to_owned()), + Token::Comma, + Token::Ident("c".to_owned()), + Token::RParen, + Token::Semi, + Token::Input, + Token::Ident("a".to_owned()), + Token::Comma, + Token::Ident("b".to_owned()), + Token::Semi, + Token::Output, + Token::Ident("s".to_owned()), + Token::Comma, + Token::Ident("c".to_owned()), + Token::Semi, + Token::Assign, + Token::Ident("s".to_owned()), + Token::Eq, + Token::Ident("a".to_owned()), + Token::Xor, + Token::Ident("b".to_owned()), + Token::Semi, + Token::Assign, + Token::Ident("c".to_owned()), + Token::Eq, + Token::Ident("a".to_owned()), + Token::And, + Token::Ident("b".to_owned()), + Token::Semi, + Token::EndModule, + ] + ); + + Ok(()) + } +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: + +```powershell +cargo test --release verilog::lexer::tests::lexes_combinational_module_subset +``` + +Expected: fail because `lexer` is not exported or `lex` is not implemented. + +- [ ] **Step 3: Implement tokenization** + +Add this implementation to `src/verilog/lexer.rs`. + +```rust +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Token { + Module, + EndModule, + Input, + Output, + Wire, + Assign, + Ident(String), + LParen, + RParen, + Comma, + Semi, + Eq, + Not, + And, + Xor, + Or, +} + +pub fn lex(source: &str) -> eyre::Result> { + let chars = strip_comments(source).chars().collect::>(); + let mut tokens = Vec::new(); + let mut index = 0; + + while index < chars.len() { + let ch = chars[index]; + if ch.is_whitespace() { + index += 1; + continue; + } + + match ch { + '(' => tokens.push(Token::LParen), + ')' => tokens.push(Token::RParen), + ',' => tokens.push(Token::Comma), + ';' => tokens.push(Token::Semi), + '=' => tokens.push(Token::Eq), + '~' => tokens.push(Token::Not), + '&' => tokens.push(Token::And), + '^' => tokens.push(Token::Xor), + '|' => tokens.push(Token::Or), + ch if is_ident_start(ch) => { + let start = index; + index += 1; + while index < chars.len() && is_ident_continue(chars[index]) { + index += 1; + } + let text = chars[start..index].iter().collect::(); + tokens.push(match text.as_str() { + "module" => Token::Module, + "endmodule" => Token::EndModule, + "input" => Token::Input, + "output" => Token::Output, + "wire" => Token::Wire, + "assign" => Token::Assign, + _ => Token::Ident(text), + }); + continue; + } + _ => eyre::bail!("unsupported Verilog character `{ch}` at byte-like index {index}"), + } + + index += 1; + } + + Ok(tokens) +} + +fn strip_comments(source: &str) -> String { + let mut result = String::new(); + let mut chars = source.chars().peekable(); + while let Some(ch) = chars.next() { + if ch == '/' && chars.peek() == Some(&'/') { + chars.next(); + for next in chars.by_ref() { + if next == '\n' { + result.push('\n'); + break; + } + } + } else { + result.push(ch); + } + } + result +} + +fn is_ident_start(ch: char) -> bool { + ch == '_' || ch.is_ascii_alphabetic() +} + +fn is_ident_continue(ch: char) -> bool { + ch == '_' || ch == '$' || ch.is_ascii_alphanumeric() +} +``` + +- [ ] **Step 4: Export lexer** + +Update `src/verilog/mod.rs`. + +```rust +pub mod ast; +pub mod lexer; +``` + +- [ ] **Step 5: Run lexer test** + +Run: + +```powershell +cargo test --release verilog::lexer::tests::lexes_combinational_module_subset +``` + +Expected: pass. + +- [ ] **Step 6: Commit** + +```powershell +git add src/verilog/mod.rs src/verilog/lexer.rs +git commit -m "Add minimal Verilog lexer" +``` + +--- + +### Task 5: Parse Scalar Module Declarations And Assignments + +**Files:** +- Create: `src/verilog/parser.rs` +- Modify: `src/verilog/mod.rs` + +- [ ] **Step 1: Write parser tests** + +Create `src/verilog/parser.rs` and add these tests first. + +```rust +#[cfg(test)] +mod tests { + use super::parse_module; + use crate::verilog::ast::{BinaryOp, Expr, PortDirection}; + + #[test] + fn parses_half_adder_module() -> eyre::Result<()> { + let module = parse_module( + r#" + module half_adder(a, b, s, c); + input a, b; + output s, c; + wire tmp; + assign s = a ^ b; + assign c = a & b; + endmodule + "#, + )?; + + assert_eq!(module.name, "half_adder"); + assert_eq!(module.ports, vec!["a", "b", "s", "c"]); + assert_eq!(module.declarations[0].direction, Some(PortDirection::Input)); + assert_eq!(module.declarations[1].direction, Some(PortDirection::Output)); + assert_eq!(module.declarations[2].direction, Some(PortDirection::Wire)); + assert_eq!(module.assignments.len(), 2); + assert_eq!(module.assignments[0].output, "s"); + assert_eq!( + module.assignments[0].expr, + Expr::Binary { + op: BinaryOp::Xor, + left: Box::new(Expr::Ident("a".to_owned())), + right: Box::new(Expr::Ident("b".to_owned())), + } + ); + + Ok(()) + } + + #[test] + fn parses_verilog_operator_precedence() -> eyre::Result<()> { + let module = parse_module( + r#" + module precedence(a, b, c, y); + input a, b, c; + output y; + assign y = a ^ b & c; + endmodule + "#, + )?; + + assert_eq!(module.assignments[0].expr.to_logic_stmt(), "(a^(b&c))"); + + Ok(()) + } +} +``` + +- [ ] **Step 2: Run parser tests to verify they fail** + +Run: + +```powershell +cargo test --release verilog::parser::tests +``` + +Expected: fail because parser is not implemented. + +- [ ] **Step 3: Implement parser skeleton** + +Use `crate::verilog::lexer::lex` and a cursor over `Vec`. The public entry point should be: + +```rust +pub fn parse_module(source: &str) -> eyre::Result { + Parser::new(lex(source)?).parse_module() +} +``` + +Define: + +```rust +struct Parser { + tokens: Vec, + index: usize, +} +``` + +Add methods: + +```rust +impl Parser { + fn new(tokens: Vec) -> Self { + Self { tokens, index: 0 } + } + + fn parse_module(&mut self) -> eyre::Result { + self.expect(Token::Module)?; + let name = self.expect_ident()?; + self.expect(Token::LParen)?; + let ports = self.parse_ident_list()?; + self.expect(Token::RParen)?; + self.expect(Token::Semi)?; + + let mut declarations = Vec::new(); + let mut assignments = Vec::new(); + while !self.consume(&Token::EndModule) { + match self.peek() { + Some(Token::Input) | Some(Token::Output) | Some(Token::Wire) => { + declarations.push(self.parse_declaration()?); + } + Some(Token::Assign) => assignments.push(self.parse_assignment()?), + Some(token) => eyre::bail!("unsupported Verilog token in module body: {token:?}"), + None => eyre::bail!("expected endmodule"), + } + } + + Ok(VerilogModule { + name, + ports, + declarations, + assignments, + }) + } +} +``` + +- [ ] **Step 4: Implement declarations, assignments, and expressions** + +Expression precedence must be: + +```text +primary / unary ~ +& +^ +| +``` + +Implement methods with these signatures: + +```rust +fn parse_declaration(&mut self) -> eyre::Result; +fn parse_assignment(&mut self) -> eyre::Result; +fn parse_expr(&mut self) -> eyre::Result; +fn parse_or(&mut self) -> eyre::Result; +fn parse_xor(&mut self) -> eyre::Result; +fn parse_and(&mut self) -> eyre::Result; +fn parse_unary(&mut self) -> eyre::Result; +fn parse_primary(&mut self) -> eyre::Result; +``` + +Use left-associative binary construction: + +```rust +expr = Expr::Binary { + op: BinaryOp::And, + left: Box::new(expr), + right: Box::new(rhs), +}; +``` + +- [ ] **Step 5: Export parser** + +Update `src/verilog/mod.rs`. + +```rust +pub mod ast; +pub mod lexer; +pub mod parser; +``` + +- [ ] **Step 6: Run parser tests** + +Run: + +```powershell +cargo test --release verilog::parser::tests +``` + +Expected: pass. + +- [ ] **Step 7: Commit** + +```powershell +git add src/verilog/mod.rs src/verilog/parser.rs +git commit -m "Parse minimal combinational Verilog modules" +``` + +--- + +### Task 6: Lower Parsed Verilog To LogicGraph + +**Files:** +- Create: `src/verilog/lower.rs` +- Modify: `src/verilog/mod.rs` + +- [ ] **Step 1: Write lowering tests** + +Create `src/verilog/lower.rs` and add these tests first. + +```rust +#[cfg(test)] +mod tests { + use super::lower_module; + use crate::verilog::parser::parse_module; + + #[test] + fn lowers_half_adder_to_truth_table() -> eyre::Result<()> { + let module = parse_module( + r#" + module half_adder(a, b, s, c); + input a, b; + output s, c; + assign s = a ^ b; + assign c = a & b; + endmodule + "#, + )?; + + let graph = lower_module(&module)?; + let table = graph.truth_table()?; + + assert_eq!(table.input_names, vec!["a", "b"]); + assert_eq!(table.output_tables["s"], vec![false, true, true, false]); + assert_eq!(table.output_tables["c"], vec![false, false, false, true]); + + Ok(()) + } + + #[test] + fn lowers_full_adder_with_intermediate_output_use() -> eyre::Result<()> { + let module = parse_module( + r#" + module full_adder(a, b, cin, sum, cout); + input a, b, cin; + output sum, cout; + assign sum = (a ^ b) ^ cin; + assign cout = (a & b) | (sum & cin); + endmodule + "#, + )?; + + let graph = lower_module(&module)?; + let table = graph.truth_table()?; + let sum = (0..8) + .map(|mask: usize| mask.count_ones() % 2 == 1) + .collect::>(); + let carry = (0..8) + .map(|mask: usize| mask.count_ones() >= 2) + .collect::>(); + + assert_eq!(table.output_tables["sum"], sum); + assert_eq!(table.output_tables["cout"], carry); + + Ok(()) + } +} +``` + +- [ ] **Step 2: Run lowering tests to verify they fail** + +Run: + +```powershell +cargo test --release verilog::lower::tests +``` + +Expected: fail because `lower_module` is not implemented or not exported. + +- [ ] **Step 3: Implement lower_module** + +Add: + +```rust +use crate::graph::logic::LogicGraph; +use crate::verilog::ast::VerilogModule; + +pub fn lower_module(module: &VerilogModule) -> eyre::Result { + if module.assignments.is_empty() { + eyre::bail!("module `{}` has no continuous assignments", module.name); + } + + LogicGraph::from_assignments( + module + .assignments + .iter() + .map(|assign| (assign.output.clone(), assign.expr.to_logic_stmt())), + ) +} +``` + +- [ ] **Step 4: Export lower and public helpers** + +Update `src/verilog/mod.rs`. + +```rust +pub mod ast; +pub mod lexer; +pub mod lower; +pub mod parser; + +use std::fs; +use std::path::Path; + +use crate::graph::logic::LogicGraph; + +pub fn load_logic_graph(path: impl AsRef) -> eyre::Result { + let source = fs::read_to_string(path)?; + let module = parser::parse_module(&source)?; + lower::lower_module(&module) +} +``` + +- [ ] **Step 5: Run lowering tests** + +Run: + +```powershell +cargo test --release verilog::lower::tests +``` + +Expected: pass. + +- [ ] **Step 6: Commit** + +```powershell +git add src/verilog/mod.rs src/verilog/lower.rs +git commit -m "Lower minimal Verilog to logic graph" +``` + +--- + +### Task 7: Add Unsupported-Construct Diagnostics + +**Files:** +- Modify: `src/verilog/lexer.rs` +- Modify: `src/verilog/parser.rs` + +- [ ] **Step 1: Write tests for clear failures** + +Add these tests to `src/verilog/parser.rs`. + +```rust +#[test] +fn rejects_always_blocks_with_clear_message() { + let error = parse_module( + r#" + module bad(clk, q); + input clk; + output q; + always @(posedge clk) q = ~q; + endmodule + "#, + ) + .unwrap_err(); + + assert!(error.to_string().contains("unsupported")); +} + +#[test] +fn rejects_vector_declarations_with_clear_message() { + let error = parse_module( + r#" + module bad(a, y); + input [3:0] a; + output y; + assign y = a; + endmodule + "#, + ) + .unwrap_err(); + + assert!(error.to_string().contains("unsupported")); +} +``` + +- [ ] **Step 2: Run tests to see current failure behavior** + +Run: + +```powershell +cargo test --release verilog::parser::tests +``` + +Expected: fail if the lexer reports a confusing character error or parser misses the unsupported construct. + +- [ ] **Step 3: Add keyword tokens for common rejected constructs** + +In `src/verilog/lexer.rs`, add token variants: + +```rust +Always, +At, +LBracket, +RBracket, +Colon, +``` + +Map: + +```rust +"always" => Token::Always, +"posedge" => Token::Ident("posedge".to_owned()), +``` + +And character tokens: + +```rust +'@' => tokens.push(Token::At), +'[' => tokens.push(Token::LBracket), +']' => tokens.push(Token::RBracket), +':' => tokens.push(Token::Colon), +``` + +- [ ] **Step 4: Reject unsupported constructs in parser** + +In module-body parsing, add explicit branches: + +```rust +Some(Token::Always) => eyre::bail!("unsupported Verilog construct: always block"), +Some(Token::LBracket) => eyre::bail!("unsupported Verilog construct: vector declaration"), +``` + +In `parse_declaration`, reject a bracket immediately after the direction: + +```rust +if matches!(self.peek(), Some(Token::LBracket)) { + eyre::bail!("unsupported Verilog construct: vector declaration"); +} +``` + +- [ ] **Step 5: Run diagnostics tests** + +Run: + +```powershell +cargo test --release verilog::parser::tests +``` + +Expected: pass. + +- [ ] **Step 6: Commit** + +```powershell +git add src/verilog/lexer.rs src/verilog/parser.rs +git commit -m "Report unsupported Verilog constructs clearly" +``` + +--- + +### Task 8: Wire Verilog Graph Loading Into CLI + +**Files:** +- Modify: `src/main.rs` + +- [ ] **Step 1: Write or run a manual fixture** + +Use existing `test/alu.v` only after fixing it to valid continuous assignments in a separate task. For this task, use a temporary valid file: + +```verilog +module half_adder(a, b, s, c); + input a, b; + output s, c; + assign s = a ^ b; + assign c = a & b; +endmodule +``` + +- [ ] **Step 2: Implement parse-and-prepare path** + +Update `main`: + +```rust +fn main() -> eyre::Result<()> { + tracing_subscriber::fmt::init(); + let opt = CompilerOption::from_args(); + + if opt.input.extension().and_then(|ext| ext.to_str()) == Some("v") { + let graph = redstone_compiler::verilog::load_logic_graph(&opt.input)?; + let prepared = graph.prepare_place()?; + println!( + "loaded Verilog graph: nodes={} inputs={} outputs={}", + prepared.nodes.len(), + prepared.inputs().len(), + prepared.outputs().len() + ); + return Ok(()); + } + + eyre::bail!("unsupported input file extension: {:?}", opt.input); +} +``` + +If `main.rs` cannot refer to the library as `redstone_compiler` in this package layout, use: + +```rust +use redstone_compiler::verilog; +``` + +and call: + +```rust +let graph = verilog::load_logic_graph(&opt.input)?; +``` + +- [ ] **Step 3: Run CLI against the temporary fixture** + +Run: + +```powershell +cargo run --release -- test/half-adder-verilog-smoke.v +``` + +Expected output contains: + +```text +loaded Verilog graph: +``` + +- [ ] **Step 4: Add a checked-in smoke fixture** + +Create `test/half-adder.v`: + +```verilog +module half_adder(a, b, s, c); + input a, b; + output s, c; + + assign s = a ^ b; + assign c = a & b; +endmodule +``` + +Run: + +```powershell +cargo run --release -- test/half-adder.v +``` + +Expected: same parse-and-prepare success output. + +- [ ] **Step 5: Commit** + +```powershell +git add src/main.rs test/half-adder.v +git commit -m "Load minimal Verilog from CLI" +``` + +--- + +### Task 9: Add Tiny-Gate NBT Export From Verilog + +**Files:** +- Modify: `src/main.rs` + +- [ ] **Step 1: Keep export limited to tiny graphs** + +Do not try full adder NBT export in this task. Start with `and` or `half_adder`; `full_adder` placement is already a separate local placer quality problem. + +- [ ] **Step 2: Add export path when output is provided** + +In `main.rs`, after `prepare_place()`, if `opt.output` is `Some(path)`, run local placement and save the first sampled world: + +```rust +use redstone_compiler::nbt::ToNBT; +use redstone_compiler::transform::place_and_route::local_placer::{ + InputPlacementStrategy, LocalPlacer, LocalPlacerConfig, NotRouteStrategy, + PlacementSamplingPolicy, SamplingPolicy, TorchPlacementStrategy, +}; +use redstone_compiler::world::position::DimSize; +``` + +Use a conservative config copied from the existing AND/half-adder component tests: + +```rust +let config = LocalPlacerConfig { + random_seed: 42, + greedy_input_generation: true, + input_placement_strategy: InputPlacementStrategy::Boundary, + input_candidate_limit: None, + step_sampling_policy: SamplingPolicy::Random(10000), + placement_sampling_policy: PlacementSamplingPolicy::StepPolicy, + leak_sampling: false, + route_torch_directly: true, + torch_placement_strategy: TorchPlacementStrategy::DirectOnly, + not_route_strategy: NotRouteStrategy::DirectOnly, + max_not_route_step: 3, + not_route_step_sampling_policy: SamplingPolicy::Random(100), + max_route_step: 3, + route_step_sampling_policy: SamplingPolicy::Random(100), +}; +let placer = LocalPlacer::new(prepared.clone(), config)?; +let worlds = placer.generate(DimSize(10, 10, 5), None); +let Some(world) = worlds.into_iter().next() else { + eyre::bail!("placement produced no worlds"); +}; +world.to_nbt().save(output); +``` + +- [ ] **Step 3: Run export smoke test** + +Run: + +```powershell +cargo run --release -- test/half-adder.v test/half-adder-generated-from-verilog.nbt +``` + +Expected: command exits successfully and creates `test/half-adder-generated-from-verilog.nbt`. + +- [ ] **Step 4: Verify generated NBT can be read** + +Run: + +```powershell +cargo run --release --bin check_nbt_world_cycle -- test/half-adder-generated-from-verilog.nbt +``` + +Expected: command exits successfully. If equivalence checking for this filename is not recognized by the binary, success here only proves the NBT is readable; graph equivalence should be added as a separate test. + +- [ ] **Step 5: Commit** + +```powershell +git add src/main.rs test/half-adder-generated-from-verilog.nbt +git commit -m "Export tiny Verilog circuits to NBT" +``` + +--- + +### Task 10: Add Bus Flattening As The First Real Extension + +**Files:** +- Modify: `src/verilog/ast.rs` +- Modify: `src/verilog/lexer.rs` +- Modify: `src/verilog/parser.rs` +- Modify: `src/verilog/lower.rs` + +- [ ] **Step 1: Add test for vector declarations and bit selects** + +Add parser/lowering test: + +```rust +#[test] +fn lowers_vector_bit_selects_by_flattening_names() -> eyre::Result<()> { + let module = parse_module( + r#" + module bit_xor(a, y); + input [1:0] a; + output y; + assign y = a[0] ^ a[1]; + endmodule + "#, + )?; + + let graph = lower_module(&module)?; + let table = graph.truth_table()?; + + assert_eq!(table.input_names, vec!["a_0", "a_1"]); + assert_eq!(table.output_tables["y"], vec![false, true, true, false]); + + Ok(()) +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: + +```powershell +cargo test --release verilog::lower::tests::lowers_vector_bit_selects_by_flattening_names +``` + +Expected: fail because vectors are rejected. + +- [ ] **Step 3: Extend AST declarations** + +Change `Declaration`: + +```rust +pub struct Declaration { + pub direction: Option, + pub range: Option, + pub names: Vec, +} + +pub struct Range { + pub msb: usize, + pub lsb: usize, +} +``` + +- [ ] **Step 4: Parse ranges and bit selects** + +Support only numeric `[msb:lsb]` declarations and single-bit `name[index]` references. Lower bit select names to `format!("{name}_{index}")`. + +- [ ] **Step 5: Run vector test** + +Run: + +```powershell +cargo test --release verilog::lower::tests::lowers_vector_bit_selects_by_flattening_names +``` + +Expected: pass. + +- [ ] **Step 6: Commit** + +```powershell +git add src/verilog/ast.rs src/verilog/lexer.rs src/verilog/parser.rs src/verilog/lower.rs +git commit -m "Flatten simple Verilog bit selects" +``` + +--- + +### Task 11: Add Named Module Instance Lowering + +**Files:** +- Modify: `src/verilog/ast.rs` +- Modify: `src/verilog/parser.rs` +- Modify: `src/verilog/lower.rs` +- Consider: `src/graph/module.rs` + +- [ ] **Step 1: Add test for two half adders composed structurally** + +Use a source with two modules: + +```verilog +module half_adder(a, b, s, c); + input a, b; + output s, c; + assign s = a ^ b; + assign c = a & b; +endmodule + +module two_half_adders(a, b, cin, sum, carry0, carry1); + input a, b, cin; + output sum, carry0, carry1; + wire s0; + half_adder ha0(.a(a), .b(b), .s(s0), .c(carry0)); + half_adder ha1(.a(s0), .b(cin), .s(sum), .c(carry1)); +endmodule +``` + +The lowering test should assert the `sum` truth table matches `a ^ b ^ cin`. + +- [ ] **Step 2: Parse multiple modules** + +Add: + +```rust +pub fn parse_modules(source: &str) -> eyre::Result>; +``` + +Keep `parse_module` as a wrapper that requires exactly one module. + +- [ ] **Step 3: Parse named instances only** + +Add AST: + +```rust +pub struct Instance { + pub module_name: String, + pub instance_name: String, + pub connections: Vec<(String, String)>, +} +``` + +Reject positional instances with `unsupported Verilog construct: positional instance ports`. + +- [ ] **Step 4: Lower instances by inlining** + +For each instance, clone the child module assignments and rename local signals with `instance_name__signal`. Apply named port substitutions before calling `LogicGraph::from_assignments`. + +- [ ] **Step 5: Run instance test** + +Run: + +```powershell +cargo test --release verilog::lower::tests::lowers_named_module_instances_by_inlining +``` + +Expected: pass. + +- [ ] **Step 6: Commit** + +```powershell +git add src/verilog/ast.rs src/verilog/parser.rs src/verilog/lower.rs +git commit -m "Inline simple named Verilog module instances" +``` + +--- + +## Verification Commands + +Run focused tests after each task. At the end of Tasks 1 through 8, run: + +```powershell +cargo test --release graph::logic::tests +cargo test --release verilog:: +``` + +Before claiming the frontend is ready for local placement, run: + +```powershell +cargo test --release +``` + +This project explicitly prefers `cargo test --release` because local placer and place-and-route tests are too slow in debug builds. + +## Implementation Notes + +- Keep Verilog lowering combinational until sequential semantics are explicit. +- Render parsed expressions as fully parenthesized strings before passing them to `LogicGraph::from_stmt`; this avoids relying on the current expression parser's precedence. +- Treat clear unsupported diagnostics as a feature. A narrow frontend that fails clearly is better than silently accepting RTL it cannot lower correctly. +- Do not use full adder NBT export as the first success criterion. Graph lowering for full adder is appropriate early; physical placement for full adder depends on ongoing local placer quality work. +- Add `sv-parser` only when the handcrafted subset becomes the bottleneck. The AST and lowering split in this plan makes that replacement local to parsing. From 1415912e98a017efa3b5603fa03b719f21d892fa Mon Sep 17 00:00:00 2001 From: rollrat Date: Sun, 24 May 2026 23:55:31 +0900 Subject: [PATCH 02/19] Add multi-assignment logic graph helper --- src/graph/logic.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/graph/logic.rs b/src/graph/logic.rs index 648eb9b..a519974 100644 --- a/src/graph/logic.rs +++ b/src/graph/logic.rs @@ -18,6 +18,27 @@ impl LogicGraph { LogicGraphBuilder::new(stmt.to_string()).build(output.to_string()) } + pub fn from_assignments(assignments: I) -> eyre::Result + where + I: IntoIterator, + { + let mut graphs = assignments + .into_iter() + .map(|(output, expr)| LogicGraph::from_stmt(&expr, &output)) + .collect::>>()? + .into_iter(); + + let Some(mut graph) = graphs.next() else { + eyre::bail!("expected at least one logic assignment"); + }; + + for next in graphs { + graph.graph.merge(next.graph); + } + + Ok(graph) + } + pub fn prepare_place(self) -> eyre::Result { let mut transform = LogicGraphTransformer::new(self); transform.decompose_xor()?; @@ -608,6 +629,21 @@ mod tests { Ok(()) } + #[test] + fn from_assignments_builds_half_adder() -> eyre::Result<()> { + let graph = LogicGraph::from_assignments([ + ("s".to_owned(), "a^b".to_owned()), + ("c".to_owned(), "a&b".to_owned()), + ])?; + let table = graph.truth_table()?; + + assert_eq!(table.input_names, vec!["a", "b"]); + assert_eq!(table.output_tables["s"], vec![false, true, true, false]); + assert_eq!(table.output_tables["c"], vec![false, false, false, true]); + + Ok(()) + } + #[test] fn unittest_logicgraph_full_adder() -> eyre::Result<()> { // s = (a ^ b) ^ cin; From 006cb3614552bfbd3671fd2d07e6be72d967b536 Mon Sep 17 00:00:00 2001 From: rollrat Date: Sun, 24 May 2026 23:57:12 +0900 Subject: [PATCH 03/19] Accept Verilog-style logic identifiers --- src/graph/logic.rs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/graph/logic.rs b/src/graph/logic.rs index a519974..9d2beb3 100644 --- a/src/graph/logic.rs +++ b/src/graph/logic.rs @@ -267,6 +267,14 @@ enum LogicStringTokenType { Eof, } +fn is_ident_start(ch: char) -> bool { + ch == '_' || ch.is_ascii_alphabetic() +} + +fn is_ident_continue(ch: char) -> bool { + ch == '_' || ch == '$' || ch.is_ascii_alphanumeric() +} + impl LogicGraphBuilder { pub fn new(stmt: String) -> Self { LogicGraphBuilder { @@ -352,11 +360,11 @@ impl LogicGraphBuilder { '(' => LogicStringTokenType::ParStart, ')' => LogicStringTokenType::ParEnd, '~' => LogicStringTokenType::Not, - 'a'..='z' => { + ch if is_ident_start(ch) => { let mut result = String::new(); while self.stmt.len() != next_ptr - && matches!(self.stmt.chars().nth(next_ptr).unwrap(), 'a'..='z' | '0'..='9') + && is_ident_continue(self.stmt.chars().nth(next_ptr).unwrap()) { result.push(self.stmt.chars().nth(next_ptr).unwrap()); next_ptr = self.next_ptr(); @@ -644,6 +652,17 @@ mod tests { Ok(()) } + #[test] + fn logic_parser_accepts_verilog_style_identifiers() -> eyre::Result<()> { + let graph = LogicGraph::from_stmt("A_0&carry_in", "SUM_0")?; + let table = graph.truth_table()?; + + assert_eq!(table.input_names, vec!["A_0", "carry_in"]); + assert_eq!(table.output_tables["SUM_0"], vec![false, false, false, true]); + + Ok(()) + } + #[test] fn unittest_logicgraph_full_adder() -> eyre::Result<()> { // s = (a ^ b) ^ cin; From a9cc3e13f6ab82bd64f073fe89d07bfbebb4b15a Mon Sep 17 00:00:00 2001 From: rollrat Date: Sun, 24 May 2026 23:58:32 +0900 Subject: [PATCH 04/19] Add minimal Verilog AST --- src/verilog/ast.rs | 81 ++++++++++++++++++++++++++++++++++++++++++++++ src/verilog/mod.rs | 16 +-------- 2 files changed, 82 insertions(+), 15 deletions(-) create mode 100644 src/verilog/ast.rs diff --git a/src/verilog/ast.rs b/src/verilog/ast.rs new file mode 100644 index 0000000..0b8040c --- /dev/null +++ b/src/verilog/ast.rs @@ -0,0 +1,81 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct VerilogModule { + pub name: String, + pub ports: Vec, + pub declarations: Vec, + pub assignments: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Declaration { + pub direction: Option, + pub names: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PortDirection { + Input, + Output, + Wire, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Assignment { + pub output: String, + pub expr: Expr, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Expr { + Ident(String), + Not(Box), + Binary { + op: BinaryOp, + left: Box, + right: Box, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BinaryOp { + And, + Xor, + Or, +} + +impl Expr { + pub fn to_logic_stmt(&self) -> String { + match self { + Expr::Ident(name) => name.clone(), + Expr::Not(expr) => format!("~({})", expr.to_logic_stmt()), + Expr::Binary { op, left, right } => { + let op = match op { + BinaryOp::And => "&", + BinaryOp::Xor => "^", + BinaryOp::Or => "|", + }; + format!("({}{}{})", left.to_logic_stmt(), op, right.to_logic_stmt()) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::{BinaryOp, Expr}; + + #[test] + fn expr_renders_fully_parenthesized_logic_stmt() { + let expr = Expr::Binary { + op: BinaryOp::Xor, + left: Box::new(Expr::Ident("a".to_owned())), + right: Box::new(Expr::Binary { + op: BinaryOp::And, + left: Box::new(Expr::Ident("b".to_owned())), + right: Box::new(Expr::Ident("c".to_owned())), + }), + }; + + assert_eq!(expr.to_logic_stmt(), "(a^(b&c))"); + } +} diff --git a/src/verilog/mod.rs b/src/verilog/mod.rs index 6d4ae4a..851c0bc 100644 --- a/src/verilog/mod.rs +++ b/src/verilog/mod.rs @@ -1,15 +1 @@ -// use std::{collections::HashMap, path::PathBuf}; - -// use sv_parser::{parse_sv, SyntaxTree}; - -// pub fn load(path: &PathBuf) -> eyre::Result { -// let defines = HashMap::new(); -// let includes: Vec = Vec::new(); - -// let result = parse_sv(path, &defines, &includes, false, false); -// let Ok((syntax_tree, _)) = result else { -// eyre::bail!("System-verilog input parse err! {}", result.err().unwrap()); -// }; - -// Ok(syntax_tree) -// } +pub mod ast; From c0a8ae84c8fea25c007df83a354a17fdf9acb076 Mon Sep 17 00:00:00 2001 From: rollrat Date: Mon, 25 May 2026 00:00:31 +0900 Subject: [PATCH 05/19] Add minimal Verilog lexer --- src/verilog/lexer.rs | 160 +++++++++++++++++++++++++++++++++++++++++++ src/verilog/mod.rs | 1 + 2 files changed, 161 insertions(+) create mode 100644 src/verilog/lexer.rs diff --git a/src/verilog/lexer.rs b/src/verilog/lexer.rs new file mode 100644 index 0000000..a87d8b1 --- /dev/null +++ b/src/verilog/lexer.rs @@ -0,0 +1,160 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Token { + Module, + EndModule, + Input, + Output, + Wire, + Assign, + Ident(String), + LParen, + RParen, + Comma, + Semi, + Eq, + Not, + And, + Xor, + Or, +} + +pub fn lex(source: &str) -> eyre::Result> { + let stripped = strip_comments(source); + let chars = stripped.chars().collect::>(); + let mut tokens = Vec::new(); + let mut index = 0; + + while index < chars.len() { + let ch = chars[index]; + if ch.is_whitespace() { + index += 1; + continue; + } + + match ch { + '(' => tokens.push(Token::LParen), + ')' => tokens.push(Token::RParen), + ',' => tokens.push(Token::Comma), + ';' => tokens.push(Token::Semi), + '=' => tokens.push(Token::Eq), + '~' => tokens.push(Token::Not), + '&' => tokens.push(Token::And), + '^' => tokens.push(Token::Xor), + '|' => tokens.push(Token::Or), + ch if is_ident_start(ch) => { + let start = index; + index += 1; + while index < chars.len() && is_ident_continue(chars[index]) { + index += 1; + } + let text = chars[start..index].iter().collect::(); + tokens.push(match text.as_str() { + "module" => Token::Module, + "endmodule" => Token::EndModule, + "input" => Token::Input, + "output" => Token::Output, + "wire" => Token::Wire, + "assign" => Token::Assign, + _ => Token::Ident(text), + }); + continue; + } + _ => eyre::bail!("unsupported Verilog character `{ch}` at byte-like index {index}"), + } + + index += 1; + } + + Ok(tokens) +} + +fn strip_comments(source: &str) -> String { + let mut result = String::new(); + let mut chars = source.chars().peekable(); + while let Some(ch) = chars.next() { + if ch == '/' && chars.peek() == Some(&'/') { + chars.next(); + for next in chars.by_ref() { + if next == '\n' { + result.push('\n'); + break; + } + } + } else { + result.push(ch); + } + } + result +} + +fn is_ident_start(ch: char) -> bool { + ch == '_' || ch.is_ascii_alphabetic() +} + +fn is_ident_continue(ch: char) -> bool { + ch == '_' || ch == '$' || ch.is_ascii_alphanumeric() +} + +#[cfg(test)] +mod tests { + use super::{lex, Token}; + + #[test] + fn lexes_combinational_module_subset() -> eyre::Result<()> { + let tokens = lex( + r#" + module half_adder(a, b, s, c); + input a, b; + output s, c; + assign s = a ^ b; + assign c = a & b; + endmodule + "#, + )?; + + assert_eq!( + tokens, + vec![ + Token::Module, + Token::Ident("half_adder".to_owned()), + Token::LParen, + Token::Ident("a".to_owned()), + Token::Comma, + Token::Ident("b".to_owned()), + Token::Comma, + Token::Ident("s".to_owned()), + Token::Comma, + Token::Ident("c".to_owned()), + Token::RParen, + Token::Semi, + Token::Input, + Token::Ident("a".to_owned()), + Token::Comma, + Token::Ident("b".to_owned()), + Token::Semi, + Token::Output, + Token::Ident("s".to_owned()), + Token::Comma, + Token::Ident("c".to_owned()), + Token::Semi, + Token::Assign, + Token::Ident("s".to_owned()), + Token::Eq, + Token::Ident("a".to_owned()), + Token::Xor, + Token::Ident("b".to_owned()), + Token::Semi, + Token::Assign, + Token::Ident("c".to_owned()), + Token::Eq, + Token::Ident("a".to_owned()), + Token::And, + Token::Ident("b".to_owned()), + Token::Semi, + Token::EndModule, + ] + ); + + Ok(()) + } +} diff --git a/src/verilog/mod.rs b/src/verilog/mod.rs index 851c0bc..c3f86b1 100644 --- a/src/verilog/mod.rs +++ b/src/verilog/mod.rs @@ -1 +1,2 @@ pub mod ast; +pub mod lexer; From b6cbbd897cdd201961101d9eae2799d795e0830a Mon Sep 17 00:00:00 2001 From: rollrat Date: Mon, 25 May 2026 00:03:06 +0900 Subject: [PATCH 06/19] Parse minimal combinational Verilog modules --- src/verilog/mod.rs | 1 + src/verilog/parser.rs | 246 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 src/verilog/parser.rs diff --git a/src/verilog/mod.rs b/src/verilog/mod.rs index c3f86b1..483656f 100644 --- a/src/verilog/mod.rs +++ b/src/verilog/mod.rs @@ -1,2 +1,3 @@ pub mod ast; pub mod lexer; +pub mod parser; diff --git a/src/verilog/parser.rs b/src/verilog/parser.rs new file mode 100644 index 0000000..aefbdbd --- /dev/null +++ b/src/verilog/parser.rs @@ -0,0 +1,246 @@ +use crate::verilog::ast::{Assignment, BinaryOp, Declaration, Expr, PortDirection, VerilogModule}; +use crate::verilog::lexer::{lex, Token}; + +pub fn parse_module(source: &str) -> eyre::Result { + Parser::new(lex(source)?).parse_module() +} + +struct Parser { + tokens: Vec, + index: usize, +} + +impl Parser { + fn new(tokens: Vec) -> Self { + Self { tokens, index: 0 } + } + + fn parse_module(&mut self) -> eyre::Result { + self.expect(Token::Module)?; + let name = self.expect_ident()?; + self.expect(Token::LParen)?; + let ports = self.parse_ident_list()?; + self.expect(Token::RParen)?; + self.expect(Token::Semi)?; + + let mut declarations = Vec::new(); + let mut assignments = Vec::new(); + while !self.consume(&Token::EndModule) { + match self.peek() { + Some(Token::Input) | Some(Token::Output) | Some(Token::Wire) => { + declarations.push(self.parse_declaration()?); + } + Some(Token::Assign) => assignments.push(self.parse_assignment()?), + Some(token) => eyre::bail!("unsupported Verilog token in module body: {token:?}"), + None => eyre::bail!("expected endmodule"), + } + } + + Ok(VerilogModule { + name, + ports, + declarations, + assignments, + }) + } + + fn parse_declaration(&mut self) -> eyre::Result { + let direction = match self.next() { + Some(Token::Input) => PortDirection::Input, + Some(Token::Output) => PortDirection::Output, + Some(Token::Wire) => PortDirection::Wire, + Some(token) => eyre::bail!("expected declaration direction, got {token:?}"), + None => eyre::bail!("expected declaration direction"), + }; + let names = self.parse_ident_list()?; + self.expect(Token::Semi)?; + + Ok(Declaration { + direction: Some(direction), + names, + }) + } + + fn parse_assignment(&mut self) -> eyre::Result { + self.expect(Token::Assign)?; + let output = self.expect_ident()?; + self.expect(Token::Eq)?; + let expr = self.parse_expr()?; + self.expect(Token::Semi)?; + + Ok(Assignment { output, expr }) + } + + fn parse_expr(&mut self) -> eyre::Result { + self.parse_or() + } + + fn parse_or(&mut self) -> eyre::Result { + let mut expr = self.parse_xor()?; + while self.consume(&Token::Or) { + let rhs = self.parse_xor()?; + expr = Expr::Binary { + op: BinaryOp::Or, + left: Box::new(expr), + right: Box::new(rhs), + }; + } + Ok(expr) + } + + fn parse_xor(&mut self) -> eyre::Result { + let mut expr = self.parse_and()?; + while self.consume(&Token::Xor) { + let rhs = self.parse_and()?; + expr = Expr::Binary { + op: BinaryOp::Xor, + left: Box::new(expr), + right: Box::new(rhs), + }; + } + Ok(expr) + } + + fn parse_and(&mut self) -> eyre::Result { + let mut expr = self.parse_unary()?; + while self.consume(&Token::And) { + let rhs = self.parse_unary()?; + expr = Expr::Binary { + op: BinaryOp::And, + left: Box::new(expr), + right: Box::new(rhs), + }; + } + Ok(expr) + } + + fn parse_unary(&mut self) -> eyre::Result { + if self.consume(&Token::Not) { + return Ok(Expr::Not(Box::new(self.parse_unary()?))); + } + + self.parse_primary() + } + + fn parse_primary(&mut self) -> eyre::Result { + match self.next() { + Some(Token::Ident(name)) => Ok(Expr::Ident(name)), + Some(Token::LParen) => { + let expr = self.parse_expr()?; + self.expect(Token::RParen)?; + Ok(expr) + } + Some(token) => eyre::bail!("expected expression, got {token:?}"), + None => eyre::bail!("expected expression"), + } + } + + fn parse_ident_list(&mut self) -> eyre::Result> { + let mut names = vec![self.expect_ident()?]; + while self.consume(&Token::Comma) { + names.push(self.expect_ident()?); + } + Ok(names) + } + + fn expect_ident(&mut self) -> eyre::Result { + match self.next() { + Some(Token::Ident(name)) => Ok(name), + Some(token) => eyre::bail!("expected identifier, got {token:?}"), + None => eyre::bail!("expected identifier"), + } + } + + fn expect(&mut self, expected: Token) -> eyre::Result<()> { + let got = self.next(); + if got == Some(expected.clone()) { + return Ok(()); + } + + eyre::bail!("expected {expected:?}, got {got:?}") + } + + fn consume(&mut self, expected: &Token) -> bool { + if self.peek() == Some(expected) { + self.index += 1; + true + } else { + false + } + } + + fn peek(&self) -> Option<&Token> { + self.tokens.get(self.index) + } + + fn next(&mut self) -> Option { + let token = self.tokens.get(self.index).cloned()?; + self.index += 1; + Some(token) + } +} + +#[cfg(test)] +mod tests { + use super::parse_module; + use crate::verilog::ast::{BinaryOp, Expr, PortDirection}; + + #[test] + fn parses_half_adder_module() -> eyre::Result<()> { + let module = parse_module( + r#" + module half_adder(a, b, s, c); + input a, b; + output s, c; + wire tmp; + assign s = a ^ b; + assign c = a & b; + endmodule + "#, + )?; + + assert_eq!(module.name, "half_adder"); + assert_eq!(module.ports, vec!["a", "b", "s", "c"]); + assert_eq!( + module.declarations[0].direction.as_ref(), + Some(&PortDirection::Input) + ); + assert_eq!( + module.declarations[1].direction.as_ref(), + Some(&PortDirection::Output) + ); + assert_eq!( + module.declarations[2].direction.as_ref(), + Some(&PortDirection::Wire) + ); + assert_eq!(module.assignments.len(), 2); + assert_eq!(module.assignments[0].output, "s"); + assert_eq!( + module.assignments[0].expr, + Expr::Binary { + op: BinaryOp::Xor, + left: Box::new(Expr::Ident("a".to_owned())), + right: Box::new(Expr::Ident("b".to_owned())), + } + ); + + Ok(()) + } + + #[test] + fn parses_verilog_operator_precedence() -> eyre::Result<()> { + let module = parse_module( + r#" + module precedence(a, b, c, y); + input a, b, c; + output y; + assign y = a ^ b & c; + endmodule + "#, + )?; + + assert_eq!(module.assignments[0].expr.to_logic_stmt(), "(a^(b&c))"); + + Ok(()) + } +} From 2b9b6d109e851449abd8a41474d85e93dc9a7f32 Mon Sep 17 00:00:00 2001 From: rollrat Date: Mon, 25 May 2026 00:05:48 +0900 Subject: [PATCH 07/19] Lower minimal Verilog to logic graph --- src/verilog/lower.rs | 74 ++++++++++++++++++++++++++++++++++++++++++++ src/verilog/mod.rs | 1 + 2 files changed, 75 insertions(+) create mode 100644 src/verilog/lower.rs diff --git a/src/verilog/lower.rs b/src/verilog/lower.rs new file mode 100644 index 0000000..01fa14d --- /dev/null +++ b/src/verilog/lower.rs @@ -0,0 +1,74 @@ +use crate::graph::logic::LogicGraph; +use crate::verilog::ast::VerilogModule; + +pub fn lower_module(module: &VerilogModule) -> eyre::Result { + if module.assignments.is_empty() { + eyre::bail!("module `{}` has no continuous assignments", module.name); + } + + LogicGraph::from_assignments( + module + .assignments + .iter() + .map(|assign| (assign.output.clone(), assign.expr.to_logic_stmt())), + ) +} + +#[cfg(test)] +mod tests { + use super::lower_module; + use crate::verilog::parser::parse_module; + + #[test] + fn lowers_half_adder_to_truth_table() -> eyre::Result<()> { + let module = parse_module( + r#" + module half_adder(a, b, s, c); + input a, b; + output s, c; + assign s = a ^ b; + assign c = a & b; + endmodule + "#, + )?; + + let graph = lower_module(&module)?; + let table = graph.truth_table()?; + + assert_eq!(table.input_names, vec!["a", "b"]); + assert_eq!(table.output_tables["s"], vec![false, true, true, false]); + assert_eq!(table.output_tables["c"], vec![false, false, false, true]); + + Ok(()) + } + + #[test] + fn lowers_full_adder_with_intermediate_output_use() -> eyre::Result<()> { + let module = parse_module( + r#" + module full_adder(a, b, cin, sum, cout); + input a, b, cin; + output sum, cout; + wire s; + assign s = a ^ b; + assign sum = s ^ cin; + assign cout = (a & b) | (s & cin); + endmodule + "#, + )?; + + let graph = lower_module(&module)?; + let table = graph.truth_table()?; + let sum = (0..8) + .map(|mask: usize| mask.count_ones() % 2 == 1) + .collect::>(); + let carry = (0..8) + .map(|mask: usize| mask.count_ones() >= 2) + .collect::>(); + + assert_eq!(table.output_tables["sum"], sum); + assert_eq!(table.output_tables["cout"], carry); + + Ok(()) + } +} diff --git a/src/verilog/mod.rs b/src/verilog/mod.rs index 483656f..11dc4eb 100644 --- a/src/verilog/mod.rs +++ b/src/verilog/mod.rs @@ -1,3 +1,4 @@ pub mod ast; pub mod lexer; +pub mod lower; pub mod parser; From f0191e164bc7c1ceb1acf69e034f16470f3bf2fa Mon Sep 17 00:00:00 2001 From: rollrat Date: Mon, 25 May 2026 00:07:26 +0900 Subject: [PATCH 08/19] Report unsupported Verilog constructs clearly --- src/verilog/lexer.rs | 21 +++++++++++++++++++++ src/verilog/parser.rs | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/src/verilog/lexer.rs b/src/verilog/lexer.rs index a87d8b1..3a10cc0 100644 --- a/src/verilog/lexer.rs +++ b/src/verilog/lexer.rs @@ -6,12 +6,18 @@ pub enum Token { Output, Wire, Assign, + Always, Ident(String), + Number(usize), LParen, RParen, + LBracket, + RBracket, Comma, Semi, + Colon, Eq, + At, Not, And, Xor, @@ -34,13 +40,27 @@ pub fn lex(source: &str) -> eyre::Result> { match ch { '(' => tokens.push(Token::LParen), ')' => tokens.push(Token::RParen), + '[' => tokens.push(Token::LBracket), + ']' => tokens.push(Token::RBracket), ',' => tokens.push(Token::Comma), ';' => tokens.push(Token::Semi), + ':' => tokens.push(Token::Colon), '=' => tokens.push(Token::Eq), + '@' => tokens.push(Token::At), '~' => tokens.push(Token::Not), '&' => tokens.push(Token::And), '^' => tokens.push(Token::Xor), '|' => tokens.push(Token::Or), + ch if ch.is_ascii_digit() => { + let start = index; + index += 1; + while index < chars.len() && chars[index].is_ascii_digit() { + index += 1; + } + let text = chars[start..index].iter().collect::(); + tokens.push(Token::Number(text.parse()?)); + continue; + } ch if is_ident_start(ch) => { let start = index; index += 1; @@ -55,6 +75,7 @@ pub fn lex(source: &str) -> eyre::Result> { "output" => Token::Output, "wire" => Token::Wire, "assign" => Token::Assign, + "always" => Token::Always, _ => Token::Ident(text), }); continue; diff --git a/src/verilog/parser.rs b/src/verilog/parser.rs index aefbdbd..c89d5f2 100644 --- a/src/verilog/parser.rs +++ b/src/verilog/parser.rs @@ -31,6 +31,10 @@ impl Parser { declarations.push(self.parse_declaration()?); } Some(Token::Assign) => assignments.push(self.parse_assignment()?), + Some(Token::Always) => eyre::bail!("unsupported Verilog construct: always block"), + Some(Token::LBracket) => { + eyre::bail!("unsupported Verilog construct: vector declaration") + } Some(token) => eyre::bail!("unsupported Verilog token in module body: {token:?}"), None => eyre::bail!("expected endmodule"), } @@ -52,6 +56,9 @@ impl Parser { Some(token) => eyre::bail!("expected declaration direction, got {token:?}"), None => eyre::bail!("expected declaration direction"), }; + if matches!(self.peek(), Some(Token::LBracket)) { + eyre::bail!("unsupported Verilog construct: vector declaration"); + } let names = self.parse_ident_list()?; self.expect(Token::Semi)?; @@ -243,4 +250,36 @@ mod tests { Ok(()) } + + #[test] + fn rejects_always_blocks_with_clear_message() { + let error = parse_module( + r#" + module bad(clk, q); + input clk; + output q; + always @(posedge clk) q = ~q; + endmodule + "#, + ) + .unwrap_err(); + + assert!(error.to_string().contains("always block")); + } + + #[test] + fn rejects_vector_declarations_with_clear_message() { + let error = parse_module( + r#" + module bad(a, y); + input [3:0] a; + output y; + assign y = a; + endmodule + "#, + ) + .unwrap_err(); + + assert!(error.to_string().contains("vector declaration")); + } } From 1401a571326c1dfb33a9941c08b31d6da734d4ae Mon Sep 17 00:00:00 2001 From: rollrat Date: Mon, 25 May 2026 00:09:34 +0900 Subject: [PATCH 09/19] Load minimal Verilog from CLI --- src/main.rs | 18 ++++++++++++++---- src/verilog/mod.rs | 28 ++++++++++++++++++++++++++++ test/half-adder.v | 7 +++++++ 3 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 test/half-adder.v diff --git a/src/main.rs b/src/main.rs index d3072ae..9a25b1c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use mimalloc::MiMalloc; +use redstone_compiler::verilog; use structopt::StructOpt; #[global_allocator] @@ -18,10 +19,19 @@ pub struct CompilerOption { fn main() -> eyre::Result<()> { tracing_subscriber::fmt::init(); - let _opt = CompilerOption::from_args(); - // let syntax = verilog::load(&opt.input)?; + let opt = CompilerOption::from_args(); - // println!("{syntax:?}"); + if opt.input.extension().and_then(|ext| ext.to_str()) == Some("v") { + let graph = verilog::load_logic_graph(&opt.input)?; + let prepared = graph.prepare_place()?; + println!( + "loaded Verilog graph: nodes={} inputs={} outputs={}", + prepared.nodes.len(), + prepared.inputs().len(), + prepared.outputs().len() + ); + return Ok(()); + } - Ok(()) + eyre::bail!("unsupported input file extension: {:?}", opt.input) } diff --git a/src/verilog/mod.rs b/src/verilog/mod.rs index 11dc4eb..0ff6b0c 100644 --- a/src/verilog/mod.rs +++ b/src/verilog/mod.rs @@ -2,3 +2,31 @@ pub mod ast; pub mod lexer; pub mod lower; pub mod parser; + +use std::fs; +use std::path::Path; + +use crate::graph::logic::LogicGraph; + +pub fn load_logic_graph(path: impl AsRef) -> eyre::Result { + let source = fs::read_to_string(path)?; + let module = parser::parse_module(&source)?; + lower::lower_module(&module) +} + +#[cfg(test)] +mod tests { + use super::load_logic_graph; + + #[test] + fn load_logic_graph_reads_verilog_file() -> eyre::Result<()> { + let graph = load_logic_graph("test/half-adder.v")?; + let table = graph.truth_table()?; + + assert_eq!(table.input_names, vec!["a", "b"]); + assert_eq!(table.output_tables["s"], vec![false, true, true, false]); + assert_eq!(table.output_tables["c"], vec![false, false, false, true]); + + Ok(()) + } +} diff --git a/test/half-adder.v b/test/half-adder.v new file mode 100644 index 0000000..6822fc4 --- /dev/null +++ b/test/half-adder.v @@ -0,0 +1,7 @@ +module half_adder(a, b, s, c); + input a, b; + output s, c; + + assign s = a ^ b; + assign c = a & b; +endmodule From 05ebf9d971003003dfa29da9a1734c2e369f750e Mon Sep 17 00:00:00 2001 From: rollrat Date: Mon, 25 May 2026 00:11:33 +0900 Subject: [PATCH 10/19] Export tiny Verilog circuits to NBT --- src/main.rs | 40 +++++++++++++++++++++ test/half-adder-generated-from-verilog.nbt | Bin 0 -> 402 bytes 2 files changed, 40 insertions(+) create mode 100644 test/half-adder-generated-from-verilog.nbt diff --git a/src/main.rs b/src/main.rs index 9a25b1c..63622ba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,14 @@ use std::path::PathBuf; use mimalloc::MiMalloc; +use redstone_compiler::nbt::ToNBT; +use redstone_compiler::transform::place_and_route::local_placer::{ + InputPlacementStrategy, LocalPlacer, LocalPlacerConfig, NotRouteStrategy, + PlacementSamplingPolicy, TorchPlacementStrategy, +}; +use redstone_compiler::transform::place_and_route::sampling::SamplingPolicy; use redstone_compiler::verilog; +use redstone_compiler::world::position::DimSize; use structopt::StructOpt; #[global_allocator] @@ -24,6 +31,39 @@ fn main() -> eyre::Result<()> { if opt.input.extension().and_then(|ext| ext.to_str()) == Some("v") { let graph = verilog::load_logic_graph(&opt.input)?; let prepared = graph.prepare_place()?; + if let Some(output) = opt.output { + let output_display = output.display().to_string(); + let config = LocalPlacerConfig { + random_seed: 42, + greedy_input_generation: true, + input_placement_strategy: InputPlacementStrategy::Boundary, + input_candidate_limit: None, + step_sampling_policy: SamplingPolicy::Random(10000), + placement_sampling_policy: PlacementSamplingPolicy::StepPolicy, + leak_sampling: false, + route_torch_directly: true, + torch_placement_strategy: TorchPlacementStrategy::DirectOnly, + not_route_strategy: NotRouteStrategy::DirectOnly, + max_not_route_step: 3, + not_route_step_sampling_policy: SamplingPolicy::Random(100), + max_route_step: 3, + route_step_sampling_policy: SamplingPolicy::Random(100), + }; + let placer = LocalPlacer::new(prepared.clone(), config)?; + let worlds = placer.generate(DimSize(10, 10, 5), None); + let Some(world) = worlds.into_iter().next() else { + eyre::bail!("placement produced no worlds"); + }; + world.to_nbt().save(output); + println!( + "exported Verilog graph: nodes={} inputs={} outputs={} path={}", + prepared.nodes.len(), + prepared.inputs().len(), + prepared.outputs().len(), + output_display + ); + return Ok(()); + } println!( "loaded Verilog graph: nodes={} inputs={} outputs={}", prepared.nodes.len(), diff --git a/test/half-adder-generated-from-verilog.nbt b/test/half-adder-generated-from-verilog.nbt new file mode 100644 index 0000000000000000000000000000000000000000..9a20df7e293902ad8d219afad6f9fb7b26c5ba8c GIT binary patch literal 402 zcmV;D0d4*tiwFP!00002|E-iwZi6roMkg4Ep^NU?RTsTM*F8kl8&nRNG^h?nw%bTO zeeEC`h=*Y1M?wkV`OW7UK)#Zy`ZE5XX9k(E!lL`_R=^0NEQLCW#o`-#A5wl>Yo+Zu9&5K3ME2uTWjB4?)hjSiIc?y~t z&8rOi9PF#0nYkXO5t-8&PtPQ7?XndTtwn!i-Em0Y#17$MS2tK`v9Yhlss=w+x^~AJ z^v@rU&o!<)WM6A1Dc`kfQQH~~hpbh5)UbGQ>rfc$kIgl@n?3e7@b7%0oI=VEt8}$J zG3Ogm)y-#T;=p8WA=?OP2EzNJP~s2j2}RZ@ZMSZbyGJK8PRb2jo(o=0pep0E(%|T>t<8 literal 0 HcmV?d00001 From 632fafc8d1017c3e4bfef4ec8867c650d467ebea Mon Sep 17 00:00:00 2001 From: rollrat Date: Mon, 25 May 2026 00:14:00 +0900 Subject: [PATCH 11/19] Flatten simple Verilog bit selects --- src/verilog/ast.rs | 7 +++++ src/verilog/lower.rs | 21 ++++++++++++++ src/verilog/parser.rs | 64 +++++++++++++++++++++++++++++++++---------- 3 files changed, 77 insertions(+), 15 deletions(-) diff --git a/src/verilog/ast.rs b/src/verilog/ast.rs index 0b8040c..e7cea4d 100644 --- a/src/verilog/ast.rs +++ b/src/verilog/ast.rs @@ -9,9 +9,16 @@ pub struct VerilogModule { #[derive(Debug, Clone, PartialEq, Eq)] pub struct Declaration { pub direction: Option, + pub range: Option, pub names: Vec, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Range { + pub msb: usize, + pub lsb: usize, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum PortDirection { Input, diff --git a/src/verilog/lower.rs b/src/verilog/lower.rs index 01fa14d..83e7f09 100644 --- a/src/verilog/lower.rs +++ b/src/verilog/lower.rs @@ -71,4 +71,25 @@ mod tests { Ok(()) } + + #[test] + fn lowers_vector_bit_selects_by_flattening_names() -> eyre::Result<()> { + let module = parse_module( + r#" + module bit_xor(a, y); + input [1:0] a; + output y; + assign y = a[0] ^ a[1]; + endmodule + "#, + )?; + + let graph = lower_module(&module)?; + let table = graph.truth_table()?; + + assert_eq!(table.input_names, vec!["a_0", "a_1"]); + assert_eq!(table.output_tables["y"], vec![false, true, true, false]); + + Ok(()) + } } diff --git a/src/verilog/parser.rs b/src/verilog/parser.rs index c89d5f2..487ce59 100644 --- a/src/verilog/parser.rs +++ b/src/verilog/parser.rs @@ -1,4 +1,6 @@ -use crate::verilog::ast::{Assignment, BinaryOp, Declaration, Expr, PortDirection, VerilogModule}; +use crate::verilog::ast::{ + Assignment, BinaryOp, Declaration, Expr, PortDirection, Range, VerilogModule, +}; use crate::verilog::lexer::{lex, Token}; pub fn parse_module(source: &str) -> eyre::Result { @@ -32,9 +34,6 @@ impl Parser { } Some(Token::Assign) => assignments.push(self.parse_assignment()?), Some(Token::Always) => eyre::bail!("unsupported Verilog construct: always block"), - Some(Token::LBracket) => { - eyre::bail!("unsupported Verilog construct: vector declaration") - } Some(token) => eyre::bail!("unsupported Verilog token in module body: {token:?}"), None => eyre::bail!("expected endmodule"), } @@ -56,14 +55,13 @@ impl Parser { Some(token) => eyre::bail!("expected declaration direction, got {token:?}"), None => eyre::bail!("expected declaration direction"), }; - if matches!(self.peek(), Some(Token::LBracket)) { - eyre::bail!("unsupported Verilog construct: vector declaration"); - } + let range = self.parse_optional_range()?; let names = self.parse_ident_list()?; self.expect(Token::Semi)?; Ok(Declaration { direction: Some(direction), + range, names, }) } @@ -131,7 +129,15 @@ impl Parser { fn parse_primary(&mut self) -> eyre::Result { match self.next() { - Some(Token::Ident(name)) => Ok(Expr::Ident(name)), + Some(Token::Ident(name)) => { + if self.consume(&Token::LBracket) { + let index = self.expect_number()?; + self.expect(Token::RBracket)?; + return Ok(Expr::Ident(format!("{name}_{index}"))); + } + + Ok(Expr::Ident(name)) + } Some(Token::LParen) => { let expr = self.parse_expr()?; self.expect(Token::RParen)?; @@ -150,6 +156,19 @@ impl Parser { Ok(names) } + fn parse_optional_range(&mut self) -> eyre::Result> { + if !self.consume(&Token::LBracket) { + return Ok(None); + } + + let msb = self.expect_number()?; + self.expect(Token::Colon)?; + let lsb = self.expect_number()?; + self.expect(Token::RBracket)?; + + Ok(Some(Range { msb, lsb })) + } + fn expect_ident(&mut self) -> eyre::Result { match self.next() { Some(Token::Ident(name)) => Ok(name), @@ -158,6 +177,14 @@ impl Parser { } } + fn expect_number(&mut self) -> eyre::Result { + match self.next() { + Some(Token::Number(value)) => Ok(value), + Some(token) => eyre::bail!("expected number, got {token:?}"), + None => eyre::bail!("expected number"), + } + } + fn expect(&mut self, expected: Token) -> eyre::Result<()> { let got = self.next(); if got == Some(expected.clone()) { @@ -268,18 +295,25 @@ mod tests { } #[test] - fn rejects_vector_declarations_with_clear_message() { - let error = parse_module( + fn parses_vector_declarations() -> eyre::Result<()> { + let module = parse_module( r#" - module bad(a, y); + module vectors(a, y); input [3:0] a; output y; - assign y = a; + assign y = a[0]; endmodule "#, - ) - .unwrap_err(); + )?; - assert!(error.to_string().contains("vector declaration")); + let range = module.declarations[0] + .range + .as_ref() + .expect("expected parsed vector range"); + assert_eq!(range.msb, 3); + assert_eq!(range.lsb, 0); + assert_eq!(module.assignments[0].expr.to_logic_stmt(), "a_0"); + + Ok(()) } } From 1556908146eba826c15edf68245ad5869839873c Mon Sep 17 00:00:00 2001 From: rollrat Date: Mon, 25 May 2026 00:17:33 +0900 Subject: [PATCH 12/19] Inline simple named Verilog module instances --- src/verilog/ast.rs | 8 +++ src/verilog/lexer.rs | 2 + src/verilog/lower.rs | 153 +++++++++++++++++++++++++++++++++++++++--- src/verilog/mod.rs | 4 +- src/verilog/parser.rs | 82 +++++++++++++++++++--- 5 files changed, 229 insertions(+), 20 deletions(-) diff --git a/src/verilog/ast.rs b/src/verilog/ast.rs index e7cea4d..c597a21 100644 --- a/src/verilog/ast.rs +++ b/src/verilog/ast.rs @@ -4,6 +4,7 @@ pub struct VerilogModule { pub ports: Vec, pub declarations: Vec, pub assignments: Vec, + pub instances: Vec, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -32,6 +33,13 @@ pub struct Assignment { pub expr: Expr, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Instance { + pub module_name: String, + pub instance_name: String, + pub connections: Vec<(String, String)>, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum Expr { Ident(String), diff --git a/src/verilog/lexer.rs b/src/verilog/lexer.rs index 3a10cc0..ca13b29 100644 --- a/src/verilog/lexer.rs +++ b/src/verilog/lexer.rs @@ -13,6 +13,7 @@ pub enum Token { RParen, LBracket, RBracket, + Dot, Comma, Semi, Colon, @@ -42,6 +43,7 @@ pub fn lex(source: &str) -> eyre::Result> { ')' => tokens.push(Token::RParen), '[' => tokens.push(Token::LBracket), ']' => tokens.push(Token::RBracket), + '.' => tokens.push(Token::Dot), ',' => tokens.push(Token::Comma), ';' => tokens.push(Token::Semi), ':' => tokens.push(Token::Colon), diff --git a/src/verilog/lower.rs b/src/verilog/lower.rs index 83e7f09..cb2f5b1 100644 --- a/src/verilog/lower.rs +++ b/src/verilog/lower.rs @@ -1,23 +1,126 @@ +use std::collections::HashMap; + use crate::graph::logic::LogicGraph; -use crate::verilog::ast::VerilogModule; +use crate::verilog::ast::{Expr, VerilogModule}; pub fn lower_module(module: &VerilogModule) -> eyre::Result { - if module.assignments.is_empty() { + let mut context = HashMap::new(); + context.insert(module.name.clone(), module); + lower_module_with_context(&context, module) +} + +pub fn lower_modules(modules: &[VerilogModule]) -> eyre::Result { + let Some(top_module) = modules.last() else { + eyre::bail!("expected at least one Verilog module"); + }; + let context = modules + .iter() + .map(|module| (module.name.clone(), module)) + .collect::>(); + + lower_module_with_context(&context, top_module) +} + +fn lower_module_with_context( + context: &HashMap, + module: &VerilogModule, +) -> eyre::Result { + let assignments = collect_assignments(context, module, None, &HashMap::new())?; + if assignments.is_empty() { eyre::bail!("module `{}` has no continuous assignments", module.name); } - LogicGraph::from_assignments( - module - .assignments + LogicGraph::from_assignments(assignments) +} + +fn collect_assignments( + context: &HashMap, + module: &VerilogModule, + instance_prefix: Option<&str>, + substitutions: &HashMap, +) -> eyre::Result> { + let mut assignments = Vec::new(); + + for instance in &module.instances { + let child = context.get(&instance.module_name).ok_or_else(|| { + eyre::eyre!( + "unknown Verilog module `{}` for instance `{}`", + instance.module_name, + instance.instance_name + ) + })?; + let child_prefix = scoped_name(instance_prefix, &instance.instance_name); + let child_substitutions = instance + .connections .iter() - .map(|assign| (assign.output.clone(), assign.expr.to_logic_stmt())), - ) + .map(|(port, signal)| { + ( + port.clone(), + rewrite_signal(signal, instance_prefix, substitutions), + ) + }) + .collect::>(); + assignments.extend(collect_assignments( + context, + child, + Some(&child_prefix), + &child_substitutions, + )?); + } + + for assignment in &module.assignments { + let output = rewrite_signal(&assignment.output, instance_prefix, substitutions); + let expr = rewrite_expr(&assignment.expr, instance_prefix, substitutions); + assignments.push((output, expr.to_logic_stmt())); + } + + Ok(assignments) +} + +fn rewrite_expr( + expr: &Expr, + instance_prefix: Option<&str>, + substitutions: &HashMap, +) -> Expr { + match expr { + Expr::Ident(name) => Expr::Ident(rewrite_signal(name, instance_prefix, substitutions)), + Expr::Not(expr) => Expr::Not(Box::new(rewrite_expr( + expr, + instance_prefix, + substitutions, + ))), + Expr::Binary { op, left, right } => Expr::Binary { + op: *op, + left: Box::new(rewrite_expr(left, instance_prefix, substitutions)), + right: Box::new(rewrite_expr(right, instance_prefix, substitutions)), + }, + } +} + +fn rewrite_signal( + name: &str, + instance_prefix: Option<&str>, + substitutions: &HashMap, +) -> String { + if let Some(replacement) = substitutions.get(name) { + return replacement.clone(); + } + + instance_prefix + .map(|prefix| scoped_name(Some(prefix), name)) + .unwrap_or_else(|| name.to_owned()) +} + +fn scoped_name(prefix: Option<&str>, name: &str) -> String { + prefix + .map(|prefix| format!("{prefix}__{name}")) + .unwrap_or_else(|| name.to_owned()) } #[cfg(test)] mod tests { - use super::lower_module; - use crate::verilog::parser::parse_module; + use super::{lower_module, lower_modules}; + use crate::verilog::parser::{parse_module, parse_modules}; #[test] fn lowers_half_adder_to_truth_table() -> eyre::Result<()> { @@ -92,4 +195,36 @@ mod tests { Ok(()) } + + #[test] + fn lowers_named_module_instances_by_inlining() -> eyre::Result<()> { + let modules = parse_modules( + r#" + module half_adder(a, b, s, c); + input a, b; + output s, c; + assign s = a ^ b; + assign c = a & b; + endmodule + + module two_half_adders(a, b, cin, sum, carry0, carry1); + input a, b, cin; + output sum, carry0, carry1; + wire s0; + half_adder ha0(.a(a), .b(b), .s(s0), .c(carry0)); + half_adder ha1(.a(s0), .b(cin), .s(sum), .c(carry1)); + endmodule + "#, + )?; + + let graph = lower_modules(&modules)?; + let table = graph.truth_table()?; + let sum = (0..8) + .map(|mask: usize| mask.count_ones() % 2 == 1) + .collect::>(); + + assert_eq!(table.output_tables["sum"], sum); + + Ok(()) + } } diff --git a/src/verilog/mod.rs b/src/verilog/mod.rs index 0ff6b0c..59d6d13 100644 --- a/src/verilog/mod.rs +++ b/src/verilog/mod.rs @@ -10,8 +10,8 @@ use crate::graph::logic::LogicGraph; pub fn load_logic_graph(path: impl AsRef) -> eyre::Result { let source = fs::read_to_string(path)?; - let module = parser::parse_module(&source)?; - lower::lower_module(&module) + let modules = parser::parse_modules(&source)?; + lower::lower_modules(&modules) } #[cfg(test)] diff --git a/src/verilog/parser.rs b/src/verilog/parser.rs index 487ce59..c3ce531 100644 --- a/src/verilog/parser.rs +++ b/src/verilog/parser.rs @@ -1,10 +1,18 @@ use crate::verilog::ast::{ - Assignment, BinaryOp, Declaration, Expr, PortDirection, Range, VerilogModule, + Assignment, BinaryOp, Declaration, Expr, Instance, PortDirection, Range, VerilogModule, }; use crate::verilog::lexer::{lex, Token}; pub fn parse_module(source: &str) -> eyre::Result { - Parser::new(lex(source)?).parse_module() + let mut modules = parse_modules(source)?; + if modules.len() != 1 { + eyre::bail!("expected exactly one module, got {}", modules.len()); + } + Ok(modules.remove(0)) +} + +pub fn parse_modules(source: &str) -> eyre::Result> { + Parser::new(lex(source)?).parse_modules() } struct Parser { @@ -17,7 +25,15 @@ impl Parser { Self { tokens, index: 0 } } - fn parse_module(&mut self) -> eyre::Result { + fn parse_modules(&mut self) -> eyre::Result> { + let mut modules = Vec::new(); + while self.peek().is_some() { + modules.push(self.parse_one_module()?); + } + Ok(modules) + } + + fn parse_one_module(&mut self) -> eyre::Result { self.expect(Token::Module)?; let name = self.expect_ident()?; self.expect(Token::LParen)?; @@ -27,12 +43,14 @@ impl Parser { let mut declarations = Vec::new(); let mut assignments = Vec::new(); + let mut instances = Vec::new(); while !self.consume(&Token::EndModule) { match self.peek() { Some(Token::Input) | Some(Token::Output) | Some(Token::Wire) => { declarations.push(self.parse_declaration()?); } Some(Token::Assign) => assignments.push(self.parse_assignment()?), + Some(Token::Ident(_)) => instances.push(self.parse_instance()?), Some(Token::Always) => eyre::bail!("unsupported Verilog construct: always block"), Some(token) => eyre::bail!("unsupported Verilog token in module body: {token:?}"), None => eyre::bail!("expected endmodule"), @@ -44,6 +62,7 @@ impl Parser { ports, declarations, assignments, + instances, }) } @@ -76,6 +95,41 @@ impl Parser { Ok(Assignment { output, expr }) } + fn parse_instance(&mut self) -> eyre::Result { + let module_name = self.expect_ident()?; + let instance_name = self.expect_ident()?; + self.expect(Token::LParen)?; + let connections = self.parse_named_connections()?; + self.expect(Token::RParen)?; + self.expect(Token::Semi)?; + + Ok(Instance { + module_name, + instance_name, + connections, + }) + } + + fn parse_named_connections(&mut self) -> eyre::Result> { + let mut connections = vec![self.parse_named_connection()?]; + while self.consume(&Token::Comma) { + connections.push(self.parse_named_connection()?); + } + Ok(connections) + } + + fn parse_named_connection(&mut self) -> eyre::Result<(String, String)> { + if !self.consume(&Token::Dot) { + eyre::bail!("unsupported Verilog construct: positional instance ports"); + } + let port = self.expect_ident()?; + self.expect(Token::LParen)?; + let signal = self.parse_signal_name()?; + self.expect(Token::RParen)?; + + Ok((port, signal)) + } + fn parse_expr(&mut self) -> eyre::Result { self.parse_or() } @@ -130,12 +184,7 @@ impl Parser { fn parse_primary(&mut self) -> eyre::Result { match self.next() { Some(Token::Ident(name)) => { - if self.consume(&Token::LBracket) { - let index = self.expect_number()?; - self.expect(Token::RBracket)?; - return Ok(Expr::Ident(format!("{name}_{index}"))); - } - + let name = self.finish_signal_name(name)?; Ok(Expr::Ident(name)) } Some(Token::LParen) => { @@ -156,6 +205,21 @@ impl Parser { Ok(names) } + fn parse_signal_name(&mut self) -> eyre::Result { + let name = self.expect_ident()?; + self.finish_signal_name(name) + } + + fn finish_signal_name(&mut self, name: String) -> eyre::Result { + if self.consume(&Token::LBracket) { + let index = self.expect_number()?; + self.expect(Token::RBracket)?; + return Ok(format!("{name}_{index}")); + } + + Ok(name) + } + fn parse_optional_range(&mut self) -> eyre::Result> { if !self.consume(&Token::LBracket) { return Ok(None); From 9c8bddf9ca40a58d05e83bac7c8df9428e7d37b6 Mon Sep 17 00:00:00 2001 From: rollrat Date: Mon, 25 May 2026 00:32:35 +0900 Subject: [PATCH 13/19] Simplify Verilog CLI main flow --- src/main.rs | 78 +++++++++++++++++++++++++---------------------------- 1 file changed, 37 insertions(+), 41 deletions(-) diff --git a/src/main.rs b/src/main.rs index 63622ba..6671f3c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,50 +28,46 @@ fn main() -> eyre::Result<()> { tracing_subscriber::fmt::init(); let opt = CompilerOption::from_args(); - if opt.input.extension().and_then(|ext| ext.to_str()) == Some("v") { - let graph = verilog::load_logic_graph(&opt.input)?; - let prepared = graph.prepare_place()?; - if let Some(output) = opt.output { - let output_display = output.display().to_string(); - let config = LocalPlacerConfig { - random_seed: 42, - greedy_input_generation: true, - input_placement_strategy: InputPlacementStrategy::Boundary, - input_candidate_limit: None, - step_sampling_policy: SamplingPolicy::Random(10000), - placement_sampling_policy: PlacementSamplingPolicy::StepPolicy, - leak_sampling: false, - route_torch_directly: true, - torch_placement_strategy: TorchPlacementStrategy::DirectOnly, - not_route_strategy: NotRouteStrategy::DirectOnly, - max_not_route_step: 3, - not_route_step_sampling_policy: SamplingPolicy::Random(100), - max_route_step: 3, - route_step_sampling_policy: SamplingPolicy::Random(100), - }; - let placer = LocalPlacer::new(prepared.clone(), config)?; - let worlds = placer.generate(DimSize(10, 10, 5), None); - let Some(world) = worlds.into_iter().next() else { - eyre::bail!("placement produced no worlds"); - }; - world.to_nbt().save(output); - println!( - "exported Verilog graph: nodes={} inputs={} outputs={} path={}", - prepared.nodes.len(), - prepared.inputs().len(), - prepared.outputs().len(), - output_display - ); - return Ok(()); - } + if opt.input.extension().and_then(|ext| ext.to_str()) != Some("v") { + eyre::bail!("unsupported input file extension: {:?}", opt.input); + } + + let graph = verilog::load_logic_graph(&opt.input)?.prepare_place()?; + + let Some(output) = opt.output else { println!( "loaded Verilog graph: nodes={} inputs={} outputs={}", - prepared.nodes.len(), - prepared.inputs().len(), - prepared.outputs().len() + graph.nodes.len(), + graph.inputs().len(), + graph.outputs().len() ); return Ok(()); - } + }; + + let config = LocalPlacerConfig { + random_seed: 42, + greedy_input_generation: true, + input_placement_strategy: InputPlacementStrategy::Boundary, + input_candidate_limit: None, + step_sampling_policy: SamplingPolicy::Random(10000), + placement_sampling_policy: PlacementSamplingPolicy::StepPolicy, + leak_sampling: false, + route_torch_directly: true, + torch_placement_strategy: TorchPlacementStrategy::DirectOnly, + not_route_strategy: NotRouteStrategy::DirectOnly, + max_not_route_step: 3, + not_route_step_sampling_policy: SamplingPolicy::Random(100), + max_route_step: 3, + route_step_sampling_policy: SamplingPolicy::Random(100), + }; + let placer = LocalPlacer::new(graph, config)?; + let worlds = placer.generate(DimSize(10, 10, 5), None); + let Some(world) = worlds.into_iter().next() else { + eyre::bail!("placement produced no worlds"); + }; + world.to_nbt().save(&output); + + println!("exported Verilog graph: path={}", output.display()); - eyre::bail!("unsupported input file extension: {:?}", opt.input) + Ok(()) } From 6d4a347661e512eb46a1cd70fc1ce59b56e3c97b Mon Sep 17 00:00:00 2001 From: rollrat Date: Mon, 25 May 2026 08:14:01 +0900 Subject: [PATCH 14/19] Add Verilog graph boundary tests --- src/verilog/mod.rs | 132 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/src/verilog/mod.rs b/src/verilog/mod.rs index 59d6d13..e72b495 100644 --- a/src/verilog/mod.rs +++ b/src/verilog/mod.rs @@ -17,6 +17,12 @@ pub fn load_logic_graph(path: impl AsRef) -> eyre::Result { #[cfg(test)] mod tests { use super::load_logic_graph; + use crate::graph::graphviz::ToGraphvizGraph; + use crate::graph::logic::LogicGraph; + use crate::graph::GraphNodeKind; + use crate::logic::LogicType; + use crate::nbt::NBTRoot; + use crate::transform::place_and_route::utils::world_to_logic; #[test] fn load_logic_graph_reads_verilog_file() -> eyre::Result<()> { @@ -29,4 +35,130 @@ mod tests { Ok(()) } + + #[test] + fn half_adder_verilog_raw_graph_has_named_logic_outputs() -> eyre::Result<()> { + let graph = load_logic_graph("test/half-adder.v")?; + maybe_dump_graphviz("raw Verilog LogicGraph", &graph); + + assert_eq!( + output_source_logic_types(&graph), + vec![ + ("c".to_owned(), LogicType::And), + ("s".to_owned(), LogicType::Xor) + ] + ); + assert_eq!(output_source_input_names(&graph, "s"), vec!["a", "b"]); + assert_eq!(output_source_input_names(&graph, "c"), vec!["a", "b"]); + + Ok(()) + } + + #[test] + fn half_adder_prepare_place_preserves_named_outputs() -> eyre::Result<()> { + let graph = load_logic_graph("test/half-adder.v")?; + let prepared = graph.prepare_place()?; + maybe_dump_graphviz("prepared Verilog LogicGraph", &prepared); + + assert_eq!(output_names(&prepared), vec!["c", "s"]); + let observable_sources = prepared.externally_observable_output_source_ids(); + assert_eq!(observable_sources.len(), 2); + for source_id in output_source_ids(&prepared) { + assert!(observable_sources.contains(&source_id)); + } + + let table = prepared.truth_table()?; + assert_eq!(table.input_names, vec!["a", "b"]); + assert_eq!(table.output_tables["s"], vec![false, true, true, false]); + assert_eq!(table.output_tables["c"], vec![false, false, false, true]); + + Ok(()) + } + + #[test] + fn half_adder_generated_nbt_matches_verilog_functions_without_named_outputs() -> eyre::Result<()> + { + let expected = load_logic_graph("test/half-adder.v")?; + let nbt = NBTRoot::load("test/half-adder-generated-from-verilog.nbt")?; + let generated = world_to_logic(&nbt.to_world())?; + maybe_dump_graphviz("NBT roundtrip LogicGraph", &generated); + + assert!(generated + .truth_table()? + .contains_output_tables_under_input_permutation(&expected.truth_table()?)); + assert!(output_names(&generated).is_empty()); + + Ok(()) + } + + fn maybe_dump_graphviz(name: &str, graph: &LogicGraph) { + if std::env::var_os("PRINT_VERILOG_GRAPHS").is_some() { + eprintln!("--- {name} ---\n{}", graph.to_graphviz()); + } + } + + fn output_names(graph: &LogicGraph) -> Vec { + let mut names = graph + .nodes + .iter() + .filter_map(|node| match &node.kind { + GraphNodeKind::Output(name) => Some(name.clone()), + _ => None, + }) + .collect::>(); + names.sort(); + names + } + + fn output_source_ids(graph: &LogicGraph) -> Vec { + let mut ids = graph + .nodes + .iter() + .filter_map(|node| match &node.kind { + GraphNodeKind::Output(_) => Some(node.inputs[0]), + _ => None, + }) + .collect::>(); + ids.sort(); + ids + } + + fn output_source_logic_types(graph: &LogicGraph) -> Vec<(String, LogicType)> { + let mut outputs = graph + .nodes + .iter() + .filter_map(|node| match &node.kind { + GraphNodeKind::Output(name) => { + let source = graph.find_node_by_id(node.inputs[0]).unwrap(); + match &source.kind { + GraphNodeKind::Logic(logic) => Some((name.clone(), logic.logic_type)), + _ => None, + } + } + _ => None, + }) + .collect::>(); + outputs.sort_by(|(a, _), (b, _)| a.cmp(b)); + outputs + } + + fn output_source_input_names(graph: &LogicGraph, output_name: &str) -> Vec { + let output = graph + .nodes + .iter() + .find(|node| matches!(&node.kind, GraphNodeKind::Output(name) if name == output_name)) + .unwrap(); + let source = graph.find_node_by_id(output.inputs[0]).unwrap(); + let mut input_names = source + .inputs + .iter() + .map(|input_id| graph.find_node_by_id(*input_id).unwrap()) + .filter_map(|node| match &node.kind { + GraphNodeKind::Input(name) => Some(name.clone()), + _ => None, + }) + .collect::>(); + input_names.sort(); + input_names + } } From 770b4e97e94c9d694baa7995c2fa7668e0466cc1 Mon Sep 17 00:00:00 2001 From: rollrat Date: Mon, 25 May 2026 08:29:57 +0900 Subject: [PATCH 15/19] Materialize Verilog output endpoints --- src/main.rs | 1 + .../local_placer/component_tests.rs | 9 +++ .../place_and_route/local_placer/config.rs | 3 + .../place_and_route/local_placer/mod.rs | 14 ++++ .../place_and_route/local_placer/routing.rs | 71 ++++++++++++++++++ .../place_and_route/local_placer/tests.rs | 71 ++++++++++++++++-- test/half-adder-generated-from-verilog.nbt | Bin 402 -> 403 bytes 7 files changed, 162 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index 6671f3c..d67e13d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -53,6 +53,7 @@ fn main() -> eyre::Result<()> { placement_sampling_policy: PlacementSamplingPolicy::StepPolicy, leak_sampling: false, route_torch_directly: true, + materialize_outputs: true, torch_placement_strategy: TorchPlacementStrategy::DirectOnly, not_route_strategy: NotRouteStrategy::DirectOnly, max_not_route_step: 3, diff --git a/src/transform/place_and_route/local_placer/component_tests.rs b/src/transform/place_and_route/local_placer/component_tests.rs index 15a6bd2..a7ea6d0 100644 --- a/src/transform/place_and_route/local_placer/component_tests.rs +++ b/src/transform/place_and_route/local_placer/component_tests.rs @@ -32,6 +32,7 @@ fn test_generate_component_and_shortest() -> eyre::Result<()> { placement_sampling_policy: PlacementSamplingPolicy::StepPolicy, leak_sampling: false, route_torch_directly: false, + materialize_outputs: false, torch_placement_strategy: TorchPlacementStrategy::DirectOnly, not_route_strategy: NotRouteStrategy::DirectOnly, max_not_route_step: 1, @@ -279,6 +280,7 @@ fn test_generate_component_rs_latch() -> eyre::Result<()> { placement_sampling_policy: PlacementSamplingPolicy::StepPolicy, leak_sampling: false, route_torch_directly: true, + materialize_outputs: false, torch_placement_strategy: TorchPlacementStrategy::DirectOnly, not_route_strategy: NotRouteStrategy::DirectAndRedstone, max_not_route_step: 4, @@ -346,6 +348,7 @@ fn test_generate_component_d_latch() -> eyre::Result<()> { placement_sampling_policy: PlacementSamplingPolicy::StepPolicy, leak_sampling: false, route_torch_directly: true, + materialize_outputs: false, torch_placement_strategy: TorchPlacementStrategy::DirectOnly, not_route_strategy: NotRouteStrategy::DirectAndRedstone, max_not_route_step: 4, @@ -438,6 +441,7 @@ fn test_generate_component_xor_simple() -> eyre::Result<()> { placement_sampling_policy: PlacementSamplingPolicy::StepPolicy, leak_sampling: false, route_torch_directly: true, + materialize_outputs: false, torch_placement_strategy: TorchPlacementStrategy::DirectOnly, not_route_strategy: NotRouteStrategy::DirectOnly, max_not_route_step: 5, @@ -470,6 +474,7 @@ fn test_generate_component_xor_complex() -> eyre::Result<()> { placement_sampling_policy: PlacementSamplingPolicy::StepPolicy, leak_sampling: true, route_torch_directly: true, + materialize_outputs: false, torch_placement_strategy: TorchPlacementStrategy::DirectOnly, not_route_strategy: NotRouteStrategy::DirectOnly, max_not_route_step: 3, @@ -508,6 +513,7 @@ fn test_generate_component_xor_shortest() -> eyre::Result<()> { placement_sampling_policy: PlacementSamplingPolicy::StepPolicy, leak_sampling: true, route_torch_directly: true, + materialize_outputs: false, torch_placement_strategy: TorchPlacementStrategy::DirectOnly, not_route_strategy: NotRouteStrategy::DirectOnly, max_not_route_step: 1, @@ -551,6 +557,7 @@ fn test_generate_component_half_adder() -> eyre::Result<()> { placement_sampling_policy: PlacementSamplingPolicy::StepPolicy, leak_sampling: false, route_torch_directly: true, + materialize_outputs: false, torch_placement_strategy: TorchPlacementStrategy::DirectOnly, not_route_strategy: NotRouteStrategy::DirectOnly, max_not_route_step: 3, @@ -586,6 +593,7 @@ fn test_generate_component_full_adder() -> eyre::Result<()> { placement_sampling_policy: LocalPlacerConfig::ranked_sampling(2000, 500, 0), leak_sampling: false, route_torch_directly: true, + materialize_outputs: false, torch_placement_strategy: TorchPlacementStrategy::DirectOnly, not_route_strategy: NotRouteStrategy::DirectOnly, max_not_route_step: 0, @@ -646,6 +654,7 @@ fn debug_full_adder_with_cost_sampling() -> eyre::Result<()> { }, leak_sampling: false, route_torch_directly: true, + materialize_outputs: false, torch_placement_strategy: TorchPlacementStrategy::DirectOnly, not_route_strategy: NotRouteStrategy::DirectOnly, max_not_route_step: 8, diff --git a/src/transform/place_and_route/local_placer/config.rs b/src/transform/place_and_route/local_placer/config.rs index 39f2853..7a1c43a 100644 --- a/src/transform/place_and_route/local_placer/config.rs +++ b/src/transform/place_and_route/local_placer/config.rs @@ -12,6 +12,7 @@ pub struct LocalPlacerConfig { pub leak_sampling: bool, // torch placement를 input과 direct로 연결하도록 강제한다. pub route_torch_directly: bool, + pub materialize_outputs: bool, pub torch_placement_strategy: TorchPlacementStrategy, pub not_route_strategy: NotRouteStrategy, pub max_not_route_step: usize, @@ -32,6 +33,7 @@ impl Default for LocalPlacerConfig { placement_sampling_policy: PlacementSamplingPolicy::default(), leak_sampling: false, route_torch_directly: true, + materialize_outputs: false, torch_placement_strategy: TorchPlacementStrategy::default(), not_route_strategy: NotRouteStrategy::default(), max_not_route_step: 0, @@ -101,6 +103,7 @@ impl LocalPlacerConfig { placement_sampling_policy: PlacementSamplingPolicy::StepPolicy, leak_sampling: false, route_torch_directly: false, + materialize_outputs: false, torch_placement_strategy: TorchPlacementStrategy::AnywhereNonAdjacent, not_route_strategy: NotRouteStrategy::DirectAndRedstone, max_not_route_step: max_route_step, diff --git a/src/transform/place_and_route/local_placer/mod.rs b/src/transform/place_and_route/local_placer/mod.rs index 282677b..5f3dfc4 100644 --- a/src/transform/place_and_route/local_placer/mod.rs +++ b/src/transform/place_and_route/local_placer/mod.rs @@ -286,6 +286,20 @@ impl LocalPlacer { .collect() } } + GraphNodeKind::Output(_) if self.config.materialize_outputs => { + generate_output_routes(&world, state[&node.inputs[0]]) + .into_iter() + .map(|(world, position)| { + let mut state = state.clone(); + state.set_node_position(node.id, position); + state.set_signal_footprint( + node.id, + [Some(position), position.down()].into_iter().flatten(), + ); + (world, state) + }) + .collect() + } GraphNodeKind::Output(_) => vec![(world.clone(), state.clone())], GraphNodeKind::Logic(logic) => match logic.logic_type { LogicType::Not => not_node_kind() diff --git a/src/transform/place_and_route/local_placer/routing.rs b/src/transform/place_and_route/local_placer/routing.rs index 6d39eb3..61363e3 100644 --- a/src/transform/place_and_route/local_placer/routing.rs +++ b/src/transform/place_and_route/local_placer/routing.rs @@ -95,6 +95,27 @@ pub(super) fn generate_place_and_routes( } } +pub(super) fn generate_output_routes( + world: &World3D, + source: Position, +) -> Vec<(World3D, Position)> { + let source_node = PlacedNode::new(source, world[source]); + let forbidden_cobble = torch_source_support(world, source); + + source_node + .propagation_bound(Some(world)) + .into_iter() + .filter(|bound| world.size.bound_on(bound.position())) + .filter_map(|bound| place_output_redstone(world, bound, source)) + .filter(|(world, position)| { + !route_powers_forbidden_cobble(world, &[source, *position], forbidden_cobble) + }) + .map(|(world, position)| (source.manhattan_distance(&position), world, position)) + .sorted_by_key(|(cost, _, _)| *cost) + .map(|(_, world, position)| (world, position)) + .collect() +} + pub(super) fn generate_torch_place_and_routes( config: &LocalPlacerConfig, world: &World3D, @@ -131,6 +152,56 @@ pub(super) fn generate_torch_place_and_routes( .collect() } +fn place_output_redstone( + world: &World3D, + bound: PlaceBound, + source: Position, +) -> Option<(World3D, Position)> { + let redstone_pos = bound.position(); + let cobble_pos = redstone_pos.down()?; + let cobble_except = if world[source].kind.is_torch() { + vec![cobble_pos, source] + } else { + Vec::new() + }; + let cobble_node = try_generate_cobble_node(world, cobble_pos, &cobble_except)?; + + let mut new_world = world.clone(); + place_node(&mut new_world, cobble_node); + + let bound_back_pos = redstone_pos.walk(bound.direction())?; + let redstone_node = PlacedNode::new_redstone(redstone_pos); + let except = [source, bound_back_pos, redstone_pos] + .into_iter() + .collect::>(); + if redstone_node.has_conflict(&new_world, &except) { + return None; + } + + let short_except = [source, redstone_pos].into_iter().collect::>(); + if redstone_node.has_short(world, &short_except) { + return None; + } + + place_node(&mut new_world, redstone_node); + new_world.update_redstone_states(source); + if !target_powers_redstone(&new_world, source, redstone_pos) { + return None; + } + if redstone_node.has_short(&new_world, &short_except) { + return None; + } + if let BlockKind::Torch { .. } = world[source].kind { + if let Some(source_cobble) = source.walk(world[source].direction) { + if redstone_powers_cobble(&new_world, redstone_pos, source_cobble) { + return None; + } + } + } + + Some((new_world, redstone_pos)) +} + pub(super) fn place_torch_with_cobble( world: &World3D, torch: Block, diff --git a/src/transform/place_and_route/local_placer/tests.rs b/src/transform/place_and_route/local_placer/tests.rs index e2d6279..5ab9dc3 100644 --- a/src/transform/place_and_route/local_placer/tests.rs +++ b/src/transform/place_and_route/local_placer/tests.rs @@ -36,6 +36,7 @@ fn config(max_route_step: usize) -> LocalPlacerConfig { placement_sampling_policy: PlacementSamplingPolicy::StepPolicy, leak_sampling: false, route_torch_directly: true, + materialize_outputs: false, torch_placement_strategy: TorchPlacementStrategy::DirectOnly, not_route_strategy: NotRouteStrategy::DirectOnly, max_not_route_step: max_route_step, @@ -204,7 +205,7 @@ fn compact_queue_keeps_positions_needed_by_future_logic() -> eyre::Result<()> { } #[test] -fn compact_queue_drops_positions_used_only_by_outputs() -> eyre::Result<()> { +fn compact_queue_keeps_positions_needed_by_external_outputs() -> eyre::Result<()> { let graph = LogicGraph::from_stmt("a|b", "c")?.prepare_place()?; let placer = LocalPlacer::new(graph.clone(), config(1))?; let or_id = graph @@ -231,14 +232,16 @@ fn compact_queue_drops_positions_used_only_by_outputs() -> eyre::Result<()> { let compacted = placer.compact_queue_after_step(or_step, queue); - assert_eq!(compacted.len(), 1); + assert_eq!(compacted.len(), 2); Ok(()) } #[test] -fn output_step_does_not_extend_placement_state() -> eyre::Result<()> { +fn output_step_routes_visible_redstone_endpoint_from_or_source() -> eyre::Result<()> { let graph = LogicGraph::from_stmt("a|b", "c")?.prepare_place()?; - let placer = LocalPlacer::new(graph.clone(), config(1))?; + let mut config = config(1); + config.materialize_outputs = true; + let placer = LocalPlacer::new(graph.clone(), config)?; let or_id = graph .nodes .iter() @@ -258,11 +261,65 @@ fn output_step_does_not_extend_placement_state() -> eyre::Result<()> { .iter() .position(|id| *id == output_id) .unwrap(); - let state = [(or_id, Position(1, 1, 1))].into_iter().collect(); + let source = Position(2, 2, 1); + let mut world = empty_world(); + place_node(&mut world, PlacedNode::new_cobble(Position(2, 2, 0))); + place_node(&mut world, PlacedNode::new_redstone(source)); + let state = [(or_id, source)].into_iter().collect(); - let result = placer.do_step(output_step, vec![(empty_world(), state)]); + let result = placer.do_step(output_step, vec![(world, state)]); + + assert!(!result.queue.is_empty()); + assert!(result.queue.iter().any(|(world, state)| { + state + .node_position(output_id) + .is_some_and(|position| world[position].kind.is_redstone()) + })); + Ok(()) +} - assert_eq!(result.queue[0].1.endpoint_positions().len(), 1); +#[test] +fn output_step_routes_visible_redstone_endpoint_from_torch_source() -> eyre::Result<()> { + let graph = LogicGraph::from_stmt("~a", "out")?.prepare_place()?; + let mut config = config(1); + config.materialize_outputs = true; + let placer = LocalPlacer::new(graph.clone(), config)?; + let not_id = graph + .nodes + .iter() + .find(|node| { + matches!(&node.kind, GraphNodeKind::Logic(logic) if logic.logic_type == LogicType::Not) + }) + .unwrap() + .id; + let output_id = graph + .nodes + .iter() + .find(|node| matches!(&node.kind, GraphNodeKind::Output(name) if name == "out")) + .unwrap() + .id; + let output_step = placer + .visit_orders + .iter() + .position(|id| *id == output_id) + .unwrap(); + let source = Position(2, 2, 1); + let mut world = empty_world(); + place_node(&mut world, PlacedNode::new_cobble(Position(2, 2, 0))); + place_node( + &mut world, + PlacedNode::new(source, torch(Direction::Bottom)), + ); + let state = [(not_id, source)].into_iter().collect(); + + let result = placer.do_step(output_step, vec![(world, state)]); + + assert!(!result.queue.is_empty()); + assert!(result.queue.iter().any(|(world, state)| { + state + .node_position(output_id) + .is_some_and(|position| world[position].kind.is_redstone()) + })); Ok(()) } diff --git a/test/half-adder-generated-from-verilog.nbt b/test/half-adder-generated-from-verilog.nbt index 9a20df7e293902ad8d219afad6f9fb7b26c5ba8c..793afd8f54fc491a8a0ffecc3c759ed4924a7e46 100644 GIT binary patch literal 403 zcmV;E0c`#siwFP!00002|FxA(Zo)7Sg(v|C{aU`G>I9MjWD`V5WGaZJ)>NSNvMqW<0Ida;nsfZ-t>7G|dWq#=g; z8Ip*c$uOTZ%#a8jIx{a4F)z>02ty_^q#-}dpV!>oS;XC0sMiR?y3E3#`&>Rea$Ynd zLmJ_EPKW*VoX*heHNvn7hDZ3}AwS&j#fD7g>ztSz7JE^nG1xvH^99X zHs)0u+2C@eD*FfpcYb#{ukjmI0mZwLdsL=Eok6-0Wz~EDZ=^C?TV(eL>>%novkm0| z6|RAAM6InmC;D^|wa2HvZOPI|Rc^;xX!+H)pmk+D#fOk$rEs9dkB%UDv2`f4Y2PGk xw3`FE&6O^XvbQmL3Sj8+|Ds);>^!;aRXY)mS$zzMhO+F12fvV)mY?zm004i;#83bL literal 402 zcmV;D0d4*tiwFP!00002|E-iwZi6roMkg4Ep^NU?RTsTM*F8kl8&nRNG^h?nw%bTO zeeEC`h=*Y1M?wkV`OW7UK)#Zy`ZE5XX9k(E!lL`_R=^0NEQLCW#o`-#A5wl>Yo+Zu9&5K3ME2uTWjB4?)hjSiIc?y~t z&8rOi9PF#0nYkXO5t-8&PtPQ7?XndTtwn!i-Em0Y#17$MS2tK`v9Yhlss=w+x^~AJ z^v@rU&o!<)WM6A1Dc`kfQQH~~hpbh5)UbGQ>rfc$kIgl@n?3e7@b7%0oI=VEt8}$J zG3Ogm)y-#T;=p8WA=?OP2EzNJP~s2j2}RZ@ZMSZbyGJK8PRb2jo(o=0pep0E(%|T>t<8 From 2124c301e8aa24d0583c77438c25331ccbcf8845 Mon Sep 17 00:00:00 2001 From: rollrat Date: Mon, 25 May 2026 09:10:45 +0900 Subject: [PATCH 16/19] Add output metadata roundtrip support --- src/graph/logic.rs | 83 +++++++++- src/lib.rs | 1 + src/main.rs | 14 +- src/output.rs | 65 ++++++++ .../place_and_route/local_placer/mod.rs | 44 +++++- .../place_and_route/local_placer/tests.rs | 23 ++- src/transform/place_and_route/utils.rs | 142 ++++++++++++++++++ src/transform/world_to_logic.rs | 35 ++++- src/verilog/lexer.rs | 6 +- src/verilog/lower.rs | 6 +- src/verilog/mod.rs | 20 ++- ...-adder-generated-from-verilog.outputs.json | 21 +++ 12 files changed, 437 insertions(+), 23 deletions(-) create mode 100644 src/output.rs create mode 100644 test/half-adder-generated-from-verilog.outputs.json diff --git a/src/graph/logic.rs b/src/graph/logic.rs index 9d2beb3..2ea0f55 100644 --- a/src/graph/logic.rs +++ b/src/graph/logic.rs @@ -70,6 +70,67 @@ impl LogicGraph { .collect() } + pub fn named_outputs(&self) -> Vec<(String, GraphNodeId)> { + let mut outputs = self + .nodes + .iter() + .filter_map(|node| match &node.kind { + GraphNodeKind::Output(name) => Some((name.clone(), node.inputs[0])), + _ => None, + }) + .collect::>(); + outputs.sort_by(|(a, _), (b, _)| a.cmp(b)); + outputs + } + + pub fn terminal_sources(&self) -> Vec { + self.outputs() + .into_iter() + .filter(|node_id| { + self.find_node_by_id(*node_id) + .is_some_and(|node| !matches!(node.kind, GraphNodeKind::Output(_))) + }) + .sorted() + .collect() + } + + pub fn attach_outputs(mut self, outputs: I) -> eyre::Result + where + I: IntoIterator, + { + let mut next_id = self.graph.max_node_id().map_or(0, |id| id + 1); + for (name, source_id) in outputs { + if self.find_node_by_id(source_id).is_none() { + eyre::bail!("cannot attach output {name}: missing source node {source_id}"); + } + + self.graph.nodes.push(GraphNode { + id: next_id, + kind: GraphNodeKind::Output(name), + inputs: vec![source_id], + ..Default::default() + }); + next_id += 1; + } + + self.graph.nodes.sort_by_key(|node| node.id); + self.graph.build_outputs(); + self.graph.build_producers(); + self.graph.build_consumers(); + self.graph.verify()?; + Ok(self) + } + + pub fn attach_anonymous_outputs(self) -> eyre::Result { + let outputs = self + .terminal_sources() + .into_iter() + .enumerate() + .map(|(index, source_id)| (format!("#{index}"), source_id)) + .collect::>(); + self.attach_outputs(outputs) + } + pub fn externally_observable_truth_table(&self) -> eyre::Result { let table = self.truth_table()?; let output_source_ids = self.externally_observable_output_source_ids(); @@ -658,7 +719,27 @@ mod tests { let table = graph.truth_table()?; assert_eq!(table.input_names, vec!["A_0", "carry_in"]); - assert_eq!(table.output_tables["SUM_0"], vec![false, false, false, true]); + assert_eq!( + table.output_tables["SUM_0"], + vec![false, false, false, true] + ); + + Ok(()) + } + + #[test] + fn attach_anonymous_outputs_names_terminal_sources() -> eyre::Result<()> { + let mut graph = LogicGraph::from_stmt("a&b", "out")?; + graph.graph.remove_output("out"); + let graph = graph.attach_anonymous_outputs()?; + + assert_eq!(graph.named_outputs().len(), 1); + assert_eq!(graph.named_outputs()[0].0, "#0"); + assert_eq!(graph.terminal_sources().len(), 0); + assert_eq!( + graph.truth_table()?.output_tables["#0"], + vec![false, false, false, true] + ); Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 639c205..96cf9a7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ pub mod cluster; pub mod graph; pub mod logic; pub mod nbt; +pub mod output; pub mod sequential; pub mod transform; pub mod utils; diff --git a/src/main.rs b/src/main.rs index d67e13d..889db79 100644 --- a/src/main.rs +++ b/src/main.rs @@ -62,13 +62,19 @@ fn main() -> eyre::Result<()> { route_step_sampling_policy: SamplingPolicy::Random(100), }; let placer = LocalPlacer::new(graph, config)?; - let worlds = placer.generate(DimSize(10, 10, 5), None); - let Some(world) = worlds.into_iter().next() else { + let worlds = placer.generate_with_outputs(DimSize(10, 10, 5), None); + let Some(placed) = worlds.into_iter().next() else { eyre::bail!("placement produced no worlds"); }; - world.to_nbt().save(&output); + placed.world.to_nbt().save(&output); + let metadata_path = output.with_extension("outputs.json"); + placed.metadata().save(&metadata_path)?; - println!("exported Verilog graph: path={}", output.display()); + println!( + "exported Verilog graph: path={} outputs={}", + output.display(), + metadata_path.display() + ); Ok(()) } diff --git a/src/output.rs b/src/output.rs new file mode 100644 index 0000000..5cd80d8 --- /dev/null +++ b/src/output.rs @@ -0,0 +1,65 @@ +use std::fs; +use std::path::Path; + +use serde::{Deserialize, Serialize}; + +use crate::world::position::Position; +use crate::world::World3D; + +const FORMAT: &str = "redstone-compiler.outputs.v1"; + +#[derive(Debug, Clone)] +pub struct PlacedWorld { + pub world: World3D, + pub outputs: Vec, +} + +impl PlacedWorld { + pub fn metadata(&self) -> OutputMetadata { + OutputMetadata::new(self.outputs.clone()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct OutputMetadata { + pub format: String, + pub outputs: Vec, +} + +impl OutputMetadata { + pub fn new(outputs: Vec) -> Self { + Self { + format: FORMAT.to_owned(), + outputs, + } + } + + pub fn load(path: impl AsRef) -> eyre::Result { + let metadata = serde_json::from_str(&fs::read_to_string(path)?)?; + Ok(metadata) + } + + pub fn save(&self, path: impl AsRef) -> eyre::Result<()> { + fs::write(path, serde_json::to_string_pretty(self)?)?; + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct OutputEndpoint { + pub name: String, + pub position: [usize; 3], +} + +impl OutputEndpoint { + pub fn new(name: String, position: Position) -> Self { + Self { + name, + position: [position.0, position.1, position.2], + } + } + + pub fn position(&self) -> Position { + Position(self.position[0], self.position[1], self.position[2]) + } +} diff --git a/src/transform/place_and_route/local_placer/mod.rs b/src/transform/place_and_route/local_placer/mod.rs index 5f3dfc4..2fde7c2 100644 --- a/src/transform/place_and_route/local_placer/mod.rs +++ b/src/transform/place_and_route/local_placer/mod.rs @@ -12,6 +12,7 @@ use super::sampling::SamplingPolicy; use crate::graph::logic::LogicGraph; use crate::graph::{GraphNode, GraphNodeId, GraphNodeKind}; use crate::logic::LogicType; +use crate::output::{OutputEndpoint, PlacedWorld}; use crate::sequential::layout::SequentialMacro; use crate::sequential::{SequentialPrimitive, SequentialType}; use crate::transform::place_and_route::estimate::{bounding_box_of_positions, world_compact_cost}; @@ -139,6 +140,20 @@ impl LocalPlacer { self.generate_inner(dim, finish_step, None) } + pub fn generate_with_outputs( + &self, + dim: DimSize, + finish_step: Option, + ) -> Vec { + self.generate_queue(dim, finish_step, None) + .into_iter() + .map(|(world, state)| PlacedWorld { + world, + outputs: self.output_endpoints(&state), + }) + .collect() + } + pub fn generate_with_debug( &self, dim: DimSize, @@ -160,6 +175,20 @@ impl LocalPlacer { .collect() } + fn output_endpoints(&self, state: &PlacementState) -> Vec { + self.graph + .nodes + .iter() + .filter_map(|node| match &node.kind { + GraphNodeKind::Output(name) => state + .node_position(node.id) + .map(|position| OutputEndpoint::new(name.clone(), position)), + _ => None, + }) + .sorted_by(|a, b| a.name.cmp(&b.name)) + .collect() + } + fn generate_queue( &self, dim: DimSize, @@ -450,9 +479,22 @@ impl LocalPlacer { .iter() .skip(step + 1) .filter_map(|node_id| self.graph.find_node_by_id(*node_id)) - .filter(|node| !matches!(node.kind, GraphNodeKind::Output(_))) + .filter(|node| { + self.config.materialize_outputs || !matches!(node.kind, GraphNodeKind::Output(_)) + }) .flat_map(|node| node.inputs.iter().copied()) .chain(self.graph.externally_observable_output_source_ids()) + .chain( + self.config + .materialize_outputs + .then(|| { + self.graph.nodes.iter().filter_map(|node| { + matches!(node.kind, GraphNodeKind::Output(_)).then_some(node.id) + }) + }) + .into_iter() + .flatten(), + ) .collect::>(); for (_, state) in &mut queue { diff --git a/src/transform/place_and_route/local_placer/tests.rs b/src/transform/place_and_route/local_placer/tests.rs index 5ab9dc3..f9a69a0 100644 --- a/src/transform/place_and_route/local_placer/tests.rs +++ b/src/transform/place_and_route/local_placer/tests.rs @@ -323,6 +323,24 @@ fn output_step_routes_visible_redstone_endpoint_from_torch_source() -> eyre::Res Ok(()) } +#[test] +fn generate_with_outputs_reports_materialized_output_positions() -> eyre::Result<()> { + let graph = LogicGraph::from_stmt("~a", "out")?.prepare_place()?; + let mut config = config(1); + config.materialize_outputs = true; + let placer = LocalPlacer::new(graph, config)?; + + let placed = placer.generate_with_outputs(DimSize(5, 5, 3), None); + + assert!(!placed.is_empty()); + assert!(placed.iter().any(|placed| { + placed.outputs.iter().any(|endpoint| { + endpoint.name == "out" && placed.world[endpoint.position()].kind.is_redstone() + }) + })); + Ok(()) +} + #[test] fn future_join_cost_weights_pairs_with_remaining_fanout() { let mut graph = Graph { @@ -746,7 +764,10 @@ fn redstone_below_switch_powered_cobble_is_short() { let cobble_pos = Position(1, 2, 2); let redstone_pos = Position(1, 2, 1); - place_node(&mut world, PlacedNode::new(switch_pos, switch(Direction::East))); + place_node( + &mut world, + PlacedNode::new(switch_pos, switch(Direction::East)), + ); place_node(&mut world, PlacedNode::new_cobble(cobble_pos)); let redstone_node = PlacedNode::new_redstone(redstone_pos); diff --git a/src/transform/place_and_route/utils.rs b/src/transform/place_and_route/utils.rs index 1db3d90..8dfc165 100644 --- a/src/transform/place_and_route/utils.rs +++ b/src/transform/place_and_route/utils.rs @@ -2,8 +2,11 @@ use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use crate::graph::logic::LogicGraph; use crate::graph::world::WorldGraph; +use crate::graph::{GraphNodeId, GraphNodeKind}; +use crate::output::OutputMetadata; use crate::transform::logic::LogicGraphTransformer; use crate::transform::world_to_logic::WorldToLogicTransformer; +use crate::world::block::BlockKind; use crate::world::{World, World3D}; pub fn world3d_to_logic(world3d: &World3D) -> eyre::Result { @@ -16,6 +19,145 @@ pub fn world_to_logic(world: &World) -> eyre::Result { normalize_logic_graph(logic_graph) } +pub fn world_to_logic_with_outputs( + world: &World, + metadata: &OutputMetadata, +) -> eyre::Result { + let raw_world_graph = WorldGraph::from(world); + let raw_position_to_node = raw_world_graph + .positions + .iter() + .map(|(node_id, position)| (*position, *node_id)) + .collect::>(); + let transformer = WorldToLogicTransformer::new(raw_world_graph.clone(), true)?; + let folded_graph = transformer.world_graph().clone(); + let logic_graph = transformer.transform_preserving_node_ids()?; + let outputs = metadata + .outputs + .iter() + .map(|endpoint| { + let position = endpoint.position(); + let Some(raw_source_id) = raw_position_to_node.get(&position).copied() else { + eyre::bail!( + "missing world graph node for output {} at {:?}", + endpoint.name, + position + ); + }; + let source_id = + resolve_folded_output_source(raw_source_id, &raw_world_graph, &folded_graph)?; + let source_id = if logic_graph.find_node_by_id(source_id).is_some() { + source_id + } else if let Some(node) = logic_graph + .nodes + .iter() + .filter(|node| node.tag == format!("From #{source_id}")) + .max_by_key(|node| node.id) + { + node.id + } else { + eyre::bail!( + "missing logic graph node {} for output {} at {:?}", + source_id, + endpoint.name, + position + ); + }; + Ok((endpoint.name.clone(), source_id)) + }) + .collect::>>()?; + + normalize_logic_graph(logic_graph.attach_outputs(outputs)?) +} + +fn resolve_folded_output_source( + raw_source_id: GraphNodeId, + raw_graph: &WorldGraph, + folded_graph: &WorldGraph, +) -> eyre::Result { + if folded_graph.graph.find_node_by_id(raw_source_id).is_some() { + return Ok(raw_source_id); + } + + let Some(raw_node) = raw_graph.graph.find_node_by_id(raw_source_id) else { + eyre::bail!("missing raw output source node {raw_source_id}"); + }; + if !matches!(&raw_node.kind, GraphNodeKind::Block(block) if matches!(block.kind, BlockKind::Redstone { .. })) + { + return Ok(raw_source_id); + } + + let component = collect_redstone_component(raw_source_id, raw_graph); + let component_inputs = component_external_edges(raw_graph, &component, true); + let component_outputs = component_external_edges(raw_graph, &component, false); + + folded_graph + .graph + .nodes + .iter() + .find(|node| { + matches!(&node.kind, GraphNodeKind::Block(block) if matches!(block.kind, BlockKind::Redstone { .. })) + && node.inputs.iter().copied().collect::>() + == component_inputs + && node.outputs.iter().copied().collect::>() + == component_outputs + }) + .map(|node| node.id) + .ok_or_else(|| eyre::eyre!("missing folded redstone source for raw node {raw_source_id}")) +} + +fn collect_redstone_component( + root: GraphNodeId, + graph: &WorldGraph, +) -> std::collections::HashSet { + let mut component = std::collections::HashSet::new(); + let mut stack = vec![root]; + + while let Some(node_id) = stack.pop() { + if !component.insert(node_id) { + continue; + } + + let neighbors = graph + .graph + .producers + .get(&node_id) + .into_iter() + .flatten() + .chain(graph.graph.consumers.get(&node_id).into_iter().flatten()); + for neighbor in neighbors { + if graph.graph.find_node_by_id(*neighbor).is_some_and(|node| { + matches!(&node.kind, GraphNodeKind::Block(block) if matches!(block.kind, BlockKind::Redstone { .. })) + }) { + stack.push(*neighbor); + } + } + } + + component +} + +fn component_external_edges( + graph: &WorldGraph, + component: &std::collections::HashSet, + producers: bool, +) -> std::collections::HashSet { + component + .iter() + .flat_map(|node_id| { + if producers { + graph.graph.producers.get(node_id) + } else { + graph.graph.consumers.get(node_id) + } + .into_iter() + .flatten() + .copied() + }) + .filter(|node_id| !component.contains(node_id)) + .collect() +} + pub fn equivalent_graph(src: &LogicGraph, tar: &LogicGraph) -> bool { petgraph::algo::is_isomorphic_matching( &src.to_petgraph(), diff --git a/src/transform/world_to_logic.rs b/src/transform/world_to_logic.rs index 601d753..862b698 100644 --- a/src/transform/world_to_logic.rs +++ b/src/transform/world_to_logic.rs @@ -3,9 +3,10 @@ use std::collections::HashMap; use super::world::WorldGraphTransformer; use crate::graph::logic::LogicGraph; use crate::graph::world::WorldGraph; -use crate::graph::{Graph, GraphNode, GraphNodeKind}; +use crate::graph::{Graph, GraphNode, GraphNodeId, GraphNodeKind}; use crate::logic::{Logic, LogicType}; use crate::world::block::BlockKind; +use crate::world::position::Position; // for testing Layout vs. Schematic #[derive(Default)] @@ -63,6 +64,22 @@ impl WorldToLogicTransformer { } pub fn transform(mut self) -> eyre::Result { + self.transform_inner(true) + } + + pub fn positions(&self) -> &HashMap { + &self.graph.positions + } + + pub fn world_graph(&self) -> &WorldGraph { + &self.graph + } + + pub fn transform_preserving_node_ids(mut self) -> eyre::Result { + self.transform_inner(false) + } + + fn transform_inner(&mut self, rebuild_node_ids: bool) -> eyre::Result { let mut new_in_id = HashMap::new(); let mut nodes = Vec::new(); @@ -147,11 +164,19 @@ impl WorldToLogicTransformer { nodes.extend(new_nodes); } - let graph = Graph { + let mut graph = Graph { nodes, ..Default::default() + }; + + if rebuild_node_ids { + graph = graph.rebuild_node_ids(); + } else { + graph.nodes.sort_by_key(|node| node.id); + graph.build_outputs(); + graph.build_producers(); + graph.build_consumers(); } - .rebuild_node_ids(); Ok(LogicGraph { graph }) } @@ -268,9 +293,7 @@ mod tests { let table = logic.truth_table()?; assert!( - table - .output_table_set() - .contains(&vec![true, true]), + table.output_table_set().contains(&vec![true, true]), "shorted switch and inverted output should extract as a constant-high output, got {:?}", table ); diff --git a/src/verilog/lexer.rs b/src/verilog/lexer.rs index ca13b29..b3030d8 100644 --- a/src/verilog/lexer.rs +++ b/src/verilog/lexer.rs @@ -124,16 +124,14 @@ mod tests { #[test] fn lexes_combinational_module_subset() -> eyre::Result<()> { - let tokens = lex( - r#" + let tokens = lex(r#" module half_adder(a, b, s, c); input a, b; output s, c; assign s = a ^ b; assign c = a & b; endmodule - "#, - )?; + "#)?; assert_eq!( tokens, diff --git a/src/verilog/lower.rs b/src/verilog/lower.rs index cb2f5b1..25aec9e 100644 --- a/src/verilog/lower.rs +++ b/src/verilog/lower.rs @@ -84,11 +84,7 @@ fn rewrite_expr( ) -> Expr { match expr { Expr::Ident(name) => Expr::Ident(rewrite_signal(name, instance_prefix, substitutions)), - Expr::Not(expr) => Expr::Not(Box::new(rewrite_expr( - expr, - instance_prefix, - substitutions, - ))), + Expr::Not(expr) => Expr::Not(Box::new(rewrite_expr(expr, instance_prefix, substitutions))), Expr::Binary { op, left, right } => Expr::Binary { op: *op, left: Box::new(rewrite_expr(left, instance_prefix, substitutions)), diff --git a/src/verilog/mod.rs b/src/verilog/mod.rs index e72b495..2ea4c89 100644 --- a/src/verilog/mod.rs +++ b/src/verilog/mod.rs @@ -22,7 +22,8 @@ mod tests { use crate::graph::GraphNodeKind; use crate::logic::LogicType; use crate::nbt::NBTRoot; - use crate::transform::place_and_route::utils::world_to_logic; + use crate::output::OutputMetadata; + use crate::transform::place_and_route::utils::{world_to_logic, world_to_logic_with_outputs}; #[test] fn load_logic_graph_reads_verilog_file() -> eyre::Result<()> { @@ -91,6 +92,23 @@ mod tests { Ok(()) } + #[test] + fn half_adder_generated_nbt_restores_outputs_with_metadata() -> eyre::Result<()> { + let expected = load_logic_graph("test/half-adder.v")?; + let nbt = NBTRoot::load("test/half-adder-generated-from-verilog.nbt")?; + let metadata = OutputMetadata::load("test/half-adder-generated-from-verilog.outputs.json")?; + let generated = world_to_logic_with_outputs(&nbt.to_world(), &metadata)?; + + assert_eq!(output_names(&generated), vec!["c", "s"]); + assert!(generated + .externally_observable_truth_table()? + .contains_output_tables_under_input_permutation( + &expected.externally_observable_truth_table()? + )); + + Ok(()) + } + fn maybe_dump_graphviz(name: &str, graph: &LogicGraph) { if std::env::var_os("PRINT_VERILOG_GRAPHS").is_some() { eprintln!("--- {name} ---\n{}", graph.to_graphviz()); diff --git a/test/half-adder-generated-from-verilog.outputs.json b/test/half-adder-generated-from-verilog.outputs.json new file mode 100644 index 0000000..c49af2a --- /dev/null +++ b/test/half-adder-generated-from-verilog.outputs.json @@ -0,0 +1,21 @@ +{ + "format": "redstone-compiler.outputs.v1", + "outputs": [ + { + "name": "c", + "position": [ + 2, + 6, + 1 + ] + }, + { + "name": "s", + "position": [ + 4, + 4, + 3 + ] + } + ] +} \ No newline at end of file From debb032fbd9ea5b1090e867995540cb9b3414205 Mon Sep 17 00:00:00 2001 From: rollrat Date: Mon, 25 May 2026 12:40:41 +0900 Subject: [PATCH 17/19] Remove superpowers Verilog plan doc --- .../plans/2026-05-24-verilog-frontend.md | 1372 ----------------- 1 file changed, 1372 deletions(-) delete mode 100644 docs/superpowers/plans/2026-05-24-verilog-frontend.md diff --git a/docs/superpowers/plans/2026-05-24-verilog-frontend.md b/docs/superpowers/plans/2026-05-24-verilog-frontend.md deleted file mode 100644 index 6e51134..0000000 --- a/docs/superpowers/plans/2026-05-24-verilog-frontend.md +++ /dev/null @@ -1,1372 +0,0 @@ -# Verilog Frontend Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add a small, testable Verilog frontend that lowers easy `assign`-based combinational modules into the existing `LogicGraph` pipeline before adding broader RTL features. - -**Architecture:** Start with no external Verilog dependency: parse a deliberately small Verilog subset into a local AST, render expressions into fully parenthesized logic strings, and reuse `LogicGraph::from_stmt` plus a new multi-assignment helper. Keep parser, AST, and lowering in separate files so a real parser such as `sv-parser` can replace only the source parsing layer later. - -**Tech Stack:** Rust 2021, existing `LogicGraph`, `Graph::merge`, `eyre`, `cargo test --release` for local placer and place-and-route tests. - ---- - -## Scope - -Build this in increasing order of value: - -1. Multi-assignment combinational graph helper. -2. Verilog-friendly identifier support in the existing expression parser. -3. Tiny Verilog AST and source parser for scalar `module`, `input`, `output`, `wire`, and `assign`. -4. Verilog expression lowering for `~`, `&`, `^`, `|`, and parentheses with Verilog precedence. -5. CLI parse-and-prepare integration. -6. Small-gate NBT export path only after graph lowering is stable. -7. Bus flattening and module instances as follow-up work. - -This plan intentionally excludes `always`, procedural assignments, parameters, generate blocks, signed arithmetic, memories, delays, tasks/functions, tri-state, and full SystemVerilog syntax. - -## File Structure - -- Modify `src/graph/logic.rs`: add a small public helper for merging multiple output assignments into one `LogicGraph`; extend identifier tokenization enough for common Verilog scalar names. -- Replace `src/verilog/mod.rs`: expose the frontend API and keep the old commented `sv_parser` stub out of the active path. -- Create `src/verilog/ast.rs`: define `VerilogModule`, declarations, assignments, and expression AST. -- Create `src/verilog/lexer.rs`: tokenize the supported source subset and strip comments. -- Create `src/verilog/parser.rs`: parse the supported subset into `VerilogModule`. -- Create `src/verilog/lower.rs`: convert `VerilogModule` into `LogicGraph`. -- Modify `src/main.rs`: call the frontend for `.v` files and report parse/prepare success before adding NBT output. -- Optional parser replacement path: modify `Cargo.toml` to add `sv-parser` only when the handwritten subset is not enough. - ---- - -### Task 1: Add Multi-Assignment LogicGraph Helper - -**Files:** -- Modify: `src/graph/logic.rs` - -- [ ] **Step 1: Write the failing test** - -Add this test inside `#[cfg(test)] mod tests` in `src/graph/logic.rs`. - -```rust -#[test] -fn from_assignments_builds_half_adder() -> eyre::Result<()> { - let graph = LogicGraph::from_assignments([ - ("s".to_owned(), "a^b".to_owned()), - ("c".to_owned(), "a&b".to_owned()), - ])?; - let table = graph.truth_table()?; - - assert_eq!(table.input_names, vec!["a", "b"]); - assert_eq!(table.output_tables["s"], vec![false, true, true, false]); - assert_eq!(table.output_tables["c"], vec![false, false, false, true]); - - Ok(()) -} -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: - -```powershell -cargo test --release graph::logic::tests::from_assignments_builds_half_adder -``` - -Expected: fail to compile because `LogicGraph::from_assignments` does not exist. - -- [ ] **Step 3: Implement the helper** - -Add this method to `impl LogicGraph` in `src/graph/logic.rs`. - -```rust -pub fn from_assignments(assignments: I) -> eyre::Result -where - I: IntoIterator, -{ - let mut graphs = assignments - .into_iter() - .map(|(output, expr)| LogicGraph::from_stmt(&expr, &output)) - .collect::>>()? - .into_iter(); - - let Some(mut graph) = graphs.next() else { - eyre::bail!("expected at least one logic assignment"); - }; - - for next in graphs { - graph.graph.merge(next.graph); - } - - Ok(graph) -} -``` - -- [ ] **Step 4: Run test to verify it passes** - -Run: - -```powershell -cargo test --release graph::logic::tests::from_assignments_builds_half_adder -``` - -Expected: pass. - -- [ ] **Step 5: Commit** - -```powershell -git add src/graph/logic.rs -git commit -m "Add multi-assignment logic graph helper" -``` - ---- - -### Task 2: Accept Common Verilog Identifiers In Logic Expressions - -**Files:** -- Modify: `src/graph/logic.rs` - -- [ ] **Step 1: Write the failing test** - -Add this test inside `#[cfg(test)] mod tests` in `src/graph/logic.rs`. - -```rust -#[test] -fn logic_parser_accepts_verilog_style_identifiers() -> eyre::Result<()> { - let graph = LogicGraph::from_stmt("A_0&carry_in", "SUM_0")?; - let table = graph.truth_table()?; - - assert_eq!(table.input_names, vec!["A_0", "carry_in"]); - assert_eq!(table.output_tables["SUM_0"], vec![false, false, false, true]); - - Ok(()) -} -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: - -```powershell -cargo test --release graph::logic::tests::logic_parser_accepts_verilog_style_identifiers -``` - -Expected: panic or fail while lexing `A_0`. - -- [ ] **Step 3: Extend identifier scanning** - -Replace the identifier branch in `LogicGraphBuilder::next` with helper predicates. - -```rust -fn is_ident_start(ch: char) -> bool { - ch == '_' || ch.is_ascii_alphabetic() -} - -fn is_ident_continue(ch: char) -> bool { - ch == '_' || ch == '$' || ch.is_ascii_alphanumeric() -} -``` - -Then change the match arm from the lowercase-only branch to: - -```rust -ch if is_ident_start(ch) => { - let mut result = String::new(); - - while self.stmt.len() != next_ptr - && is_ident_continue(self.stmt.chars().nth(next_ptr).unwrap()) - { - result.push(self.stmt.chars().nth(next_ptr).unwrap()); - next_ptr = self.next_ptr(); - } - - self.ptr -= 1; - - LogicStringTokenType::Ident(result) -} -``` - -- [ ] **Step 4: Run focused tests** - -Run: - -```powershell -cargo test --release graph::logic::tests -``` - -Expected: both pass. - -- [ ] **Step 5: Commit** - -```powershell -git add src/graph/logic.rs -git commit -m "Accept Verilog-style logic identifiers" -``` - ---- - -### Task 3: Define Minimal Verilog AST - -**Files:** -- Create: `src/verilog/ast.rs` -- Modify: `src/verilog/mod.rs` - -- [ ] **Step 1: Write the AST file** - -Create `src/verilog/ast.rs`. - -```rust -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct VerilogModule { - pub name: String, - pub ports: Vec, - pub declarations: Vec, - pub assignments: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Declaration { - pub direction: Option, - pub names: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PortDirection { - Input, - Output, - Wire, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Assignment { - pub output: String, - pub expr: Expr, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Expr { - Ident(String), - Not(Box), - Binary { - op: BinaryOp, - left: Box, - right: Box, - }, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BinaryOp { - And, - Xor, - Or, -} - -impl Expr { - pub fn to_logic_stmt(&self) -> String { - match self { - Expr::Ident(name) => name.clone(), - Expr::Not(expr) => format!("~({})", expr.to_logic_stmt()), - Expr::Binary { op, left, right } => { - let op = match op { - BinaryOp::And => "&", - BinaryOp::Xor => "^", - BinaryOp::Or => "|", - }; - format!("({}{}{})", left.to_logic_stmt(), op, right.to_logic_stmt()) - } - } - } -} -``` - -- [ ] **Step 2: Expose the module** - -Replace `src/verilog/mod.rs` with: - -```rust -pub mod ast; -``` - -- [ ] **Step 3: Add a unit test for expression rendering** - -Add this to `src/verilog/ast.rs`. - -```rust -#[cfg(test)] -mod tests { - use super::{BinaryOp, Expr}; - - #[test] - fn expr_renders_fully_parenthesized_logic_stmt() { - let expr = Expr::Binary { - op: BinaryOp::Xor, - left: Box::new(Expr::Ident("a".to_owned())), - right: Box::new(Expr::Binary { - op: BinaryOp::And, - left: Box::new(Expr::Ident("b".to_owned())), - right: Box::new(Expr::Ident("c".to_owned())), - }), - }; - - assert_eq!(expr.to_logic_stmt(), "(a^(b&c))"); - } -} -``` - -- [ ] **Step 4: Run AST test** - -Run: - -```powershell -cargo test --release verilog::ast::tests::expr_renders_fully_parenthesized_logic_stmt -``` - -Expected: pass. - -- [ ] **Step 5: Commit** - -```powershell -git add src/verilog/mod.rs src/verilog/ast.rs -git commit -m "Add minimal Verilog AST" -``` - ---- - -### Task 4: Add Lexer For Supported Verilog Subset - -**Files:** -- Create: `src/verilog/lexer.rs` -- Modify: `src/verilog/mod.rs` - -- [ ] **Step 1: Write failing lexer test** - -Create `src/verilog/lexer.rs` with token definitions and this test first. - -```rust -#[cfg(test)] -mod tests { - use super::{lex, Token}; - - #[test] - fn lexes_combinational_module_subset() -> eyre::Result<()> { - let tokens = lex( - r#" - module half_adder(a, b, s, c); - input a, b; - output s, c; - assign s = a ^ b; - assign c = a & b; - endmodule - "#, - )?; - - assert_eq!( - tokens, - vec![ - Token::Module, - Token::Ident("half_adder".to_owned()), - Token::LParen, - Token::Ident("a".to_owned()), - Token::Comma, - Token::Ident("b".to_owned()), - Token::Comma, - Token::Ident("s".to_owned()), - Token::Comma, - Token::Ident("c".to_owned()), - Token::RParen, - Token::Semi, - Token::Input, - Token::Ident("a".to_owned()), - Token::Comma, - Token::Ident("b".to_owned()), - Token::Semi, - Token::Output, - Token::Ident("s".to_owned()), - Token::Comma, - Token::Ident("c".to_owned()), - Token::Semi, - Token::Assign, - Token::Ident("s".to_owned()), - Token::Eq, - Token::Ident("a".to_owned()), - Token::Xor, - Token::Ident("b".to_owned()), - Token::Semi, - Token::Assign, - Token::Ident("c".to_owned()), - Token::Eq, - Token::Ident("a".to_owned()), - Token::And, - Token::Ident("b".to_owned()), - Token::Semi, - Token::EndModule, - ] - ); - - Ok(()) - } -} -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: - -```powershell -cargo test --release verilog::lexer::tests::lexes_combinational_module_subset -``` - -Expected: fail because `lexer` is not exported or `lex` is not implemented. - -- [ ] **Step 3: Implement tokenization** - -Add this implementation to `src/verilog/lexer.rs`. - -```rust -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Token { - Module, - EndModule, - Input, - Output, - Wire, - Assign, - Ident(String), - LParen, - RParen, - Comma, - Semi, - Eq, - Not, - And, - Xor, - Or, -} - -pub fn lex(source: &str) -> eyre::Result> { - let chars = strip_comments(source).chars().collect::>(); - let mut tokens = Vec::new(); - let mut index = 0; - - while index < chars.len() { - let ch = chars[index]; - if ch.is_whitespace() { - index += 1; - continue; - } - - match ch { - '(' => tokens.push(Token::LParen), - ')' => tokens.push(Token::RParen), - ',' => tokens.push(Token::Comma), - ';' => tokens.push(Token::Semi), - '=' => tokens.push(Token::Eq), - '~' => tokens.push(Token::Not), - '&' => tokens.push(Token::And), - '^' => tokens.push(Token::Xor), - '|' => tokens.push(Token::Or), - ch if is_ident_start(ch) => { - let start = index; - index += 1; - while index < chars.len() && is_ident_continue(chars[index]) { - index += 1; - } - let text = chars[start..index].iter().collect::(); - tokens.push(match text.as_str() { - "module" => Token::Module, - "endmodule" => Token::EndModule, - "input" => Token::Input, - "output" => Token::Output, - "wire" => Token::Wire, - "assign" => Token::Assign, - _ => Token::Ident(text), - }); - continue; - } - _ => eyre::bail!("unsupported Verilog character `{ch}` at byte-like index {index}"), - } - - index += 1; - } - - Ok(tokens) -} - -fn strip_comments(source: &str) -> String { - let mut result = String::new(); - let mut chars = source.chars().peekable(); - while let Some(ch) = chars.next() { - if ch == '/' && chars.peek() == Some(&'/') { - chars.next(); - for next in chars.by_ref() { - if next == '\n' { - result.push('\n'); - break; - } - } - } else { - result.push(ch); - } - } - result -} - -fn is_ident_start(ch: char) -> bool { - ch == '_' || ch.is_ascii_alphabetic() -} - -fn is_ident_continue(ch: char) -> bool { - ch == '_' || ch == '$' || ch.is_ascii_alphanumeric() -} -``` - -- [ ] **Step 4: Export lexer** - -Update `src/verilog/mod.rs`. - -```rust -pub mod ast; -pub mod lexer; -``` - -- [ ] **Step 5: Run lexer test** - -Run: - -```powershell -cargo test --release verilog::lexer::tests::lexes_combinational_module_subset -``` - -Expected: pass. - -- [ ] **Step 6: Commit** - -```powershell -git add src/verilog/mod.rs src/verilog/lexer.rs -git commit -m "Add minimal Verilog lexer" -``` - ---- - -### Task 5: Parse Scalar Module Declarations And Assignments - -**Files:** -- Create: `src/verilog/parser.rs` -- Modify: `src/verilog/mod.rs` - -- [ ] **Step 1: Write parser tests** - -Create `src/verilog/parser.rs` and add these tests first. - -```rust -#[cfg(test)] -mod tests { - use super::parse_module; - use crate::verilog::ast::{BinaryOp, Expr, PortDirection}; - - #[test] - fn parses_half_adder_module() -> eyre::Result<()> { - let module = parse_module( - r#" - module half_adder(a, b, s, c); - input a, b; - output s, c; - wire tmp; - assign s = a ^ b; - assign c = a & b; - endmodule - "#, - )?; - - assert_eq!(module.name, "half_adder"); - assert_eq!(module.ports, vec!["a", "b", "s", "c"]); - assert_eq!(module.declarations[0].direction, Some(PortDirection::Input)); - assert_eq!(module.declarations[1].direction, Some(PortDirection::Output)); - assert_eq!(module.declarations[2].direction, Some(PortDirection::Wire)); - assert_eq!(module.assignments.len(), 2); - assert_eq!(module.assignments[0].output, "s"); - assert_eq!( - module.assignments[0].expr, - Expr::Binary { - op: BinaryOp::Xor, - left: Box::new(Expr::Ident("a".to_owned())), - right: Box::new(Expr::Ident("b".to_owned())), - } - ); - - Ok(()) - } - - #[test] - fn parses_verilog_operator_precedence() -> eyre::Result<()> { - let module = parse_module( - r#" - module precedence(a, b, c, y); - input a, b, c; - output y; - assign y = a ^ b & c; - endmodule - "#, - )?; - - assert_eq!(module.assignments[0].expr.to_logic_stmt(), "(a^(b&c))"); - - Ok(()) - } -} -``` - -- [ ] **Step 2: Run parser tests to verify they fail** - -Run: - -```powershell -cargo test --release verilog::parser::tests -``` - -Expected: fail because parser is not implemented. - -- [ ] **Step 3: Implement parser skeleton** - -Use `crate::verilog::lexer::lex` and a cursor over `Vec`. The public entry point should be: - -```rust -pub fn parse_module(source: &str) -> eyre::Result { - Parser::new(lex(source)?).parse_module() -} -``` - -Define: - -```rust -struct Parser { - tokens: Vec, - index: usize, -} -``` - -Add methods: - -```rust -impl Parser { - fn new(tokens: Vec) -> Self { - Self { tokens, index: 0 } - } - - fn parse_module(&mut self) -> eyre::Result { - self.expect(Token::Module)?; - let name = self.expect_ident()?; - self.expect(Token::LParen)?; - let ports = self.parse_ident_list()?; - self.expect(Token::RParen)?; - self.expect(Token::Semi)?; - - let mut declarations = Vec::new(); - let mut assignments = Vec::new(); - while !self.consume(&Token::EndModule) { - match self.peek() { - Some(Token::Input) | Some(Token::Output) | Some(Token::Wire) => { - declarations.push(self.parse_declaration()?); - } - Some(Token::Assign) => assignments.push(self.parse_assignment()?), - Some(token) => eyre::bail!("unsupported Verilog token in module body: {token:?}"), - None => eyre::bail!("expected endmodule"), - } - } - - Ok(VerilogModule { - name, - ports, - declarations, - assignments, - }) - } -} -``` - -- [ ] **Step 4: Implement declarations, assignments, and expressions** - -Expression precedence must be: - -```text -primary / unary ~ -& -^ -| -``` - -Implement methods with these signatures: - -```rust -fn parse_declaration(&mut self) -> eyre::Result; -fn parse_assignment(&mut self) -> eyre::Result; -fn parse_expr(&mut self) -> eyre::Result; -fn parse_or(&mut self) -> eyre::Result; -fn parse_xor(&mut self) -> eyre::Result; -fn parse_and(&mut self) -> eyre::Result; -fn parse_unary(&mut self) -> eyre::Result; -fn parse_primary(&mut self) -> eyre::Result; -``` - -Use left-associative binary construction: - -```rust -expr = Expr::Binary { - op: BinaryOp::And, - left: Box::new(expr), - right: Box::new(rhs), -}; -``` - -- [ ] **Step 5: Export parser** - -Update `src/verilog/mod.rs`. - -```rust -pub mod ast; -pub mod lexer; -pub mod parser; -``` - -- [ ] **Step 6: Run parser tests** - -Run: - -```powershell -cargo test --release verilog::parser::tests -``` - -Expected: pass. - -- [ ] **Step 7: Commit** - -```powershell -git add src/verilog/mod.rs src/verilog/parser.rs -git commit -m "Parse minimal combinational Verilog modules" -``` - ---- - -### Task 6: Lower Parsed Verilog To LogicGraph - -**Files:** -- Create: `src/verilog/lower.rs` -- Modify: `src/verilog/mod.rs` - -- [ ] **Step 1: Write lowering tests** - -Create `src/verilog/lower.rs` and add these tests first. - -```rust -#[cfg(test)] -mod tests { - use super::lower_module; - use crate::verilog::parser::parse_module; - - #[test] - fn lowers_half_adder_to_truth_table() -> eyre::Result<()> { - let module = parse_module( - r#" - module half_adder(a, b, s, c); - input a, b; - output s, c; - assign s = a ^ b; - assign c = a & b; - endmodule - "#, - )?; - - let graph = lower_module(&module)?; - let table = graph.truth_table()?; - - assert_eq!(table.input_names, vec!["a", "b"]); - assert_eq!(table.output_tables["s"], vec![false, true, true, false]); - assert_eq!(table.output_tables["c"], vec![false, false, false, true]); - - Ok(()) - } - - #[test] - fn lowers_full_adder_with_intermediate_output_use() -> eyre::Result<()> { - let module = parse_module( - r#" - module full_adder(a, b, cin, sum, cout); - input a, b, cin; - output sum, cout; - assign sum = (a ^ b) ^ cin; - assign cout = (a & b) | (sum & cin); - endmodule - "#, - )?; - - let graph = lower_module(&module)?; - let table = graph.truth_table()?; - let sum = (0..8) - .map(|mask: usize| mask.count_ones() % 2 == 1) - .collect::>(); - let carry = (0..8) - .map(|mask: usize| mask.count_ones() >= 2) - .collect::>(); - - assert_eq!(table.output_tables["sum"], sum); - assert_eq!(table.output_tables["cout"], carry); - - Ok(()) - } -} -``` - -- [ ] **Step 2: Run lowering tests to verify they fail** - -Run: - -```powershell -cargo test --release verilog::lower::tests -``` - -Expected: fail because `lower_module` is not implemented or not exported. - -- [ ] **Step 3: Implement lower_module** - -Add: - -```rust -use crate::graph::logic::LogicGraph; -use crate::verilog::ast::VerilogModule; - -pub fn lower_module(module: &VerilogModule) -> eyre::Result { - if module.assignments.is_empty() { - eyre::bail!("module `{}` has no continuous assignments", module.name); - } - - LogicGraph::from_assignments( - module - .assignments - .iter() - .map(|assign| (assign.output.clone(), assign.expr.to_logic_stmt())), - ) -} -``` - -- [ ] **Step 4: Export lower and public helpers** - -Update `src/verilog/mod.rs`. - -```rust -pub mod ast; -pub mod lexer; -pub mod lower; -pub mod parser; - -use std::fs; -use std::path::Path; - -use crate::graph::logic::LogicGraph; - -pub fn load_logic_graph(path: impl AsRef) -> eyre::Result { - let source = fs::read_to_string(path)?; - let module = parser::parse_module(&source)?; - lower::lower_module(&module) -} -``` - -- [ ] **Step 5: Run lowering tests** - -Run: - -```powershell -cargo test --release verilog::lower::tests -``` - -Expected: pass. - -- [ ] **Step 6: Commit** - -```powershell -git add src/verilog/mod.rs src/verilog/lower.rs -git commit -m "Lower minimal Verilog to logic graph" -``` - ---- - -### Task 7: Add Unsupported-Construct Diagnostics - -**Files:** -- Modify: `src/verilog/lexer.rs` -- Modify: `src/verilog/parser.rs` - -- [ ] **Step 1: Write tests for clear failures** - -Add these tests to `src/verilog/parser.rs`. - -```rust -#[test] -fn rejects_always_blocks_with_clear_message() { - let error = parse_module( - r#" - module bad(clk, q); - input clk; - output q; - always @(posedge clk) q = ~q; - endmodule - "#, - ) - .unwrap_err(); - - assert!(error.to_string().contains("unsupported")); -} - -#[test] -fn rejects_vector_declarations_with_clear_message() { - let error = parse_module( - r#" - module bad(a, y); - input [3:0] a; - output y; - assign y = a; - endmodule - "#, - ) - .unwrap_err(); - - assert!(error.to_string().contains("unsupported")); -} -``` - -- [ ] **Step 2: Run tests to see current failure behavior** - -Run: - -```powershell -cargo test --release verilog::parser::tests -``` - -Expected: fail if the lexer reports a confusing character error or parser misses the unsupported construct. - -- [ ] **Step 3: Add keyword tokens for common rejected constructs** - -In `src/verilog/lexer.rs`, add token variants: - -```rust -Always, -At, -LBracket, -RBracket, -Colon, -``` - -Map: - -```rust -"always" => Token::Always, -"posedge" => Token::Ident("posedge".to_owned()), -``` - -And character tokens: - -```rust -'@' => tokens.push(Token::At), -'[' => tokens.push(Token::LBracket), -']' => tokens.push(Token::RBracket), -':' => tokens.push(Token::Colon), -``` - -- [ ] **Step 4: Reject unsupported constructs in parser** - -In module-body parsing, add explicit branches: - -```rust -Some(Token::Always) => eyre::bail!("unsupported Verilog construct: always block"), -Some(Token::LBracket) => eyre::bail!("unsupported Verilog construct: vector declaration"), -``` - -In `parse_declaration`, reject a bracket immediately after the direction: - -```rust -if matches!(self.peek(), Some(Token::LBracket)) { - eyre::bail!("unsupported Verilog construct: vector declaration"); -} -``` - -- [ ] **Step 5: Run diagnostics tests** - -Run: - -```powershell -cargo test --release verilog::parser::tests -``` - -Expected: pass. - -- [ ] **Step 6: Commit** - -```powershell -git add src/verilog/lexer.rs src/verilog/parser.rs -git commit -m "Report unsupported Verilog constructs clearly" -``` - ---- - -### Task 8: Wire Verilog Graph Loading Into CLI - -**Files:** -- Modify: `src/main.rs` - -- [ ] **Step 1: Write or run a manual fixture** - -Use existing `test/alu.v` only after fixing it to valid continuous assignments in a separate task. For this task, use a temporary valid file: - -```verilog -module half_adder(a, b, s, c); - input a, b; - output s, c; - assign s = a ^ b; - assign c = a & b; -endmodule -``` - -- [ ] **Step 2: Implement parse-and-prepare path** - -Update `main`: - -```rust -fn main() -> eyre::Result<()> { - tracing_subscriber::fmt::init(); - let opt = CompilerOption::from_args(); - - if opt.input.extension().and_then(|ext| ext.to_str()) == Some("v") { - let graph = redstone_compiler::verilog::load_logic_graph(&opt.input)?; - let prepared = graph.prepare_place()?; - println!( - "loaded Verilog graph: nodes={} inputs={} outputs={}", - prepared.nodes.len(), - prepared.inputs().len(), - prepared.outputs().len() - ); - return Ok(()); - } - - eyre::bail!("unsupported input file extension: {:?}", opt.input); -} -``` - -If `main.rs` cannot refer to the library as `redstone_compiler` in this package layout, use: - -```rust -use redstone_compiler::verilog; -``` - -and call: - -```rust -let graph = verilog::load_logic_graph(&opt.input)?; -``` - -- [ ] **Step 3: Run CLI against the temporary fixture** - -Run: - -```powershell -cargo run --release -- test/half-adder-verilog-smoke.v -``` - -Expected output contains: - -```text -loaded Verilog graph: -``` - -- [ ] **Step 4: Add a checked-in smoke fixture** - -Create `test/half-adder.v`: - -```verilog -module half_adder(a, b, s, c); - input a, b; - output s, c; - - assign s = a ^ b; - assign c = a & b; -endmodule -``` - -Run: - -```powershell -cargo run --release -- test/half-adder.v -``` - -Expected: same parse-and-prepare success output. - -- [ ] **Step 5: Commit** - -```powershell -git add src/main.rs test/half-adder.v -git commit -m "Load minimal Verilog from CLI" -``` - ---- - -### Task 9: Add Tiny-Gate NBT Export From Verilog - -**Files:** -- Modify: `src/main.rs` - -- [ ] **Step 1: Keep export limited to tiny graphs** - -Do not try full adder NBT export in this task. Start with `and` or `half_adder`; `full_adder` placement is already a separate local placer quality problem. - -- [ ] **Step 2: Add export path when output is provided** - -In `main.rs`, after `prepare_place()`, if `opt.output` is `Some(path)`, run local placement and save the first sampled world: - -```rust -use redstone_compiler::nbt::ToNBT; -use redstone_compiler::transform::place_and_route::local_placer::{ - InputPlacementStrategy, LocalPlacer, LocalPlacerConfig, NotRouteStrategy, - PlacementSamplingPolicy, SamplingPolicy, TorchPlacementStrategy, -}; -use redstone_compiler::world::position::DimSize; -``` - -Use a conservative config copied from the existing AND/half-adder component tests: - -```rust -let config = LocalPlacerConfig { - random_seed: 42, - greedy_input_generation: true, - input_placement_strategy: InputPlacementStrategy::Boundary, - input_candidate_limit: None, - step_sampling_policy: SamplingPolicy::Random(10000), - placement_sampling_policy: PlacementSamplingPolicy::StepPolicy, - leak_sampling: false, - route_torch_directly: true, - torch_placement_strategy: TorchPlacementStrategy::DirectOnly, - not_route_strategy: NotRouteStrategy::DirectOnly, - max_not_route_step: 3, - not_route_step_sampling_policy: SamplingPolicy::Random(100), - max_route_step: 3, - route_step_sampling_policy: SamplingPolicy::Random(100), -}; -let placer = LocalPlacer::new(prepared.clone(), config)?; -let worlds = placer.generate(DimSize(10, 10, 5), None); -let Some(world) = worlds.into_iter().next() else { - eyre::bail!("placement produced no worlds"); -}; -world.to_nbt().save(output); -``` - -- [ ] **Step 3: Run export smoke test** - -Run: - -```powershell -cargo run --release -- test/half-adder.v test/half-adder-generated-from-verilog.nbt -``` - -Expected: command exits successfully and creates `test/half-adder-generated-from-verilog.nbt`. - -- [ ] **Step 4: Verify generated NBT can be read** - -Run: - -```powershell -cargo run --release --bin check_nbt_world_cycle -- test/half-adder-generated-from-verilog.nbt -``` - -Expected: command exits successfully. If equivalence checking for this filename is not recognized by the binary, success here only proves the NBT is readable; graph equivalence should be added as a separate test. - -- [ ] **Step 5: Commit** - -```powershell -git add src/main.rs test/half-adder-generated-from-verilog.nbt -git commit -m "Export tiny Verilog circuits to NBT" -``` - ---- - -### Task 10: Add Bus Flattening As The First Real Extension - -**Files:** -- Modify: `src/verilog/ast.rs` -- Modify: `src/verilog/lexer.rs` -- Modify: `src/verilog/parser.rs` -- Modify: `src/verilog/lower.rs` - -- [ ] **Step 1: Add test for vector declarations and bit selects** - -Add parser/lowering test: - -```rust -#[test] -fn lowers_vector_bit_selects_by_flattening_names() -> eyre::Result<()> { - let module = parse_module( - r#" - module bit_xor(a, y); - input [1:0] a; - output y; - assign y = a[0] ^ a[1]; - endmodule - "#, - )?; - - let graph = lower_module(&module)?; - let table = graph.truth_table()?; - - assert_eq!(table.input_names, vec!["a_0", "a_1"]); - assert_eq!(table.output_tables["y"], vec![false, true, true, false]); - - Ok(()) -} -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: - -```powershell -cargo test --release verilog::lower::tests::lowers_vector_bit_selects_by_flattening_names -``` - -Expected: fail because vectors are rejected. - -- [ ] **Step 3: Extend AST declarations** - -Change `Declaration`: - -```rust -pub struct Declaration { - pub direction: Option, - pub range: Option, - pub names: Vec, -} - -pub struct Range { - pub msb: usize, - pub lsb: usize, -} -``` - -- [ ] **Step 4: Parse ranges and bit selects** - -Support only numeric `[msb:lsb]` declarations and single-bit `name[index]` references. Lower bit select names to `format!("{name}_{index}")`. - -- [ ] **Step 5: Run vector test** - -Run: - -```powershell -cargo test --release verilog::lower::tests::lowers_vector_bit_selects_by_flattening_names -``` - -Expected: pass. - -- [ ] **Step 6: Commit** - -```powershell -git add src/verilog/ast.rs src/verilog/lexer.rs src/verilog/parser.rs src/verilog/lower.rs -git commit -m "Flatten simple Verilog bit selects" -``` - ---- - -### Task 11: Add Named Module Instance Lowering - -**Files:** -- Modify: `src/verilog/ast.rs` -- Modify: `src/verilog/parser.rs` -- Modify: `src/verilog/lower.rs` -- Consider: `src/graph/module.rs` - -- [ ] **Step 1: Add test for two half adders composed structurally** - -Use a source with two modules: - -```verilog -module half_adder(a, b, s, c); - input a, b; - output s, c; - assign s = a ^ b; - assign c = a & b; -endmodule - -module two_half_adders(a, b, cin, sum, carry0, carry1); - input a, b, cin; - output sum, carry0, carry1; - wire s0; - half_adder ha0(.a(a), .b(b), .s(s0), .c(carry0)); - half_adder ha1(.a(s0), .b(cin), .s(sum), .c(carry1)); -endmodule -``` - -The lowering test should assert the `sum` truth table matches `a ^ b ^ cin`. - -- [ ] **Step 2: Parse multiple modules** - -Add: - -```rust -pub fn parse_modules(source: &str) -> eyre::Result>; -``` - -Keep `parse_module` as a wrapper that requires exactly one module. - -- [ ] **Step 3: Parse named instances only** - -Add AST: - -```rust -pub struct Instance { - pub module_name: String, - pub instance_name: String, - pub connections: Vec<(String, String)>, -} -``` - -Reject positional instances with `unsupported Verilog construct: positional instance ports`. - -- [ ] **Step 4: Lower instances by inlining** - -For each instance, clone the child module assignments and rename local signals with `instance_name__signal`. Apply named port substitutions before calling `LogicGraph::from_assignments`. - -- [ ] **Step 5: Run instance test** - -Run: - -```powershell -cargo test --release verilog::lower::tests::lowers_named_module_instances_by_inlining -``` - -Expected: pass. - -- [ ] **Step 6: Commit** - -```powershell -git add src/verilog/ast.rs src/verilog/parser.rs src/verilog/lower.rs -git commit -m "Inline simple named Verilog module instances" -``` - ---- - -## Verification Commands - -Run focused tests after each task. At the end of Tasks 1 through 8, run: - -```powershell -cargo test --release graph::logic::tests -cargo test --release verilog:: -``` - -Before claiming the frontend is ready for local placement, run: - -```powershell -cargo test --release -``` - -This project explicitly prefers `cargo test --release` because local placer and place-and-route tests are too slow in debug builds. - -## Implementation Notes - -- Keep Verilog lowering combinational until sequential semantics are explicit. -- Render parsed expressions as fully parenthesized strings before passing them to `LogicGraph::from_stmt`; this avoids relying on the current expression parser's precedence. -- Treat clear unsupported diagnostics as a feature. A narrow frontend that fails clearly is better than silently accepting RTL it cannot lower correctly. -- Do not use full adder NBT export as the first success criterion. Graph lowering for full adder is appropriate early; physical placement for full adder depends on ongoing local placer quality work. -- Add `sv-parser` only when the handcrafted subset becomes the bottleneck. The AST and lowering split in this plan makes that replacement local to parsing. From c5f2e71c666ab36623a477a6f0c27a0dd9e1eaae Mon Sep 17 00:00:00 2001 From: rollrat Date: Mon, 25 May 2026 12:51:51 +0900 Subject: [PATCH 18/19] Fix torch cobble power simulation --- src/world/simulator.rs | 192 ++++++++++++++--------------------------- 1 file changed, 67 insertions(+), 125 deletions(-) diff --git a/src/world/simulator.rs b/src/world/simulator.rs index 060135c..da5cdd2 100644 --- a/src/world/simulator.rs +++ b/src/world/simulator.rs @@ -490,11 +490,7 @@ impl Simulator { if !self.world.size.bound_on(support) || !self.world[support].kind.is_cobble() { return None; } - let BlockKind::Cobble { on_count, .. } = self.world[support].kind else { - return None; - }; - let support_is_powered = - on_count > 0 || self.redstone_currently_powers(support, Some(pos)); + let support_is_powered = self.cobble_power_counts(support).0 > 0; Some(Event { id: None, from_id: None, @@ -515,120 +511,6 @@ impl Simulator { self.queue.push_back(events.into()); } - fn redstone_currently_powers(&self, target: Position, ignored_torch: Option) -> bool { - self.world - .iter_block() - .into_iter() - .any(|(pos, block)| match block.kind { - BlockKind::Redstone { - state, strength, .. - } if strength > 0 => { - self.redstone_is_powered_independently_of(pos, ignored_torch) - && self - .redstone_propagate_targets(pos, state) - .into_iter() - .any(|redstone_target| redstone_target == target) - } - _ => false, - }) - } - - fn redstone_is_powered_independently_of( - &self, - pos: Position, - ignored_torch: Option, - ) -> bool { - self.redstone_is_powered_independently_of_inner(pos, ignored_torch, &mut HashSet::new()) - } - - fn redstone_is_powered_independently_of_inner( - &self, - pos: Position, - ignored_torch: Option, - visited: &mut HashSet, - ) -> bool { - if !visited.insert(pos) { - return false; - } - - let BlockKind::Redstone { - on_count, strength, .. - } = self.world[pos].kind - else { - return false; - }; - - if strength == 0 { - return false; - } - - if on_count > 0 && !self.is_torch_direct_redstone_output(ignored_torch, pos) { - return true; - } - - self.world - .iter_block() - .into_iter() - .any(|(source_pos, source_block)| match source_block.kind { - BlockKind::Redstone { - state, - strength: source_strength, - .. - } if source_strength > strength - && self - .redstone_propagate_targets(source_pos, state) - .contains(&pos) => - { - let mut branch_visited = visited.clone(); - self.redstone_is_powered_independently_of_inner( - source_pos, - ignored_torch, - &mut branch_visited, - ) - } - _ => false, - }) - } - - fn is_torch_direct_redstone_output( - &self, - ignored_torch: Option, - redstone_pos: Position, - ) -> bool { - let Some(torch_pos) = ignored_torch else { - return false; - }; - if !self.world.size.bound_on(torch_pos) { - return false; - } - let torch = self.world[torch_pos]; - if !torch.kind.is_torch() { - return false; - } - - self.torch_output_targets(torch_pos, torch.direction) - .into_iter() - .any(|target| target == redstone_pos) - } - - fn torch_output_targets(&self, pos: Position, direction: Direction) -> Vec { - let mut targets = match direction { - Direction::Bottom => pos.cardinal(), - Direction::East | Direction::West | Direction::South | Direction::North => { - let mut positions = pos.cardinal_except(direction); - positions.extend(pos.down()); - positions - } - _ => Vec::new(), - }; - targets.push(pos.up()); - targets - .into_iter() - .filter(|&target| self.world.size.bound_on(target)) - .filter(|&target| self.world[target].kind.is_redstone()) - .collect() - } - fn redstone_propagate_targets(&self, pos: Position, state: usize) -> Vec { let mut propagate_targets = Vec::new(); @@ -765,17 +647,27 @@ impl Simulator { } fn cobble_power_counts(&self, target: Position) -> (usize, usize) { + let (sources, hard_sources) = self.cobble_power_sources(target); + (sources.len(), hard_sources.len()) + } + + fn cobble_power_sources(&self, target: Position) -> (HashSet, HashSet) { let mut sources = HashSet::new(); let mut hard_sources = HashSet::new(); for (source_pos, source_block) in self.world.iter_block() { match source_block.kind { BlockKind::Torch { is_on } if is_on => { - for position in source_pos - .cardinal_except(source_block.direction) - .into_iter() - .chain(source_pos.down()) - { + let soft_targets = match source_block.direction { + Direction::Bottom => source_pos.cardinal(), + Direction::East | Direction::West | Direction::South | Direction::North => { + let mut positions = source_pos.cardinal_except(source_block.direction); + positions.extend(source_pos.down()); + positions + } + _ => Vec::new(), + }; + for position in soft_targets { if position == target { sources.insert(source_pos); } @@ -830,7 +722,7 @@ impl Simulator { } } - (sources.len(), hard_sources.len()) + (sources, hard_sources) } fn init_switch_event(&mut self, dir: Direction, pos: Position) { @@ -1934,6 +1826,15 @@ mod test { switches } + fn block_is_powered(world: &World3D, pos: Position) -> bool { + match world[pos].kind { + BlockKind::Redstone { strength, .. } => strength > 0, + BlockKind::Torch { is_on } | BlockKind::Switch { is_on } => is_on, + BlockKind::Cobble { on_count, .. } => on_count > 0, + _ => false, + } + } + fn signal_snapshot(world: &World3D) -> Vec<(Position, BlockKind)> { let mut snapshot = world .iter_block() @@ -2034,6 +1935,47 @@ mod test { Ok(()) } + #[test] + fn unittest_simulator_half_adder_generated_truth_table() -> eyre::Result<()> { + let nbt = NBTRoot::from_nbt_bytes(&std::fs::read( + "test/half-adder-generated-from-verilog.nbt", + )?)?; + let world = nbt.to_world(); + let switches = switch_positions(&world); + let sum_output = Position(4, 4, 3); + let carry_output = Position(2, 6, 1); + + assert_eq!(switches.len(), 2); + for mask in 0..4 { + let mut sim = Simulator::from_with_limits_and_trace(&world, 256, 50_000, 0) + .map_err(|error| eyre::eyre!(error.message().to_owned()))?; + sim.change_state_with_limits( + switches + .iter() + .enumerate() + .map(|(index, pos)| (*pos, (mask & (1 << index)) != 0)) + .collect(), + 256, + 50_000, + )?; + + let a = (mask & 0b01) != 0; + let b = (mask & 0b10) != 0; + assert_eq!( + block_is_powered(sim.world(), sum_output), + a ^ b, + "half-adder sum mismatch for mask {mask:02b}" + ); + assert_eq!( + block_is_powered(sim.world(), carry_output), + a & b, + "half-adder carry mismatch for mask {mask:02b}" + ); + } + + Ok(()) + } + #[test] fn unittest_simulator_repeater() { let _ = tracing_subscriber::fmt::try_init(); From ecd08325fd596f085ddfd02de5bbcaa7c44ee2be Mon Sep 17 00:00:00 2001 From: rollrat Date: Mon, 25 May 2026 12:58:16 +0900 Subject: [PATCH 19/19] Refresh NBT viewer examples --- tools/nbt-viewer/public/examples/d-latch.nbt | Bin 562 -> 551 bytes .../nbt-viewer/public/examples/full-adder.nbt | Bin 508 -> 508 bytes .../half-adder-generated-from-verilog.nbt | Bin 0 -> 403 bytes .../nbt-viewer/public/examples/half-adder.nbt | Bin 21800 -> 21376 bytes .../nbt-viewer/public/examples/manifest.json | 16 +++++++++++----- .../public/examples/xor-gate-complex.nbt | Bin 12595 -> 10570 bytes .../public/examples/xor-gate-shortest.nbt | Bin 7269 -> 6958 bytes .../public/examples/xor-gate-simple.nbt | Bin 11128 -> 11748 bytes 8 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 tools/nbt-viewer/public/examples/half-adder-generated-from-verilog.nbt diff --git a/tools/nbt-viewer/public/examples/d-latch.nbt b/tools/nbt-viewer/public/examples/d-latch.nbt index 5b613cf9298c6046a6f4e4ffc972943769e55272..fabdce9aa9bbe21d0587f774a000d43e86fbc373 100644 GIT binary patch literal 551 zcmV+?0@(c@iwFP!00002|FxD&Zrd;rhDYKmDG>AU6mUdz=VSEQ4fX zAg2!%OY(ywl86%n1Wf7UKR;(i1Q!4rxNTmM0YE<%ekebFXpnbpw|unylPdQK#Gcsj_sV0?qMhy%($O>{ zyoYLAx9A-9*Szlv_;vk+@aw7B;L_A<_usm1gSU64@mE0@-~T?mH@Mm(yT*8t#f|B< zXk3F#(-2ziO_~Keyepd)Uw50?}g~KihOBRQ-8SZSe)onVMYJ%Vu*w zEcT219D(oZwpBhieJEUkSl6vR68%eA^-FGDXO<5$A>LD{dAPz95uXR9oHC4NQfni~nbQo5#2OeLiKQrp@VUtF zFkl4D%zTzK6I)|4BOgUEoMw22j>uqSBxf<6WpNM7#5$DY|GLkLilt~qM0%m9Sj0YM z8GD&6_r(gs68^A^y~7ed%kd0KGdzP4i9N^?9;%F-Rff;iGng`infY9W{o>15ie_Tx zAs>4`Vd}8&JaHxnN~a2Y!IwV2p_$a$;0d24mCs5n9`SHsM5J~_)wRORa(+uQEOKU6 z8GWy)zDt^6?n<7T4y{H1x%f^2`_Vige0*q+xYy0WelxZ|;?1pY-6tgs?>}7MclZ}| z4fRjmpHSNtjRJ9K_U-W(@MEv-y_@9j5!kuuI^_cPXViEH{Lpj;VHEvx6?LyKas8AV z+w1-PS_sqs@gd5twSnR%O0m_r(Bh97!S1V@hmEoR%lZ!8%^8QA%``8TY#@5gO+A^g z6#YzyQdVwd-7KSx%A8iVp?R~8kmkK;F+N)G>T5C2m6sLZLoNz{|I9PcD{T${0DSQk A*#H0l diff --git a/tools/nbt-viewer/public/examples/full-adder.nbt b/tools/nbt-viewer/public/examples/full-adder.nbt index 68099a9b6b21964aa607fbf0fe21d9715b2f3db5..9d4ffaa6c757ed94c413335a94111a2ad723ef17 100644 GIT binary patch delta 423 zcmV;Y0a*V01N;M!I0oj-o$Ilbkv=Pb4SF;f!NCJK(xXW>hltRqwk8?&mGnIeUel?C zyr$Ea9QKt&!s*mfyhcXU>g1F1n$*LQoaU^nflWrf_1bED873Sj<24wV4bjgB zd5sF}i%>1WahUZA<%3+Ul-I}zj*8HP)_qVbTSoaYga^ms9?uxz86!L+=Xpd&dORcNnaOjg zmW)_a$@yWjD*LrAhS`t-KO%xbj$u5( zIfI?(Ehn0n<+H8y?5!uAF7jEid~I|yz_PHQ7I|q}f`4_ntn|?35u$4fwSKs0TW8Gb zQ3Gv+M<#ll;;*?=D*jG%MDS`aSkf2ifGO$D$qBlWnR^%N`O^FpN$M;X`l*uCj%TaF Re`xw620y^}?0BvW008W_)SUnT diff --git a/tools/nbt-viewer/public/examples/half-adder-generated-from-verilog.nbt b/tools/nbt-viewer/public/examples/half-adder-generated-from-verilog.nbt new file mode 100644 index 0000000000000000000000000000000000000000..793afd8f54fc491a8a0ffecc3c759ed4924a7e46 GIT binary patch literal 403 zcmV;E0c`#siwFP!00002|FxA(Zo)7Sg(v|C{aU`G>I9MjWD`V5WGaZJ)>NSNvMqW<0Ida;nsfZ-t>7G|dWq#=g; z8Ip*c$uOTZ%#a8jIx{a4F)z>02ty_^q#-}dpV!>oS;XC0sMiR?y3E3#`&>Rea$Ynd zLmJ_EPKW*VoX*heHNvn7hDZ3}AwS&j#fD7g>ztSz7JE^nG1xvH^99X zHs)0u+2C@eD*FfpcYb#{ukjmI0mZwLdsL=Eok6-0Wz~EDZ=^C?TV(eL>>%novkm0| z6|RAAM6InmC;D^|wa2HvZOPI|Rc^;xX!+H)pmk+D#fOk$rEs9dkB%UDv2`f4Y2PGk xw3`FE&6O^XvbQmL3Sj8+|Ds);>^!;aRXY)mS$zzMhO+F12fvV)mY?zm004i;#83bL literal 0 HcmV?d00001 diff --git a/tools/nbt-viewer/public/examples/half-adder.nbt b/tools/nbt-viewer/public/examples/half-adder.nbt index 878250db037ba0811af84f1b8cf8c619ffa31fcb..b6ba5ee3c7633c437da1656ec687f83ca91eac21 100644 GIT binary patch literal 21376 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 R(rhXN<8v$1!=V_5`7gtGxB&nF diff --git a/tools/nbt-viewer/public/examples/manifest.json b/tools/nbt-viewer/public/examples/manifest.json index f040ae9..ce27093 100644 --- a/tools/nbt-viewer/public/examples/manifest.json +++ b/tools/nbt-viewer/public/examples/manifest.json @@ -21,7 +21,7 @@ "name": "d-latch.nbt", "file": "d-latch.nbt", "path": "examples/d-latch.nbt", - "size": 562 + "size": 551 }, { "name": "full-adder.nbt", @@ -29,6 +29,12 @@ "path": "examples/full-adder.nbt", "size": 508 }, + { + "name": "half-adder-generated-from-verilog.nbt", + "file": "half-adder-generated-from-verilog.nbt", + "path": "examples/half-adder-generated-from-verilog.nbt", + "size": 403 + }, { "name": "half-adder-generated.nbt", "file": "half-adder-generated.nbt", @@ -39,7 +45,7 @@ "name": "half-adder.nbt", "file": "half-adder.nbt", "path": "examples/half-adder.nbt", - "size": 21800 + "size": 21376 }, { "name": "level.nbt", @@ -75,19 +81,19 @@ "name": "xor-gate-complex.nbt", "file": "xor-gate-complex.nbt", "path": "examples/xor-gate-complex.nbt", - "size": 12595 + "size": 10570 }, { "name": "xor-gate-shortest.nbt", "file": "xor-gate-shortest.nbt", "path": "examples/xor-gate-shortest.nbt", - "size": 7269 + "size": 6958 }, { "name": "xor-gate-simple.nbt", "file": "xor-gate-simple.nbt", "path": "examples/xor-gate-simple.nbt", - "size": 11128 + "size": 11748 }, { "name": "xor-generated.nbt", diff --git a/tools/nbt-viewer/public/examples/xor-gate-complex.nbt b/tools/nbt-viewer/public/examples/xor-gate-complex.nbt index c31212f306498e4f31dc0bd726f5d7a9b03a199f..501ab94946d6b7b09f5271aa9fb8b1a0c7e6c028 100644 GIT binary patch literal 10570 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 literal 7269 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 YJ6p$md8)Q_YO8`&x3|=P;2nhcKesEbv;Y7A