///|
/// Precompiled rule programs.
///
/// The compile_rules operation validates and canonicalizes a rule set
/// once, returning an opaque program list whose conditions and compute
/// expressions are canonical renderings. The run_program operation then
/// executes that program over fact batches without re-deriving anything:
/// parsing already happened at compile time, canonical conditions parse
/// to identical trees, and structural identity is re-verified so a
/// tampered program is rejected with a coded error.
///|
fn compile_rules_request(request : Value) -> Value raise {
let rules = arr(get(request, "rules"))
let program : Array[Value] = []
let conditions : Map[String, Bool] = Map([])
let mut index = 0
for item in rules {
let id = str(get(item, "id"))
let priority = optional_num(get(item, "priority"), 0.0)
if priority.is_nan() || priority.is_inf() {
raise input_error("E3004", "priority for rule " + id + " must be finite")
}
let condition = compile(str(get(item, "when")))
let canonical_condition = condition.render()
ignore(compile(canonical_condition))
let output = get(item, "output")
let compute_value : Value = match get(item, "compute") {
Null => Null
String(source) => {
let expr = compile(source)
let canonical = expr.render()
ignore(compile(canonical))
String(canonical)
}
_ => raise input_error("E4007", "compute must be a string")
}
if conditions.contains(canonical_condition) {
program.push(
record([
("id", String(id)),
("priority", Number(priority)),
("condition", String(canonical_condition)),
("compute", compute_value),
("output", output),
("duplicate_condition", Bool(true)),
]),
)
} else {
conditions[canonical_condition] = true
program.push(
record([
("id", String(id)),
("priority", Number(priority)),
("condition", String(canonical_condition)),
("compute", compute_value),
("output", output),
("duplicate_condition", Bool(false)),
]),
)
}
index += 1
}
ignore(index)
let mut duplicates = 0
for entry in program {
if get(entry, "duplicate_condition") is Bool(true) {
duplicates += 1
}
}
record([
("program", Array(program)),
("rule_count", Number(rules.length().to_double())),
("duplicate_conditions", Number(duplicates.to_double())),
])
}
///|
fn program_rule(item : Value) -> Rule raise {
let id = str(get(item, "id"))
let priority = optional_num(get(item, "priority"), 0.0)
let condition_source = str(get(item, "condition"))
let condition = compile(condition_source)
if condition.render() != condition_source {
raise input_error(
"E3003",
"program condition for " + id + " is not canonical",
)
}
let computed : Expression? = match get(item, "compute") {
Null => None
String(source) => {
let expr = compile(source)
if expr.render() != source {
raise input_error(
"E3003",
"program compute for " + id + " is not canonical",
)
}
Some(expr)
}
_ => raise input_error("E4007", "compute must be a string")
}
{ id, priority, condition, output: get(item, "output"), computed, }
}
///|
fn run_program_request(request : Value) -> Value raise {
let program = arr(get(request, "program"))
let ids : Map[String, Bool] = Map([])
let rules : Array[Rule] = []
for item in program {
let rule = program_rule(item)
if ids.contains(rule.id) {
raise input_error("E4001", "duplicate rule ID: " + rule.id)
}
ids[rule.id] = true
rules.push(rule)
}
rules.sort_by(fn(a, b) {
let cmp = b.priority.compare(a.priority)
if cmp == 0 {
stable_string_compare(a.id, b.id)
} else {
cmp
}
})
let mode = optional_str(get(request, "mode"), "first")
if !["first", "all", "unique"].contains(mode) {
raise input_error("E3006", "unknown decision mode")
}
let budget = optional_num(get(request, "budget"), 10000.0).to_int()
if budget <= 0 || budget > 1000000 {
raise input_error("E3007", "budget out of range")
}
let results : Array[Value] = []
let facts = match get(request, "facts") {
Array(items) => items
other => [other]
}
for i = 0; i < facts.length(); i = i + 1 {
results.push(
record([
("index", Number(i.to_double())),
(
"result",
decide(rules, facts[i], mode, get(request, "default"), budget),
),
]),
) catch {
error =>
results.push(
record([
("index", Number(i.to_double())),
("error", String(error.to_string())),
]),
)
}
}
record([
("results", Array(results)),
("rule_count", Number(rules.length().to_double())),
])
}