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.
- instance names the Fumola VM instance the code runs against. Uses that name the same instance share its store.
- editor | archivist picks which of Fumola's two modes the code runs in. Exactly one is on. The archivist's name field shows only while the archivist is on.
- tiles switches the code between Fumola tiles and a string. Code that cannot be converted, tiles with a hole in them or text that does not parse, stays as it is.
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:
`myLivelit(`input): written from Hazel. This is the In wire.`myLivelit(`compute): the thunk the code runs in. It is reused across edits, so an edit keeps the thunk's history.`myLivelit(`output): the code's result, read back. This is the Out wire, and what the livelit expands to.
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:
- Program: the Fumola text the last run sent, printed from the tiles. Its parentheses show how the tiles grouped the code, which may not be how it reads.
- Outline: the runs' forces as trees, drawn the way web play draws them.
- Printed: what the last run printed.
- Events, Nodes and Edges: the instance's history, as in the panel described below.
What is not done yet#
- Editing is slow. Every keystroke in the tiles costs a full livelit view and update.
- The Outline covers the whole instance, not just this use: after a rename, the old name's tree is still there. An outline of one moment would fix it, and needs new prims from the runtime.
- The Program tab shows one long line. #143 adds a formatter to the runtime for it to use.
- The editor's input is a value, not a cell. The editor owns no cells, so there is no name to put one under.
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.