cleanup
This commit is contained in:
parent
f66b87708d
commit
94df47c738
3 changed files with 80 additions and 669 deletions
|
|
@ -20,7 +20,7 @@ let-signal {
|
||||||
// SIGNAL WORLD (RHS of :=)
|
// SIGNAL WORLD (RHS of :=)
|
||||||
// We extract values from the graph.
|
// We extract values from the graph.
|
||||||
x := signal-expr-1,
|
x := signal-expr-1,
|
||||||
y := signal-expr-2
|
y := signal-expr-2,
|
||||||
.
|
.
|
||||||
// NORMAL WORLD (Body)
|
// NORMAL WORLD (Body)
|
||||||
// We compute a new value using the extracted data.
|
// We compute a new value using the extracted data.
|
||||||
|
|
@ -40,9 +40,14 @@ We also have a constant signal expression `const e` where `e` is a normal expres
|
||||||
|
|
||||||
Note that a normal expression can't contain `@` nor `let-expr`.
|
Note that a normal expression can't contain `@` nor `let-expr`.
|
||||||
|
|
||||||
|
Broadly we divide signals into sources and closure-signals.
|
||||||
|
- Sources are either constants or externally directed signals. They don't have any dependencies on other signals.
|
||||||
|
- Closure-Signals are signals that depend on other signals (either other closure-signals or sources).
|
||||||
|
For example let's assume `count` is a signal producing some integers, then `let-signal { x := @count . *($x, 2) }` is the `double` signal.
|
||||||
|
|
||||||
# Top-Level Constructs
|
# Top-Level Constructs
|
||||||
|
|
||||||
We introduce two new top-level constructs, via keywords `signal` and `fn-signal`.
|
We introduce three new top-level constructs, via keywords `signal`, `cell`, and `fn-signal`.
|
||||||
|
|
||||||
The following
|
The following
|
||||||
```
|
```
|
||||||
|
|
@ -61,6 +66,34 @@ signal sigName {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
We also have a way to introduce new source signal called a cell
|
||||||
|
```
|
||||||
|
cell (counter-dispatch, counter) 0 { count, msg . match $state {
|
||||||
|
| #inc . +($count, 1)
|
||||||
|
| #dec . -($count, 1)
|
||||||
|
| #reset . 0
|
||||||
|
| #set-to x . $x
|
||||||
|
}}
|
||||||
|
```
|
||||||
|
The above defined a signal `@counter` initialized with value `0`, and defines a `counter-dispatch` "effect".
|
||||||
|
Via the `counter-dispatch` we can send messages (values) to the counter signal, which is gonna be updated via
|
||||||
|
the update-function (that's given `count` and `msg`).
|
||||||
|
|
||||||
|
Cells are signals together with a way to set them. You can think of them as "actors" holding state and being updated based on a message (which is just a regular value).
|
||||||
|
|
||||||
|
TODO: What is an effect exactly? How can we perform it? What are their types?
|
||||||
|
TODO: Can you consider cells that depend on other signals somehow? Or maybe that doesn't really make sense? Hmm. The only way to change it should be via the dispatch.
|
||||||
|
|
||||||
|
General syntax
|
||||||
|
```
|
||||||
|
cell (dispatch-name, signal-name) normal-expr-init-state { state, msg . normal-body-expr }
|
||||||
|
```
|
||||||
|
Also consider wordier syntax:
|
||||||
|
```
|
||||||
|
cell (dispatch-name, signal-name) starts: normal-expr-init-state receives: { state, msg . normal-body-expr }
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
We also have parametrised top-level signal expressions. These can be used to define UI components
|
We also have parametrised top-level signal expressions. These can be used to define UI components
|
||||||
```
|
```
|
||||||
fn-signal Counter(color) {
|
fn-signal Counter(color) {
|
||||||
|
|
@ -102,434 +135,58 @@ signal App {
|
||||||
TODO: Now that I'm thinking about it, I think we should separate Views from Components.
|
TODO: Now that I'm thinking about it, I think we should separate Views from Components.
|
||||||
Basically a `View` is a pure function that happens to return UI. While a Component is a View together with all the signals feds into it.
|
Basically a `View` is a pure function that happens to return UI. While a Component is a View together with all the signals feds into it.
|
||||||
|
|
||||||
|
# Cells
|
||||||
|
It is really unclear to me, what the `dispatch` is. Do I need to introduce a new syntactic category for something like "commands"?
|
||||||
|
|
||||||
|
Also the basic static html elements can be clearly represented.
|
||||||
|
For components that change the view based on some state: I can use a signal that produces html.
|
||||||
|
But something like a `button` seems mysterious to me. To make a button, I need to specify the `on-click` thing.
|
||||||
|
But what the hell is that? I want to keep things as pure as possible and delay side-effects as far as possible. But this seems fundamentaly impure. Is this the correct point at which to do the side-effects?
|
||||||
|
|
||||||
|
|
||||||
# Runtime Constraints & Implementation
|
# Runtime Constraints & Implementation
|
||||||
|
|
||||||
- Static Topology? We should be able to know the dependency graph between signals at compile-time.
|
At the start we load all the top-level functions, signal definitions etc.
|
||||||
- Rank-based Update, topological-srot on graph once at startup. Each node gets a rank (how close it is to the root signal).
|
Then we take all the signal expressions and evaluate them.
|
||||||
Updates ar eprocessed via priority-queue - low-rank dependencies are processed earlier
|
This creates a DAG (Directed Acyclic Graph) of nodes that have inputs and outputs. We track rank-info so
|
||||||
|
that we can efficiently traverse parts of the graph in a topological-sort order. This allows us to eliminate glitches.
|
||||||
|
|
||||||
|
Then the fun part: We allow - while the system is running - hot-swapping of signals.
|
||||||
|
We may have a signal that's defined by e.g. a `let-signal` expression. We may change the signal-expr and attempt a hot-reload. This creates a checkpoint, in which the new expression is evaluated - this can spawn new nameless signals - and then we check if we can safely disconnect current signal from old parents (and do cleanup) and if we are cycle-free - if there's failure, we return to the checkpoint while cleaning up the new stuff. If we suceed, we disconnect the signal from old parents, connect new parents, and we propagate the new value.
|
||||||
|
|
||||||
|
Since signals can carry any values - including closures - by default while propagating we're not doing a check about whether anything changed, because e.g. closures don't support structural equality. But in our language we allow to specify for specific signals whether when recomputed, should they propagate to children or not... these are called relational-barriers (in a relational barrier we get access to the previous value and the new value and we compare them and then decide whether to change and propagate to children).
|
||||||
|
|
||||||
|
Signals can also be subscribed-to by external observers - that's how side-effects happen. When a new value is being propagated, it first propagates to all the descendants without notifying the external observers. Only once the graph has succesfuly propagated the new value, the external observers are notified.
|
||||||
|
|
||||||
|
Also, in some sense the DAG is "static" - it can't change its topology when signals do their own thing. The only way
|
||||||
|
to change it is if user does the hot-swap or spawns new signals - and that's not programatic - user has to do this manually. So in some sense our DAG is compile-time known - even though we don't relaly have the concept of compile-time.
|
||||||
|
|
||||||
|
# External Observers
|
||||||
|
|
||||||
|
The UI renderer is the primary external-observer.
|
||||||
|
It can subscribe to a signal that returns html, and actually do some stuff,
|
||||||
|
like initially create and mount the DOM elements. But when the signal changes, it needs to mutate what it has created.
|
||||||
|
|
||||||
|
How to do this well? The only changes are from signals... in the end, we probably have some signal-closure
|
||||||
|
that evaluates to an html value while observing other signals. When one of the dependencies changes,
|
||||||
|
how is the re-evaluation done?
|
||||||
|
|
||||||
|
There's some sort of incremental-computation thing going on here.
|
||||||
|
We have an initial value being computed, and then stuff changes. But only one tiny part may change in the output as a result of the change in input. How to do this efficiently and not recompute everything from scratch?
|
||||||
|
|
||||||
|
We can ofcourse do diffing... But that sucks.
|
||||||
|
|
||||||
|
|
||||||
|
# Side-Effects
|
||||||
# TODO: Linear Types
|
How to do some side-effects? Imagine that you have a counter cell, and when that reaches 5, some side-effect to the
|
||||||
|
world should happen.
|
||||||
What exactly is the meaning of the below?
|
|
||||||
Seems a bit weird that the signal `state` can be accessed multiple times like this.
|
|
||||||
```
|
|
||||||
let-signal {
|
|
||||||
x0 := @state,
|
|
||||||
x1 := @state,
|
|
||||||
. ...
|
|
||||||
}
|
|
||||||
It would make sense to me if we sampled once, then duplicated the value we got...
|
|
||||||
let-signal {
|
|
||||||
x := @state,
|
|
||||||
x0 := $x,
|
|
||||||
x1 := $x,
|
|
||||||
. ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
But then again, I guess it does make sense to sample twice - but there's no guarantee of getting the same result... e.g.
|
|
||||||
```
|
|
||||||
@ : Signal(A) -> Signal(A), A
|
|
||||||
```
|
|
||||||
the signal read operation may potentially change the signal itself.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
This is Solidjs. How does it work?
|
|
||||||
|
|
||||||
```
|
```
|
||||||
function Counter() {
|
let-effect {
|
||||||
const [ count, setCount ] = createSignal(0);
|
x := sig0,
|
||||||
return (
|
y := sig1,
|
||||||
<button onClick={ () => setCount(n => n + 1) }>
|
. side-effectful-expression?
|
||||||
Count: { count() }
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let-signal foo = x0;
|
|
||||||
@foo // read
|
|
||||||
@foo := x1 // write
|
|
||||||
|
|
||||||
@foo { x . body }
|
|
||||||
|
|
||||||
|
|
||||||
function Counter() {
|
|
||||||
let-signal count = 0;
|
|
||||||
|
|
||||||
<button onClick={ e => @count { c . c + 1 } } >
|
|
||||||
Count: { @count } // how can only this part be re-rendered? How is this communicated?
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let count = 0
|
|
||||||
let double = count * 2
|
|
||||||
|
|
||||||
count = 11
|
|
||||||
console.log(double) // 0
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ===Count===
|
|
||||||
let-signal count = 0;
|
|
||||||
|
|
||||||
//asignment
|
|
||||||
@count := 1
|
|
||||||
@count := 2
|
|
||||||
|
|
||||||
// subscription
|
|
||||||
@count { x .
|
|
||||||
console.log(x);
|
|
||||||
}
|
|
||||||
|
|
||||||
// POSSIBILITY 1: manual...
|
|
||||||
let-signal double = @count
|
|
||||||
@count { x .
|
|
||||||
@double := 2*x
|
|
||||||
}
|
|
||||||
|
|
||||||
// POSSIBILITY 2: automatic
|
|
||||||
let-signal??? double = 2*@count
|
|
||||||
// but where' sthe dependency on count? Is it just auto?
|
|
||||||
// I guess you can just analyze the code and see that this deopends on the count...
|
|
||||||
// but what if I wanted to just initialize `double` with the current value of the @count signal...
|
|
||||||
//
|
|
||||||
// maybe that's not how signals should be used...
|
|
||||||
// if a signal is used in a thing - that thing must change according to the signal - no other way?
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
What can we do?
|
|
||||||
- Creation: We can introduce a new signal
|
|
||||||
- Reading/Subsribing: We can read the current value of a signal
|
|
||||||
- should we then automatically change with that signal?
|
|
||||||
Are we dependent on it?
|
|
||||||
This is like we can read only by subscription.
|
|
||||||
- Updating: If we have the capability, we can update the signal to new value (can be a function of the current value)
|
|
||||||
- ???Subscription: We can subscribe to changes in the signal
|
|
||||||
- Map: We can create new signals that are a function of the original signal:
|
|
||||||
But this is a bit weird... in this output signal we can still set it...
|
|
||||||
Maybe we should be able to create only read-only signals?
|
|
||||||
|
|
||||||
|
|
||||||
Read/Write Signal
|
|
||||||
ReadOnly Signal (from map?)
|
|
||||||
|
|
||||||
What about stuff like `andThen`?
|
|
||||||
|
|
||||||
```
|
|
||||||
Signal(A)
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
type Model
|
|
||||||
create initState: Model
|
|
||||||
type Msg
|
|
||||||
create update : Msg, Model -> Model
|
|
||||||
|
|
||||||
const { dispatchMsg: (Msg) -> Void } = useElm(initState, update)
|
|
||||||
```
|
|
||||||
|
|
||||||
how to call the combination of a signal together with the events?
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
=== Tracking Scopes ===
|
|
||||||
|
|
||||||
When are tracking-scopes created?
|
|
||||||
```
|
|
||||||
// effects
|
|
||||||
createEffect(() => {
|
|
||||||
...
|
|
||||||
})
|
|
||||||
|
|
||||||
// returning jsx from a component
|
|
||||||
// would be better if we had to actually wrap it explicitely into something.
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
...
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
Nesting tracking-scopes seem complex.
|
|
||||||
We need some sort of a stack for tracking effects.
|
|
||||||
And when a child effect is pushed, it needs to say to its parent (current top-of-stack),
|
|
||||||
that, "hey, I'm yo kid, please clean me up when u refresh"
|
|
||||||
|
|
||||||
|
|
||||||
TODO:
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
createEffect(() => {
|
|
||||||
if (showDetails()) {
|
|
||||||
console.log(userID());
|
|
||||||
} else {
|
|
||||||
console.log("Hidden");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
Async is gonna be a pain in the ass too.
|
|
||||||
Basically anything to do with continuations.
|
|
||||||
|
|
||||||
Then I need to worry about `Scheduler`.
|
|
||||||
|
|
||||||
=== Store ===
|
|
||||||
|
|
||||||
How do you call a combination of a signal together with event-source that governs it together with a capability to dispatch it. For example imagine fictional:
|
|
||||||
```
|
|
||||||
type Model = ... // probably-big-record
|
|
||||||
type Msg = ... // probably-big-sum-type
|
|
||||||
let initModel: Model = ...
|
|
||||||
fn update : Model, Msg -> Model
|
|
||||||
|
|
||||||
// now we can make the thing ???
|
|
||||||
let X = Store(initModel, update)
|
|
||||||
```
|
|
||||||
Now from `X` we should be able to get `Signal(Model)`, but we should also get a capability to trigger events - i.e. some sort of dispatch mechanism.
|
|
||||||
So I'm wondering, how this `X` should be called (its type depends both on `Model` and `Msg`). Or should that just be a pair? Like `(Signal(Model), EventSourceDIspatchCapability(Msg))`?
|
|
||||||
|
|
||||||
Apparently this is called a `Store(Model, Msg)`
|
|
||||||
|
|
||||||
Other people call it `Subject` - something that can both be read and written to
|
|
||||||
Other people call it `Atom/Agent`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
signals, managing state
|
|
||||||
storing/updating/reading values reactively
|
|
||||||
|
|
||||||
- current-user
|
|
||||||
- current-page
|
|
||||||
- current-theme
|
|
||||||
|
|
||||||
|
|
||||||
createSignal(initState)
|
|
||||||
|
|
||||||
|
|
||||||
tracking-signal
|
|
||||||
|
|
||||||
signals are reactive
|
|
||||||
they auto-update when their value changes
|
|
||||||
- this is stupid. Ofcourse...
|
|
||||||
|
|
||||||
when a signal is called within a tracking scope,
|
|
||||||
signal adds a dependency
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
=== Reactive Runtime ===
|
|
||||||
|
|
||||||
- Signal Dependency Graph (or DAG)
|
|
||||||
- Effect Ownership Tree
|
|
||||||
- Effect Stack
|
|
||||||
|
|
||||||
for async
|
|
||||||
- Scheduler
|
|
||||||
- Context Restoration (when a thing in Scheduler is ready and needs to be resumed)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
=== Syntax ===
|
|
||||||
Wait a sec... @count returns a Signal object?
|
|
||||||
I'm not sure that I like that...
|
|
||||||
|
|
||||||
+(@count, 1) would be error?
|
|
||||||
|
|
||||||
hmm, to read from a signal, we need to be in a tracking block...
|
|
||||||
|
|
||||||
```
|
|
||||||
track {
|
|
||||||
@count
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
let {
|
|
||||||
double = track { *(@count, 2) } // this returns a signal... interesting!
|
|
||||||
.
|
|
||||||
track {
|
|
||||||
+(@double) // need different syntax I think... `double` is a regular variable...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
What about
|
|
||||||
```
|
|
||||||
let-signal {
|
|
||||||
double = *(@count, 2),
|
|
||||||
.
|
|
||||||
+(@double, 5)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
the w
|
|
||||||
|
|
||||||
|
|
||||||
`track` expression returns a signal, interesting.
|
|
||||||
|
|
||||||
Actually the let-signal could be like a sequential pairing mechanism.
|
|
||||||
|
|
||||||
Signal(A) Signal(B), (A, B -> C) -> Signal(C)
|
|
||||||
|
|
||||||
let-signal {
|
|
||||||
a := signal-expression,
|
|
||||||
b := signal-expression
|
|
||||||
.
|
|
||||||
pure-expression that has a, b as local variables...
|
|
||||||
}
|
|
||||||
|
|
||||||
Mapping:
|
|
||||||
we could have a sing
|
|
||||||
this is like a map
|
|
||||||
```
|
|
||||||
let-signal {
|
|
||||||
x := @count
|
|
||||||
.
|
|
||||||
*($x, 2)
|
|
||||||
} // the following is like the double signal
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
let {
|
|
||||||
double = let-signal { x := @count . *($x, 2) }
|
|
||||||
.
|
|
||||||
let-signal {
|
|
||||||
y := $double // fucking shit. `double` here is a local var...
|
|
||||||
. +($y, 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
hmm, maybe I could somehow syntactically distinguish Signal expressions from normal expressions?
|
|
||||||
|
|
||||||
|
|
||||||
We can also do the same sort of thing with effects, I guess...
|
|
||||||
```
|
|
||||||
effect {
|
|
||||||
x := @count
|
|
||||||
.
|
|
||||||
log("The count is now: ", x)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
The following is basically what I would have to put into the tiddler's code...
|
|
||||||
```
|
|
||||||
let-signal {
|
|
||||||
x := $count
|
|
||||||
.
|
|
||||||
+($x, 1)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
not exactly simple, damn.
|
|
||||||
|
|
||||||
The only way to sample a signal is by using this let-signal thing...
|
|
||||||
|
|
||||||
$count { x . +($x, 1) }
|
|
||||||
|
|
||||||
|
|
||||||
// This one is simple... but not exactly with clear semantics
|
|
||||||
+($count, 1)
|
|
||||||
|
|
||||||
Maybe we could somehow auto-transform these into the let-signal things?
|
|
||||||
Yeah, but evaluation rules need to be strict and that's what let-signal thing allows...
|
|
||||||
|
|
||||||
mhm, it also would be nice if we had a syntax for constant signals..., right?
|
|
||||||
|
|
||||||
```
|
|
||||||
let-signal {
|
|
||||||
divisor = 2, // note that this is a pure expression...
|
|
||||||
x := @mouse-x,
|
|
||||||
y := let-signal {
|
|
||||||
a := @mouse-y,
|
|
||||||
b := @offset
|
|
||||||
. +($a, $b)
|
|
||||||
}
|
|
||||||
.
|
|
||||||
/(+(*), $divisor)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
# Parametrise Signals
|
|
||||||
|
|
||||||
```
|
|
||||||
// like a top-level function of type (A, B, C) -> Signal(D)
|
|
||||||
fn-signal Foo(x1, x2, x3) {
|
|
||||||
// signal-expression
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
# Implementation
|
|
||||||
|
|
||||||
## Signal Env/Frame/Binding
|
|
||||||
|
|
||||||
```
|
|
||||||
type SignalFrame = {
|
|
||||||
pattern: ProductPattern,
|
|
||||||
expr: Signal<Value>,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This is almost like a signal-env. Seems useful.
|
|
||||||
|
|
||||||
## Let-Signal binding
|
|
||||||
```
|
|
||||||
let-signal {
|
|
||||||
x := sig-expr-0,
|
|
||||||
y := sig-expr-1
|
|
||||||
. f(x, y)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
What happens during the evaluation of the above signal-expression?
|
|
||||||
|
|
||||||
1. evaluate `(sig-expr-0, sig-expr-1)` to `(sig0, sig1)` and construct a signal-env
|
|
||||||
`[ x := sig0, y := sig1 ]`
|
|
||||||
2. evaluate `initVal := f(x, y) in env [ x := sig0.read(), y := sig1.read() ]`
|
|
||||||
3. construct new signal `Z := signal(initVal)`
|
|
||||||
4. make `Z` depend on `sig0` and `sig1`.
|
|
||||||
When one of them changes, push new value on `Z` that's the result of evaluation of
|
|
||||||
`f(x, y) in env [ x := sig0.read(), y := sig1.read() ]`
|
|
||||||
|
|
||||||
Note how `Z` is a signal together with a special closure:
|
|
||||||
- body of the closure is `f(x, y)`
|
|
||||||
- the captured signal-env of the closure is `[ x := sig0, y := sig1 ]`
|
|
||||||
- `Z` depends on `(sig0, sig1)`
|
|
||||||
|
|
||||||
|
|
||||||
TODO: Maybe it would be better to have something like signal-values?
|
|
||||||
These can either be plain constants,
|
|
||||||
or something more complex that has dependencies...
|
|
||||||
Right now everything is forced to be `Signal<Value>`.
|
|
||||||
|
|
||||||
```
|
|
||||||
type Signal =
|
|
||||||
| Constant(Value)
|
|
||||||
| Closure(... ? ...)
|
|
||||||
| NamedSignal(SignalName) // ???
|
|
||||||
```
|
|
||||||
|
|
||||||
But... if we allow recompilation at runtime of signals, a constant signal may become something more complex with dependencies.
|
|
||||||
That's why you always have to track dependencies - even when the original value ain't changing (atleast with the current compiled code)
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,246 +0,0 @@
|
||||||
import { Product } from "electron";
|
|
||||||
import { Expr, ProductPattern, SignalExpr, SignalName } from "../expr";
|
|
||||||
import { Program } from "../program";
|
|
||||||
import { Result, RuntimeError, ThrownRuntimeError } from "./error";
|
|
||||||
import { eval_expr, eval_start } from "./evaluator";
|
|
||||||
import { match_product_pattern } from "./pattern_match";
|
|
||||||
import { Env, Value } from "./value";
|
|
||||||
|
|
||||||
|
|
||||||
export type SignalId = number;
|
|
||||||
|
|
||||||
export type SignalRuntime = {
|
|
||||||
next_signal_id: SignalId,
|
|
||||||
store: Map<SignalId, Signal<Value>>,
|
|
||||||
};
|
|
||||||
|
|
||||||
export namespace SignalRuntime {
|
|
||||||
export function make(): SignalRuntime {
|
|
||||||
return {
|
|
||||||
next_signal_id: 0,
|
|
||||||
store: new Map(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateSignalId(runtime: SignalRuntime): SignalId {
|
|
||||||
const id = runtime.next_signal_id;
|
|
||||||
runtime.next_signal_id += 1;
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSignal(runtime: SignalRuntime, signalId: SignalId): Signal<Value> | undefined {
|
|
||||||
return runtime.store.get(signalId);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// 1. initialization phase where we compile top-level signal expressions into a graph
|
|
||||||
// 2. runtime, subscriptions, derived signals etc.
|
|
||||||
|
|
||||||
// may throw `ThrownRuntimeError`
|
|
||||||
export function eval_signal_expression(program: Program, env: Env, e: SignalExpr): Signal<Value> {
|
|
||||||
switch (e.tag) {
|
|
||||||
case "read":
|
|
||||||
return Program.get_or_create_signal(program, e.name);
|
|
||||||
case "const":
|
|
||||||
const val = eval_expr(program, env, e.arg);
|
|
||||||
return signal(val);
|
|
||||||
case "let":
|
|
||||||
// TODO: To change this, first look at how `tupleThen` works. It's a simpler more isolated case. Easier to get right.
|
|
||||||
// === Get/Create dependencies ===
|
|
||||||
let signalBindings: SignalBinding[] = [];
|
|
||||||
for (const { pattern, expr } of e.bindings) {
|
|
||||||
// This will either get a reference to an existing signal or create a bunch of new signals
|
|
||||||
// Note that we're using `env` here and not `cur_env`. That's intentional.
|
|
||||||
const signal = eval_signal_expression(program, env, expr);
|
|
||||||
signalBindings.push({ pattern, signal });
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Initialization ===
|
|
||||||
const initValue = eval_expr_in_signal_bindings(program, env, signalBindings, e.body);
|
|
||||||
const letSignal = signal(initValue);
|
|
||||||
|
|
||||||
// === Reactive Update ===
|
|
||||||
// Setup a subscription to each dependency, when it changes, poll every signal and re-evaluate
|
|
||||||
// TODO: This is extremely inefficient.
|
|
||||||
for (const { signal } of signalBindings) {
|
|
||||||
const cancelSignal = signal.subscribe(() => {
|
|
||||||
letSignal.set(() => {
|
|
||||||
const value = eval_expr_in_signal_bindings(program, env, signalBindings, e.body);
|
|
||||||
return value;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
letSignal.dependencies.push(cancelSignal);
|
|
||||||
}
|
|
||||||
|
|
||||||
return letSignal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type SignalBinding = {
|
|
||||||
pattern: ProductPattern,
|
|
||||||
signal: Signal<Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// may throw `ThrownRuntimeError`
|
|
||||||
function eval_expr_in_signal_bindings(program: Program, env: Env, signalBindings: SignalBinding[], expr: Expr): Value {
|
|
||||||
let cur_env = env;
|
|
||||||
for (const { pattern, signal } of signalBindings) {
|
|
||||||
const value = signal.read();
|
|
||||||
const res = match_product_pattern(pattern, value);
|
|
||||||
if (res.tag === "failure") {
|
|
||||||
// TODO: idk what to do here...
|
|
||||||
// TODO: Do you actually need to cleanup the old signals? What should be done here?
|
|
||||||
// TODO: We shouldn't throw here... the individual let-signal-branches should ve independent of each other.
|
|
||||||
// not sure...
|
|
||||||
throw ThrownRuntimeError.error({
|
|
||||||
tag: "UnableToFindMatchingPattern",
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
cur_env = Env.push_frame(cur_env, res.frame);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const value = eval_expr(program, cur_env, expr);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// may throw `ThrownRuntimeError`
|
|
||||||
export function signal_set_value(program: Program, sig: Signal<Value>, e: Expr): Value {
|
|
||||||
const value = eval_expr(program, Env.nil(), e);
|
|
||||||
sig.set(() => value);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Signal<T> {
|
|
||||||
state: T,
|
|
||||||
observers: Observer<T>[],
|
|
||||||
set(transform: (state: T) => T): void,
|
|
||||||
read(): T,
|
|
||||||
subscribe(observer: Observer<T>): UnsubscribeCapability,
|
|
||||||
map<S>(transform: (state: T) => S): Signal<S>,
|
|
||||||
dependencies: UnsubscribeCapability[],
|
|
||||||
dropDependencies(): void,
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Observer<T> = (state: T) => void;
|
|
||||||
export type UnsubscribeCapability = () => void;
|
|
||||||
|
|
||||||
export function signal<T>(initState: T): Signal<T> {
|
|
||||||
return {
|
|
||||||
state: initState,
|
|
||||||
observers: [],
|
|
||||||
dependencies: [],
|
|
||||||
set(transform: (state: T) => T) {
|
|
||||||
const state = transform(this.state);
|
|
||||||
this.state = state;
|
|
||||||
this.observers.forEach((observer: Observer<T>) => {
|
|
||||||
observer(state);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
read(): T {
|
|
||||||
return this.state;
|
|
||||||
},
|
|
||||||
subscribe(observer: Observer<T>) {
|
|
||||||
this.observers.push(observer);
|
|
||||||
const that = this;
|
|
||||||
return () => {
|
|
||||||
that.observers = that.observers.filter(sub => sub !== observer);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
dropDependencies() {
|
|
||||||
for (const cancel of this.dependencies) {
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
this.dependencies = [];
|
|
||||||
},
|
|
||||||
map<S>(transform: (state: T) => S): Signal<S> {
|
|
||||||
const Y = signal(transform(this.state));
|
|
||||||
const cancelY = this.subscribe((state: T) => {
|
|
||||||
Y.set(() => transform(state));
|
|
||||||
});
|
|
||||||
Y.dependencies.push(cancelY);
|
|
||||||
return Y;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function pair<A, B>(X: Signal<A>, Y: Signal<B>): Signal<[A, B]> {
|
|
||||||
const Z = signal([X.read(), Y.read()] as [A, B]);
|
|
||||||
const cancelX = X.subscribe(x => {
|
|
||||||
Z.set(() => [x, Y.read()]);
|
|
||||||
});
|
|
||||||
Z.dependencies.push(cancelX);
|
|
||||||
const cancelY = Y.subscribe(y => {
|
|
||||||
Z.set(() => [X.read(), y]);
|
|
||||||
});
|
|
||||||
Z.dependencies.push(cancelY);
|
|
||||||
return Z;
|
|
||||||
}
|
|
||||||
|
|
||||||
function tupleThen<A, B>(Xs: Signal<A>[], f: (values: A[]) => B): Signal<B> {
|
|
||||||
const Z = signal(f(Xs.map(X => X.read())));
|
|
||||||
|
|
||||||
// TODO: This is just preliminary. Has the glitch bug. Also has insane quadratic behaviour.
|
|
||||||
Xs.forEach((X, i) => {
|
|
||||||
const cancelX = X.subscribe(_ => {
|
|
||||||
Z.set(() => f(Xs.map(X => X.read())));
|
|
||||||
});
|
|
||||||
Z.dependencies.push(cancelX);
|
|
||||||
});
|
|
||||||
|
|
||||||
return Z;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// === example ===
|
|
||||||
// const count = signal(0);
|
|
||||||
// console.log("COUNT EXISTS");
|
|
||||||
|
|
||||||
// console.log(count.read())
|
|
||||||
|
|
||||||
// const _ = count.subscribe(x => {
|
|
||||||
// console.log("count is now", x);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// count.set(() => 1)
|
|
||||||
// count.set(() => 2)
|
|
||||||
|
|
||||||
// const double = count.map(x => 2*x);
|
|
||||||
// console.log("DOUBLE EXISTS");
|
|
||||||
|
|
||||||
|
|
||||||
// const _ = double.subscribe(x => {
|
|
||||||
// console.log("double is now", x);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// count.set(() => 3);
|
|
||||||
// count.set(() => 9);
|
|
||||||
|
|
||||||
// const WTF = pair(count, double)
|
|
||||||
// console.log("-> WTF EXISTS");
|
|
||||||
|
|
||||||
// const _ = WTF.subscribe(([x, y]) => {
|
|
||||||
// console.log("WTF is now ", [x, y]);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// count.set(() => 13);
|
|
||||||
|
|
||||||
|
|
||||||
// === Primitive Signals ===
|
|
||||||
export function makeTickSignal(intervalMs: number): Signal<Value> {
|
|
||||||
const s = signal(Value.number(0));
|
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
const state = s.read();
|
|
||||||
if (state.tag === "number"){
|
|
||||||
s.set(() => Value.number(state.value + 1));
|
|
||||||
} else {
|
|
||||||
console.log("Something is really wrong with the state of tick-signal", state);
|
|
||||||
}
|
|
||||||
}, intervalMs);
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -29,7 +29,7 @@ export namespace SignalRuntime {
|
||||||
internalInputs: DAGNode[],
|
internalInputs: DAGNode[],
|
||||||
|
|
||||||
// ===outputs===
|
// ===outputs===
|
||||||
internalOutputs: DAGNode[],
|
internalOutputs: DAGNode[], // TODO: What if we actually ordered this by rank? Wouldn't it be much faster? We wouldn't have to rebuild the rank-heap all the time when propagating.
|
||||||
externalOutputs: ExternalObserver[],
|
externalOutputs: ExternalObserver[],
|
||||||
|
|
||||||
currentValue: Value,
|
currentValue: Value,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue