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/src/graph/logic.rs b/src/graph/logic.rs index 648eb9b..2ea0f55 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()?; @@ -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::>(); + 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(); @@ -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 { @@ -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(); @@ -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; 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 d3072ae..889db79 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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] @@ -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(()) } 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/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..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, @@ -286,6 +315,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() @@ -436,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/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..f9a69a0 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,83 @@ 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_eq!(result.queue[0].1.endpoint_positions().len(), 1); + 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(()) +} + +#[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(()) +} + +#[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(()) } @@ -689,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/ast.rs b/src/verilog/ast.rs new file mode 100644 index 0000000..c597a21 --- /dev/null +++ b/src/verilog/ast.rs @@ -0,0 +1,96 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct VerilogModule { + pub name: String, + pub ports: Vec, + pub declarations: Vec, + pub assignments: Vec, + pub instances: Vec, +} + +#[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, + Output, + Wire, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Assignment { + pub output: String, + 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), + 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/lexer.rs b/src/verilog/lexer.rs new file mode 100644 index 0000000..b3030d8 --- /dev/null +++ b/src/verilog/lexer.rs @@ -0,0 +1,181 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Token { + Module, + EndModule, + Input, + Output, + Wire, + Assign, + Always, + Ident(String), + Number(usize), + LParen, + RParen, + LBracket, + RBracket, + Dot, + Comma, + Semi, + Colon, + Eq, + At, + 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::LBracket), + ']' => tokens.push(Token::RBracket), + '.' => tokens.push(Token::Dot), + ',' => 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; + 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, + "always" => Token::Always, + _ => 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/lower.rs b/src/verilog/lower.rs new file mode 100644 index 0000000..25aec9e --- /dev/null +++ b/src/verilog/lower.rs @@ -0,0 +1,226 @@ +use std::collections::HashMap; + +use crate::graph::logic::LogicGraph; +use crate::verilog::ast::{Expr, VerilogModule}; + +pub fn lower_module(module: &VerilogModule) -> eyre::Result { + 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(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(|(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, lower_modules}; + use crate::verilog::parser::{parse_module, parse_modules}; + + #[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(()) + } + + #[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(()) + } + + #[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 6d4ae4a..2ea4c89 100644 --- a/src/verilog/mod.rs +++ b/src/verilog/mod.rs @@ -1,15 +1,182 @@ -// use std::{collections::HashMap, path::PathBuf}; +pub mod ast; +pub mod lexer; +pub mod lower; +pub mod parser; -// use sv_parser::{parse_sv, SyntaxTree}; +use std::fs; +use std::path::Path; -// pub fn load(path: &PathBuf) -> eyre::Result { -// let defines = HashMap::new(); -// let includes: Vec = Vec::new(); +use crate::graph::logic::LogicGraph; -// 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()); -// }; +pub fn load_logic_graph(path: impl AsRef) -> eyre::Result { + let source = fs::read_to_string(path)?; + let modules = parser::parse_modules(&source)?; + lower::lower_modules(&modules) +} -// Ok(syntax_tree) -// } +#[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::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<()> { + 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(()) + } + + #[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(()) + } + + #[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()); + } + } + + 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 + } +} diff --git a/src/verilog/parser.rs b/src/verilog/parser.rs new file mode 100644 index 0000000..c3ce531 --- /dev/null +++ b/src/verilog/parser.rs @@ -0,0 +1,383 @@ +use crate::verilog::ast::{ + Assignment, BinaryOp, Declaration, Expr, Instance, PortDirection, Range, VerilogModule, +}; +use crate::verilog::lexer::{lex, Token}; + +pub fn parse_module(source: &str) -> eyre::Result { + 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 { + tokens: Vec, + index: usize, +} + +impl Parser { + fn new(tokens: Vec) -> Self { + Self { tokens, index: 0 } + } + + 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)?; + let ports = self.parse_ident_list()?; + self.expect(Token::RParen)?; + self.expect(Token::Semi)?; + + 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"), + } + } + + Ok(VerilogModule { + name, + ports, + declarations, + assignments, + instances, + }) + } + + 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 range = self.parse_optional_range()?; + let names = self.parse_ident_list()?; + self.expect(Token::Semi)?; + + Ok(Declaration { + direction: Some(direction), + range, + 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_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() + } + + 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)) => { + let name = self.finish_signal_name(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 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); + } + + 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), + Some(token) => eyre::bail!("expected identifier, got {token:?}"), + None => eyre::bail!("expected identifier"), + } + } + + 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()) { + 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(()) + } + + #[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 parses_vector_declarations() -> eyre::Result<()> { + let module = parse_module( + r#" + module vectors(a, y); + input [3:0] a; + output y; + assign y = a[0]; + endmodule + "#, + )?; + + 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(()) + } +} 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(); diff --git a/test/half-adder-generated-from-verilog.nbt b/test/half-adder-generated-from-verilog.nbt new file mode 100644 index 0000000..793afd8 Binary files /dev/null and b/test/half-adder-generated-from-verilog.nbt differ 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 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 diff --git a/tools/nbt-viewer/public/examples/d-latch.nbt b/tools/nbt-viewer/public/examples/d-latch.nbt index 5b613cf..fabdce9 100644 Binary files a/tools/nbt-viewer/public/examples/d-latch.nbt and b/tools/nbt-viewer/public/examples/d-latch.nbt differ diff --git a/tools/nbt-viewer/public/examples/full-adder.nbt b/tools/nbt-viewer/public/examples/full-adder.nbt index 68099a9..9d4ffaa 100644 Binary files a/tools/nbt-viewer/public/examples/full-adder.nbt and b/tools/nbt-viewer/public/examples/full-adder.nbt differ 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 0000000..793afd8 Binary files /dev/null and b/tools/nbt-viewer/public/examples/half-adder-generated-from-verilog.nbt differ diff --git a/tools/nbt-viewer/public/examples/half-adder.nbt b/tools/nbt-viewer/public/examples/half-adder.nbt index 878250d..b6ba5ee 100644 Binary files a/tools/nbt-viewer/public/examples/half-adder.nbt and b/tools/nbt-viewer/public/examples/half-adder.nbt differ 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 c31212f..501ab94 100644 Binary files a/tools/nbt-viewer/public/examples/xor-gate-complex.nbt and b/tools/nbt-viewer/public/examples/xor-gate-complex.nbt differ diff --git a/tools/nbt-viewer/public/examples/xor-gate-shortest.nbt b/tools/nbt-viewer/public/examples/xor-gate-shortest.nbt index 58bfa5d..be7158f 100644 Binary files a/tools/nbt-viewer/public/examples/xor-gate-shortest.nbt and b/tools/nbt-viewer/public/examples/xor-gate-shortest.nbt differ diff --git a/tools/nbt-viewer/public/examples/xor-gate-simple.nbt b/tools/nbt-viewer/public/examples/xor-gate-simple.nbt index 66375dc..e8ecc8c 100644 Binary files a/tools/nbt-viewer/public/examples/xor-gate-simple.nbt and b/tools/nbt-viewer/public/examples/xor-gate-simple.nbt differ