aboutsummaryrefslogblamecommitdiffstats
path: root/lib/vm.ml
blob: a518aa5af60e47fa0e5a8eb5f47f3e81ab334705 (plain) (tree)
1
2
3
4
5
6
7






                                                                                                   













                                                                     



                                      




                                                                







                                                                   

                                               

                                                                           



                                                                        


                                                                                 

                          









                                                                                 


















                                                                         

                                                                            














                                                                         

                                                                                 










































































































































                                                                                         
                                     
                   


                                                        









                                                                                     
                                                     



                                                                    
                                                          
                                                            
                       













                                                                              
                                                  

                                                  




                                                                    


                                                
                   








                                                                          

                                 



                                                                                 
                                      

                                                         
                     






                                                                
                                                  




                                                                  
                                               
 



                                                                   

                                
                           




                                                  

                                                                             





                                                                            













































                                                                                   










                                                                        
                        


                                                                  

                                                           
      
                                                              
                                                           



















                                                                                       

                                



                                                                           


                        

                                            

                                                                   
                       




                                           
type error = Non_total_plan of string list

let pp_error fmt (Non_total_plan goals) =
  Format.fprintf fmt
    "non-total plan rejected: no decidable goal among %s (bind more arguments or bound the search)"
    (String.concat ", " goals)

type residual = string * Pattern.t Label.Map.t

let pp_residual fmt (name, bindings) =
  Format.fprintf fmt "@[<hv 1>[%s" name;
  Label.Map.iter
    (fun label pattern ->
      Format.fprintf fmt "@ %a %a" Label.pp label Pattern.pp pattern)
    bindings;
  Format.fprintf fmt "]@]"

type solution = {
  bindings : (string * Pattern.t) list;
  residue : residual list;
}

module Make (W : World.WORLD) = struct
  exception Stuck of string list

  (* A goal is a proposition to establish or a denial to refute.
     Conjunction flattens; [u :not d] contributes the universe
     positively and the denial negatively. *)
  type goal = Positive of World.expr | Negated of World.expr

  (* What attempting a goal can say: run it into branches, or wait.
     A waiting goal is residuatable when it is pure membership that
     decides as soon as its arguments are ground; searches are
     never residuatable. *)
  type attempt =
    | Run of (Pattern.Subst.t * goal list) list
    | Wait of { residuatable : bool }

  let rec flatten = function
    | World.And (a, b) -> flatten a @ flatten b
    | World.Not (universe, denial) -> flatten universe @ [ Negated denial ]
    | expr -> [ Positive expr ]

  let rec expr_vars = function
    | World.Apply (_, bindings) ->
        Label.Map.fold (fun _ p acc -> acc @ Pattern.vars p) bindings []
    | World.And (a, b) | World.Or (a, b) | World.Not (a, b) | World.Forall (a, b)
      ->
        expr_vars a @ expr_vars b

  let goal_name = function
    | Positive (World.Apply (name, _)) -> name
    | Positive (World.Or _) -> ":or"
    | Positive (World.And _) -> ":and"
    | Positive (World.Not _) | Negated _ -> ":not"
    | Positive (World.Forall _) -> "ForAll"

  let unresolved_vars subst expr =
    expr_vars expr
    |> List.filter (fun v ->
           not (Pattern.is_ground (Pattern.Subst.resolve subst (Pattern.Var v))))

  (* Rule variables are renamed apart at every unfolding. The suffix
     character cannot be lexed, so renamed variables are unforgeable
     from source text. *)
  let freshen =
    let counter = ref 0 in
    fun rule ->
      incr counter;
      let stamp = "@" ^ string_of_int !counter in
      let rec rename = function
        | Pattern.Var v -> Pattern.Var (v ^ stamp)
        | Pattern.Record map -> Pattern.Record (Label.Map.map rename map)
        | ground -> ground
      in
      let rec rename_expr = function
        | World.Apply (name, bindings) ->
            World.Apply (name, Label.Map.map rename bindings)
        | World.And (a, b) -> World.And (rename_expr a, rename_expr b)
        | World.Or (a, b) -> World.Or (rename_expr a, rename_expr b)
        | World.Not (a, b) -> World.Not (rename_expr a, rename_expr b)
        | World.Forall (a, b) -> World.Forall (rename_expr a, rename_expr b)
      in
      (Label.Map.map rename rule.World.head, rename_expr rule.World.body)

  (* Unify a goal's bindings against a rule head or a stored tuple.
     Goal labels absent from the head fail the branch; head labels
     absent from the goal are existential. *)
  let unify_bindings subst goal_bindings target_bindings =
    Label.Map.fold
      (fun label pattern acc ->
        Option.bind acc (fun subst ->
            match Label.Map.find_opt label target_bindings with
            | Some target -> Pattern.Subst.unify subst pattern target
            | None -> None))
      goal_bindings (Some subst)

  let proven solutions = List.exists (fun (_, residue) -> residue = []) solutions

  (* Interval propagation over waiting membership constraints. What
     residuation returns inert, this looks at together: bounds with
     an empty intersection fail the branch outright, and an integer
     variable squeezed into a finite range converts to bounded
     generation, the same admissible enumeration Between performs
     explicitly. Sakura's cardinality analysis, inside the engine. *)
  module Propagate = struct
    let forced_label name =
      match Label.of_string name with Ok l -> l | Error _ -> assert false

    let value_l = forced_label "value"
    let left_l = forced_label "left"
    let right_l = forced_label "right"

    type bound = { value : Q.t; strict : bool }

    type info = {
      integer : bool;
      stringy : bool;
      lower : bound option;
      upper : bound option;
    }

    let no_info = { integer = false; stringy = false; lower = None; upper = None }

    module M = Map.Make (String)

    let note var f store =
      M.update var
        (fun info -> Some (f (Option.value ~default:no_info info)))
        store

    let stronger_lower a b =
      if Q.gt a.value b.value then a
      else if Q.lt a.value b.value then b
      else { a with strict = a.strict || b.strict }

    let stronger_upper a b =
      if Q.lt a.value b.value then a
      else if Q.gt a.value b.value then b
      else { a with strict = a.strict || b.strict }

    let add_lower bound info =
      {
        info with
        lower =
          Some
            (match info.lower with
            | None -> bound
            | Some existing -> stronger_lower bound existing);
      }

    let add_upper bound info =
      {
        info with
        upper =
          Some
            (match info.upper with
            | None -> bound
            | Some existing -> stronger_upper bound existing);
      }

    let numeric_value = function
      | Pattern.Integer z -> Some (Q.of_bigint z)
      | Pattern.Rational q -> Some q
      | _ -> None

    (* Read one waiting builtin goal into the store. Bindings arrive
       already resolved, so a variable here is genuinely unbound. *)
    let gather store (name, bindings) =
      let value = Label.Map.find_opt value_l bindings
      and left = Label.Map.find_opt left_l bindings
      and right = Label.Map.find_opt right_l bindings in
      match (name, value, left, right) with
      | ("Integer" | "Natural"), Some (Pattern.Var x), _, _ ->
          let store = note x (fun i -> { i with integer = true }) store in
          if String.equal name "Natural" then
            note x (add_lower { value = Q.zero; strict = false }) store
          else store
      | "String", Some (Pattern.Var x), _, _ ->
          note x (fun i -> { i with stringy = true }) store
      | ( ("LessThan" | "LessThanEqual" | "GreaterThan" | "GreaterEqual"),
          _,
          Some left,
          Some right ) -> (
          let strict =
            String.equal name "LessThan" || String.equal name "GreaterThan"
          in
          let less = String.equal name "LessThan" || String.equal name "LessThanEqual" in
          match (left, right) with
          | Pattern.Var x, ground when Option.is_some (numeric_value ground) ->
              let bound = { value = Option.get (numeric_value ground); strict } in
              note x (if less then add_upper bound else add_lower bound) store
          | ground, Pattern.Var x when Option.is_some (numeric_value ground) ->
              let bound = { value = Option.get (numeric_value ground); strict } in
              note x (if less then add_lower bound else add_upper bound) store
          | _ -> store)
      | _ -> store

    (* Integer endpoints of a rational interval. *)
    let lower_int { value; strict } =
      let n = Q.num value and d = Q.den value in
      if strict then Z.add (Z.fdiv n d) Z.one else Z.cdiv n d

    let upper_int { value; strict } =
      let n = Q.num value and d = Q.den value in
      if strict then Z.sub (Z.cdiv n d) Z.one else Z.fdiv n d

    type verdict = Fail | Enumerate of string * Z.t * Z.t | Unknown

    let analyze constraints =
      let store = List.fold_left gather M.empty constraints in
      let contradictory _ info =
        (info.stringy && (info.integer || info.lower <> None || info.upper <> None))
        ||
        match (info.lower, info.upper) with
        | Some lo, Some hi ->
            Q.gt lo.value hi.value
            || (Q.equal lo.value hi.value && (lo.strict || hi.strict))
            || (info.integer && Z.gt (lower_int lo) (upper_int hi))
        | _ -> false
      in
      if M.exists contradictory store then Fail
      else
        match
          M.choose_opt
            (M.filter
               (fun _ info ->
                 info.integer && info.lower <> None && info.upper <> None)
               store)
        with
        | Some (var, info) ->
            Enumerate
              ( var,
                lower_int (Option.get info.lower),
                upper_int (Option.get info.upper) )
        | None -> Unknown
  end

  let rec try_goal world subst goal =
    match goal with
    | Negated denial ->
        if unresolved_vars subst denial = [] then
          match search world subst (flatten denial) with
          | [] -> Run [ (subst, []) ]
          | solutions ->
              (* Ground denials cannot residuate in practice, but if
                 one somehow does, refusing to conclude is the only
                 sound answer. *)
              if proven solutions then Run [] else Wait { residuatable = false }
        else Wait { residuatable = false }
    | Positive (World.Or (a, b)) ->
        Run [ (subst, flatten a); (subst, flatten b) ]
    | Positive ((World.And _ | World.Not _) as expr) -> Run [ (subst, flatten expr) ]
    | Positive (World.Forall (quantifier, body)) -> (
        (* Universal over the quantifier's solutions. Runs only when
           the quantifier is decidable and closed; an undetermined
           quantifier or body defers the universal, and an infinite
           one therefore never runs. *)
        match search world subst (flatten quantifier) with
        | exception Stuck _ -> Wait { residuatable = false }
        | quantified ->
            if List.exists (fun (_, residue) -> residue <> []) quantified then
              Wait { residuatable = false }
            else
              let body_status (s, _) =
                match search world s (flatten body) with
                | exception Stuck _ -> `Unknown
                | [] -> `Fails
                | solutions -> if proven solutions then `Holds else `Unknown
              in
              let statuses = List.map body_status quantified in
              if List.mem `Fails statuses then Run []
              else if List.mem `Unknown statuses then
                Wait { residuatable = false }
              else Run [ (subst, []) ])
    | Positive (World.Apply (name, bindings)) -> (
        match Builtin.run subst name bindings with
        | Some (Builtin.Solved substs) ->
            Run (List.map (fun s -> (s, [])) substs)
        | Some Builtin.Stuck ->
            (* Pure membership waiting on bindings: the residuatable
               case. Constraining is not searching. *)
            Wait { residuatable = true }
        | None -> (
            match W.find world name with
            | Some (World.Table { facts; _ }) ->
                Run
                  (List.filter_map
                     (fun tuple ->
                       let target = Label.Map.map Pattern.of_term tuple in
                       Option.map
                         (fun s -> (s, []))
                         (unify_bindings subst bindings target))
                     facts)
            | Some (World.Rules rules) ->
                let witness_ground rule =
                  List.for_all
                    (fun label ->
                      match Label.Map.find_opt label bindings with
                      | Some pattern ->
                          Pattern.is_ground (Pattern.Subst.resolve subst pattern)
                      | None -> false)
                    rule.World.witness
                in
                if List.for_all witness_ground rules then
                  Run
                    (List.filter_map
                       (fun rule ->
                         let head, body = freshen rule in
                         Option.map
                           (fun s -> (s, flatten body))
                           (unify_bindings subst bindings head))
                       rules)
                else Wait { residuatable = false }
            | None ->
                (* Unreachable on admitted programs: the loader
                   checks references. An embedded host that lies
                   about its catalog surfaces here as a stuck goal
                   rather than a crash. *)
                Wait { residuatable = false }))

  (* Search yields substitutions with the goals they left
     residuated. A state where every remaining goal is residuatable
     membership is an answer; one with a stalled search is a
     non-total plan. *)
  and search world subst goals =
    match goals with
    | [] -> [ (subst, []) ]
    | _ -> (
        let rec select scanned = function
          | [] -> None
          | goal :: rest -> (
              match try_goal world subst goal with
              | Run branches -> Some (branches, List.rev_append scanned rest)
              | Wait _ -> select (goal :: scanned) rest)
        in
        match select [] goals with
        | Some (branches, remaining) ->
            List.concat_map
              (fun (subst, extra) -> search world subst (extra @ remaining))
              branches
        | None -> (
            (* Everything waits. Propagate over the waiting
               membership constraints before residuating or
               rejecting: they may be jointly unsatisfiable, or may
               pin a variable into a finite range that unblocks the
               rest. *)
            let attempts = List.map (fun g -> (g, try_goal world subst g)) goals in
            let membership =
              List.filter_map
                (fun (goal, attempt) ->
                  match (goal, attempt) with
                  | ( Positive (World.Apply (name, bindings)),
                      Wait { residuatable = true } ) ->
                      Some
                        ( name,
                          Label.Map.map (Pattern.Subst.resolve subst) bindings )
                  | _ -> None)
                attempts
            in
            match Propagate.analyze membership with
            | Propagate.Fail -> []
            | Propagate.Enumerate (var, lo, hi) ->
                let rec enumerate v acc =
                  if Z.gt v hi then acc
                  else
                    let branch =
                      match
                        Pattern.Subst.unify subst (Pattern.Var var)
                          (Pattern.Integer v)
                      with
                      | Some subst -> search world subst goals
                      | None -> []
                    in
                    enumerate (Z.succ v) (acc @ branch)
                in
                enumerate lo []
            | Propagate.Unknown ->
                if
                  List.for_all
                    (fun (_, attempt) ->
                      match attempt with
                      | Wait { residuatable } -> residuatable
                      | Run _ -> false)
                    attempts
                then [ (subst, goals) ]
                else raise (Stuck (List.map goal_name goals))))

  (* Solution variables: the free variables of the query. Whatever
     occurs only under a universal is bound per quantifier solution
     and is nobody's answer. *)
  let rec outer_vars = function
    | World.Apply (_, bindings) ->
        Label.Map.fold (fun _ p acc -> acc @ Pattern.vars p) bindings []
    | World.And (a, b) | World.Or (a, b) | World.Not (a, b) ->
        outer_vars a @ outer_vars b
    | World.Forall _ -> []

  let solve world expr =
    let variables =
      List.fold_left
        (fun acc v -> if List.mem v acc then acc else acc @ [ v ])
        [] (outer_vars expr)
      |> List.filter (fun v -> not (String.contains v '@'))
    in
    match search world Pattern.Subst.empty (flatten expr) with
    | exception Stuck goals -> Error (Non_total_plan goals)
    | outcomes ->
        let read (subst, residue_goals) =
          let bindings =
            List.map
              (fun v -> (v, Pattern.Subst.resolve subst (Pattern.Var v)))
              variables
          in
          let residue =
            List.filter_map
              (function
                | Positive (World.Apply (name, goal_bindings)) ->
                    Some
                      (name, Label.Map.map (Pattern.Subst.resolve subst) goal_bindings)
                | _ -> None)
              residue_goals
          in
          { bindings; residue }
        in
        let equal_residual (n, b) (m, c) =
          String.equal n m && Label.Map.equal Pattern.equal b c
        in
        let equal_solution a b =
          List.for_all2 (fun (_, p) (_, q) -> Pattern.equal p q) a.bindings
            b.bindings
          && List.length a.residue = List.length b.residue
          && List.for_all2 equal_residual a.residue b.residue
        in
        let solutions =
          List.fold_left
            (fun acc outcome ->
              let solution = read outcome in
              if List.exists (equal_solution solution) acc then acc
              else acc @ [ solution ])
            [] outcomes
        in
        Ok solutions
end

module Standalone = Make (World.Standalone)