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

feat: noir-wasm takes dependency graph #3213

Merged
merged 10 commits into from
Oct 24, 2023
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
10 changes: 7 additions & 3 deletions .github/workflows/test-noir_wasm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,16 @@ jobs:
name: nargo
path: ./nargo

- name: Compile test program with Nargo CLI
working-directory: ./compiler/wasm/noir-script
- name: Compile fixtures with Nargo CLI
working-directory: ./compiler/wasm/fixtures
run: |
nargo_binary=${{ github.workspace }}/nargo/nargo
chmod +x $nargo_binary
$nargo_binary compile
for dir in $(ls -d */); do
pushd $dir/noir-script
$nargo_binary compile
popd
done

- name: Install Yarn dependencies
uses: ./.github/actions/setup
Expand Down
1 change: 1 addition & 0 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 @@ -77,7 +77,7 @@ dirs = "4"
lsp-types = "0.94"
serde = { version = "1.0.136", features = ["derive"] }
serde_json = "1.0"
smol_str = "0.1.17"
smol_str = { version = "0.1.17", features = ["serde"] }
thiserror = "1.0.21"
toml = "0.7.2"
tower = "0.4"
Expand Down
1 change: 1 addition & 0 deletions compiler/noirc_frontend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ chumsky.workspace = true
thiserror.workspace = true
smol_str.workspace = true
serde_json.workspace = true
serde.workspace = true
rustc-hash = "1.1.0"
small-ord-set = "0.1.3"
regex = "1.9.1"
Expand Down
8 changes: 7 additions & 1 deletion compiler/noirc_frontend/src/graph/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::{fmt::Display, str::FromStr};

use fm::FileId;
use rustc_hash::{FxHashMap, FxHashSet};
use serde::{Deserialize, Serialize};
use smol_str::SmolStr;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
Expand All @@ -32,7 +33,7 @@ impl CrateId {
}
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
pub struct CrateName(SmolStr);

impl CrateName {
Expand Down Expand Up @@ -90,6 +91,11 @@ mod crate_name {
assert!(!CrateName::is_valid_name(&bad_char_string));
}
}

#[test]
fn it_rejects_bad_crate_names_when_deserializing() {
assert!(serde_json::from_str::<CrateName>("bad-name").is_err());
}
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
Expand Down
8 changes: 8 additions & 0 deletions compiler/wasm/fixtures/deps/lib-a/Nargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name="lib_a"
type="lib"
authors = [""]
compiler_version = "0.1"

[dependencies]
lib_b = { path = "../lib-b" }
7 changes: 7 additions & 0 deletions compiler/wasm/fixtures/deps/lib-a/src/lib.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

use dep::lib_b::assert_non_zero;

pub fn divide(a: u64, b: u64) -> u64 {
assert_non_zero(b);
a / b
}
7 changes: 7 additions & 0 deletions compiler/wasm/fixtures/deps/lib-b/Nargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name="lib_b"
type="lib"
authors = [""]
compiler_version = "0.1"

[dependencies]
4 changes: 4 additions & 0 deletions compiler/wasm/fixtures/deps/lib-b/src/lib.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

pub fn assert_non_zero(x: u64) {
assert(x != 0);
}
8 changes: 8 additions & 0 deletions compiler/wasm/fixtures/deps/noir-script/Nargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name="noir_wasm_testing"
type="bin"
authors = [""]
compiler_version = "0.1"

[dependencies]
lib_a = { path="../lib-a" }
4 changes: 4 additions & 0 deletions compiler/wasm/fixtures/deps/noir-script/src/main.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
use dep::lib_a::divide;
fn main(x : u64, y : pub u64) {
divide(x, y);
}
208 changes: 169 additions & 39 deletions compiler/wasm/src/compile.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use fm::FileManager;
use gloo_utils::format::JsValueSerdeExt;
use js_sys::Array;
use js_sys::Object;
use nargo::artifacts::{
contract::{PreprocessedContract, PreprocessedContractFunction},
program::PreprocessedProgram,
Expand All @@ -9,29 +9,54 @@ use noirc_driver::{
add_dep, compile_contract, compile_main, prepare_crate, prepare_dependency, CompileOptions,
CompiledContract, CompiledProgram,
};
use noirc_frontend::{graph::CrateGraph, hir::Context};
use std::path::Path;
use noirc_frontend::{
graph::{CrateGraph, CrateId, CrateName},
hir::Context,
};
use serde::Deserialize;
use std::{collections::HashMap, path::Path};
use wasm_bindgen::prelude::*;

use crate::errors::JsCompileError;
use crate::errors::{CompileError, JsCompileError};

const BACKEND_IDENTIFIER: &str = "acvm-backend-barretenberg";

#[wasm_bindgen(typescript_custom_section)]
const DEPENDENCY_GRAPH: &'static str = r#"
export type DependencyGraph = {
root_dependencies: readonly string[];
library_dependencies: Readonly<Record<string, readonly string[]>>;
TomAFrench marked this conversation as resolved.
Show resolved Hide resolved
}
"#;

#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(extends = Array, js_name = "StringArray", typescript_type = "string[]")]
#[wasm_bindgen(extends = Object, js_name = "DependencyGraph", typescript_type = "DependencyGraph")]
#[derive(Clone, Debug, PartialEq, Eq)]
pub type StringArray;
pub type JsDependencyGraph;
}

#[derive(Deserialize)]
struct DependencyGraph {
root_dependencies: Vec<CrateName>,
library_dependencies: HashMap<CrateName, Vec<CrateName>>,
}

#[wasm_bindgen]
pub fn compile(
entry_point: String,
contracts: Option<bool>,
dependencies: Option<StringArray>,
dependency_graph: Option<JsDependencyGraph>,
) -> Result<JsValue, JsCompileError> {
console_error_panic_hook::set_once();

let dependency_graph: DependencyGraph = if let Some(dependency_graph) = dependency_graph {
<JsValue as JsValueSerdeExt>::into_serde(&JsValue::from(dependency_graph))
.map_err(|err| err.to_string())?
} else {
DependencyGraph { root_dependencies: vec![], library_dependencies: HashMap::new() }
};

let root = Path::new("/");
let fm = FileManager::new(root, Box::new(get_non_stdlib_asset));
let graph = CrateGraph::default();
Expand All @@ -40,12 +65,7 @@ pub fn compile(
let path = Path::new(&entry_point);
let crate_id = prepare_crate(&mut context, path);

let dependencies: Vec<String> = dependencies
.map(|array| array.iter().map(|element| element.as_string().unwrap()).collect())
.unwrap_or_default();
for dependency in dependencies {
add_noir_lib(&mut context, dependency.as_str());
}
process_dependency_graph(&mut context, dependency_graph);

let compile_options = CompileOptions::default();

Expand All @@ -57,7 +77,11 @@ pub fn compile(
if contracts.unwrap_or_default() {
let compiled_contract = compile_contract(&mut context, crate_id, &compile_options)
.map_err(|errs| {
JsCompileError::new("Failed to compile contract", errs, &context.file_manager)
CompileError::with_file_diagnostics(
"Failed to compile contract",
errs,
&context.file_manager,
)
})?
.0;

Expand All @@ -71,7 +95,11 @@ pub fn compile(
} else {
let compiled_program = compile_main(&mut context, crate_id, &compile_options, None, true)
.map_err(|errs| {
JsCompileError::new("Failed to compile program", errs, &context.file_manager)
CompileError::with_file_diagnostics(
"Failed to compile program",
errs,
&context.file_manager,
)
})?
.0;

Expand All @@ -85,34 +113,38 @@ pub fn compile(
}
}

fn add_noir_lib(context: &mut Context, library_name: &str) {
let path_to_lib = Path::new(&library_name).join("lib.nr");
let library_crate_id = prepare_dependency(context, &path_to_lib);

add_dep(context, *context.root_crate_id(), library_crate_id, library_name.parse().unwrap());

// TODO: Remove this code that attaches every crate to every other crate as a dependency
let root_crate_id = context.root_crate_id();
let stdlib_crate_id = context.stdlib_crate_id();
let other_crate_ids: Vec<_> = context
.crate_graph
.iter_keys()
.filter(|crate_id| {
// We don't want to attach this crate to itself or stdlib, nor re-attach it to the root crate
crate_id != &library_crate_id
&& crate_id != root_crate_id
&& crate_id != stdlib_crate_id
})
.collect();
fn process_dependency_graph(context: &mut Context, dependency_graph: DependencyGraph) {
let mut crate_names: HashMap<&CrateName, CrateId> = HashMap::new();

for crate_id in other_crate_ids {
context
.crate_graph
.add_dep(crate_id, library_name.parse().unwrap(), library_crate_id)
.unwrap_or_else(|_| panic!("ICE: Cyclic error triggered by {library_name} library"));
for lib in &dependency_graph.root_dependencies {
alexghr marked this conversation as resolved.
Show resolved Hide resolved
let crate_id = add_noir_lib(context, lib);
crate_names.insert(lib, crate_id);

add_dep(context, *context.root_crate_id(), crate_id, lib.clone());
TomAFrench marked this conversation as resolved.
Show resolved Hide resolved
}

for (lib_name, dependencies) in &dependency_graph.library_dependencies {
TomAFrench marked this conversation as resolved.
Show resolved Hide resolved
// first create the library crate if needed
// this crate might not have been registered yet because of the order of the HashMap
// e.g. {root: [lib1], libs: { lib2 -> [lib3], lib1 -> [lib2] }}
let crate_id =
*crate_names.entry(lib_name).or_insert_with(|| add_noir_lib(context, lib_name));

for dependency_name in dependencies {
let dep_crate_id: &CrateId = crate_names
.entry(dependency_name)
.or_insert_with(|| add_noir_lib(context, dependency_name));

add_dep(context, crate_id, *dep_crate_id, dependency_name.clone());
}
}
}

fn add_noir_lib(context: &mut Context, library_name: &CrateName) -> CrateId {
let path_to_lib = Path::new(&library_name.to_string()).join("lib.nr");
prepare_dependency(context, &path_to_lib)
}

fn preprocess_program(program: CompiledProgram) -> PreprocessedProgram {
PreprocessedProgram {
hash: program.hash,
Expand Down Expand Up @@ -166,3 +198,101 @@ cfg_if::cfg_if! {
}
}
}

#[cfg(test)]
mod test {
use fm::FileManager;
use noirc_driver::prepare_crate;
use noirc_frontend::{
graph::{CrateGraph, CrateName},
hir::Context,
};

use super::{process_dependency_graph, DependencyGraph};
use std::{collections::HashMap, path::Path};

fn mock_get_non_stdlib_asset(_path_to_file: &Path) -> std::io::Result<String> {
Ok("".to_string())
}

fn setup_test_context() -> Context {
let fm = FileManager::new(Path::new("/"), Box::new(mock_get_non_stdlib_asset));
let graph = CrateGraph::default();
let mut context = Context::new(fm, graph);

prepare_crate(&mut context, Path::new("/main.nr"));

context
}

fn crate_name(name: &str) -> CrateName {
name.parse().unwrap()
}

#[test]
fn test_works_with_empty_dependency_graph() {
let mut context = setup_test_context();
let dependency_graph =
DependencyGraph { root_dependencies: vec![], library_dependencies: HashMap::new() };

process_dependency_graph(&mut context, dependency_graph);

// one stdlib + one root crate
assert_eq!(context.crate_graph.number_of_crates(), 2);
}

#[test]
fn test_works_with_root_dependencies() {
let mut context = setup_test_context();
let dependency_graph = DependencyGraph {
root_dependencies: vec![crate_name("lib1")],
library_dependencies: HashMap::new(),
};

process_dependency_graph(&mut context, dependency_graph);

assert_eq!(context.crate_graph.number_of_crates(), 3);
}

#[test]
fn test_works_with_duplicate_root_dependencies() {
let mut context = setup_test_context();
let dependency_graph = DependencyGraph {
root_dependencies: vec![crate_name("lib1"), crate_name("lib1")],
library_dependencies: HashMap::new(),
};

process_dependency_graph(&mut context, dependency_graph);

assert_eq!(context.crate_graph.number_of_crates(), 3);
TomAFrench marked this conversation as resolved.
Show resolved Hide resolved
}

#[test]
fn test_works_with_transitive_dependencies() {
let mut context = setup_test_context();
let dependency_graph = DependencyGraph {
root_dependencies: vec![crate_name("lib1")],
library_dependencies: HashMap::from([
(crate_name("lib1"), vec![crate_name("lib2")]),
(crate_name("lib2"), vec![crate_name("lib3")]),
]),
};

process_dependency_graph(&mut context, dependency_graph);

assert_eq!(context.crate_graph.number_of_crates(), 5);
}

#[test]
fn test_works_with_missing_dependencies() {
let mut context = setup_test_context();
let dependency_graph = DependencyGraph {
root_dependencies: vec![crate_name("lib1")],
library_dependencies: HashMap::from([(crate_name("lib2"), vec![crate_name("lib3")])]),
};

process_dependency_graph(&mut context, dependency_graph);

assert_eq!(context.crate_graph.number_of_crates(), 5);
}
}
Loading