///|
/// Engine self-description.
///
/// The stats operation summarizes a rule set the way a maintainer
/// would ask about it: rule counts with and without compute, priority
/// range, aggregated condition size (nodes and depth), the union of
/// fact-path dependencies with per-path usage counts, and the
/// functions the rule set exercises. It never evaluates conditions
/// against facts; everything is derived from the compiled trees.
///|
fn collect_calls(expr : Expression, names : Map[String, Bool]) -> Unit {
match expr {
Literal(_) | Variable(_) => ()
Unary(_, operand) => collect_calls(operand, names)
Binary(_, left, right) => {
collect_calls(left, names)
collect_calls(right, names)
}
Call(name, args) => {
names[name] = true
for arg in args {
collect_calls(arg, names)
}
}
}
}
///|
fn note_rule_paths(
path_count : Map[String, Int32],
paths : Map[String, Bool],
) -> Unit {
for path, _ in paths {
match path_count.get(path) {
Some(n) => path_count[path] = n + 1
None => path_count[path] = 1
}
}
}
///|
fn stats_request(request : Value) -> Value raise {
let rules = arr(get(request, "rules"))
let with_compute : Array[Value] = []
let priorities : Array[Double] = []
let mut total_nodes = 0
let mut max_depth = 0
let path_count : Map[String, Int32] = Map([])
let fn_count : Map[String, Int32] = Map([])
let mut computes = 0
for item in rules {
let condition = compile(str(get(item, "when")))
let (nodes, depth) = condition.metrics()
total_nodes += nodes
if depth > max_depth {
max_depth = depth
}
let paths : Map[String, Bool] = Map([])
collect_paths(condition, paths)
let calls : Map[String, Bool] = Map([])
collect_calls(condition, calls)
match get(item, "compute") {
Null => ()
String(source) => {
computes += 1
let compute = compile(source)
let (cn, cd) = compute.metrics()
total_nodes += cn
if cd > max_depth {
max_depth = cd
}
collect_paths(compute, paths)
collect_calls(compute, calls)
with_compute.push(String(str(get(item, "id"))))
}
_ => raise input_error("E4007", "compute must be a string")
}
// A path counts once per rule that uses it, wherever it appears.
note_rule_paths(path_count, paths)
for name, _ in calls {
match fn_count.get(name) {
Some(n) => fn_count[name] = n + 1
None => fn_count[name] = 1
}
}
let priority = optional_num(get(item, "priority"), 0.0)
priorities.push(priority)
}
priorities.sort_by(fn(a, b) { a.compare(b) })
let min_priority = match priorities.get(0) {
Some(value) => value
None => 0.0
}
let max_priority = match priorities.get(priorities.length() - 1) {
Some(value) => value
None => 0.0
}
let names : Array[String] = []
for path, _ in path_count {
names.push(path)
}
names.sort_by(fn(a, b) { stable_string_compare(a, b) })
let path_report : Array[Value] = []
for path in names {
path_report.push(
record([
("path", String(path)),
(
"rules",
Number(
(match path_count.get(path) {
Some(n) => n
None => 0
}).to_double(),
),
),
]),
)
}
let fn_names : Array[String] = []
for name, _ in fn_count {
fn_names.push(name)
}
fn_names.sort_by(fn(a, b) { stable_string_compare(a, b) })
let avg_nodes = if rules.length() == 0 {
0.0
} else {
total_nodes.to_double() / rules.length().to_double()
}
record([
("rule_count", Number(rules.length().to_double())),
("compute_count", Number(computes.to_double())),
("compute_rules", Array(with_compute)),
("min_priority", Number(min_priority)),
("max_priority", Number(max_priority)),
("condition_nodes", Number(total_nodes.to_double())),
("avg_nodes", Number(avg_nodes)),
("max_depth", Number(max_depth.to_double())),
("condition_paths", Number(names.length().to_double())),
("paths", Array(path_report)),
("functions", strings(fn_names)),
])
}