Skip to content

load_store_forwarding: immutable references passed to a call shouldn't invalidate cached data #12333

@asterite

Description

@asterite

When analyzing a Call instruction, whenever an argument that is a reference type is found, we invalidate data related to it:

Instruction::Call { .. } => {
// Simple reference (`&mut T` where T has no refs): invalidate that
// address and all its potential aliases.
// Container or nested reference: clear all state.
instruction.for_each_value(|value| {
let value = inserter.resolve(value);
let typ = inserter.function.dfg.type_of_value(value);
let is_simple_ref = matches!(typ.reference_element_type(), Some(inner) if !inner.contains_reference());
if is_simple_ref {
let dfg = &inserter.function.dfg;
known_values
.retain(|k, _| !may_alias(value, *k, allocations, dfg));
last_loads
.retain(|k, _| !may_alias(value, *k, allocations, dfg));
last_stores
.retain(|k, _| !may_alias(value, *k, allocations, dfg));
} else if typ.contains_reference() {
known_values.clear();
last_loads.clear();
last_stores.clear();
}
});

However, this should only be necessary if the argument's type contains any mutable reference. If the type only has immutable references there's no way an alias of that argument will change because of the call.

For example this test should pass but currently doesn't:

    #[test]
    fn call_with_immutable_reference_does_not_invalidate_cache() {
        // A call that only receives an immutable reference cannot write through
        // it, so cached values for that address must remain valid after the call.
        // Currently the pass treats &T the same as &mut T in the call handler
        // (is_simple_ref fires for both), so it incorrectly invalidates the
        // cached load and fails to forward v2 to v1.
        let src = "
        brillig(inline) fn main f0 {
          b0(v0: &Field):
            v1 = load v0 -> Field
            call f1(v0)
            v2 = load v0 -> Field
            return v2
        }
        brillig(inline) fn f1 f1 {
          b0(v0: &Field):
            return
        }
        ";
        let ssa = Ssa::from_str(src).unwrap();
        let ssa = ssa.load_store_forwarding();

        // The call only holds an immutable reference; it cannot modify v0's
        // memory. The second load should be forwarded to v1.
        assert_ssa_snapshot!(ssa, @r"
        brillig(inline) fn main f0 {
          b0(v0: &Field):
            v1 = load v0 -> Field
            call f1(v0)
            return v1
        }
        brillig(inline) fn f1 f1 {
          b0(v0: &Field):
            return
        }
        ");
    }

This is just an optimization, though.

Metadata

Metadata

Assignees

No one assigned

    Labels

    auditPreparation or execution of security code audits.ssa

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions