diff --git a/benches/benches.rs b/benches/benches.rs index 627a3c25..3b902c65 100644 --- a/benches/benches.rs +++ b/benches/benches.rs @@ -1,8 +1,8 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; use hvmc::{ ast::{Book, Net}, - host::Host, run::{Heap, Net as RtNet, Strict}, + stdlib::create_host, }; use std::{ fs, @@ -60,11 +60,11 @@ fn run_file(path: &PathBuf, group: Option, c: &mut Criterion) { fn benchmark(file_name: &str, book: Book, c: &mut Criterion) { let area = Heap::new_words(1 << 29); - let host = Host::new(&book); + let host = create_host(&book); c.bench_function(file_name, |b| { b.iter(|| { let mut net = RtNet::::new(&area); - net.boot(host.defs.get("main").unwrap()); + net.boot(host.lock().unwrap().defs.get("main").unwrap()); black_box(black_box(net).normal()) }); }); @@ -72,12 +72,12 @@ fn benchmark(file_name: &str, book: Book, c: &mut Criterion) { fn benchmark_group(file_name: &str, group: String, book: Book, c: &mut Criterion) { let area = Heap::new_words(1 << 29); - let host = Host::new(&book); + let host = create_host(&book); c.benchmark_group(group).bench_function(file_name, |b| { b.iter(|| { let mut net = RtNet::::new(&area); - net.boot(host.defs.get("main").unwrap()); + net.boot(host.lock().unwrap().defs.get("main").unwrap()); black_box(black_box(net).normal()) }); }); @@ -99,11 +99,11 @@ fn interact_benchmark(c: &mut Criterion) { let mut book = Book::default(); book.insert("main".to_string(), Net { root: Era, redexes: vec![redex] }); let area = Heap::new_words(1 << 24); - let host = Host::new(&book); + let host = create_host(&book); group.bench_function(name, |b| { b.iter(|| { let mut net = RtNet::::new(&area); - net.boot(host.defs.get("main").unwrap()); + net.boot(host.lock().unwrap().defs.get("main").unwrap()); black_box(black_box(net).normal()) }); }); diff --git a/cspell.json b/cspell.json index fb7cf2de..815868c8 100644 --- a/cspell.json +++ b/cspell.json @@ -3,6 +3,7 @@ "language": "en", "words": [ "anni", + "annihilations", "backlinks", "backoffs", "combinators", @@ -10,6 +11,7 @@ "dereferencable", "dref", "dups", + "effectful", "fmts", "fuzzer", "hasher", @@ -57,5 +59,5 @@ "vtable" ], "files": ["**/*.rs", "**/*.md"], - "ignoreRegExpList": ["HexValues"] + "ignoreRegExpList": ["HexValues", "(? Arr gen = λn match n { 0: λx (Single x) - +: λx + 1+: λx let x0 = (<< x 1) let x1 = (| x0 1) (Concat (gen n-1 x0) (gen n-1 x1)) @@ -102,7 +102,7 @@ radix = λn // swap : u32 -> Map -> Map -> Map swap = λn match n { 0: λx0 λx1 (Node x0 x1) - +: λx0 λx1 (Node x1 x0) + 1+: λx0 λx1 (Node x1 x0) } // main : u32 diff --git a/src/host.rs b/src/host.rs index f706cdf2..38d5a795 100644 --- a/src/host.rs +++ b/src/host.rs @@ -54,18 +54,33 @@ impl Host { } /// Converts all of the nets from the book into runtime defs, and inserts them - /// into the host. + /// into the host. The book must not have refs that are not in the book or the + /// host. pub fn insert_book(&mut self, book: &Book) { + self.insert_book_with_default(book, &mut |x| panic!("Found reference {x:?}, which is not in the book!")) + } + + /// Like `insert_book`, but allows specifying a function (`default_def`) that + /// will be run when the name of a definition is not found in the book. + /// The return value of the function will be inserted into the host. + pub fn insert_book_with_default(&mut self, book: &Book, default_def: &mut dyn FnMut(&str) -> DefRef) { self.defs.reserve(book.len()); self.back.reserve(book.len()); - // Because there may be circular dependencies, inserting the definitions // must be done in two phases: // First, we insert empty defs into the host. Even though their instructions // are not yet set, the address of the def will not change, meaning that // `net_to_runtime_def` can safely use `Port::new_def` on them. - for (name, labs) in calculate_label_sets(book, self) { + for (name, labs) in calculate_label_sets(book, |nam| match self.defs.get(nam) { + Some(x) => x.labs.clone(), + None => { + self.insert_def(&nam, default_def(nam)); + self.defs[nam].labs.clone() + } + }) + .into_iter() + { let def = DefRef::Owned(Box::new(Def::new(labs, InterpretedDef::default()))); self.insert_def(name, def); } @@ -73,7 +88,9 @@ impl Host { // Now that `defs` is fully populated, we can fill in the instructions of // each of the new defs. for (nam, net) in book.iter() { - let instr = ast_net_to_instructions(&self.defs, net); + let instr = ast_net_to_instructions(net, |nam| { + Port::new_ref(&self.defs[nam] /* calculate_label_sets already ensures all ref names are in self.defs */) + }); match self.defs.get_mut(nam).unwrap() { DefRef::Owned(def) => def.downcast_mut::().unwrap().data.instr = instr, DefRef::Static(_) => unreachable!(), diff --git a/src/host/calc_labels.rs b/src/host/calc_labels.rs index fc4395c6..64c9978c 100644 --- a/src/host/calc_labels.rs +++ b/src/host/calc_labels.rs @@ -1,6 +1,7 @@ -use super::*; use crate::util::maybe_grow; +use super::*; + /// Calculates the labels used in each definition of a book. /// /// # Background: Naive Algorithms @@ -74,113 +75,121 @@ use crate::util::maybe_grow; /// /// This algorithm runs in linear time (as refs are traversed at most twice), /// and requires no more space than the naive algorithm. -pub(crate) fn calculate_label_sets<'a>(book: &'a Book, host: &Host) -> impl Iterator { - let mut state = State { book, host, labels: HashMap::with_capacity(book.len()) }; +pub(crate) fn calculate_label_sets<'b, 'l>(book: &'b Book, lookup: impl FnMut(&'b str) -> LabSet) -> LabelSets<'b> { + let mut state = State { book, lookup, labels: HashMap::with_capacity(book.len()) }; for name in book.keys() { state.visit_def(name, Some(0), None); } - return state.labels.into_iter().map(|(nam, lab)| match lab { - LabelState::Done(lab) => (nam, lab), - _ => unreachable!(), - }); + LabelSets(state.labels) +} + +pub(crate) struct LabelSets<'b>(HashMap<&'b str, LabelState>); - struct State<'a, 'b> { - book: &'a Book, - host: &'b Host, - labels: HashMap<&'a str, LabelState>, +impl<'b> LabelSets<'b> { + pub(crate) fn into_iter(self) -> impl Iterator { + self.0.into_iter().map(|(nam, lab)| match lab { + LabelState::Done(lab) => (nam, lab), + _ => unreachable!(), + }) } +} - #[derive(Debug)] - enum LabelState { - Done(LabSet), - /// Encountering this node indicates participation in a cycle with the given - /// head depth. - Cycle(usize), - } +struct State<'b, F> { + book: &'b Book, + lookup: F, + labels: HashMap<&'b str, LabelState>, +} - /// All of these methods share a similar signature: - /// - `depth` is optional; `None` indicates that this is the second processing - /// pass (where the depth is irrelevant, as all cycles have been detected) - /// - `out`, if supplied, will be unioned with the result of this traversal - /// - the return value indicates the head depth, as defined above (or an - /// arbitrary value `>= depth` if no cycles are involved) - impl<'a, 'b> State<'a, 'b> { - fn visit_def(&mut self, key: &'a str, depth: Option, out: Option<&mut LabSet>) -> usize { - match self.labels.entry(key) { - Entry::Vacant(e) => { - e.insert(LabelState::Cycle(depth.unwrap())); +#[derive(Debug)] +enum LabelState { + Done(LabSet), + /// Encountering this node indicates participation in a cycle with the given + /// head depth. + Cycle(usize), +} + +/// All of these methods share a similar signature: +/// - `depth` is optional; `None` indicates that this is the second processing +/// pass (where the depth is irrelevant, as all cycles have been detected) +/// - `out`, if supplied, will be unioned with the result of this traversal +/// - the return value indicates the head depth, as defined above (or an +/// arbitrary value `>= depth` if no cycles are involved) +impl<'b, F: FnMut(&'b str) -> LabSet> State<'b, F> { + fn visit_def(&mut self, key: &'b str, depth: Option, out: Option<&mut LabSet>) -> usize { + match self.labels.entry(key) { + Entry::Vacant(e) => { + e.insert(LabelState::Cycle(depth.unwrap())); + self.calc_def(key, depth, out) + } + Entry::Occupied(mut e) => match e.get_mut() { + LabelState::Done(labs) => { + if let Some(out) = out { + out.union(labs); + } + usize::MAX + } + LabelState::Cycle(d) if depth.is_some() => *d, + LabelState::Cycle(_) => { + e.insert(LabelState::Done(LabSet::default())); self.calc_def(key, depth, out) } - Entry::Occupied(mut e) => match e.get_mut() { - LabelState::Done(labs) => { - if let Some(out) = out { - out.union(labs); - } - usize::MAX - } - LabelState::Cycle(d) if depth.is_some() => *d, - LabelState::Cycle(_) => { - e.insert(LabelState::Done(LabSet::default())); - self.calc_def(key, depth, out) - } - }, - } + }, } + } - fn calc_def(&mut self, key: &'a str, depth: Option, out: Option<&mut LabSet>) -> usize { - let mut labs = LabSet::default(); - let head_depth = self.visit_within_def(key, depth, Some(&mut labs)); - if let Some(out) = out { - out.union(&labs); - } - if depth.is_some_and(|x| x > head_depth) { - self.labels.insert(key, LabelState::Cycle(head_depth)); - } else { - self.labels.insert(key, LabelState::Done(labs)); - if depth == Some(head_depth) { - self.visit_within_def(key, None, None); - } + fn calc_def(&mut self, key: &'b str, depth: Option, out: Option<&mut LabSet>) -> usize { + let mut labs = LabSet::default(); + let head_depth = self.visit_within_def(key, depth, Some(&mut labs)); + if let Some(out) = out { + out.union(&labs); + } + if depth.is_some_and(|x| x > head_depth) { + self.labels.insert(key, LabelState::Cycle(head_depth)); + } else { + self.labels.insert(key, LabelState::Done(labs)); + if depth == Some(head_depth) { + self.visit_within_def(key, None, None); } - head_depth } + head_depth + } - fn visit_within_def(&mut self, key: &str, depth: Option, mut out: Option<&mut LabSet>) -> usize { - let def = &self.book[key]; - let mut head_depth = self.visit_tree(&def.root, depth, out.as_deref_mut()); - for (a, b) in &def.redexes { - head_depth = head_depth.min(self.visit_tree(a, depth, out.as_deref_mut())); - head_depth = head_depth.min(self.visit_tree(b, depth, out.as_deref_mut())); - } - head_depth + fn visit_within_def(&mut self, key: &str, depth: Option, mut out: Option<&mut LabSet>) -> usize { + let def = &self.book[key]; + let mut head_depth = self.visit_tree(&def.root, depth, out.as_deref_mut()); + for (a, b) in &def.redexes { + head_depth = head_depth.min(self.visit_tree(a, depth, out.as_deref_mut())); + head_depth = head_depth.min(self.visit_tree(b, depth, out.as_deref_mut())); } + head_depth + } - fn visit_tree(&mut self, tree: &'a Tree, depth: Option, mut out: Option<&mut LabSet>) -> usize { - maybe_grow(move || match tree { - Tree::Era | Tree::Var { .. } | Tree::Num { .. } => usize::MAX, - Tree::Ctr { lab, lft, rgt } => { - if let Some(out) = out.as_deref_mut() { - out.add(*lab); - } - usize::min(self.visit_tree(lft, depth, out.as_deref_mut()), self.visit_tree(rgt, depth, out.as_deref_mut())) + fn visit_tree(&mut self, tree: &'b Tree, depth: Option, mut out: Option<&mut LabSet>) -> usize { + maybe_grow(move || match tree { + Tree::Era | Tree::Var { .. } | Tree::Num { .. } => usize::MAX, + Tree::Ctr { lab, lft, rgt } => { + if let Some(out) = out.as_deref_mut() { + out.add(*lab); } - Tree::Ref { nam } => { - if let Some(def) = self.host.defs.get(nam) { - if let Some(out) = out { - out.union(&def.labs); - } - usize::MAX - } else { - self.visit_def(nam, depth.map(|x| x + 1), out) + usize::min(self.visit_tree(lft, depth, out.as_deref_mut()), self.visit_tree(rgt, depth, out.as_deref_mut())) + } + Tree::Ref { nam } => { + if self.book.contains_key(nam) { + self.visit_def(nam, depth.map(|x| x + 1), out) + } else { + if let Some(out) = out { + out.union(&(self.lookup)(nam)); } + usize::MAX } - Tree::Op1 { rgt, .. } => self.visit_tree(rgt, depth, out), - Tree::Op2 { lft, rgt, .. } | Tree::Mat { sel: lft, ret: rgt } => { - usize::min(self.visit_tree(lft, depth, out.as_deref_mut()), self.visit_tree(rgt, depth, out.as_deref_mut())) - } - }) - } + } + Tree::Op1 { rgt, .. } => self.visit_tree(rgt, depth, out), + Tree::Op2 { lft, rgt, .. } | Tree::Mat { sel: lft, ret: rgt } => { + usize::min(self.visit_tree(lft, depth, out.as_deref_mut()), self.visit_tree(rgt, depth, out.as_deref_mut())) + } + }) } } @@ -203,8 +212,9 @@ fn test_calculate_labels() { " .parse() .unwrap(), - &Host::default(), + |_| unreachable!(), ) + .into_iter() .collect::>(), [ ("a", [0, 1, 2].into_iter().collect()), diff --git a/src/host/encode_def.rs b/src/host/encode_def.rs index 10e67bc4..d13c7df7 100644 --- a/src/host/encode_def.rs +++ b/src/host/encode_def.rs @@ -4,9 +4,12 @@ use crate::util::maybe_grow; /// Converts an ast net to a list of instructions to create the net. /// /// `defs` must be populated with every `Ref` node that may appear in the net. -pub(super) fn ast_net_to_instructions(defs: &HashMap, net: &Net) -> Vec { +/// Converts an ast net to a list of instructions to create the net. +/// +/// `get_def` gets the `Port` corresponding to a `Ref` name +pub(crate) fn ast_net_to_instructions Port>(net: &Net, get_def: F) -> Vec { let mut state = - State { defs, scope: Default::default(), instr: Default::default(), end: Default::default(), next_index: 1 }; + State { get_def, scope: Default::default(), instr: Default::default(), end: Default::default(), next_index: 1 }; state.visit_tree(&net.root, TrgId::new(0)); @@ -18,15 +21,15 @@ pub(super) fn ast_net_to_instructions(defs: &HashMap, net: &Net) return state.instr; - struct State<'a> { - defs: &'a HashMap, + struct State<'a, F: FnMut(&str) -> Port> { + get_def: F, scope: HashMap<&'a str, TrgId>, instr: Vec, end: Vec, next_index: usize, } - impl<'a> State<'a> { + impl<'a, F: FnMut(&str) -> Port> State<'a, F> { fn id(&mut self) -> TrgId { let i = self.next_index; self.next_index += 1; @@ -35,7 +38,7 @@ pub(super) fn ast_net_to_instructions(defs: &HashMap, net: &Net) fn visit_redex(&mut self, a: &'a Tree, b: &'a Tree) { let (port, tree) = match (a, b) { (Tree::Era, t) | (t, Tree::Era) => (Port::ERA, t), - (Tree::Ref { nam }, t) | (t, Tree::Ref { nam }) => (Port::new_ref(&self.defs[nam]), t), + (Tree::Ref { nam }, t) | (t, Tree::Ref { nam }) => ((self.get_def)(&nam), t), (Tree::Num { val }, t) | (t, Tree::Num { val }) => (Port::new_num(*val), t), (t, u) => { let av = self.id(); @@ -60,7 +63,7 @@ pub(super) fn ast_net_to_instructions(defs: &HashMap, net: &Net) self.instr.push(Instruction::LinkConst { trg, port: Port::ERA }); } Tree::Ref { nam } => { - self.instr.push(Instruction::LinkConst { trg, port: Port::new_ref(&self.defs[nam]) }); + self.instr.push(Instruction::LinkConst { trg, port: (self.get_def)(&nam) }); } Tree::Num { val } => { self.instr.push(Instruction::LinkConst { trg, port: Port::new_num(*val) }); diff --git a/src/lib.rs b/src/lib.rs index efa6fd0d..ec7f6f37 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ pub mod host; pub mod ops; pub mod run; pub mod stdlib; - +pub mod transform; pub mod util; #[doc(hidden)] // not public api diff --git a/src/main.rs b/src/main.rs index c56ff34f..a9ab4300 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,9 +2,10 @@ use clap::{Args, Parser, Subcommand}; use hvmc::{ - ast::{Net, Tree}, + ast::{Book, Net, Tree}, host::Host, run::{DynNet, Mode, Trg}, + stdlib::create_host, *, }; @@ -13,7 +14,6 @@ use std::{ path::Path, process::{self, Stdio}, str::FromStr, - sync::{Arc, Mutex}, time::{Duration, Instant}, }; @@ -24,23 +24,29 @@ fn main() { if cfg!(feature = "_full_cli") { let cli = FullCli::parse(); match cli.mode { - CliMode::Compile { file, output } => { + CliMode::Compile { file, transform_opts, 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 = load_files(&[file.clone()]); + let host = create_host(&load_book(&[file.clone()], &transform_opts)); compile_executable(output, &host.lock().unwrap()).unwrap(); } - CliMode::Run { opts, file, args } => { - let host = load_files(&[file]); - run(&host.lock().unwrap(), opts, args); + CliMode::Run { run_opts, mut transform_opts, file, args } => { + // Don't pre-reduce the main reduction + transform_opts.pre_reduce_skip.push(args.entry_point.clone()); + let host = create_host(&load_book(&[file], &transform_opts)); + run(&host.lock().unwrap(), run_opts, args); } - CliMode::Reduce { run_opts, files, exprs } => { - let host = load_files(&files); + CliMode::Reduce { run_opts, transform_opts, files, exprs } => { + let host = create_host(&load_book(&files, &transform_opts)); let exprs: Vec<_> = exprs.iter().map(|x| Net::from_str(x).unwrap()).collect(); reduce_exprs(&host.lock().unwrap(), &exprs, &run_opts); } + CliMode::Transform { transform_opts, files } => { + let book = load_book(&files, &transform_opts); + println!("{}", book); + } } } else { let cli = BareCli::parse(); @@ -81,42 +87,6 @@ struct BareCli { pub args: RunArgs, } -#[derive(Args, Clone, Debug)] -struct RuntimeOpts { - #[arg(short = 's', long = "stats")] - /// Show performance statistics. - show_stats: bool, - #[arg(short = '1', long = "single")] - /// Single-core mode (no parallelism). - single_core: bool, - #[arg(short = 'l', long = "lazy")] - /// Lazy mode. - /// - /// Lazy mode only expands references that are reachable - /// 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", default_value = "1G", value_parser = mem_parser)] - /// How much memory to allocate on startup, measured in bytes. - /// - /// Supports abbreviations such as '4G' or '400M'. - memory: u64, -} - -#[derive(Args, Clone, Debug)] -struct RunArgs { - #[arg(short = 'e', default_value = "main")] - /// Name of the definition that will get reduced. - entry_point: String, - /// List of arguments to pass to the program. - /// - /// Arguments are passed using the lambda-calculus interpretation - /// of interaction combinators. So, for example, if the arguments are - /// "#1" "#2" "#3", then the expression that will get reduced is - /// `r & @main ~ (#1 (#2 (#3 r)))`. - args: Vec, -} - #[derive(Subcommand, Clone, Debug)] #[command(author, version)] enum CliMode { @@ -127,15 +97,19 @@ enum CliMode { #[arg(short = 'o', long = "output")] /// Output path; defaults to the input file with `.hvmc` stripped. output: Option, + #[command(flatten)] + transform_opts: TransformOpts, }, /// Run a program, optionally passing a list of arguments to it. Run { - #[command(flatten)] - opts: RuntimeOpts, /// Name of the file to load. file: String, #[command(flatten)] args: RunArgs, + #[command(flatten)] + run_opts: RuntimeOpts, + #[command(flatten)] + transform_opts: TransformOpts, }, /// Reduce hvm-core expressions to their normal form. /// @@ -144,8 +118,6 @@ enum CliMode { /// which makes it possible to reference definitions from the file /// in the expression. Reduce { - #[command(flatten)] - run_opts: RuntimeOpts, #[arg(required = false)] /// Files to load before reducing the expressions. /// @@ -158,7 +130,110 @@ enum CliMode { /// printed on a new line. This list must be separated from the file list /// with a double dash ('--'). exprs: Vec, + #[command(flatten)] + run_opts: RuntimeOpts, + #[command(flatten)] + transform_opts: TransformOpts, }, + /// Transform a hvm-core program using one of the optimization passes. + Transform { + /// Files to load before reducing the expressions. + /// + /// Multiple files will act as if they're concatenated together. + #[arg(required = true)] + files: Vec, + #[command(flatten)] + transform_opts: TransformOpts, + }, +} + +#[derive(Args, Clone, Debug)] +struct TransformOpts { + #[arg(short = 'O', value_delimiter = ' ', action = clap::ArgAction::Append)] + /// Enables or disables transformation passes. + 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", default_value = "1G", value_parser = parse_abbrev_number::)] + /// How much memory to allocate when pre-reducing. + /// + /// Supports abbreviations such as '4G' or '400M'. + pre_reduce_memory: usize, + #[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, +} + +#[derive(Args, Clone, Debug)] +struct RuntimeOpts { + #[arg(short = 's', long = "stats")] + /// Show performance statistics. + show_stats: bool, + #[arg(short = '1', long = "single")] + /// Single-core mode (no parallelism). + single_core: bool, + #[arg(short = 'l', long = "lazy")] + /// Lazy mode. + /// + /// Lazy mode only expands references that are reachable + /// 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", default_value = "1G", value_parser = parse_abbrev_number::)] + /// How much memory to allocate on startup. + /// + /// Supports abbreviations such as '4G' or '400M'. + memory: usize, +} +#[derive(Args, Clone, Debug)] +struct RunArgs { + #[arg(short = 'e', default_value = "main")] + /// Name of the definition that will get reduced. + entry_point: String, + /// List of arguments to pass to the program. + /// + /// Arguments are passed using the lambda-calculus interpretation + /// of interaction combinators. So, for example, if the arguments are + /// "#1" "#2" "#3", then the expression that will get reduced is + /// `r & @main ~ (#1 (#2 (#3 r)))`. + args: Vec, +} + +#[derive(clap::ValueEnum, Clone, Debug)] +pub enum TransformPass { + All, + NoAll, + PreReduce, + NoPreReduce, +} + +#[derive(Default)] +pub struct TransformPasses { + pre_reduce: bool, +} + +impl TransformPass { + fn passes_from_cli(args: &Vec) -> TransformPasses { + use TransformPass::*; + let mut opts = TransformPasses::default(); + for arg in args { + match arg { + All => opts.pre_reduce = true, + NoAll => opts.pre_reduce = false, + PreReduce => opts.pre_reduce = true, + NoPreReduce => opts.pre_reduce = false, + } + } + opts + } } fn run(host: &Host, opts: RuntimeOpts, args: RunArgs) { @@ -177,46 +252,53 @@ fn run(host: &Host, opts: RuntimeOpts, args: RunArgs) { /// 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 mem_parser(arg: &str) -> Result { +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], 1 << 10), - Some('m') => (&arg[0 .. arg.len() - 1], 1 << 20), - Some('g') => (&arg[0 .. arg.len() - 1], 1 << 30), + 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(_) => (arg, 1), }; let base = base.parse::().map_err(|e| e.to_string())?; - Ok(base * scale) + Ok((base * scale).try_into().map_err(|e| format!("{:?}", e))?) } -fn load_files(files: &[String]) -> Arc> { - let files: Vec<_> = files +fn load_book(files: &[String], transform_opts: &TransformOpts) -> Book { + let mut book = files .iter() .map(|name| { - fs::read_to_string(name).unwrap_or_else(|_| { + let contents = fs::read_to_string(name).unwrap_or_else(|_| { eprintln!("Input file {:?} not found", name); process::exit(1); + }); + contents.parse::().unwrap_or_else(|e| { + eprintln!("Parsing error {e}"); + process::exit(1); }) }) - .collect(); - let host = Arc::new(Mutex::new(host::Host::default())); - host.lock().unwrap().insert_def( - "HVM.log", - host::DefRef::Owned(Box::new(stdlib::LogDef::new({ - let host = Arc::downgrade(&host); - move |wire| { - println!("{}", host.upgrade().unwrap().lock().unwrap().readback_tree(&wire)); - } - }))), - ); - for file_contents in files { - host.lock().unwrap().insert_book(&ast::Book::from_str(&file_contents).unwrap()); + .fold(Book::default(), |mut acc, i| { + acc.nets.extend(i.nets); + acc + }); + let transform_passes = TransformPass::passes_from_cli(&transform_opts.transform_passes); + if transform_passes.pre_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, + ) + .unwrap(); } - host + book } fn reduce_exprs(host: &Host, exprs: &[Net], opts: &RuntimeOpts) { - let heap = run::Heap::new_bytes(opts.memory as usize); + let heap = run::Heap::new_bytes(opts.memory); for expr in exprs { let mut net = DynNet::new(&heap, opts.lazy_mode); dispatch_dyn_net!(&mut net => { @@ -315,6 +397,9 @@ fn compile_executable(target: &str, host: &host::Host) -> Result<(), io::Error> } stdlib trace + transform { + pre_reduce + } util { apply_tree bi_enum diff --git a/src/stdlib.rs b/src/stdlib.rs index f9f53a6a..c19a332f 100644 --- a/src/stdlib.rs +++ b/src/stdlib.rs @@ -1,6 +1,10 @@ -use std::sync::Arc; +use std::sync::{Arc, Mutex}; -use crate::run::{AsDef, Def, LabSet, Mode, Net, Port, Tag, Trg, Wire}; +use crate::{ + ast::Book, + host::{DefRef, Host}, + run::{AsDef, Def, LabSet, Mode, Net, Port, Tag, Trg, Wire}, +}; /// `@IDENTITY = (x x)` pub const IDENTITY: *const Def = const { &Def::new(LabSet::from_bits(&[1]), (call_identity, call_identity)) }.upcast(); @@ -98,3 +102,19 @@ impl AsDef for ActiveLogDef { def.data.logger.maybe_log(net); } } + +/// Create a `Host` from a `Book`, including `hvm-core`'s built-in definitions +pub fn create_host(book: &Book) -> Arc> { + let host = Arc::new(Mutex::new(Host::default())); + host.lock().unwrap().insert_def( + "HVM.log", + DefRef::Owned(Box::new(crate::stdlib::LogDef::new({ + let host = Arc::downgrade(&host); + move |wire| { + println!("{}", host.upgrade().unwrap().lock().unwrap().readback_tree(&wire)); + } + }))), + ); + host.lock().unwrap().insert_book(&book); + host +} diff --git a/src/transform.rs b/src/transform.rs new file mode 100644 index 00000000..a5220e51 --- /dev/null +++ b/src/transform.rs @@ -0,0 +1 @@ +pub mod pre_reduce; diff --git a/src/transform/pre_reduce.rs b/src/transform/pre_reduce.rs new file mode 100644 index 00000000..9fa10259 --- /dev/null +++ b/src/transform/pre_reduce.rs @@ -0,0 +1,57 @@ +// Reduce the compiled networks, solving any annihilations and commutations. + +use std::sync::Mutex; + +use crate::{ + ast::Book, + host::{DefRef, Host}, + run::{self, Def, LabSet}, +}; + +/// A Def that pushes all interactions to its inner Vec. +#[derive(Default)] +struct InertDef(Mutex>); + +impl run::AsDef for InertDef { + unsafe fn call(def: *const run::Def, _: &mut run::Net, port: run::Port) { + let def = unsafe { &*def }; + def.data.0.lock().unwrap().push((run::Port::new_ref(def), port)); + } +} + +impl Book { + /// Reduces the definitions in the book individually, except for the skipped + /// ones. + /// + /// Defs that are not in the book are treated as inert defs. + pub fn pre_reduce(&mut self, skip: &dyn Fn(&str) -> bool, max_memory: usize, max_rwts: u64) -> Result<(), String> { + let mut host = Host::default(); + // When a ref is not found in the `Host`, then + // put an inert def in its place + host.insert_book_with_default(self, &mut |_| DefRef::Owned(Box::new(Def::new(LabSet::ALL, InertDef::default())))); + let area = run::Heap::new_bytes(max_memory); + + for (nam, net) in self.nets.iter_mut() { + // Skip unnecessary work + if net.redexes.is_empty() || skip(nam) { + continue; + } + + let mut rt = run::Net::::new(&area); + rt.boot(host.defs.get(nam).expect("No function.")); + rt.expand(); + rt.reduce(max_rwts as usize); + + // Move interactions with inert defs back into the net redexes array + for def in host.defs.values() { + if let Some(def) = def.downcast_ref::() { + let mut stored_redexes = def.data.0.lock().unwrap(); + rt.redexes.extend(core::mem::take(&mut *stored_redexes)); + } + } + // Place the reduced net back into the def map + *net = host.readback(&mut rt); + } + Ok(()) + } +} diff --git a/tests/cli.rs b/tests/cli.rs index 9c3a6798..4b1d933e 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -113,6 +113,64 @@ fn test_cli_run_with_args() { ); } +#[test] +fn test_cli_transform() { + let arithmetic_program = get_arithmetic_program_path(); + + // Test simple program running + assert_display_snapshot!( + execute_hvmc(&[ + "transform", + "-Opre-reduce", + &arithmetic_program, + ]).unwrap().1, + @r###" + @add = (<+ a b> (a b)) + @div = ( (a b)) + @main = ({3 <% c d>} ({5 a c} [b d])) + @mod = (<% a b> (a b)) + @mul = (<* a b> (a b)) + @sub = (<- a b> (a b)) + + "### + ); + + assert_display_snapshot!( + execute_hvmc(&[ + "transform", + "-Opre-reduce", + "--pre-reduce-skip", "main", + &arithmetic_program, + ]).unwrap().1, + @r###" + @add = (<+ a b> (a b)) + @div = ( (a b)) + @main = ({3 a b} ({5 c d} [e f])) + & @mod ~ (b (d f)) + & @div ~ (a (c e)) + @mod = (<% a b> (a b)) + @mul = (<* a b> (a b)) + @sub = (<- a b> (a b)) + + "### + ); + + // Test log + + assert_display_snapshot!( + execute_hvmc(&[ + "transform", + "-Opre-reduce", + &(env!("CARGO_MANIFEST_DIR").to_owned() + "/tests/programs/log.hvmc") + ]).unwrap().1, + @r###" + @main = a + & @HVM.log ~ (#1 (#2 a)) + + "### + ); +} + #[test] fn test_cli_errors() { // Test passing all arguments. diff --git a/tests/lists.rs b/tests/lists.rs index 6e36b594..4cfe148f 100644 --- a/tests/lists.rs +++ b/tests/lists.rs @@ -6,7 +6,6 @@ mod loaders; fn list_got(index: u32) -> Book { let code = load_file("list_put_got.hvmc"); let mut book = parse_core(&code); - println!("{:#?}", book.keys().collect::>()); let def = book.get_mut("GenGotIndex").unwrap(); def.apply_tree(hvmc::ast::Tree::Ref { nam: format!("S{index}") }); let def = book.get_mut("main").unwrap(); @@ -20,7 +19,6 @@ fn list_put(index: u32, value: u32) -> Book { let def = book.get_mut("GenPutIndexValue").unwrap(); def.apply_tree(hvmc::ast::Tree::Ref { nam: format!("S{index}") }); def.apply_tree(hvmc::ast::Tree::Ref { nam: format!("S{value}") }); - println!("{:?}", def); let def = book.get_mut("main").unwrap(); def.apply_tree(hvmc::ast::Tree::Ref { nam: format!("GenPutIndexValue") }); book diff --git a/tests/programs/log.hvm2 b/tests/programs/log.hvm2 new file mode 100644 index 00000000..ef813b5d --- /dev/null +++ b/tests/programs/log.hvm2 @@ -0,0 +1 @@ +main = (HVM.log 1 2) \ No newline at end of file diff --git a/tests/programs/log.hvmc b/tests/programs/log.hvmc new file mode 100644 index 00000000..49d90ca4 --- /dev/null +++ b/tests/programs/log.hvmc @@ -0,0 +1,3 @@ +@main = a +& @HVM.log ~ (#1 (#2 a)) + diff --git a/tests/programs/stress_tests/all_tree.hvm2 b/tests/programs/stress_tests/all_tree.hvm2 index defa4293..73dccc24 100644 --- a/tests/programs/stress_tests/all_tree.hvm2 +++ b/tests/programs/stress_tests/all_tree.hvm2 @@ -8,7 +8,7 @@ and = λa (a λb(b) λb(False)) gen = λn match n { 0: (Leaf True) - +: (Node (gen n-1) (gen n-1)) + 1+: (Node (gen n-1) (gen n-1)) } all = λt diff --git a/tests/programs/stress_tests/apelacion.hvm2 b/tests/programs/stress_tests/apelacion.hvm2 index b985c9fe..8d5eb44c 100644 --- a/tests/programs/stress_tests/apelacion.hvm2 +++ b/tests/programs/stress_tests/apelacion.hvm2 @@ -1,11 +1,11 @@ sum = λa match a { 0: λs s - +: λs (sum a-1 (+ a-1 s)) + 1+: λs (sum a-1 (+ a-1 s)) } rec = λa match a { 0: (sum 1000000 0) - +: (+ (rec a-1) (rec a-1)) + 1+: (+ (rec a-1) (rec a-1)) } main = (rec 6) diff --git a/tests/programs/stress_tests/fib_rec.hvm2 b/tests/programs/stress_tests/fib_rec.hvm2 index 23325c31..5b5da50a 100644 --- a/tests/programs/stress_tests/fib_rec.hvm2 +++ b/tests/programs/stress_tests/fib_rec.hvm2 @@ -2,9 +2,9 @@ add = λa λb (+ a b) fib = λx match x { 0: 1 - +: let p = x-1; match p { + 1+: let p = x-1; match p { 0: 1 - +: (+ (fib p) (fib p-1)) + 1+: (+ (fib p) (fib p-1)) } } diff --git a/tests/programs/stress_tests/sum_rec.hvm2 b/tests/programs/stress_tests/sum_rec.hvm2 index 9dea25f8..d094f909 100644 --- a/tests/programs/stress_tests/sum_rec.hvm2 +++ b/tests/programs/stress_tests/sum_rec.hvm2 @@ -2,7 +2,7 @@ add = λa λb (+ a b) sum = λn match n { 0: 1 - +: (add (sum n-1) (sum n-1)) + 1+: (add (sum n-1) (sum n-1)) } main = (sum 26) diff --git a/tests/programs/stress_tests/sum_tail.hvm2 b/tests/programs/stress_tests/sum_tail.hvm2 index 04c09f55..11cc502a 100644 --- a/tests/programs/stress_tests/sum_tail.hvm2 +++ b/tests/programs/stress_tests/sum_tail.hvm2 @@ -1,6 +1,6 @@ sum = λa match a { 0: λs s - +: λs (sum a-1 (+ a-1 s)) + 1+: λs (sum a-1 (+ a-1 s)) } main = (sum 10000000 0) diff --git a/tests/programs/stress_tests/sum_tree.hvm2 b/tests/programs/stress_tests/sum_tree.hvm2 index 790acb37..5b1fb5c4 100644 --- a/tests/programs/stress_tests/sum_tree.hvm2 +++ b/tests/programs/stress_tests/sum_tree.hvm2 @@ -5,7 +5,7 @@ add = λa λb (+ a b) gen = λn match n { 0: (Leaf 1) - +: (Node (gen n-1) (gen n-1)) + 1+: (Node (gen n-1) (gen n-1)) } sum = λt diff --git a/tests/programs/stress_tests/tuple_rots.hvm2 b/tests/programs/stress_tests/tuple_rots.hvm2 index c90fe8f5..d181428f 100644 --- a/tests/programs/stress_tests/tuple_rots.hvm2 +++ b/tests/programs/stress_tests/tuple_rots.hvm2 @@ -4,7 +4,7 @@ rot = λx (x λa λb λc λd λe λf λg λh (MkTup8 b c d e f g h a)) app = λn match n { 0: λf λx x - +: λf λx (app n-1 f (f x)) + 1+: λf λx (app n-1 f (f x)) } main = (app 2000000 rot (MkTup8 1 2 3 4 5 6 7 8)) diff --git a/tests/snapshots/tests__run@log.hvmc.snap b/tests/snapshots/tests__run@log.hvmc.snap new file mode 100644 index 00000000..df3f86c8 --- /dev/null +++ b/tests/snapshots/tests__run@log.hvmc.snap @@ -0,0 +1,12 @@ +--- +source: tests/tests.rs +expression: output +input_file: tests/programs/log.hvmc +--- +#2 +RWTS : 7 +- ANNI : 2 +- COMM : 0 +- ERAS : 1 +- DREF : 4 +- OPER : 0 diff --git a/tests/tests.rs b/tests/tests.rs index 05da2523..6b8f02e7 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -3,6 +3,7 @@ use std::{ io::{self, Write}, path::{Path, PathBuf}, str::FromStr, + sync::{Arc, Mutex}, time::Instant, }; @@ -59,28 +60,32 @@ fn test_bool_and() { assert_debug_snapshot!(rwts.total(), @"9"); } -fn test_run(name: &str, host: Host) { +fn test_run(name: &str, host: Arc>) { print!("{name}..."); io::stdout().flush().unwrap(); - let Some(entrypoint) = host.defs.get("main") else { - println!(" skipping"); - return; - }; let heap = run::Heap::new_words(1 << 29); let mut net = run::Net::::new(&heap); - net.boot(entrypoint); + // The host is locked inside this block. + { + let lock = host.lock().unwrap(); + let Some(entrypoint) = lock.defs.get("main") else { + println!(" skipping"); + return; + }; + net.boot(entrypoint); + } let start = Instant::now(); net.parallel_normal(); println!(" {:.3?}", start.elapsed()); - let output = format!("{}\n{}", host.readback(&net), show_rewrites(&net.rwts)); + let output = format!("{}\n{}", host.lock().unwrap().readback(&net), show_rewrites(&net.rwts)); assert_snapshot!(output); } fn test_path(path: &Path) { let code = fs::read_to_string(&path).unwrap(); let book = ast::Book::from_str(&code).unwrap(); - let host = Host::new(&book); + let host = hvmc::stdlib::create_host(&book); let path = path.strip_prefix(env!("CARGO_MANIFEST_DIR")).unwrap();