From 562a76ce2abbaf30355559a3348ebab3ad359e23 Mon Sep 17 00:00:00 2001 From: "Christopher D. Leary" Date: Mon, 25 Aug 2025 15:39:39 -0700 Subject: [PATCH 1/2] Prototype scaffolding for ir2dslx for lifting IR back to DSLX. --- xlsynth-driver/README.md | 15 ++ xlsynth-driver/src/ir2dslx.rs | 55 ++++++++ xlsynth-driver/src/main.rs | 14 ++ xlsynth-driver/tests/invoke_test.rs | 45 ++++++ xlsynth-g8r/src/xls_ir/ir2dslx.rs | 207 ++++++++++++++++++++++++++++ xlsynth-g8r/src/xls_ir/mod.rs | 1 + xlsynth/src/dslx.rs | 12 +- 7 files changed, 343 insertions(+), 6 deletions(-) create mode 100644 xlsynth-driver/src/ir2dslx.rs create mode 100644 xlsynth-g8r/src/xls_ir/ir2dslx.rs diff --git a/xlsynth-driver/README.md b/xlsynth-driver/README.md index 3bdbed08..8914e55d 100644 --- a/xlsynth-driver/README.md +++ b/xlsynth-driver/README.md @@ -239,6 +239,21 @@ optimization, and gatification using either the toolchain or the runtime APIs. Runs the XLS optimizer on an IR file and prints the optimized IR to **stdout**. Requires `--top ` to select the entry point. +### `ir2dslx`: IR to simple DSLX + +Emits a simple DSLX function corresponding to an XLS IR function by walking +nodes in topological order and producing let-bound expressions. The output is +printed to **stdout**. + +- Positional: `` – path to the IR package file +- Optional: `--top ` – entry point function (defaults to the package `top` or first function) + +Example: + +```shell +xlsynth-driver ir2dslx my_pkg.ir --top add_u32 +``` + ### `ir2pipeline`: IR to pipelined Verilog Produces a pipelined SystemVerilog design from an IR file. The generated code diff --git a/xlsynth-driver/src/ir2dslx.rs b/xlsynth-driver/src/ir2dslx.rs new file mode 100644 index 00000000..3a92e347 --- /dev/null +++ b/xlsynth-driver/src/ir2dslx.rs @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 + +use crate::toolchain_config::ToolchainConfig; +use clap::ArgMatches; + +pub fn handle_ir2dslx(matches: &ArgMatches, _config: &Option) { + let ir_path = std::path::Path::new(matches.get_one::("ir_input_file").unwrap()); + let top_opt = matches.get_one::("ir_top").map(|s| s.as_str()); + let pkg = match xlsynth_g8r::xls_ir::ir_parser::parse_path_to_package(ir_path) { + Ok(p) => p, + Err(e) => { + crate::report_cli_error::report_cli_error_and_exit( + "Failed to parse IR", + Some(&format!("{}", e)), + vec![("ir_input_file", &ir_path.display().to_string())], + ); + unreachable!(); + } + }; + let func = if let Some(top) = top_opt { + pkg.get_fn(top).unwrap_or_else(|| { + crate::report_cli_error::report_cli_error_and_exit( + "Top function not found in package", + None, + vec![ + ("top", top), + ("ir_input_file", &ir_path.display().to_string()), + ], + ); + unreachable!() + }) + } else { + pkg.get_top().unwrap_or_else(|| { + crate::report_cli_error::report_cli_error_and_exit( + "No function found in package", + None, + vec![("ir_input_file", &ir_path.display().to_string())], + ); + unreachable!() + }) + }; + + match xlsynth_g8r::xls_ir::ir2dslx::emit_fn_as_dslx(func) { + Ok(text) => { + println!("{}", text); + } + Err(e) => { + crate::report_cli_error::report_cli_error_and_exit( + "IR→DSLX emission failed", + Some(&e), + vec![("ir_input_file", &ir_path.display().to_string())], + ); + } + } +} diff --git a/xlsynth-driver/src/main.rs b/xlsynth-driver/src/main.rs index 6c302fa0..5510ba25 100644 --- a/xlsynth-driver/src/main.rs +++ b/xlsynth-driver/src/main.rs @@ -49,6 +49,7 @@ mod gv2ir; mod gv_read_stats; mod ir2combo; mod ir2delayinfo; +mod ir2dslx; mod ir2gates; mod ir2opt; mod ir2pipeline; @@ -500,6 +501,17 @@ fn main() { ), ) // ir2opt subcommand requires a top symbol + .subcommand( + clap::Command::new("ir2dslx") + .about("Emits a simple DSLX function from IR") + .arg( + clap::Arg::new("ir_input_file") + .help("The input IR file") + .required(true) + .index(1), + ) + .add_ir_top_arg(false), + ) .subcommand( clap::Command::new("ir2opt") .about("Converts IR to optimized IR") @@ -1328,6 +1340,8 @@ fn main() { ir2opt::handle_ir2opt(matches, &config); } else if let Some(matches) = matches.subcommand_matches("ir2pipeline") { ir2pipeline::handle_ir2pipeline(matches, &config); + } else if let Some(matches) = matches.subcommand_matches("ir2dslx") { + ir2dslx::handle_ir2dslx(matches, &config); } else if let Some(matches) = matches.subcommand_matches("dslx2sv-types") { dslx2sv_types::handle_dslx2sv_types(matches, &config); } else if let Some(matches) = matches.subcommand_matches("dslx-show") { diff --git a/xlsynth-driver/tests/invoke_test.rs b/xlsynth-driver/tests/invoke_test.rs index d92edbce..533c1d63 100644 --- a/xlsynth-driver/tests/invoke_test.rs +++ b/xlsynth-driver/tests/invoke_test.rs @@ -6260,6 +6260,51 @@ top fn main(x: bits[8]) -> bits[8] { ); } +// ----------------------------------------------------------------------------- +// ir2dslx +// ----------------------------------------------------------------------------- + +#[test] +fn test_ir2dslx_simple_plus_one() { + let _ = env_logger::builder().is_test(true).try_init(); + + let ir_text = r#"package p + +top fn plus_one(x: bits[32] id=1) -> bits[32] { + one: bits[32] = literal(value=1, id=2) + ret add.3: bits[32] = add(x, one, id=3) +} +"#; + + let tmp = tempfile::tempdir().unwrap(); + let ir_path = tmp.path().join("p.ir"); + std::fs::write(&ir_path, ir_text).unwrap(); + + let driver = env!("CARGO_BIN_EXE_xlsynth-driver"); + let output = std::process::Command::new(driver) + .arg("ir2dslx") + .arg(ir_path.to_str().unwrap()) + .arg("--top") + .arg("plus_one") + .output() + .unwrap(); + assert!( + output.status.success(), + "stdout: {}\nstderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let got = stdout.trim(); + let expected = r#"fn plus_one(x: u32) -> u32 { + let one: u32 = u32:1; + let add_3: u32 = x + one; + add_3 +}"# + .trim(); + assert_eq!(got, expected, "stdout: {}", stdout); +} + // ----------------------------------------------------------------------------- // Uninterpreted function (UF) tests for prove-quickcheck // ----------------------------------------------------------------------------- diff --git a/xlsynth-g8r/src/xls_ir/ir2dslx.rs b/xlsynth-g8r/src/xls_ir/ir2dslx.rs new file mode 100644 index 00000000..207c73e5 --- /dev/null +++ b/xlsynth-g8r/src/xls_ir/ir2dslx.rs @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Minimal IR→DSLX emitter. +//! +//! This walks IR nodes in their existing order (expected to be topological) +//! and emits a simple, deterministic DSLX function using let-bound +//! expressions, finishing with the returned node. +//! +//! Supported ops (initial): +//! - Params, Literals +//! - Binops: add, sub, umul/smul ("*") +//! - Shifts: shll ("<<"), shrl (">>") +//! - Nary: and/or/xor (folded with &, |, ^) +//! - Tuple/Array construction (basic) +//! +//! For unsupported nodes, we return an error describing the operator. + +use crate::xls_ir::ir; + +fn ty_to_dslx(t: &ir::Type) -> String { + match t { + ir::Type::Token => "token".to_string(), + ir::Type::Bits(n) => format!("u{}", n), + ir::Type::Tuple(members) => { + let parts: Vec = members.iter().map(|m| ty_to_dslx(m)).collect(); + format!("({})", parts.join(", ")) + } + ir::Type::Array(arr) => { + let elem = ty_to_dslx(&arr.element_type); + format!("{}[{}]", elem, arr.element_count) + } + } +} + +fn literal_to_dslx(value: &xlsynth::IrValue) -> String { + // Prefer uN:VALUE form in DSLX. + let bits = value.bit_count().unwrap_or(0); + // Use IR value printer without the leading type prefix and attach DSLX uN: + let no_prefix = value + .to_string_fmt_no_prefix(xlsynth::ir_value::IrFormatPreference::Default) + .unwrap(); + format!("u{}:{}", bits, no_prefix) +} + +fn node_display_name(f: &ir::Fn, nr: ir::NodeRef) -> String { + let n = f.get_node(nr); + if let Some(name) = &n.name { + return name.clone(); + } + format!("{}_{}", n.payload.get_operator(), n.text_id) +} + +fn emit_expr_for_node(f: &ir::Fn, nr: ir::NodeRef) -> Result { + use ir::NodePayload as P; + let n = f.get_node(nr); + let name = node_display_name(f, nr); + let ty = ty_to_dslx(&n.ty); + let expr = match &n.payload { + P::Nil => return Ok(String::new()), + P::GetParam(_) => return Ok(String::new()), + P::Literal(v) => format!("let {}: {} = {};", name, ty, literal_to_dslx(v)), + + P::Binop(op, a, b) => { + let lhs = node_display_name(f, *a); + let rhs = node_display_name(f, *b); + let op_str = match op { + ir::Binop::Add => "+", + ir::Binop::Sub => "-", + ir::Binop::Umul | ir::Binop::Smul => "*", + ir::Binop::Shll => "<<", + ir::Binop::Shrl => ">>", + _ => return Err(format!("unsupported binop: {}", ir::binop_to_operator(*op))), + }; + format!("let {}: {} = {} {} {};", name, ty, lhs, op_str, rhs) + } + + P::Unop(op, a) => { + let arg = node_display_name(f, *a); + let s = match op { + ir::Unop::Not => format!("!{}", arg), + ir::Unop::Neg => format!("-{}", arg), + ir::Unop::Identity => format!("{}", arg), + _ => return Err(format!("unsupported unop: {}", ir::unop_to_operator(*op))), + }; + format!("let {}: {} = {};", name, ty, s) + } + + P::Nary(nop, nodes) => { + let op_str = match nop { + ir::NaryOp::And => "&", + ir::NaryOp::Or => "|", + ir::NaryOp::Xor => "^", + _ => { + return Err(format!( + "unsupported n-ary op: {}", + ir::nary_op_to_operator(*nop) + )); + } + }; + let parts: Vec = nodes.iter().map(|r| node_display_name(f, *r)).collect(); + let folded = if parts.is_empty() { + return Err("empty n-ary node".to_string()); + } else { + parts.join(&format!(" {} ", op_str)) + }; + format!("let {}: {} = {};", name, ty, folded) + } + + P::Tuple(elems) => { + let parts: Vec = elems.iter().map(|r| node_display_name(f, *r)).collect(); + format!("let {}: {} = ({});", name, ty, parts.join(", ")) + } + + P::Array(elems) => { + let parts: Vec = elems.iter().map(|r| node_display_name(f, *r)).collect(); + format!("let {}: {} = [{}];", name, ty, parts.join(", ")) + } + + // Many nodes are currently unsupported in this minimal emitter. + other => return Err(format!("unsupported op: {}", other.get_operator())), + }; + Ok(expr) +} + +/// Emits a DSLX function text for the given IR function. +pub fn emit_fn_as_dslx(func: &ir::Fn) -> Result { + let params_str = func + .params + .iter() + .map(|p| format!("{}: {}", p.name, ty_to_dslx(&p.ty))) + .collect::>() + .join(", "); + let ret_ty = ty_to_dslx(&func.ret_ty); + let mut lines: Vec = Vec::new(); + lines.push(format!("fn {}({}) -> {} {{", func.name, params_str, ret_ty)); + + for (i, _node) in func.nodes.iter().enumerate() { + let nr = ir::NodeRef { index: i }; + if let Ok(s) = emit_expr_for_node(func, nr) { + if !s.is_empty() { + lines.push(format!(" {}", s)); + } + } else { + let n = func.get_node(nr); + return Err(format!( + "unsupported node id {} op {}", + n.text_id, + n.payload.get_operator() + )); + } + } + + let ret_ref = func + .ret_node_ref + .ok_or_else(|| "function missing ret node ref".to_string())?; + let ret_name = node_display_name(func, ret_ref); + lines.push(format!(" {}", ret_name)); + lines.push("}".to_string()); + Ok(lines.join("\n")) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::xls_ir::ir_parser::Parser; + use test_case::test_case; + + #[test] + fn test_add_literal() { + let ir_text = r#"fn plus_one(x: bits[32] id=1) -> bits[32] { + one: bits[32] = literal(value=1, id=2) + ret add.3: bits[32] = add(x, one, id=3) +}"#; + let mut p = Parser::new(ir_text); + let f = p.parse_fn().unwrap(); + let dslx = emit_fn_as_dslx(&f).unwrap(); + let want = r#"fn plus_one(x: u32) -> u32 { + let one: u32 = u32:1; + let add_3: u32 = x + one; + add_3 +}"#; + assert_eq!(dslx, want); + } + + fn make_ir_with_binop(op: &str) -> String { + format!( + "fn f(x: bits[32] id=1) -> bits[32] {{\n one: bits[32] = literal(value=1, id=2)\n ret {}.3: bits[32] = {}(x, one, id=3)\n}}", + op, op + ) + } + + #[test_case("sub", "x - one", "sub_3"; "sub")] + #[test_case("umul", "x * one", "umul_3"; "umul")] + #[test_case("smul", "x * one", "smul_3"; "smul")] + #[test_case("shll", "x << one", "shll_3"; "shll")] + #[test_case("shrl", "x >> one", "shrl_3"; "shrl")] + fn test_binop_emission(op: &str, expr: &str, name: &str) { + let ir_text = make_ir_with_binop(op); + let mut p = Parser::new(&ir_text); + let f = p.parse_fn().unwrap(); + let dslx = emit_fn_as_dslx(&f).unwrap(); + let want = format!( + "fn f(x: u32) -> u32 {{\n let one: u32 = u32:1;\n let {name}: u32 = {expr};\n {name}\n}}" + ); + assert_eq!(dslx, want); + } +} diff --git a/xlsynth-g8r/src/xls_ir/mod.rs b/xlsynth-g8r/src/xls_ir/mod.rs index 082cf8f7..2c9e498d 100644 --- a/xlsynth-g8r/src/xls_ir/mod.rs +++ b/xlsynth-g8r/src/xls_ir/mod.rs @@ -5,6 +5,7 @@ pub mod edit_distance; pub mod ir; +pub mod ir2dslx; pub mod ir_node_env; pub mod ir_parser; pub mod ir_utils; diff --git a/xlsynth/src/dslx.rs b/xlsynth/src/dslx.rs index 0f58ade2..140a76ed 100644 --- a/xlsynth/src/dslx.rs +++ b/xlsynth/src/dslx.rs @@ -211,12 +211,12 @@ pub enum MatchableModuleMember { impl MatchableModuleMember { pub fn to_text(&self) -> String { match self { - MatchableModuleMember::EnumDef(e) => format!("{}", e), - MatchableModuleMember::StructDef(s) => format!("{}", s), - MatchableModuleMember::TypeAlias(t) => format!("{}", t), - MatchableModuleMember::ConstantDef(c) => format!("{}", c), - MatchableModuleMember::Function(f) => format!("{}", f), - MatchableModuleMember::Quickcheck(qc) => format!("{}", qc), + MatchableModuleMember::EnumDef(e) => format!("{e}"), + MatchableModuleMember::StructDef(s) => format!("{s}"), + MatchableModuleMember::TypeAlias(t) => format!("{t}"), + MatchableModuleMember::ConstantDef(c) => format!("{c}"), + MatchableModuleMember::Function(f) => format!("{f}"), + MatchableModuleMember::Quickcheck(qc) => format!("{qc}"), } } } From 294a02b1eb96aa58d165e49e4b11294a96e708b8 Mon Sep 17 00:00:00 2001 From: "Christopher D. Leary" Date: Mon, 25 Aug 2025 15:46:02 -0700 Subject: [PATCH 2/2] More op testing. --- xlsynth-g8r/src/xls_ir/ir2dslx.rs | 87 +++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/xlsynth-g8r/src/xls_ir/ir2dslx.rs b/xlsynth-g8r/src/xls_ir/ir2dslx.rs index 207c73e5..40ec24dd 100644 --- a/xlsynth-g8r/src/xls_ir/ir2dslx.rs +++ b/xlsynth-g8r/src/xls_ir/ir2dslx.rs @@ -164,6 +164,13 @@ mod tests { use super::*; use crate::xls_ir::ir_parser::Parser; use test_case::test_case; + use xlsynth::dslx; + + fn assert_parses_and_typechecks(dslx_text: &str) { + let mut import_data = dslx::ImportData::default(); + dslx::parse_and_typecheck(dslx_text, "/fake/path.x", "m", &mut import_data) + .expect("parse-and-typecheck success"); + } #[test] fn test_add_literal() { @@ -180,6 +187,7 @@ mod tests { add_3 }"#; assert_eq!(dslx, want); + assert_parses_and_typechecks(&dslx); } fn make_ir_with_binop(op: &str) -> String { @@ -203,5 +211,84 @@ mod tests { "fn f(x: u32) -> u32 {{\n let one: u32 = u32:1;\n let {name}: u32 = {expr};\n {name}\n}}" ); assert_eq!(dslx, want); + assert_parses_and_typechecks(&dslx); + } + + fn make_ir_with_unop(op: &str) -> String { + format!( + "fn f(x: bits[32] id=1) -> bits[32] {{\n ret {}.2: bits[32] = {}(x, id=2)\n}}", + op, op + ) + } + + #[test_case("not", "!x", "not_2"; "not")] + #[test_case("neg", "-x", "neg_2"; "neg")] + #[test_case("identity", "x", "identity_2"; "identity")] + fn test_unop_emission(op: &str, expr: &str, name: &str) { + let ir_text = make_ir_with_unop(op); + let mut p = Parser::new(&ir_text); + let f = p.parse_fn().unwrap(); + let dslx = emit_fn_as_dslx(&f).unwrap(); + let want = format!("fn f(x: u32) -> u32 {{\n let {name}: u32 = {expr};\n {name}\n}}"); + assert_eq!(dslx, want); + assert_parses_and_typechecks(&dslx); + } + + fn make_ir_with_nary(op: &str) -> String { + format!( + "fn f(x: bits[32] id=1) -> bits[32] {{\n one: bits[32] = literal(value=1, id=2)\n two: bits[32] = literal(value=2, id=3)\n ret {}.4: bits[32] = {}(x, one, two, id=4)\n}}", + op, op + ) + } + + #[test_case("and", "x & one & two", "and_4"; "and")] + #[test_case("or", "x | one | two", "or_4"; "or")] + #[test_case("xor", "x ^ one ^ two", "xor_4"; "xor")] + fn test_nary_emission(op: &str, expr: &str, name: &str) { + let ir_text = make_ir_with_nary(op); + let mut p = Parser::new(&ir_text); + let f = p.parse_fn().unwrap(); + let dslx = emit_fn_as_dslx(&f).unwrap(); + let want = format!( + "fn f(x: u32) -> u32 {{\n let one: u32 = u32:1;\n let two: u32 = u32:2;\n let {name}: u32 = {expr};\n {name}\n}}" + ); + assert_eq!(dslx, want); + assert_parses_and_typechecks(&dslx); + } + + #[test] + fn test_tuple_construction() { + let ir_text = r#"fn f(x: bits[32] id=1) -> (bits[32], bits[32]) { + one: bits[32] = literal(value=1, id=2) + ret tuple.4: (bits[32], bits[32]) = tuple(x, one, id=4) +}"#; + let mut p = Parser::new(ir_text); + let f = p.parse_fn().unwrap(); + let dslx = emit_fn_as_dslx(&f).unwrap(); + let want = r#"fn f(x: u32) -> (u32, u32) { + let one: u32 = u32:1; + let tuple_4: (u32, u32) = (x, one); + tuple_4 +}"#; + assert_eq!(dslx, want); + assert_parses_and_typechecks(&dslx); + } + + #[test] + fn test_array_construction() { + let ir_text = r#"fn f(x: bits[32] id=1) -> bits[32][2] { + one: bits[32] = literal(value=1, id=2) + ret array.4: bits[32][2] = array(x, one, id=4) +}"#; + let mut p = Parser::new(ir_text); + let f = p.parse_fn().unwrap(); + let dslx = emit_fn_as_dslx(&f).unwrap(); + let want = r#"fn f(x: u32) -> u32[2] { + let one: u32 = u32:1; + let array_4: u32[2] = [x, one]; + array_4 +}"#; + assert_eq!(dslx, want); + assert_parses_and_typechecks(&dslx); } }