fumola / hazel integration

Fumola in Hazel

Fumola programs running inside Hazel documents: first the current design, then the earlier attempts that led to it.

The latest: one livelit#

The current integration is one builtin livelit, ^fumola_wip. It combines the best parts of the three earlier steps described below: a livelit's place in the document, a Fumola program written as Hazel tiles, and a live view of the instance beside the code.

It lives on the Hazel branch remote-splices (#2616), which has a live build that opens on the slide shown here: Documentation › Livelits › Advanced › Fumola, a Work in Progress. The name says what it is. It is a prototype, and editing tiles inside a livelit is slow today: every keystroke costs a full view and update.

The slide#

The slide has two uses of the livelit, sharing one Fumola instance. In the program's text they read as below; in the editor each one is drawn as its GUI.

let n = 20 in
let result = ^^livelit(^fumola_wip((
  instance = "myInstance", name = "myLivelit", editor = false,
  bind_input = true, tiles = true, input = (n * 2),
  code = (fumola $graphical as myInstance in
    let halve  = `halve`  := thunk { (@ input) / 2 };
    let double = `double` := thunk { (force halve) * 2 };
    force double
  end)))) in
let events = ^^livelit(^fumola_wip((
  instance = "myInstance", name = "", editor = true,
  bind_input = false, tiles = true, input = (0),
  code = (fumola $graphical as myInstance in
    (prim "adaptonPeekHistory" ()).events
  end)))) in
(result, events)

The first use is an archivist computing with its input. The second is the editor of the same instance, reading back the events the first one caused. The result is (40, […]): the computation's answer, and the instance's history as a Hazel list.

The head row#

Each use starts with one row: instance [myInstance]  editor | archivist [myLivelit]  tiles.

The archivist#

As the archivist, the code runs inside a force, in a thunk named by the archivist's name, so what it reads and writes is recorded. Every cell it owns is named under that name:

The input is a splice, so it can name a Hazel variable bound outside the livelit, like n above. It crosses into Fumola as a hazel … end escape, evaluated when the program runs, where n is in scope.

The code's input is the cell, not the value in it. The code reads it with @ input, wherever and whenever it chooses, and each read is an edge of its own. In the slide, the computation never reads the input itself. It makes two sub-thunks. halve reads the input cell, inside itself, and halves it. double is given halve unforced, forces it, and doubles the result. The watch pane's Outline shows that structure: double forcing halve, and halve reading `myLivelit(`input). With n = 20 the input is 40, halved to 20, and doubled back to 40.

Left empty, the archivist's name defaults to hazel_ followed by the id of the Hazel term the use is, with underscores for the id's hyphens, since a Fumola name cannot hold a hyphen. The default is written into the use's model once, so it stays the same when the document is reloaded.

The editor#

As the editor, the code runs at the top level of the instance, with no force around it. It needs no name and owns no cells, and its value is what the livelit expands to. peek, reset and the graph introspection belong here.

The editor's input is optional, with a checkbox beside the role switch. Ticked, input is bound to the Hazel value. Unticked, as in the slide's second use, input is not bound at all. That use asks the instance for its history and returns the events, which cross back into Hazel as data: a list of (event = …, metaTime = …) records, the same events the Events tab shows. It runs after the archivist, so the archivist's run is in them.

The watch pane#

Below the head row, each use takes the full width of the notebook. On the left are the In wire, the code, and the Out wire. The Out wire shows the last run's value, or the runtime's error when the run failed. On the right is the watch pane for the instance, with six tabs:

What is not done yet#

How we got here#

The earlier work came in three steps, and each settled a question the next could build on. All three are in one build, beside a third embedded sub-language (the core logic of the Blackboard proof assistant), on the branch experimental-lang-integration (#2536). It has a live build served from this site.

First: livelits, and what they proved#

The first route put Fumola behind four builtin livelits. A program was a string in the livelit's model, and Hazel handed it to Fumola's own parser and got a value back.

# declare a runtime, and the Adapton semantics it runs #
let rt : Int = ^fumola_new(7, Graphical) in

# run a program as a named thunk, and use its result as a Hazel value #
let answer : Int =
  ^fumola_put_force(7, "`gcd", "Gcd.gcd(12, 18)") in

# read the graph the run left behind, as a table of records #
let events : [EventRow] =
  ^fumola_eval(7, "Adapton.peekEvents()") in

^^probe_table(events);

The split followed Fumola's two modes. ^fumola_put_force wrapped its program as force(`n := thunk { … }), so it ran as the archivist, and editing the program re-forced the same name, reusing the thunk's history. ^fumola_eval ran its program at the top level, as the editor. ^fumola_with took a Hazel value as input, and ^fumola_new declared which semantics a runtime ran.

It proved the thing that had to be proved first: a Fumola program can run inside a live document and hand its result back as a value the surrounding Hazel program can use. Because the string reached Fumola's real parser, every form Fumola has worked from day one. The cost was the editor: the program was opaque text in a one-line field, with no tiles, no structure and no highlighting. And there was one input slot, in one of the four livelits.

^fumola_wip keeps the two modes, now as a switch in one livelit rather than a choice between two, and gives both of them an input.

Then: tiles, and the program as a term#

The second route made Fumola a sort in Hazel's tile grammar. The program was no longer a string that happened to be Fumola. It was a term, with tiles in the editor, printed to a string only on the way to the runtime.

let answer = fumola $graphical as store in
  { let cell = `myCell` := 333;
    let t = `myThunk` := thunk { @ cell };
    force(t) }
end in

answer

store names the instance, $graphical is the semantics it runs, and the program sits between in and end. The instance is named in text the programmer wrote, on purpose: deriving it from the Hazel node's id would start a fresh store whenever that id changed, which is on exactly the edits worth watching. Because the program was a term, unmatched delimiters became the editor's business, errors had locations, and a Hazel expression could stand anywhere in the program as hazel … end. This route also made graphical the default, as it is in Fumola.

^fumola_wip's code field is exactly this form: a fumola … end tile, in a splice.

And then: watching the store#

Once Hazel could see inside the program, it could show what the program did. That build has a panel for the instance the cursor is in, with three views of one history. Nodes are the cells and thunks the store holds. Edges are what passed between them, each with the action that made it. Events tell the same story in order, with the Begin and End of every force.

Most edges in a live notebook come from @here, the editor putting values in rather than the program computing. The panel can dim or hide those, leaving only what the program itself did, and a reset empties the store and rebuilds it in either semantics.

^fumola_wip draws this same panel inside the livelit, for its own instance, beside the code: the watch pane's Events, Nodes and Edges tabs.

The bet, and how it came out#

The earlier write-up ended on a bet: that tiles were the path, and livelits a dead end. It gave two reasons. A program held as an opaque string cannot be given structure later. And a livelit runs at the wrong time: it expands during elaboration, while a fumola … end form runs during evaluation, and with a store those are different times.

^fumola_wip is a livelit that avoids both. Its program is not a string but a tiles splice. And what it expands to is a fumola … end term, so the program runs during evaluation, like any other. The livelit supplies the GUI and the wires, and the Fumola program is still a term. So the bet was right about tiles, but wrong that a livelit has to give them up.

The step in between was remote refs (#2616): splices that a Fumola instance reads from and writes to. The In and Out wires follow that design: an In ref carries a Hazel value into a Fumola cell, and an Out ref brings a cell back.