Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
174 changes: 152 additions & 22 deletions compiler/noirc_evaluator/src/ssa/opt/defunctionalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,13 @@ impl DefunctionalizationContext {
};

// Find the correct apply function
let Some(apply_function) =
self.get_apply_function(signature.clone(), func.runtime())
let Some(apply_function) = self.get_apply_function(signature, func.runtime())
else {
// If there is no apply function then this should be a parameter in a function
// that will never actually be called, and the DIE pass will eventually remove it.
continue;
// We should have generated an apply function for all function's used as a value,
// even if there are no variants for that function call.
panic!(
"ICE: It is expected to have an apply function for every function used as a value"
);
};

// Replace the instruction with a call to apply
Expand Down Expand Up @@ -405,6 +406,10 @@ fn find_dynamic_dispatches(func: &Function) -> BTreeSet<Signature> {
/// for a specific ([Signature], [RuntimeType]) group.
/// Otherwise, if there is a single variant that function is simply reused.
///
/// If there are no variants a dummy function is created.
/// A dummy function acts as a safe no-op to continue compilation even though there are no variants
/// for a first-class function call. For more information you can reference [create_dummy_function].
///
/// # Arguments
/// - `ssa`: A mutable reference to the full [Ssa] structure containing all functions.
/// - `variants_map`: [Variants]
Expand All @@ -417,10 +422,6 @@ fn create_apply_functions(ssa: &mut Ssa, variants_map: Variants) -> ApplyFunctio
let mut apply_functions = HashMap::default();

for ((signature, runtime), variants) in variants_map.into_iter() {
if variants.is_empty() {
// If no variants exist for a dynamic call we leave removing those dead parameters to DIE
continue;
}
let dispatches_to_multiple_functions = variants.len() > 1;

// This will be the same signature but with each function type replaced with
Expand All @@ -443,9 +444,14 @@ fn create_apply_functions(ssa: &mut Ssa, variants_map: Variants) -> ApplyFunctio
// If we have multiple variants for this signature and runtime type group
// we need to generate an apply function.
create_apply_function(ssa, defunctionalized_signature, runtime, variants)
} else {
} else if !variants.is_empty() {
// If there is only variant, we can use it directly rather than creating a new apply function.
variants[0]
} else {
// If no variants exist for a dynamic call we leave removing those dead calls and parameters to DIE.
// However, we have to construct a dummy function for these dead calls as to keep a well formed SSA
// and to not break the semantics of other SSA passes before DIE is reached.
create_dummy_function(ssa, defunctionalized_signature, runtime)
};
apply_functions
.insert((signature, runtime), ApplyFunction { id, dispatches_to_multiple_functions });
Expand Down Expand Up @@ -616,6 +622,86 @@ fn create_apply_function(
})
}

/// Creates a placeholder (dummy) function to replace calls to invalid function references.
/// An example of a possible invalid function reference is an out-of-bounds access on a zero-length function array.
///
/// This prevents the compiler from crashing by ensuring that the IR always has a valid function to call.
/// The dummy function is created using the supplied function's signature as to maintain a well formed SSA IR.
/// The dummy function is pure, contains no logic, and just returns zeroed out values for its return types.
///
/// This is especially useful in cases where we cannot statically resolve the function reference,
/// but want to continue compiling the rest of the program safely.
///
/// Returns the [FunctionId] of the newly created dummy function.
fn create_dummy_function(
ssa: &mut Ssa,
signature: Signature,
caller_runtime: RuntimeType,
) -> FunctionId {
ssa.add_fn(|id| {
let mut function_builder = FunctionBuilder::new("apply_dummy".to_string(), id);

// Set the runtime of the dummy function. The dummy function is expect to always be simplified out
// but we let the caller set the runtime here as to match Noir's runtime semantics.
let runtime = match caller_runtime {
RuntimeType::Acir(_) => RuntimeType::Acir(InlineType::InlineAlways),
RuntimeType::Brillig(_) => RuntimeType::Brillig(InlineType::InlineAlways),
};
function_builder.set_runtime(runtime);

// The remaining dummy function parameters are the actual parameters of the function call without any variants.
// We generate these just to maintain a well-formed IR. Not doing this could result in errors if the dummy function
// was set to be inlined before the call to it was removed by DIE.
vecmap(signature.params, |typ| function_builder.add_parameter(typ));

// We can mark the dummy function pure as all it does is return.
// As the dummy function is just meant to be a placeholder for any calls to
// higher-order functions without variants, we want the function to be marked pure
// so that dead instruction elimination can remove any calls to it.
let mut purities = HashMap::default();
purities.insert(id, super::pure::Purity::Pure);
function_builder.set_purities(Arc::new(purities));

let results =
vecmap(signature.returns, |typ| make_dummy_return_data(&mut function_builder, &typ));

function_builder.terminate_with_return(results);

function_builder.current_function
})
}

/// Construct a dummy value to be returned from the placeholder function for calls to invalid lambda references.
/// We need to construct the appropriate value so that our SSA is well formed even if the function
/// pointer has no variants.
fn make_dummy_return_data(function_builder: &mut FunctionBuilder, typ: &Type) -> ValueId {
match typ {
Type::Numeric(numeric_type) => function_builder.numeric_constant(0_u128, *numeric_type),
Type::Array(element_types, len) => {
let mut array = im::Vector::new();
for _ in 0..*len {
for typ in element_types.iter() {
array.push_back(make_dummy_return_data(function_builder, typ));
}
}
function_builder.insert_make_array(array, typ.clone())
}
Type::Slice(_) => {
let array = im::Vector::new();
// The contents of a slice do not matter for a dummy function, we simply
// desire to have a well formed SSA by returning the correct value for a type.
// Thus, we return an empty slice here.
function_builder.insert_make_array(array, typ.clone())
}
Type::Reference(element_type) => function_builder.insert_allocate((**element_type).clone()),
Type::Function => {
unreachable!(
"ICE: Any function passed as a value should have already been converted to a field type"
)
}
}
}

/// Check post-execution properties:
/// * All blocks which took function parameters should receive a discriminator instead
#[cfg(debug_assertions)]
Expand Down Expand Up @@ -964,8 +1050,12 @@ mod tests {
}
brillig(inline) fn func_2 f2 {
b0(v0: Field):
v2 = call v0(u128 1) -> u1
return v2
v3 = call f3(u128 1) -> u1
return v3
}
brillig(inline_always) pure fn apply_dummy f3 {
b0(v0: u128):
return u1 0
}
");
}
Expand Down Expand Up @@ -1178,34 +1268,74 @@ mod tests {
}

#[test]
fn empty_make_array_updates_type() {
fn empty_make_array_with_functions() {
let src = r#"
acir(inline) fn main f0 {
b0(v0: u32):
v1 = make_array [] : [function; 0]
b0():
v0 = make_array [] : [function; 0]
constrain u1 0 == u1 1, "Index out of bounds"
v5 = array_get v1, index u32 0 -> function
call v5()
v4 = array_get v0, index u32 0 -> function
v6, v7 = call v4(u32 5) -> (u32, [u32; 2])
return
}
"#;

let ssa = Ssa::from_str(src).unwrap();
let ssa = ssa.defunctionalize();

// Guarantee that we still accurately modify the make_array instruction type for an empty array
// Guarantee we make the following updates:
// 1. The make_array instruction type is modified
// 2. We generate a dummy function which is used to modify function calls when there are no variants
assert_ssa_snapshot!(ssa, @r#"
acir(inline) fn main f0 {
b0(v0: u32):
v1 = make_array [] : [Field; 0]
b0():
v0 = make_array [] : [Field; 0]
constrain u1 0 == u1 1, "Index out of bounds"
v5 = array_get v1, index u32 0 -> Field
call v5()
v4 = array_get v0, index u32 0 -> Field
v7, v8 = call f1(u32 5) -> (u32, [u32; 2])
return
}
acir(inline_always) pure fn apply_dummy f1 {
b0(v0: u32):
v2 = make_array [u32 0, u32 0] : [u32; 2]
return u32 0, v2
}
"#);
}

#[test]
fn empty_make_array_with_functions_returning_functions() {
let src = "
acir(inline) fn main f0 {
b0():
v0 = make_array [] : [function; 0]
constrain u1 0 == u1 1
v4 = array_get v0, index u32 0 -> function
v6 = call v4(u32 2) -> function
return
}
";

let ssa = Ssa::from_str(src).unwrap();
let ssa = ssa.defunctionalize();
println!("{}", ssa);

assert_ssa_snapshot!(ssa, @r"
acir(inline) fn main f0 {
b0():
v0 = make_array [] : [Field; 0]
constrain u1 0 == u1 1
v4 = array_get v0, index u32 0 -> Field
v7 = call f1(u32 2) -> Field
return
}
acir(inline_always) pure fn apply_dummy f1 {
b0(v0: u32):
return Field 0
}
");
}

#[test]
fn mut_ref_function() {
let src = "
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[package]
name = "lambda_from_empty_array"
type = "bin"
authors = [""]

[dependencies]
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Regression from issue #5503 (https://github.com/noir-lang/noir/issues/5503)
fn main() {
let lambdas: [fn((u32)) -> (u32, [u32; 2]); 0] = [];
let _ = lambdas[0]((0));

let lambdas: [fn((u32)) -> (&mut u32); 0] = [];
let _ = lambdas[0]((1));

let lambdas: [fn((u32)) -> (fn((u32)) -> ()); 0] = [];
let _ = lambdas[0]((2));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[package]
name = "lambda_from_empty_array_dyn_index"
type = "bin"
authors = [""]

[dependencies]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
x = 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Regression from issue #5503 (https://github.com/noir-lang/noir/issues/5503),
// except this test uses a dynamic index for the array access.
fn main(x: u32) {
let lambdas: [fn(()) -> (); 0] = [];
lambdas[x - 1](());
}

5 changes: 0 additions & 5 deletions test_programs/execution_success/lambda_from_array/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,6 @@ fn main(x: u32) {
let lambdas: [fn(()) -> ()] = &[|_: ()| {}];
lambdas[0](());
lambdas[x - 1](());

// Still panics when there are no other lambdas
// This should fail either way as we are attempting to access an empty array at zero
// let lambdas: [fn(()) -> (); 0] = [];
// lambdas[0](());
}

fn lambdas_in_array_literal(x: u32) {
Expand Down
Loading
Loading