Sample run: (computes 3 + 2 * 4)
npx @cicada-lang/inet-js run https://gist.githubusercontent.com/bojidar-bg/6c52d1f1ed3fb3a583aaff9a184687fe/raw/73ab4e6d9dbcddaba230c529a4fc0c236a353db8/Nat_bootstrap.test.i | grep 'activeEdge-' | wc -l
Result:
11
Meaning: 11 bootstrapped nodes exist in the final graph, which is 10 s_add1
nodes and 1 s_zero
node.
Hence: 3 + 2 * 4 = 10 \o/
A longer writeup would be necessary to explain the full operation, but in brief:
The bootstrapped nodes execute independently, without any "program" which walks a list of active nodes. This way, any improvements in the parallelism of the underlying executor would be immediatelly passed to the nodes.
Simulated nodes are represented by two node types, nodePos
and nodeNeg
-- depending on whether their active/primary edge is positive (an input) or negative (an output).
Each node holds its identity as a Symbol
. Symbol
is pretty similar to a Binary number, just because base two encoding makes for some rather short symbols without complicating the rest of the functionality.
Since any interaction that happens is guaranteed to be between a pair of (nodePos, nodeNeg)
, we can store the interaction rules with either of the two. Without loss of generality, we store the rules in nodePos
.
The rules are represented as a SymbolMap(SymbolMap(Rule))
-- where SymbolMap
is a simple structure that allows retrieving a particular entry in O(log2(symbolsize)) operations.
When an interaction occurs, we look up the positive node's Symbol in the first layer map, followed by the negative node's Symbol in the second map. Note that this mean two nodes could in theory share a symbol if they have different active edge signs, though we never make use of that.
The Rule
-s themselves are implemented really succinctly (thanks to interaction nets, it felt quite profound the first time it clicked in place), with a rule
node which outputs two lists of non-active edge connections for each of the two nodes that are part of the interaction, and collects a list of all the newly-created nodes as input.
To execute a Rule
we duplicate it and then use an exec
node to replace the missing inputs and grab the output.
And finally, the "magic" happens as we "inflate" the outputs by giving the newly-created nodes full references to duplicates of the rules table... aaand...
waves hands for dramatic effect
...they suddenly become normal nodePos
-s and nodeNeg
-s!
And the whole process can start anew!
Two sleepless nights (no caffeine, sheer force of will), plenty of headscratching to get the ideas flowing. It was a wonderfully fun challenge, I would definitelly recommend... assuming one has the time.
At first, I started with the rule table datastructure and got Symbol
and SymbolMap
done.
Then, there was some playing around with the generic del
and dup
nodes (those feel like something which would be best if it could be auto-generated), until I roughly mastered those.
Next, I got some preliminary rules going; at first the rule had a list of "Instructions" that would have had been executed in-order. Experimentation with the pop
instruction led to discovering dupOut
and the fact that serializing the instructions is unnecessary.
Armed with those new tools, I started tinkering with nodes and edges, but that's about where the first night was over and I decided to sleep on the issue instead.
Next time I set out to do things, I had already gotten the basic idea of what a node is - something with an active edge - and the rest of the edges kept in lists.
To store both positive and negative edges in a list, pos
and neg
were born; soon after nodePos
and nodeNeg
came to be.
..and the rest was just wiring everything up, making sure the instructions work, adding the inflateNode
system..
..testing everything a few times, fixing a few silly oversights (missing dup
operators, etc.)..
..refactoring some of the names..
..writing out a Nat sample and testing that (nope, it did not work on first try).. ..getting everything out of the single file I had it in so far.. ..ironing out the issues around imports..
And finally writing this file. Tadah! (:
Update: 2023-10-30/31: One day later, realized the massive savings that can be achieved by duplicating the rules table lazily, hence the addition of Lazy.i
. This simple change gets the runtime of the example bootstrap from nearly 13k steps to a mere 4k steps -- and takes a headless test testing squaring 10 * 10 from ~9 seconds to ~5 seconds. Executing rules which create more than one positive node is still O(rule table size), but at least those are a minority of the cases.
The main thing missing at the moment for a full bootstrap is a parser and a compiler for the rules. As parsing and compiling seemed like the "less fun" part (and since we don't have IO), those has been left as an exercise for the reader.
Minimizing the bootstrap should be possible. A curious project might be to see how minimal it could be made, so that a bootstrap-in-bootstrap can be attempted reasonably. Note that according to Y. Lafont, 1997 (via Wikipedia), Interaction Combinators should be able to simulate any system with just three kinds of nodes (or perhaps six-ish to account for signs?); but I would rather not minimize the bootstrap that far.
// SPDX-License-Identifier: GPL-3.0-only
Amazing!