From 29b35811fa1fb3873a70d8e44352bbc1f4280e27 Mon Sep 17 00:00:00 2001 From: Michael McLoughlin Date: Thu, 28 Mar 2024 21:02:24 -0400 Subject: [PATCH 1/3] initial version --- Cargo.lock | 24 +++ cranelift/isle/isle/Cargo.toml | 2 + cranelift/isle/isle/src/lib.rs | 2 + cranelift/isle/isle/src/printer.rs | 250 +++++++++++++++++++++++++++++ cranelift/isle/islec/Cargo.toml | 2 +- 5 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 cranelift/isle/isle/src/printer.rs diff --git a/Cargo.lock b/Cargo.lock index fc4bd14c26f0..a91f15185719 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -123,6 +123,12 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "async-trait" version = "0.1.71" @@ -721,6 +727,7 @@ version = "0.107.0" dependencies = [ "codespan-reporting", "log", + "pretty", "tempfile", ] @@ -2096,6 +2103,17 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +[[package]] +name = "pretty" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55c4d17d994b637e2f4daf6e5dc5d660d209d5642377d675d7a1c3ab69fa579" +dependencies = [ + "arrayvec", + "typed-arena", + "unicode-width", +] + [[package]] name = "pretty_env_logger" version = "0.5.0" @@ -2979,6 +2997,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + [[package]] name = "typenum" version = "1.15.0" diff --git a/cranelift/isle/isle/Cargo.toml b/cranelift/isle/isle/Cargo.toml index eb92a1ac1193..2180ba09e615 100644 --- a/cranelift/isle/isle/Cargo.toml +++ b/cranelift/isle/isle/Cargo.toml @@ -14,6 +14,7 @@ workspace = true [dependencies] codespan-reporting = { version = "0.11.1", optional = true } log = { workspace = true, optional = true } +pretty = { version = "0.12", optional = true } [dev-dependencies] tempfile = "3" @@ -23,3 +24,4 @@ default = [] logging = ["log"] fancy-errors = ["codespan-reporting"] +printer = ["pretty"] diff --git a/cranelift/isle/isle/src/lib.rs b/cranelift/isle/isle/src/lib.rs index a01d5a8da0ac..9b52d2d0b7db 100644 --- a/cranelift/isle/isle/src/lib.rs +++ b/cranelift/isle/isle/src/lib.rs @@ -266,6 +266,8 @@ pub mod lexer; mod log; pub mod overlap; pub mod parser; +#[cfg(feature = "printer")] +pub mod printer; pub mod sema; pub mod serialize; pub mod trie_again; diff --git a/cranelift/isle/isle/src/printer.rs b/cranelift/isle/isle/src/printer.rs new file mode 100644 index 000000000000..0001b24e5837 --- /dev/null +++ b/cranelift/isle/isle/src/printer.rs @@ -0,0 +1,250 @@ +//! Printer for ISLE language. + +use crate::ast::*; +use crate::error::Errors; +use pretty::{Doc, Pretty, RcAllocator, RcDoc}; +use std::io::Write; + +/// Printable is a trait satisfied by AST nodes that can be printed. +pub trait Printable { + /// Map the node to a pretty document. + fn to_doc(&self) -> RcDoc<()>; +} + +/// Print the given AST node with specified line width. +pub fn print(node: &N, width: usize, out: &mut W) -> Result<(), Errors> +where + N: Printable, + W: ?Sized + Write, +{ + node.to_doc() + .render(width, out) + .map_err(|e| Errors::from_io(e, "failed to print isle")) +} + +/// Dump AST node to standard output. +pub fn dump(node: &N) -> Result<(), Errors> { + let mut stdout = std::io::stdout(); + print(node, 78, &mut stdout) +} + +impl Printable for Defs { + fn to_doc(&self) -> RcDoc<()> { + let sep = RcDoc::hardline().append(Doc::hardline()); + RcDoc::intersperse(self.defs.iter().map(|d| d.to_doc()), sep).append(Doc::hardline()) + } +} + +impl Printable for Def { + fn to_doc(&self) -> RcDoc<()> { + match self { + Def::Pragma(_) => unimplemented!("pragmas not supported"), + Def::Type(ref t) => { + let mut parts = vec![RcDoc::text("type")]; + parts.push(t.name.to_doc()); + if t.is_extern { + parts.push(RcDoc::text("extern")); + } + if t.is_nodebug { + parts.push(RcDoc::text("nodebug")); + } + parts.push(t.ty.to_doc()); + sexp(parts) + } + Def::Rule(ref r) => { + let mut parts = Vec::new(); + parts.push(RcDoc::text("rule")); + if let Some(prio) = &r.prio { + parts.push(RcDoc::as_string(prio)); + } + parts.push(r.pattern.to_doc()); + parts.extend(r.iflets.iter().map(|il| il.to_doc())); + parts.push(r.expr.to_doc()); + sexp(parts) + } + Def::Extractor(ref e) => sexp(vec![ + RcDoc::text("extractor"), + sexp( + Vec::from([e.term.to_doc()]) + .into_iter() + .chain(e.args.iter().map(|v| v.to_doc())), + ), + e.template.to_doc(), + ]), + Def::Decl(ref d) => { + let mut parts = Vec::new(); + parts.push(RcDoc::text("decl")); + if d.pure { + parts.push(RcDoc::text("pure")); + } + if d.multi { + parts.push(RcDoc::text("multi")); + } + if d.partial { + parts.push(RcDoc::text("partial")); + } + parts.push(d.term.to_doc()); + parts.push(sexp(d.arg_tys.iter().map(|ty| ty.to_doc()))); + parts.push(d.ret_ty.to_doc()); + sexp(parts) + } + Def::Extern(ref e) => e.to_doc(), + Def::Converter(ref c) => sexp(vec![ + RcDoc::text("convert"), + c.inner_ty.to_doc(), + c.outer_ty.to_doc(), + c.term.to_doc(), + ]), + } + } +} + +impl Printable for Ident { + fn to_doc(&self) -> RcDoc<()> { + RcDoc::text(self.0.clone()) + } +} + +impl Printable for TypeValue { + fn to_doc(&self) -> RcDoc<()> { + match self { + TypeValue::Primitive(ref name, _) => { + sexp(vec![RcDoc::text("primitive"), name.to_doc()]) + } + TypeValue::Enum(ref variants, _) => sexp( + // TODO(mbm): convenience for sexp with a fixed first element + Vec::from([RcDoc::text("enum")]) + .into_iter() + .chain(variants.iter().map(|v| v.to_doc())), + ), + } + } +} + +impl Printable for Variant { + fn to_doc(&self) -> RcDoc<()> { + sexp( + // TODO(mbm): convenience for sexp with a fixed first element + Vec::from([self.name.to_doc()]) + .into_iter() + .chain(self.fields.iter().map(|f| f.to_doc())), + ) + } +} + +impl Printable for Field { + fn to_doc(&self) -> RcDoc<()> { + sexp(vec![self.name.to_doc(), self.ty.to_doc()]) + } +} + +impl Printable for Pattern { + fn to_doc(&self) -> RcDoc<()> { + match self { + Pattern::Var { var, .. } => var.to_doc(), + Pattern::BindPattern { var, subpat, .. } => RcDoc::intersperse( + vec![var.to_doc(), RcDoc::text("@"), subpat.to_doc()], + Doc::space(), + ), + Pattern::ConstInt { val, .. } => RcDoc::as_string(val), + Pattern::ConstPrim { val, .. } => RcDoc::text("$").append(val.to_doc()), + Pattern::Wildcard { .. } => RcDoc::text("_"), + Pattern::Term { sym, args, .. } => sexp( + // TODO(mbm): convenience for sexp with a fixed first element + Vec::from([sym.to_doc()]) + .into_iter() + .chain(args.iter().map(|f| f.to_doc())), + ), + Pattern::And { subpats, .. } => sexp( + Vec::from([RcDoc::text("and")]) + .into_iter() + .chain(subpats.iter().map(|p| p.to_doc())), + ), + Pattern::MacroArg { .. } => unimplemented!("macro arguments are for internal use only"), + } + } +} + +impl Printable for IfLet { + fn to_doc(&self) -> RcDoc<()> { + // TODO(mbm): `if` shorthand when pattern is wildcard + sexp(vec![ + RcDoc::text("if-let"), + self.pattern.to_doc(), + self.expr.to_doc(), + ]) + } +} + +impl Printable for Expr { + fn to_doc(&self) -> RcDoc<()> { + match self { + Expr::Term { sym, args, .. } => sexp( + // TODO(mbm): convenience for sexp with a fixed first element + Vec::from([sym.to_doc()]) + .into_iter() + .chain(args.iter().map(|f| f.to_doc())), + ), + Expr::Var { name, .. } => name.to_doc(), + Expr::ConstInt { val, .. } => RcDoc::as_string(val), + Expr::ConstPrim { val, .. } => RcDoc::text("$").append(val.to_doc()), + Expr::Let { defs, body, .. } => { + let mut parts = Vec::new(); + parts.push(RcDoc::text("let")); + parts.push(sexp(defs.iter().map(|d| d.to_doc()))); + parts.push(body.to_doc()); + sexp(parts) + } + } + } +} + +impl Printable for LetDef { + fn to_doc(&self) -> RcDoc<()> { + sexp(vec![self.var.to_doc(), self.ty.to_doc(), self.val.to_doc()]) + } +} + +impl Printable for Extern { + fn to_doc(&self) -> RcDoc<()> { + match self { + Extern::Extractor { + term, + func, + pos: _, + infallible, + } => { + let mut parts = vec![RcDoc::text("extern"), RcDoc::text("extractor")]; + if *infallible { + parts.push(RcDoc::text("infallible")); + } + parts.push(term.to_doc()); + parts.push(func.to_doc()); + sexp(parts) + } + Extern::Constructor { term, func, .. } => sexp(vec![ + RcDoc::text("extern"), + RcDoc::text("constructor"), + term.to_doc(), + func.to_doc(), + ]), + Extern::Const { name, ty, .. } => sexp(vec![ + RcDoc::text("extern"), + RcDoc::text("const"), + RcDoc::text("$").append(name.to_doc()), + ty.to_doc(), + ]), + } + } +} + +fn sexp<'a, I, A>(docs: I) -> RcDoc<'a, A> +where + I: IntoIterator, + I::Item: Pretty<'a, RcAllocator, A>, + A: Clone, +{ + RcDoc::text("(") + .append(RcDoc::intersperse(docs, Doc::line()).nest(4).group()) + .append(RcDoc::text(")")) +} diff --git a/cranelift/isle/islec/Cargo.toml b/cranelift/isle/islec/Cargo.toml index 340c2aafdeef..c82915c20fe4 100644 --- a/cranelift/isle/islec/Cargo.toml +++ b/cranelift/isle/islec/Cargo.toml @@ -7,6 +7,6 @@ license = "Apache-2.0 WITH LLVM-exception" publish = false [dependencies] -cranelift-isle = { version = "*", path = "../isle/", features = ["fancy-errors", "logging"] } +cranelift-isle = { version = "*", path = "../isle/", features = ["fancy-errors", "logging", "printer"] } env_logger = { workspace = true } clap = { workspace = true, features = ['default'] } From 4d84d7a819e94d9899c523dd0d2b8ea4dd4ffdab Mon Sep 17 00:00:00 2001 From: Michael McLoughlin Date: Thu, 28 Mar 2024 21:25:39 -0400 Subject: [PATCH 2/3] parse_without_pos --- cranelift/isle/isle/src/parser.rs | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/cranelift/isle/isle/src/parser.rs b/cranelift/isle/isle/src/parser.rs index b613e65c5c75..64ee54ad0a65 100644 --- a/cranelift/isle/isle/src/parser.rs +++ b/cranelift/isle/isle/src/parser.rs @@ -12,12 +12,21 @@ pub fn parse(lexer: Lexer) -> Result { parser.parse_defs() } +/// Parse without positional information. Provided mainly to support testing, to +/// enable equality testing on structure alone. +pub fn parse_without_pos(lexer: Lexer) -> Result { + let mut parser = Parser::new(lexer); + parser.disable_pos(); + parser.parse_defs() +} + /// The ISLE parser. /// /// Takes in a lexer and creates an AST. #[derive(Clone, Debug)] struct Parser<'a> { lexer: Lexer<'a>, + disable_pos: bool, } /// Used during parsing a `(rule ...)` to encapsulate some form that @@ -31,7 +40,14 @@ enum IfLetOrExpr { impl<'a> Parser<'a> { /// Construct a new parser from the given lexer. pub fn new(lexer: Lexer<'a>) -> Parser<'a> { - Parser { lexer } + Parser { + lexer, + disable_pos: false, + } + } + + pub fn disable_pos(&mut self) { + self.disable_pos = true; } fn error(&self, pos: Pos, msg: String) -> Errors { @@ -76,9 +92,13 @@ impl<'a> Parser<'a> { } fn pos(&self) -> Pos { - self.lexer - .peek() - .map_or_else(|| self.lexer.pos(), |(pos, _)| *pos) + if !self.disable_pos { + self.lexer + .peek() + .map_or_else(|| self.lexer.pos(), |(pos, _)| *pos) + } else { + Pos::default() + } } fn is_lparen(&self) -> bool { From c17bcd72615cb26faa3d40675f3c2f5b78b5e5e3 Mon Sep 17 00:00:00 2001 From: Michael McLoughlin Date: Thu, 28 Mar 2024 21:42:26 -0400 Subject: [PATCH 3/3] generated printer tests --- cranelift/isle/isle/Cargo.toml | 4 +++ cranelift/isle/isle/build.rs | 14 ++++++++++ cranelift/isle/isle/tests/printer_tests.rs | 32 ++++++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 cranelift/isle/isle/tests/printer_tests.rs diff --git a/cranelift/isle/isle/Cargo.toml b/cranelift/isle/isle/Cargo.toml index 2180ba09e615..ce22ea23d927 100644 --- a/cranelift/isle/isle/Cargo.toml +++ b/cranelift/isle/isle/Cargo.toml @@ -8,6 +8,10 @@ readme = "../README.md" repository = "https://github.com/bytecodealliance/wasmtime/tree/main/cranelift/isle" version = "0.107.0" +[[test]] +name = "printer_tests" +required-features = ["printer"] + [lints] workspace = true diff --git a/cranelift/isle/isle/build.rs b/cranelift/isle/isle/build.rs index 96aa9ec06d44..7e88411d8fa5 100644 --- a/cranelift/isle/isle/build.rs +++ b/cranelift/isle/isle/build.rs @@ -8,6 +8,11 @@ fn main() { std::env::var_os("OUT_DIR").expect("The OUT_DIR environment variable must be set"), ); + isle_tests(&out_dir); + isle_printer_tests(&out_dir); +} + +fn isle_tests(out_dir: &std::path::PathBuf) { let mut out = String::new(); emit_tests(&mut out, "isle_examples/pass", "run_pass"); @@ -19,6 +24,15 @@ fn main() { std::fs::write(output, out).unwrap(); } +fn isle_printer_tests(out_dir: &std::path::PathBuf) { + let mut out = String::new(); + + emit_tests(&mut out, "isle_examples/pass", "run_print"); + + let output = out_dir.join("isle_printer_tests.rs"); + std::fs::write(output, out).unwrap(); +} + fn emit_tests(out: &mut String, dir_name: &str, runner_func: &str) { for test_file in std::fs::read_dir(dir_name).unwrap() { let test_file = test_file.unwrap().file_name().into_string().unwrap(); diff --git a/cranelift/isle/isle/tests/printer_tests.rs b/cranelift/isle/isle/tests/printer_tests.rs new file mode 100644 index 000000000000..e449776fd6c1 --- /dev/null +++ b/cranelift/isle/isle/tests/printer_tests.rs @@ -0,0 +1,32 @@ +//! Auto-generated ISLE printer tests. + +use cranelift_isle::lexer; +use cranelift_isle::parser; +use cranelift_isle::printer; +use std::io::BufWriter; +use std::iter::zip; + +pub fn run_print(isle_filename: &str) { + // Parse. + let lexer = lexer::Lexer::from_files(&[isle_filename]).unwrap(); + let original = parser::parse_without_pos(lexer).unwrap(); + + // Print. + let mut buf = BufWriter::new(Vec::new()); + printer::print(&original, 78, &mut buf).unwrap(); + let bytes = buf.into_inner().unwrap(); + let isle_source = String::from_utf8(bytes).unwrap(); + + // Round trip. + let lexer = lexer::Lexer::from_str(&isle_source, "").unwrap(); + let round_trip = parser::parse_without_pos(lexer).unwrap(); + + // Ensure equal. + assert_eq!(original.defs.len(), round_trip.defs.len()); + for (orig, rt) in zip(original.defs, round_trip.defs) { + assert_eq!(orig, rt); + } +} + +// Generated by build.rs. +include!(concat!(env!("OUT_DIR"), "/isle_printer_tests.rs"));