aboutsummaryrefslogblamecommitdiffstats
path: root/lib/evaluators/Constraint.ml
blob: 5ccd1aef9f2f96198df5c0d376ec435b01fcb1f1 (plain) (tree)




























































































































































































































































































































                                                                                                    
                                                                                    






































































                                                                                           
module Error = struct
  open Concepts.Condition

  let unbound_variable name =
    condition "unbound-variable" "A comparison mentioned a variable that nothing has bound"
      ("variable" |=| Concepts.Value.String name)

  let absent_attribute target attribute =
    condition "absent-attribute"
      "A membership named an attribute that the tuples of its target do not carry"
      ( "target" |=| Concepts.Value.String (Kernel.Path.to_string target)
      & "attribute" |=| Concepts.Value.String attribute )

  let open_constraint name variables =
    condition "open-constraint"
      "A constraint is asserted of every state and so must be closed, and this one leaves \
       variables that nothing covers"
      ( "constraint" |=| Concepts.Value.String name
      & "variables" |=| Concepts.Value.String (String.concat ", " variables) )

  let violated name =
    condition "constraint-violated"
      "A candidate state was refused because the violations of a constraint imposed on it are not \
       empty"
      ("constraint" |=| Concepts.Value.String name)
end

type binding_expr = Var of string | Const of Concepts.Value.value
type membership = {target: Kernel.Path.t; binding: binding_expr BatMap.String.t}

type formula =
  | Exists of comprehension
  | Not of formula
  | And of formula * formula
  | Or of formula * formula
  | Eq of {left: binding_expr; right: binding_expr}
  | Truth of bool

and comprehension = {quantifiers: membership list; body: formula}

type scope = Protocols.Handle.t

let variables_of quantifiers =
  List.fold_left
    (fun acc {binding; _} ->
      BatMap.String.values binding
      |> BatEnum.fold
           (fun acc -> function Var name -> BatSet.String.add name acc | Const _ -> acc)
           acc )
    BatSet.String.empty quantifiers

let rec free = function
  | Truth _ -> BatSet.String.empty
  | Not formula -> free formula
  | And (left, right) | Or (left, right) -> BatSet.String.union (free left) (free right)
  | Eq {left; right} ->
      let mentioned = function
        | Var name -> BatSet.String.singleton name
        | Const _ -> BatSet.String.empty
      in
      BatSet.String.union (mentioned left) (mentioned right)
  | Exists {quantifiers; body} -> BatSet.String.diff (free body) (variables_of quantifiers)

let attributes_of ({quantifiers; _} as comprehension) =
  BatSet.String.union (variables_of quantifiers) (free (Exists comprehension))

(* Both takes nested negation; SQL also gets it from outer joins, which this form lacks. *)
type polarity = Positive | Negative | Both

let merged existing polarity =
  match existing, polarity with
  | Positive, Positive -> Positive
  | Negative, Negative -> Negative
  | _ -> Both

let at target (candidate, _) = Kernel.Path.equal candidate target

let relevance formula =
  let flipped = function Positive -> Negative | Negative -> Positive | Both -> Both in
  let record polarity occurrences {target; _} =
    let polarity =
      match List.find_opt (at target) occurrences with
      | None -> polarity
      | Some (_, existing) -> merged existing polarity
    in
    (target, polarity) :: List.filter (Fun.negate (at target)) occurrences
  in
  let rec walk polarity occurrences = function
    | Truth _ | Eq _ -> occurrences
    | Not formula -> walk (flipped polarity) occurrences formula
    | And (left, right) | Or (left, right) -> walk polarity (walk polarity occurrences left) right
    | Exists {quantifiers; body} ->
        walk polarity (List.fold_left (record polarity) occurrences quantifiers) body
  in
  walk Positive [] formula

type environment = Concepts.Value.value BatMap.String.t

(* Cursors a walk holds open are suspended inside it when a caller
   abandons the walk, so they are tracked to be closed on release. *)
module Live = struct
  let none () = ref []
  let opened live handle = live := handle :: !live

  let closed live handle =
    if List.memq handle !live then begin
      live := List.filter (( != ) handle) !live;
      Protocols.Handle.release handle
    end

  let abandoned live =
    List.iter Protocols.Handle.release !live;
    live := []
end

let resolve scope target = Kernel.Path.lookup scope target

let value_of environment = function
  | Const value -> Ok value
  | Var name ->
      BatMap.String.find_opt name environment
      |> Option.to_result ~none:(Error.unbound_variable name)

let fixed environment = function
  | Const value -> Some value
  | Var name -> BatMap.String.find_opt name environment

let pushed environment binding =
  BatMap.String.enum binding
  |> BatEnum.fold
       (fun pushed (attribute, expression) ->
         match fixed environment expression with
         | Some value -> BatMap.String.add attribute value pushed
         | None -> pushed )
       Protocols.Generative.nothing

let agreeing environment expression value =
  match expression, fixed environment expression with
  | _, Some held -> if Concepts.Value.equal held value then Some environment else None
  | Var name, None -> Some (BatMap.String.add name value environment)
  | Const _, None -> None

let extend target binding environment tuple =
  let open Utilities.Result in
  BatMap.String.enum binding
  |> BatEnum.fold
       (fun acc (attribute, expression) ->
         let* acc = acc in
         match acc with
         | None -> Ok None
         | Some environment -> (
           match BatMap.String.find_opt attribute tuple.Concepts.Tuple.attributes with
           | None -> Error (Error.absent_attribute target attribute)
           | Some value -> Ok (agreeing environment expression value) ) )
       (Ok (Some environment))

type step = Continue | Stop

let rec holds ~live scope environment formula =
  let open Utilities.Result in
  match formula with
  | Truth truth -> Ok truth
  | Not formula -> holds ~live scope environment formula |> Result.map not
  | And (left, right) ->
      let* left = holds ~live scope environment left in
      if left then holds ~live scope environment right else Ok false
  | Or (left, right) ->
      let* left = holds ~live scope environment left in
      if left then Ok true else holds ~live scope environment right
  | Eq {left; right} ->
      let* left = value_of environment left in
      let* right = value_of environment right in
      Ok (Concepts.Value.equal left right)
  | Exists comprehension ->
      let* step = assignments ~live scope environment comprehension ~visit:(fun _ -> Stop) in
      Ok (step = Stop)

(* An assignment reached through several tuples is still one
   assignment, which the walk settles rather than its callers, since a
   relation built over it has to be a set. *)
and assignments ~live scope environment {quantifiers; body} ~visit =
  let open Utilities.Result in
  let fingerprint environment =
    Concepts.Tuple.hash {Concepts.Tuple.type_= ""; attributes= environment}
    |> Concepts.Hash.to_raw_string
  in
  let rec walk seen environment = function
    | [] -> (
        let* satisfied = holds ~live scope environment body in
        let taken = fingerprint environment in
        match satisfied, BatSet.String.mem taken seen with
        | false, _ | true, true -> Ok (seen, Continue)
        | true, false -> Ok (BatSet.String.add taken seen, visit environment) )
    | {target; binding} :: rest ->
        let* handle = resolve scope target in
        let* generative = Protocols.Generative.require handle in
        let* cursor = Protocols.Generative.generate generative (pushed environment binding) in
        Live.opened live cursor;
        Fun.protect
          ~finally:(fun () -> Live.closed live cursor)
          (fun () ->
            let* scan = Protocols.Cursor.require cursor in
            let rec drain seen =
              let* tuple = Protocols.Cursor.next scan in
              match tuple with
              | None -> Ok (seen, Continue)
              | Some tuple -> (
                  let* extended = extend target binding environment tuple in
                  match extended with
                  | None -> drain seen
                  | Some environment -> (
                      let* seen, step = walk seen environment rest in
                      match step with Stop -> Ok (seen, Stop) | Continue -> drain seen ) )
            in
            drain seen )
  in
  walk BatSet.String.empty environment quantifiers |> Result.map snd

type expression = {label: string; comprehension: comprehension; state: scope}

let rendered {label; comprehension; _} =
  let expression = function
    | Var name -> "?" ^ name
    | Const value -> "=" ^ Concepts.Value.to_string value
  in
  let quantifier {target; binding} =
    BatMap.String.enum binding
    |> BatEnum.map (fun (attribute, held) -> attribute ^ ":" ^ expression held)
    |> BatList.of_enum
    |> String.concat ";"
    |> Printf.sprintf "%s[%s]" (Kernel.Path.to_string target)
  in
  let rec formula = function
    | Truth truth -> if truth then "T" else "F"
    | Not inner -> Printf.sprintf "!(%s)" (formula inner)
    | And (left, right) -> Printf.sprintf "&(%s,%s)" (formula left) (formula right)
    | Or (left, right) -> Printf.sprintf "|(%s,%s)" (formula left) (formula right)
    | Eq {left; right} -> Printf.sprintf "eq(%s,%s)" (expression left) (expression right)
    | Exists {quantifiers; body} ->
        Printf.sprintf "E(%s,%s)"
          (List.map quantifier quantifiers |> String.concat "")
          (formula body)
  in
  label ^ "/" ^ formula (Exists comprehension)

let provenance {quantifiers; _} variable =
  List.concat_map
    (fun {target; binding} ->
      BatMap.String.enum binding
      |> BatEnum.filter_map (fun (attribute, expression) ->
          match expression with
          | Var name when String.equal name variable ->
              Some {Protocols.Schematics.source= Kernel.Path.to_list target; attribute}
          | Var _ | Const _ -> None )
      |> BatList.of_enum )
    quantifiers

let unknown_domain = ""

let domain_of state origins =
  let open Utilities.Result in
  match origins with
  | {Protocols.Schematics.source; attribute} :: _ -> (
      let* handle = resolve state (Kernel.Path.of_list source) in
      match Protocols.Schematics.from handle with
      | None -> Ok unknown_domain
      | Some schematics -> (
          let* description = Protocols.Schematics.describe schematics in
          match description with
          | Protocols.Schematics.Relation description ->
              BatMap.String.find_opt attribute description
              |> Option.fold ~none:unknown_domain ~some:(fun attribute ->
                  attribute.Protocols.Schematics.domain )
              |> Result.ok
          | Protocols.Schematics.Multigroup _ | Protocols.Schematics.Tuple _ -> Ok unknown_domain )
      )
  | [] -> Ok unknown_domain

module Derivation = struct
  type plan = expression

  let identity plan = rendered plan |> Bytes.of_string |> Concepts.Hash.hash_of_bytes

  let describe {comprehension; state; _} =
    attributes_of comprehension
    |> BatSet.String.elements
    |> List.map (fun variable ->
        let origins = provenance comprehension variable in
        domain_of state origins
        |> Result.map (fun domain -> variable, {Protocols.Schematics.domain; provenance= origins}) )
    |> Utilities.List.sequence
    |> Result.map BatMap.String.of_list

  let modes {comprehension; _} =
    let supplied = free (Exists comprehension) |> BatSet.String.elements in
    Ok
      (Concepts.Mode.of_list
         [ Concepts.Mode.decides_when (attributes_of comprehension |> BatSet.String.elements);
           Concepts.Mode.generates_when supplied Concepts.Cardinality.Finite ] )

  let generate {label; comprehension; state} binding =
    let open Utilities.Result in
    let heading = attributes_of comprehension in
    let tuple_of environment =
      { Concepts.Tuple.type_= label;
        attributes= BatMap.String.filter (fun name _ -> BatSet.String.mem name heading) environment
      }
    in
    let live = Live.none () in
    let produce ~yield =
      let* _ =
        assignments ~live state binding comprehension ~visit:(fun environment ->
            yield (tuple_of environment);
            Continue )
      in
      Ok ()
    in
    Ok (Kernel.Generator.cursor_of ~finally:(fun () -> Live.abandoned live) produce)
end

module Derived = Kernel.EphemeralRelation.Make (Derivation)

module Program = Protocols.Program.Make (struct
  type t = expression
end)

let execute expression = Derived.derive expression

class evaluator =
  object (self)
    inherit Kernel.Lifecycle.null
    inherit Kernel.Identity.of_id
    method invoke = execute
    method protocols : Protocols.Handle.protocol list = [Program.make self]
  end

let make () = new evaluator |> Protocols.Handle.make

type t = {name: string; body: formula}
type change = Assertion | Retraction

let declare ~name body =
  let uncovered = free body in
  if BatSet.String.is_empty uncovered then Ok {name; body}
  else Error (Error.open_constraint name (BatSet.String.enum uncovered |> BatList.of_enum))

let name {name; _} = name
let body {body; _} = body

(* Decker: a denial yields its own quantifiers, any other closed
   formula denies itself into degree zero. *)
let denial = function
  | Not (Exists comprehension) -> comprehension
  | formula -> {quantifiers= []; body= Not formula}

let violations {name; body} state = {label= name; comprehension= denial body; state}

let relevant {body; _} ~relation ~change =
  match List.find_opt (at relation) (relevance body) with
  | None -> false
  | Some (_, Both) -> true
  | Some (_, Negative) -> change = Assertion
  | Some (_, Positive) -> change = Retraction

let with_handle handle body =
  Fun.protect ~finally:(fun () -> Protocols.Handle.release handle) (fun () -> body handle)

let vacant relation =
  let open Utilities.Result in
  let* enumerable = Protocols.Enumerable.require relation in
  let* cursor = Protocols.Enumerable.enumerate enumerable in
  with_handle cursor (fun cursor ->
      let* scan = Protocols.Cursor.require cursor in
      let* witness = Protocols.Cursor.next scan in
      Ok (Option.is_none witness) )

let satisfied interpreter constrains state =
  let open Utilities.Result in
  let* relation = Program.invoke interpreter (violations constrains state) in
  with_handle relation vacant

let admits interpreter state constraints =
  let open Utilities.Result in
  List.fold_left
    (fun acc constrains ->
      let* () = acc in
      let* upheld = satisfied interpreter constrains state in
      if upheld then Ok () else Error (Error.violated (name constrains)) )
    (Ok ()) constraints