Compare commits
10 commits
8e4dcb5de7
...
94df47c738
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94df47c738 | ||
|
|
f66b87708d | ||
|
|
49ef33f113 | ||
|
|
115b457173 | ||
|
|
6cca0d17a1 | ||
|
|
b9332ad565 | ||
|
|
c0198d419f | ||
|
|
bf5eb54932 | ||
|
|
e841106029 | ||
|
|
b0280b9d74 |
37 changed files with 1980 additions and 1231 deletions
|
|
@ -20,7 +20,7 @@ let-signal {
|
|||
// SIGNAL WORLD (RHS of :=)
|
||||
// We extract values from the graph.
|
||||
x := signal-expr-1,
|
||||
y := signal-expr-2
|
||||
y := signal-expr-2,
|
||||
.
|
||||
// NORMAL WORLD (Body)
|
||||
// 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`.
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
```
|
||||
|
|
@ -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
|
||||
```
|
||||
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.
|
||||
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
|
||||
|
||||
- Static Topology? We should be able to know the dependency graph between signals at compile-time.
|
||||
- Rank-based Update, topological-srot on graph once at startup. Each node gets a rank (how close it is to the root signal).
|
||||
Updates ar eprocessed via priority-queue - low-rank dependencies are processed earlier
|
||||
At the start we load all the top-level functions, signal definitions etc.
|
||||
Then we take all the signal expressions and evaluate them.
|
||||
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.
|
||||
|
||||
|
||||
|
||||
# TODO: Linear Types
|
||||
|
||||
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?
|
||||
# Side-Effects
|
||||
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.
|
||||
|
||||
```
|
||||
function Counter() {
|
||||
const [ count, setCount ] = createSignal(0);
|
||||
return (
|
||||
<button onClick={ () => setCount(n => n + 1) }>
|
||||
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)
|
||||
}
|
||||
let-effect {
|
||||
x := sig0,
|
||||
y := sig1,
|
||||
. side-effectful-expression?
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ export type RuntimeError =
|
|||
| { tag: "TypeMismatch", expected: ValueTag, received: Value }
|
||||
| { tag: "DuplicateVariableNamesInPattern", pattern: Pattern, duplicates: VariableName[] }
|
||||
// | { tag: "DuplicateVariableNamesInProductPattern", pattern: ProductPattern, duplicates: VariableName[] }
|
||||
| { tag: "ClosureEqualityComparison", value0: Closure, value1: Value } // Closures cannot be compared for equality
|
||||
| { tag: "NotABoolean", value: Value } // Attempt to use a non-boolean when a boolean is expected
|
||||
|
||||
export type Result<T> =
|
||||
| { tag: "ok", value: T }
|
||||
|
|
|
|||
79
src/lang/eval/priority_queue.ts
Normal file
79
src/lang/eval/priority_queue.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { SignalRuntime } from "./signalValue";
|
||||
|
||||
type T = SignalRuntime.DAGNode;
|
||||
|
||||
export class PriorityQueue {
|
||||
private heap: T[] = [];
|
||||
private inQueue = new Set<T>;
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this.heap.length === 0;
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.heap.length;
|
||||
}
|
||||
|
||||
// Pushes a node into the queue if it's not already present.
|
||||
push(node: T): void {
|
||||
if (this.inQueue.has(node)) return;
|
||||
|
||||
this.inQueue.add(node);
|
||||
this.heap.push(node);
|
||||
this.bubbleUp(this.heap.length - 1);
|
||||
}
|
||||
|
||||
// Removes and returns the node with the lowest rank.
|
||||
pop(): T | undefined {
|
||||
if (this.heap.length === 0) return undefined;
|
||||
|
||||
const top = this.heap[0];
|
||||
const last = this.heap.pop()!;
|
||||
|
||||
if (this.heap.length > 0) {
|
||||
this.heap[0] = last;
|
||||
this.bubbleDown(0);
|
||||
}
|
||||
|
||||
this.inQueue.delete(top);
|
||||
return top;
|
||||
}
|
||||
|
||||
private swap(i: number, j: number): void {
|
||||
const temp = this.heap[i];
|
||||
this.heap[i] = this.heap[j];
|
||||
this.heap[j] = temp;
|
||||
}
|
||||
|
||||
private bubbleUp(index: number): void {
|
||||
while (index > 0) {
|
||||
const parentIndex = (index - 1) >> 1; // Faster Math.floor((i-1)/2)
|
||||
if (this.heap[index].rank >= this.heap[parentIndex].rank) break;
|
||||
|
||||
this.swap(index, parentIndex);
|
||||
index = parentIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private bubbleDown(index: number): void {
|
||||
const length = this.heap.length;
|
||||
while (true) {
|
||||
let smallest = index;
|
||||
const left = (index << 1) + 1; // 2 * i + 1
|
||||
const right = (index << 1) + 2; // 2 * i + 2
|
||||
|
||||
if (left < length && this.heap[left].rank < this.heap[smallest].rank) {
|
||||
smallest = left;
|
||||
}
|
||||
if (right < length && this.heap[right].rank < this.heap[smallest].rank) {
|
||||
smallest = right;
|
||||
}
|
||||
|
||||
if (smallest === index) break;
|
||||
|
||||
this.swap(index, smallest);
|
||||
index = smallest;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,229 +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.nil(), 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) {
|
||||
signal.subscribe(() => {
|
||||
letSignal.set(() => {
|
||||
const value = eval_expr_in_signal_bindings(program, env, signalBindings, e.body);
|
||||
return value;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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>): void,
|
||||
map<S>(transform: (state: T) => S): Signal<S>,
|
||||
};
|
||||
|
||||
export type Observer<T> = (state: T) => void;
|
||||
|
||||
export function signal<T>(initState: T): Signal<T> {
|
||||
return {
|
||||
state: initState,
|
||||
observers: [],
|
||||
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>) {
|
||||
// TODO: This needs to return `cancellation`
|
||||
this.observers.push(observer);
|
||||
},
|
||||
map<S>(transform: (state: T) => S): Signal<S> {
|
||||
const Y = signal(transform(this.state));
|
||||
this.subscribe((state: T) => {
|
||||
Y.set(() => transform(state));
|
||||
});
|
||||
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]);
|
||||
X.subscribe(x => {
|
||||
Z.set(() => [x, Y.read()]);
|
||||
});
|
||||
Y.subscribe(y => {
|
||||
Z.set(() => [X.read(), y]);
|
||||
});
|
||||
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) => {
|
||||
X.subscribe(_ => {
|
||||
Z.set(() => f(Xs.map(X => X.read())));
|
||||
});
|
||||
});
|
||||
|
||||
return Z;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// === example ===
|
||||
// const count = signal(0);
|
||||
// console.log("COUNT EXISTS");
|
||||
|
||||
// console.log(count.read())
|
||||
|
||||
// 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");
|
||||
|
||||
|
||||
// double.subscribe(x => {
|
||||
// console.log("double is now", x);
|
||||
// });
|
||||
|
||||
// count.set(() => 3);
|
||||
// count.set(() => 9);
|
||||
|
||||
// const WTF = pair(count, double)
|
||||
// console.log("-> WTF EXISTS");
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
515
src/lang/eval/signalValue.ts
Normal file
515
src/lang/eval/signalValue.ts
Normal file
|
|
@ -0,0 +1,515 @@
|
|||
import { Expr, ProductPattern, SignalExpr, SignalName, VariableName } from "../expr";
|
||||
import { Program } from "../program";
|
||||
import { Result, RuntimeError, ThrownRuntimeError } from "./error";
|
||||
import { eval_expr } from "./evaluator";
|
||||
import { match_product_pattern } from "./pattern_match";
|
||||
import { Env, equals, forceBool, Value } from "./value";
|
||||
import { PriorityQueue } from "./priority_queue"
|
||||
|
||||
// === Reactive DAG ===
|
||||
|
||||
export type SignalRuntime = {
|
||||
// Named signals (there are also nameless ones - and the distinction matters for garbage collection)
|
||||
store: Map<SignalName, SignalRuntime.DAGNode>,
|
||||
// TODO: consider tracking root nodes (and even the signal-closures with 0 bindings). Would be useful to build a global visualization of the DAG.
|
||||
};
|
||||
|
||||
export type ExternalObserver = (state: Value) => void;
|
||||
export type UnsubscribeCapability = () => void;
|
||||
|
||||
export namespace SignalRuntime {
|
||||
// TODO: This is a terrible name. Looking for a new name... Just don't call it `Node`, which clashes with the builtin ts `Node` type all the time.
|
||||
// TODO: consider naming this `SignalId` actually...
|
||||
export type DAGNode = {
|
||||
signalName?: SignalName,
|
||||
signal: SignalValue,
|
||||
rank: number,
|
||||
|
||||
// ===inputs===
|
||||
internalInputs: DAGNode[],
|
||||
|
||||
// ===outputs===
|
||||
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[],
|
||||
|
||||
currentValue: Value,
|
||||
}
|
||||
|
||||
export function make(): SignalRuntime {
|
||||
return {
|
||||
store: new Map(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function internalSubscribe(node: SignalRuntime.DAGNode, observer: SignalRuntime.DAGNode) {
|
||||
node.internalOutputs.push(observer);
|
||||
observer.internalInputs.push(node);
|
||||
}
|
||||
|
||||
export function externalSubscribe(node: SignalRuntime.DAGNode, observer: ExternalObserver): UnsubscribeCapability {
|
||||
node.externalOutputs.push(observer);
|
||||
return () => {
|
||||
node.externalOutputs = node.externalOutputs.filter(sub => sub !== observer);
|
||||
};
|
||||
}
|
||||
|
||||
export function getNode(program: Program, signalName: SignalName): SignalRuntime.DAGNode {
|
||||
const maybeNode = program.signal_runtime.store.get(signalName);
|
||||
if (maybeNode === undefined) {
|
||||
// TODO: Make this into a proper error
|
||||
throw Error(`Signal '${signalName}' not found!`);
|
||||
} else {
|
||||
return maybeNode;
|
||||
}
|
||||
}
|
||||
|
||||
export function spawnSignal(program: Program, signalName: SignalName, expr: SignalExpr): SignalRuntime.DAGNode {
|
||||
const maybeNode = program.signal_runtime.store.get(signalName);
|
||||
if (maybeNode !== undefined) {
|
||||
// TODO: Make this into a proper error
|
||||
throw Error(`Attempt to spawn a signal '${signalName}' that already exists`);
|
||||
}
|
||||
const node = eval_signal_expression(program, Env.nil(), expr);
|
||||
node.signalName = signalName;
|
||||
program.signal_runtime.store.set(signalName, node);
|
||||
return node;
|
||||
}
|
||||
|
||||
export function spawnSource(program: Program, signalName: SignalName, initValue: Value): [SignalRuntime.DAGNode, (value: Value) => void] {
|
||||
const maybeNode = program.signal_runtime.store.get(signalName);
|
||||
if (maybeNode !== undefined) {
|
||||
// TODO: Make this into a proper error
|
||||
throw Error(`Attempt to spawn a signal '${signalName}' that already exists`);
|
||||
}
|
||||
|
||||
const node: SignalRuntime.DAGNode = {
|
||||
signal: SignalValue.const_(initValue),
|
||||
signalName,
|
||||
rank: 0,
|
||||
internalInputs: [],
|
||||
internalOutputs: [],
|
||||
externalOutputs: [],
|
||||
currentValue: initValue,
|
||||
};
|
||||
program.signal_runtime.store.set(signalName, node);
|
||||
|
||||
|
||||
function setValue(value: Value) {
|
||||
node.currentValue = value;
|
||||
propagate(program, node);
|
||||
}
|
||||
|
||||
return [node, setValue];
|
||||
}
|
||||
|
||||
// TODO: Should take in a `SignalName`, and find `oldNode` first
|
||||
export function hotSwapSignal(program: Program, oldNode: SignalRuntime.DAGNode, expr: SignalExpr) {
|
||||
reeval_signal_expression(program, Env.nil(), oldNode, expr)
|
||||
}
|
||||
|
||||
function disconnectInputs(program: Program, node: SignalRuntime.DAGNode) {
|
||||
for (const parent of node.internalInputs) {
|
||||
parent.internalOutputs = parent.internalOutputs.filter(sub => sub !== node);
|
||||
if (parent.signalName === undefined && parent.internalOutputs.length === 0) {
|
||||
// Note that we're not checking for `parent.externalOutputs.length === 0`, since nameless modes can't be subscribed to externally.
|
||||
disconnectInputs(program, parent);
|
||||
}
|
||||
}
|
||||
node.internalInputs = [];
|
||||
}
|
||||
|
||||
// This is used during rollback when a cycle is detected.
|
||||
function disconnectTrialNodes(program: Program, nodes: SignalRuntime.DAGNode[]) {
|
||||
for (const node of nodes) {
|
||||
if (node.signalName === undefined && node.internalOutputs.length === 0) {
|
||||
// Note that we're not checking for `parent.externalOutputs.length === 0`, since nameless modes can't be subscribed to externally.
|
||||
disconnectInputs(program, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Does old-rank based priority-queue traversal to update the ranks of all the descendants of a node.
|
||||
// IMPORTANT: Assumes that the `node` already has the new rank - otherwise will corrupt the DAG.
|
||||
function refreshRank(node: SignalRuntime.DAGNode) {
|
||||
const heap = new PriorityQueue();
|
||||
|
||||
for (const child of node.internalOutputs) {
|
||||
heap.push(child);
|
||||
}
|
||||
|
||||
while (!heap.isEmpty()) {
|
||||
const child = heap.pop()!;
|
||||
|
||||
let maxParentRank = 0;
|
||||
for (const childParent of child.internalInputs) {
|
||||
maxParentRank = Math.max(maxParentRank, childParent.rank);
|
||||
}
|
||||
|
||||
const expectedRank = maxParentRank + 1;
|
||||
|
||||
if (child.rank < expectedRank) {
|
||||
child.rank = expectedRank;
|
||||
for (const grandchild of child.internalOutputs) {
|
||||
heap.push(grandchild);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function wouldCreateCycle(node: SignalRuntime.DAGNode, newParents: SignalRuntime.DAGNode[]): boolean {
|
||||
const descendants = new Set<SignalRuntime.DAGNode>();
|
||||
|
||||
// ===Compute Descendants===
|
||||
function addDescendants(node: SignalRuntime.DAGNode) {
|
||||
if (descendants.has(node)) {
|
||||
return
|
||||
}
|
||||
descendants.add(node);
|
||||
for (const child of node.internalOutputs) {
|
||||
addDescendants(child)
|
||||
}
|
||||
}
|
||||
|
||||
addDescendants(node);
|
||||
|
||||
// ===Check if new parents are reachable (thus creating a cycle)
|
||||
for (const parent of newParents) {
|
||||
if (descendants.has(parent)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// === Value ===
|
||||
|
||||
type SignalValue =
|
||||
| { tag: "closure", closure: SignalClosure }
|
||||
// "source" is either a constant signal, or an external-input signal. The point is that these have no internal-inputs, i.e. rank-0 nodes.
|
||||
| { tag: "source", value: Value } // Is this really necessary? Yes... its not a special case of SignalClosure - it has a value as a body, not an expression...
|
||||
| { tag: "proxy", parent: SignalRuntime.DAGNode }
|
||||
|
||||
type SignalClosure = {
|
||||
bindings: SignalBinding[],
|
||||
env: Env,
|
||||
body: Expr,
|
||||
barrier?: Barrier, // Affects how propagation is done
|
||||
}
|
||||
|
||||
type Barrier =
|
||||
| { tag: "eq" }
|
||||
// User-defined predicate (old, new) -> bool. What value is considered to be the canonical bool?
|
||||
| { tag: "relation", relation: Relation }
|
||||
|
||||
type Relation = {
|
||||
oldVar: VariableName,
|
||||
newVar: VariableName,
|
||||
body: Expr,
|
||||
}
|
||||
|
||||
type SignalBinding = {
|
||||
pattern: ProductPattern,
|
||||
// TODO: remove?
|
||||
// signal: SignalValue, // TODO: Maybe this needs to be a Node in a DAG... I bet it will then infect the `eval` to also return a Node
|
||||
node: SignalRuntime.DAGNode,
|
||||
}
|
||||
|
||||
// === Evaluation ===
|
||||
|
||||
// You may think that you don't need `env, because branches in a `let-signal` expression are independent.
|
||||
// But in the future we could allow "usual" let-expressions on top, so `env` is useful! e.g.
|
||||
// ```
|
||||
// let {
|
||||
// x = e0,
|
||||
// y = e1,
|
||||
// . let-signal { // note that this is a signal-expression, nor a regular expression!
|
||||
// u = s0,
|
||||
// v = s1,
|
||||
// . body-expr
|
||||
// }
|
||||
// }
|
||||
// ```
|
||||
export function eval_signal_expression(program: Program, env: Env, e: SignalExpr): SignalRuntime.DAGNode {
|
||||
switch (e.tag) {
|
||||
case "read": {
|
||||
// Note that we're not returning a proxy here - it would be pointless. And would create lot's of inefficient indirections.
|
||||
return getNode(program, e.name);
|
||||
}
|
||||
case "const":
|
||||
// TODO: This may throw an exception!
|
||||
const val = eval_expr(program, env, e.arg);
|
||||
const node: SignalRuntime.DAGNode = {
|
||||
signal: SignalValue.const_(val),
|
||||
rank: 0,
|
||||
internalInputs: [],
|
||||
internalOutputs: [],
|
||||
externalOutputs: [],
|
||||
currentValue: val,
|
||||
};
|
||||
return node;
|
||||
case "let":
|
||||
let bindings: SignalBinding[] = [];
|
||||
const closure: SignalClosure = { bindings, body: e.body, env };
|
||||
const letNode: SignalRuntime.DAGNode = {
|
||||
signal: SignalValue.closure(closure),
|
||||
rank: 0, // (1) Rank set later
|
||||
|
||||
internalInputs: [], // (2) internal inputs set later
|
||||
internalOutputs: [],
|
||||
externalOutputs: [],
|
||||
currentValue: null as any, // (3) Current value set later
|
||||
};
|
||||
let maxParentRank = 0;
|
||||
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.
|
||||
// TODO: This may throw an exception!
|
||||
const parentNode = eval_signal_expression(program, env, expr);
|
||||
|
||||
internalSubscribe(parentNode, letNode); // (2) internal inputs set
|
||||
|
||||
maxParentRank = Math.max(maxParentRank, parentNode.rank);
|
||||
bindings.push({ pattern, node: parentNode });
|
||||
}
|
||||
letNode.rank = 1 + maxParentRank; // (1) Rank set
|
||||
|
||||
// === Initialization ===
|
||||
// TODO: This may throw an exception!
|
||||
const initValue = eval_signal_closure(program, closure);
|
||||
letNode.currentValue = initValue; // (3) Current value set
|
||||
|
||||
return letNode;
|
||||
}
|
||||
}
|
||||
|
||||
export function reeval_signal_expression(program: Program, env: Env, node: SignalRuntime.DAGNode, e: SignalExpr) {
|
||||
switch (e.tag) {
|
||||
case "read": {
|
||||
const parentNode = getNode(program, e.name);
|
||||
const newNodeRank = 1 + parentNode.rank;
|
||||
|
||||
if (wouldCreateCycle(node, [parentNode])) {
|
||||
// TODO: You need to throw a proper error value here
|
||||
throw new Error("Cycle detected while hot-swapping signal");
|
||||
} else {
|
||||
// ===We commit to re-eval===
|
||||
disconnectInputs(program, node);
|
||||
node.signal = SignalValue.proxy(parentNode);
|
||||
node.rank = newNodeRank; // (1)
|
||||
internalSubscribe(parentNode, node);
|
||||
refreshRank(node); // `node` has to have the new rank for this to work - which is ensured by (1).
|
||||
|
||||
// === re-initialization ===
|
||||
node.currentValue = parentNode.currentValue;
|
||||
propagate(program, node);
|
||||
break;
|
||||
}
|
||||
}
|
||||
case "const":
|
||||
// TODO: This may throw an exception!
|
||||
const val = eval_expr(program, env, e.arg);
|
||||
const signal = SignalValue.const_(val);
|
||||
|
||||
disconnectInputs(program, node);
|
||||
node.signal = signal;
|
||||
node.rank = 0;
|
||||
node.currentValue = val;
|
||||
// We don't have to rerank, since this has no inputs and its rank is 0.
|
||||
|
||||
propagate(program, node);
|
||||
break;
|
||||
case "let":
|
||||
// ===Can we re-eval succesfully?===
|
||||
let bindings: SignalBinding[] = [];
|
||||
|
||||
let maxParentRank = 0;
|
||||
const parents: SignalRuntime.DAGNode[] = [];
|
||||
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.
|
||||
// TODO: This may throw an exception!
|
||||
const parentNode = eval_signal_expression(program, env, expr);
|
||||
parents.push(parentNode);
|
||||
|
||||
maxParentRank = Math.max(maxParentRank, parentNode.rank);
|
||||
bindings.push({ pattern, node: parentNode });
|
||||
}
|
||||
const newLetNodeRank = 1 + maxParentRank;
|
||||
|
||||
const letNode = node;
|
||||
// Obligations after finding out we can succesfully re-eval:
|
||||
// (0) to detach from old parents
|
||||
// (1) to set rank
|
||||
// (2) to set internal inputs
|
||||
// (3) to set current value
|
||||
|
||||
if (wouldCreateCycle(letNode, parents)) {
|
||||
// disconnect would be parents from the graph (if they are newly created)
|
||||
disconnectTrialNodes(program, parents);
|
||||
// TODO: You need to throw a proper error value here
|
||||
throw new Error("Cycle detected while hot-swapping signal");
|
||||
} else {
|
||||
// ===We commit to re-eval===
|
||||
disconnectInputs(program, letNode); // (0) detached from old parents
|
||||
const closure: SignalClosure = { bindings, body: e.body, env };
|
||||
letNode.signal = SignalValue.closure(closure);
|
||||
letNode.rank = newLetNodeRank; // (1) Rank set
|
||||
for (const parentNode of parents) {
|
||||
internalSubscribe(parentNode, letNode); // (2) internal inputs set
|
||||
}
|
||||
refreshRank(letNode); // `letNode` has to have the new rank for this to work - which is ensured by (1).
|
||||
|
||||
// === re-initialization ===
|
||||
// TODO: This may throw an exception!
|
||||
const newValue = eval_signal_closure(program, closure);
|
||||
letNode.currentValue = newValue; // (3) Current value set
|
||||
propagate(program, letNode);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function eval_signal_closure(program: Program, closure: SignalClosure): Value {
|
||||
let cur_env = closure.env;
|
||||
for (const { pattern, node } of closure.bindings) {
|
||||
const value = node.currentValue;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This may throw an exception!
|
||||
const value = eval_expr(program, cur_env, closure.body);
|
||||
return value;
|
||||
}
|
||||
|
||||
export namespace SignalValue {
|
||||
export const const_ = (value: Value): SignalValue => ({ tag: "source", value });
|
||||
export const closure = (closure: SignalClosure): SignalValue => ({ tag: "closure", closure });
|
||||
export const proxy = (parent: SignalRuntime.DAGNode): SignalValue => ({ tag: "proxy", parent });
|
||||
}
|
||||
|
||||
// Calling convention: This assumes that the root-node's current-value has already been set - but its external-outputs haven't yet been notified.
|
||||
function propagate(program: Program, rootNode: SignalRuntime.DAGNode) {
|
||||
// Note that `rootNode` is not included in the `heap`.
|
||||
const heap = new PriorityQueue();
|
||||
// But `rootNode` is queued into `externalEffectNodes`, since we're doing effects after internal propagation.
|
||||
// We push the descendants of `rootNode` in a priority-queue order.
|
||||
const descendantsWithExternalEffects: SignalRuntime.DAGNode[] = [rootNode];
|
||||
|
||||
// Add children
|
||||
for (const childNode of rootNode.internalOutputs) {
|
||||
heap.push(childNode);
|
||||
}
|
||||
|
||||
// === Internal Outputs ===
|
||||
while (!heap.isEmpty()) {
|
||||
const node = heap.pop()!;
|
||||
switch (node.signal.tag) {
|
||||
case "source":
|
||||
break;
|
||||
case "proxy": {
|
||||
const value = node.internalInputs[0].currentValue;
|
||||
node.currentValue = value;
|
||||
descendantsWithExternalEffects.push(node);
|
||||
for (const childNode of node.internalOutputs) {
|
||||
heap.push(childNode);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "closure": {
|
||||
const closure = node.signal.closure;
|
||||
// TODO: This may throw an exception!
|
||||
const value = eval_signal_closure(program, closure);
|
||||
if (closure.barrier !== undefined) {
|
||||
switch (closure.barrier.tag) {
|
||||
case "eq":
|
||||
// TODO: This may throw an exception!
|
||||
if (equals(value, node.currentValue)) {
|
||||
continue
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
case "relation":
|
||||
// TODO: This may throw an exception!
|
||||
const should_propagate = eval_relation(
|
||||
program,
|
||||
closure.env,
|
||||
node.currentValue,
|
||||
value,
|
||||
closure.barrier.relation
|
||||
);
|
||||
if (!should_propagate) {
|
||||
continue
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
node.currentValue = value;
|
||||
descendantsWithExternalEffects.push(node);
|
||||
for (const childNode of node.internalOutputs) {
|
||||
heap.push(childNode);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === External Outputs ===
|
||||
for (const node of descendantsWithExternalEffects) {
|
||||
for (const observer of node.externalOutputs) {
|
||||
observer(node.currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function eval_relation(
|
||||
program: Program,
|
||||
env: Env,
|
||||
old_value: Value,
|
||||
new_value: Value,
|
||||
relation: Relation
|
||||
): boolean {
|
||||
const frame = Env.nil_frame();
|
||||
Env.frame_insert_mut(frame, relation.oldVar, old_value);
|
||||
Env.frame_insert_mut(frame, relation.newVar, new_value);
|
||||
|
||||
const extendedEnv = Env.push_frame(env, frame);
|
||||
// TODO: This may throw an exception!
|
||||
const result = eval_expr(program, extendedEnv, relation.body);
|
||||
|
||||
// TODO: This may throw an exception!
|
||||
return forceBool(result);
|
||||
}
|
||||
|
||||
// ===External Input Sources===
|
||||
|
||||
export function spawnTick(program: Program, signalName: SignalName, intervalMs: number): SignalRuntime.DAGNode {
|
||||
const [node, setValue] = spawnSource(program, signalName, Value.number(0));
|
||||
|
||||
setInterval(() => {
|
||||
const state = node.currentValue;
|
||||
if (state.tag === "number"){
|
||||
setValue(Value.number(state.value + 1))
|
||||
} else {
|
||||
console.log(`Something is really wrong with the state of tick-signal '${signalName}'`, state);
|
||||
}
|
||||
}, intervalMs);
|
||||
return node;
|
||||
}
|
||||
|
||||
|
|
@ -75,3 +75,59 @@ export namespace Env {
|
|||
}
|
||||
}
|
||||
|
||||
export function equals(v1: Value, v2: Value): boolean {
|
||||
if (v1 === v2) return true; // Reference equality optimization
|
||||
if (v1.tag !== v2.tag) return false;
|
||||
switch (v1.tag) {
|
||||
case "number":
|
||||
return v1.value === (v2 as Extract<Value, { tag: "number" }>).value;
|
||||
case "string":
|
||||
return v1.value === (v2 as Extract<Value, { tag: "string" }>).value;
|
||||
case "tag":
|
||||
return v1.tag_name === (v2 as Extract<Value, { tag: "tag" }>).tag_name;
|
||||
case "tagged": {
|
||||
const other = v2 as Extract<Value, { tag: "tagged" }>;
|
||||
return v1.tag_name === other.tag_name && equals(v1.value, other.value);
|
||||
}
|
||||
case "tuple": {
|
||||
const other = v2 as Extract<Value, { tag: "tuple" }>;
|
||||
if (v1.values.length !== other.values.length) return false;
|
||||
for (let i = 0; i < v1.values.length; i++) {
|
||||
if (!equals(v1.values[i], other.values[i])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case "record": {
|
||||
const other = v2 as Extract<Value, { tag: "record" }>;
|
||||
if (v1.fields.size !== other.fields.size) return false;
|
||||
for (const [key, val1] of v1.fields) {
|
||||
const val2 = other.fields.get(key);
|
||||
if (val2 === undefined || !equals(val1, val2)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case "closure":
|
||||
// Philosophical/Mathematical barrier: throw error as requested
|
||||
throw ThrownRuntimeError.error({
|
||||
tag: "ClosureEqualityComparison",
|
||||
value0: v1.closure,
|
||||
value1: v2,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Canonical bools are:
|
||||
// - True is `#T`
|
||||
// - False is `#F`
|
||||
// TODO: This is not a great design. Probably introducing completely new values would be better.
|
||||
export function forceBool(value: Value): boolean {
|
||||
if (value.tag === "tag") {
|
||||
if (value.tag_name === "T") return true;
|
||||
if (value.tag_name === "F") return false;
|
||||
}
|
||||
throw ThrownRuntimeError.error({
|
||||
tag: "NotABoolean",
|
||||
value
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ export namespace Expr {
|
|||
export namespace SignalExpr {
|
||||
export const read = (name: SignalName, span: Span): SignalExpr => ({ tag: "read", name, span });
|
||||
export const signalBinding = (pattern: ProductPattern, expr: SignalExpr, span: Span): SignalExprBinding => ({ pattern, expr, span });
|
||||
export const let_ = (bindings: SignalExprBinding[], body: Expr, span: Span): SignalExpr => ({ tag: "let", bindings, body, span });
|
||||
}
|
||||
|
||||
export namespace ProductPattern {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Cursor } from './cursor';
|
|||
import { ExprScanError, exprStart, ExprStartToken, IdentifierKind, identifierScanner, isNextTokenExprStart, isNextTokenProductPatternStart, patternStart, PatternStartToken, signalExprStart, SignalExprStartToken, skipWhitespaceAndComments } from './scanner';
|
||||
import { char, CodePoint, SourceText, Span } from './source_text';
|
||||
import { Result } from '../result';
|
||||
import { Expr, ExprBinding, FieldAssignment, FieldPattern, FunctionName, MatchBranch, Pattern, ProductPattern, SignalExpr } from '../expr';
|
||||
import { Expr, ExprBinding, FieldAssignment, FieldPattern, FunctionName, MatchBranch, Pattern, ProductPattern, SignalExpr, SignalExprBinding } from '../expr';
|
||||
|
||||
// CONVENTION: Every parser is responsible to consume whitespace/comments at the end.
|
||||
// Every parser is not responsible for cleaning up whitespace/comments at the start - only the final `parse` that's exposed to the public.
|
||||
|
|
@ -29,6 +29,8 @@ export type ParseError =
|
|||
| { tag: "ExpectedRecordOpen", span: Span } // Expected '(' after ':'
|
||||
| { tag: "ExpectedLetBlockOpen", span: Span } // Expected '{' after 'let'
|
||||
| { tag: "ExpectedLetBlockClose", span: Span } // Expected '}' at end of 'let' expression
|
||||
| { tag: "ExpectedLetSignalBlockOpen", span: Span } // Expected '{' after `let-signal`
|
||||
| { tag: "ExpectedLetSignalBlockClose", span: Span } // Expected '}' at end of 'let-signal' expression
|
||||
| { tag: "ExpectedMatchBlockOpen", span: Span } // Expected '{' after 'match'
|
||||
| { tag: "ExpectedMatchBlockClose", span: Span } // Expected '}' at end of 'match' expression
|
||||
| { tag: "ExpectedLambdaBlockOpen", span: Span } // Expected '{' after `fn`
|
||||
|
|
@ -37,7 +39,7 @@ export type ParseError =
|
|||
| { tag: "ExpectedApplySeparator", span: Span } // Expected '!' inside 'apply'
|
||||
| { tag: "UnexpectedTagPattern", span: Span } // Found #tag where product pattern expected
|
||||
| { tag: "ExpectedPattern", span: Span } // EOF or invalid start of pattern
|
||||
| { tag: "ExpectedRecordPatternOpen", span: Span } // Expected '(' at start of record pattern
|
||||
| { tag: "ExpectedRecordPatternOpen", span: Span } // Expected ':(' at start of record pattern
|
||||
| { tag: "ExpectedRecordField", span: Span }; // Expected identifier in record pattern
|
||||
|
||||
// TODO: Delete?
|
||||
|
|
@ -50,6 +52,8 @@ export type Expectation =
|
|||
| "ExpectedRecordOpen"
|
||||
| "ExpectedLetBlockOpen"
|
||||
| "ExpectedLetBlockClose"
|
||||
| "ExpectedLetSignalBlockOpen"
|
||||
| "ExpectedLetSignalBlockClose"
|
||||
| "ExpectedMatchBlockOpen"
|
||||
| "ExpectedMatchBlockClose"
|
||||
| "ExpectedApplyStart"
|
||||
|
|
@ -314,6 +318,7 @@ function expr(cursor: Cursor): Expr {
|
|||
function signalExpr(cursor: Cursor): SignalExpr {
|
||||
const start = cursor.currentLocation();
|
||||
const token = signalExprStartToken(cursor);
|
||||
|
||||
switch (token.tag) {
|
||||
case "EOF":
|
||||
throw {
|
||||
|
|
@ -323,41 +328,27 @@ function signalExpr(cursor: Cursor): SignalExpr {
|
|||
} as ParseError;
|
||||
case "signal_read":
|
||||
return SignalExpr.read(token.name, token.span);
|
||||
// case "function_name":
|
||||
// TODO: when components are ready
|
||||
// // e.g. my_func(arg1, arg2)
|
||||
// // parse a `,` delimiter sequence of expr
|
||||
// // need to consume )
|
||||
// if (!tryConsume(cursor, char('('))) {
|
||||
// throw {
|
||||
// tag: "ExpectedFunctionCallStart",
|
||||
// span: cursor.makeSpan(cursor.currentLocation())
|
||||
// } as ParseError;
|
||||
// }
|
||||
// const args = delimitedTerminalSequence(cursor, DELIMITER_COMMA, TERMINATOR_CLOSE_PAREN, expr);
|
||||
// return Expr.call(token.name, args, cursor.makeSpan(start));
|
||||
case "keyword":
|
||||
switch (token.kw) {
|
||||
case "let-signal":
|
||||
// TODO:
|
||||
// // let { p0 = e0, p1 = e2 . body }
|
||||
// if (!tryConsume(cursor, char('{'))) {
|
||||
// throw {
|
||||
// tag: "ExpectedLetBlockOpen",
|
||||
// span: cursor.makeSpan(cursor.currentLocation())
|
||||
// } as ParseError;
|
||||
// }
|
||||
// const bindings = delimitedTerminalSequence(cursor, DELIMITER_COMMA, TERMINATOR_DOT, productPatternBinding);
|
||||
// const body = expr(cursor);
|
||||
// let { x := sig-expr, y := sig-expr . normal-expr }
|
||||
// TODO: Decide if to introduce new keyword `:=` or just reuse `=`?
|
||||
if (!tryConsume(cursor, char('{'))) {
|
||||
throw {
|
||||
tag: "ExpectedLetSignalBlockOpen",
|
||||
span: cursor.makeSpan(cursor.currentLocation())
|
||||
} as ParseError;
|
||||
}
|
||||
const bindings = delimitedTerminalSequence(cursor, DELIMITER_COMMA, TERMINATOR_DOT, productPatternSignalBinding);
|
||||
const body = expr(cursor);
|
||||
|
||||
// if (!tryConsume(cursor, TERMINATOR_CLOSE_BRACE)) {
|
||||
// throw {
|
||||
// tag: "ExpectedLetBlockClose",
|
||||
// span: cursor.makeSpan(cursor.currentLocation())
|
||||
// } as ParseError;
|
||||
// }
|
||||
// return Expr.let_(bindings, body, cursor.makeSpan(start));
|
||||
return 0 as any;
|
||||
if (!tryConsume(cursor, TERMINATOR_CLOSE_BRACE)) {
|
||||
throw {
|
||||
tag: "ExpectedLetSignalBlockClose",
|
||||
span: cursor.makeSpan(cursor.currentLocation())
|
||||
} as ParseError;
|
||||
}
|
||||
return SignalExpr.let_(bindings, body, cursor.makeSpan(start));
|
||||
case "let":
|
||||
case "fn":
|
||||
case "match":
|
||||
|
|
@ -415,6 +406,20 @@ function productPatternBinding(cursor: Cursor): ExprBinding {
|
|||
return Expr.exprBinding(pattern, e, cursor.makeSpan(start));
|
||||
}
|
||||
|
||||
function productPatternSignalBinding(cursor: Cursor): SignalExprBinding {
|
||||
const start = cursor.currentLocation();
|
||||
const pattern = productPattern(cursor);
|
||||
|
||||
if (!tryConsume(cursor, char('='))) {
|
||||
throw {
|
||||
tag: "ExpectedPatternBindingSymbol",
|
||||
span: cursor.makeSpan(cursor.currentLocation())
|
||||
} as ParseError;
|
||||
}
|
||||
const e = signalExpr(cursor);
|
||||
return SignalExpr.signalBinding(pattern, e, cursor.makeSpan(start));
|
||||
}
|
||||
|
||||
function fieldAssignment(cursor: Cursor): FieldAssignment {
|
||||
const start = cursor.currentLocation();
|
||||
// `f = e`
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ export type ExprScanError =
|
|||
| NumberError
|
||||
| StringError
|
||||
| { tag: "InvalidIdentifier", text: string, kind: IdentifierKind, reason: IdentifierErrorReason, span: Span }
|
||||
| { tag: "UnexpectedIdentifier", identifier: string, span: Span }
|
||||
|
||||
// What kind of identifier were we trying to parse?
|
||||
export type IdentifierKind =
|
||||
|
|
@ -268,9 +269,11 @@ export function signalExprStart(cursor: Cursor): SignalExprStartToken {
|
|||
case "keyword":
|
||||
return result;
|
||||
case "identifier":
|
||||
// TODO: when we have parametrized signal-expressions
|
||||
// return { tag: "function_name", name: result.name, span: result.span };
|
||||
return 0 as any;
|
||||
throw ({
|
||||
tag: "UnexpectedIdentifier",
|
||||
identifier: result.name,
|
||||
span: result.span,
|
||||
} as ExprScanError);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -349,9 +352,6 @@ export function isNextTokenExprStart(cursor: Cursor): boolean {
|
|||
|
||||
case "EOF":
|
||||
return false;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
|
|
@ -386,7 +386,8 @@ export function isNextTokenProductPatternStart(cursor: Cursor): boolean {
|
|||
case "!":
|
||||
return false;
|
||||
}
|
||||
default:
|
||||
case "tag":
|
||||
case "EOF":
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { eval_signal_expression, makeTickSignal, signal, Signal, SignalId, SignalRuntime } from "./eval/signal";
|
||||
import { ThrownRuntimeError } from "./eval/error";
|
||||
import { Env, Value } from "./eval/value";
|
||||
import { Expr, FunctionName, SignalName, ProductPattern, SignalExpr } from "./expr";
|
||||
import { installPrimitives } from "./primitive";
|
||||
import { eval_expr } from "./eval/evaluator";
|
||||
import { eval_signal_expression, spawnTick, spawnSignal, SignalRuntime, getNode, spawnSource, hotSwapSignal } from "./eval/signalValue"
|
||||
|
||||
export type Timestamp = number;
|
||||
|
||||
type SignalId = SignalRuntime.DAGNode;
|
||||
|
||||
export type Program = {
|
||||
function_definitions: Map<FunctionName, FunctionDefinition>,
|
||||
function_definition_order: FunctionName[],
|
||||
|
|
@ -70,7 +72,7 @@ type CellDefinition = {
|
|||
|
||||
body: Expr,
|
||||
|
||||
signalId?: SignalId,
|
||||
cell?: [SignalId, (value: Value) => void],
|
||||
|
||||
// Metadata
|
||||
createdAt: Timestamp,
|
||||
|
|
@ -78,7 +80,7 @@ type CellDefinition = {
|
|||
}
|
||||
|
||||
type PrimitiveSignalDefinition = {
|
||||
name: FunctionName,
|
||||
name: SignalName,
|
||||
signalId: SignalId,
|
||||
}
|
||||
|
||||
|
|
@ -162,11 +164,11 @@ export namespace Program {
|
|||
}
|
||||
|
||||
function install_primitive_signals(program: Program) {
|
||||
install_primitive_signal(program, "tick",makeTickSignal(1000));
|
||||
const signalId = spawnTick(program, "tick", 1000);
|
||||
install_primitive_signal(program, "tick", signalId);
|
||||
}
|
||||
|
||||
function install_primitive_signal(program: Program, name: SignalName, signal: Signal<Value>) {
|
||||
const signalId = attachNewSignal(program, signal);
|
||||
function install_primitive_signal(program: Program, name: SignalName, signalId: SignalId) {
|
||||
const def: SignalDefinition = {
|
||||
tag: "primitive",
|
||||
def: { name, signalId }
|
||||
|
|
@ -175,19 +177,14 @@ export namespace Program {
|
|||
}
|
||||
|
||||
// may throw `ThrownRuntimeError`. This is used during initialization.
|
||||
export function get_or_create_signal(program: Program, name: SignalName): Signal<Value> {
|
||||
export function get_or_create_signal(program: Program, name: SignalName): SignalId {
|
||||
const sigDef = lookup_signal_definition(program, name);
|
||||
|
||||
switch (sigDef.tag) {
|
||||
case "user": {
|
||||
const def = sigDef.def;
|
||||
if (def.signalId !== undefined) {
|
||||
const signal = SignalRuntime.getSignal(program.signal_runtime, def.signalId);
|
||||
if (signal === undefined) {
|
||||
throw ThrownRuntimeError.error({ tag: "SignalDefinitionHasSignalIdWithoutSignal", name, });
|
||||
} else {
|
||||
return signal;
|
||||
}
|
||||
return def.signalId;
|
||||
}
|
||||
|
||||
// We need to create the signal for the first time.
|
||||
|
|
@ -197,90 +194,28 @@ export namespace Program {
|
|||
def.is_initializing = true;
|
||||
|
||||
try {
|
||||
const newSignal = eval_signal_expression(program, Env.nil(), def.body);
|
||||
const newId = attachNewSignal(program, newSignal);
|
||||
program.signal_runtime.store.set(newId, newSignal);
|
||||
const newId = spawnSignal(program, name, def.body);
|
||||
def.signalId = newId;
|
||||
|
||||
return newSignal;
|
||||
return newId;
|
||||
} finally {
|
||||
def.is_initializing = false;
|
||||
}
|
||||
}
|
||||
case "cell": {
|
||||
const def = sigDef.def;
|
||||
if (def.signalId !== undefined) {
|
||||
const signal = SignalRuntime.getSignal(program.signal_runtime, def.signalId);
|
||||
if (signal === undefined) {
|
||||
throw ThrownRuntimeError.error({ tag: "SignalDefinitionHasSignalIdWithoutSignal", name, });
|
||||
} else {
|
||||
return signal;
|
||||
}
|
||||
if (def.cell !== undefined) {
|
||||
return def.cell[0];
|
||||
}
|
||||
|
||||
// We need to create the cell-signal for the first time.
|
||||
const initialValue = eval_expr(program, Env.nil(), def.body);
|
||||
const sig = signal(initialValue);
|
||||
const id = attachNewSignal(program, sig);
|
||||
def.signalId = id;
|
||||
return sig;
|
||||
const cell = spawnSource(program, name, initialValue)
|
||||
def.cell = cell;
|
||||
return cell[0];
|
||||
}
|
||||
case "primitive": {
|
||||
const def = sigDef.def;
|
||||
const signal = SignalRuntime.getSignal(program.signal_runtime, def.signalId);
|
||||
if (signal === undefined) {
|
||||
throw ThrownRuntimeError.error({ tag: "SignalDefinitionHasSignalIdWithoutSignal", name, });
|
||||
} else {
|
||||
return signal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function attachNewSignal(program: Program, signal: Signal<Value>): SignalId {
|
||||
const newId = SignalRuntime.generateSignalId(program.signal_runtime);
|
||||
program.signal_runtime.store.set(newId, signal);
|
||||
return newId;
|
||||
}
|
||||
|
||||
// TODO: Is this necessary? Maybe `get_or_create_signal` is sufficient for all cases.
|
||||
// may throw `ThrownRuntimeError`. This is used by evaluator.
|
||||
export function lookup_signal(program: Program, name: SignalName): Signal<Value> {
|
||||
const sigDef = lookup_signal_definition(program, name);
|
||||
|
||||
switch (sigDef.tag) {
|
||||
case "user": {
|
||||
const def = sigDef.def;
|
||||
if (def.signalId === undefined) {
|
||||
throw ThrownRuntimeError.error({ tag: "SignalLookupFailure", name, });
|
||||
}
|
||||
const signal = SignalRuntime.getSignal(program.signal_runtime, def.signalId);
|
||||
if (signal === undefined){
|
||||
throw ThrownRuntimeError.error({ tag: "SignalLookupFailure", name, });
|
||||
} else {
|
||||
return signal;
|
||||
}
|
||||
}
|
||||
case "cell": {
|
||||
const def = sigDef.def;
|
||||
if (def.signalId === undefined) {
|
||||
throw ThrownRuntimeError.error({ tag: "SignalLookupFailure", name, });
|
||||
}
|
||||
const signal = SignalRuntime.getSignal(program.signal_runtime, def.signalId);
|
||||
if (signal === undefined){
|
||||
throw ThrownRuntimeError.error({ tag: "SignalLookupFailure", name, });
|
||||
} else {
|
||||
return signal;
|
||||
}
|
||||
}
|
||||
case "primitive": {
|
||||
const def = sigDef.def;
|
||||
const signal = SignalRuntime.getSignal(program.signal_runtime, def.signalId);
|
||||
if (signal === undefined) {
|
||||
throw ThrownRuntimeError.error({ tag: "SignalDefinitionHasSignalIdWithoutSignal", name, });
|
||||
} else {
|
||||
return signal;
|
||||
}
|
||||
return sigDef.def.signalId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -294,10 +229,18 @@ export namespace Program {
|
|||
raw_body: string,
|
||||
}
|
||||
|
||||
export function getFunction(program: Program, name: FunctionName): Result<FunctionDefinition> {
|
||||
const fn = program.function_definitions.get(name);
|
||||
if (fn === undefined) {
|
||||
return Result.error({ tag: "FunctionNotFound", name });
|
||||
}
|
||||
return Result.ok(fn);
|
||||
}
|
||||
|
||||
export function registerFunction(
|
||||
program: Program,
|
||||
{ name, body, parameters, raw_parameters, raw_body }: CreateFunction
|
||||
): Result<void> {
|
||||
): Result<FunctionName> {
|
||||
if (program.function_definitions.has(name)) {
|
||||
return Result.error({ tag: "DuplicateFunctionName", name });
|
||||
}
|
||||
|
|
@ -317,13 +260,12 @@ export namespace Program {
|
|||
program.function_definitions.set(name, { tag: "user", def: newFunction });
|
||||
program.function_definition_order.push(name);
|
||||
|
||||
return Result.ok(undefined);
|
||||
return Result.ok(name);
|
||||
}
|
||||
|
||||
export type UpdateFunction = {
|
||||
parameters: ProductPattern[],
|
||||
body: Expr,
|
||||
raw_name: string,
|
||||
raw_parameters: string,
|
||||
raw_body: string,
|
||||
}
|
||||
|
|
@ -370,6 +312,14 @@ export namespace Program {
|
|||
}
|
||||
|
||||
// === Signals ===
|
||||
export function getSignal(program: Program, name: SignalName): Result<SignalDefinition> {
|
||||
const sigDef = program.signal_definitions.get(name);
|
||||
if (sigDef === undefined) {
|
||||
return Result.error({ tag: "SignalNotFound", name });
|
||||
}
|
||||
return Result.ok(sigDef);
|
||||
}
|
||||
|
||||
export type CreateSignal = {
|
||||
name: SignalName,
|
||||
body: SignalExpr,
|
||||
|
|
@ -379,7 +329,7 @@ export namespace Program {
|
|||
export function registerSignal(
|
||||
program: Program,
|
||||
{ name, body, raw_body }: CreateSignal
|
||||
): Result<void> {
|
||||
): Result<SignalName> {
|
||||
if (program.signal_definitions.has(name)) {
|
||||
return Result.error({ tag: "DuplicateSignalName", name });
|
||||
}
|
||||
|
|
@ -400,10 +350,10 @@ export namespace Program {
|
|||
program.signal_definitions.set(name, { tag: "user", def: newSignal });
|
||||
program.signal_definition_order.push(name);
|
||||
|
||||
// TODO: Note that this doesn't actually evaluate the signal and doesn't insert it into signal-runtime.
|
||||
// Note that this doesn't actually evaluate the signal and doesn't insert it into signal-runtime.
|
||||
// For that we will use `get_or_create_signal`
|
||||
|
||||
return Result.ok(undefined);
|
||||
return Result.ok(name);
|
||||
}
|
||||
|
||||
export type UpdateSignal = {
|
||||
|
|
@ -435,45 +385,40 @@ export function updateSignal(
|
|||
def.raw_body = raw_body;
|
||||
def.lastModifiedAt = Date.now();
|
||||
|
||||
// TODO: When to recompile?
|
||||
// 2. CRITICAL: Invalidate the Runtime Cache
|
||||
// We clear the ID so the next 'read' forces a re-compile.
|
||||
// Note: This does NOT automatically update other signals that
|
||||
// are currently holding a reference to the *old* signal ID.
|
||||
// That requires a more complex hot-reload strategy, but this
|
||||
// is the correct first step.
|
||||
def.signalId = undefined;
|
||||
def.is_initializing = false;
|
||||
if (def.signalId !== undefined) {
|
||||
hotSwapSignal(program, def.signalId, body);
|
||||
}
|
||||
|
||||
return Result.ok(undefined);
|
||||
}
|
||||
|
||||
export function deleteSignal(program: Program, name: SignalName): Result<void> {
|
||||
const existingEntry = program.signal_definitions.get(name);
|
||||
// TODO: This needs careful thought about transitive dependencies of a signal
|
||||
// export function deleteSignal(program: Program, name: SignalName): Result<void> {
|
||||
// const existingEntry = program.signal_definitions.get(name);
|
||||
|
||||
if (!existingEntry) {
|
||||
return Result.error({ tag: "SignalNotFound", name } as any);
|
||||
}
|
||||
// if (!existingEntry) {
|
||||
// return Result.error({ tag: "SignalNotFound", name } as any);
|
||||
// }
|
||||
|
||||
if (existingEntry.tag === "primitive") {
|
||||
return Result.error({ tag: "CannotDeletePrimitiveSignal", name } as any);
|
||||
}
|
||||
// if (existingEntry.tag === "primitive") {
|
||||
// return Result.error({ tag: "CannotDeletePrimitiveSignal", name } as any);
|
||||
// }
|
||||
|
||||
program.signal_definitions.delete(name);
|
||||
// program.signal_definitions.delete(name);
|
||||
|
||||
const orderIndex = program.signal_definition_order.indexOf(name);
|
||||
if (orderIndex !== -1) {
|
||||
program.signal_definition_order.splice(orderIndex, 1);
|
||||
}
|
||||
// const orderIndex = program.signal_definition_order.indexOf(name);
|
||||
// if (orderIndex !== -1) {
|
||||
// program.signal_definition_order.splice(orderIndex, 1);
|
||||
// }
|
||||
|
||||
// TODO:
|
||||
// Note: The old signal instance still exists in program.signal_runtime.store
|
||||
// We technically leak memory here unless we also remove it from the runtime store.
|
||||
// However, since other signals might still depend on that ID,
|
||||
// leaving it is actually safer for now to prevent crashes.
|
||||
// // TODO:
|
||||
// // Note: The old signal instance still exists in program.signal_runtime.store
|
||||
// // We technically leak memory here unless we also remove it from the runtime store.
|
||||
// // However, since other signals might still depend on that ID,
|
||||
// // leaving it is actually safer for now to prevent crashes.
|
||||
|
||||
return Result.ok(undefined);
|
||||
}
|
||||
// return Result.ok(undefined);
|
||||
// }
|
||||
|
||||
// === Cells ===
|
||||
export type CreateCell = {
|
||||
|
|
@ -482,34 +427,35 @@ export function updateSignal(
|
|||
raw_body: string,
|
||||
}
|
||||
|
||||
// TODO: refactor pending
|
||||
export function registerCell(
|
||||
program: Program,
|
||||
{ name, body, raw_body }: CreateCell
|
||||
): Result<void> {
|
||||
|
||||
if (program.signal_definitions.has(name)) {
|
||||
return Result.error({ tag: "DuplicateSignalName", name });
|
||||
}
|
||||
// if (program.signal_definitions.has(name)) {
|
||||
// return Result.error({ tag: "DuplicateSignalName", name });
|
||||
// }
|
||||
|
||||
const now: Timestamp = Date.now();
|
||||
// const now: Timestamp = Date.now();
|
||||
|
||||
// TODO: MAY THROW RuntimeError. Should probably switch to `eval_start` - and extend the `Program.Error` with runtime errors.
|
||||
const initialValue = eval_expr(program, Env.nil(), body);
|
||||
// // TODO: MAY THROW RuntimeError. Should probably switch to `eval_start` - and extend the `Program.Error` with runtime errors.
|
||||
// const initialValue = eval_expr(program, Env.nil(), body);
|
||||
|
||||
const sig = signal(initialValue);
|
||||
const signalId = attachNewSignal(program, sig);
|
||||
// const sig = signal(initialValue);
|
||||
// const signalId = attachNewSignal(program, sig);
|
||||
|
||||
const newCell: CellDefinition = {
|
||||
name,
|
||||
raw_body,
|
||||
body,
|
||||
signalId,
|
||||
createdAt: now,
|
||||
lastModifiedAt: now,
|
||||
};
|
||||
// const newCell: CellDefinition = {
|
||||
// name,
|
||||
// raw_body,
|
||||
// body,
|
||||
// signalId,
|
||||
// createdAt: now,
|
||||
// lastModifiedAt: now,
|
||||
// };
|
||||
|
||||
program.signal_definitions.set(name, { tag: "cell", def: newCell });
|
||||
program.signal_definition_order.push(name);
|
||||
// program.signal_definitions.set(name, { tag: "cell", def: newCell });
|
||||
// program.signal_definition_order.push(name);
|
||||
|
||||
return Result.ok(undefined);
|
||||
}
|
||||
|
|
@ -519,44 +465,45 @@ export function updateSignal(
|
|||
raw_body: string,
|
||||
}
|
||||
|
||||
// TODO: refactor pending
|
||||
export function updateCell(
|
||||
program: Program,
|
||||
name: SignalName,
|
||||
{ body, raw_body }: UpdateCell
|
||||
): Result<void> {
|
||||
const existingEntry = program.signal_definitions.get(name);
|
||||
// const existingEntry = program.signal_definitions.get(name);
|
||||
|
||||
if (!existingEntry) {
|
||||
return Result.error({ tag: "SignalNotFound", name } as any);
|
||||
}
|
||||
// if (!existingEntry) {
|
||||
// return Result.error({ tag: "SignalNotFound", name } as any);
|
||||
// }
|
||||
|
||||
// Ensure we are editing a Cell
|
||||
if (existingEntry.tag !== "cell") {
|
||||
return Result.error({ tag: "CannotEditCell", name } as any);
|
||||
}
|
||||
// // Ensure we are editing a Cell
|
||||
// if (existingEntry.tag !== "cell") {
|
||||
// return Result.error({ tag: "CannotEditCell", name } as any);
|
||||
// }
|
||||
|
||||
const def = existingEntry.def;
|
||||
// const def = existingEntry.def;
|
||||
|
||||
// TODO: MAY THROW RuntimeError. Should probably switch to `eval_start` - and extend the `Program.Error` with runtime errors.
|
||||
const newValue = eval_expr(program, Env.nil(), body);
|
||||
// // TODO: MAY THROW RuntimeError. Should probably switch to `eval_start` - and extend the `Program.Error` with runtime errors.
|
||||
// const newValue = eval_expr(program, Env.nil(), body);
|
||||
|
||||
// 2. Find the existing runtime signal
|
||||
if (def.signalId === undefined) {
|
||||
// This should theoretically not happen for cells since we initialize them eagerly,
|
||||
// but good to be safe.
|
||||
return Result.error({ tag: "SignalDefinitionHasSignalIdWithoutSignal", name } as any);
|
||||
}
|
||||
// // Find the existing runtime signal
|
||||
// if (def.signalId === undefined) {
|
||||
// // This should theoretically not happen for cells since we initialize them eagerly,
|
||||
// // but good to be safe.
|
||||
// return Result.error({ tag: "SignalDefinitionHasSignalIdWithoutSignal", name } as any);
|
||||
// }
|
||||
|
||||
const sig = SignalRuntime.getSignal(program.signal_runtime, def.signalId);
|
||||
if (!sig) {
|
||||
return Result.error({ tag: "SignalDefinitionHasSignalIdWithoutSignal", name } as any);
|
||||
}
|
||||
// const sig = SignalRuntime.getSignal(program.signal_runtime, def.signalId);
|
||||
// if (!sig) {
|
||||
// return Result.error({ tag: "SignalDefinitionHasSignalIdWithoutSignal", name } as any);
|
||||
// }
|
||||
|
||||
sig.set(() => newValue);
|
||||
// sig.set(() => newValue);
|
||||
|
||||
def.body = body;
|
||||
def.raw_body = raw_body;
|
||||
def.lastModifiedAt = Date.now();
|
||||
// def.body = body;
|
||||
// def.raw_body = raw_body;
|
||||
// def.lastModifiedAt = Date.now();
|
||||
|
||||
return Result.ok(undefined);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { createSignal } from 'solid-js';
|
||||
import { Scrowl } from './Scrowl';
|
||||
import { Controls } from './Controls';
|
||||
import { Sidebar } from './Sidebar';
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
|
|
@ -14,7 +13,7 @@ export default function App() {
|
|||
}}
|
||||
>
|
||||
<Scrowl />
|
||||
<Controls />
|
||||
<Sidebar />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Expr, FieldAssignment, FieldPattern, Literal, Pattern, ProductPattern } from "../lang/expr";
|
||||
import { Expr, FieldAssignment, FieldPattern, Literal, Pattern, ProductPattern } from "src/lang/expr";
|
||||
|
||||
export function Expression(prop: { expr: Expr }) {
|
||||
return (
|
||||
|
|
@ -4,12 +4,6 @@ import { DisplayLineViews } from "./LineView";
|
|||
|
||||
export function formatErrorMesage(err: ParseError): string {
|
||||
switch (err.tag) {
|
||||
case "UnexpectedToken":
|
||||
return `Unexpected token. Expected: ${err.expected}`;
|
||||
|
||||
case "UnexpectedTokenWhileParsingSequence":
|
||||
return `Unexpected token in sequence. Expected delimiter ${formatChar(err.expectedDelimiter)} or terminator ${formatChar(err.expectedTerminator)}, but found ${formatChar(err.received)}.`;
|
||||
|
||||
case "UnexpectedCharacter":
|
||||
return `Unexpected character: ${formatChar(err.char)}`;
|
||||
|
||||
|
|
@ -20,13 +14,12 @@ export function formatErrorMesage(err: ParseError): string {
|
|||
return "Expected a number here.";
|
||||
|
||||
case "InvalidNumber":
|
||||
return err.reason === "NotFinite"
|
||||
? "Number is too large or invalid."
|
||||
: "Invalid number format (missing fractional digits?).";
|
||||
|
||||
case "InvalidIdentifier":
|
||||
// Handle nested reasons if needed, e.g. "Keyword 'let' cannot be used as an identifier"
|
||||
return `Invalid identifier '${err.text}': ${err.reason.tag}`;
|
||||
switch (err.reason) {
|
||||
case "NotFinite":
|
||||
return "Number is too large or invalid.";
|
||||
case "MissingFractionalDigits":
|
||||
return "Invalid number format (missing fractional digits?).";
|
||||
}
|
||||
|
||||
case "InvalidEscape":
|
||||
switch (err.reason.tag) {
|
||||
|
|
@ -37,8 +30,56 @@ export function formatErrorMesage(err: ParseError): string {
|
|||
case "UnicodeOverflow": return `Unicode code point ${err.reason.value.toString(16)} is out of bounds.`;
|
||||
}
|
||||
|
||||
case "InvalidIdentifier": {
|
||||
let identifierKind = "";
|
||||
switch (err.kind) {
|
||||
case "variable_use":
|
||||
identifierKind = "variable name";
|
||||
break;
|
||||
case "field_name":
|
||||
identifierKind = "field name ";
|
||||
break;
|
||||
case "tag_construction":
|
||||
identifierKind = "tag";
|
||||
break;
|
||||
case "function_call":
|
||||
identifierKind = "function name";
|
||||
break;
|
||||
case "signal_read":
|
||||
identifierKind = "signal name";
|
||||
break;
|
||||
case "pattern_binding":
|
||||
identifierKind = "pattern variable";
|
||||
break;
|
||||
}
|
||||
|
||||
let reason = "";
|
||||
switch (err.reason.tag) {
|
||||
case "Empty":
|
||||
reason = "It's empty";
|
||||
break;
|
||||
case "StartsWithDigit":
|
||||
reason = "Can't start with a digit"
|
||||
break;
|
||||
case "IsKeyword":
|
||||
reason = "I'ts a keyword";
|
||||
break;
|
||||
}
|
||||
return `Invalid ${identifierKind} '${err.text}' ${reason}.`;
|
||||
}
|
||||
|
||||
case "UnexpectedIdentifier":
|
||||
return `Unexpected identifier encountered '${err.identifier}'`;
|
||||
|
||||
case "UnexpectedToken":
|
||||
return `Unexpected token. Expected: ${err.expected}`;
|
||||
|
||||
case "UnexpectedTokenWhileParsingSequence":
|
||||
return `Unexpected token in sequence. Expected delimiter ${formatChar(err.expectedDelimiter)} or terminator ${formatChar(err.expectedTerminator)}, but found ${formatChar(err.received)}.`;
|
||||
|
||||
// Context specific errors
|
||||
case "ExpectedExpression": return "Expected an expression here.";
|
||||
case "ExpectedSignalExpression": return "Expected a signal expression here.";
|
||||
case "ExpectedFieldAssignmentSymbol": return "Expected '=' for field assignment.";
|
||||
case "ExpectedPatternAssignmentSymbol": return "Expected '=' for pattern assignment.";
|
||||
case "ExpectedPatternBindingSymbol": return "Expected '.' for pattern binding.";
|
||||
|
|
@ -46,6 +87,8 @@ export function formatErrorMesage(err: ParseError): string {
|
|||
case "ExpectedRecordOpen": return "Expected '(' to start record.";
|
||||
case "ExpectedLetBlockOpen": return "Expected '{' to start let-block.";
|
||||
case "ExpectedLetBlockClose": return "Expected '}' to close let-block.";
|
||||
case "ExpectedLetSignalBlockOpen": return "Expected '{' to start let-signal-block.";
|
||||
case "ExpectedLetSignalBlockClose": return "Expected '}' to close let-signal-block.";
|
||||
case "ExpectedMatchBlockOpen": return "Expected '{' to start match-block.";
|
||||
case "ExpectedMatchBlockClose": return "Expected '}' to close match-block.";
|
||||
case "ExpectedLambdaBlockOpen": return "Expected '{' to start lambda body.";
|
||||
|
|
@ -54,11 +97,8 @@ export function formatErrorMesage(err: ParseError): string {
|
|||
case "ExpectedApplySeparator": return "Expected '!' inside 'apply'.";
|
||||
case "UnexpectedTagPattern": return "Unexpected tag pattern (expected product pattern).";
|
||||
case "ExpectedPattern": return "Expected a pattern here.";
|
||||
case "ExpectedRecordPatternOpen": return "Expected '(' for record pattern.";
|
||||
case "ExpectedRecordPatternOpen": return "Expected a ':(' at start of record pattern here.";
|
||||
case "ExpectedRecordField": return "Expected a field name in record pattern.";
|
||||
|
||||
default:
|
||||
return `Unknown error: ${(err as any).tag}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Closure, Value, Env, EnvFrame } from '../lang/eval/value';
|
||||
import { Closure, Value, Env, EnvFrame } from 'src/lang/eval/value';
|
||||
import { exprToString, productPatternToString } from './Expr';
|
||||
|
||||
export function Val(prop: { value: Value }) {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { spawnFunctionDraft } from "./scrowlStore";
|
||||
import { spawnNewFunctionDraftDigith, spawnNewSignalDraftDigith } from "src/ui/Scrowl/scrowlStore";
|
||||
|
||||
type Props = {
|
||||
// TODO
|
||||
|
|
@ -6,20 +6,14 @@ type Props = {
|
|||
|
||||
export function Controls(props: Props) {
|
||||
return (
|
||||
<aside
|
||||
style={{
|
||||
position: "sticky",
|
||||
top: "2rem",
|
||||
height: "fit-content"
|
||||
}}
|
||||
>
|
||||
<aside>
|
||||
<article>
|
||||
<header>Controls</header>
|
||||
|
||||
<div style={{ display: "flex", gap: "0.5rem", "flex-wrap": "wrap" }}>
|
||||
<button
|
||||
class="outline secondary"
|
||||
onClick={spawnFunctionDraft}
|
||||
onClick={spawnNewFunctionDraftDigith}
|
||||
style={{ padding: "2px 8px", "font-size": "0.8rem", width: "auto" }}
|
||||
>
|
||||
+fn
|
||||
|
|
@ -27,8 +21,8 @@ export function Controls(props: Props) {
|
|||
|
||||
<button
|
||||
class="outline secondary"
|
||||
onClick={spawnNewSignalDraftDigith}
|
||||
style={{ padding: "2px 8px", "font-size": "0.8rem", width: "auto" }}
|
||||
disabled
|
||||
>
|
||||
+signal
|
||||
</button>
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import { FunctionName } from "src/lang/expr"
|
||||
|
||||
export type Digith =
|
||||
| Digith.Repl
|
||||
| Digith.NewFunctionDraft
|
||||
| Digith.Function
|
||||
|
||||
export namespace Digith {
|
||||
export type Repl = {
|
||||
tag: "repl",
|
||||
}
|
||||
|
||||
export type NewFunctionDraft = {
|
||||
tag: "new-fn-draft",
|
||||
raw_name: string,
|
||||
raw_parameters: string,
|
||||
raw_body: string,
|
||||
}
|
||||
|
||||
export type Function = {
|
||||
tag: "fn",
|
||||
name: FunctionName,
|
||||
raw_parameters: string,
|
||||
raw_body: string,
|
||||
is_loaded: boolean
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
134
src/ui/Digith/DigithError.tsx
Normal file
134
src/ui/Digith/DigithError.tsx
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import { For, Match, Show, Switch } from "solid-js";
|
||||
import { ParseError } from "src/lang/parser/parser";
|
||||
import { SourceText } from "src/lang/parser/source_text";
|
||||
import { ShowParseError } from 'src/ui/Component/ParseError';
|
||||
import { Program } from "src/lang/program";
|
||||
|
||||
export type DigithError = {
|
||||
payload: DigithError.Payload,
|
||||
ids: DigithError.Id[],
|
||||
tags: DigithError.Tag[],
|
||||
config: DigithError.Config,
|
||||
}
|
||||
|
||||
export namespace DigithError {
|
||||
export type Payload =
|
||||
| { tag: "Parse", err: ParseError, src: SourceText }
|
||||
| { tag: "Program", err: Program.Error };
|
||||
|
||||
export type Id = string;
|
||||
export type Tag = string;
|
||||
|
||||
export type Config = {
|
||||
title?: string,
|
||||
display?: "box" | "flat",
|
||||
}
|
||||
|
||||
function findById(errors: DigithError[], id: Id): DigithError | undefined {
|
||||
return errors.find((e) => e.ids.includes(id));
|
||||
}
|
||||
|
||||
function allWithTag(errors: DigithError[], tag: Tag): DigithError[] {
|
||||
return errors.filter((e) => e.tags.includes(tag));
|
||||
}
|
||||
|
||||
export function All(props: { errors: DigithError[] }) {
|
||||
return (
|
||||
<div class="digith-errors-container">
|
||||
<For each={props.errors}>
|
||||
{(error) => <Single error={error} />}
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ById(props: { errors: DigithError[], id: Id }) {
|
||||
const error = () => findById(props.errors, props.id);
|
||||
return (
|
||||
<Show when={error()}>
|
||||
{(e) => <Single error={e()} />}
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
||||
export function ByTag(props: { errors: DigithError[], tag: Tag }) {
|
||||
const matched = () => allWithTag(props.errors, props.tag);
|
||||
return (
|
||||
<Show when={matched().length > 0}>
|
||||
<All errors={matched()} />
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
||||
function Single(props: { error: DigithError }) {
|
||||
const display = () => props.error.config.display ?? "box";
|
||||
return (
|
||||
<div style={{ "margin-bottom": display() === "box" ? "1rem" : "0.5rem" }} >
|
||||
<Switch>
|
||||
<Match when={display() === "box"}>
|
||||
<article style={{ border: "1px solid var(--pico-del-color)", padding: "0.5rem 1rem", margin: 0 }}>
|
||||
<Show when={props.error.config.title}>
|
||||
<header style={{ "margin-bottom": "0.5rem", color: "var(--pico-del-color)", "font-weight": "bold" }}>
|
||||
{props.error.config.title}
|
||||
</header>
|
||||
</Show>
|
||||
<PayloadView payload={props.error.payload} />
|
||||
</article>
|
||||
</Match>
|
||||
|
||||
<Match when={display() === "flat"}>
|
||||
<div style={{ color: "var(--pico-del-color)" }}>
|
||||
<Show when={props.error.config.title}>
|
||||
<small style={{ "font-weight": "bold", display: "block" }}>{props.error.config.title}:</small>
|
||||
</Show>
|
||||
<PayloadView payload={props.error.payload} />
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PayloadView(props: { payload: Payload }) {
|
||||
return (
|
||||
<Switch>
|
||||
<Match
|
||||
when={props.payload.tag === "Parse" ? (props.payload as Extract<Payload, { tag: "Parse" }>) : undefined}
|
||||
>
|
||||
{(err) => <ShowParseError text={err().src} err={err().err} />}
|
||||
</Match>
|
||||
|
||||
<Match
|
||||
when={props.payload.tag === "Program" ? (props.payload as Extract<Payload, { tag: "Program" }>) : undefined}
|
||||
>
|
||||
{(err) => <ProgramErrorDisplay error={err().err} />}
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function ProgramErrorDisplay(props: { error: Program.Error }) {
|
||||
const message = () => {
|
||||
switch (props.error.tag) {
|
||||
case "DuplicateFunctionName":
|
||||
return `A function named '${props.error.name}' already exists.`;
|
||||
case "PrimitiveFunctionAlreadyExists":
|
||||
return `Cannot overwrite the primitive function '${props.error.name}'.`;
|
||||
// TODO: handle other cases
|
||||
default:
|
||||
return `Runtime Error: ${props.error.tag}`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<article style={{ border: "1px solid var(--pico-del-color)", padding: "0.5rem 1rem" }}>
|
||||
<small style={{ color: "var(--pico-del-color)", "font-weight": "bold" }}>
|
||||
Registration Failed
|
||||
</small>
|
||||
<p style={{ margin: 0 }}>{message()}</p>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
147
src/ui/Digith/Function/FunctionDigith.tsx
Normal file
147
src/ui/Digith/Function/FunctionDigith.tsx
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
import { createSignal, Show } from "solid-js";
|
||||
import { Digith } from "src/ui/Digith";
|
||||
import { useProgram } from "src/ui/ProgramProvider";
|
||||
import { CodeEditor } from "src/ui/Component/CodeEditor";
|
||||
import { sourceText } from "src/lang/parser/source_text";
|
||||
import { Program } from "src/lang/program";
|
||||
import { V, Validation, letValidate } from "src/ui/validation";
|
||||
import { validateExprRaw, validateParamsRaw } from "src/ui/validation/helpers";
|
||||
import { updateDigith } from "src/ui/Scrowl/scrowlStore";
|
||||
import { DigithError } from "src/ui/Digith/DigithError";
|
||||
|
||||
type Input = {
|
||||
raw_params: string,
|
||||
raw_body: string,
|
||||
}
|
||||
|
||||
const validator: Validation<Input, Program.UpdateFunction, DigithError> = letValidate(
|
||||
(input) => ({
|
||||
parameters: V.elseErr(validateParamsRaw(input.raw_params), err => ({
|
||||
payload: { tag: "Parse", field: "params", err, src: sourceText(input.raw_params) },
|
||||
ids: ["params"],
|
||||
tags: ["footer"],
|
||||
config: { title: "Parameters" },
|
||||
})),
|
||||
body: V.elseErr(validateExprRaw(input.raw_body), err => ({
|
||||
payload: { tag: "Parse", field: "body", err, src: sourceText(input.raw_body) },
|
||||
ids: ["body"],
|
||||
tags: ["footer"],
|
||||
config: { title: "Function Body" },
|
||||
})),
|
||||
}),
|
||||
(fields, input) => {
|
||||
return V.ok({
|
||||
parameters: fields.parameters,
|
||||
body: fields.body,
|
||||
raw_parameters: input.raw_params,
|
||||
raw_body: input.raw_body
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// TODO: What about renaming?
|
||||
export function FunctionDigith(props: { function: Digith.Function }) {
|
||||
const program = useProgram();
|
||||
|
||||
const [params, setParams] = createSignal(props.function.raw_parameters);
|
||||
const [body, setBody] = createSignal(props.function.raw_body);
|
||||
|
||||
const [errors, setErrors] = createSignal<DigithError[]>([]);
|
||||
|
||||
const isDirty = () =>
|
||||
params() !== props.function.raw_parameters ||
|
||||
body() !== props.function.raw_body;
|
||||
|
||||
function handleRedefine() {
|
||||
setErrors([]);
|
||||
|
||||
const validRes = validator({ raw_params: params(), raw_body: body() });
|
||||
if (validRes.tag === "errors") {
|
||||
setErrors(validRes.errors);
|
||||
return;
|
||||
}
|
||||
const updateData = validRes.value;
|
||||
|
||||
const progRes = Program.updateFunction(program, props.function.name, {
|
||||
parameters: updateData.parameters,
|
||||
body: updateData.body,
|
||||
raw_parameters: updateData.raw_parameters,
|
||||
raw_body: updateData.raw_body
|
||||
});
|
||||
|
||||
if (progRes.tag === "error") {
|
||||
setErrors([{
|
||||
payload: { tag: "Program", err: progRes.error },
|
||||
ids: ["program"],
|
||||
tags: ["footer"],
|
||||
config: { title: "Update Failed" },
|
||||
}]);
|
||||
return;
|
||||
}
|
||||
|
||||
// reloading the digith
|
||||
updateDigith(props.function.id, {
|
||||
...props.function,
|
||||
raw_parameters: updateData.raw_parameters,
|
||||
raw_body: updateData.raw_body
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<article>
|
||||
<header>
|
||||
<strong>Fn</strong>
|
||||
|
||||
{/* Dirty Indicator / Status */}
|
||||
<div>
|
||||
<Show when={isDirty()} fallback={<span style={{color: "var(--pico-muted-color)"}}>Synced</span>}>
|
||||
<span style={{color: "var(--pico-primary)"}}>● Unsaved Changes</span>
|
||||
</Show>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Name
|
||||
<input
|
||||
type="text"
|
||||
value={props.function.name}
|
||||
disabled
|
||||
style={{ opacity: 0.7, cursor: "not-allowed" }}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Parameters
|
||||
<input
|
||||
type="text"
|
||||
value={params()}
|
||||
onInput={(e) => setParams(e.currentTarget.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label>Body</label>
|
||||
<CodeEditor
|
||||
value={body()}
|
||||
onUpdate={setBody}
|
||||
onRun={handleRedefine}
|
||||
/>
|
||||
|
||||
<footer style={{ display: "flex", "align-items": "center", gap: "1rem" }}>
|
||||
<button
|
||||
onClick={handleRedefine}
|
||||
disabled={!isDirty()}
|
||||
>
|
||||
Redefine
|
||||
</button>
|
||||
|
||||
</footer>
|
||||
|
||||
<div style={{ "margin-top": "1rem" }}>
|
||||
<DigithError.ByTag errors={errors()} tag="footer" />
|
||||
</div>
|
||||
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
125
src/ui/Digith/Function/NewFunctionDraftDigith.tsx
Normal file
125
src/ui/Digith/Function/NewFunctionDraftDigith.tsx
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import { createSignal } from "solid-js";
|
||||
import { Digith } from "src/ui/Digith";
|
||||
import { useProgram } from "src/ui/ProgramProvider";
|
||||
import { CodeEditor } from "src/ui/Component/CodeEditor";
|
||||
import { sourceText } from "src/lang/parser/source_text";
|
||||
import { Program } from "src/lang/program";
|
||||
import { V, Validation, letValidate } from "src/ui/validation";
|
||||
import { validateExprRaw, validateNameRaw, validateParamsRaw } from "src/ui/validation/helpers";
|
||||
import { spawnFunctionDigith } from "src/ui/Scrowl/scrowlStore";
|
||||
import { DigithError } from "src/ui/Digith/DigithError";
|
||||
|
||||
type Input = {
|
||||
raw_name: string,
|
||||
raw_params: string,
|
||||
raw_body: string,
|
||||
}
|
||||
|
||||
const validator: Validation<Input, Program.CreateFunction, DigithError> = letValidate(
|
||||
(input) =>({
|
||||
name: V.elseErr(validateNameRaw(input.raw_name), err =>({
|
||||
payload: { tag: "Parse", err, src: sourceText(input.raw_name) },
|
||||
ids: ["name"],
|
||||
tags: ["footer"],
|
||||
config: { title: "Function Name", display: "flat" },
|
||||
})),
|
||||
parameters: V.elseErr(validateParamsRaw(input.raw_params), err => ({
|
||||
payload: { tag: "Parse", err, src: sourceText(input.raw_params) },
|
||||
ids: ["params"],
|
||||
tags: ["footer"],
|
||||
config: { title: "Parameters", display: "flat" },
|
||||
})),
|
||||
body: V.elseErr(validateExprRaw(input.raw_body), err => ({
|
||||
payload: { tag: "Parse", err, src: sourceText(input.raw_body) },
|
||||
ids: ["body"],
|
||||
tags: ["footer"],
|
||||
config: { title: "Function Body", display: "flat" },
|
||||
})),
|
||||
}),
|
||||
(fields, input) => {
|
||||
const createFunction: Program.CreateFunction = {
|
||||
name: fields.name,
|
||||
parameters: fields.parameters,
|
||||
body: fields.body,
|
||||
raw_parameters: input.raw_params,
|
||||
raw_body: input.raw_body,
|
||||
};
|
||||
return V.ok(createFunction);
|
||||
})
|
||||
|
||||
export function NewFunctionDraftDigith(props: { draft: Digith.NewFunctionDraft }) {
|
||||
const program = useProgram();
|
||||
|
||||
const [name, setName] = createSignal(props.draft.raw_name);
|
||||
const [params, setParams] = createSignal(props.draft.raw_parameters);
|
||||
const [body, setBody] = createSignal(props.draft.raw_body);
|
||||
|
||||
const [errors, setErrors] = createSignal<DigithError[]>([]);
|
||||
|
||||
function handleCommit() {
|
||||
setErrors([]);
|
||||
const validRes = validator({ raw_name: name(), raw_params: params(), raw_body: body() });
|
||||
if (validRes.tag === "errors") {
|
||||
setErrors(validRes.errors);
|
||||
return;
|
||||
}
|
||||
const createFunction = validRes.value;
|
||||
|
||||
const programRes = Program.registerFunction(program, createFunction);
|
||||
if (programRes.tag === "error") {
|
||||
setErrors([{
|
||||
payload: { tag: "Program", err: programRes.error },
|
||||
ids: ["program"],
|
||||
tags: ["footer"],
|
||||
config: { title: "Registration Failed" },
|
||||
}]);
|
||||
return;
|
||||
}
|
||||
const fnName = programRes.value;
|
||||
|
||||
spawnFunctionDigith(program, fnName, props.draft.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<article>
|
||||
<header><strong>Fn (Draft)</strong></header>
|
||||
|
||||
<div class="grid">
|
||||
<label>
|
||||
Name
|
||||
<input
|
||||
type="text"
|
||||
placeholder="my_func"
|
||||
value={name()}
|
||||
onInput={(e) => setName(e.currentTarget.value)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Parameters (comma separated)
|
||||
<input
|
||||
type="text"
|
||||
placeholder="x, y"
|
||||
value={params()}
|
||||
onInput={(e) => setParams(e.currentTarget.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label>Body</label>
|
||||
<CodeEditor
|
||||
value={body()}
|
||||
onUpdate={setBody}
|
||||
onRun={handleCommit}
|
||||
/>
|
||||
|
||||
<footer>
|
||||
<button class="primary" onClick={handleCommit}>Commit</button>
|
||||
</footer>
|
||||
|
||||
<div style={{ "margin-top": "1rem" }}>
|
||||
<DigithError.ByTag errors={errors()} tag="footer" />
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import { createSignal, Match, Switch } from 'solid-js';
|
||||
import { useProgram } from './ProgramProvider';
|
||||
import { useProgram } from 'src/ui/ProgramProvider';
|
||||
import { eval_start } from 'src/lang/eval/evaluator';
|
||||
import { Value } from 'src/lang/eval/value';
|
||||
import { RuntimeError } from 'src/lang/eval/error';
|
||||
import { SourceText, sourceText } from 'src/lang/parser/source_text';
|
||||
import { ParseError, parseExpr } from 'src/lang/parser/parser';
|
||||
import { ShowParseError } from './ParseError';
|
||||
import { Val } from './Value';
|
||||
import { CodeEditor } from './CodeEditor';
|
||||
import { ShowParseError } from 'src/ui/Component/ParseError';
|
||||
import { Val } from 'src/ui/Component/Value';
|
||||
import { CodeEditor } from 'src/ui/Component/CodeEditor';
|
||||
|
||||
namespace ReplResult {
|
||||
export type Idle =
|
||||
107
src/ui/Digith/Signal/NewSignalDraftDigith.tsx
Normal file
107
src/ui/Digith/Signal/NewSignalDraftDigith.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { createSignal } from "solid-js";
|
||||
import { Digith } from "src/ui/Digith";
|
||||
import { useProgram } from "src/ui/ProgramProvider";
|
||||
import { CodeEditor } from "src/ui/Component/CodeEditor";
|
||||
import { sourceText } from "src/lang/parser/source_text";
|
||||
import { Program } from "src/lang/program";
|
||||
import { V, Validation, letValidate } from "src/ui/validation";
|
||||
import { validateNameRaw, validateSignalExprRaw } from "src/ui/validation/helpers";
|
||||
import { DigithError } from "src/ui/Digith/DigithError";
|
||||
import { spawnSignalDigith } from "src/ui/Scrowl/scrowlStore";
|
||||
|
||||
type Input = {
|
||||
raw_name: string,
|
||||
raw_body: string,
|
||||
}
|
||||
|
||||
const validator: Validation<Input, Program.CreateSignal, DigithError> = letValidate(
|
||||
(input) =>({
|
||||
name: V.elseErr(validateNameRaw(input.raw_name), err =>({
|
||||
payload: { tag: "Parse", err, src: sourceText(input.raw_name) },
|
||||
ids: ["name"],
|
||||
tags: ["footer"],
|
||||
config: { title: "Signal Name", display: "flat" },
|
||||
})),
|
||||
body: V.elseErr(validateSignalExprRaw(input.raw_body), err => ({
|
||||
payload: { tag: "Parse", err, src: sourceText(input.raw_body) },
|
||||
ids: ["body"],
|
||||
tags: ["footer"],
|
||||
config: { title: "Signal Body", display: "flat" },
|
||||
})),
|
||||
}),
|
||||
(fields, input) => {
|
||||
const createSignal: Program.CreateSignal = {
|
||||
name: fields.name,
|
||||
body: fields.body,
|
||||
raw_body: input.raw_body,
|
||||
};
|
||||
|
||||
return V.ok(createSignal);
|
||||
})
|
||||
|
||||
|
||||
export function NewSignalDraftDigith(props: { draft: Digith.NewSignalDraft }) {
|
||||
const program = useProgram();
|
||||
|
||||
const [name, setName] = createSignal(props.draft.raw_name);
|
||||
const [body, setBody] = createSignal(props.draft.raw_body);
|
||||
|
||||
const [errors, setErrors] = createSignal<DigithError[]>([]);
|
||||
|
||||
function handleCommit() {
|
||||
setErrors([]);
|
||||
const validRes = validator({ raw_name: name(), raw_body: body() });
|
||||
if (validRes.tag === "errors") {
|
||||
setErrors(validRes.errors);
|
||||
return;
|
||||
}
|
||||
const createSignal = validRes.value;
|
||||
|
||||
const programRes = Program.registerSignal(program, createSignal);
|
||||
if (programRes.tag === "error") {
|
||||
setErrors([{
|
||||
payload: { tag: "Program", err: programRes.error },
|
||||
ids: ["program"],
|
||||
tags: ["footer"],
|
||||
config: { title: "Registration Failed" },
|
||||
}]);
|
||||
return;
|
||||
}
|
||||
const signalName = programRes.value;
|
||||
|
||||
spawnSignalDigith(program, signalName, props.draft.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<article>
|
||||
<header><strong>Signal (Draft)</strong></header>
|
||||
|
||||
<div class="grid">
|
||||
<label>
|
||||
Name
|
||||
<input
|
||||
type="text"
|
||||
placeholder="my_sig"
|
||||
value={name()}
|
||||
onInput={(e) => setName(e.currentTarget.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label>Body</label>
|
||||
<CodeEditor
|
||||
value={body()}
|
||||
onUpdate={setBody}
|
||||
onRun={handleCommit}
|
||||
/>
|
||||
|
||||
<footer>
|
||||
<button class="primary" onClick={handleCommit}>Commit</button>
|
||||
</footer>
|
||||
|
||||
<div style={{ "margin-top": "1rem" }}>
|
||||
<DigithError.ByTag errors={errors()} tag="footer" />
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
153
src/ui/Digith/Signal/SignalDigith.tsx
Normal file
153
src/ui/Digith/Signal/SignalDigith.tsx
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import { createEffect, createSignal, onCleanup, Show } from "solid-js";
|
||||
import { Digith } from "src/ui/Digith";
|
||||
import { useProgram } from "src/ui/ProgramProvider";
|
||||
import { CodeEditor } from "src/ui/Component/CodeEditor";
|
||||
import { sourceText } from "src/lang/parser/source_text";
|
||||
import { Program } from "src/lang/program";
|
||||
import { V, Validation, letValidate } from "src/ui/validation";
|
||||
import { validateSignalExprRaw } from "src/ui/validation/helpers";
|
||||
import { updateDigith } from "src/ui/Scrowl/scrowlStore";
|
||||
import { DigithError } from "src/ui/Digith/DigithError";
|
||||
import { Value } from "src/lang/eval/value";
|
||||
import { Val } from "src/ui/Component/Value";
|
||||
import { externalSubscribe } from "src/lang/eval/signalValue";
|
||||
|
||||
type Input = {
|
||||
raw_body: string,
|
||||
}
|
||||
|
||||
const validator: Validation<Input, Program.UpdateSignal, DigithError> = letValidate(
|
||||
(input) => ({
|
||||
body: V.elseErr(validateSignalExprRaw(input.raw_body), err => ({
|
||||
payload: { tag: "Parse", field: "body", err, src: sourceText(input.raw_body) },
|
||||
ids: ["body"],
|
||||
tags: ["footer"],
|
||||
config: { title: "Signal Body" },
|
||||
})),
|
||||
}),
|
||||
(fields, input) => {
|
||||
return V.ok({
|
||||
body: fields.body,
|
||||
raw_body: input.raw_body
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export function SignalDigith(props: { signal: Digith.Signal }) {
|
||||
const program = useProgram();
|
||||
|
||||
const [value, setValue] = createSignal<Value | null>(null);
|
||||
|
||||
const [body, setBody] = createSignal(props.signal.raw_body);
|
||||
|
||||
const [errors, setErrors] = createSignal<DigithError[]>([]);
|
||||
|
||||
const isDirty = () =>
|
||||
body() !== props.signal.raw_body;
|
||||
|
||||
createEffect(() => {
|
||||
// TODO: Improve runtime error view
|
||||
try {
|
||||
const signal = Program.get_or_create_signal(program, props.signal.name);
|
||||
|
||||
// TODO: Not sure about this one. Setting a signal in `setValue` is discouraged.
|
||||
setTimeout(() => setValue(signal.currentValue), 0);
|
||||
|
||||
const cancel = externalSubscribe(signal, (newValue) => {
|
||||
setValue(newValue);
|
||||
});
|
||||
onCleanup(cancel);
|
||||
} catch (e) {
|
||||
// TODO: setErrors... but how? Shouldn't this be independent of `errors`?
|
||||
console.log("Failed to link Signal: ", e);
|
||||
}
|
||||
});
|
||||
|
||||
function handleRedefine() {
|
||||
setErrors([]);
|
||||
|
||||
const validRes = validator({ raw_body: body() });
|
||||
if (validRes.tag === "errors") {
|
||||
setErrors(validRes.errors);
|
||||
return;
|
||||
}
|
||||
const updateData = validRes.value;
|
||||
|
||||
const progRes = Program.updateSignal(program, props.signal.name, {
|
||||
body: updateData.body,
|
||||
raw_body: updateData.raw_body
|
||||
});
|
||||
|
||||
if (progRes.tag === "error") {
|
||||
setErrors([{
|
||||
payload: { tag: "Program", err: progRes.error },
|
||||
ids: ["program"],
|
||||
tags: ["footer"],
|
||||
config: { title: "Update Failed" },
|
||||
}]);
|
||||
return;
|
||||
}
|
||||
|
||||
// reloading the digith
|
||||
updateDigith(props.signal.id, {
|
||||
...props.signal,
|
||||
raw_body: updateData.raw_body
|
||||
});
|
||||
}
|
||||
// TODO
|
||||
return (
|
||||
<article>
|
||||
<header>
|
||||
<strong>Signal</strong>
|
||||
|
||||
{/* Dirty Indicator / Status */}
|
||||
<div>
|
||||
<Show when={isDirty()} fallback={<span style={{color: "var(--pico-muted-color)"}}>Synced</span>}>
|
||||
<span style={{color: "var(--pico-primary)"}}>● Unsaved Changes</span>
|
||||
</Show>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div>
|
||||
Name
|
||||
<input
|
||||
type="text"
|
||||
value={props.signal.name}
|
||||
disabled
|
||||
style={{ opacity: 0.7, cursor: "not-allowed" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label>Body</label>
|
||||
<CodeEditor
|
||||
value={body()}
|
||||
onUpdate={setBody}
|
||||
onRun={handleRedefine}
|
||||
/>
|
||||
|
||||
<footer style={{ display: "flex", "align-items": "center", gap: "1rem" }}>
|
||||
<button
|
||||
onClick={handleRedefine}
|
||||
disabled={!isDirty()}
|
||||
>
|
||||
Redefine
|
||||
</button>
|
||||
|
||||
</footer>
|
||||
|
||||
<div style={{ "margin-top": "1rem" }}>
|
||||
<DigithError.ByTag errors={errors()} tag="footer" />
|
||||
</div>
|
||||
|
||||
<Show when={ value() } fallback={<div>Initializing...</div>}>
|
||||
{(value) => (
|
||||
<div>
|
||||
<label>Current Value</label>
|
||||
<Val value={ value() } />
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
48
src/ui/Digith/index.tsx
Normal file
48
src/ui/Digith/index.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { FunctionName, SignalName } from "src/lang/expr";
|
||||
import { DigithId } from "src/ui/Scrowl/scrowlStore";
|
||||
|
||||
export type Digith =
|
||||
| Digith.Repl
|
||||
| Digith.NewFunctionDraft
|
||||
| Digith.Function
|
||||
| Digith.NewSignalDraft
|
||||
| Digith.Signal
|
||||
|
||||
|
||||
export namespace Digith {
|
||||
export type Repl = {
|
||||
id: DigithId,
|
||||
tag: "repl",
|
||||
}
|
||||
|
||||
export type NewFunctionDraft = {
|
||||
id: DigithId,
|
||||
tag: "new-fn-draft",
|
||||
raw_name: string,
|
||||
raw_parameters: string,
|
||||
raw_body: string,
|
||||
}
|
||||
|
||||
export type Function = {
|
||||
id: DigithId,
|
||||
tag: "fn",
|
||||
name: FunctionName,
|
||||
raw_parameters: string,
|
||||
raw_body: string,
|
||||
}
|
||||
|
||||
export type NewSignalDraft = {
|
||||
id: DigithId,
|
||||
tag: "new-signal-draft",
|
||||
raw_name: string,
|
||||
raw_body: string,
|
||||
}
|
||||
|
||||
export type Signal = {
|
||||
id: DigithId,
|
||||
tag: "signal",
|
||||
name: SignalName,
|
||||
raw_body: string,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import { createSignal, For, Match, Show, Switch } from "solid-js";
|
||||
import { Digith } from "../Digith";
|
||||
import { useProgram } from "../ProgramProvider";
|
||||
import { CodeEditor } from "../CodeEditor";
|
||||
import { ParseError, parseExpr, parseFunctionName, parseFunctionParameters } from "src/lang/parser/parser";
|
||||
import { sourceText, SourceText } from "src/lang/parser/source_text";
|
||||
import { Expr, FunctionName, ProductPattern } from "src/lang/expr";
|
||||
import { ShowParseError } from "../ParseError";
|
||||
import { Program } from "src/lang/program";
|
||||
import { V, Validation, letValidate } from "../validation";
|
||||
|
||||
// TODO: What about renaming?
|
||||
export function FunctionDigith(props: { function: Digith.Function }) {
|
||||
const program = useProgram();
|
||||
|
||||
const [params, setParams] = createSignal(props.function.raw_parameters);
|
||||
const [body, setBody] = createSignal(props.function.raw_body);
|
||||
|
||||
const handleCommit = () => {
|
||||
// TODO: Update the old function with new code
|
||||
console.log(`Committing ${props.function.name} to the Program...`);
|
||||
};
|
||||
|
||||
return (
|
||||
<article>
|
||||
<header><strong>Fn</strong></header>
|
||||
|
||||
<div>TODO: Fn under construction</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
import { ParseError, parseExpr, parseFunctionName, parseFunctionParameters } from "src/lang/parser/parser";
|
||||
import { sourceText } from "src/lang/parser/source_text";
|
||||
import { Expr, FunctionName, ProductPattern } from "src/lang/expr";
|
||||
import { Program } from "src/lang/program";
|
||||
import { V } from "../validation";
|
||||
|
||||
// === Parser wrappers ===
|
||||
export function validateNameRaw(input: string): V<FunctionName, ParseError> {
|
||||
const src = sourceText(input);
|
||||
const res = parseFunctionName(src);
|
||||
return res.tag === "ok" ? V.ok(res.value) : V.errors([res.error]);
|
||||
};
|
||||
|
||||
export function validateParamsRaw(input: string): V<ProductPattern[], ParseError> {
|
||||
const src = sourceText(input);
|
||||
const res = parseFunctionParameters(src);
|
||||
return res.tag === "ok" ? V.ok(res.value) : V.errors([res.error]);
|
||||
};
|
||||
|
||||
export function validateExprRaw(input: string): V<Expr, ParseError> {
|
||||
const src = sourceText(input);
|
||||
const res = parseExpr(src);
|
||||
return res.tag === "ok" ? V.ok(res.value) : V.errors([res.error]);
|
||||
};
|
||||
|
||||
|
||||
// === Displaying Errors ===
|
||||
|
||||
// TODO: Move this into more appropriate place
|
||||
export function ProgramErrorDisplay(props: { error: Program.Error }) {
|
||||
const message = () => {
|
||||
switch (props.error.tag) {
|
||||
case "DuplicateFunctionName":
|
||||
return `A function named '${props.error.name}' already exists.`;
|
||||
case "PrimitiveFunctionAlreadyExists":
|
||||
return `Cannot overwrite the primitive function '${props.error.name}'.`;
|
||||
// TODO: handle other cases
|
||||
default:
|
||||
return `Runtime Error: ${props.error.tag}`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<article style={{ border: "1px solid var(--pico-del-color)", padding: "0.5rem 1rem" }}>
|
||||
<small style={{ color: "var(--pico-del-color)", "font-weight": "bold" }}>
|
||||
Registration Failed
|
||||
</small>
|
||||
<p style={{ margin: 0 }}>{message()}</p>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,155 +0,0 @@
|
|||
import { createSignal, For, Match, Show, Switch } from "solid-js";
|
||||
import { Digith } from "../Digith";
|
||||
import { useProgram } from "../ProgramProvider";
|
||||
import { CodeEditor } from "../CodeEditor";
|
||||
import { ParseError } from "src/lang/parser/parser";
|
||||
import { sourceText, SourceText } from "src/lang/parser/source_text";
|
||||
import { ShowParseError } from "../ParseError";
|
||||
import { Program } from "src/lang/program";
|
||||
import { V, Validation, letValidate } from "../validation";
|
||||
import { ProgramErrorDisplay, validateExprRaw, validateNameRaw, validateParamsRaw } from "./Helpers";
|
||||
|
||||
|
||||
type NewFnError =
|
||||
| { tag: "Parse", field: "name" | "params" | "body", err: ParseError, src: SourceText }
|
||||
| { tag: "Program", err: Program.Error };
|
||||
|
||||
const fieldLabels: Record<string, string> = {
|
||||
name: "Function Name",
|
||||
params: "Parameters",
|
||||
body: "Function Body"
|
||||
};
|
||||
|
||||
export function SingleErrorDisplay(props: { error: NewFnError }) {
|
||||
return (
|
||||
<div style={{ "margin-bottom": "1rem" }}>
|
||||
<Switch>
|
||||
<Match
|
||||
when={props.error.tag === "Parse" ? (props.error as Extract<NewFnError, { tag: "Parse" }>) : undefined}
|
||||
>
|
||||
{(err) => (
|
||||
<article style={{ border: "1px solid var(--pico-del-color)", padding: "0.5rem 1rem" }}>
|
||||
<header style={{ "margin-bottom": "0.5rem", color: "var(--pico-del-color)", "font-weight": "bold" }}>
|
||||
{fieldLabels[err().field]} Error
|
||||
</header>
|
||||
<ShowParseError text={err().src} err={err().err} />
|
||||
</article>
|
||||
)}
|
||||
</Match>
|
||||
|
||||
<Match
|
||||
when={props.error.tag === "Program" ? (props.error as Extract<NewFnError, { tag: "Program" }>) : undefined}
|
||||
>
|
||||
{(err) => ( <ProgramErrorDisplay error={err().err} />)}
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorListDisplay(props: { errors: NewFnError[] }) {
|
||||
return (
|
||||
<div style={{ "margin-top": "2rem", "border-top": "1px solid var(--pico-muted-border-color)", "padding-top": "1rem" }}>
|
||||
<For each={props.errors}>
|
||||
{(error) => <SingleErrorDisplay error={error} />}
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function NewFunctionDraftDigith(props: { draft: Digith.NewFunctionDraft }) {
|
||||
const program = useProgram();
|
||||
|
||||
const [name, setName] = createSignal(props.draft.raw_name);
|
||||
const [params, setParams] = createSignal(props.draft.raw_parameters);
|
||||
const [body, setBody] = createSignal(props.draft.raw_body);
|
||||
|
||||
const [validResult, setValidResult] = createSignal<V<void, NewFnError> | null>(null);
|
||||
|
||||
type Input = {
|
||||
raw_name: string,
|
||||
raw_params: string,
|
||||
raw_body: string,
|
||||
}
|
||||
|
||||
const validator: Validation<Input, void, NewFnError> = letValidate((input: Input) => ({
|
||||
name: V.elseErr(validateNameRaw(input.raw_name), err => ({ tag: "Parse", field: "name", err, src: sourceText(input.raw_name) })),
|
||||
parameters: V.elseErr(validateParamsRaw(input.raw_params), err => ({ tag: "Parse", field: "params", err, src: sourceText(input.raw_params) })),
|
||||
body: V.elseErr(validateExprRaw(input.raw_body), err => ({ tag: "Parse", field: "body", err, src: sourceText(input.raw_body) })),
|
||||
}),
|
||||
(fields, input) => {
|
||||
const createFunction: Program.CreateFunction = {
|
||||
name: fields.name,
|
||||
parameters: fields.parameters,
|
||||
body: fields.body,
|
||||
raw_parameters: input.raw_name,
|
||||
raw_body: input.raw_body,
|
||||
};
|
||||
|
||||
const regResult = Program.registerFunction(program, createFunction);
|
||||
if (regResult.tag === "ok") {
|
||||
// TODO: Side effects? Not sure about this. Ideally validator would be pure... but it is nice that we can return errors here.
|
||||
// But then again... these are not really normal errors, right? or? But that's probably a misuse...
|
||||
// For now we just return Ok
|
||||
return V.ok(undefined);
|
||||
} else {
|
||||
return V.error({ tag: "Program", err: regResult.error });
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: There's something wrong with this, it doesn't trigger when expected... WTF
|
||||
function handleCommit() {
|
||||
const result = validator({ raw_name: name(), raw_params: params(), raw_body: body() });
|
||||
setValidResult(result);
|
||||
if (result.tag === "ok") {
|
||||
// Handle success closure here if needed
|
||||
console.log("Function created successfully!");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<article>
|
||||
<header><strong>Fn Draft</strong></header>
|
||||
|
||||
<div class="grid">
|
||||
<label>
|
||||
Name
|
||||
<input
|
||||
type="text"
|
||||
placeholder="my_func"
|
||||
value={name()}
|
||||
onInput={(e) => setName(e.currentTarget.value)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Parameters (comma separated)
|
||||
<input
|
||||
type="text"
|
||||
placeholder="x, y"
|
||||
value={params()}
|
||||
onInput={(e) => setParams(e.currentTarget.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label>Body</label>
|
||||
<CodeEditor
|
||||
value={body()}
|
||||
onUpdate={setBody}
|
||||
onRun={handleCommit}
|
||||
/>
|
||||
|
||||
<footer>
|
||||
<button class="primary" onClick={handleCommit}>Commit</button>
|
||||
</footer>
|
||||
|
||||
<Show when={validResult()?.tag === "errors"}>
|
||||
<ErrorListDisplay
|
||||
errors={(validResult() as { tag: "errors", errors: NewFnError[] }).errors}
|
||||
/>
|
||||
</Show>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
10
src/ui/ProgramMeta.tsx
Normal file
10
src/ui/ProgramMeta.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
export function ProgramMeta(props: {}) {
|
||||
return (
|
||||
<aside>
|
||||
<article>
|
||||
<header>Controls</header>
|
||||
</article>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
import { For, Match, Switch } from "solid-js";
|
||||
import { ExprREPL } from "./REPL";
|
||||
import { scrowl } from "./scrowlStore";
|
||||
import { NewFunctionDraftDigith } from "./Function/NewFunctionDraftDigith";
|
||||
import { FunctionDigith } from "./Function/FunctionDigith";
|
||||
import { Digith } from "./Digith";
|
||||
|
||||
// WTF are these names?
|
||||
// Scrowl
|
||||
// - thining about "scrawling", "howl", "crows", "owls", "nests", "scrolling", "soul", "crawling"
|
||||
// Digith
|
||||
// - thinking about fingers/digits. Hand is an object to think with (counting).
|
||||
// - Thinking of a prosthesis that's an extension of your body doing your bidding without you knowing exactly how.
|
||||
// - digital
|
||||
|
||||
type Props = {
|
||||
// TODO
|
||||
}
|
||||
|
||||
export function Scrowl(props: Props) {
|
||||
return (
|
||||
<div
|
||||
id="scrowl"
|
||||
style={{
|
||||
display: "flex",
|
||||
"flex-direction": "column",
|
||||
gap: "1.5rem",
|
||||
}}
|
||||
>
|
||||
<For each={scrowl.digiths}>
|
||||
{(digith) => (
|
||||
<Switch>
|
||||
<Match when={digith.tag === 'repl'}>
|
||||
<article class="digith">
|
||||
<header><strong>REPL</strong></header>
|
||||
<ExprREPL />
|
||||
</article>
|
||||
</Match>
|
||||
|
||||
<Match when={digith.tag === 'new-fn-draft'}>
|
||||
<NewFunctionDraftDigith draft={digith as Digith.NewFunctionDraft} />
|
||||
</Match>
|
||||
|
||||
<Match when={digith.tag === 'fn'}>
|
||||
<FunctionDigith function={digith as Digith.Function} />
|
||||
</Match>
|
||||
</Switch>
|
||||
)}
|
||||
</For>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
90
src/ui/Scrowl/index.tsx
Normal file
90
src/ui/Scrowl/index.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { createEffect, For, Match, onMount, Switch } from "solid-js";
|
||||
import { ExprREPL } from "src/ui/Digith/REPL";
|
||||
import { clearFocus, DigithId, scrowl } from "./scrowlStore";
|
||||
import { NewFunctionDraftDigith } from "src/ui/Digith/Function/NewFunctionDraftDigith";
|
||||
import { FunctionDigith } from "src/ui/Digith/Function/FunctionDigith";
|
||||
import { Digith } from "src/ui/Digith";
|
||||
import { NewSignalDraftDigith } from "../Digith/Signal/NewSignalDraftDigith";
|
||||
import { SignalDigith } from "../Digith/Signal/SignalDigith";
|
||||
|
||||
// WTF are these names?
|
||||
// Scrowl
|
||||
// - thining about "scrawling", "howl", "crows", "owls", "nests", "scrolling", "soul", "crawling"
|
||||
// Digith
|
||||
// - thinking about fingers/digits. Hand is an object to think with (counting).
|
||||
// - Thinking of a prosthesis that's an extension of your body doing your bidding without you knowing exactly how.
|
||||
// - digital
|
||||
|
||||
export function Scrowl() {
|
||||
return (
|
||||
<div
|
||||
id="scrowl"
|
||||
style={{
|
||||
display: "flex",
|
||||
"flex-direction": "column",
|
||||
gap: "1.5rem",
|
||||
}}
|
||||
>
|
||||
<For each={scrowl.digiths}>
|
||||
{(digith) => (
|
||||
<DigithWrapper id={digith.id}>
|
||||
<Switch>
|
||||
<Match when={digith.tag === 'repl'}>
|
||||
<article class="digith">
|
||||
<header><strong>REPL</strong></header>
|
||||
<ExprREPL />
|
||||
</article>
|
||||
</Match>
|
||||
|
||||
<Match when={digith.tag === 'new-fn-draft'}>
|
||||
<NewFunctionDraftDigith draft={digith as Digith.NewFunctionDraft} />
|
||||
</Match>
|
||||
|
||||
<Match when={digith.tag === 'fn'}>
|
||||
<FunctionDigith function={digith as Digith.Function} />
|
||||
</Match>
|
||||
|
||||
<Match when={digith.tag === 'new-signal-draft'}>
|
||||
<NewSignalDraftDigith draft={digith as Digith.NewSignalDraft} />
|
||||
</Match>
|
||||
|
||||
<Match when={digith.tag === 'signal'}>
|
||||
<SignalDigith signal={digith as Digith.Signal} />
|
||||
</Match>
|
||||
</Switch>
|
||||
</DigithWrapper>
|
||||
)}
|
||||
</For>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// For focusing on a Digith
|
||||
// This works basically in two ways:
|
||||
// - We have the list of digiths, and each time any of them is mounted, there's `attemptScroll` check done on all of them.
|
||||
// - Whenever `scrowl.focusTarget` changes, it reactively broadcasts the change to each of the digith,
|
||||
// and each digith asks itself, "Am I focus-target?".
|
||||
function DigithWrapper(props: { id: DigithId, children: any }) {
|
||||
let ref: HTMLDivElement | undefined;
|
||||
|
||||
// The Logic: Run this whenever 'scrowl.focusTarget' changes OR when this component mounts
|
||||
const attemptScroll = () => {
|
||||
if (scrowl.focusTarget === props.id && ref) {
|
||||
ref.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
setTimeout(clearFocus, 0); // This sets asynchronously the focus-target to null.
|
||||
}
|
||||
};
|
||||
|
||||
onMount(attemptScroll);
|
||||
createEffect(attemptScroll);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
style={{ "scroll-margin-top": "2rem" }}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
160
src/ui/Scrowl/scrowlStore.ts
Normal file
160
src/ui/Scrowl/scrowlStore.ts
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import { createStore } from "solid-js/store";
|
||||
import { Digith } from "src/ui/Digith";
|
||||
import { Program } from "src/lang/program";
|
||||
import { FunctionName, SignalName } from "src/lang/expr";
|
||||
|
||||
export type DigithId = number;
|
||||
|
||||
export type Scrowl = {
|
||||
digiths: Digith[],
|
||||
nextId: DigithId,
|
||||
focusTarget: DigithId | null, // 99.99999% of time this is null, but it flickers briefly - and on that flicker focus can happen.
|
||||
}
|
||||
|
||||
export namespace Scrowl {
|
||||
export type Error =
|
||||
| Program.Error
|
||||
|
||||
export type Result<T> =
|
||||
| { tag: "ok", value: T }
|
||||
| { tag: "error", error: Error }
|
||||
|
||||
export namespace Result {
|
||||
export function ok<T>(value: T): Result<T> { return { tag: "ok", value } }
|
||||
export function error<T>(error: Error): Result<T> { return { tag: "error", error } }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const [scrowl, setScrowl] = createStore<Scrowl>({
|
||||
digiths: [{ tag: 'repl', id: 0 }],
|
||||
nextId: 1,
|
||||
focusTarget: null,
|
||||
});
|
||||
|
||||
function prependDigith(newDigith: Digith) {
|
||||
setScrowl("digiths", (prev) => [newDigith, ...prev]);
|
||||
}
|
||||
|
||||
export function updateDigith(targetId: DigithId, newDigith: Digith) {
|
||||
setScrowl("digiths", (items) =>
|
||||
items.map((item) => item.id === targetId ? newDigith : item)
|
||||
);
|
||||
}
|
||||
|
||||
export function closeDigith(targetId: DigithId) {
|
||||
setScrowl("digiths", (prev) => prev.filter((d) => d.id !== targetId));
|
||||
}
|
||||
|
||||
export function closeAllDigiths() {
|
||||
setScrowl("digiths", []);
|
||||
}
|
||||
|
||||
export function requestFocus(id: DigithId) {
|
||||
setScrowl("focusTarget", id);
|
||||
}
|
||||
|
||||
export function clearFocus() {
|
||||
setScrowl("focusTarget", null);
|
||||
}
|
||||
|
||||
function generateId(): DigithId {
|
||||
const id = scrowl.nextId;
|
||||
setScrowl("nextId", (prev) => prev + 1);
|
||||
return id;
|
||||
}
|
||||
|
||||
export function spawnNewFunctionDraftDigith() {
|
||||
const id = generateId();
|
||||
|
||||
const newDraft: Digith = {
|
||||
id,
|
||||
tag: 'new-fn-draft',
|
||||
raw_name: '',
|
||||
raw_parameters: '',
|
||||
raw_body: '',
|
||||
};
|
||||
|
||||
requestFocus(id);
|
||||
setScrowl("digiths", (prev) => [newDraft, ...prev]);
|
||||
};
|
||||
|
||||
export function spawnFunctionDigith(program: Program, name: FunctionName, targetId?: DigithId): Scrowl.Result<Digith.Function> {
|
||||
const lookupRes = Program.getFunction(program, name);
|
||||
if (lookupRes.tag === "error") {
|
||||
return Scrowl.Result.error(lookupRes.error);
|
||||
}
|
||||
const fnDef = lookupRes.value;
|
||||
|
||||
// TODO: Maybe consider representing some read-only Digith for primitive (it would just display the name, it wouldn't have code).
|
||||
if (fnDef.tag === "primitive") {
|
||||
return Scrowl.Result.error({ tag: "CannotEditPrimitiveFunction", name });
|
||||
}
|
||||
const userDef = fnDef.def;
|
||||
const id = targetId ?? generateId();
|
||||
|
||||
const newDigith: Digith.Function = {
|
||||
id: id,
|
||||
tag: "fn",
|
||||
|
||||
name: userDef.name,
|
||||
raw_parameters: userDef.raw_parameters,
|
||||
raw_body: userDef.raw_body,
|
||||
};
|
||||
|
||||
if (targetId === undefined) {
|
||||
prependDigith(newDigith);
|
||||
} else {
|
||||
// Swap with old function draft.
|
||||
updateDigith(targetId, newDigith);
|
||||
}
|
||||
|
||||
return Scrowl.Result.ok(newDigith);
|
||||
}
|
||||
|
||||
export function spawnNewSignalDraftDigith() {
|
||||
const id = generateId();
|
||||
|
||||
const newDraft: Digith = {
|
||||
id,
|
||||
tag: 'new-signal-draft',
|
||||
raw_name: '',
|
||||
raw_body: '',
|
||||
};
|
||||
|
||||
requestFocus(id);
|
||||
setScrowl("digiths", (prev) => [newDraft, ...prev]);
|
||||
};
|
||||
|
||||
export function spawnSignalDigith(program: Program, name: SignalName, targetId?: DigithId): Scrowl.Result<Digith.Signal> {
|
||||
const lookupRes = Program.getSignal(program, name);
|
||||
if (lookupRes.tag === "error") {
|
||||
return Scrowl.Result.error(lookupRes.error);
|
||||
}
|
||||
const sigDef = lookupRes.value;
|
||||
|
||||
// TODO: Maybe consider representing some read-only Digith for primitive (it would just display the name, it wouldn't have code).
|
||||
if (sigDef.tag === "primitive") {
|
||||
return Scrowl.Result.error({ tag: "CannotEditPrimitiveSignal", name });
|
||||
}
|
||||
const userDef = sigDef.def;
|
||||
const id = targetId ?? generateId();
|
||||
|
||||
const newDigith: Digith.Signal = {
|
||||
id: id,
|
||||
tag: "signal",
|
||||
|
||||
name: userDef.name,
|
||||
raw_body: userDef.raw_body,
|
||||
};
|
||||
|
||||
if (targetId === undefined) {
|
||||
prependDigith(newDigith);
|
||||
} else {
|
||||
// Swap with old signal draft.
|
||||
updateDigith(targetId, newDigith);
|
||||
}
|
||||
|
||||
return Scrowl.Result.ok(newDigith);
|
||||
}
|
||||
|
||||
19
src/ui/Sidebar.tsx
Normal file
19
src/ui/Sidebar.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { Controls } from "./Controls/index";
|
||||
import { ProgramMeta } from "./ProgramMeta";
|
||||
|
||||
export function Sidebar() {
|
||||
return (
|
||||
<aside style={{
|
||||
display: "flex",
|
||||
"flex-direction": "column",
|
||||
gap: "2rem", // Space between Controls and the List
|
||||
position: "sticky",
|
||||
top: "2rem",
|
||||
"max-height": "calc(100vh - 4rem)",
|
||||
"overflow-y": "auto" // Allow the list to scroll if it gets too long
|
||||
}}>
|
||||
<Controls />
|
||||
<ProgramMeta />
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import { createStore } from "solid-js/store";
|
||||
import { Digith } from "./Digith";
|
||||
|
||||
export type Scrowl = {
|
||||
digiths: Digith[];
|
||||
}
|
||||
|
||||
export const [scrowl, setScrowl] = createStore<Scrowl>({
|
||||
digiths: [{ tag: 'repl' }]
|
||||
});
|
||||
|
||||
export function spawnFunctionDraft() {
|
||||
const newDraft: Digith = {
|
||||
tag: 'new-fn-draft',
|
||||
raw_name: '',
|
||||
raw_parameters: '',
|
||||
raw_body: '',
|
||||
};
|
||||
setScrowl("digiths", (prev) => [newDraft, ...prev]);
|
||||
};
|
||||
|
||||
30
src/ui/validation/helpers.ts
Normal file
30
src/ui/validation/helpers.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { ParseError, parseExpr, parseFunctionName, parseFunctionParameters, parseSignalExpr } from "src/lang/parser/parser";
|
||||
import { sourceText } from "src/lang/parser/source_text";
|
||||
import { Expr, FunctionName, ProductPattern, SignalExpr } from "src/lang/expr";
|
||||
import { V } from "./";
|
||||
|
||||
// === Parser wrappers ===
|
||||
export function validateNameRaw(input: string): V<FunctionName, ParseError> {
|
||||
const src = sourceText(input);
|
||||
const res = parseFunctionName(src);
|
||||
return res.tag === "ok" ? V.ok(res.value) : V.errors([res.error]);
|
||||
};
|
||||
|
||||
export function validateParamsRaw(input: string): V<ProductPattern[], ParseError> {
|
||||
const src = sourceText(input);
|
||||
const res = parseFunctionParameters(src);
|
||||
return res.tag === "ok" ? V.ok(res.value) : V.errors([res.error]);
|
||||
};
|
||||
|
||||
export function validateExprRaw(input: string): V<Expr, ParseError> {
|
||||
const src = sourceText(input);
|
||||
const res = parseExpr(src);
|
||||
return res.tag === "ok" ? V.ok(res.value) : V.errors([res.error]);
|
||||
};
|
||||
|
||||
export function validateSignalExprRaw(input: string): V<SignalExpr, ParseError> {
|
||||
const src = sourceText(input);
|
||||
const res = parseSignalExpr(src);
|
||||
return res.tag === "ok" ? V.ok(res.value) : V.errors([res.error]);
|
||||
};
|
||||
|
||||
|
|
@ -98,3 +98,4 @@ export function letValidate<A, T extends Record<string, any>, E, B>(
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue