diff --git a/tooling/debugger/src/foreign_calls.rs b/tooling/debugger/src/foreign_calls.rs index 899ba892d8f..91242edecd5 100644 --- a/tooling/debugger/src/foreign_calls.rs +++ b/tooling/debugger/src/foreign_calls.rs @@ -4,7 +4,7 @@ use acvm::{ AcirField, FieldElement, }; use nargo::{ - foreign_calls::{DefaultForeignCallExecutor, ForeignCallExecutor}, + foreign_calls::{layers::Layer, DefaultForeignCallExecutor, ForeignCallExecutor}, PrintOutput, }; use noirc_artifacts::debug::{DebugArtifact, DebugVars, StackFrame}; @@ -44,23 +44,31 @@ pub trait DebugForeignCallExecutor: ForeignCallExecutor { fn current_stack_frame(&self) -> Option>; } -pub struct DefaultDebugForeignCallExecutor<'a> { - executor: DefaultForeignCallExecutor<'a, FieldElement>, +#[derive(Default)] +pub struct DefaultDebugForeignCallExecutor { pub debug_vars: DebugVars, } -impl<'a> DefaultDebugForeignCallExecutor<'a> { - pub fn new(output: PrintOutput<'a>) -> Self { - Self { - executor: DefaultForeignCallExecutor::new(output, None, None, None), - debug_vars: DebugVars::default(), - } +impl DefaultDebugForeignCallExecutor { + pub fn make( + output: PrintOutput<'_>, + ex: DefaultDebugForeignCallExecutor, + ) -> impl DebugForeignCallExecutor + '_ { + Layer::new(ex, DefaultForeignCallExecutor::new(output, None, None, None)) + } + + #[allow(clippy::new_ret_no_self, dead_code)] + pub fn new(output: PrintOutput<'_>) -> impl DebugForeignCallExecutor + '_ { + Self::make(output, Self::default()) } - pub fn from_artifact(output: PrintOutput<'a>, artifact: &DebugArtifact) -> Self { - let mut ex = Self::new(output); + pub fn from_artifact<'a>( + output: PrintOutput<'a>, + artifact: &DebugArtifact, + ) -> impl DebugForeignCallExecutor + 'a { + let mut ex = Self::default(); ex.load_artifact(artifact); - ex + Self::make(output, ex) } pub fn load_artifact(&mut self, artifact: &DebugArtifact) { @@ -73,7 +81,7 @@ impl<'a> DefaultDebugForeignCallExecutor<'a> { } } -impl DebugForeignCallExecutor for DefaultDebugForeignCallExecutor<'_> { +impl DebugForeignCallExecutor for DefaultDebugForeignCallExecutor { fn get_variables(&self) -> Vec> { self.debug_vars.get_variables() } @@ -91,7 +99,7 @@ fn debug_fn_id(value: &FieldElement) -> DebugFnId { DebugFnId(value.to_u128() as u32) } -impl ForeignCallExecutor for DefaultDebugForeignCallExecutor<'_> { +impl ForeignCallExecutor for DefaultDebugForeignCallExecutor { fn execute( &mut self, foreign_call: &ForeignCallWaitInfo, @@ -166,7 +174,21 @@ impl ForeignCallExecutor for DefaultDebugForeignCallExecutor<'_> { self.debug_vars.pop_fn(); Ok(ForeignCallResult::default()) } - None => self.executor.execute(foreign_call), + None => Err(ForeignCallError::NoHandler(foreign_call_name.to_string())), } } } + +impl DebugForeignCallExecutor for Layer +where + H: DebugForeignCallExecutor, + I: ForeignCallExecutor, +{ + fn get_variables(&self) -> Vec> { + self.handler().get_variables() + } + + fn current_stack_frame(&self) -> Option> { + self.handler().current_stack_frame() + } +} diff --git a/tooling/nargo/src/foreign_calls/layers.rs b/tooling/nargo/src/foreign_calls/layers.rs new file mode 100644 index 00000000000..16ea678f691 --- /dev/null +++ b/tooling/nargo/src/foreign_calls/layers.rs @@ -0,0 +1,144 @@ +use std::marker::PhantomData; + +use acvm::{acir::brillig::ForeignCallResult, pwg::ForeignCallWaitInfo, AcirField}; +use noirc_printable_type::ForeignCallError; + +use super::ForeignCallExecutor; + +/// Returns an empty result when called. +/// +/// If all executors have no handler for the given foreign call then we cannot +/// return a correct response to the ACVM. The best we can do is to return an empty response, +/// this allows us to ignore any foreign calls which exist solely to pass information from inside +/// the circuit to the environment (e.g. custom logging) as the execution will still be able to progress. +/// +/// We optimistically return an empty response for all oracle calls as the ACVM will error +/// should a response have been required. +pub struct Empty; + +impl ForeignCallExecutor for Empty { + fn execute( + &mut self, + _foreign_call: &ForeignCallWaitInfo, + ) -> Result, ForeignCallError> { + Ok(ForeignCallResult::default()) + } +} + +/// Returns `NoHandler` for every call. +pub struct Unhandled; + +impl ForeignCallExecutor for Unhandled { + fn execute( + &mut self, + foreign_call: &ForeignCallWaitInfo, + ) -> Result, ForeignCallError> { + Err(ForeignCallError::NoHandler(foreign_call.function.clone())) + } +} + +/// Forwards to the inner executor if its own handler doesn't handle the call. +pub struct Layer { + pub handler: H, + pub inner: I, + _field: PhantomData, +} + +impl ForeignCallExecutor for Layer +where + H: ForeignCallExecutor, + I: ForeignCallExecutor, +{ + fn execute( + &mut self, + foreign_call: &ForeignCallWaitInfo, + ) -> Result, ForeignCallError> { + match self.handler.execute(foreign_call) { + Err(ForeignCallError::NoHandler(_)) => self.inner.execute(foreign_call), + handled => handled, + } + } +} + +impl Layer { + /// Create a layer from two handlers + pub fn new(handler: H, inner: I) -> Self { + Self { handler, inner, _field: PhantomData } + } +} + +impl Layer { + /// Create a layer from a handler. + /// If the handler doesn't handle a call, a default empty response is returned. + pub fn or_empty(handler: H) -> Self { + Self { handler, inner: Empty, _field: PhantomData } + } +} + +impl Layer { + /// Create a layer from a handler. + /// If the handler doesn't handle a call, `NoHandler` error is returned. + pub fn or_unhandled(handler: H) -> Self { + Self { handler, inner: Unhandled, _field: PhantomData } + } +} + +impl Layer { + /// A base layer that doesn't handle anything. + pub fn unhandled() -> Self { + Self { handler: Unhandled, inner: Unhandled, _field: PhantomData } + } +} + +impl Layer { + /// Add another layer on top of this one. + pub fn add_layer(self, handler: J) -> Layer { + Layer::new(handler, self) + } + + pub fn handler(&self) -> &H { + &self.handler + } + + pub fn inner(&self) -> &I { + &self.inner + } +} + +/// Compose handlers. +pub trait Layering { + /// Layer an executor on top of this one. + /// The `other` executor will be called first. + fn add_layer(self, other: L) -> Layer + where + Self: Sized + ForeignCallExecutor, + L: ForeignCallExecutor; +} + +impl Layering for T { + fn add_layer(self, other: L) -> Layer + where + T: Sized + ForeignCallExecutor, + L: ForeignCallExecutor, + { + Layer::new(other, self) + } +} + +/// Support disabling a layer by making it optional. +/// This way we can still have a known static type for a composition, +/// because layers are always added, potentially wrapped in an `Option`. +impl ForeignCallExecutor for Option +where + H: ForeignCallExecutor, +{ + fn execute( + &mut self, + foreign_call: &ForeignCallWaitInfo, + ) -> Result, ForeignCallError> { + match self { + Some(handler) => handler.execute(foreign_call), + None => Err(ForeignCallError::NoHandler(foreign_call.function.clone())), + } + } +} diff --git a/tooling/nargo/src/foreign_calls/mocker.rs b/tooling/nargo/src/foreign_calls/mocker.rs index c93d16bbaf6..59ce7479cd4 100644 --- a/tooling/nargo/src/foreign_calls/mocker.rs +++ b/tooling/nargo/src/foreign_calls/mocker.rs @@ -1,3 +1,5 @@ +use std::marker::PhantomData; + use acvm::{ acir::brillig::{ForeignCallParam, ForeignCallResult}, pwg::ForeignCallWaitInfo, @@ -45,7 +47,7 @@ impl MockedCall { } #[derive(Debug, Default)] -pub(crate) struct MockForeignCallExecutor { +pub struct MockForeignCallExecutor { /// Mocks have unique ids used to identify them in Noir, allowing to update or remove them. last_mock_id: usize, /// The registered mocks @@ -78,8 +80,9 @@ impl MockForeignCallExecutor { } } -impl Deserialize<'a>> ForeignCallExecutor - for MockForeignCallExecutor +impl ForeignCallExecutor for MockForeignCallExecutor +where + F: AcirField + Serialize + for<'a> Deserialize<'a>, { fn execute( &mut self, @@ -174,3 +177,32 @@ impl Deserialize<'a>> ForeignCallExecutor } } } + +/// Handler that panics if any of the mock functions are called. +#[allow(dead_code)] // TODO: Make the mocker optional +pub(crate) struct DisabledMockForeignCallExecutor { + _field: PhantomData, +} + +impl ForeignCallExecutor for DisabledMockForeignCallExecutor { + fn execute( + &mut self, + foreign_call: &ForeignCallWaitInfo, + ) -> Result, ForeignCallError> { + let foreign_call_name = foreign_call.function.as_str(); + if let Some(call) = ForeignCall::lookup(foreign_call_name) { + match call { + ForeignCall::CreateMock + | ForeignCall::SetMockParams + | ForeignCall::GetMockLastParams + | ForeignCall::SetMockReturns + | ForeignCall::SetMockTimes + | ForeignCall::ClearMock => { + panic!("unexpected mock call: {}", foreign_call.function) + } + _ => {} + } + } + Err(ForeignCallError::NoHandler(foreign_call.function.clone())) + } +} diff --git a/tooling/nargo/src/foreign_calls/mod.rs b/tooling/nargo/src/foreign_calls/mod.rs index 65ff051bcbf..72311558250 100644 --- a/tooling/nargo/src/foreign_calls/mod.rs +++ b/tooling/nargo/src/foreign_calls/mod.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use acvm::{acir::brillig::ForeignCallResult, pwg::ForeignCallWaitInfo, AcirField}; +use layers::{Layer, Layering}; use mocker::MockForeignCallExecutor; use noirc_printable_type::ForeignCallError; use print::{PrintForeignCallExecutor, PrintOutput}; @@ -8,6 +9,7 @@ use rand::Rng; use rpc::RPCForeignCallExecutor; use serde::{Deserialize, Serialize}; +pub mod layers; pub(crate) mod mocker; pub(crate) mod print; pub(crate) mod rpc; @@ -64,77 +66,46 @@ impl ForeignCall { } } -#[derive(Debug, Default)] -pub struct DefaultForeignCallExecutor<'a, F> { - /// The executor for any [`ForeignCall::Print`] calls. - printer: PrintForeignCallExecutor<'a>, - mocker: MockForeignCallExecutor, - external: Option, -} +pub struct DefaultForeignCallExecutor; -impl<'a, F: Default> DefaultForeignCallExecutor<'a, F> { - pub fn new( +impl DefaultForeignCallExecutor { + #[allow(clippy::new_ret_no_self)] + pub fn new<'a, F>( output: PrintOutput<'a>, resolver_url: Option<&str>, root_path: Option, package_name: Option, - ) -> Self { - let id = rand::thread_rng().gen(); - let printer = PrintForeignCallExecutor { output }; - let external_resolver = resolver_url.map(|resolver_url| { - RPCForeignCallExecutor::new(resolver_url, id, root_path, package_name) - }); - DefaultForeignCallExecutor { - printer, - mocker: MockForeignCallExecutor::default(), - external: external_resolver, - } + ) -> impl ForeignCallExecutor + 'a + where + F: AcirField + Serialize + for<'de> Deserialize<'de> + 'a, + { + Self::with_base(layers::Empty, output, resolver_url, root_path, package_name) } -} - -impl<'a, F: AcirField + Serialize + for<'b> Deserialize<'b>> ForeignCallExecutor - for DefaultForeignCallExecutor<'a, F> -{ - fn execute( - &mut self, - foreign_call: &ForeignCallWaitInfo, - ) -> Result, ForeignCallError> { - let foreign_call_name = foreign_call.function.as_str(); - match ForeignCall::lookup(foreign_call_name) { - Some(ForeignCall::Print) => self.printer.execute(foreign_call), - Some( - ForeignCall::CreateMock - | ForeignCall::SetMockParams - | ForeignCall::GetMockLastParams - | ForeignCall::SetMockReturns - | ForeignCall::SetMockTimes - | ForeignCall::ClearMock, - ) => self.mocker.execute(foreign_call), - - None => { - // First check if there's any defined mock responses for this foreign call. - match self.mocker.execute(foreign_call) { - Err(ForeignCallError::NoHandler(_)) => (), - response_or_error => return response_or_error, - }; - if let Some(external_resolver) = &mut self.external { - // If the user has registered an external resolver then we forward any remaining oracle calls there. - match external_resolver.execute(foreign_call) { - Err(ForeignCallError::NoHandler(_)) => (), - response_or_error => return response_or_error, - }; - } - - // If all executors have no handler for the given foreign call then we cannot - // return a correct response to the ACVM. The best we can do is to return an empty response, - // this allows us to ignore any foreign calls which exist solely to pass information from inside - // the circuit to the environment (e.g. custom logging) as the execution will still be able to progress. - // - // We optimistically return an empty response for all oracle calls as the ACVM will error - // should a response have been required. - Ok(ForeignCallResult::default()) - } - } + pub fn with_base<'a, F, B>( + base: B, + output: PrintOutput<'a>, + resolver_url: Option<&str>, + root_path: Option, + package_name: Option, + ) -> DefaultForeignCallLayers<'a, B, F> + where + F: AcirField + Serialize + for<'de> Deserialize<'de> + 'a, + B: ForeignCallExecutor + 'a, + { + // Adding them in the opposite order, so print is the outermost layer. + base.add_layer(resolver_url.map(|resolver_url| { + let id = rand::thread_rng().gen(); + RPCForeignCallExecutor::new(resolver_url, id, root_path, package_name) + })) + .add_layer(MockForeignCallExecutor::default()) + .add_layer(PrintForeignCallExecutor::new(output)) } } + +/// Facilitate static typing of layers on a base layer, so inner layers can be accessed. +pub type DefaultForeignCallLayers<'a, B, F> = Layer< + PrintForeignCallExecutor<'a>, + Layer, Layer, B, F>, F>, + F, +>; diff --git a/tooling/nargo/src/foreign_calls/print.rs b/tooling/nargo/src/foreign_calls/print.rs index 8b2b5efd8b6..5dc006ca775 100644 --- a/tooling/nargo/src/foreign_calls/print.rs +++ b/tooling/nargo/src/foreign_calls/print.rs @@ -12,8 +12,14 @@ pub enum PrintOutput<'a> { } #[derive(Debug, Default)] -pub(crate) struct PrintForeignCallExecutor<'a> { - pub(crate) output: PrintOutput<'a>, +pub struct PrintForeignCallExecutor<'a> { + output: PrintOutput<'a>, +} + +impl<'a> PrintForeignCallExecutor<'a> { + pub fn new(output: PrintOutput<'a>) -> Self { + Self { output } + } } impl ForeignCallExecutor for PrintForeignCallExecutor<'_> { diff --git a/tooling/nargo/src/foreign_calls/rpc.rs b/tooling/nargo/src/foreign_calls/rpc.rs index 565a2a036c3..aa11c24bc9f 100644 --- a/tooling/nargo/src/foreign_calls/rpc.rs +++ b/tooling/nargo/src/foreign_calls/rpc.rs @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; use super::ForeignCallExecutor; #[derive(Debug)] -pub(crate) struct RPCForeignCallExecutor { +pub struct RPCForeignCallExecutor { /// A randomly generated id for this `DefaultForeignCallExecutor`. /// /// This is used so that a single `external_resolver` can distinguish between requests from multiple @@ -54,7 +54,7 @@ struct ResolveForeignCallRequest { type ResolveForeignCallResult = Result, ForeignCallError>; impl RPCForeignCallExecutor { - pub(crate) fn new( + pub fn new( resolver_url: &str, id: u64, root_path: Option, diff --git a/tooling/nargo/src/ops/test.rs b/tooling/nargo/src/ops/test.rs index 1306150518d..56afbbf29ec 100644 --- a/tooling/nargo/src/ops/test.rs +++ b/tooling/nargo/src/ops/test.rs @@ -13,16 +13,13 @@ use noirc_driver::{compile_no_check, CompileError, CompileOptions, DEFAULT_EXPRE use noirc_errors::{debug_info::DebugInfo, FileDiagnostic}; use noirc_frontend::hir::{def_map::TestFunction, Context}; use noirc_printable_type::ForeignCallError; -use rand::Rng; use serde::{Deserialize, Serialize}; use crate::{ errors::try_to_diagnose_runtime_error, foreign_calls::{ - mocker::MockForeignCallExecutor, - print::{PrintForeignCallExecutor, PrintOutput}, - rpc::RPCForeignCallExecutor, - ForeignCall, ForeignCallExecutor, + layers, print::PrintOutput, DefaultForeignCallExecutor, DefaultForeignCallLayers, + ForeignCallExecutor, }, NargoError, }; @@ -279,32 +276,31 @@ fn check_expected_failure_message( /// A specialized foreign call executor which tracks whether it has encountered any unknown foreign calls struct TestForeignCallExecutor<'a, F> { - /// The executor for any [`ForeignCall::Print`] calls. - printer: PrintForeignCallExecutor<'a>, - mocker: MockForeignCallExecutor, - external: Option, - + executor: DefaultForeignCallLayers<'a, layers::Unhandled, F>, encountered_unknown_foreign_call: bool, } -impl<'a, F: Default> TestForeignCallExecutor<'a, F> { +impl<'a, F> TestForeignCallExecutor<'a, F> +where + F: Default + AcirField + Serialize + for<'de> Deserialize<'de> + 'a, +{ + #[allow(clippy::new_ret_no_self)] fn new( output: PrintOutput<'a>, resolver_url: Option<&str>, root_path: Option, package_name: Option, ) -> Self { - let id = rand::thread_rng().gen(); - let printer = PrintForeignCallExecutor { output }; - let external_resolver = resolver_url.map(|resolver_url| { - RPCForeignCallExecutor::new(resolver_url, id, root_path, package_name) - }); - TestForeignCallExecutor { - printer, - mocker: MockForeignCallExecutor::default(), - external: external_resolver, - encountered_unknown_foreign_call: false, - } + // Use a base layer that doesn't handle anything, which we handle in the `execute` below. + let executor = DefaultForeignCallExecutor::with_base( + layers::Unhandled, + output, + resolver_url, + root_path, + package_name, + ); + + Self { executor, encountered_unknown_foreign_call: false } } } @@ -317,46 +313,12 @@ impl<'a, F: AcirField + Serialize + for<'b> Deserialize<'b>> ForeignCallExecutor ) -> Result, ForeignCallError> { // If the circuit has reached a new foreign call opcode then it can't have failed from any previous unknown foreign calls. self.encountered_unknown_foreign_call = false; - - let foreign_call_name = foreign_call.function.as_str(); - match ForeignCall::lookup(foreign_call_name) { - Some(ForeignCall::Print) => self.printer.execute(foreign_call), - - Some( - ForeignCall::CreateMock - | ForeignCall::SetMockParams - | ForeignCall::GetMockLastParams - | ForeignCall::SetMockReturns - | ForeignCall::SetMockTimes - | ForeignCall::ClearMock, - ) => self.mocker.execute(foreign_call), - - None => { - // First check if there's any defined mock responses for this foreign call. - match self.mocker.execute(foreign_call) { - Err(ForeignCallError::NoHandler(_)) => (), - response_or_error => return response_or_error, - }; - - if let Some(external_resolver) = &mut self.external { - // If the user has registered an external resolver then we forward any remaining oracle calls there. - match external_resolver.execute(foreign_call) { - Err(ForeignCallError::NoHandler(_)) => (), - response_or_error => return response_or_error, - }; - } - + match self.executor.execute(foreign_call) { + Err(ForeignCallError::NoHandler(_)) => { self.encountered_unknown_foreign_call = true; - - // If all executors have no handler for the given foreign call then we cannot - // return a correct response to the ACVM. The best we can do is to return an empty response, - // this allows us to ignore any foreign calls which exist solely to pass information from inside - // the circuit to the environment (e.g. custom logging) as the execution will still be able to progress. - // - // We optimistically return an empty response for all oracle calls as the ACVM will error - // should a response have been required. - Ok(ForeignCallResult::default()) + layers::Empty.execute(foreign_call) } + other => other, } } }