From 1ec9cdc7013e867db3672d27e3a6104e4b7e7eef Mon Sep 17 00:00:00 2001 From: Maxim Vezenov Date: Thu, 25 Apr 2024 18:19:43 +0100 Subject: [PATCH] feat: Add `#[inline(tag)]` attribute and codegen (#4913) # Description ## Problem\* Resolves #4910 ## Summary\* We add a new attribute `Inline(String)`. Currently we only support one `InlineType` variant of `Never`. This PR also moves `InlineType` into the monomorphization ast as its functionality is expected to be shared across various frontend passes as well as the SSA/ACIR gen. ## Additional Context I know there is some plans to split the AST off into its own crate as per (https://github.com/noir-lang/noir/issues/4852). This change shouldn't affect make the split much more dififcult as the evaluator already depends on the AST as the issue mentions and this `InlineType` is pretty isolated in its usage. ## Documentation\* Check one: - [ ] No documentation needed. - [ ] Documentation included in this PR. - [X] **[For Experimental Features]** Documentation to be submitted in a separate PR. # PR Checklist\* - [X] I have tested the changes locally. - [X] I have formatted the changes with [Prettier](https://prettier.io/) and/or `cargo fmt` on default settings. --------- Co-authored-by: Tom French <15848336+TomAFrench@users.noreply.github.com> --- .../noirc_evaluator/src/ssa/acir_gen/mod.rs | 66 +++++++++++-------- .../src/ssa/function_builder/mod.rs | 3 +- .../noirc_evaluator/src/ssa/ir/function.rs | 27 +------- .../noirc_evaluator/src/ssa/opt/inlining.rs | 2 +- .../src/ssa/ssa_gen/context.rs | 5 +- .../noirc_evaluator/src/ssa/ssa_gen/mod.rs | 9 +-- compiler/noirc_frontend/src/ast/function.rs | 1 + .../src/hir/resolution/errors.rs | 28 ++++++++ .../src/hir/resolution/resolver.rs | 24 ++++++- .../noirc_frontend/src/hir/type_check/mod.rs | 4 +- .../noirc_frontend/src/hir_def/function.rs | 4 +- compiler/noirc_frontend/src/lexer/token.rs | 18 +++++ .../src/monomorphization/ast.rs | 61 ++++++++++++++++- .../src/monomorphization/mod.rs | 14 ++-- compiler/noirc_frontend/src/tests.rs | 32 +++++++++ .../inline_never_basic/Nargo.toml | 7 ++ .../inline_never_basic/Prover.toml | 2 + .../inline_never_basic/src/main.nr | 8 +++ tooling/debugger/ignored-tests.txt | 2 +- 19 files changed, 240 insertions(+), 77 deletions(-) create mode 100644 test_programs/execution_success/inline_never_basic/Nargo.toml create mode 100644 test_programs/execution_success/inline_never_basic/Prover.toml create mode 100644 test_programs/execution_success/inline_never_basic/src/main.nr diff --git a/compiler/noirc_evaluator/src/ssa/acir_gen/mod.rs b/compiler/noirc_evaluator/src/ssa/acir_gen/mod.rs index 6c7d78acdea..63d1c1b564f 100644 --- a/compiler/noirc_evaluator/src/ssa/acir_gen/mod.rs +++ b/compiler/noirc_evaluator/src/ssa/acir_gen/mod.rs @@ -27,8 +27,8 @@ use crate::brillig::brillig_ir::artifact::{BrilligParameter, GeneratedBrillig}; use crate::brillig::brillig_ir::BrilligContext; use crate::brillig::{brillig_gen::brillig_fn::FunctionContext as BrilligFunctionContext, Brillig}; use crate::errors::{InternalError, InternalWarning, RuntimeError, SsaReport}; -use crate::ssa::ir::function::InlineType; pub(crate) use acir_ir::generated_acir::GeneratedAcir; +use noirc_frontend::monomorphization::ast::InlineType; use acvm::acir::circuit::brillig::BrilligBytecode; use acvm::acir::circuit::OpcodeLocation; @@ -385,12 +385,12 @@ impl<'a> Context<'a> { match function.runtime() { RuntimeType::Acir(inline_type) => { match inline_type { - InlineType::Fold => {} InlineType::Inline => { if function.id() != ssa.main_id { panic!("ACIR function should have been inlined earlier if not marked otherwise"); } } + InlineType::Fold | InlineType::Never => {} } // We only want to convert entry point functions. This being `main` and those marked with `InlineType::Fold` Ok(Some(self.convert_acir_main(function, ssa, brillig)?)) @@ -2610,25 +2610,22 @@ mod test { }, FieldElement, }; + use noirc_frontend::monomorphization::ast::InlineType; use crate::{ brillig::Brillig, ssa::{ acir_gen::acir_ir::generated_acir::BrilligStdlibFunc, function_builder::FunctionBuilder, - ir::{ - function::{FunctionId, InlineType}, - instruction::BinaryOp, - map::Id, - types::Type, - }, + ir::{function::FunctionId, instruction::BinaryOp, map::Id, types::Type}, }, }; fn build_basic_foo_with_return( builder: &mut FunctionBuilder, foo_id: FunctionId, - is_brillig_func: bool, + // `InlineType` can only exist on ACIR functions, so if the option is `None` we should generate a Brillig function + inline_type: Option, ) { // fn foo f1 { // b0(v0: Field, v1: Field): @@ -2636,10 +2633,10 @@ mod test { // constrain v2 == u1 0 // return v0 // } - if is_brillig_func { - builder.new_brillig_function("foo".into(), foo_id); + if let Some(inline_type) = inline_type { + builder.new_function("foo".into(), foo_id, inline_type); } else { - builder.new_function("foo".into(), foo_id, InlineType::Fold); + builder.new_brillig_function("foo".into(), foo_id); } let foo_v0 = builder.add_parameter(Type::field()); let foo_v1 = builder.add_parameter(Type::field()); @@ -2650,8 +2647,25 @@ mod test { builder.terminate_with_return(vec![foo_v0]); } + /// Check that each `InlineType` which prevents inlining functions generates code in the same manner #[test] - fn basic_call_with_outputs_assert() { + fn basic_calls_fold() { + basic_call_with_outputs_assert(InlineType::Fold); + call_output_as_next_call_input(InlineType::Fold); + basic_nested_call(InlineType::Fold); + + call_output_as_next_call_input(InlineType::Never); + basic_nested_call(InlineType::Never); + basic_call_with_outputs_assert(InlineType::Never); + } + + #[test] + #[should_panic] + fn call_without_inline_attribute() { + basic_call_with_outputs_assert(InlineType::Inline); + } + + fn basic_call_with_outputs_assert(inline_type: InlineType) { // acir(inline) fn main f0 { // b0(v0: Field, v1: Field): // v2 = call f1(v0, v1) @@ -2679,7 +2693,7 @@ mod test { builder.insert_constrain(main_call1_results[0], main_call2_results[0], None); builder.terminate_with_return(vec![]); - build_basic_foo_with_return(&mut builder, foo_id, false); + build_basic_foo_with_return(&mut builder, foo_id, Some(inline_type)); let ssa = builder.finish(); @@ -2745,8 +2759,7 @@ mod test { } } - #[test] - fn call_output_as_next_call_input() { + fn call_output_as_next_call_input(inline_type: InlineType) { // acir(inline) fn main f0 { // b0(v0: Field, v1: Field): // v3 = call f1(v0, v1) @@ -2775,7 +2788,7 @@ mod test { builder.insert_constrain(main_call1_results[0], main_call2_results[0], None); builder.terminate_with_return(vec![]); - build_basic_foo_with_return(&mut builder, foo_id, false); + build_basic_foo_with_return(&mut builder, foo_id, Some(inline_type)); let ssa = builder.finish(); @@ -2794,8 +2807,7 @@ mod test { check_call_opcode(&main_opcodes[1], 1, vec![Witness(2), Witness(1)], vec![Witness(3)]); } - #[test] - fn basic_nested_call() { + fn basic_nested_call(inline_type: InlineType) { // SSA for the following Noir program: // fn main(x: Field, y: pub Field) { // let z = func_with_nested_foo_call(x, y); @@ -2851,7 +2863,7 @@ mod test { builder.new_function( "func_with_nested_foo_call".into(), func_with_nested_foo_call_id, - InlineType::Fold, + inline_type, ); let func_with_nested_call_v0 = builder.add_parameter(Type::field()); let func_with_nested_call_v1 = builder.add_parameter(Type::field()); @@ -2866,7 +2878,7 @@ mod test { .to_vec(); builder.terminate_with_return(vec![foo_call[0]]); - build_basic_foo_with_return(&mut builder, foo_id, false); + build_basic_foo_with_return(&mut builder, foo_id, Some(inline_type)); let ssa = builder.finish(); @@ -2977,8 +2989,8 @@ mod test { builder.insert_call(bar, vec![main_v0, main_v1], vec![Type::field()]).to_vec(); builder.terminate_with_return(vec![]); - build_basic_foo_with_return(&mut builder, foo_id, true); - build_basic_foo_with_return(&mut builder, bar_id, true); + build_basic_foo_with_return(&mut builder, foo_id, None); + build_basic_foo_with_return(&mut builder, bar_id, None); let ssa = builder.finish(); let brillig = ssa.to_brillig(false); @@ -3106,7 +3118,7 @@ mod test { builder.terminate_with_return(vec![]); - build_basic_foo_with_return(&mut builder, foo_id, true); + build_basic_foo_with_return(&mut builder, foo_id, None); let ssa = builder.finish(); // We need to generate Brillig artifacts for the regular Brillig function and pass them to the ACIR generation pass. @@ -3192,8 +3204,10 @@ mod test { builder.terminate_with_return(vec![]); - build_basic_foo_with_return(&mut builder, foo_id, true); - build_basic_foo_with_return(&mut builder, bar_id, false); + // Build a Brillig function + build_basic_foo_with_return(&mut builder, foo_id, None); + // Build an ACIR function which has the same logic as the Brillig function above + build_basic_foo_with_return(&mut builder, bar_id, Some(InlineType::Fold)); let ssa = builder.finish(); // We need to generate Brillig artifacts for the regular Brillig function and pass them to the ACIR generation pass. diff --git a/compiler/noirc_evaluator/src/ssa/function_builder/mod.rs b/compiler/noirc_evaluator/src/ssa/function_builder/mod.rs index 75a427397b6..013462ed38c 100644 --- a/compiler/noirc_evaluator/src/ssa/function_builder/mod.rs +++ b/compiler/noirc_evaluator/src/ssa/function_builder/mod.rs @@ -4,6 +4,7 @@ use std::{borrow::Cow, rc::Rc}; use acvm::FieldElement; use noirc_errors::Location; +use noirc_frontend::monomorphization::ast::InlineType; use crate::ssa::ir::{ basic_block::BasicBlockId, @@ -17,7 +18,7 @@ use super::{ ir::{ basic_block::BasicBlock, dfg::{CallStack, InsertInstructionResult}, - function::{InlineType, RuntimeType}, + function::RuntimeType, instruction::{ConstrainError, InstructionId, Intrinsic}, }, ssa_gen::Ssa, diff --git a/compiler/noirc_evaluator/src/ssa/ir/function.rs b/compiler/noirc_evaluator/src/ssa/ir/function.rs index 011bee36661..057786bf5ec 100644 --- a/compiler/noirc_evaluator/src/ssa/ir/function.rs +++ b/compiler/noirc_evaluator/src/ssa/ir/function.rs @@ -1,6 +1,7 @@ use std::collections::BTreeSet; use iter_extended::vecmap; +use noirc_frontend::monomorphization::ast::InlineType; use super::basic_block::BasicBlockId; use super::dfg::DataFlowGraph; @@ -17,18 +18,6 @@ pub(crate) enum RuntimeType { Brillig, } -/// Represents how a RuntimeType::Acir function should be inlined. -/// This type is only relevant for ACIR functions as we do not inline any Brillig functions -#[derive(Default, Clone, Copy, PartialEq, Eq, Debug, Hash)] -pub(crate) enum InlineType { - /// The most basic entry point can expect all its functions to be inlined. - /// All function calls are expected to be inlined into a single ACIR. - #[default] - Inline, - /// Functions marked as foldable will not be inlined and compiled separately into ACIR - Fold, -} - impl RuntimeType { /// Returns whether the runtime type represents an entry point. /// We return `false` for InlineType::Inline on default, which is true @@ -36,10 +25,7 @@ impl RuntimeType { /// handling in any places where this function determines logic. pub(crate) fn is_entry_point(&self) -> bool { match self { - RuntimeType::Acir(inline_type) => match inline_type { - InlineType::Inline => false, - InlineType::Fold => true, - }, + RuntimeType::Acir(inline_type) => inline_type.is_entry_point(), RuntimeType::Brillig => true, } } @@ -163,15 +149,6 @@ impl std::fmt::Display for RuntimeType { } } -impl std::fmt::Display for InlineType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - InlineType::Inline => write!(f, "inline"), - InlineType::Fold => write!(f, "fold"), - } - } -} - /// FunctionId is a reference for a function /// /// This Id is how each function refers to other functions diff --git a/compiler/noirc_evaluator/src/ssa/opt/inlining.rs b/compiler/noirc_evaluator/src/ssa/opt/inlining.rs index ead3cac071c..0b78d47fbb1 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/inlining.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/inlining.rs @@ -517,12 +517,12 @@ impl<'function> PerFunctionContext<'function> { #[cfg(test)] mod test { use acvm::FieldElement; + use noirc_frontend::monomorphization::ast::InlineType; use crate::ssa::{ function_builder::FunctionBuilder, ir::{ basic_block::BasicBlockId, - function::InlineType, instruction::{BinaryOp, Intrinsic, TerminatorInstruction}, map::Id, types::Type, diff --git a/compiler/noirc_evaluator/src/ssa/ssa_gen/context.rs b/compiler/noirc_evaluator/src/ssa/ssa_gen/context.rs index fdd17097d19..f2cc2ba53cc 100644 --- a/compiler/noirc_evaluator/src/ssa/ssa_gen/context.rs +++ b/compiler/noirc_evaluator/src/ssa/ssa_gen/context.rs @@ -11,8 +11,8 @@ use noirc_frontend::monomorphization::ast::{FuncId, Program}; use crate::errors::RuntimeError; use crate::ssa::function_builder::FunctionBuilder; use crate::ssa::ir::basic_block::BasicBlockId; +use crate::ssa::ir::function::FunctionId as IrFunctionId; use crate::ssa::ir::function::{Function, RuntimeType}; -use crate::ssa::ir::function::{FunctionId as IrFunctionId, InlineType}; use crate::ssa::ir::instruction::BinaryOp; use crate::ssa::ir::instruction::Instruction; use crate::ssa::ir::map::AtomicCounter; @@ -125,8 +125,7 @@ impl<'a> FunctionContext<'a> { if func.unconstrained { self.builder.new_brillig_function(func.name.clone(), id); } else { - let inline_type = if func.should_fold { InlineType::Fold } else { InlineType::Inline }; - self.builder.new_function(func.name.clone(), id, inline_type); + self.builder.new_function(func.name.clone(), id, func.inline_type); } self.add_parameters_to_scope(&func.parameters); } diff --git a/compiler/noirc_evaluator/src/ssa/ssa_gen/mod.rs b/compiler/noirc_evaluator/src/ssa/ssa_gen/mod.rs index 79f7cda4ae2..ebde3dcf66e 100644 --- a/compiler/noirc_evaluator/src/ssa/ssa_gen/mod.rs +++ b/compiler/noirc_evaluator/src/ssa/ssa_gen/mod.rs @@ -12,10 +12,7 @@ use noirc_frontend::monomorphization::ast::{self, Expression, Program}; use crate::{ errors::{InternalError, RuntimeError}, - ssa::{ - function_builder::data_bus::DataBusBuilder, - ir::{function::InlineType, instruction::Intrinsic}, - }, + ssa::{function_builder::data_bus::DataBusBuilder, ir::instruction::Intrinsic}, }; use self::{ @@ -61,9 +58,7 @@ pub(crate) fn generate_ssa( if force_brillig_runtime || main.unconstrained { RuntimeType::Brillig } else { - let main_inline_type = - if main.should_fold { InlineType::Fold } else { InlineType::Inline }; - RuntimeType::Acir(main_inline_type) + RuntimeType::Acir(main.inline_type) }, &context, ); diff --git a/compiler/noirc_frontend/src/ast/function.rs b/compiler/noirc_frontend/src/ast/function.rs index 9816218c5f7..9115178671e 100644 --- a/compiler/noirc_frontend/src/ast/function.rs +++ b/compiler/noirc_frontend/src/ast/function.rs @@ -109,6 +109,7 @@ impl From for NoirFunction { Some(FunctionAttribute::Oracle(_)) => FunctionKind::Oracle, Some(FunctionAttribute::Recursive) => FunctionKind::Recursive, Some(FunctionAttribute::Fold) => FunctionKind::Normal, + Some(FunctionAttribute::Inline(_)) => FunctionKind::Normal, None => FunctionKind::Normal, }; diff --git a/compiler/noirc_frontend/src/hir/resolution/errors.rs b/compiler/noirc_frontend/src/hir/resolution/errors.rs index 0fac6f96086..70e7a8e40f2 100644 --- a/compiler/noirc_frontend/src/hir/resolution/errors.rs +++ b/compiler/noirc_frontend/src/hir/resolution/errors.rs @@ -86,6 +86,10 @@ pub enum ResolverError { JumpInConstrainedFn { is_break: bool, span: Span }, #[error("break/continue are only allowed within loops")] JumpOutsideLoop { is_break: bool, span: Span }, + #[error("#[inline(tag)] attribute is only allowed on constrained functions")] + InlineAttributeOnUnconstrained { ident: Ident }, + #[error("#[fold] attribute is only allowed on constrained functions")] + FoldAttributeOnUnconstrained { ident: Ident }, } impl ResolverError { @@ -340,6 +344,30 @@ impl From for Diagnostic { span, ) }, + ResolverError::InlineAttributeOnUnconstrained { ident } => { + let name = &ident.0.contents; + + let mut diag = Diagnostic::simple_error( + format!("misplaced #[inline(tag)] attribute on unconstrained function {name}. Only allowed on constrained functions"), + "misplaced #[inline(tag)] attribute".to_string(), + ident.0.span(), + ); + + diag.add_note("The `#[inline(tag)]` attribute specifies to the compiler whether it should diverge from auto-inlining constrained functions".to_owned()); + diag + } + ResolverError::FoldAttributeOnUnconstrained { ident } => { + let name = &ident.0.contents; + + let mut diag = Diagnostic::simple_error( + format!("misplaced #[fold] attribute on unconstrained function {name}. Only allowed on constrained functions"), + "misplaced #[fold] attribute".to_string(), + ident.0.span(), + ); + + diag.add_note("The `#[fold]` attribute specifies whether a constrained function should be treated as a separate circuit rather than inlined into the program entry point".to_owned()); + diag + } } } } diff --git a/compiler/noirc_frontend/src/hir/resolution/resolver.rs b/compiler/noirc_frontend/src/hir/resolution/resolver.rs index 8e29f8f9fce..b17cd50564e 100644 --- a/compiler/noirc_frontend/src/hir/resolution/resolver.rs +++ b/compiler/noirc_frontend/src/hir/resolution/resolver.rs @@ -926,7 +926,23 @@ impl<'a> Resolver<'a> { let name_ident = HirIdent::non_trait_method(id, location); let attributes = func.attributes().clone(); + let has_inline_attribute = attributes.is_inline(); let should_fold = attributes.is_foldable(); + if !self.inline_attribute_allowed(func) { + if has_inline_attribute { + self.push_err(ResolverError::InlineAttributeOnUnconstrained { + ident: func.name_ident().clone(), + }); + } else if should_fold { + self.push_err(ResolverError::FoldAttributeOnUnconstrained { + ident: func.name_ident().clone(), + }); + } + } + // Both the #[fold] and #[inline(tag)] alter a function's inline type and code generation in similar ways. + // In certain cases such as type checking (for which the following flag will be used) both attributes + // indicate we should code generate in the same way. Thus, we unify the attributes into one flag here. + let has_inline_or_fold_attribute = has_inline_attribute || should_fold; let mut generics = vecmap(&self.generics, |(_, typevar, _)| typevar.clone()); let mut parameters = vec![]; @@ -1021,7 +1037,7 @@ impl<'a> Resolver<'a> { has_body: !func.def.body.is_empty(), trait_constraints: self.resolve_trait_constraints(&func.def.where_clause), is_entry_point: self.is_entry_point_function(func), - should_fold, + has_inline_or_fold_attribute, } } @@ -1057,6 +1073,12 @@ impl<'a> Resolver<'a> { } } + fn inline_attribute_allowed(&self, func: &NoirFunction) -> bool { + // Inline attributes are only relevant for constrained functions + // as all unconstrained functions are not inlined + !func.def.is_unconstrained + } + fn declare_numeric_generics(&mut self, params: &[Type], return_type: &Type) { if self.generics.is_empty() { return; diff --git a/compiler/noirc_frontend/src/hir/type_check/mod.rs b/compiler/noirc_frontend/src/hir/type_check/mod.rs index 2a1ff1dd42c..03ebb44fa1f 100644 --- a/compiler/noirc_frontend/src/hir/type_check/mod.rs +++ b/compiler/noirc_frontend/src/hir/type_check/mod.rs @@ -173,7 +173,7 @@ fn check_if_type_is_valid_for_program_input( ) { let meta = type_checker.interner.function_meta(&func_id); if (meta.is_entry_point && !param.1.is_valid_for_program_input()) - || (meta.should_fold && !param.1.is_valid_non_inlined_function_input()) + || (meta.has_inline_or_fold_attribute && !param.1.is_valid_non_inlined_function_input()) { let span = param.0.span(); errors.push(TypeCheckError::InvalidTypeForEntryPoint { span }); @@ -545,7 +545,7 @@ pub mod test { trait_constraints: Vec::new(), direct_generics: Vec::new(), is_entry_point: true, - should_fold: false, + has_inline_or_fold_attribute: false, }; interner.push_fn_meta(func_meta, func_id); diff --git a/compiler/noirc_frontend/src/hir_def/function.rs b/compiler/noirc_frontend/src/hir_def/function.rs index 67b6412a21c..57d3038a135 100644 --- a/compiler/noirc_frontend/src/hir_def/function.rs +++ b/compiler/noirc_frontend/src/hir_def/function.rs @@ -126,8 +126,8 @@ pub struct FuncMeta { pub is_entry_point: bool, /// True if this function is marked with an attribute - /// that indicates it should not be inlined, such as for folding. - pub should_fold: bool, + /// that indicates it should not be inlined, such as `fold` or `inline(never)` + pub has_inline_or_fold_attribute: bool, } impl FuncMeta { diff --git a/compiler/noirc_frontend/src/lexer/token.rs b/compiler/noirc_frontend/src/lexer/token.rs index 0242fc7e7ff..82e17ac3912 100644 --- a/compiler/noirc_frontend/src/lexer/token.rs +++ b/compiler/noirc_frontend/src/lexer/token.rs @@ -581,6 +581,10 @@ impl Attributes { pub fn is_foldable(&self) -> bool { self.function.as_ref().map_or(false, |func_attribute| func_attribute.is_foldable()) } + + pub fn is_inline(&self) -> bool { + self.function.as_ref().map_or(false, |func_attribute| func_attribute.is_inline()) + } } /// An Attribute can be either a Primary Attribute or a Secondary Attribute @@ -641,6 +645,10 @@ impl Attribute { ["test"] => Attribute::Function(FunctionAttribute::Test(TestScope::None)), ["recursive"] => Attribute::Function(FunctionAttribute::Recursive), ["fold"] => Attribute::Function(FunctionAttribute::Fold), + ["inline", tag] => { + validate(tag)?; + Attribute::Function(FunctionAttribute::Inline(tag.to_string())) + } ["test", name] => { validate(name)?; let malformed_scope = @@ -693,6 +701,7 @@ pub enum FunctionAttribute { Test(TestScope), Recursive, Fold, + Inline(String), } impl FunctionAttribute { @@ -725,6 +734,13 @@ impl FunctionAttribute { pub fn is_foldable(&self) -> bool { matches!(self, FunctionAttribute::Fold) } + + /// Check whether we have an `inline` attribute + /// Although we also do not want to inline foldable functions, + /// we keep the two attributes distinct for clarity. + pub fn is_inline(&self) -> bool { + matches!(self, FunctionAttribute::Inline(_)) + } } impl fmt::Display for FunctionAttribute { @@ -736,6 +752,7 @@ impl fmt::Display for FunctionAttribute { FunctionAttribute::Oracle(ref k) => write!(f, "#[oracle({k})]"), FunctionAttribute::Recursive => write!(f, "#[recursive]"), FunctionAttribute::Fold => write!(f, "#[fold]"), + FunctionAttribute::Inline(ref k) => write!(f, "#[inline({k})]"), } } } @@ -781,6 +798,7 @@ impl AsRef for FunctionAttribute { FunctionAttribute::Test { .. } => "", FunctionAttribute::Recursive => "", FunctionAttribute::Fold => "", + FunctionAttribute::Inline(string) => string, } } } diff --git a/compiler/noirc_frontend/src/monomorphization/ast.rs b/compiler/noirc_frontend/src/monomorphization/ast.rs index 434cc922a05..dc43f53e038 100644 --- a/compiler/noirc_frontend/src/monomorphization/ast.rs +++ b/compiler/noirc_frontend/src/monomorphization/ast.rs @@ -5,8 +5,11 @@ use noirc_errors::{ Location, }; -use crate::ast::{BinaryOpKind, Distinctness, IntegerBitSize, Signedness, Visibility}; use crate::hir_def::function::FunctionSignature; +use crate::{ + ast::{BinaryOpKind, Distinctness, IntegerBitSize, Signedness, Visibility}, + token::{Attributes, FunctionAttribute}, +}; /// The monomorphized AST is expression-based, all statements are also /// folded into this expression enum. Compared to the HIR, the monomorphized @@ -200,6 +203,60 @@ pub enum LValue { pub type Parameters = Vec<(LocalId, /*mutable:*/ bool, /*name:*/ String, Type)>; +/// Represents how an Acir function should be inlined. +/// This type is only relevant for ACIR functions as we do not inline any Brillig functions +#[derive(Default, Clone, Copy, PartialEq, Eq, Debug, Hash)] +pub enum InlineType { + /// The most basic entry point can expect all its functions to be inlined. + /// All function calls are expected to be inlined into a single ACIR. + #[default] + Inline, + /// Functions marked as foldable will not be inlined and compiled separately into ACIR + Fold, + /// Similar to `Fold`, these functions will not be inlined and compile separately into ACIR. + /// They are different from `Fold` though as they are expected to be inlined into the program + /// entry point before being used in the backend. + Never, +} + +impl From<&Attributes> for InlineType { + fn from(attributes: &Attributes) -> Self { + attributes.function.as_ref().map_or(InlineType::default(), |func_attribute| { + match func_attribute { + FunctionAttribute::Fold => InlineType::Fold, + FunctionAttribute::Inline(tag) => { + if tag == "never" { + InlineType::Never + } else { + InlineType::default() + } + } + _ => InlineType::default(), + } + }) + } +} + +impl InlineType { + pub fn is_entry_point(&self) -> bool { + match self { + InlineType::Inline => false, + InlineType::Fold => true, + InlineType::Never => true, + } + } +} + +impl std::fmt::Display for InlineType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + InlineType::Inline => write!(f, "inline"), + InlineType::Fold => write!(f, "fold"), + InlineType::Never => write!(f, "inline(never)"), + } + } +} + #[derive(Debug, Clone, Hash)] pub struct Function { pub id: FuncId, @@ -210,7 +267,7 @@ pub struct Function { pub return_type: Type, pub unconstrained: bool, - pub should_fold: bool, + pub inline_type: InlineType, pub func_sig: FunctionSignature, } diff --git a/compiler/noirc_frontend/src/monomorphization/mod.rs b/compiler/noirc_frontend/src/monomorphization/mod.rs index b130af9d125..b5754897d3f 100644 --- a/compiler/noirc_frontend/src/monomorphization/mod.rs +++ b/compiler/noirc_frontend/src/monomorphization/mod.rs @@ -30,6 +30,7 @@ use std::{ unreachable, }; +use self::ast::InlineType; use self::debug_types::DebugTypeTracker; use self::{ ast::{Definition, FuncId, Function, LocalId, Program}, @@ -132,7 +133,7 @@ pub fn monomorphize_debug( .finished_functions .iter() .flat_map(|(_, f)| { - if f.should_fold || f.id == Program::main_id() { + if f.inline_type.is_entry_point() || f.id == Program::main_id() { Some(f.func_sig.clone()) } else { None @@ -304,7 +305,8 @@ impl<'interner> Monomorphizer<'interner> { let return_type = Self::convert_type(return_type, meta.location)?; let unconstrained = modifiers.is_unconstrained; - let should_fold = meta.should_fold; + let attributes = self.interner.function_attributes(&f); + let inline_type = InlineType::from(attributes); let parameters = self.parameters(&meta.parameters)?; let body = self.expr(body_expr_id)?; @@ -315,7 +317,7 @@ impl<'interner> Monomorphizer<'interner> { body, return_type, unconstrained, - should_fold, + inline_type, func_sig, }; @@ -1398,7 +1400,7 @@ impl<'interner> Monomorphizer<'interner> { body, return_type, unconstrained, - should_fold: false, + inline_type: InlineType::default(), func_sig: FunctionSignature::default(), }; self.push_function(id, function); @@ -1524,7 +1526,7 @@ impl<'interner> Monomorphizer<'interner> { body, return_type, unconstrained, - should_fold: false, + inline_type: InlineType::default(), func_sig: FunctionSignature::default(), }; self.push_function(id, function); @@ -1649,7 +1651,7 @@ impl<'interner> Monomorphizer<'interner> { body, return_type, unconstrained, - should_fold: false, + inline_type: InlineType::default(), func_sig: FunctionSignature::default(), }; self.push_function(id, function); diff --git a/compiler/noirc_frontend/src/tests.rs b/compiler/noirc_frontend/src/tests.rs index c6cef568ed8..5d0f2472a43 100644 --- a/compiler/noirc_frontend/src/tests.rs +++ b/compiler/noirc_frontend/src/tests.rs @@ -1282,4 +1282,36 @@ fn lambda$f1(mut env$l1: (Field)) -> Field { "#; assert_eq!(get_program_errors(src).len(), 0); } + + #[test] + fn deny_inline_attribute_on_unconstrained() { + let src = r#" + #[inline(never)] + unconstrained fn foo(x: Field, y: Field) { + assert(x != y); + } + "#; + let errors = get_program_errors(src); + assert_eq!(errors.len(), 1); + assert!(matches!( + errors[0].0, + CompilationError::ResolverError(ResolverError::InlineAttributeOnUnconstrained { .. }) + )); + } + + #[test] + fn deny_fold_attribute_on_unconstrained() { + let src = r#" + #[fold] + unconstrained fn foo(x: Field, y: Field) { + assert(x != y); + } + "#; + let errors = get_program_errors(src); + assert_eq!(errors.len(), 1); + assert!(matches!( + errors[0].0, + CompilationError::ResolverError(ResolverError::FoldAttributeOnUnconstrained { .. }) + )); + } } diff --git a/test_programs/execution_success/inline_never_basic/Nargo.toml b/test_programs/execution_success/inline_never_basic/Nargo.toml new file mode 100644 index 00000000000..16691770d76 --- /dev/null +++ b/test_programs/execution_success/inline_never_basic/Nargo.toml @@ -0,0 +1,7 @@ +[package] +name = "inline_never_basic" +type = "bin" +authors = [""] +compiler_version = ">=0.27.0" + +[dependencies] \ No newline at end of file diff --git a/test_programs/execution_success/inline_never_basic/Prover.toml b/test_programs/execution_success/inline_never_basic/Prover.toml new file mode 100644 index 00000000000..f28f2f8cc48 --- /dev/null +++ b/test_programs/execution_success/inline_never_basic/Prover.toml @@ -0,0 +1,2 @@ +x = "5" +y = "10" diff --git a/test_programs/execution_success/inline_never_basic/src/main.nr b/test_programs/execution_success/inline_never_basic/src/main.nr new file mode 100644 index 00000000000..1922aaedb6c --- /dev/null +++ b/test_programs/execution_success/inline_never_basic/src/main.nr @@ -0,0 +1,8 @@ +fn main(x: Field, y: pub Field) { + basic_check(x, y); +} + +#[inline(never)] +fn basic_check(x: Field, y: Field) { + assert(x != y); +} diff --git a/tooling/debugger/ignored-tests.txt b/tooling/debugger/ignored-tests.txt index 3b63f8d5542..8f49fe273e1 100644 --- a/tooling/debugger/ignored-tests.txt +++ b/tooling/debugger/ignored-tests.txt @@ -17,4 +17,4 @@ fold_basic_nested_call fold_call_witness_condition fold_after_inlined_calls fold_numeric_generic_poseidon - +inline_never_basic