From dafda21f65d98ffe903eb730740ae5a17c661e2e Mon Sep 17 00:00:00 2001 From: Tom French Date: Tue, 3 Oct 2023 21:50:13 +0100 Subject: [PATCH 1/2] feat(wasm)!: improve and simplify wasm compiler interface --- Cargo.lock | 1 + Cargo.toml | 1 + acvm-repo/acvm_js/Cargo.toml | 2 +- .../barretenberg_blackbox_solver/Cargo.toml | 2 +- .../test/browser/compile_prove_verify.test.ts | 3 +- .../test/node/smart_contract_verifier.test.ts | 14 +- compiler/wasm/Cargo.toml | 1 + compiler/wasm/noir-script/src/main.nr | 2 +- compiler/wasm/src/compile.rs | 140 +++++++----------- compiler/wasm/src/errors.rs | 29 ++++ compiler/wasm/src/lib.rs | 3 +- compiler/wasm/test/shared.ts | 4 +- tooling/noirc_abi_wasm/Cargo.toml | 2 +- 13 files changed, 94 insertions(+), 110 deletions(-) create mode 100644 compiler/wasm/src/errors.rs diff --git a/Cargo.lock b/Cargo.lock index 347e0b0521f..ac69607c4d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2406,6 +2406,7 @@ dependencies = [ "fm", "getrandom", "gloo-utils", + "js-sys", "log", "nargo", "noirc_driver", diff --git a/Cargo.toml b/Cargo.toml index dcd2c20d195..80848fbcf1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,6 +80,7 @@ tower = "0.4" url = "2.2.0" wasm-bindgen = { version = "=0.2.86", features = ["serde-serialize"] } wasm-bindgen-test = "0.3.33" +js-sys = "0.3.62" base64 = "0.21.2" fxhash = "0.2.1" acir = { path = "acvm-repo/acir", default-features = false } diff --git a/acvm-repo/acvm_js/Cargo.toml b/acvm-repo/acvm_js/Cargo.toml index 28d99d243d1..f977eb5234f 100644 --- a/acvm-repo/acvm_js/Cargo.toml +++ b/acvm-repo/acvm_js/Cargo.toml @@ -28,7 +28,7 @@ log = "0.4.17" wasm-logger = "0.2.0" console_error_panic_hook = "0.1.7" gloo-utils = { version = "0.1", features = ["serde"] } -js-sys = "0.3.62" +js-sys.workspace = true const-str = "0.5.5" [build-dependencies] diff --git a/acvm-repo/barretenberg_blackbox_solver/Cargo.toml b/acvm-repo/barretenberg_blackbox_solver/Cargo.toml index a76d6313b4e..acecb24c142 100644 --- a/acvm-repo/barretenberg_blackbox_solver/Cargo.toml +++ b/acvm-repo/barretenberg_blackbox_solver/Cargo.toml @@ -32,7 +32,7 @@ wasmer = { version = "3.3", default-features = false, features = [ getrandom = { version = "0.2", features = ["js"] } wasm-bindgen-futures = "0.4.36" -js-sys = "0.3.62" +js-sys.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] getrandom = "0.2" diff --git a/compiler/integration-tests/test/browser/compile_prove_verify.test.ts b/compiler/integration-tests/test/browser/compile_prove_verify.test.ts index 43770478ae9..a6627e9237c 100644 --- a/compiler/integration-tests/test/browser/compile_prove_verify.test.ts +++ b/compiler/integration-tests/test/browser/compile_prove_verify.test.ts @@ -38,7 +38,8 @@ async function getCircuit(noirSource: string) { return noirSource; }); - return compile({}); + // We're ignoring this in the resolver but pass in something sensible. + return compile('/main.nr'); } test_cases.forEach((testInfo) => { diff --git a/compiler/integration-tests/test/node/smart_contract_verifier.test.ts b/compiler/integration-tests/test/node/smart_contract_verifier.test.ts index 6c15dc063d6..5c888cd12d1 100644 --- a/compiler/integration-tests/test/node/smart_contract_verifier.test.ts +++ b/compiler/integration-tests/test/node/smart_contract_verifier.test.ts @@ -25,10 +25,6 @@ const test_cases = [ }, ]; -async function getCircuit(entry_point: string) { - return compile({ entry_point }); -} - test_cases.forEach((testInfo) => { const test_name = testInfo.case.split('/').pop(); @@ -38,15 +34,7 @@ test_cases.forEach((testInfo) => { const noir_source_path = resolve(`${base_relative_path}/${test_case}/src/main.nr`); - let compile_output; - try { - compile_output = await getCircuit(noir_source_path); - - expect(await compile_output, 'Compile output ').to.be.an('object'); - } catch (e) { - expect(e, 'Compilation Step').to.not.be.an('error'); - throw e; - } + const compile_output = compile(noir_source_path); const noir_program = { bytecode: compile_output.circuit, abi: compile_output.abi }; const backend = new BarretenbergBackend(noir_program); diff --git a/compiler/wasm/Cargo.toml b/compiler/wasm/Cargo.toml index ebcfca8b49b..47a0acdf8ac 100644 --- a/compiler/wasm/Cargo.toml +++ b/compiler/wasm/Cargo.toml @@ -19,6 +19,7 @@ noirc_driver.workspace = true noirc_frontend.workspace = true wasm-bindgen.workspace = true serde.workspace = true +js-sys.workspace = true cfg-if.workspace = true console_error_panic_hook = "0.1.7" diff --git a/compiler/wasm/noir-script/src/main.nr b/compiler/wasm/noir-script/src/main.nr index 7f3767f4a48..36fcc1916f5 100644 --- a/compiler/wasm/noir-script/src/main.nr +++ b/compiler/wasm/noir-script/src/main.nr @@ -1,3 +1,3 @@ fn main(x : u64, y : pub u64) { assert(x < y); -} \ No newline at end of file +} diff --git a/compiler/wasm/src/compile.rs b/compiler/wasm/src/compile.rs index dde2310118f..f06e51e4da8 100644 --- a/compiler/wasm/src/compile.rs +++ b/compiler/wasm/src/compile.rs @@ -1,58 +1,72 @@ use fm::FileManager; use gloo_utils::format::JsValueSerdeExt; -use log::debug; +use js_sys::Array; use noirc_driver::{ add_dep, compile_contract, compile_main, prepare_crate, prepare_dependency, CompileOptions, }; use noirc_frontend::{graph::CrateGraph, hir::Context}; -use serde::{Deserialize, Serialize}; use std::path::Path; use wasm_bindgen::prelude::*; -#[derive(Debug, Serialize, Deserialize)] -pub struct WASMCompileOptions { - #[serde(default = "default_entry_point")] +use crate::errors::JsCompileError; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(extends = Array, js_name = "StringArray", typescript_type = "string[]")] + #[derive(Clone, Debug, PartialEq, Eq)] + pub type StringArray; +} + +#[wasm_bindgen] +pub fn compile( entry_point: String, + contracts: Option, + dependencies: Option, +) -> Result { + console_error_panic_hook::set_once(); - #[serde(default = "default_circuit_name")] - circuit_name: String, + let root = Path::new("/"); + let fm = FileManager::new(root, Box::new(get_non_stdlib_asset)); + let graph = CrateGraph::default(); + let mut context = Context::new(fm, graph); - // Compile each contract function used within the program - #[serde(default = "bool::default")] - contracts: bool, + let path = Path::new(&entry_point); + let crate_id = prepare_crate(&mut context, path); - #[serde(default)] - compile_options: CompileOptions, + let dependencies: Vec = 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()); + } - #[serde(default)] - optional_dependencies_set: Vec, + let compile_options = CompileOptions::default(); - #[serde(default = "default_log_level")] - log_level: String, -} + // For now we default to plonk width = 3, though we can add it as a parameter + let np_language = acvm::Language::PLONKCSat { width: 3 }; + #[allow(deprecated)] + let is_opcode_supported = acvm::pwg::default_is_opcode_supported(np_language); -fn default_log_level() -> String { - String::from("info") -} + if contracts.unwrap_or_default() { + let compiled_contract = compile_contract(&mut context, crate_id, &compile_options) + .map_err(|_| JsCompileError::new("Failed to compile contract".to_string()))? + .0; -fn default_circuit_name() -> String { - String::from("contract") -} + let optimized_contract = + nargo::ops::optimize_contract(compiled_contract, np_language, &is_opcode_supported) + .expect("Contract optimization failed"); -fn default_entry_point() -> String { - String::from("main.nr") -} + Ok(::from_serde(&optimized_contract).unwrap()) + } else { + let compiled_program = compile_main(&mut context, crate_id, &compile_options, None, true) + .map_err(|_| JsCompileError::new("Failed to compile program".to_string()))? + .0; -impl Default for WASMCompileOptions { - fn default() -> Self { - Self { - entry_point: default_entry_point(), - circuit_name: default_circuit_name(), - log_level: default_log_level(), - contracts: false, - compile_options: CompileOptions::default(), - optional_dependencies_set: vec![], - } + let optimized_program = + nargo::ops::optimize_program(compiled_program, np_language, &is_opcode_supported) + .expect("Program optimization failed"); + + Ok(::from_serde(&optimized_program).unwrap()) } } @@ -84,60 +98,6 @@ fn add_noir_lib(context: &mut Context, library_name: &str) { } } -#[wasm_bindgen] -pub fn compile(args: JsValue) -> JsValue { - console_error_panic_hook::set_once(); - - let options: WASMCompileOptions = if args.is_undefined() || args.is_null() { - debug!("Initializing compiler with default values."); - WASMCompileOptions::default() - } else { - JsValueSerdeExt::into_serde(&args).expect("Could not deserialize compile arguments") - }; - - debug!("Compiler configuration {:?}", &options); - - let root = Path::new("/"); - let fm = FileManager::new(root, Box::new(get_non_stdlib_asset)); - let graph = CrateGraph::default(); - let mut context = Context::new(fm, graph); - - let path = Path::new(&options.entry_point); - let crate_id = prepare_crate(&mut context, path); - - for dependency in options.optional_dependencies_set { - add_noir_lib(&mut context, dependency.as_str()); - } - - // For now we default to plonk width = 3, though we can add it as a parameter - let np_language = acvm::Language::PLONKCSat { width: 3 }; - #[allow(deprecated)] - let is_opcode_supported = acvm::pwg::default_is_opcode_supported(np_language); - - if options.contracts { - let compiled_contract = compile_contract(&mut context, crate_id, &options.compile_options) - .expect("Contract compilation failed") - .0; - - let optimized_contract = - nargo::ops::optimize_contract(compiled_contract, np_language, &is_opcode_supported) - .expect("Contract optimization failed"); - - ::from_serde(&optimized_contract).unwrap() - } else { - let compiled_program = - compile_main(&mut context, crate_id, &options.compile_options, None, true) - .expect("Compilation failed") - .0; - - let optimized_program = - nargo::ops::optimize_program(compiled_program, np_language, &is_opcode_supported) - .expect("Program optimization failed"); - - ::from_serde(&optimized_program).unwrap() - } -} - cfg_if::cfg_if! { if #[cfg(target_os = "wasi")] { fn get_non_stdlib_asset(path_to_file: &Path) -> std::io::Result { diff --git a/compiler/wasm/src/errors.rs b/compiler/wasm/src/errors.rs new file mode 100644 index 00000000000..6aa70dafa90 --- /dev/null +++ b/compiler/wasm/src/errors.rs @@ -0,0 +1,29 @@ +use js_sys::JsString; + +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(typescript_custom_section)] +const COMPILE_ERROR: &'static str = r#" +export type CompileError = Error; +"#; + +/// `CompileError` is a raw js error. +/// It'd be ideal that `CompileError` was a subclass of `Error`, but for that we'd need to use JS snippets or a js module. +/// Currently JS snippets don't work with a nodejs target. And a module would be too much for just a custom error type. +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(extends = js_sys::Error, js_name = "CompileError", typescript_type = "CompileError")] + #[derive(Clone, Debug, PartialEq, Eq)] + pub type JsCompileError; + + #[wasm_bindgen(constructor, js_class = "Error")] + fn constructor(message: JsString) -> JsCompileError; +} + +impl JsCompileError { + /// Creates a new execution error with the given call stack. + /// Call stacks won't be optional in the future, after removing ErrorLocation in ACVM. + pub fn new(message: String) -> Self { + JsCompileError::constructor(JsString::from(message)) + } +} diff --git a/compiler/wasm/src/lib.rs b/compiler/wasm/src/lib.rs index e3a2a5fc340..3a8e00bc6dd 100644 --- a/compiler/wasm/src/lib.rs +++ b/compiler/wasm/src/lib.rs @@ -13,9 +13,10 @@ use wasm_bindgen::prelude::*; mod circuit; mod compile; +mod errors; pub use circuit::{acir_read_bytes, acir_write_bytes}; -pub use compile::{compile, WASMCompileOptions}; +pub use compile::compile; #[derive(Serialize, Deserialize)] pub struct BuildInfo { diff --git a/compiler/wasm/test/shared.ts b/compiler/wasm/test/shared.ts index d1b7831befa..7c723cd6883 100644 --- a/compiler/wasm/test/shared.ts +++ b/compiler/wasm/test/shared.ts @@ -14,13 +14,15 @@ export async function compileNoirSource(noir_source: string): Promise { if (typeof source === 'undefined') { throw Error(`Could not resolve source for '${id}'`); + } else if (id !== '/main.nr') { + throw Error(`Unexpected id: '${id}'`); } else { return source; } }); try { - const compiled_noir = compile({}); + const compiled_noir = compile('main.nr'); console.log('Noir source compilation done.'); diff --git a/tooling/noirc_abi_wasm/Cargo.toml b/tooling/noirc_abi_wasm/Cargo.toml index 3383d3f21e8..130f022dd1f 100644 --- a/tooling/noirc_abi_wasm/Cargo.toml +++ b/tooling/noirc_abi_wasm/Cargo.toml @@ -17,11 +17,11 @@ noirc_abi.workspace = true iter-extended.workspace = true wasm-bindgen.workspace = true serde.workspace = true +js-sys.workspace = true console_error_panic_hook = "0.1.7" gloo-utils = { version = "0.1", features = ["serde"] } -js-sys = "0.3.62" # This is an unused dependency, we are adding it # so that we can enable the js feature in getrandom. From 22e91cb8ae46b9a49660ad3cb0d673803aede5a7 Mon Sep 17 00:00:00 2001 From: Tom French Date: Tue, 3 Oct 2023 23:05:27 +0100 Subject: [PATCH 2/2] chore: fix integration test --- compiler/integration-tests/test/browser/recursion.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/integration-tests/test/browser/recursion.test.ts b/compiler/integration-tests/test/browser/recursion.test.ts index 6d5e976d5dc..bd9f18064d9 100644 --- a/compiler/integration-tests/test/browser/recursion.test.ts +++ b/compiler/integration-tests/test/browser/recursion.test.ts @@ -32,7 +32,8 @@ async function getCircuit(noirSource: string) { return noirSource; }); - return compile({}); + // We're ignoring this in the resolver but pass in something sensible. + return compile('./main.nr'); } describe('It compiles noir program code, receiving circuit bytes and abi object.', () => {