Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement remaining hvm1 builtins. #236

Merged
merged 6 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading