type expr = | Apply of string * Pattern.t Label.Map.t | And of expr * expr | Or of expr * expr | Not of expr * expr | Forall of expr * expr type rule = { rule_name : string; head : Pattern.t Label.Map.t; body : expr; witness : Label.t option; } type primitive = D_integer | D_natural | D_rational | D_string type domain = | Primitive of primitive | Criterion of { subject : string; body : expr } type def = | Table of { schema : (Label.t * domain) list; facts : Term.t Label.Map.t list } | Rules of rule list module type WORLD = sig type t val find : t -> string -> def option end type error = | Invalid_label of string | Duplicate_label of string | Zero_denominator | Lowercase_name of string | Term_expected of string | Proposition_expected of string | Malformed_directive of string | Unknown_domain of string | Unknown_relation of string | Already_defined of string | Fact_not_ground of string | Schema_mismatch of { relation : string; detail : string } | Domain_violation of { relation : string; label : string } | Non_total_rule of string | Mutual_recursion of string list | Criterion_arity of { relation : string; label : string } type pending = { pending_relation : string; pending_label : Label.t; pending_body : expr; } let pp_error fmt = function | Invalid_label l -> Format.fprintf fmt "invalid label: %s" l | Duplicate_label l -> Format.fprintf fmt "duplicate label: %s:" l | Zero_denominator -> Format.fprintf fmt "rational literal with zero denominator" | Lowercase_name n -> Format.fprintf fmt "lowercase name %s cannot be a variable or relation here" n | Term_expected d -> Format.fprintf fmt "a term was expected: %s" d | Proposition_expected d -> Format.fprintf fmt "a proposition was expected: %s" d | Malformed_directive d -> Format.fprintf fmt "malformed directive: %s" d | Unknown_domain d -> Format.fprintf fmt "unknown domain: %s" d | Unknown_relation r -> Format.fprintf fmt "unknown relation: %s" r | Already_defined r -> Format.fprintf fmt "relation already defined: %s" r | Fact_not_ground r -> Format.fprintf fmt "fact for %s is not ground" r | Schema_mismatch { relation; detail } -> Format.fprintf fmt "schema mismatch on %s: %s" relation detail | Domain_violation { relation; label } -> Format.fprintf fmt "domain violation on %s at %s:" relation label | Non_total_rule name -> Format.fprintf fmt "rule %S rejected: no structurally decreasing argument in its recursive call" name | Mutual_recursion cycle -> Format.fprintf fmt "mutual recursion rejected: %s" (String.concat " -> " cycle) | Criterion_arity { relation; label } -> Format.fprintf fmt "criterion on %s at %s: must have exactly one free variable, the subject" relation label let ( let* ) = Result.bind let is_uppercase s = String.length s > 0 && s.[0] >= 'A' && s.[0] <= 'Z' let label_of_string s = match Label.of_string s with | Ok l -> Ok l | Error (`Invalid_label l) -> Error (Invalid_label l) let bindings_to_map ~of_node bindings = List.fold_left (fun acc (name, node) -> let* map = acc in let* label = label_of_string name in if Label.Map.mem label map then Error (Duplicate_label name) else let* value = of_node node in Ok (Label.Map.add label value map)) (Ok Label.Map.empty) bindings (* Term position: uppercase names are variables. Blocks and combinators are propositions and cannot appear here. *) let rec pattern_of_node (node : Ast.node) : (Pattern.t, error) result = match node with | Ast.Name n -> if is_uppercase n then Ok (Pattern.Var n) else Error (Lowercase_name n) | Ast.Int z -> Ok (Pattern.Integer z) | Ast.Rat (num, den) -> ( match Term.rational ~num ~den with | Ok t -> Ok (Pattern.of_term t) | Error `Zero_denominator -> Error Zero_denominator) | Ast.Str s -> Ok (Pattern.String s) | Ast.Record fields -> let* map = bindings_to_map ~of_node:pattern_of_node fields in Ok (Pattern.Record map) | Ast.Seq (items, tail) -> let* tail = match tail with | None -> Ok Pattern.Nil | Some node -> pattern_of_node node in List.fold_right (fun item acc -> let* acc = acc in let* head = pattern_of_node item in Ok (Pattern.cons ~head ~tail:acc)) items (Ok tail) | Ast.Block (head, _) -> Error (Term_expected ("[" ^ head ^ " ...]")) | Ast.And _ -> Error (Term_expected ":and") | Ast.Or _ -> Error (Term_expected ":or") | Ast.Not _ -> Error (Term_expected ":not") (* Renaming a quantified variable apart. The stamp cannot be lexed, so quantifier scopes are unforgeable from source text. *) let quantifier_counter = ref 0 let rec rename_var ~from ~into = function | Pattern.Var v when String.equal v from -> Pattern.Var into | Pattern.Record map -> Pattern.Record (Label.Map.map (rename_var ~from ~into) map) | other -> other let rec rename_var_expr ~from ~into = function | Apply (name, bindings) -> Apply (name, Label.Map.map (rename_var ~from ~into) bindings) | And (a, b) -> And (rename_var_expr ~from ~into a, rename_var_expr ~from ~into b) | Or (a, b) -> Or (rename_var_expr ~from ~into a, rename_var_expr ~from ~into b) | Not (a, b) -> Not (rename_var_expr ~from ~into a, rename_var_expr ~from ~into b) | Forall (a, b) -> Forall (rename_var_expr ~from ~into a, rename_var_expr ~from ~into b) (* Proposition position: blocks apply relations, combinators compose. Terms cannot appear here. Exists and ForAll are the two higher-order forms; their variable is renamed apart, and Exists lowers to a conjunction because query variables are existential already. *) let rec expr_of_node (node : Ast.node) : (expr, error) result = match node with | Ast.Block (("Exists" | "ForAll") as form, bindings) -> ( match ( List.assoc_opt "variable" bindings, List.assoc_opt "quantifier" bindings, List.assoc_opt "body" bindings ) with | Some (Ast.Name variable), Some quantifier, Some body when is_uppercase variable && List.length bindings = 3 -> let* quantifier = expr_of_node quantifier in let* body = expr_of_node body in incr quantifier_counter; let fresh = variable ^ "@q" ^ string_of_int !quantifier_counter in let quantifier = rename_var_expr ~from:variable ~into:fresh quantifier and body = rename_var_expr ~from:variable ~into:fresh body in if String.equal form "Exists" then Ok (And (quantifier, body)) else Ok (Forall (quantifier, body)) | _ -> Error (Malformed_directive (form ^ " variable: Name quantifier: [...] body: [...]"))) | Ast.Block (head, bindings) -> let* map = bindings_to_map ~of_node:pattern_of_node bindings in Ok (Apply (head, map)) | Ast.And (a, b) -> let* a = expr_of_node a in let* b = expr_of_node b in Ok (And (a, b)) | Ast.Or (a, b) -> let* a = expr_of_node a in let* b = expr_of_node b in Ok (Or (a, b)) | Ast.Not (a, b) -> let* a = expr_of_node a in let* b = expr_of_node b in Ok (Not (a, b)) | Ast.Name n -> Error (Proposition_expected n) | Ast.Int _ | Ast.Rat _ | Ast.Str _ | Ast.Record _ | Ast.Seq _ -> Error (Proposition_expected "a literal") let rec expr_free_vars = function | Apply (_, bindings) -> Label.Map.fold (fun _ p acc -> acc @ Pattern.vars p) bindings [] | And (a, b) | Or (a, b) | Not (a, b) | Forall (a, b) -> expr_free_vars a @ expr_free_vars b let primitive_of_name = function | "Integer" -> Some D_integer | "Natural" -> Some D_natural | "Rational" -> Some D_rational | "String" -> Some D_string | _ -> None (* Membership of a ground term in a primitive domain. Note the inclusions of decision 12: a Natural is an Integer, an Integer is a Rational, all literally. *) let member_of_primitive (term : Term.t) = function | D_integer -> ( match term with Term.Integer _ -> true | _ -> false) | D_natural -> ( match term with Term.Integer z -> Z.geq z Z.zero | _ -> false) | D_rational -> ( match term with Term.Integer _ | Term.Rational _ -> true | _ -> false) | D_string -> ( match term with Term.String _ -> true | _ -> false) let rec applications = function | Apply (name, bindings) -> [ (name, bindings) ] | And (a, b) | Or (a, b) | Not (a, b) | Forall (a, b) -> applications a @ applications b module String_map = Map.Make (String) module Standalone = struct type t = def String_map.t let find world name = String_map.find_opt name world let schema_of_node ~relation = function | Ast.Seq (entries, None) -> List.fold_left (fun acc entry -> let* schema = acc in match entry with | Ast.Block (attr, [ ("domain", domain_node) ]) -> ( let* label = label_of_string attr in match domain_node with | Ast.Name name when Option.is_some (primitive_of_name name) -> let primitive = Option.get (primitive_of_name name) in Ok ((label, Primitive primitive) :: schema) | Ast.Name other -> Error (Unknown_domain other) | criterion -> (* A membership criterion: any relational expression with exactly one free variable, the subject. *) let* body = expr_of_node criterion in let subjects = List.sort_uniq String.compare (expr_free_vars body) in (match subjects with | [ subject ] -> Ok ((label, Criterion { subject; body }) :: schema) | _ -> Error (Criterion_arity { relation; label = Label.to_string label }))) | _ -> Error (Malformed_directive "schema entries are [attr domain: ...]")) (Ok []) entries |> Result.map List.rev | _ -> Error (Malformed_directive "schema: takes #( ... ) of entries") (* The structural decrease check of decision 10, rung one. Every self-recursive application must have an argument strictly contained in the head's argument at the same label; that label is kept as the runtime witness. *) let admit_rule ~relation rule_name head body = let recursive = applications body |> List.filter (fun (name, _) -> String.equal name relation) in match recursive with | [] -> Ok { rule_name; head; body; witness = None } | _ -> let witness_for (_, call_bindings) = Label.Map.fold (fun label head_arg found -> match found with | Some _ -> found | None -> ( match Label.Map.find_opt label call_bindings with | Some call_arg when Pattern.strictly_contains head_arg ~sub:call_arg -> Some label | _ -> None)) head None in let witnesses = List.map witness_for recursive in if List.for_all Option.is_some witnesses then (* All recursive calls decrease; any common witness works at runtime, and per-call witnesses agree by label here because they are all drawn from the same head. *) Ok { rule_name; head; body; witness = List.nth witnesses 0 } else Error (Non_total_rule rule_name) let load statements = (* First pass: declare tables, collect raw material. *) let* world, raw_facts, raw_rules, raw_queries = List.fold_left (fun acc statement -> let* world, facts, rules, queries = acc in match (statement : Ast.statement) with | Ast.Directive ("Relation", [ ("named", Ast.Name name); ("schema", schema) ]) -> if String_map.mem name world then Error (Already_defined name) else let* schema = schema_of_node ~relation:name schema in Ok ( String_map.add name (Table { schema; facts = [] }) world, facts, rules, queries ) | Ast.Directive ("Relation", _) -> Error (Malformed_directive "Relation named: Name schema: #( ... )") | Ast.Directive ("Rule", [ ("name", Ast.Str rule_name); ("head", head); ("body", body) ]) -> Ok (world, facts, (rule_name, head, body) :: rules, queries) | Ast.Directive ("Rule", _) -> Error (Malformed_directive "Rule name: \"...\" head: [...] body: ...") | Ast.Directive ("Query", [ ("expression", body) ]) -> Ok (world, facts, rules, body :: queries) | Ast.Directive ("Query", _) -> Error (Malformed_directive "Query expression: ...") | Ast.Directive (name, _) -> Error (Malformed_directive name) | Ast.Assert node -> Ok (world, node :: facts, rules, queries)) (Ok (String_map.empty, [], [], [])) statements in let raw_facts = List.rev raw_facts and raw_rules = List.rev raw_rules and raw_queries = List.rev raw_queries in (* Second pass: compile rules, group them by head relation, admit each with the totality check. *) let* compiled_rules = List.fold_left (fun acc (rule_name, head_node, body_node) -> let* grouped = acc in let* head_expr = expr_of_node head_node in let* relation, head = match head_expr with | Apply (name, bindings) -> Ok (name, bindings) | _ -> Error (Malformed_directive "a rule head is a single [...] block") in let* body = expr_of_node body_node in let* rule = admit_rule ~relation rule_name head body in let existing = Option.value ~default:[] (String_map.find_opt relation grouped) in Ok (String_map.add relation (existing @ [ rule ]) grouped)) (Ok String_map.empty) raw_rules in let* world = String_map.fold (fun relation rules acc -> let* world = acc in if String_map.mem relation world then Error (Already_defined relation) else Ok (String_map.add relation (Rules rules) world)) compiled_rules (Ok world) in (* Third pass: admit facts against schemas and domains. This is integrity control at loading, the same check membership always is. Primitive domains decide here; criteria need the solver, so they are handed to Program as pending checks with the subject already bound to the attribute value. *) let equal_l = Result.get_ok (label_of_string "left") and right_l = Result.get_ok (label_of_string "right") in let bind_subject subject value body = And ( Apply ( "Equal", Label.Map.( empty |> add equal_l (Pattern.Var subject) |> add right_l (Pattern.of_term value)) ), body ) in let* world, pendings = List.fold_left (fun acc node -> let* world, pendings = acc in let* expr = expr_of_node node in let* relation, bindings = match expr with | Apply (name, bindings) -> Ok (name, bindings) | _ -> Error (Malformed_directive "a fact is a single [...] block") in match String_map.find_opt relation world with | Some (Table { schema; facts }) -> let* tuple = Label.Map.fold (fun label pattern acc -> let* tuple = acc in match Pattern.to_term pattern with | Some term -> Ok (Label.Map.add label term tuple) | None -> Error (Fact_not_ground relation)) bindings (Ok Label.Map.empty) in let* () = if List.length schema = Label.Map.cardinal tuple && List.for_all (fun (label, _) -> Label.Map.mem label tuple) schema then Ok () else Error (Schema_mismatch { relation; detail = "fact labels must match the schema" }) in let* new_pendings = List.fold_left (fun acc (label, domain) -> let* pendings = acc in let value = Label.Map.find label tuple in match domain with | Primitive primitive -> if member_of_primitive value primitive then Ok pendings else Error (Domain_violation { relation; label = Label.to_string label }) | Criterion { subject; body } -> Ok ({ pending_relation = relation; pending_label = label; pending_body = bind_subject subject value body; } :: pendings)) (Ok []) schema in Ok ( String_map.add relation (Table { schema; facts = facts @ [ tuple ] }) world, pendings @ List.rev new_pendings ) | Some (Rules _) -> Error (Schema_mismatch { relation; detail = "facts cannot be asserted on a rule-defined relation" }) | None -> Error (Unknown_relation relation)) (Ok (world, [])) raw_facts in (* Fourth pass: reference checking and the mutual recursion cut. Self recursion is admitted with a witness; longer cycles are rejected outright. *) let known name = Builtin.is_builtin name || String_map.mem name world in let check_refs body = List.fold_left (fun acc (name, _) -> let* () = acc in if known name then Ok () else Error (Unknown_relation name)) (Ok ()) (applications body) in let* () = String_map.fold (fun _ def acc -> let* () = acc in match def with | Table { schema; _ } -> List.fold_left (fun acc (_, domain) -> let* () = acc in match domain with | Primitive _ -> Ok () | Criterion { body; _ } -> check_refs body) (Ok ()) schema | Rules rules -> List.fold_left (fun acc rule -> let* () = acc in check_refs rule.body) (Ok ()) rules) world (Ok ()) in let dependencies relation = match String_map.find_opt relation world with | Some (Rules rules) -> List.concat_map (fun rule -> applications rule.body |> List.filter_map (fun (name, _) -> if (not (String.equal name relation)) && (match String_map.find_opt name world with | Some (Rules _) -> true | _ -> false) then Some name else None)) rules | _ -> [] in let* () = let rec visit path relation = if List.mem relation path then Error (Mutual_recursion (List.rev (relation :: path))) else List.fold_left (fun acc dep -> let* () = acc in visit (relation :: path) dep) (Ok ()) (dependencies relation) in String_map.fold (fun relation def acc -> let* () = acc in match def with Rules _ -> visit [] relation | Table _ -> Ok ()) world (Ok ()) in (* Queries compile last, against the full catalog. *) let* queries = List.fold_left (fun acc node -> let* queries = acc in let* expr = expr_of_node node in let* () = check_refs expr in Ok (expr :: queries)) (Ok []) raw_queries |> Result.map List.rev in Ok (world, queries, pendings) end