Skip to content

Commit

Permalink
Merge pull request #236 from HigherOrderCO/feature/sc-509/implement-h…
Browse files Browse the repository at this point in the history
…vm1-builtins

Implement remaining `hvm1` builtins.
  • Loading branch information
imaqtkatt authored Mar 18, 2024
2 parents 81d6486 + 4572062 commit 90f718d
Show file tree
Hide file tree
Showing 27 changed files with 566 additions and 113 deletions.
18 changes: 9 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ cli = ["dep:clap"]
chumsky = { version = "= 1.0.0-alpha.4", features = ["label"] }
clap = { version = "4.4.1", features = ["derive"], optional = true }
highlight_error = "0.1.1"
hvm-core = { git = "https://github.com/HigherOrderCO/hvm-core" }
hvm-core = { git = "https://github.com/HigherOrderCO/hvm-core.git", branch = "feature/sc-506/smart-pointer-def-api" }
indexmap = "2.2.3"
interner = "0.2.1"
itertools = "0.11.0"
Expand Down
69 changes: 69 additions & 0 deletions docs/builtin-defs.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

HVM-lang includes some built-in definitions that can make certain tasks easier, such as debugging, benchmarking, etc. These definitions are "special", and can't be implemented using regular HVM code.

### On errors.

Some of the functions may error. `hvm-lang` defines a builtin `Result` ADT which looks like this:

```
data Result = (Ok val) | (Err val)
```

This allows hvm-lang to do basic error handling. Right now, `val` is meant to be opaque; `val`'s type is not stable and might change from one update to another.

## `HVM.log`

Reads back its first argument and displays it on stdout, then returns its second argument.
Expand Down Expand Up @@ -52,6 +62,65 @@ This can be very useful, for example, to print strings with newlines in multiple

When the first argument is not a string `HVM.print` returns the second argument and no side effects are produced (In other words, it fails silently).

## `HVM.query`

`HVM.query` reads a line from standard input, and calls its argument with a `Result` that might contain a string containing the user's input. It expects the standard input to be valid utf-8.

Here's an example program using `HVM.query`
```rs
Join (String.nil) x = x
Join (String.cons head tail) x = (String.cons head (Join tail x))
main = (((HVM.print "What's your name?") HVM.query) λresult match result {
(Result.ok name): (HVM.print (Join "Hi, " (Join name "!")) *)
(Result.err err): err
})
```
This program also shows using the return value of `HVM.print` (which is the identity function) to block `HVM.query` from reducing too early. If we used a naive version of the program, which is this:
```rs
main = (HVM.print "What's your name?" (HVM.query λresult match result {
(Result.ok name): (HVM.print (Join "Hi, " (Join name "!")) *)
(Result.err err): err
}))
```
We would get asked our name after typing it in.

## `HVM.store`

`HVM.store` writes a string to a file, and calls its return value with a `Result` (the `Result`, if it's `Result.ok`, contains `ERA`).

Example:

```rs
main = (HVM.store "file.txt" "These are the file contents" λres match res {
(Ok *): "Return value of program"
(Err err): err
})
```

## `HVM.load`

`HVM.load`, given a filename, reads the file and passes a `Result` that might contain the contents of the file to its argument

```rs
Join (String.nil) x = x
Join (String.cons head tail) x = (String.cons head (Join tail x))
main = (HVM.load "file.txt" λres match res {
(Result.ok contents): (HVM.print (Join "File contents: " contents) "Program return value")
(Result.err err): err
})
```

## `HVM.exit`

`HVM.exit` terminates the processes with the status code determined by its argument.

```rs
main = (HVM.exit #42)
// Outputs nothing, but `hvml`'s status code will be 42.
```

On failure, it terminates the process with a -1 exit status code.

## `HVM.black_box`

`HVM.black_box` is simply the identity function, but it does not get [pre-reduced](compiler-options.md#pre-reduce). This makes it possible to prevent some redexes from getting pre-reduced.
Expand Down
72 changes: 72 additions & 0 deletions src/builtins/exit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use std::sync::{Arc, Mutex};

use hvmc::{
host::Host,
run::{Def, LabSet, Mode, Net, Port, Tag, Wire},
stdlib::{ArcDef, AsArcDef, HostedDef},
};

use crate::builtins::util::AsDefFunction;

use super::util::FunctionLikeHosted;

pub(crate) fn add_exit_def(host: Arc<Mutex<Host>>) {
/// `HVM.exit`.
/// Implements the following reduction rule
///
/// ```txt
/// ExitDef ~ (a b)
/// ---
/// ExitDefGetStatus ~ a & <dangling> ~ b
/// ```
struct ExitDef;

impl AsDefFunction for ExitDef {
fn call<M: Mode>(&self, net: &mut Net<M>, input: Wire, output: Wire) {
// Purposefully deadlock "output" to prevent further reduction
drop(output);

net.link_wire_port(input, ArcDef::new_arc_port(LabSet::ALL, ExitDefGetStatus));
}
}

/// Def that scans for a number, and exits the program with that number as the exit code
///
/// ```txt
/// ExitDefGetStatus ~ {lab a b} // also works on match and op
/// ---
/// ExitDefGetStatus ~ a & ExitDefGetStatus ~ b
///
/// ExitDefGetStatus ~ #num
/// ---
/// process::exit(num)
///
/// ExitDefGetStatus ~ other
/// ---
/// process::exit(-1)
/// ```
struct ExitDefGetStatus;
impl AsArcDef for ExitDefGetStatus {
fn call<M: Mode>(slf: Arc<Def<Self>>, net: &mut Net<M>, port: Port) {
match port.tag() {
Tag::Red | Tag::Var | Tag::Num => {
std::process::exit(port.num().try_into().unwrap_or(-1));
}
Tag::Ref => {
std::process::exit(-1);
}
_ => {
// Commute
let node = port.consume_node();
net.link_wire_port(node.p1, ArcDef::to_port(slf.clone()));
net.link_wire_port(node.p2, ArcDef::to_port(slf));
}
}
}
}

host
.lock()
.unwrap()
.insert_def("HVM.exit", unsafe { HostedDef::new_hosted(LabSet::ALL, FunctionLikeHosted(ExitDef)) });
}
171 changes: 171 additions & 0 deletions src/builtins/fs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
use std::sync::{Arc, Mutex};

use hvmc::{
ast, dispatch_dyn_net,
host::Host,
run::{LabSet, Port, Trg, Wire},
stdlib::{ArcDef, HostedDef},
};
use term_to_net::term_to_compat_net;

use crate::{
builtins::util::{AsDefFunction, FunctionLike, FunctionLikeHosted},
net::net_to_hvmc::net_to_hvmc,
readback_hvmc,
term::{
term_to_net::{self, Labels},
AdtEncoding, Book, Term,
},
};

#[derive(Clone)]
struct ReadbackData {
book: Arc<Book>,
host: Arc<Mutex<Host>>,
labels: Arc<Labels>,
adt_encoding: AdtEncoding,
}

const VICIOUS_CIRCLE_MSG: &str = "Found vicious circle";
const FILENAME_NOT_VALID_MSG: &str = "Filename is not valid string.";
const CONTENTS_NOT_VALID_MSG: &str = "Content is not valid string.";
const FS_ERROR_MSG: &str = "Filesystem error: ";
const INVALID_UTF8_MSG: &str = "UTF-8 error: ";

/// Adds the filesystem definitions (`HVM.store` and `HVM.load`) to the book
pub(crate) fn add_fs_defs(
book: Arc<Book>,
host: Arc<Mutex<Host>>,
labels: Arc<Labels>,
adt_encoding: AdtEncoding,
) {
#[derive(Clone)]
struct Fs0 {
readback_data: ReadbackData,
save: bool,
}
impl AsDefFunction for Fs0 {
fn call<M: hvmc::run::Mode>(&self, net: &mut hvmc::run::Net<M>, input: Wire, output: Wire) {
let (wire, port) = net.create_wire();
let slf = self.clone();
let readback_node = hvmc::stdlib::readback(slf.readback_data.host.clone(), port, move |net, tree| {
dispatch_dyn_net!(net => {
net.link_wire_port(wire, Port::ERA);
let (term, _errs) = readback_hvmc(&ast::Net { root: tree,redexes: vec![]} , &slf.readback_data.book, &slf.readback_data.labels, false, slf.readback_data.adt_encoding);
let filename = if let Term::Str { ref val } = term {
Some(val.to_string())
} else {
None
};
net.link_wire_port(output, ArcDef::new_arc_port(LabSet::ALL, FunctionLike(Fs1 { readback_data: slf.readback_data, save: slf.save, filename })));
})
});
net.link_wire_port(input, readback_node);
}
}

#[derive(Clone)]
struct Fs1 {
readback_data: ReadbackData,
save: bool,
filename: Option<String>,
}
impl AsDefFunction for Fs1 {
fn call<M: hvmc::run::Mode>(&self, net: &mut hvmc::run::Net<M>, input: Wire, output: Wire) {
if self.save {
let (wire, port) = net.create_wire();
let slf = self.clone();
let mut labels = (*self.readback_data.labels).clone();
let host = self.readback_data.host.clone();
let readback_node = hvmc::stdlib::readback(
self.readback_data.host.clone(),
port,
move |net, tree| {
dispatch_dyn_net!(net => {
let (term, _errs) = readback_hvmc(&ast::Net { root: tree,redexes: vec![]} , &slf.readback_data.book, &slf.readback_data.labels, false, slf.readback_data.adt_encoding);
let contents = if let Term::Str { ref val } = term {
Some(val.to_string())
} else {
None
};
// Save file
let result = match (slf.filename, contents) {
(None, _) => {
Term::encode_err(Term::encode_str(FILENAME_NOT_VALID_MSG))
},
(_, None) => {
Term::encode_err(Term::encode_str(CONTENTS_NOT_VALID_MSG))
},
(Some(filename), Some(contents)) => {
match std::fs::write(filename, contents) {
Ok(_) => Term::encode_ok(Term::Era),
Err(e) => Term::encode_err(Term::encode_str(&format!("{FS_ERROR_MSG}{e}"))),
}
},
};
let result = term_to_compat_net(&result, &mut labels);
match net_to_hvmc(&result) {
Ok(result) => {
// Return λx (x result)
let app = net.create_node(hvmc::run::Tag::Ctr, 0);
let lam = net.create_node(hvmc::run::Tag::Ctr, 0);
host.lock().unwrap().encode_net(net, Trg::port(app.p1), &result);
net.link_wire_port(wire, Port::ERA);
net.link_port_port(app.p0, lam.p1);
net.link_port_port(app.p2, lam.p2);

net.link_wire_port(output, lam.p0);
},
Err(_) => {
// If this happens, we can't even report an error to
// the hvm program, so simply print an error, and plug in an ERA
// The other option would be panicking.
eprintln!("{VICIOUS_CIRCLE_MSG}");
net.link_wire_port(output, Port::ERA);
},
}
})
},
);
net.link_wire_port(input, readback_node);
} else {
let app = net.create_node(hvmc::run::Tag::Ctr, 0);
let result = self
.filename
.as_ref()
.ok_or(FILENAME_NOT_VALID_MSG.to_owned())
.and_then(|filename| std::fs::read(filename).map_err(|e| format!("{FS_ERROR_MSG}{e}")))
.and_then(|x| {
std::str::from_utf8(&x).map(|x| x.to_string()).map_err(|e| format!("{INVALID_UTF8_MSG}{e}"))
});
let result = match result {
Ok(s) => Term::encode_ok(Term::encode_str(&s)),
Err(s) => Term::encode_err(Term::encode_str(&s)),
};
let mut labels = (*self.readback_data.labels).clone();
let result = term_to_compat_net(&result, &mut labels);
if let Ok(result) = net_to_hvmc(&result) {
self.readback_data.host.lock().unwrap().encode_net(net, Trg::port(app.p1), &result);
} else {
eprintln!("{VICIOUS_CIRCLE_MSG}");
net.link_port_port(Port::ERA, app.p1);
}
net.link_wire_port(output, app.p2);
net.link_wire_port(input, app.p0);
}
}
}
let readback_data = ReadbackData { book, host: host.clone(), labels, adt_encoding };
host.lock().unwrap().insert_def("HVM.store", unsafe {
HostedDef::new_hosted(
LabSet::ALL,
FunctionLikeHosted(Fs0 { readback_data: readback_data.clone(), save: true }),
)
});
host.lock().unwrap().insert_def("HVM.load", unsafe {
HostedDef::new_hosted(
LabSet::ALL,
FunctionLikeHosted(Fs0 { readback_data: readback_data.clone(), save: false }),
)
});
}
Loading

0 comments on commit 90f718d

Please sign in to comment.