diff options
| author | Marcos Magueta <maguetamarcos@gmail.com> | 2026-10-31 00:51:06 -0300 |
|---|---|---|
| committer | Marcos Magueta <maguetamarcos@gmail.com> | 2026-10-31 00:51:06 -0300 |
| commit | 38e60910a40a4b009db87049f11563f55d88b20f (patch) | |
| tree | 9e2bbb5ecbce210b561b38447ef658bc227cfd96 /subsystem/karuta | |
| parent | 8c2f0db7337328570af57461a3d95203537ac185 (diff) | |
| download | RNT-master.tar.gz RNT-master.zip | |
Diffstat (limited to 'subsystem/karuta')
| -rw-r--r-- | subsystem/karuta/Beam.ml | 414 | ||||
| -rw-r--r-- | subsystem/karuta/Translator.ml | 107 |
2 files changed, 521 insertions, 0 deletions
diff --git a/subsystem/karuta/Beam.ml b/subsystem/karuta/Beam.ml new file mode 100644 index 0000000..82ae827 --- /dev/null +++ b/subsystem/karuta/Beam.ml @@ -0,0 +1,414 @@ +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, Function, 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 no inputs" 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; + function_: 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 4 source with + | Some ([module_; function_; beam], attributes) + when String.length module_ <= 0xffff && String.length function_ <= 0xffff -> + let heading = BatMap.String.filter_map domain attributes in + if BatMap.String.cardinal heading = BatMap.String.cardinal attributes then + Ok (Beam {module_; function_; 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/0 -> o | e + - i Id:64 M:16 Module F:16 Function B:32 Beam + start Module:Function() 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 invoke the program 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. *) +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 + | 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' -> finished (Invoke (path ())) + | '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 [program], which it releases, keeping a cursor on the result + for the node to read. *) + let invoke node program = + let open Utilities.Result in + let* relation = + Protocols.Handle.releasing program (fun () -> + let* executable = Protocols.Executable.require program in + Protocols.Executable.invoke executable [] ~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) + + 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 -> + answer (Result.bind (Kernel.Path.lookup node.root (Kernel.Path.of_list path)) (invoke node)) + | 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/0 functions 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_; function_; beam; _} = + let buffer = Buffer.create (64 + String.length beam) in + Buffer.add_char buffer 'i'; + Buffer.add_int64_be buffer (Int64.of_int id); + List.iter + (fun name -> + Buffer.add_uint16_be buffer (String.length name); + Buffer.add_string buffer name ) + [module_; function_]; + Buffer.add_int32_be buffer (Int32.of_int (String.length beam)); + Buffer.add_string buffer beam; + 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 = + let poll = new t node in + T.send node.transport (invoke_message (Oo.id poll) program) + |> Result.map (fun () -> Protocols.Handle.make poll) + end + + 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, inputs with + | Beam program, [] -> + Ok + (Kernel.EphemeralRelation.instantiate program.heading + ~modes:(fun () -> Ok [Concepts.Mode.mode [] Concepts.Cardinality.Countable]) + (Kernel.EphemeralRelation.temporary program.heading) + (fun () -> Poll.start node program) ) + | Beam _, _ -> Error (Error.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 diff --git a/subsystem/karuta/Translator.ml b/subsystem/karuta/Translator.ml new file mode 100644 index 0000000..5dec2e7 --- /dev/null +++ b/subsystem/karuta/Translator.ml @@ -0,0 +1,107 @@ +open Rnt + +module Error = struct + open Concepts.Condition + + let malformed () = + condition "malformed-beam-tuple" + "A term is not {Type, #{Name => Value}} of binaries, and integers that fit an int" empty +end + +let binary buffer s = + Buffer.add_uint8 buffer 109; + Buffer.add_int32_be buffer (Int32.of_int (String.length s)); + Buffer.add_string buffer s + +let integer buffer n = + let rec digits m = if m = 0 then [] else (m land 0xff) :: digits (m lsr 8) in + let digits = digits (abs n) in + Buffer.add_uint8 buffer 110; + Buffer.add_uint8 buffer (List.length digits); + Buffer.add_uint8 buffer (if n < 0 then 1 else 0); + List.iter (Buffer.add_uint8 buffer) digits + +let encode {Concepts.Tuple.type_; attributes} = + let buffer = Buffer.create 64 in + Buffer.add_uint8 buffer 131; + Buffer.add_uint8 buffer 104; + Buffer.add_uint8 buffer 2; + binary buffer type_; + Buffer.add_uint8 buffer 116; + Buffer.add_int32_be buffer (Int32.of_int (BatMap.String.cardinal attributes)); + BatMap.String.iter + (fun name value -> + binary buffer name; + match value with + | Concepts.Value.String s -> binary buffer s + | Concepts.Value.Integer n -> integer buffer n ) + attributes; + Buffer.contents buffer + +exception Malformed + +let read arity bytes = + let position = ref 0 in + let take n = + if !position + n > String.length bytes then raise Malformed; + let at = !position in + position := at + n; + at + in + let u8 () = String.get_uint8 bytes (take 1) in + let i32 () = Int32.to_int (String.get_int32_be bytes (take 4)) in + let u32 () = i32 () land 0xffff_ffff in + let expect byte = if u8 () <> byte then raise Malformed in + let binary () = + let n = u32 () in + String.sub bytes (take n) n + in + let big n = + let negative = u8 () = 1 in + if n > 8 then raise Malformed; + let at = take n in + let rec magnitude i m = + if i < 0 then m + else + magnitude (i - 1) + (Int64.logor (Int64.shift_left m 8) (Int64.of_int (String.get_uint8 bytes (at + i)))) + in + let m = magnitude (n - 1) 0L in + let limit = if negative then Int64.neg (Int64.of_int min_int) else Int64.of_int max_int in + if Int64.compare m 0L < 0 || Int64.compare m limit > 0 then raise Malformed; + Int64.to_int (if negative then Int64.neg m else m) + in + let value () = + match u8 () with + | 109 -> Concepts.Value.String (binary ()) + | 97 -> Concepts.Value.Integer (u8 ()) + | 98 -> Concepts.Value.Integer (i32 ()) + | 110 -> Concepts.Value.Integer (big (u8 ())) + | _ -> raise Malformed + in + let rec attributes n map = + if n = 0 then map + else begin + expect 109; + let name = binary () in + let value = value () in + attributes (n - 1) (BatMap.String.add name value map) + end + in + match + expect 131; + expect 104; + expect arity; + let names = List.init (arity - 1) (fun _ -> expect 109; binary ()) in + expect 116; + let attributes = attributes (u32 ()) BatMap.String.empty in + if !position <> String.length bytes then raise Malformed; + names, attributes + with + | read -> Some read + | exception Malformed -> None + +let decode bytes = + match read 2 bytes with + | Some ([type_], attributes) -> Ok {Concepts.Tuple.type_; attributes} + | _ -> Error (Error.malformed ()) |
