Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
140 changes: 138 additions & 2 deletions src/graph/logic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,27 @@ impl LogicGraph {
LogicGraphBuilder::new(stmt.to_string()).build(output.to_string())
}

pub fn from_assignments<I>(assignments: I) -> eyre::Result<LogicGraph>
where
I: IntoIterator<Item = (String, String)>,
{
let mut graphs = assignments
.into_iter()
.map(|(output, expr)| LogicGraph::from_stmt(&expr, &output))
.collect::<eyre::Result<Vec<_>>>()?
.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<Self> {
let mut transform = LogicGraphTransformer::new(self);
transform.decompose_xor()?;
Expand Down Expand Up @@ -49,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::<Vec<_>>();
outputs.sort_by(|(a, _), (b, _)| a.cmp(b));
outputs
}

pub fn terminal_sources(&self) -> Vec<GraphNodeId> {
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<I>(mut self, outputs: I) -> eyre::Result<Self>
where
I: IntoIterator<Item = (String, GraphNodeId)>,
{
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<Self> {
let outputs = self
.terminal_sources()
.into_iter()
.enumerate()
.map(|(index, source_id)| (format!("#{index}"), source_id))
.collect::<Vec<_>>();
self.attach_outputs(outputs)
}

pub fn externally_observable_truth_table(&self) -> eyre::Result<LogicTruthTable> {
let table = self.truth_table()?;
let output_source_ids = self.externally_observable_output_source_ids();
Expand Down Expand Up @@ -246,6 +328,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 {
Expand Down Expand Up @@ -331,11 +421,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();
Expand Down Expand Up @@ -608,6 +698,52 @@ 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 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 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(())
}

#[test]
fn unittest_logicgraph_full_adder() -> eyre::Result<()> {
// s = (a ^ b) ^ cin;
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
59 changes: 56 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +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]
Expand All @@ -18,10 +26,55 @@ 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") {
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={}",
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,
materialize_outputs: 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_with_outputs(DimSize(10, 10, 5), None);
let Some(placed) = worlds.into_iter().next() else {
eyre::bail!("placement produced no worlds");
};
placed.world.to_nbt().save(&output);
let metadata_path = output.with_extension("outputs.json");
placed.metadata().save(&metadata_path)?;

println!(
"exported Verilog graph: path={} outputs={}",
output.display(),
metadata_path.display()
);

Ok(())
}
65 changes: 65 additions & 0 deletions src/output.rs
Original file line number Diff line number Diff line change
@@ -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<OutputEndpoint>,
}

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<OutputEndpoint>,
}

impl OutputMetadata {
pub fn new(outputs: Vec<OutputEndpoint>) -> Self {
Self {
format: FORMAT.to_owned(),
outputs,
}
}

pub fn load(path: impl AsRef<Path>) -> eyre::Result<Self> {
let metadata = serde_json::from_str(&fs::read_to_string(path)?)?;
Ok(metadata)
}

pub fn save(&self, path: impl AsRef<Path>) -> 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])
}
}
Loading
Loading