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

Propagate unsafety to definitions that depend on unsafe definitions #379

Merged
merged 12 commits into from
Jun 10, 2024
2 changes: 1 addition & 1 deletion 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 @@ -2,7 +2,7 @@
name = "hvm"
description = "A massively parallel, optimal functional runtime in Rust."
license = "Apache-2.0"
version = "2.0.19"
version = "2.0.20"
edition = "2021"
rust-version = "1.74"
build = "build.rs"
Expand Down
96 changes: 95 additions & 1 deletion src/ast.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use TSPL::{new_parser, Parser};
use highlight_error::highlight_error;
use crate::hvm;
use std::{collections::BTreeMap, fmt::{Debug, Display}};
use std::fmt::{Debug, Display};
use std::collections::{btree_map::Entry, BTreeMap, BTreeSet};

// Types
// -----
Expand Down Expand Up @@ -488,6 +489,19 @@ impl Tree {
},
}
}

pub fn direct_dependencies<'name>(&'name self) -> BTreeSet<&'name str> {
match self {
Tree::Ref { nam } => BTreeSet::from([nam.as_str()]),
Tree::Con { fst, snd } => &fst.direct_dependencies() | &snd.direct_dependencies(),
Tree::Dup { fst, snd } => &fst.direct_dependencies() | &snd.direct_dependencies(),
Tree::Opr { fst, snd } => &fst.direct_dependencies() | &snd.direct_dependencies(),
Tree::Swi { fst, snd } => &fst.direct_dependencies() | &snd.direct_dependencies(),
Tree::Num { val } => BTreeSet::new(),
Tree::Var { nam } => BTreeSet::new(),
Tree::Era => BTreeSet::new(),
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Writing a recursive function like this may not be the best idea since we still don't have a way to increase stack size as needed like in the hvm-64 repo. I think it's fine for now to add this function like this, since pretty much all the other functions related to trees are also recursive and if this one fails, they do too.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it becomes a problem we can just write this imperatively

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is quadratic, due to repeatedly cloning these btreesets; (@a (@b (@c (@d (@e ...))))) will take triangular time (which is asymptotically quadratic).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is quadratic, due to repeatedly cloning these btreesets; (@a (@b (@c (@d (@e ...))))) will take triangular time (which is asymptotically quadratic).

Thanks! I thought it would be similar to merging in merge sort, but I tested it and it really is slow. It should be better now.

}

impl Net {
Expand Down Expand Up @@ -523,6 +537,7 @@ impl Book {
}
}
let mut book = hvm::Book { defs: Vec::new() };
let mut lookup = BTreeMap::new();
edusporto marked this conversation as resolved.
Show resolved Hide resolved
for (fid, name) in &fid_to_name {
let ast_def = self.defs.get(name).expect("missing `@main` definition");
let mut def = hvm::Def {
Expand All @@ -535,7 +550,86 @@ impl Book {
};
ast_def.build(&mut def, &name_to_fid, &mut BTreeMap::new());
book.defs.push(def);
lookup.insert(name.clone(), book.defs.len() - 1);
}
self.propagate_safety(&mut book, &lookup);
return book;
}

/// Propagate unsafe definitions to those that reference them.
///
/// When calling this function, it is expected that definitions that are directly
/// unsafe are already marked as such in the `compiled_book`.
///
/// This does not completely solve the cloning safety in HVM. It only stops invalid
/// **global** definitions from being cloned, but local unsafe code can still be
/// cloned and can generate seemingly unexpected results, such as placing eraser
/// nodes in weird places. See HVM issue [#362](https://github.com/HigherOrderCO/HVM/issues/362)
/// for an example.
fn propagate_safety(&self, compiled_book: &mut hvm::Book, lookup: &BTreeMap<String, usize>) {
let rev_dependencies = self.direct_dependencies_reversed();
let mut visited: BTreeSet<&str> = BTreeSet::new();
let mut stack: Vec<&str> = Vec::new();

for (name, _) in self.defs.iter() {
let def = &compiled_book.defs[lookup[name]];
if !def.safe {
stack.push(&name);
}
}

while let Some(curr) = stack.pop() {
if visited.contains(curr) {
continue;
}
visited.insert(curr);
edusporto marked this conversation as resolved.
Show resolved Hide resolved

let def = &mut compiled_book.defs[lookup[curr]];
def.safe = false;

for &next in rev_dependencies[curr].iter() {
stack.push(next);
}
}
}

/// Calculates the dependencies of each definition but stores them reversed,
/// that is, if definition `A` requires `B`, `B: A` is in the return map.
/// This is used to propagate unsafe definitions to others that depend on them.
///
/// This solution has linear complexity on the number of definitions in the
/// book and the number of direct references in each definition, but it also
/// traverses each definition's trees entirely once.
///
/// Complexity: O(d*t + r)
/// - `d` is the number of definitions in the book
/// - `r` is the number of direct references in each definition
/// - `t` is the number of nodes in each tree
fn direct_dependencies_reversed<'name>(&'name self) -> BTreeMap<&'name str, BTreeSet<&'name str>> {
let mut result = BTreeMap::new();
for (name, _) in self.defs.iter() {
result.insert(name.as_str(), BTreeSet::new());
}

let mut process = |tree: &'name Tree, name: &'name str| {
for dependency in tree.direct_dependencies() {
match result.entry(dependency) {
Entry::Vacant(_) => panic!("global definition depends on undeclared reference"),
Entry::Occupied(mut entry) => {
// dependency => name
entry.get_mut().insert(name);
},
}
edusporto marked this conversation as resolved.
Show resolved Hide resolved
}
};

for (name, net) in self.defs.iter() {
process(&net.root, name);
for (_, r1, r2) in net.rbag.iter() {
process(r1, name);
process(r2, name);
}
}
result
}
}
31 changes: 31 additions & 0 deletions tests/programs/safety-check.hvm
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
@List/Cons = (a (b ((@List/Cons/tag (a (b c))) c)))

@List/Cons/tag = 1

@List/Nil = ((@List/Nil/tag a) a)

@List/Nil/tag = 0

@id = (a a)

@list = c
& @List/Cons ~ (1 (b c))
& @List/Cons ~ (2 (@List/Nil b))

@main = b
& @map ~ (@main__C0 (a b))
& @List/Cons ~ (@id (@List/Nil a))

@main__C0 = (a b)
& @map ~ (a (@list b))

@map = (a ((@map__C1 (a b)) b))

@map__C0 = (* (a (d ({(a b) c} f))))
& @List/Cons ~ (b (e f))
& @map ~ (c (d e))

@map__C1 = (?(((* @List/Nil) @map__C0) a) a)

// Test flags
@test-rust-only = 1
5 changes: 5 additions & 0 deletions tests/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ fn test_file(path: &Path) {
let rust_output = execute_hvm(&["run".as_ref(), path.as_os_str()], false).unwrap();
assert_snapshot!(rust_output);

if contents.contains("@test-rust-only = 1") {
println!("only testing rust implementation for {path:?}");
return;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the possibility to only run tests in the Rust implementation, since the C one doesn't throw an error in this same case. This is something to discuss - should the C and CUDA implementations also "panic" printing this error and stopping the program?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine as is, and is in line with the other test-io pattern.

println!(" testing {path:?}, C...");
let c_output = execute_hvm(&["run-c".as_ref(), path.as_os_str()], false).unwrap();
assert_eq!(c_output, rust_output, "{path:?}: C output does not match rust output");
Expand Down
2 changes: 1 addition & 1 deletion tests/snapshots/[email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ expression: rust_output
input_file: tests/programs/empty.hvm
---
exit status: 101
thread 'main' panicked at src/ast.rs:527:41:
thread 'main' panicked at src/ast.rs:542:41:
missing `@main` definition
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
6 changes: 6 additions & 0 deletions tests/snapshots/[email protected]
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
source: tests/run.rs
expression: rust_output
input_file: tests/programs/safety-check.hvm
---
ERROR: attempt to clone a non-affine global reference.
Loading