#+TITLE: Programs and evaluators * Why the kernel has no language RNT supplies objects, storage and the execution lifecycle, and leaves languages to its clients. SQL puts retrieval, data manipulation and data definition in one language, and the boundaries between them are hard to draw. In RNT, a language =A= may serve retrieval, =B= may serve manipulation and definition, and =C= may serve manipulation beside =B=. Nothing arbitrates between =B= and =C=, and a client picks either. The [[file:../lib/evaluators/FOL.ml][FOL]] evaluator is one such language, shipped with RNT for retrieval. That leaves the kernel two jobs. It has to run a program without knowing its language, and it has to let programs in different languages read each other's results. Programs and evaluators are how it does both. * Programs The kernel holds, stores and runs programs whose structure it cannot know. A [[file:../lib/kernel/Program.ml][program]] is therefore an image, in the manner of a Windows [[https://en.wikipedia.org/wiki/Portable_Executable][Portable Executable]], whose header tells the loader which subsystem runs it. #+begin_src ocaml type kind = .. type image = {evaluator: string; kind: kind; source: string} #+end_src =evaluator= names the evaluator that runs the program, as =scope:language=. =kind= is the program as that evaluator understands it. =source= is its text, for display and for storage. =kind= is an [[https://ocaml.org/manual/extensiblevariants.html][extensible variant]], extended the way [[file:object-system.org][protocols]] are. Each evaluator adds its own constructor, as FOL adds =Fol of Path.t term=. The tree keeps its OCaml type, and only the evaluator that defined the constructor can match on it, so the kernel passes programs around with no encoding in between. The evaluator name is the one string a program carries, because the kernel looks evaluators up by name and storage can keep a name but not a constructor. Every program speaks [[file:../lib/protocols/Executable.mli][Executable]], and a program built with a root can run. #+begin_src ocaml method invoke : Handle.t list -> within:Handle.t -> (Handle.t, condition) result #+end_src Invoking a program looks up =\[scope]\evaluator\[language]= under its root, and hands that evaluator the kind, the inputs and =within=. A bare =language= names the =system= scope. The root is a capability, so a program reaches only the evaluators of the root it was given. A program built without a root can be stored but not run, and invoking it fails with =unloaded-program=. A name with nothing registered under it fails with =no-evaluator=. * Evaluators An evaluator is an object with the [[file:../lib/protocols/Evaluator.mli][Evaluator]] protocol. #+begin_src ocaml method parse : string -> (kind, condition) result method eval : kind -> Handle.t list -> within:Handle.t -> (Handle.t, condition) result #+end_src =eval= runs a kind, and fails for a kind that is not its own. =parse= turns source back into a kind, which is how the kernel reloads a stored program. A program built in code skips parsing. =FOL.program term= builds one from a tree and prints the tree as its source, so a test can build a plan without a syntax. FOL has no text syntax yet, and its source is the [[https://en.wikipedia.org/wiki/Bencode][bencoded]] term. #+begin_src plantuml :file ../images/evaluators-classes.svg @startuml hide empty members skinparam classAttributeIconSize 0 class Program <> { evaluator : string kind : Program.kind source : string } interface Executable { invoke(inputs, within) : relation } interface Evaluator { parse(source) : kind eval(kind, inputs, within) : relation } class "FOL evaluator" as FOL class EphemeralRelation interface Relation { contains(tuple) : bool modes() : Mode.t } interface Enumerable { enumerate() : cursor } interface Schematics { describe() : heading } Program ..|> Executable : runs when given a root FOL ..|> Evaluator Program --> Evaluator : names it as\nscope:language EphemeralRelation ..|> Relation EphemeralRelation ..|> Enumerable EphemeralRelation ..|> Schematics EphemeralRelation o--> "0..1" Program : wraps Evaluator ..> EphemeralRelation : eval returns @enduml #+end_src #+RESULTS: [[file:../images/evaluators-classes.svg]] * Within In RNT, holding a handle is what authorizes its use, so an evaluator gets no context beyond one directory. A program names relations by path, and its evaluator resolves those paths in the directory the program is invoked =within=. It can reach nothing else. Resolving paths at each invocation is what lets one program run against any state. Read through =\system\branch\master\multigroup\m\schema\s\relation\r=, a stored program runs within that branch state. Read through =\system\branch\master\previous\...=, it runs within the previous state and reads what was there then. It can never read outside its branch. Evaluation never commits, and neither does exhausting a cursor. Committing is an explicit host operation. Cancellation, budgets and resource accounting are not implemented. * Results Programs in different languages can read each other's results only if those results share one interface. The result of every program is an [[file:../lib/kernel/EphemeralRelation.ml][ephemeral relation]]. It describes its heading, declares its [[file:modes.org][modes]] and answers membership. Enumerating it gives a cursor, so operators stack as cursors over cursors, as in Graefe's Volcano iterator model ("Volcano, an Extensible and Parallel Query Evaluation System", 1994), and nothing in between is materialized. An ephemeral relation can wrap a program, through =EphemeralRelation.of_program=. Reading it invokes the program within the state it was given. Asking its modes, or whether it holds a tuple, invokes the program and asks the relation the program returns. For FOL this is cheap, because FOL builds its result without reading a tuple. Programs that change state have no result protocol yet. * Storage A multigroup stores an ephemeral relation that wraps a program as the program's evaluator name and source, beside its heading. It stores any other relation as its tuples. When a branch reads the relation back, the evaluator registered under that name parses the source into a kind, and the relation wraps the program that comes out. The parse happens at lookup, so a missing evaluator or a malformed source fails at =Path.lookup= rather than at the first read. #+begin_src plantuml :file ../images/evaluators-reading.svg @startuml actor Client participant "Path.lookup" as Path participant Multigroup participant EphemeralRelation as E participant Program participant "evaluator under\nthe program's name" as Ev Client -> Path : lookup a relation in a branch state Path -> Multigroup : find it Multigroup -> E : load (evaluator name, source, heading) E -> Program : Program.load root Program -> Ev : parse source Ev --> Program : kind E --> Client : relation wrapping the program Client -> E : enumerate E -> Program : invoke ~within:branch state Program -> Ev : eval kind ~within Ev --> Program : result relation Program --> E : result relation E --> Client : cursor over the result @enduml #+end_src #+RESULTS: [[file:../images/evaluators-reading.svg]] * Composition Evaluators compose through programs and results, and neither knows the other's kind. When FOL resolves a path to a relation that wraps a program, it checks the program's kind. - A FOL program is inlined into the plan. FOL then sees through it, and refuses a program that reaches itself with =cyclic-program=. - A program of any other kind is invoked within the same state, and its result enters the plan like any other relation. A program in another language reads a FOL program the same way, by resolving its path and reading the relation. The [[file:../test/unit/kernel/Branch.ml][branch tests]] do both, with a toy language whose programs shout the titles of a FOL relation. FOL detects cycles only among its own programs. A cycle across languages, where a FOL program reads a toy program that reads it back, recurses without end. Nothing checks one evaluator's assumptions about another's results either, so the caller has to establish that the callee's results meet its needs. * Registration The system's evaluators are registered at initialization, under =\system\evaluator=. A subsystem mounts its own root with [[file:../lib/kernel/Initialization.mli][Initialization.mount]], which holds =\[name]\evaluator=, and registers its evaluators there. Its programs then name them as =name:language=. A foreign registration path is a later concern. * Capability tags A client should be able to ask which evaluators serve manipulation, and get =B= and =C= back. The intent is that each evaluator declares a set of opaque labels, such as =drl=, =dml=, =ddl=, =vcs= or any a client invents, and that the registry indexes them. None of this is implemented.