open Rnt module Error = struct open Concepts.Condition let closed () = condition "beam-closed" "The BEAM node is gone" empty let taken () = condition "beam-taken" "An evaluator is already registered as karuta:beam" empty let malformed () = condition "malformed-beam-program" "A beam program is not {Module, Beam, #{Name => Domain}} of binaries" empty let not_a_program () = condition "not-a-beam-program" "The evaluator was given a program of another kind" empty let inputs () = condition "beam-program-inputs" "A beam program takes at most one input, of at most one tuple: what its caller knows" empty let failed message = condition "beam-failed" "The BEAM node failed to answer a call" ("message" |=| Concepts.Value.String message) let unexpected () = condition "unexpected-beam-message" "The BEAM node said something out of turn" empty let unnamed () = condition "unnamed-beam-program" "A program was defined at an empty path" empty let program_taken path = condition "beam-program-taken" "A program is already defined at the path" ("path" |=| Concepts.Value.String (Kernel.Path.to_string path)) let unknown_cursor id = condition "unknown-beam-cursor" "The BEAM node named a cursor it does not hold" ("cursor" |=| Concepts.Value.Integer id) end module type TRANSPORT = sig type t val send : t -> string -> (unit, Concepts.Condition.condition) result val receive : t -> (string, Concepts.Condition.condition) result val stop : t -> unit end module Port = struct type t = {input: in_channel; output: out_channel} let send {output; _} message = let header = Bytes.create 4 in Bytes.set_int32_be header 0 (Int32.of_int (String.length message)); try output_bytes output header; output_string output message; Ok (flush output) with Sys_error _ -> Error (Error.closed ()) let receive {input; _} = try let header = really_input_string input 4 in Ok (really_input_string input (Int32.to_int (String.get_int32_be header 0) land 0xffff_ffff)) with End_of_file | Sys_error _ -> Error (Error.closed ()) let start ?(erl = "erl") directories = let boot = "try [{module, _} = code:load_abs(filename:rootname(F)) || D <- init:get_plain_arguments(), \ F <- filelib:wildcard(D ++ \"/*.beam\")], rnt:start() catch C:R:S -> \ io:format(standard_error, \"~p:~p~n~p~n\", [C, R, S]), halt(1) end." in match Unix.open_process_args erl (Array.of_list ([erl; "-noshell"; "-noinput"; "-eval"; boot; "-extra"] @ directories)) with | input, output -> Ok {input; output} | exception Unix.Unix_error _ -> Error (Error.closed ()) let stop {input; output} = ignore (Unix.close_process (input, output)) end let name = "karuta" type program = { module_: string; beam: string; heading: Protocols.Schematics.relation_description } type Kernel.Program.kind += Beam of program let decode source = let domain _ = function | Concepts.Value.String domain -> Some {Protocols.Schematics.domain; provenance= []} | Concepts.Value.Integer _ -> None in match Translator.read 3 source with | Some ([module_; beam], attributes) when String.length module_ <= 0xffff -> let heading = BatMap.String.filter_map domain attributes in if BatMap.String.cardinal heading = BatMap.String.cardinal attributes then Ok (Beam {module_; beam; heading}) else Error (Error.malformed ()) | _ -> Error (Error.malformed ()) (* Both ends speak over one pipe, and each serves the other's requests while it waits for its own answer, so a BEAM program RNT is reading can call back into RNT, and so on, as deep as the calls nest. RNT asks the node: - h run every loaded rnt-init -> o | e - i Id:64 M:16 Module B:32 Beam [Bound] start Module's query from Beam - f Id:64 Limit:32 the next batch of it -> b | e - c Id:64 forget it The node asks RNT: - D Path Evaluator:16 Source define a program at Path -> o | e - X Path [Bound] invoke what is at Path -> r Id:64 | e - E Evaluator:16 Source invoke a program of no path -> r Id:64 | e - N Id:64 Limit:32 the next batch of its result -> b | e - C Id:64 release that result A Path is Count:16 then each segment as Length:16 Segment, and a batch b is Exhausted:8 then each tuple as Size:32 term_to_binary. Bound, when present, is the tuple of the attributes the caller already knows as term_to_binary, which is how a procedural program, one that can only check what it is given, gets called. *) module Make (T : TRANSPORT) = struct (* [root] is borrowed, not held: the evaluator lives under it, so holding it would keep both alive forever. [cursors] are the results the node invoked and has not finished reading. *) type node = { transport: T.t; root: Protocols.Handle.t; cursors: (int, Protocols.Handle.t * Protocols.Handle.t) Hashtbl.t; mutable next: int } let node transport root = {transport; root; cursors= Hashtbl.create 8; next= 0} module Request = struct type t = | Define of string list * string * string | Invoke of string list * Concepts.Tuple.t option | Evaluate of string * string | Next of int * int | Close of int exception Short let of_message message = let length = String.length message in let at = ref 1 in let take n = if !at + n > length then raise Short; let p = !at in at := p + n; p in let u16 () = String.get_uint16_be message (take 2) in let u32 () = Int32.to_int (String.get_int32_be message (take 4)) land 0xffff_ffff in let u64 () = Int64.to_int (String.get_int64_be message (take 8)) in let sized () = let n = u16 () in String.sub message (take n) n in let path () = List.init (u16 ()) (fun _ -> sized ()) in let finished request = if !at = length then Some request else None in match message.[0] with | 'D' -> let path = path () in let evaluator = sized () in Some (Define (path, evaluator, String.sub message !at (length - !at))) | 'E' -> let evaluator = sized () in Some (Evaluate (evaluator, String.sub message !at (length - !at))) | 'X' -> ( let path = path () in if !at = length then Some (Invoke (path, None)) else match Translator.decode (String.sub message !at (length - !at)) with | Ok bound -> Some (Invoke (path, Some bound)) | Error _ -> None ) | 'N' -> let id = u64 () in finished (Next (id, u32 ())) | 'C' -> finished (Close (u64 ())) | _ -> None | exception Short -> None end let batch_message {Protocols.Cursor.tuples; exhausted} = let buffer = Buffer.create 64 in Buffer.add_char buffer 'b'; Buffer.add_char buffer (if exhausted then '\001' else '\000'); BatFingerTree.iter (fun tuple -> let term = Translator.encode tuple in Buffer.add_int32_be buffer (Int32.of_int (String.length term)); Buffer.add_string buffer term ) tuples; Buffer.contents buffer let cursor_message id = let buffer = Buffer.create 9 in Buffer.add_char buffer 'r'; Buffer.add_int64_be buffer (Int64.of_int id); Buffer.contents buffer let forget node id = Hashtbl.find_opt node.cursors id |> Option.iter (fun (relation, cursor) -> Hashtbl.remove node.cursors id; Protocols.Handle.release cursor; Protocols.Handle.release relation ) let define node path evaluator source = let open Utilities.Result in match List.rev path with | [] -> Error (Error.unnamed ()) | key :: directory -> ( let* program = Kernel.Program.load node.root ~evaluator ~source in match Kernel.Path.update node.root (Kernel.Path.of_list (List.rev directory)) key None (Some program) with | Ok true -> Ok "o" | Ok false -> Protocols.Handle.release program; Error (Error.program_taken (Kernel.Path.of_list path)) | Error c -> Protocols.Handle.release program; Error c ) (* Runs [target], which it releases, keeping a cursor on the result for the node to read: a program's result given [inputs], or the relation itself when [target] is not a program. *) let invoke node ?(inputs = []) target = let open Utilities.Result in let* relation = match Protocols.Executable.from target with | None -> Ok target | Some executable -> Protocols.Handle.releasing target (fun () -> Protocols.Executable.invoke executable inputs ~within:node.root ) in match Protocols.Enumerable.cursor relation with | Error c -> Protocols.Handle.release relation; Error c | Ok cursor -> let id = node.next in node.next <- id + 1; Hashtbl.replace node.cursors id (relation, cursor); Ok (cursor_message id) let fetch node id limit = let open Utilities.Result in let* _, cursor = Hashtbl.find_opt node.cursors id |> Option.to_result ~none:(Error.unknown_cursor id) in let* scan = Protocols.Cursor.require cursor in let* batch = Protocols.Cursor.fetch scan limit in if batch.exhausted then forget node id; Ok (batch_message batch) (* What a caller already knows, as the one-tuple relation a program is invoked with. *) let literal ({Concepts.Tuple.attributes; _} as tuple) = let domain = function | Concepts.Value.String _ -> "string" | Concepts.Value.Integer _ -> "integer" in Kernel.EphemeralRelation.literal (BatMap.String.map (fun value -> {Protocols.Schematics.domain= domain value; provenance= []}) attributes ) [tuple] let serve node request = let answer = function | Ok message -> T.send node.transport message | Error c -> T.send node.transport ("e" ^ Concepts.Condition.to_string_hum c) in match request with | Request.Define (path, evaluator, source) -> answer (define node path evaluator source) | Request.Invoke (path, bound) -> let inputs = Option.to_list (Option.map literal bound) in let result = Result.bind (Kernel.Path.lookup node.root (Kernel.Path.of_list path)) (invoke node ~inputs) in List.iter Protocols.Handle.release inputs; answer result | Request.Evaluate (evaluator, source) -> answer (Result.bind (Kernel.Program.load node.root ~evaluator ~source) (invoke node)) | Request.Next (id, limit) -> answer (fetch node id limit) | Request.Close id -> Ok (forget node id) let batch_of message = let open Utilities.Result in let length = String.length message in let rec tuples at read = if at = length then Ok (List.rev read) else if at + 4 > length then Error (Error.unexpected ()) else let size = Int32.to_int (String.get_int32_be message at) land 0xffff_ffff in if at + 4 + size > length then Error (Error.unexpected ()) else let* tuple = Translator.decode (String.sub message (at + 4) size) in tuples (at + 4 + size) (tuple :: read) in if length < 2 then Error (Error.unexpected ()) else let* read = tuples 2 [] in Ok {Protocols.Cursor.tuples= BatFingerTree.of_list read; exhausted= message.[1] <> '\000'} (* The node's answer to what RNT last asked, serving whatever the node asks in between. *) let rec await node = let open Utilities.Result in let* message = T.receive node.transport in match if message = "" then ' ' else message.[0] with | 'b' -> Result.map (fun batch -> `Batch batch) (batch_of message) | 'o' when message = "o" -> Ok `Done | 'e' -> Error (Error.failed (String.sub message 1 (String.length message - 1))) | _ -> ( match Request.of_message message with | None -> Error (Error.unexpected ()) | Some request -> let* () = serve node request in await node ) (* Lets the node's rnt-init closures define their programs. *) let handshake node = let open Utilities.Result in let* () = T.send node.transport "h" in let* reply = await node in match reply with `Done -> Ok () | `Batch _ -> Error (Error.unexpected ()) module Poll = struct let invoke_message id {module_; beam; _} bound = let buffer = Buffer.create (64 + String.length beam) in Buffer.add_char buffer 'i'; Buffer.add_int64_be buffer (Int64.of_int id); Buffer.add_uint16_be buffer (String.length module_); Buffer.add_string buffer module_; Buffer.add_int32_be buffer (Int32.of_int (String.length beam)); Buffer.add_string buffer beam; Option.iter (fun tuple -> Buffer.add_string buffer (Translator.encode tuple)) bound; Buffer.contents buffer let fetch_message id limit = let buffer = Buffer.create 13 in Buffer.add_char buffer 'f'; Buffer.add_int64_be buffer (Int64.of_int id); Buffer.add_int32_be buffer (Int32.of_int (max 0 (min limit 0xffff_ffff))); Buffer.contents buffer let close_message id = let buffer = Buffer.create 9 in Buffer.add_char buffer 'c'; Buffer.add_int64_be buffer (Int64.of_int id); Buffer.contents buffer class t node = object (self) inherit Kernel.Lifecycle.counted inherit Kernel.Identity.of_id method to_string = name method fetch limit = let open Utilities.Result in let* () = T.send node.transport (fetch_message (Oo.id self) limit) in let* reply = await node in match reply with `Batch batch -> Ok batch | `Done -> Error (Error.unexpected ()) method destroy = ignore (T.send node.transport (close_message (Oo.id self))) method protocols : Protocols.Handle.protocol list = Protocols.[Cursor.make self] end let start node program bound = let poll = new t node in T.send node.transport (invoke_message (Oo.id poll) program bound) |> Result.map (fun () -> Protocols.Handle.make poll) end (* The tuple a program is invoked with, if any: what its caller already knows. It is read now, so the input is only borrowed. *) let bound = function | [] -> Ok None | [input] -> ( let open Utilities.Result in let* _, tuples = Protocols.Relation.read input in match tuples with | [] -> Ok None | [tuple] -> Ok (Some tuple) | _ -> Error (Error.inputs ()) ) | _ -> Error (Error.inputs ()) class evaluator node = object (self) inherit Kernel.Lifecycle.counted inherit Kernel.Identity.of_id method to_string = name method destroy = Hashtbl.to_seq_keys node.cursors |> List.of_seq |> List.iter (forget node); T.stop node.transport method parse source = decode source method eval : Kernel.Program.kind -> Protocols.Handle.t list -> within:Protocols.Handle.t -> (Protocols.Handle.t, Concepts.Condition.condition) result = fun kind inputs ~within:_ -> match kind with | Beam program -> Result.map (fun bound -> Kernel.EphemeralRelation.instantiate program.heading ~modes:(fun () -> Ok [Concepts.Mode.mode [] Concepts.Cardinality.Countable]) (Kernel.EphemeralRelation.temporary program.heading) (fun () -> Poll.start node program bound) ) (bound inputs) | _ -> Error (Error.not_a_program ()) method protocols : Protocols.Handle.protocol list = [Protocols.Evaluator.make self] end end module Node = Make (Port) (* \karuta\program, where the node can define programs of its own. *) let programs root = let open Utilities.Result in let* found = Kernel.Path.find root Kernel.Path.(name @/ "program" @/ this) in match found with | Some programs -> Ok (Protocols.Handle.release programs) | None -> ( let programs = Kernel.Namespace.make () in match Kernel.Path.update root Kernel.Path.(name @/ this) "program" None (Some programs) with | Ok true -> Ok () | Ok false -> Ok (Protocols.Handle.release programs) | Error c -> Protocols.Handle.release programs; Error c ) let register root directories = let open Utilities.Result in let* port = Port.start directories in let node = Node.node port root in let evaluator = Protocols.Handle.make (new Node.evaluator node) in let bound = let* () = Kernel.Initialization.mount root name in let* () = programs root in Kernel.Path.update root Kernel.Path.(name @/ "evaluator" @/ this) "beam" None (Some evaluator) in match bound with | Ok true -> Node.handshake node | Ok false -> Protocols.Handle.release evaluator; Error (Error.taken ()) | Error c -> Protocols.Handle.release evaluator; Error c