aboutsummaryrefslogblamecommitdiffstats
path: root/subsystem/karuta/Beam.ml
blob: 82ae82799363d66ba86c5e5e3b5b6ff21a82032f (plain) (tree)
1
2
3
4
5
6
7
8
9
10




                         
                                                                       
                                                                                                 
 

                                      
                                                                                           
 










                                                                                            









                                                                                                  






                                                                       
                      

   














                                                                       
                                                                                                   

                                                             

                                        
                                                                                                   
                                                                             

                                                                           
         

                                                                                              


                                                            



                                                                        
                   

              



                                                        


                                           




                                                                                        

                                                 


                                                                                
                                                     


                                     

















                                                                                  
                                    



















































































































































































                                                                                                    
                      

                                                             






                                                           

                                                                     














                                                                                
                  





                                        



                                                                                          
 
                                                                                    


                                                                                        


                                                                 


                                                          
                        


                                      
                             




                                                                                   
                                         
 




                                                                     






                                                                                               
                                                        

                                                


                                                                                         

   

                         












                                                                                                  


                                       

                                                                    

                                                      
                              


                                                                                                  
                                  





                                         
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