To define the order of execution, fn_graph uses two kinds of dependencies – logical and data. Logical dependencies are defined by consumers of the crate. Data dependencies are calculated by inspecting the type and mutability of function parameters.

Using the example:

type Fn1 = fn(&mut i32, &mut i32, &mut i32, &mut i32);
type Fn2 = fn(&i32, &mut i32);
type Fn3 = fn(&i32, &mut i32);
type Fn4 = fn(&i32, &mut i32);
type Fn5 = fn(&i32, &i32, &i32);

let fn1: Fn1 = |a, b, c, d| { *a = 1; *b = 2; *c = 0; *d = 0; };
let fn2: Fn2 = |a,    c   | *c += *a;
let fn3: Fn3 = |   b, c   | *c += *b;
let fn4: Fn4 = |      c, d| *d += *c;
let fn5: Fn5 = |a, b, c   | println!("{a} + {b} = {c}");

Even though it is possible to order these sequentially, we also want to be able to achieve concurrency to increase performance.


Logical Dependencies

To encode this into a function graph, logical dependencies can be specified through edges:


For correctness, fn1 must be executed before both fn2 and fn3, and those must complete before fn4 and fn5 are executed.

Data Dependencies

Note that fn4 and fn5 can both be executed in parallel, as there is no overlap in accessed data.

However, fn2 and fn3 both require mutable access to C. Since it is not possible to safely provide both fn2 and fn3 with mutable access, fn_graph automatically introduces a data dependency between those functions to ensure consistent ordering: