From f9933fa50c42aade8ddc13d29deef7645ddcf586 Mon Sep 17 00:00:00 2001
From: Alex Gherghisan <alexghr@users.noreply.github.com>
Date: Mon, 30 Oct 2023 23:54:11 +0000
Subject: [PATCH] feat!: noir-wasm outputs debug symbols (#3317)

---
 .../test/browser/compile_prove_verify.test.ts |  17 ++-
 .../test/browser/recursion.test.ts            |  13 +-
 .../test/node/smart_contract_verifier.test.ts |   7 +-
 compiler/wasm/src/compile.rs                  | 130 ++++++++++++++++--
 compiler/wasm/test/browser/index.test.ts      |  20 ++-
 compiler/wasm/test/node/index.test.ts         |  20 ++-
 6 files changed, 170 insertions(+), 37 deletions(-)

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 f2063c5e4b0..95a1aa502ad 100644
--- a/compiler/integration-tests/test/browser/compile_prove_verify.test.ts
+++ b/compiler/integration-tests/test/browser/compile_prove_verify.test.ts
@@ -3,7 +3,7 @@ import { Logger } from 'tslog';
 import * as TOML from 'smol-toml';
 
 import { initializeResolver } from '@noir-lang/source-resolver';
-import newCompiler, { compile, init_log_level as compilerLogLevel } from '@noir-lang/noir_wasm';
+import newCompiler, { CompiledProgram, compile, init_log_level as compilerLogLevel } from '@noir-lang/noir_wasm';
 import { Noir } from '@noir-lang/noir_js';
 import { InputMap } from '@noir-lang/noirc_abi';
 import { BarretenbergBackend } from '@noir-lang/backend_barretenberg';
@@ -32,7 +32,7 @@ const suite = Mocha.Suite.create(mocha.suite, 'Noir end to end test');
 
 suite.timeout(60 * 20e3); //20mins
 
-async function getCircuit(noirSource: string) {
+function getCircuit(noirSource: string): CompiledProgram {
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   initializeResolver((id: string) => {
     logger.debug('source-resolver: resolving:', id);
@@ -40,7 +40,12 @@ async function getCircuit(noirSource: string) {
   });
 
   // We're ignoring this in the resolver but pass in something sensible.
-  return compile('/main.nr');
+  const result = compile('/main.nr');
+  if (!('program' in result)) {
+    throw new Error('Compilation failed');
+  }
+
+  return result.program;
 }
 
 test_cases.forEach((testInfo) => {
@@ -51,11 +56,11 @@ test_cases.forEach((testInfo) => {
 
     const noir_source = await getFile(`${base_relative_path}/${test_case}/src/main.nr`);
 
-    let noir_program;
+    let noir_program: CompiledProgram;
     try {
-      noir_program = await getCircuit(noir_source);
+      noir_program = getCircuit(noir_source);
 
-      expect(await noir_program, 'Compile output ').to.be.an('object');
+      expect(noir_program, 'Compile output ').to.be.an('object');
     } catch (e) {
       expect(e, 'Compilation Step').to.not.be.an('error');
       throw e;
diff --git a/compiler/integration-tests/test/browser/recursion.test.ts b/compiler/integration-tests/test/browser/recursion.test.ts
index 6a5592bca67..dbf74882654 100644
--- a/compiler/integration-tests/test/browser/recursion.test.ts
+++ b/compiler/integration-tests/test/browser/recursion.test.ts
@@ -3,7 +3,7 @@ import { expect } from '@esm-bundle/chai';
 import { TEST_LOG_LEVEL } from '../environment.js';
 import { Logger } from 'tslog';
 import { initializeResolver } from '@noir-lang/source-resolver';
-import newCompiler, { compile, init_log_level as compilerLogLevel } from '@noir-lang/noir_wasm';
+import newCompiler, { CompiledProgram, compile, init_log_level as compilerLogLevel } from '@noir-lang/noir_wasm';
 import { acvm, abi, Noir } from '@noir-lang/noir_js';
 
 import * as TOML from 'smol-toml';
@@ -26,7 +26,7 @@ const base_relative_path = '../../../../..';
 const circuit_main = 'compiler/integration-tests/circuits/main';
 const circuit_recursion = 'compiler/integration-tests/circuits/recursion';
 
-async function getCircuit(noirSource: string) {
+function getCircuit(noirSource: string): CompiledProgram {
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   initializeResolver((id: string) => {
     logger.debug('source-resolver: resolving:', id);
@@ -34,7 +34,12 @@ async function getCircuit(noirSource: string) {
   });
 
   // We're ignoring this in the resolver but pass in something sensible.
-  return compile('./main.nr');
+  const result = compile('/main.nr');
+  if (!('program' in result)) {
+    throw new Error('Compilation failed');
+  }
+
+  return result.program;
 }
 
 describe('It compiles noir program code, receiving circuit bytes and abi object.', () => {
@@ -50,7 +55,7 @@ describe('It compiles noir program code, receiving circuit bytes and abi object.
   });
 
   it('Should generate valid inner proof for correct input, then verify proof within a proof', async () => {
-    const main_program = await getCircuit(circuit_main_source);
+    const main_program = getCircuit(circuit_main_source);
     const main_inputs: InputMap = TOML.parse(circuit_main_toml) as InputMap;
 
     const main_backend = new BarretenbergBackend(main_program);
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 038c692220f..738bc2df8dd 100644
--- a/compiler/integration-tests/test/node/smart_contract_verifier.test.ts
+++ b/compiler/integration-tests/test/node/smart_contract_verifier.test.ts
@@ -33,7 +33,12 @@ test_cases.forEach((testInfo) => {
 
     const noir_source_path = resolve(`${base_relative_path}/${test_case}/src/main.nr`);
 
-    const noir_program = compile(noir_source_path);
+    const compileResult = compile(noir_source_path);
+    if (!('program' in compileResult)) {
+      throw new Error('Compilation failed');
+    }
+
+    const noir_program = compileResult.program;
 
     const backend = new BarretenbergBackend(noir_program);
     const program = new Noir(noir_program, backend);
diff --git a/compiler/wasm/src/compile.rs b/compiler/wasm/src/compile.rs
index ac6219ee625..e7fd3dd5212 100644
--- a/compiler/wasm/src/compile.rs
+++ b/compiler/wasm/src/compile.rs
@@ -1,8 +1,9 @@
 use fm::FileManager;
 use gloo_utils::format::JsValueSerdeExt;
-use js_sys::Object;
+use js_sys::{JsString, Object};
 use nargo::artifacts::{
     contract::{PreprocessedContract, PreprocessedContractFunction},
+    debug::DebugArtifact,
     program::PreprocessedProgram,
 };
 use noirc_driver::{
@@ -27,6 +28,38 @@ export type DependencyGraph = {
     root_dependencies: readonly string[];
     library_dependencies: Readonly<Record<string, readonly string[]>>;
 }
+
+export type CompiledContract = {
+    noir_version: string;
+    name: string;
+    backend: string;
+    functions: Array<any>;
+    events: Array<any>;
+};
+
+export type CompiledProgram = {
+    noir_version: string;
+    backend: string;
+    abi: any;
+    bytecode: string;
+}
+
+export type DebugArtifact = {
+    debug_symbols: Array<any>;
+    file_map: Record<number, any>;
+    warnings: Array<any>;
+};
+
+export type CompileResult = (
+    | {
+        contract: CompiledContract;
+        debug: DebugArtifact;
+    }
+    | {
+        program: CompiledProgram;
+        debug: DebugArtifact;
+    }
+);
 "#;
 
 #[wasm_bindgen]
@@ -34,6 +67,57 @@ extern "C" {
     #[wasm_bindgen(extends = Object, js_name = "DependencyGraph", typescript_type = "DependencyGraph")]
     #[derive(Clone, Debug, PartialEq, Eq)]
     pub type JsDependencyGraph;
+
+    #[wasm_bindgen(extends = Object, js_name = "CompileResult", typescript_type = "CompileResult")]
+    #[derive(Clone, Debug, PartialEq, Eq)]
+    pub type JsCompileResult;
+
+    #[wasm_bindgen(constructor, js_class = "Object")]
+    fn constructor() -> JsCompileResult;
+}
+
+impl JsCompileResult {
+    const CONTRACT_PROP: &'static str = "contract";
+    const PROGRAM_PROP: &'static str = "program";
+    const DEBUG_PROP: &'static str = "debug";
+
+    pub fn new(resp: CompileResult) -> JsCompileResult {
+        let obj = JsCompileResult::constructor();
+        match resp {
+            CompileResult::Contract { contract, debug } => {
+                js_sys::Reflect::set(
+                    &obj,
+                    &JsString::from(JsCompileResult::CONTRACT_PROP),
+                    &<JsValue as JsValueSerdeExt>::from_serde(&contract).unwrap(),
+                )
+                .unwrap();
+
+                js_sys::Reflect::set(
+                    &obj,
+                    &JsString::from(JsCompileResult::DEBUG_PROP),
+                    &<JsValue as JsValueSerdeExt>::from_serde(&debug).unwrap(),
+                )
+                .unwrap();
+            }
+            CompileResult::Program { program, debug } => {
+                js_sys::Reflect::set(
+                    &obj,
+                    &JsString::from(JsCompileResult::PROGRAM_PROP),
+                    &<JsValue as JsValueSerdeExt>::from_serde(&program).unwrap(),
+                )
+                .unwrap();
+
+                js_sys::Reflect::set(
+                    &obj,
+                    &JsString::from(JsCompileResult::DEBUG_PROP),
+                    &<JsValue as JsValueSerdeExt>::from_serde(&debug).unwrap(),
+                )
+                .unwrap();
+            }
+        };
+
+        obj
+    }
 }
 
 #[derive(Deserialize)]
@@ -42,12 +126,17 @@ struct DependencyGraph {
     library_dependencies: HashMap<CrateName, Vec<CrateName>>,
 }
 
+pub enum CompileResult {
+    Contract { contract: PreprocessedContract, debug: DebugArtifact },
+    Program { program: PreprocessedProgram, debug: DebugArtifact },
+}
+
 #[wasm_bindgen]
 pub fn compile(
     entry_point: String,
     contracts: Option<bool>,
     dependency_graph: Option<JsDependencyGraph>,
-) -> Result<JsValue, JsCompileError> {
+) -> Result<JsCompileResult, JsCompileError> {
     console_error_panic_hook::set_once();
 
     let dependency_graph: DependencyGraph = if let Some(dependency_graph) = dependency_graph {
@@ -89,9 +178,8 @@ pub fn compile(
             nargo::ops::optimize_contract(compiled_contract, np_language, &is_opcode_supported)
                 .expect("Contract optimization failed");
 
-        let preprocessed_contract = preprocess_contract(optimized_contract);
-
-        Ok(<JsValue as JsValueSerdeExt>::from_serde(&preprocessed_contract).unwrap())
+        let compile_output = preprocess_contract(optimized_contract);
+        Ok(JsCompileResult::new(compile_output))
     } else {
         let compiled_program = compile_main(&mut context, crate_id, &compile_options, None, true)
             .map_err(|errs| {
@@ -107,9 +195,8 @@ pub fn compile(
             nargo::ops::optimize_program(compiled_program, np_language, &is_opcode_supported)
                 .expect("Program optimization failed");
 
-        let preprocessed_program = preprocess_program(optimized_program);
-
-        Ok(<JsValue as JsValueSerdeExt>::from_serde(&preprocessed_program).unwrap())
+        let compile_output = preprocess_program(optimized_program);
+        Ok(JsCompileResult::new(compile_output))
     }
 }
 
@@ -145,17 +232,30 @@ fn add_noir_lib(context: &mut Context, library_name: &CrateName) -> CrateId {
     prepare_dependency(context, &path_to_lib)
 }
 
-fn preprocess_program(program: CompiledProgram) -> PreprocessedProgram {
-    PreprocessedProgram {
+fn preprocess_program(program: CompiledProgram) -> CompileResult {
+    let debug_artifact = DebugArtifact {
+        debug_symbols: vec![program.debug],
+        file_map: program.file_map,
+        warnings: program.warnings,
+    };
+
+    let preprocessed_program = PreprocessedProgram {
         hash: program.hash,
         backend: String::from(BACKEND_IDENTIFIER),
         abi: program.abi,
         noir_version: NOIR_ARTIFACT_VERSION_STRING.to_string(),
         bytecode: program.circuit,
-    }
+    };
+
+    CompileResult::Program { program: preprocessed_program, debug: debug_artifact }
 }
 
-fn preprocess_contract(contract: CompiledContract) -> PreprocessedContract {
+fn preprocess_contract(contract: CompiledContract) -> CompileResult {
+    let debug_artifact = DebugArtifact {
+        debug_symbols: contract.functions.iter().map(|function| function.debug.clone()).collect(),
+        file_map: contract.file_map,
+        warnings: contract.warnings,
+    };
     let preprocessed_functions = contract
         .functions
         .into_iter()
@@ -168,13 +268,15 @@ fn preprocess_contract(contract: CompiledContract) -> PreprocessedContract {
         })
         .collect();
 
-    PreprocessedContract {
+    let preprocessed_contract = PreprocessedContract {
         noir_version: String::from(NOIR_ARTIFACT_VERSION_STRING),
         name: contract.name,
         backend: String::from(BACKEND_IDENTIFIER),
         functions: preprocessed_functions,
         events: contract.events,
-    }
+    };
+
+    CompileResult::Contract { contract: preprocessed_contract, debug: debug_artifact }
 }
 
 cfg_if::cfg_if! {
diff --git a/compiler/wasm/test/browser/index.test.ts b/compiler/wasm/test/browser/index.test.ts
index cad2ada0c61..8a3f82ffff9 100644
--- a/compiler/wasm/test/browser/index.test.ts
+++ b/compiler/wasm/test/browser/index.test.ts
@@ -47,10 +47,14 @@ describe('noir wasm', () => {
       const wasmCircuit = await compile('/main.nr');
       const cliCircuit = await getPrecompiledSource(simpleScriptExpectedArtifact);
 
+      if (!('program' in wasmCircuit)) {
+        throw Error('Expected program to be present');
+      }
+
       // We don't expect the hashes to match due to how `noir_wasm` handles dependencies
-      expect(wasmCircuit.bytecode).to.eq(cliCircuit.bytecode);
-      expect(wasmCircuit.abi).to.deep.eq(cliCircuit.abi);
-      expect(wasmCircuit.backend).to.eq(cliCircuit.backend);
+      expect(wasmCircuit.program.bytecode).to.eq(cliCircuit.bytecode);
+      expect(wasmCircuit.program.abi).to.deep.eq(cliCircuit.abi);
+      expect(wasmCircuit.program.backend).to.eq(cliCircuit.backend);
     }).timeout(20e3); // 20 seconds
   });
 
@@ -87,12 +91,16 @@ describe('noir wasm', () => {
         },
       });
 
+      if (!('program' in wasmCircuit)) {
+        throw Error('Expected program to be present');
+      }
+
       const cliCircuit = await getPrecompiledSource(depsScriptExpectedArtifact);
 
       // We don't expect the hashes to match due to how `noir_wasm` handles dependencies
-      expect(wasmCircuit.bytecode).to.eq(cliCircuit.bytecode);
-      expect(wasmCircuit.abi).to.deep.eq(cliCircuit.abi);
-      expect(wasmCircuit.backend).to.eq(cliCircuit.backend);
+      expect(wasmCircuit.program.bytecode).to.eq(cliCircuit.bytecode);
+      expect(wasmCircuit.program.abi).to.deep.eq(cliCircuit.abi);
+      expect(wasmCircuit.program.backend).to.eq(cliCircuit.backend);
     }).timeout(20e3); // 20 seconds
   });
 });
diff --git a/compiler/wasm/test/node/index.test.ts b/compiler/wasm/test/node/index.test.ts
index 2b430abdca4..c0d5f88e407 100644
--- a/compiler/wasm/test/node/index.test.ts
+++ b/compiler/wasm/test/node/index.test.ts
@@ -24,10 +24,14 @@ describe('noir wasm compilation', () => {
       const wasmCircuit = await compile(join(__dirname, simpleScriptSourcePath));
       const cliCircuit = await getPrecompiledSource(simpleScriptExpectedArtifact);
 
+      if (!('program' in wasmCircuit)) {
+        throw Error('Expected program to be present');
+      }
+
       // We don't expect the hashes to match due to how `noir_wasm` handles dependencies
-      expect(wasmCircuit.bytecode).to.eq(cliCircuit.bytecode);
-      expect(wasmCircuit.abi).to.deep.eq(cliCircuit.abi);
-      expect(wasmCircuit.backend).to.eq(cliCircuit.backend);
+      expect(wasmCircuit.program.bytecode).to.eq(cliCircuit.bytecode);
+      expect(wasmCircuit.program.abi).to.deep.eq(cliCircuit.abi);
+      expect(wasmCircuit.program.backend).to.eq(cliCircuit.backend);
     }).timeout(10e3);
   });
 
@@ -61,10 +65,14 @@ describe('noir wasm compilation', () => {
 
       const cliCircuit = await getPrecompiledSource(depsScriptExpectedArtifact);
 
+      if (!('program' in wasmCircuit)) {
+        throw Error('Expected program to be present');
+      }
+
       // We don't expect the hashes to match due to how `noir_wasm` handles dependencies
-      expect(wasmCircuit.bytecode).to.eq(cliCircuit.bytecode);
-      expect(wasmCircuit.abi).to.deep.eq(cliCircuit.abi);
-      expect(wasmCircuit.backend).to.eq(cliCircuit.backend);
+      expect(wasmCircuit.program.bytecode).to.eq(cliCircuit.bytecode);
+      expect(wasmCircuit.program.abi).to.deep.eq(cliCircuit.abi);
+      expect(wasmCircuit.program.backend).to.eq(cliCircuit.backend);
     }).timeout(10e3);
   });
 });