Ordering
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: