aboutsummaryrefslogtreecommitdiffstats
path: root/subsystem/karuta/Beam.ml
blob: 82ae82799363d66ba86c5e5e3b5b6ff21a82032f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
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