///|
priv struct Rule {
id : String
priority : Double
condition : Expression
output : Value
computed : Expression?
}
///|
fn load_rules(source : Value) -> Array[Rule] raise {
let ids : Map[String, Bool] = Map([])
let rules : Array[Rule] = []
for item in arr(source) {
let id = str(get(item, "id"))
if id == "" || ids.contains(id) {
raise input_error("E4001", "duplicate rule ID: " + id)
}
ids[id] = true
rules.push({
id,
priority: optional_num(get(item, "priority"), 0.0),
condition: compile(str(get(item, "when"))),
output: get(item, "output"),
computed: if get(item, "compute") == Null {
None
} else {
Some(compile(str(get(item, "compute"))))
},
})
}
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
}
})
rules
}
///|
fn decide(
rules : Array[Rule],
facts : Value,
mode : String,
fallback : Value,
budget : Int32,
) -> Value raise {
ignore(obj(facts))
let matched : Array[Value] = []
let evaluated : Array[Value] = []
let state : Evaluation = { fuel: budget, trace: [], }
for rule in rules {
let start = state.trace.length()
let hits = boolean(evaluate(rule.condition, facts, state))
let trace : Array[Value] = []
for i = start; i < state.trace.length(); i = i + 1 {
trace.push(state.trace[i])
}
evaluated.push(
record([
("id", String(rule.id)),
("matched", Bool(hits)),
("trace", Array(trace)),
]),
)
if hits {
let output = match rule.computed {
Some(expr) => evaluate(expr, facts, state)
None => rule.output
}
matched.push(
record([
("id", String(rule.id)),
("priority", Number(rule.priority)),
("value", output),
]),
)
}
}
let conflict = mode == "unique" && matched.length() > 1
let conflict_ids : Array[String] = if conflict {
matched.map(item => str(get(item, "id")))
} else {
[]
}
let decision = if matched.is_empty() {
fallback
} else if conflict {
Null
} else if mode == "all" {
Array(matched.map(fn(item) { get(item, "value") }))
} else {
get(matched[0], "value")
}
record([
("decision", decision),
("conflict", Bool(conflict)),
("conflict_ids", strings(conflict_ids)),
("matched", Array(matched)),
("evaluated", Array(evaluated)),
("steps", Number((budget - state.fuel).to_double())),
])
}
///|
pub fn run(request : Value) -> Value raise {
if get(request, "operation") == String("replay_versions") {
return replay_versions_request(request)
}
let operation = optional_str(get(request, "operation"), "decide")
if operation == "decide" || operation == "evaluate" {
let requested = optional_num(get(request, "budget"), 10000.0)
if requested < 1.0 ||
requested > 1000000.0 ||
requested.floor() != requested {
raise input_error("E3007", "budget must be a bounded positive integer")
}
}
if operation == "optimize" {
return optimize_request(request)
}
if operation == "compile_rules" {
return compile_rules_request(request)
}
if operation == "run_program" {
return run_program_request(request)
}
if operation == "coverage" {
return coverage_request(request)
}
if operation == "rule_graph" {
return rule_graph_request(request)
}
if operation == "pick" {
return pick_request(request)
}
if operation == "ast" {
return ast_request(request)
}
if operation == "bundle_validate" {
return bundle_validate_request(request)
}
if operation == "bundle_resolve" {
return bundle_resolve_request(request)
}
if operation == "decision_table" {
return decision_table_request(request)
}
if operation == "decide_table" {
return decide_table_request(request)
}
if operation == "lint" {
return lint_request(request)
}
if operation == "fmt" {
return fmt_rules_request(request)
}
if operation == "export_schema" {
return export_schema_request(request)
}
if operation == "migrate" {
return migrate_request(request)
}
if operation == "validate" {
return validate_request(request)
}
if operation == "conflicts" {
return conflicts_request(request)
}
if operation == "stats" {
return stats_request(request)
}
if operation == "plan_waves" {
return wave_request(request)
}
if operation == "snapshot" {
return snapshot_request(request)
}
if operation == "compare" {
return compare_request(request)
}
if operation == "template" {
return template_request(request)
}
if operation == "score" {
return score_request(request)
}
if operation == "explain_rules" {
return explain_rules_request(request)
}
if operation == "merge" {
return merge_request(request)
}
if operation == "matrix" {
return matrix_request(request)
}
if operation == "audit" {
return audit_request(request)
}
if operation == "check" {
return check_request(request)
}
if operation == "serialize" {
return serialize_request(request)
}
if operation == "roundtrip" {
return roundtrip_request(request)
}
if operation == "aggregate" {
return aggregate_request(request)
}
if operation == "infer" {
return infer_request(request)
}
if operation == "analyze" {
return analyze_rules(request)
}
if operation == "explain" {
return explain_request(request)
}
if operation == "evaluate" {
let budget = optional_num(get(request, "budget"), 10000.0).to_int()
if budget <= 0 || budget > 1000000 {
raise input_error("E3007", "budget out of range")
}
return record([
(
"value",
if truth(get(request, "optimize")) {
let state : Evaluation = { fuel: budget, trace: [], }
evaluate(
optimize_expr(compile(str(get(request, "expression")))),
get(request, "facts"),
state,
)
} else {
evaluate_expression(
str(get(request, "expression")),
get(request, "facts"),
budget~,
)
},
),
])
}
if operation == "diff" {
ignore(load_rules(get(request, "old")))
ignore(load_rules(get(request, "new")))
let old_rules : Map[String, Value] = Map([])
let new_rules : Map[String, Value] = Map([])
for r in arr(get(request, "old")) {
old_rules[str(get(r, "id"))] = r
}
for r in arr(get(request, "new")) {
new_rules[str(get(r, "id"))] = r
}
let added : Array[String] = []
let removed : Array[String] = []
let changed : Array[String] = []
for id, r in new_rules {
match old_rules.get(id) {
None => added.push(id)
Some(old) => if old != r { changed.push(id) }
}
}
for id, _ in old_rules {
if !new_rules.contains(id) {
removed.push(id)
}
}
added.sort_by(stable_string_compare)
removed.sort_by(stable_string_compare)
changed.sort_by(stable_string_compare)
return record([
("added", strings(added)),
("removed", strings(removed)),
("changed", strings(changed)),
])
}
if operation != "decide" {
raise input_error("E3005", "unknown operation")
}
let rules = load_rules(get(request, "rules"))
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())),
])
}