From 40febf34ce6c6b09a249553574da9835cb1ca976 Mon Sep 17 00:00:00 2001 From: T6 Date: Wed, 10 Apr 2024 09:27:55 -0400 Subject: [PATCH] add `Book::transform` (#107) --- Cargo.toml | 6 +- src/main.rs | 205 +++++--------------------------- src/transform.rs | 165 +++++++++++++++++++++++++ src/util.rs | 2 + src/util/parse_abbrev_number.rs | 17 +++ 5 files changed, 215 insertions(+), 180 deletions(-) create mode 100644 src/util/parse_abbrev_number.rs diff --git a/Cargo.toml b/Cargo.toml index 907a31c5..4fc7c8fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ license = "MIT" [[bin]] name = "hvmc" path = "src/main.rs" +required-features = ["cli"] bench = false [lib] @@ -25,7 +26,7 @@ debug = "full" [dependencies] TSPL = "0.0.9" arrayvec = "0.7.4" -clap = { version = "4.5.1", features = ["derive"] } +clap = { version = "4.5.1", features = ["derive"], optional = true } nohash-hasher = { version = "0.2.0" } stacker = "0.1.15" thiserror = "1.0.58" @@ -33,7 +34,8 @@ thiserror = "1.0.58" ##--COMPILER-CUTOFF--## [features] -default = ["_full_cli"] +default = ["cli", "_full_cli"] +cli = ["dep:clap"] trace = [] _full_cli = [] _fuzz = [] diff --git a/src/main.rs b/src/main.rs index 5746f89b..90939e7d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use hvmc::{ host::Host, run::{DynNet, Mode, Trg}, stdlib::create_host, + transform::{TransformOpts, TransformPass, TransformPasses}, *, }; @@ -25,28 +26,28 @@ fn main() { if cfg!(feature = "_full_cli") { let cli = FullCli::parse(); match cli.mode { - CliMode::Compile { file, transform_opts, output } => { + CliMode::Compile { file, transform_args, output } => { let output = output.as_deref().or_else(|| file.strip_suffix(".hvmc")).unwrap_or_else(|| { eprintln!("file missing `.hvmc` extension; explicitly specify an output path with `--output`."); process::exit(1); }); - let host = create_host(&load_book(&[file.clone()], &transform_opts)); + let host = create_host(&load_book(&[file.clone()], &transform_args)); compile_executable(output, host).unwrap(); } - CliMode::Run { run_opts, mut transform_opts, file, args } => { + CliMode::Run { run_opts, mut transform_args, file, args } => { // Don't pre-reduce or prune the entry point - transform_opts.pre_reduce_skip.push(args.entry_point.clone()); - transform_opts.prune_entrypoints.push(args.entry_point.clone()); - let host = create_host(&load_book(&[file], &transform_opts)); + transform_args.transform_opts.pre_reduce_skip.push(args.entry_point.clone()); + transform_args.transform_opts.prune_entrypoints.push(args.entry_point.clone()); + let host = create_host(&load_book(&[file], &transform_args)); run(host, run_opts, args); } - CliMode::Reduce { run_opts, transform_opts, files, exprs } => { - let host = create_host(&load_book(&files, &transform_opts)); + CliMode::Reduce { run_opts, transform_args, files, exprs } => { + let host = create_host(&load_book(&files, &transform_args)); let exprs: Vec<_> = exprs.iter().map(|x| Net::from_str(x).unwrap()).collect(); reduce_exprs(host, &exprs, &run_opts); } - CliMode::Transform { transform_opts, files } => { - let book = load_book(&files, &transform_opts); + CliMode::Transform { transform_args, files } => { + let book = load_book(&files, &transform_args); println!("{}", book); } } @@ -100,7 +101,7 @@ enum CliMode { /// Output path; defaults to the input file with `.hvmc` stripped. output: Option, #[command(flatten)] - transform_opts: TransformOpts, + transform_args: TransformArgs, }, /// Run a program, optionally passing a list of arguments to it. Run { @@ -111,7 +112,7 @@ enum CliMode { #[command(flatten)] run_opts: RuntimeOpts, #[command(flatten)] - transform_opts: TransformOpts, + transform_args: TransformArgs, }, /// Reduce hvm-core expressions to their normal form. /// @@ -135,7 +136,7 @@ enum CliMode { #[command(flatten)] run_opts: RuntimeOpts, #[command(flatten)] - transform_opts: TransformOpts, + transform_args: TransformArgs, }, /// Transform a hvm-core program using one of the optimization passes. Transform { @@ -145,36 +146,18 @@ enum CliMode { #[arg(required = true)] files: Vec, #[command(flatten)] - transform_opts: TransformOpts, + transform_args: TransformArgs, }, } #[derive(Args, Clone, Debug)] -struct TransformOpts { - #[arg(short = 'O', value_delimiter = ' ', action = clap::ArgAction::Append)] +struct TransformArgs { /// Enables or disables transformation passes. + #[arg(short = 'O', value_delimiter = ' ', action = clap::ArgAction::Append)] transform_passes: Vec, - #[arg(long = "pre-reduce-skip", value_delimiter = ' ', action = clap::ArgAction::Append)] - /// Names of the definitions that should not get pre-reduced. - /// - /// For programs that don't take arguments - /// and don't have side effects this is usually the entry point of the - /// program (otherwise, the whole program will get reduced to normal form). - pre_reduce_skip: Vec, - #[arg(long = "pre-reduce-memory", value_parser = parse_abbrev_number::)] - /// How much memory to allocate when pre-reducing. - /// - /// Supports abbreviations such as '4G' or '400M'. - pre_reduce_memory: Option, - #[arg(long = "pre-reduce-rewrites", default_value = "100M", value_parser = parse_abbrev_number::)] - /// Maximum amount of rewrites to do when pre-reducing. - /// - /// Supports abbreviations such as '4G' or '400M'. - pre_reduce_rewrites: u64, - /// Names of the definitions that should not get pruned. - #[arg(long = "prune-entrypoints", default_value = "main")] - prune_entrypoints: Vec, + #[command(flatten)] + transform_opts: TransformOpts, } #[derive(Args, Clone, Debug)] @@ -192,7 +175,7 @@ struct RuntimeOpts { /// by a walk from the root of the net. This leads to a dramatic slowdown, /// but allows running programs that would expand indefinitely otherwise. lazy_mode: bool, - #[arg(short = 'm', long = "memory", value_parser = parse_abbrev_number::)] + #[arg(short = 'm', long = "memory", value_parser = util::parse_abbrev_number::)] /// How much memory to allocate on startup. /// /// Supports abbreviations such as '4G' or '400M'. @@ -213,73 +196,6 @@ struct RunArgs { args: Vec, } -macro_rules! transform_passes { - ($($pass:ident: $name:literal $(| $alias:literal)*),* $(,)?) => { - #[derive(Clone, Debug)] - #[allow(non_camel_case_types)] - pub enum TransformPass { - all(bool), - $($pass(bool),)* - } - - #[derive(Default)] - struct TransformPasses { - $($pass: bool),* - } - - impl TransformPasses { - fn set_all(&mut self) { - $(self.$pass = true;)* - } - } - - impl clap::ValueEnum for TransformPass { - fn value_variants<'a>() -> &'a [Self] { - &[ - Self::all(true), Self::all(false), - $(Self::$pass(true), Self::$pass(false),)* - ] - } - - fn to_possible_value(&self) -> Option { - use TransformPass::*; - Some(match self { - all(true) => clap::builder::PossibleValue::new("all"), - all(false) => clap::builder::PossibleValue::new("no-all"), - $( - $pass(true) => clap::builder::PossibleValue::new($name)$(.alias($alias))*, - $pass(false) => clap::builder::PossibleValue::new(concat!("no-", $name))$(.alias(concat!("no-", $alias)))*, - )* - }) - } - } - - impl TransformPass { - fn passes_from_cli(args: &Vec) -> TransformPasses { - use TransformPass::*; - let mut opts = TransformPasses::default(); - for arg in args { - match arg { - all(true) => opts.set_all(), - all(false) => opts = TransformPasses::default(), - $(&$pass(b) => opts.$pass = b,)* - } - } - opts - } - } - }; -} - -transform_passes! { - pre_reduce: "pre-reduce" | "pre", - coalesce_ctrs: "coalesce-ctrs" | "coalesce", - encode_adts: "encode-adts" | "adts", - eta_reduce: "eta-reduce" | "eta", - inline: "inline", - prune: "prune", -} - fn run(host: Arc>, opts: RuntimeOpts, args: RunArgs) { let mut net = Net { root: Tree::Ref { nam: args.entry_point }, redexes: vec![] }; for arg in args.args { @@ -290,29 +206,8 @@ fn run(host: Arc>, opts: RuntimeOpts, args: RunArgs) { reduce_exprs(host, &[net], &opts); } -/// Turn a string representation of a number, such as '1G' or '400K', into a -/// number. -/// -/// This return a [`u64`] instead of [`usize`] to ensure that parsing CLI args -/// doesn't fail on 32-bit systems. We want it to fail later on, when attempting -/// to run the program. -fn parse_abbrev_number>(arg: &str) -> Result -where - >::Error: core::fmt::Debug, -{ - let (base, scale) = match arg.to_lowercase().chars().last() { - None => return Err("Mem size argument is empty".to_string()), - Some('k') => (&arg[0 .. arg.len() - 1], 1u64 << 10), - Some('m') => (&arg[0 .. arg.len() - 1], 1u64 << 20), - Some('g') => (&arg[0 .. arg.len() - 1], 1u64 << 30), - Some('t') => (&arg[0 .. arg.len() - 1], 1u64 << 40), - Some(_) => (arg, 1), - }; - let base = base.parse::().map_err(|e| e.to_string())?; - Ok((base * scale).try_into().map_err(|e| format!("{:?}", e))?) -} -fn load_book(files: &[String], transform_opts: &TransformOpts) -> Book { +fn load_book(files: &[String], transform_args: &TransformArgs) -> Book { let mut book = files .iter() .map(|name| { @@ -329,58 +224,10 @@ fn load_book(files: &[String], transform_opts: &TransformOpts) -> Book { acc.nets.extend(i.nets); acc }); - let transform_passes = TransformPass::passes_from_cli(&transform_opts.transform_passes); - if transform_passes.pre_reduce { - if transform_passes.eta_reduce { - for (_, def) in &mut book.nets { - def.eta_reduce(); - } - } - book.pre_reduce( - &|x| transform_opts.pre_reduce_skip.iter().any(|y| x == y), - transform_opts.pre_reduce_memory, - transform_opts.pre_reduce_rewrites, - ); - } - for (_, def) in &mut book.nets { - if transform_passes.eta_reduce { - def.eta_reduce(); - } - for tree in def.trees_mut() { - if transform_passes.coalesce_ctrs { - tree.coalesce_constructors(); - } - if transform_passes.encode_adts { - tree.encode_scott_adts(); - } - } - } - if transform_passes.inline { - loop { - let inline_changed = book.inline().unwrap(); - if inline_changed.is_empty() { - break; - } - if !(transform_passes.eta_reduce || transform_passes.encode_adts) { - break; - } - for name in inline_changed { - let def = book.get_mut(&name).unwrap(); - if transform_passes.eta_reduce { - def.eta_reduce(); - } - if transform_passes.encode_adts { - for tree in def.trees_mut() { - tree.encode_scott_adts(); - } - } - } - } - } - if transform_passes.prune { - book.prune(&transform_opts.prune_entrypoints); - } + let transform_passes = TransformPasses::from(&transform_args.transform_passes[..]); + book.transform(transform_passes, &transform_args.transform_opts).unwrap(); + book } @@ -434,7 +281,8 @@ fn compile_executable(target: &str, host: Arc>) -> Result<(), fs::remove_dir_all(outdir)?; } let cargo_toml = include_str!("../Cargo.toml"); - let cargo_toml = cargo_toml.split("##--COMPILER-CUTOFF--##").next().unwrap(); + let mut cargo_toml = cargo_toml.split_once("##--COMPILER-CUTOFF--##").unwrap().0.to_owned(); + cargo_toml.push_str("[features]\ndefault = ['cli']\ncli = ['dep:clap']"); macro_rules! include_files { ($([$($prefix:ident)*])? $mod:ident {$($sub:tt)*} $($rest:tt)*) => { @@ -499,6 +347,7 @@ fn compile_executable(target: &str, host: Arc>) -> Result<(), create_var deref maybe_grow + parse_abbrev_number stats } } diff --git a/src/transform.rs b/src/transform.rs index cecde3bd..023aed19 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -1,5 +1,7 @@ use thiserror::Error; +use crate::{ast::Book, util}; + pub mod coalesce_ctrs; pub mod encode_adts; pub mod eta_reduce; @@ -13,3 +15,166 @@ pub enum TransformError { #[error("infinite reference cycle in `@{0}`")] InfiniteRefCycle(String), } + +impl Book { + pub fn transform(&mut self, passes: TransformPasses, opts: &TransformOpts) -> Result<(), TransformError> { + if passes.prune { + self.prune(&opts.prune_entrypoints); + } + if passes.pre_reduce { + if passes.eta_reduce { + for (_, def) in &mut self.nets { + def.eta_reduce(); + } + } + self.pre_reduce( + &|x| opts.pre_reduce_skip.iter().any(|y| x == y), + opts.pre_reduce_memory, + opts.pre_reduce_rewrites, + ); + } + for (_, def) in &mut self.nets { + if passes.eta_reduce { + def.eta_reduce(); + } + for tree in def.trees_mut() { + if passes.coalesce_ctrs { + tree.coalesce_constructors(); + } + if passes.encode_adts { + tree.encode_scott_adts(); + } + } + } + if passes.inline { + loop { + let inline_changed = self.inline()?; + if inline_changed.is_empty() { + break; + } + if !(passes.eta_reduce || passes.encode_adts) { + break; + } + for name in inline_changed { + let def = self.get_mut(&name).unwrap(); + if passes.eta_reduce { + def.eta_reduce(); + } + if passes.encode_adts { + for tree in def.trees_mut() { + tree.encode_scott_adts(); + } + } + } + } + } + if passes.prune { + self.prune(&opts.prune_entrypoints); + } + Ok(()) + } +} + +#[derive(Clone, Debug)] +#[cfg_attr(feature = "cli", derive(clap::Args))] +#[non_exhaustive] +pub struct TransformOpts { + /// Names of the definitions that should not get pre-reduced. + /// + /// For programs that don't take arguments and don't have side effects this is + /// usually the entry point of the program (otherwise, the whole program will + /// get reduced to normal form). + #[cfg_attr(feature = "cli", arg(long = "pre-reduce-skip", value_delimiter = ' ', action = clap::ArgAction::Append))] + pub pre_reduce_skip: Vec, + + /// How much memory to allocate when pre-reducing. + /// + /// Supports abbreviations such as '4G' or '400M'. + #[cfg_attr(feature = "cli", arg(long = "pre-reduce-memory", value_parser = util::parse_abbrev_number::))] + pub pre_reduce_memory: Option, + + /// Maximum amount of rewrites to do when pre-reducing. + /// + /// Supports abbreviations such as '4G' or '400M'. + #[cfg_attr(feature = "cli", arg(long = "pre-reduce-rewrites", default_value = "100M", value_parser = util::parse_abbrev_number::))] + pub pre_reduce_rewrites: u64, + + /// Names of the definitions that should not get pruned. + #[cfg_attr(feature = "cli", arg(long = "prune-entrypoints", default_value = "main"))] + pub prune_entrypoints: Vec, +} + +impl TransformOpts { + pub fn add_entrypoint(&mut self, entrypoint: &str) { + self.pre_reduce_skip.push(entrypoint.to_owned()); + self.prune_entrypoints.push(entrypoint.to_owned()); + } +} + +macro_rules! transform_passes { + ($($pass:ident: $name:literal $(| $alias:literal)*),* $(,)?) => { + #[derive(Debug, Default, Clone, Copy)] + #[non_exhaustive] + pub struct TransformPasses { + $(pub $pass: bool),* + } + + impl TransformPasses { + pub const NONE: Self = Self { $($pass: false),* }; + pub const ALL: Self = Self { $($pass: true),* }; + } + + #[derive(Debug, Clone, Copy)] + #[allow(non_camel_case_types)] + pub enum TransformPass { + all(bool), + $($pass(bool),)* + } + + #[cfg(feature = "cli")] + impl clap::ValueEnum for TransformPass { + fn value_variants<'a>() -> &'a [Self] { + &[ + Self::all(true), Self::all(false), + $(Self::$pass(true), Self::$pass(false),)* + ] + } + + fn to_possible_value(&self) -> Option { + use TransformPass::*; + Some(match self { + all(true) => clap::builder::PossibleValue::new("all"), + all(false) => clap::builder::PossibleValue::new("no-all"), + $( + $pass(true) => clap::builder::PossibleValue::new($name)$(.alias($alias))*, + $pass(false) => clap::builder::PossibleValue::new(concat!("no-", $name))$(.alias(concat!("no-", $alias)))*, + )* + }) + } + } + + impl From<&[TransformPass]> for TransformPasses { + fn from(args: &[TransformPass]) -> TransformPasses { + use TransformPass::*; + let mut opts = TransformPasses::NONE; + for arg in args { + match arg { + all(true) => opts = TransformPasses::ALL, + all(false) => opts = TransformPasses::NONE, + $(&$pass(b) => opts.$pass = b,)* + } + } + opts + } + } + }; +} + +transform_passes! { + pre_reduce: "pre-reduce" | "pre", + coalesce_ctrs: "coalesce-ctrs" | "coalesce", + encode_adts: "encode-adts" | "adts", + eta_reduce: "eta-reduce" | "eta", + inline: "inline", + prune: "prune", +} diff --git a/src/util.rs b/src/util.rs index 7090843d..a4770622 100644 --- a/src/util.rs +++ b/src/util.rs @@ -4,10 +4,12 @@ mod bi_enum; mod create_var; mod deref; mod maybe_grow; +mod parse_abbrev_number; mod stats; pub(crate) use bi_enum::*; pub(crate) use create_var::*; pub(crate) use deref::*; pub(crate) use maybe_grow::*; +pub use parse_abbrev_number::*; pub use stats::*; diff --git a/src/util/parse_abbrev_number.rs b/src/util/parse_abbrev_number.rs new file mode 100644 index 00000000..ede54401 --- /dev/null +++ b/src/util/parse_abbrev_number.rs @@ -0,0 +1,17 @@ +/// Turn a string representation of a number, such as '1G' or '400K', into a +/// number. +pub fn parse_abbrev_number>(arg: &str) -> Result +where + >::Error: core::fmt::Debug, +{ + let (base, scale) = match arg.to_lowercase().chars().last() { + None => return Err("Mem size argument is empty".to_string()), + Some('k') => (&arg[0 .. arg.len() - 1], 1u64 << 10), + Some('m') => (&arg[0 .. arg.len() - 1], 1u64 << 20), + Some('g') => (&arg[0 .. arg.len() - 1], 1u64 << 30), + Some('t') => (&arg[0 .. arg.len() - 1], 1u64 << 40), + Some(_) => (arg, 1), + }; + let base = base.parse::().map_err(|e| e.to_string())?; + Ok((base * scale).try_into().map_err(|e| format!("{:?}", e))?) +}