From 0ba064c2cc7c31bfbf0db1fe822b78bd56bf0f05 Mon Sep 17 00:00:00 2001 From: FranchuFranchu <38839219+FranchuFranchu@users.noreply.github.com> Date: Wed, 28 Feb 2024 08:35:30 -0300 Subject: [PATCH] [sc-472] Allow passing arguments to program using a new CLI interface. (#66) Co-authored-by: tjjfvi --- Cargo.lock | 98 +++++- Cargo.toml | 1 + examples/arithmetic.hvm2 | 6 + examples/arithmetic.hvmc | 9 + src/host.rs | 49 +-- src/host/encode.rs | 71 +++++ src/main.rs | 279 ++++++++++++++---- tests/api.rs | 59 ---- tests/cli.rs | 185 ++++++++++++ .../snapshots/tests__run@arithmetic.hvmc.snap | 12 + 10 files changed, 593 insertions(+), 176 deletions(-) create mode 100644 examples/arithmetic.hvm2 create mode 100644 examples/arithmetic.hvmc create mode 100644 src/host/encode.rs delete mode 100644 tests/api.rs create mode 100644 tests/cli.rs create mode 100644 tests/snapshots/tests__run@arithmetic.hvmc.snap diff --git a/Cargo.lock b/Cargo.lock index d4cb22e2..0a107f76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,12 +17,54 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "anstream" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -37,9 +79,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bstr" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc" +checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" dependencies = [ "memchr", "serde", @@ -97,6 +139,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -105,8 +148,22 @@ version = "4.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" dependencies = [ + "anstream", "anstyle", "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -115,6 +172,12 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "console" version = "0.15.8" @@ -311,9 +374,9 @@ dependencies = [ [[package]] name = "half" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc52e53916c08643f1b56ec082790d1e86a32e58dc5268f897f313fbae7b4872" +checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e" dependencies = [ "cfg-if", "crunchy", @@ -325,16 +388,23 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd5256b483761cd23699d0da46cc6fd2ee3be420bbe6d020ae4a091e70b7e9fd" +checksum = "379dada1584ad501b383485dd706b8afb7a70fcbc7f4da7d780638a5a6124a60" [[package]] name = "hvm-core" version = "0.2.19" dependencies = [ + "clap", "criterion", "insta", "nohash-hasher", @@ -695,11 +765,17 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +[[package]] +name = "strsim" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" + [[package]] name = "syn" -version = "2.0.50" +version = "2.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb" +checksum = "6ab617d94515e94ae53b8406c628598680aa0c9587474ecbe58188f7b345d66c" dependencies = [ "proc-macro2", "quote", @@ -722,6 +798,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "walkdir" version = "2.4.0" diff --git a/Cargo.toml b/Cargo.toml index 977d75df..ed4e6532 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ panic = "abort" debug = "full" [dependencies] +clap = { version = "4.5.1", features = ["derive"] } nohash-hasher = { version = "0.2.0" } ##--COMPILER-CUTOFF--## diff --git a/examples/arithmetic.hvm2 b/examples/arithmetic.hvm2 new file mode 100644 index 00000000..3b88b5a3 --- /dev/null +++ b/examples/arithmetic.hvm2 @@ -0,0 +1,6 @@ +add = λaλb(+ a b) +sub = λaλb(- a b) +mul = λaλb(* a b) +div = λaλb(/ a b) +mod = λaλb(% a b) +main = λaλb((div a b), (mod a b)) \ No newline at end of file diff --git a/examples/arithmetic.hvmc b/examples/arithmetic.hvmc new file mode 100644 index 00000000..d8f05676 --- /dev/null +++ b/examples/arithmetic.hvmc @@ -0,0 +1,9 @@ +@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)) + diff --git a/src/host.rs b/src/host.rs index fe879034..44ec5e8d 100644 --- a/src/host.rs +++ b/src/host.rs @@ -3,7 +3,7 @@ use crate::{ ast::{Book, Net, Tree}, - run::{self, Addr, Def, Instruction, InterpretedDef, LabSet, Mode, Port, Tag, Trg, TrgId, Wire}, + run::{self, Addr, Def, Instruction, InterpretedDef, LabSet, Mode, Port, Tag, TrgId, Wire}, util::create_var, }; use std::{ @@ -11,6 +11,8 @@ use std::{ ops::{Deref, DerefMut, RangeFrom}, }; +mod encode; + /// Stores a bidirectional mapping between names and runtime defs. #[derive(Default)] pub struct Host { @@ -102,51 +104,6 @@ impl Host { net } - - pub fn encode_tree(&self, net: &mut run::Net, trg: Trg, tree: &Tree) { - EncodeState { host: self, net, vars: Default::default() }.encode(trg, tree); - - struct EncodeState<'c, 'n, M: Mode> { - host: &'c Host, - net: &'c mut run::Net<'n, M>, - vars: HashMap<&'c str, Trg>, - } - - impl<'c, 'n, M: Mode> EncodeState<'c, 'n, M> { - fn encode(&mut self, trg: Trg, tree: &'c Tree) { - match tree { - Tree::Era => self.net.link_trg_port(trg, Port::ERA), - Tree::Num { val } => self.net.link_trg_port(trg, Port::new_num(*val)), - Tree::Ref { nam } => self.net.link_trg_port(trg, Port::new_ref(&self.host.defs[nam])), - Tree::Ctr { lab, lft, rgt } => { - let (l, r) = self.net.do_ctr(*lab, trg); - self.encode(l, lft); - self.encode(r, rgt); - } - Tree::Op2 { opr, lft, rgt } => { - let (l, r) = self.net.do_op2(*opr, trg); - self.encode(l, lft); - self.encode(r, rgt); - } - Tree::Op1 { opr, lft, rgt } => { - let r = self.net.do_op1(*opr, *lft, trg); - self.encode(r, rgt); - } - Tree::Mat { sel, ret } => { - let (s, r) = self.net.do_mat(trg); - self.encode(s, sel); - self.encode(r, ret); - } - Tree::Var { nam } => match self.vars.entry(nam) { - Entry::Occupied(e) => self.net.link_trg(e.remove(), trg), - Entry::Vacant(e) => { - e.insert(trg); - } - }, - } - } - } - } } /// See [`Host::readback`]. diff --git a/src/host/encode.rs b/src/host/encode.rs new file mode 100644 index 00000000..77d1f879 --- /dev/null +++ b/src/host/encode.rs @@ -0,0 +1,71 @@ +//! Code for directly encoding a [`hvmc::ast::Net`] or a [`hvmc::ast::Tree`] +//! into a [`hvmc::run::Net`] + +use std::collections::{hash_map::Entry, HashMap}; + +use crate::{ + ast::{Net, Tree}, + host::Host, + run::{self, Mode, Port, Trg}, +}; + +impl Host { + /// Encode `tree` directly into `trg`, skipping the intermediate `Def` + /// representation. + pub fn encode_tree(&self, net: &mut run::Net, trg: Trg, tree: &Tree) { + EncodeState { host: self, net, vars: Default::default() }.encode(trg, tree); + } + /// Encode the root of `ast_net` directly into `trg` and encode its redexes + /// into `net` redex list. + pub fn encode_net(&self, net: &mut run::Net, trg: Trg, ast_net: &Net) { + let mut state = EncodeState { host: self, net, vars: Default::default() }; + for (l, r) in &ast_net.rdex { + let (ap, a, bp, b) = state.net.do_wires(); + state.encode(ap, l); + state.encode(bp, r); + state.net.link_trg(a, b); + } + state.encode(trg, &ast_net.root); + } +} + +struct EncodeState<'c, 'n, M: Mode> { + host: &'c Host, + net: &'c mut run::Net<'n, M>, + vars: HashMap<&'c str, Trg>, +} + +impl<'c, 'n, M: Mode> EncodeState<'c, 'n, M> { + fn encode(&mut self, trg: Trg, tree: &'c Tree) { + match tree { + Tree::Era => self.net.link_trg_port(trg, Port::ERA), + Tree::Num { val } => self.net.link_trg_port(trg, Port::new_num(*val)), + Tree::Ref { nam } => self.net.link_trg_port(trg, Port::new_ref(&self.host.defs[nam])), + Tree::Ctr { lab, lft, rgt } => { + let (l, r) = self.net.do_ctr(*lab, trg); + self.encode(l, lft); + self.encode(r, rgt); + } + Tree::Op2 { opr, lft, rgt } => { + let (l, r) = self.net.do_op2(*opr, trg); + self.encode(l, lft); + self.encode(r, rgt); + } + Tree::Op1 { opr, lft, rgt } => { + let r = self.net.do_op1(*opr, *lft, trg); + self.encode(r, rgt); + } + Tree::Mat { sel, ret } => { + let (s, r) = self.net.do_mat(trg); + self.encode(s, sel); + self.encode(r, ret); + } + Tree::Var { nam } => match self.vars.entry(nam) { + Entry::Occupied(e) => self.net.link_trg(e.remove(), trg), + Entry::Vacant(e) => { + e.insert(trg); + } + }, + } + } +} diff --git a/src/main.rs b/src/main.rs index 6be6320f..cd3f33d0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,90 +1,197 @@ #![cfg_attr(feature = "trace", feature(const_type_name))] -use hvmc::*; +use clap::{Args, Parser, Subcommand}; +use hvmc::{ + ast::{Net, Tree}, + host::Host, + run::{DynNet, Mode, Strict, Trg}, + *, +}; use std::{ - collections::HashSet, - env, fs, io, + fs, io, path::Path, process::{self, Stdio}, + str::FromStr, sync::{Arc, Mutex}, - time::Instant, + time::{Duration, Instant}, }; fn main() { if cfg!(feature = "trace") { trace::set_hook(); } - let args: Vec = env::args().skip(1).collect(); if cfg!(feature = "_full_cli") { - full_main(&args) + let cli = FullCli::parse(); + match cli.mode { + CliMode::Compile { file } => { + let host = load_files(&[file.clone()]); + compile_executable(&file, &host.lock().unwrap()).unwrap(); + } + CliMode::Run { opts, file, args } => { + let host = load_files(&[file]); + run(&host.lock().unwrap(), opts, args); + } + CliMode::Reduce { run_opts, files, exprs } => { + let host = load_files(&files); + let exprs: Vec<_> = exprs.iter().map(|x| Net::from_str(x).unwrap()).collect(); + reduce_exprs(&host.lock().unwrap(), &exprs, &run_opts); + } + } } else { - run(&args, Arc::new(Mutex::new(hvmc::gen::host()))) + let cli = BareCli::parse(); + let host = hvmc::gen::host(); + run(&host, cli.opts, cli.args); } if cfg!(feature = "trace") { hvmc::trace::_read_traces(usize::MAX); } } -fn full_main(args: &[String]) { - let action = args.get(0).map(|x| &**x).unwrap_or("help"); - let file_name = args.get(1); - let opts = &args.get(2 ..).unwrap_or(&[]); - match action { - "run" => { - let Some(file_name) = file_name else { - println!("Usage: hvmc run [-s] [-1]"); - process::exit(1); - }; - let host = load(file_name); - run(opts, host); - } - "compile" => { - let Some(file_name) = file_name else { - println!("Usage: hvmc compile "); - process::exit(1); - }; - let host = load(file_name); - compile_executable(file_name, &host.lock().unwrap()).unwrap(); - } - _ => { - println!("Usage: hvmc [-s]"); - println!("Commands:"); - println!(" run - Run the given file"); - println!(" compile - Compile the given file to an executable"); - println!("Options:"); - println!(" [-s] Show stats, including rewrite count"); - println!(" [-1] Single-core mode (no parallelism)"); - } - } +#[derive(Parser, Debug)] +#[command( + author, + version, + about = "A massively parallel Interaction Combinator evaluator", + long_about = r##" +A massively parallel Interaction Combinator evaluator + +Examples: +$ hvmc run examples/church_encoding/church.hvm +$ hvmc run examples/addition.hvmc "#16" "#3" +$ hvmc compile examples/addition.hvmc +$ hvmc reduce examples/addition.hvmc -- "a & @mul ~ (#3 (#4 a))" +$ hvmc reduce -- "a & #3 ~ <* #4 a>""## +)] +struct FullCli { + #[command(subcommand)] + pub mode: CliMode, } -fn run(opts: &[String], host: Arc>) { - let opts = opts.iter().map(|x| &**x).collect::>(); - let data = run::Net::::init_heap(1 << 29); - let lazy = opts.contains("-L"); - let net = run::DynNet::new(&data, lazy); - dispatch_dyn_net! { mut net => { - net.boot(&host.lock().unwrap().defs["main"]); - let start_time = Instant::now(); - if lazy || opts.contains("-1") { - net.normal(); - } else { - net.parallel_normal(); - } - let elapsed = start_time.elapsed(); - println!("{}", &host.lock().unwrap().readback(&net)); - if opts.contains("-s") { - eprint!("{}", util::show_stats(&net.rwts, elapsed)); - } - } } +#[derive(Parser, Debug)] +#[command(author, version)] +struct BareCli { + #[command(flatten)] + pub opts: RuntimeOpts, + #[command(flatten)] + pub args: RunArgs, } -fn load(file: &str) -> Arc> { - let Ok(file) = fs::read_to_string(file) else { - eprintln!("Input file not found"); - process::exit(1); +#[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. + /// + /// 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 { + /// Compile a hvm-core program into a Rust crate. + Compile { + /// hvm-core file to compile. + file: String, + }, + /// 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, + }, + /// Reduce hvm-core expressions to their normal form. + /// + /// The expressions are passed as command-line arguments. + /// It is also possible to load files before reducing the expression, + /// 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. + /// + /// Multiple files will act as if they're concatenated together. + files: Vec, + #[arg(required = false, last = true)] + /// Expressions to reduce. + /// + /// The normal form of each expression will be + /// printed on a new line. This list must be separated from the file list + /// with a double dash ('--'). + exprs: Vec, + }, +} + +fn run(host: &Host, opts: RuntimeOpts, args: RunArgs) { + let mut net = Net { root: Tree::Ref { nam: args.entry_point }, rdex: vec![] }; + for arg in args.args { + let arg: Net = Net::from_str(&arg).unwrap(); + net.rdex.extend(arg.rdex); + net.apply_tree(arg.root); + } + + 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 mem_parser(arg: &str) -> Result { + 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(_) => (arg, 1), }; + let base = base.parse::().map_err(|e| e.to_string())?; + Ok(base * scale) +} + +fn load_files(files: &[String]) -> Arc> { + let files: Vec<_> = files + .iter() + .map(|name| { + fs::read_to_string(name).unwrap_or_else(|_| { + eprintln!("Input file {:?} not found", name); + process::exit(1); + }) + }) + .collect(); let host = Arc::new(Mutex::new(host::Host::default())); host.lock().unwrap().insert_def( "HVM.log", @@ -95,10 +202,55 @@ fn load(file: &str) -> Arc> { } }))), ); - host.lock().unwrap().insert_book(&file.parse().expect("parse error")); + for file_contents in files { + host.lock().unwrap().insert_book(&ast::Book::from_str(&file_contents).unwrap()); + } host } +fn reduce_exprs(host: &Host, exprs: &[Net], opts: &RuntimeOpts) { + let heap = run::Net::::init_heap(opts.memory as usize); + for expr in exprs { + let mut net = DynNet::new(&heap, opts.lazy_mode); + dispatch_dyn_net!(&mut net => { + host.encode_net(net, Trg::port(run::Port::new_var(net.root.addr())), expr); + let start_time = Instant::now(); + if opts.single_core { + net.normal(); + } else { + net.parallel_normal(); + } + let elapsed = start_time.elapsed(); + println!("{}", host.readback(net)); + if opts.show_stats { + print_stats(net, elapsed); + } + }); + } +} + +fn print_stats(net: &run::Net, elapsed: Duration) { + eprintln!("RWTS : {:>15}", pretty_num(net.rwts.total())); + eprintln!("- ANNI : {:>15}", pretty_num(net.rwts.anni)); + eprintln!("- COMM : {:>15}", pretty_num(net.rwts.comm)); + eprintln!("- ERAS : {:>15}", pretty_num(net.rwts.eras)); + eprintln!("- DREF : {:>15}", pretty_num(net.rwts.dref)); + eprintln!("- OPER : {:>15}", pretty_num(net.rwts.oper)); + eprintln!("TIME : {:.3?}", elapsed); + eprintln!("RPS : {:.3} M", (net.rwts.total() as f64) / (elapsed.as_millis() as f64) / 1000.0); +} + +fn pretty_num(n: u64) -> String { + n.to_string() + .as_bytes() + .rchunks(3) + .rev() + .map(|x| std::str::from_utf8(x).unwrap()) + .flat_map(|x| ["_", x]) + .skip(1) + .collect() +} + fn compile_executable(file_name: &str, host: &host::Host) -> Result<(), io::Error> { let gen = compile::compile_host(host); let outdir = ".hvm"; @@ -110,9 +262,10 @@ fn compile_executable(file_name: &str, host: &host::Host) -> Result<(), io::Erro fs::create_dir_all(format!("{}/src", outdir))?; fs::write(".hvm/Cargo.toml", cargo_toml)?; fs::write(".hvm/src/ast.rs", include_str!("../src/ast.rs"))?; + fs::write(".hvm/src/compile.rs", include_str!("../src/compile.rs"))?; + fs::write(".hvm/src/host/encode.rs", include_str!("../src/host/encode.rs"))?; fs::write(".hvm/src/fuzz.rs", include_str!("../src/fuzz.rs"))?; fs::write(".hvm/src/host.rs", include_str!("../src/host.rs"))?; - fs::write(".hvm/src/compile.rs", include_str!("../src/compile.rs"))?; fs::write(".hvm/src/lib.rs", include_str!("../src/lib.rs"))?; fs::write(".hvm/src/main.rs", include_str!("../src/main.rs"))?; fs::write(".hvm/src/ops.rs", include_str!("../src/ops.rs"))?; diff --git a/tests/api.rs b/tests/api.rs deleted file mode 100644 index e80871ba..00000000 --- a/tests/api.rs +++ /dev/null @@ -1,59 +0,0 @@ -//! Tests for front-facing APIs and interfaces - -use hvmc::{ - ast::{Book, Net, Tree}, - host::Host, -}; -use insta::assert_display_snapshot; - -#[test] -fn test_apply_tree() { - use hvmc::run; - fn eval_with_args(fun: &str, args: &[&str]) -> Net { - let area = run::Net::::init_heap(1 << 10); - - let mut fun: Net = fun.parse().unwrap(); - for arg in args { - let arg: Tree = arg.parse().unwrap(); - fun.apply_tree(arg) - } - // TODO: When feature/sc-472/argument-passing, use encode_net instead. - let mut book = Book::default(); - book.nets.insert("main".into(), fun); - let host = Host::new(&book); - - let mut rnet = run::Net::::new(&area); - rnet.boot(&host.defs["main"]); - rnet.normal(); - let got_result = host.readback(&rnet); - got_result - } - assert_display_snapshot!( - eval_with_args("(a a)", &vec!["(a a)"]), - @"(a a)" - ); - assert_display_snapshot!( - eval_with_args("b & (a b) ~ a", &vec!["(a a)"]), - @"a" - ); - assert_display_snapshot!( - eval_with_args("(z0 z0)", &vec!["(z1 z1)"]), - @"(a a)" - ); - assert_display_snapshot!( - eval_with_args("(* #1)", &vec!["(a a)"]), - @"#1" - ); - assert_display_snapshot!( - eval_with_args("(<+ a b> (a b))", &vec!["#1", "#2"]), - @"#3" - ); - assert_display_snapshot!( - eval_with_args("(<* a b> (a b))", &vec!["#2", "#3"]), - @"#6" - ); - assert_display_snapshot!( - eval_with_args("(<* a b> (a b))", &vec!["#2"]), - @"(<2* a> a)" - ); -} diff --git a/tests/cli.rs b/tests/cli.rs new file mode 100644 index 00000000..6af8c6b3 --- /dev/null +++ b/tests/cli.rs @@ -0,0 +1,185 @@ +//! Test the `hvmc` binary, including its CLI interface. + +use std::{ + error::Error, + io::Read, + process::{Command, ExitStatus, Stdio}, +}; + +use hvmc::{ + ast::{Net, Tree}, + host::Host, +}; +use insta::assert_display_snapshot; + +fn get_arithmetic_program_path() -> String { + return env!("CARGO_MANIFEST_DIR").to_owned() + "/examples/arithmetic.hvmc"; +} + +fn execute_hvmc(args: &[&str]) -> Result<(ExitStatus, String), Box> { + // Spawn the command + let mut child = + Command::new(env!("CARGO_BIN_EXE_hvmc")).args(args).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn()?; + + // Capture the output of the command + let mut stdout = child.stdout.take().ok_or("Couldn't capture stdout!")?; + let mut stderr = child.stderr.take().ok_or("Couldn't capture stderr!")?; + + // Wait for the command to finish and get the exit status + let status = child.wait()?; + + // Read the output + let mut output = String::new(); + stdout.read_to_string(&mut output)?; + stderr.read_to_string(&mut output)?; + + // Print the output of the command + Ok((status, output)) +} + +#[test] +fn test_cli_reduce() { + // Test normal-form expressions + assert_display_snapshot!( + execute_hvmc(&["reduce", "-m", "100M", "--", "#1"]).unwrap().1, + @"#1" + ); + // Test non-normal form expressions + assert_display_snapshot!( + execute_hvmc(&["reduce", "-m", "100M", "--", "a & #3 ~ <* #4 a>"]).unwrap().1, + @"#12" + ); + // Test multiple expressions + assert_display_snapshot!( + execute_hvmc(&["reduce", "-m", "100M", "--", "a & #3 ~ <* #4 a>", "a & #64 ~ "]).unwrap().1, + @"#12\n#32" + ); + + // Test loading file and reducing expression + let arithmetic_program = get_arithmetic_program_path(); + + assert_display_snapshot!( + execute_hvmc(&[ + "reduce", "-m", "100M", + &arithmetic_program, + "--", "a & @mul ~ (#3 (#4 a))" + ]).unwrap().1, + @"#12" + ); + + assert_display_snapshot!( + execute_hvmc(&[ + "reduce", "-m", "100M", + &arithmetic_program, + "--", "a & @mul ~ (#3 (#4 a))", "a & @div ~ (#64 (#2 a))" + ]).unwrap().1, + @"#12\n#32" + ) +} + +#[test] +fn test_cli_run_with_args() { + let arithmetic_program = get_arithmetic_program_path(); + + // Test simple program running + assert_display_snapshot!( + execute_hvmc(&[ + "run", "-m", "100M", + &arithmetic_program, + ]).unwrap().1, + @"({3 <% c d>} ({5 a c} [b d]))" + ); + + // Test partial argument passing + assert_display_snapshot!( + execute_hvmc(&[ + "run", "-m", "100M", + &arithmetic_program, + "#64" + ]).unwrap().1, + @"({5 <64/ a> <64% b>} [a b])" + ); + + // Test passing all arguments. + assert_display_snapshot!( + execute_hvmc(&[ + "run", "-m", "100M", + &arithmetic_program, + "#64", + "#3" + ]).unwrap().1, + @"[#21 #1]" + ); +} + +#[test] +fn test_cli_errors() { + // Test passing all arguments. + assert_display_snapshot!( + execute_hvmc(&[ + "run", "this-file-does-not-exist.hvmc" + ]).unwrap().1, + @r###" + Input file "this-file-does-not-exist.hvmc" not found + "### + ); + assert_display_snapshot!( + execute_hvmc(&[ + "reduce", "this-file-does-not-exist.hvmc" + ]).unwrap().1, + @r###" + Input file "this-file-does-not-exist.hvmc" not found + "### + ); +} + +#[test] +fn test_apply_tree() { + use hvmc::run; + fn eval_with_args(fun: &str, args: &[&str]) -> Net { + let area = run::Net::::init_heap(1 << 10); + + let mut fun: Net = fun.parse().unwrap(); + for arg in args { + let arg: Tree = arg.parse().unwrap(); + fun.apply_tree(arg) + } + // TODO: When feature/sc-472/argument-passing, use encode_net instead. + let host = Host::default(); + + let mut rnet = run::Net::::new(&area); + let root_port = run::Trg::port(run::Port::new_var(rnet.root.addr())); + host.encode_net(&mut rnet, root_port, &fun); + rnet.normal(); + let got_result = host.readback(&rnet); + got_result + } + assert_display_snapshot!( + eval_with_args("(a a)", &vec!["(a a)"]), + @"(a a)" + ); + assert_display_snapshot!( + eval_with_args("b & (a b) ~ a", &vec!["(a a)"]), + @"a" + ); + assert_display_snapshot!( + eval_with_args("(z0 z0)", &vec!["(z1 z1)"]), + @"(a a)" + ); + assert_display_snapshot!( + eval_with_args("(* #1)", &vec!["(a a)"]), + @"#1" + ); + assert_display_snapshot!( + eval_with_args("(<+ a b> (a b))", &vec!["#1", "#2"]), + @"#3" + ); + assert_display_snapshot!( + eval_with_args("(<* a b> (a b))", &vec!["#2", "#3"]), + @"#6" + ); + assert_display_snapshot!( + eval_with_args("(<* a b> (a b))", &vec!["#2"]), + @"(<2* a> a)" + ); +} diff --git a/tests/snapshots/tests__run@arithmetic.hvmc.snap b/tests/snapshots/tests__run@arithmetic.hvmc.snap new file mode 100644 index 00000000..9e687677 --- /dev/null +++ b/tests/snapshots/tests__run@arithmetic.hvmc.snap @@ -0,0 +1,12 @@ +--- +source: tests/tests.rs +expression: output +input_file: examples/arithmetic.hvmc +--- +({3 <% c d>} ({5 a c} [b d])) +RWTS : 7 +- ANNI : 4 +- COMM : 0 +- ERAS : 0 +- DREF : 3 +- OPER : 0