aboutsummaryrefslogtreecommitdiffstats
path: root/subsystem/karuta/Beam.ml
blob: 0f9e93cec3a9b31e861300d7944eae4daf1a4dd1 (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
(* The beam evaluator. Its programs are functions on a BEAM node, each
   taking one relation per input and giving back a relation of the
   heading the program declares. rnt-node.lfe serves them. Tuples cross
   as Translator writes them, and a message is a byte for its kind
   followed by big-endian integers and binaries:

     RNT   i Cursor:32 M:16 Module F:16 Function Reader:32...  invoke
     RNT   f Cursor:32 Limit:32                                 fetch
     RNT   c Cursor:32                                          close
     node  n Reader:32 Limit:32                                 next
     node  l (Size:32 Segment)...                               lookup
     both  b Exhausted:8 (Size:32 Tuple)...                     a batch
     RNT   r Reader:32                                          a reader
     both  e Message                                            a failure

   fetch and next are answered with a batch or a failure, lookup with a
   reader or a failure, invoke and close not at all. The node applies
   the function at the first fetch, so a function that cannot be applied
   fails it. Calls nest: until its answer comes, whoever waits serves the
   calls that come instead.

   lookup asks for the relation at a path, within what the program runs
   within, and FOL answers it. The node reads it with next like an
   input, and the cursor being fetched, whose program asked, keeps it
   until it closes. *)

open Rnt

module Error = struct
  open Concepts.Condition

  let not_a_program () =
    condition "not-a-beam-program" "The evaluator was given a program of another kind" empty

  let failed message =
    condition "beam-failed" "The BEAM node failed to answer a call"
      ("message" |=| Concepts.Value.String message)

  let closed () = condition "beam-closed" "The BEAM node is gone" empty

  let unexpected () =
    condition "unexpected-beam-message" "The BEAM node said something out of turn" empty

  let unheld_input () =
    condition "unheld-input" "An input could not be held for as long as the program needs it" empty
end

module type TRANSPORT = sig
  type t

  val send : t -> string -> (unit, Concepts.Condition.condition) result
  val receive : t -> (string, Concepts.Condition.condition) result
end

(* A node RNT starts as its child and speaks to over the child's stdin
   and stdout, each message behind its length in four bytes, as a port
   opened with {packet, 4} frames it. *)
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)))
    with End_of_file | Sys_error _ -> Error (Error.closed ())

  (* The node compiles every LFE module in [directories], which must
     hold rnt.lfe and rnt-node.lfe, and serves until [stop] closes its
     stdin. If it cannot, it says why on stderr and halts. It is started
     with erl and not lfe, whose shell would read stdin, so LFE has to be
     where erl finds it, as ERL_LIBS says. *)
  let start ?(erl = "erl") directories =
    let boot =
      "try [begin {ok, Ms, _} = lfe_comp:file(F, [binary, return]), [{module, M} = \
       code:load_binary(M, F, B) || {ok, M, B, _} <- Ms] end || D <- init:get_plain_arguments(), F \
       <- filelib:wildcard(D ++ \"/*.lfe\")], 'rnt-node':start() catch C:R:S -> \
       io:format(standard_error, \"~p:~p~n~p~n\", [C, R, S]), halt(1) end."
    in
    let input, output =
      Unix.open_process_args erl
        (Array.of_list ([erl; "-noshell"; "-noinput"; "-eval"; boot; "-extra"] @ directories))
    in
    {input; output}

  let stop {input; output} = ignore (Unix.close_process (input, output))
end

let subsystem = "karuta"
let language = "beam"
let name = subsystem ^ ":" ^ language

type program =
  {module_: string; function_: string; heading: Protocols.Schematics.relation_description}

type Kernel.Program.kind += Beam of program

module Bencode = Concepts.Encoding.Bencode

let encode {module_; function_; heading} =
  Bencode.Dict
    [ "module", Bencode.String module_;
      "function", Bencode.String function_;
      ( "heading",
        Bencode.Dict
          ( BatMap.String.bindings heading
          |> List.map (fun (attribute, {Protocols.Schematics.domain; _}) ->
              attribute, Bencode.String domain ) ) ) ]
  |> Bencode.to_bytes
  |> Bytes.to_string

let decode source =
  let open Utilities.Result in
  let* program = Bencode.of_bytes (Bytes.of_string source) in
  let* module_ = Concepts.Encoding.Field.require_string "module" program in
  let* function_ = Concepts.Encoding.Field.require_string "function" program in
  let* heading = Bencode.field "heading" program |>> Bencode.as_dict in
  let* heading =
    heading
    |> List.map (fun (attribute, domain) ->
        Bencode.as_string domain
        |> Result.map (fun domain -> attribute, {Protocols.Schematics.domain; provenance= []}) )
    |> Utilities.List.sequence
  in
  Ok {module_; function_; heading= BatMap.String.of_list heading}

let int32 buffer n = Buffer.add_int32_be buffer (Int32.of_int n)

let invoke_message cursor {module_; function_; _} readers =
  let buffer = Buffer.create 64 in
  Buffer.add_char buffer 'i';
  int32 buffer cursor;
  List.iter
    (fun name ->
      Buffer.add_uint16_be buffer (String.length name);
      Buffer.add_string buffer name )
    [module_; function_];
  List.iter (int32 buffer) readers;
  Buffer.contents buffer

let fetch_message cursor limit =
  let buffer = Buffer.create 9 in
  Buffer.add_char buffer 'f'; int32 buffer cursor; int32 buffer limit; Buffer.contents buffer

let close_message cursor =
  let buffer = Buffer.create 5 in
  Buffer.add_char buffer 'c'; int32 buffer cursor; Buffer.contents buffer

let batch_message {Protocols.Cursor.tuples; exhausted} =
  let buffer = Buffer.create 256 in
  Buffer.add_char buffer 'b';
  Buffer.add_uint8 buffer (Bool.to_int exhausted);
  BatFingerTree.iter
    (fun tuple ->
      let term = Translator.encode tuple in
      int32 buffer (String.length term);
      Buffer.add_string buffer term )
    tuples;
  Buffer.contents buffer

let reader_message reader =
  let buffer = Buffer.create 5 in
  Buffer.add_char buffer 'r'; int32 buffer reader; Buffer.contents buffer

let failure_message condition = "e" ^ Concepts.Condition.to_string_hum condition

(* The pieces of [message] from [at] on, each behind its size in four bytes. *)
let chunks message at =
  let length = String.length message in
  let rec read at pieces =
    if at = length then Ok (List.rev pieces)
    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 read (at + 4 + size) (String.sub message (at + 4) size :: pieces)
  in
  read at []

let batch_of message =
  let open Utilities.Result in
  let* terms = chunks message 2 in
  let* tuples = List.map Translator.decode terms |> Utilities.List.sequence in
  Ok {Protocols.Cursor.tuples= BatFingerTree.of_list tuples; exhausted= message.[1] <> '\000'}

module Make (T : TRANSPORT) = struct
  (* The root programs find their evaluators in, the cursors the node
     reads through us by the reader id it knows each by, and the last id
     given to a reader or a cursor. *)
  type node =
    { root: Protocols.Handle.t;
      transport: T.t;
      readers: (int, Protocols.Handle.t) Hashtbl.t;
      mutable last: int }

  let fresh node =
    node.last <- node.last + 1;
    node.last

  let next node message =
    let open Utilities.Result in
    let reader = Int32.to_int (String.get_int32_be message 1) in
    let limit = Int32.to_int (String.get_int32_be message 5) in
    let* cursor =
      Hashtbl.find_opt node.readers reader |> Option.to_result ~none:(Error.unexpected ())
    in
    let* scan = Protocols.Cursor.require cursor in
    Protocols.Cursor.fetch scan limit

  let lookup node within message =
    let open Utilities.Result in
    let* path = chunks message 1 in
    let program =
      Evaluators.FOL.program ~root:node.root (Evaluators.FOL.Base (Kernel.Path.of_list path))
    in
    let* relation =
      Protocols.Handle.releasing program (fun () ->
          let* executable = Protocols.Executable.require program in
          Protocols.Executable.invoke executable [] ~within )
    in
    let* cursor = Kernel.EphemeralRelation.enumerate_owned relation in
    let reader = fresh node in
    Hashtbl.replace node.readers reader cursor;
    Ok reader

  (* [own] keeps a reader the node looked up for the cursor being fetched. *)
  let rec await node within own =
    let open Utilities.Result in
    let* message = T.receive node.transport in
    let reply answer =
      let* () = T.send node.transport answer in
      await node within own
    in
    match if message = "" then ' ' else message.[0] with
    | 'b' when String.length message >= 2 -> batch_of message
    | 'e' -> Error (Error.failed (String.sub message 1 (String.length message - 1)))
    | 'n' when String.length message = 9 -> (
        reply
        @@
        match next node message with
        | Ok batch -> batch_message batch
        | Error c -> failure_message c )
    | 'l' -> (
        reply
        @@
        match lookup node within message with
        | Ok reader -> own reader; reader_message reader
        | Error c -> failure_message c )
    | _ -> Error (Error.unexpected ())

  let forget node readers =
    List.iter
      (fun reader ->
        Hashtbl.find_opt node.readers reader |> Option.iter Protocols.Handle.release;
        Hashtbl.remove node.readers reader )
      readers

  (* It keeps the readers of its inputs, and of what its program looked up. *)
  class cursor node id inputs within =
    object (self)
      inherit Kernel.Lifecycle.counted
      inherit Kernel.Identity.of_id
      val mutable readers = inputs
      method to_string = "beam-cursor"

      method fetch limit =
        Result.bind
          (T.send node.transport (fetch_message id limit))
          (fun () -> await node within (fun reader -> readers <- reader :: readers))

      method destroy =
        ignore (T.send node.transport (close_message id));
        forget node readers

      method protocols : Protocols.Handle.protocol list = Protocols.[Cursor.make self]
    end

  let open_readers node inputs =
    Utilities.List.foldm_left
      (fun readers input ->
        match Protocols.Enumerable.cursor input with
        | Ok cursor ->
            let reader = fresh node in
            Hashtbl.replace node.readers reader cursor;
            Ok (reader :: readers)
        | Error c -> forget node readers; Error c )
      [] inputs
    |> Result.map List.rev

  let invoke node program inputs ~within =
    let open Utilities.Result in
    let* readers = open_readers node inputs in
    let id = fresh node in
    match T.send node.transport (invoke_message id program readers) with
    | Ok () -> Ok (Protocols.Handle.make (new cursor node id readers within))
    | Error c -> forget node readers; Error c

  (* The relation reads its inputs each time it is enumerated, so it holds them. *)
  let hold inputs =
    let held = List.filter_map Protocols.Handle.copy inputs in
    if List.length held = List.length inputs then Ok held
    else begin
      List.iter Protocols.Handle.release held;
      Error (Error.unheld_input ())
    end

  (* It looks relations up within [within] too, so it holds that as well.
     TODO: A program should declare its modes as it declares its heading.
     Until then every program is taken to end. *)
  let generate node program inputs ~within =
    let open Utilities.Result in
    let* held = hold (within :: inputs) in
    match held with
    | [] -> Error (Error.unheld_input ())
    | within :: inputs ->
        Ok
          (Kernel.EphemeralRelation.instantiate ~inputs:held program.heading
             ~modes:(fun () -> Ok [Concepts.Mode.mode [] Concepts.Cardinality.Finite])
             (Kernel.EphemeralRelation.temporary program.heading)
             (fun () -> invoke node program inputs ~within) )

  class evaluator node =
    object (self)
      inherit Kernel.Lifecycle.null
      inherit Kernel.Identity.of_id
      method to_string = "beam-evaluator"
      method parse source = decode source |> Result.map (fun program -> Beam program)

      method eval kind inputs ~within =
        match kind with
        | Beam program -> generate node program inputs ~within
        | _ -> Error (Error.not_a_program ())

      method protocols : Protocols.Handle.protocol list = [Protocols.Evaluator.make self]
    end

  (* \karuta is the subsystem's root, and its programs name the evaluator karuta:beam. *)
  let register root transport =
    let open Utilities.Result in
    let node = {root; transport; readers= Hashtbl.create 8; last= 0} in
    let* _ = Kernel.Initialization.mount root subsystem in
    let* _ =
      Kernel.Path.update root
        Kernel.Path.(subsystem @/ "evaluator" @/ this)
        language None
        (Some (Protocols.Handle.make (new evaluator node)))
    in
    Ok ()
end

let program ?root ~module_ ~function_ heading =
  let program = {module_; function_; heading} in
  Kernel.Program.make ?root ~evaluator:name ~source:(encode program) (Beam program)