module Error = struct open Concepts.Condition let unknown_relation name = condition "unknown-relation" "A plan named a relation that does not exist under this snapshot" ("name" |=| Concepts.Value.String name) let cancelled () = condition "evaluation-cancelled" "An evaluation was stopped before it produced its result" empty let not_a_plan () = condition "not-a-fol-program" "The evaluator was given a program of another kind" empty let unknown_attribute name = condition "unknown-attribute" "A plan named an attribute its relation does not have" ("attribute" |=| Concepts.Value.String name) let malformed_program () = condition "malformed-fol-program" "A stored FOL program did not conform to what was expected. Is your database corrupted?" empty let cyclic_program code = condition "cyclic-program" "A stored program reads itself" ("program" |=| Concepts.Value.String (Concepts.Hash.to_hum_string code)) end let name = "fol" type binding = Concepts.Value.value BatMap.String.t type 'r term = | Base of 'r | Project of 'r term * string BatFingerTree.t | Where of 'r term * binding type plan = Protocols.Handle.t term type Kernel.Program.kind += Fol of Kernel.Path.t term module Bencode = Concepts.Encoding.Bencode let rec bencode_of_term = let strings values = Bencode.List (List.map (fun value -> Bencode.String value) values) in function | Base path -> Bencode.Tagged ('b', strings (Kernel.Path.to_list path)) | Project (term, attributes) -> Bencode.Tagged ( 'p', Bencode.Dict ["term", bencode_of_term term; "attributes", strings (BatFingerTree.to_list attributes)] ) | Where (term, binding) -> Bencode.Tagged ( 'w', Bencode.Dict [ "term", bencode_of_term term; ( "binding", Bencode.Dict ( BatMap.String.bindings binding |> List.map (fun (name, value) -> name, Concepts.Tuple.bencode_of_value value) ) ) ] ) let rec term_of_bencode = let open Utilities.Result in let strings values = let* values = Bencode.as_list values in List.map Bencode.as_string values |> Utilities.List.sequence in function | Bencode.Tagged ('b', segments) -> strings segments |> Result.map (fun segments -> Base (Kernel.Path.of_list segments)) | Bencode.Tagged ('p', data) -> let* term = Bencode.field "term" data |>> term_of_bencode in let* attributes = Bencode.field "attributes" data |>> strings in Ok (Project (term, BatFingerTree.of_list attributes)) | Bencode.Tagged ('w', data) -> let* term = Bencode.field "term" data |>> term_of_bencode in let* binding = Bencode.field "binding" data |>> Bencode.as_dict in let* binding = List.map (fun (name, value) -> Concepts.Tuple.value_of_bencode value |> Result.map (fun value -> name, value) ) binding |> Utilities.List.sequence in Ok (Where (term, BatMap.String.of_list binding)) | _ -> Error (Error.malformed_program ()) let encode term = bencode_of_term term |> Bencode.to_bytes |> Bytes.to_string let decode source = Utilities.Result.(Bencode.of_bytes (Bytes.of_string source) |>> term_of_bencode) let bound binding = BatSet.String.of_enum (BatMap.String.keys binding) let names attributes = BatSet.String.of_list (BatFingerTree.to_list attributes) let rec push = function | Base relation -> Base relation | Project (term, attributes) -> Project (push term, attributes) | Where (term, binding) -> ( match push term with | Project (term, attributes) when BatSet.String.subset (bound binding) (names attributes) -> Project (push (Where (term, binding)), attributes) | term -> Where (term, binding) ) (* TODO: To strenghten our checks for self references, we need to construct a graph and see raise a condition if there are self references. We partially do that here with the program calling self, but if the flow is alternated, say between program A and B, where B calls A and A calls B, we also must check. *) let instantiate within = let open Utilities.Result in let rec bind expanding = function | Base reference -> ( let* relation = Kernel.Path.lookup within reference in match Kernel.Program.image relation with | None -> Ok (Base relation) | Some {kind= Fol term; source; _} -> Protocols.Handle.release relation; let identity = Concepts.Hash.hash_of_bytes (Bytes.of_string source) in if List.exists (Concepts.Hash.hash_equals identity) expanding then Error (Error.cyclic_program identity) else bind (identity :: expanding) term | Some _ -> Protocols.Handle.releasing relation (fun () -> let* program = Protocols.Executable.require relation in let* result = Protocols.Executable.invoke program [] ~within in Ok (Base result) ) ) | Project (term, attributes) -> let* plan = bind expanding term in Ok (Project (plan, attributes)) | Where (term, binding) -> let* plan = bind expanding term in Ok (Where (plan, binding)) in fun term -> bind [] term |> Result.map push let restrict attributes description = let open Utilities.Result in Utilities.FingerTree.foldm_left (fun restricted name -> let* attribute = BatMap.String.find_opt name description |> Option.to_result ~none:(Error.unknown_attribute name) in Ok (BatMap.String.add name attribute restricted) ) BatMap.String.empty attributes let rec describe = let open Utilities.Result in function | Base relation -> Protocols.Relation.heading relation | Project (plan, attributes) -> describe plan |>> restrict attributes | Where (plan, binding) -> let* heading = describe plan in let* _ = restrict (BatFingerTree.of_enum (BatMap.String.keys binding)) heading in Ok heading let index_for relation binding = let open Utilities.Option in let* indexed = Protocols.Indexed.from relation in let* attribute = List.find_opt (fun a -> BatMap.String.mem a binding) (Protocols.Indexed.indexes indexed) in Some (indexed, attribute) let rec modes = let open Utilities.Result in function | Base relation -> let* relation = Protocols.Relation.require relation in Protocols.Relation.modes relation | Project (plan, attributes) -> modes plan |> Result.map (Concepts.Mode.project (names attributes)) | Where (plan, binding) -> ( let* modes = modes plan in match plan with | Base relation -> ( match index_for relation binding with | Some (_, attribute) -> Ok (Concepts.Mode.given attribute modes) | None -> Ok modes ) | _ -> Ok modes ) let project description relation = let open Utilities.Result in let* cursor = Protocols.Enumerable.cursor relation in let produce ~yield = let* scan = Protocols.Cursor.require cursor in let rec distinct seen = let* next = Protocols.Cursor.next scan in match next with | None -> Ok () | Some tuple -> let tuple = { tuple with attributes= BatMap.String.filter (fun name _ -> BatMap.String.mem name description) tuple.Concepts.Tuple.attributes } in let key = Concepts.Tuple.hash tuple |> Concepts.Hash.to_raw_string in if BatSet.String.mem key seen then distinct seen else begin yield tuple; distinct (BatSet.String.add key seen) end in distinct BatSet.String.empty in Ok (Kernel.Generator.cursor_of ~finally:(fun () -> Protocols.Handle.release cursor) produce) let agrees binding {Concepts.Tuple.attributes; _} = BatMap.String.for_all (fun name value -> Option.fold ~none:false ~some:(Concepts.Value.equal value) (BatMap.String.find_opt name attributes) ) binding let restricted binding relation = let open Utilities.Result in let* cursor = match index_for relation binding with | Some (indexed, attribute) -> Protocols.Indexed.lookup indexed attribute (BatMap.String.find attribute binding) | None -> Protocols.Enumerable.cursor relation in Ok (Kernel.Generator.filter (agrees binding) cursor) let contains relation tuple = Result.bind (Protocols.Relation.require relation) (fun r -> Protocols.Relation.contains r tuple) let derive ?decide relation description modes enumerate = Kernel.EphemeralRelation.instantiate ~inputs:[relation] ?decide ~modes:(fun () -> Ok modes) description (Kernel.EphemeralRelation.temporary description) (fun () -> enumerate relation) let rec execute plan = let open Utilities.Result in let* description = describe plan in let* modes = modes plan in match plan with | Base relation -> let relation = Protocols.Handle.move relation in Ok (derive ~decide:(contains relation) relation description modes Protocols.Enumerable.cursor) | Project (plan, _) -> let* relation = execute plan in Ok (derive relation description modes (project description)) | Where (plan, binding) -> let* relation = match plan with | Base relation -> Ok (Protocols.Handle.move relation) | plan -> execute plan in Ok (derive ~decide:(fun tuple -> if agrees binding tuple then contains relation tuple else Ok false) relation description modes (restricted binding) ) class evaluator = object (self) inherit Kernel.Lifecycle.null inherit Kernel.Identity.of_id method to_string = "fol-evaluator" method parse source = decode source |> Result.map (fun term -> Fol term) method eval kind _inputs ~within = let open Utilities.Result in match kind with | Fol term -> let* plan = instantiate within term in execute plan | _ -> Error (Error.not_a_plan ()) method protocols : Protocols.Handle.protocol list = [Protocols.Evaluator.make self] end let make () = new evaluator |> Protocols.Handle.make let program ?root term = Kernel.Program.make ?root ~evaluator:name ~source:(encode term) (Fol term)