Compare commits

..

10 commits

Author SHA1 Message Date
Yura Dupyn
94df47c738 cleanup 2026-03-02 19:42:04 +01:00
Yura Dupyn
f66b87708d Update Program.udpate to hot-swap 2026-02-18 01:33:46 +01:00
Yura Dupyn
49ef33f113 Finally can run the UI 2026-02-18 01:20:16 +01:00
Yura Dupyn
115b457173 structural equality, booleans, relational barriers for propagation 2026-02-18 00:29:17 +01:00
Yura Dupyn
6cca0d17a1 Suffering. Implemented an initial version of a proper signal runtime. 2026-02-17 22:51:26 +01:00
Yura Dupyn
b9332ad565 track signal dependencies 2026-02-16 20:35:24 +01:00
Yura Dupyn
c0198d419f Make basic Signal Digith work 2026-02-16 19:11:57 +01:00
Yura Dupyn
bf5eb54932 Reorganize ui 2026-02-15 19:26:32 +01:00
Yura Dupyn
e841106029 Improve errors in digiths 2026-02-15 19:11:44 +01:00
Yura Dupyn
b0280b9d74 FunctinDigith and focusTarget scrolling 2026-02-14 22:41:53 +01:00
37 changed files with 1980 additions and 1231 deletions

View file

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

View file

@ -14,6 +14,8 @@ export type RuntimeError =
| { tag: "TypeMismatch", expected: ValueTag, received: Value } | { tag: "TypeMismatch", expected: ValueTag, received: Value }
| { tag: "DuplicateVariableNamesInPattern", pattern: Pattern, duplicates: VariableName[] } | { tag: "DuplicateVariableNamesInPattern", pattern: Pattern, duplicates: VariableName[] }
// | { tag: "DuplicateVariableNamesInProductPattern", pattern: ProductPattern, 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> = export type Result<T> =
| { tag: "ok", value: T } | { tag: "ok", value: T }

View 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;
}
}
}

View file

@ -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;
}

View 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;
}

View file

@ -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
});
}

View file

@ -88,6 +88,7 @@ export namespace Expr {
export namespace SignalExpr { export namespace SignalExpr {
export const read = (name: SignalName, span: Span): SignalExpr => ({ tag: "read", name, span }); 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 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 { export namespace ProductPattern {

View file

@ -2,7 +2,7 @@ import { Cursor } from './cursor';
import { ExprScanError, exprStart, ExprStartToken, IdentifierKind, identifierScanner, isNextTokenExprStart, isNextTokenProductPatternStart, patternStart, PatternStartToken, signalExprStart, SignalExprStartToken, skipWhitespaceAndComments } from './scanner'; import { ExprScanError, exprStart, ExprStartToken, IdentifierKind, identifierScanner, isNextTokenExprStart, isNextTokenProductPatternStart, patternStart, PatternStartToken, signalExprStart, SignalExprStartToken, skipWhitespaceAndComments } from './scanner';
import { char, CodePoint, SourceText, Span } from './source_text'; import { char, CodePoint, SourceText, Span } from './source_text';
import { Result } from '../result'; 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. // 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. // 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: "ExpectedRecordOpen", span: Span } // Expected '(' after ':'
| { tag: "ExpectedLetBlockOpen", span: Span } // Expected '{' after 'let' | { tag: "ExpectedLetBlockOpen", span: Span } // Expected '{' after 'let'
| { tag: "ExpectedLetBlockClose", span: Span } // Expected '}' at end of 'let' expression | { 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: "ExpectedMatchBlockOpen", span: Span } // Expected '{' after 'match'
| { tag: "ExpectedMatchBlockClose", span: Span } // Expected '}' at end of 'match' expression | { tag: "ExpectedMatchBlockClose", span: Span } // Expected '}' at end of 'match' expression
| { tag: "ExpectedLambdaBlockOpen", span: Span } // Expected '{' after `fn` | { tag: "ExpectedLambdaBlockOpen", span: Span } // Expected '{' after `fn`
@ -37,7 +39,7 @@ export type ParseError =
| { tag: "ExpectedApplySeparator", span: Span } // Expected '!' inside 'apply' | { tag: "ExpectedApplySeparator", span: Span } // Expected '!' inside 'apply'
| { tag: "UnexpectedTagPattern", span: Span } // Found #tag where product pattern expected | { tag: "UnexpectedTagPattern", span: Span } // Found #tag where product pattern expected
| { tag: "ExpectedPattern", span: Span } // EOF or invalid start of pattern | { 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 | { tag: "ExpectedRecordField", span: Span }; // Expected identifier in record pattern
// TODO: Delete? // TODO: Delete?
@ -50,6 +52,8 @@ export type Expectation =
| "ExpectedRecordOpen" | "ExpectedRecordOpen"
| "ExpectedLetBlockOpen" | "ExpectedLetBlockOpen"
| "ExpectedLetBlockClose" | "ExpectedLetBlockClose"
| "ExpectedLetSignalBlockOpen"
| "ExpectedLetSignalBlockClose"
| "ExpectedMatchBlockOpen" | "ExpectedMatchBlockOpen"
| "ExpectedMatchBlockClose" | "ExpectedMatchBlockClose"
| "ExpectedApplyStart" | "ExpectedApplyStart"
@ -314,6 +318,7 @@ function expr(cursor: Cursor): Expr {
function signalExpr(cursor: Cursor): SignalExpr { function signalExpr(cursor: Cursor): SignalExpr {
const start = cursor.currentLocation(); const start = cursor.currentLocation();
const token = signalExprStartToken(cursor); const token = signalExprStartToken(cursor);
switch (token.tag) { switch (token.tag) {
case "EOF": case "EOF":
throw { throw {
@ -323,41 +328,27 @@ function signalExpr(cursor: Cursor): SignalExpr {
} as ParseError; } as ParseError;
case "signal_read": case "signal_read":
return SignalExpr.read(token.name, token.span); 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": case "keyword":
switch (token.kw) { switch (token.kw) {
case "let-signal": case "let-signal":
// TODO: // let { x := sig-expr, y := sig-expr . normal-expr }
// // let { p0 = e0, p1 = e2 . body } // TODO: Decide if to introduce new keyword `:=` or just reuse `=`?
// if (!tryConsume(cursor, char('{'))) { if (!tryConsume(cursor, char('{'))) {
// throw { throw {
// tag: "ExpectedLetBlockOpen", tag: "ExpectedLetSignalBlockOpen",
// span: cursor.makeSpan(cursor.currentLocation()) span: cursor.makeSpan(cursor.currentLocation())
// } as ParseError; } as ParseError;
// } }
// const bindings = delimitedTerminalSequence(cursor, DELIMITER_COMMA, TERMINATOR_DOT, productPatternBinding); const bindings = delimitedTerminalSequence(cursor, DELIMITER_COMMA, TERMINATOR_DOT, productPatternSignalBinding);
// const body = expr(cursor); const body = expr(cursor);
// if (!tryConsume(cursor, TERMINATOR_CLOSE_BRACE)) { if (!tryConsume(cursor, TERMINATOR_CLOSE_BRACE)) {
// throw { throw {
// tag: "ExpectedLetBlockClose", tag: "ExpectedLetSignalBlockClose",
// span: cursor.makeSpan(cursor.currentLocation()) span: cursor.makeSpan(cursor.currentLocation())
// } as ParseError; } as ParseError;
// } }
// return Expr.let_(bindings, body, cursor.makeSpan(start)); return SignalExpr.let_(bindings, body, cursor.makeSpan(start));
return 0 as any;
case "let": case "let":
case "fn": case "fn":
case "match": case "match":
@ -415,6 +406,20 @@ function productPatternBinding(cursor: Cursor): ExprBinding {
return Expr.exprBinding(pattern, e, cursor.makeSpan(start)); 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 { function fieldAssignment(cursor: Cursor): FieldAssignment {
const start = cursor.currentLocation(); const start = cursor.currentLocation();
// `f = e` // `f = e`

View file

@ -55,6 +55,7 @@ export type ExprScanError =
| NumberError | NumberError
| StringError | StringError
| { tag: "InvalidIdentifier", text: string, kind: IdentifierKind, reason: IdentifierErrorReason, span: Span } | { 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? // What kind of identifier were we trying to parse?
export type IdentifierKind = export type IdentifierKind =
@ -268,9 +269,11 @@ export function signalExprStart(cursor: Cursor): SignalExprStartToken {
case "keyword": case "keyword":
return result; return result;
case "identifier": case "identifier":
// TODO: when we have parametrized signal-expressions throw ({
// return { tag: "function_name", name: result.name, span: result.span }; tag: "UnexpectedIdentifier",
return 0 as any; identifier: result.name,
span: result.span,
} as ExprScanError);
} }
} }
@ -349,9 +352,6 @@ export function isNextTokenExprStart(cursor: Cursor): boolean {
case "EOF": case "EOF":
return false; return false;
default:
return false;
} }
} catch (e) { } catch (e) {
@ -386,7 +386,8 @@ export function isNextTokenProductPatternStart(cursor: Cursor): boolean {
case "!": case "!":
return false; return false;
} }
default: case "tag":
case "EOF":
return false; return false;
} }
} catch (e) { } catch (e) {

View file

@ -1,12 +1,14 @@
import { eval_signal_expression, makeTickSignal, signal, Signal, SignalId, SignalRuntime } from "./eval/signal";
import { ThrownRuntimeError } from "./eval/error"; import { ThrownRuntimeError } from "./eval/error";
import { Env, Value } from "./eval/value"; import { Env, Value } from "./eval/value";
import { Expr, FunctionName, SignalName, ProductPattern, SignalExpr } from "./expr"; import { Expr, FunctionName, SignalName, ProductPattern, SignalExpr } from "./expr";
import { installPrimitives } from "./primitive"; import { installPrimitives } from "./primitive";
import { eval_expr } from "./eval/evaluator"; import { eval_expr } from "./eval/evaluator";
import { eval_signal_expression, spawnTick, spawnSignal, SignalRuntime, getNode, spawnSource, hotSwapSignal } from "./eval/signalValue"
export type Timestamp = number; export type Timestamp = number;
type SignalId = SignalRuntime.DAGNode;
export type Program = { export type Program = {
function_definitions: Map<FunctionName, FunctionDefinition>, function_definitions: Map<FunctionName, FunctionDefinition>,
function_definition_order: FunctionName[], function_definition_order: FunctionName[],
@ -70,7 +72,7 @@ type CellDefinition = {
body: Expr, body: Expr,
signalId?: SignalId, cell?: [SignalId, (value: Value) => void],
// Metadata // Metadata
createdAt: Timestamp, createdAt: Timestamp,
@ -78,7 +80,7 @@ type CellDefinition = {
} }
type PrimitiveSignalDefinition = { type PrimitiveSignalDefinition = {
name: FunctionName, name: SignalName,
signalId: SignalId, signalId: SignalId,
} }
@ -162,11 +164,11 @@ export namespace Program {
} }
function install_primitive_signals(program: 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>) { function install_primitive_signal(program: Program, name: SignalName, signalId: SignalId) {
const signalId = attachNewSignal(program, signal);
const def: SignalDefinition = { const def: SignalDefinition = {
tag: "primitive", tag: "primitive",
def: { name, signalId } def: { name, signalId }
@ -175,19 +177,14 @@ export namespace Program {
} }
// may throw `ThrownRuntimeError`. This is used during initialization. // 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); const sigDef = lookup_signal_definition(program, name);
switch (sigDef.tag) { switch (sigDef.tag) {
case "user": { case "user": {
const def = sigDef.def; const def = sigDef.def;
if (def.signalId !== undefined) { if (def.signalId !== undefined) {
const signal = SignalRuntime.getSignal(program.signal_runtime, def.signalId); return def.signalId;
if (signal === undefined) {
throw ThrownRuntimeError.error({ tag: "SignalDefinitionHasSignalIdWithoutSignal", name, });
} else {
return signal;
}
} }
// We need to create the signal for the first time. // We need to create the signal for the first time.
@ -197,90 +194,28 @@ export namespace Program {
def.is_initializing = true; def.is_initializing = true;
try { try {
const newSignal = eval_signal_expression(program, Env.nil(), def.body); const newId = spawnSignal(program, name, def.body);
const newId = attachNewSignal(program, newSignal);
program.signal_runtime.store.set(newId, newSignal);
def.signalId = newId; def.signalId = newId;
return newSignal; return newId;
} finally { } finally {
def.is_initializing = false; def.is_initializing = false;
} }
} }
case "cell": { case "cell": {
const def = sigDef.def; const def = sigDef.def;
if (def.signalId !== undefined) { if (def.cell !== undefined) {
const signal = SignalRuntime.getSignal(program.signal_runtime, def.signalId); return def.cell[0];
if (signal === undefined) {
throw ThrownRuntimeError.error({ tag: "SignalDefinitionHasSignalIdWithoutSignal", name, });
} else {
return signal;
}
} }
// We need to create the cell-signal for the first time. // We need to create the cell-signal for the first time.
const initialValue = eval_expr(program, Env.nil(), def.body); const initialValue = eval_expr(program, Env.nil(), def.body);
const sig = signal(initialValue); const cell = spawnSource(program, name, initialValue)
const id = attachNewSignal(program, sig); def.cell = cell;
def.signalId = id; return cell[0];
return sig;
} }
case "primitive": { case "primitive": {
const def = sigDef.def; return sigDef.def.signalId;
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;
}
} }
} }
} }
@ -294,10 +229,18 @@ export namespace Program {
raw_body: string, 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( export function registerFunction(
program: Program, program: Program,
{ name, body, parameters, raw_parameters, raw_body }: CreateFunction { name, body, parameters, raw_parameters, raw_body }: CreateFunction
): Result<void> { ): Result<FunctionName> {
if (program.function_definitions.has(name)) { if (program.function_definitions.has(name)) {
return Result.error({ tag: "DuplicateFunctionName", 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_definitions.set(name, { tag: "user", def: newFunction });
program.function_definition_order.push(name); program.function_definition_order.push(name);
return Result.ok(undefined); return Result.ok(name);
} }
export type UpdateFunction = { export type UpdateFunction = {
parameters: ProductPattern[], parameters: ProductPattern[],
body: Expr, body: Expr,
raw_name: string,
raw_parameters: string, raw_parameters: string,
raw_body: string, raw_body: string,
} }
@ -370,6 +312,14 @@ export namespace Program {
} }
// === Signals === // === 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 = { export type CreateSignal = {
name: SignalName, name: SignalName,
body: SignalExpr, body: SignalExpr,
@ -379,7 +329,7 @@ export namespace Program {
export function registerSignal( export function registerSignal(
program: Program, program: Program,
{ name, body, raw_body }: CreateSignal { name, body, raw_body }: CreateSignal
): Result<void> { ): Result<SignalName> {
if (program.signal_definitions.has(name)) { if (program.signal_definitions.has(name)) {
return Result.error({ tag: "DuplicateSignalName", 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_definitions.set(name, { tag: "user", def: newSignal });
program.signal_definition_order.push(name); 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` // For that we will use `get_or_create_signal`
return Result.ok(undefined); return Result.ok(name);
} }
export type UpdateSignal = { export type UpdateSignal = {
@ -411,7 +361,7 @@ export namespace Program {
raw_body: string, raw_body: string,
} }
export function updateSignal( export function updateSignal(
program: Program, program: Program,
name: SignalName, name: SignalName,
{ body, raw_body }: UpdateSignal { body, raw_body }: UpdateSignal
@ -435,45 +385,40 @@ export function updateSignal(
def.raw_body = raw_body; def.raw_body = raw_body;
def.lastModifiedAt = Date.now(); def.lastModifiedAt = Date.now();
// TODO: When to recompile? if (def.signalId !== undefined) {
// 2. CRITICAL: Invalidate the Runtime Cache hotSwapSignal(program, def.signalId, body);
// 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;
return Result.ok(undefined); return Result.ok(undefined);
} }
export function deleteSignal(program: Program, name: SignalName): Result<void> { // TODO: This needs careful thought about transitive dependencies of a signal
const existingEntry = program.signal_definitions.get(name); // export function deleteSignal(program: Program, name: SignalName): Result<void> {
// const existingEntry = program.signal_definitions.get(name);
if (!existingEntry) { // if (!existingEntry) {
return Result.error({ tag: "SignalNotFound", name } as any); // return Result.error({ tag: "SignalNotFound", name } as any);
} // }
if (existingEntry.tag === "primitive") { // if (existingEntry.tag === "primitive") {
return Result.error({ tag: "CannotDeletePrimitiveSignal", name } as any); // 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); // const orderIndex = program.signal_definition_order.indexOf(name);
if (orderIndex !== -1) { // if (orderIndex !== -1) {
program.signal_definition_order.splice(orderIndex, 1); // program.signal_definition_order.splice(orderIndex, 1);
} // }
// TODO: // // TODO:
// Note: The old signal instance still exists in program.signal_runtime.store // // 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. // // We technically leak memory here unless we also remove it from the runtime store.
// However, since other signals might still depend on that ID, // // However, since other signals might still depend on that ID,
// leaving it is actually safer for now to prevent crashes. // // leaving it is actually safer for now to prevent crashes.
return Result.ok(undefined); // return Result.ok(undefined);
} // }
// === Cells === // === Cells ===
export type CreateCell = { export type CreateCell = {
@ -482,34 +427,35 @@ export function updateSignal(
raw_body: string, raw_body: string,
} }
// TODO: refactor pending
export function registerCell( export function registerCell(
program: Program, program: Program,
{ name, body, raw_body }: CreateCell { name, body, raw_body }: CreateCell
): Result<void> { ): Result<void> {
if (program.signal_definitions.has(name)) { // if (program.signal_definitions.has(name)) {
return Result.error({ tag: "DuplicateSignalName", 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. // // 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 initialValue = eval_expr(program, Env.nil(), body);
const sig = signal(initialValue); // const sig = signal(initialValue);
const signalId = attachNewSignal(program, sig); // const signalId = attachNewSignal(program, sig);
const newCell: CellDefinition = { // const newCell: CellDefinition = {
name, // name,
raw_body, // raw_body,
body, // body,
signalId, // signalId,
createdAt: now, // createdAt: now,
lastModifiedAt: now, // lastModifiedAt: now,
}; // };
program.signal_definitions.set(name, { tag: "cell", def: newCell }); // program.signal_definitions.set(name, { tag: "cell", def: newCell });
program.signal_definition_order.push(name); // program.signal_definition_order.push(name);
return Result.ok(undefined); return Result.ok(undefined);
} }
@ -519,44 +465,45 @@ export function updateSignal(
raw_body: string, raw_body: string,
} }
// TODO: refactor pending
export function updateCell( export function updateCell(
program: Program, program: Program,
name: SignalName, name: SignalName,
{ body, raw_body }: UpdateCell { body, raw_body }: UpdateCell
): Result<void> { ): Result<void> {
const existingEntry = program.signal_definitions.get(name); // const existingEntry = program.signal_definitions.get(name);
if (!existingEntry) { // if (!existingEntry) {
return Result.error({ tag: "SignalNotFound", name } as any); // return Result.error({ tag: "SignalNotFound", name } as any);
} // }
// Ensure we are editing a Cell // // Ensure we are editing a Cell
if (existingEntry.tag !== "cell") { // if (existingEntry.tag !== "cell") {
return Result.error({ tag: "CannotEditCell", name } as any); // 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. // // 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); // const newValue = eval_expr(program, Env.nil(), body);
// 2. Find the existing runtime signal // // Find the existing runtime signal
if (def.signalId === undefined) { // if (def.signalId === undefined) {
// This should theoretically not happen for cells since we initialize them eagerly, // // This should theoretically not happen for cells since we initialize them eagerly,
// but good to be safe. // // but good to be safe.
return Result.error({ tag: "SignalDefinitionHasSignalIdWithoutSignal", name } as any); // return Result.error({ tag: "SignalDefinitionHasSignalIdWithoutSignal", name } as any);
} // }
const sig = SignalRuntime.getSignal(program.signal_runtime, def.signalId); // const sig = SignalRuntime.getSignal(program.signal_runtime, def.signalId);
if (!sig) { // if (!sig) {
return Result.error({ tag: "SignalDefinitionHasSignalIdWithoutSignal", name } as any); // return Result.error({ tag: "SignalDefinitionHasSignalIdWithoutSignal", name } as any);
} // }
sig.set(() => newValue); // sig.set(() => newValue);
def.body = body; // def.body = body;
def.raw_body = raw_body; // def.raw_body = raw_body;
def.lastModifiedAt = Date.now(); // def.lastModifiedAt = Date.now();
return Result.ok(undefined); return Result.ok(undefined);
} }

View file

@ -1,6 +1,5 @@
import { createSignal } from 'solid-js';
import { Scrowl } from './Scrowl'; import { Scrowl } from './Scrowl';
import { Controls } from './Controls'; import { Sidebar } from './Sidebar';
export default function App() { export default function App() {
return ( return (
@ -14,7 +13,7 @@ export default function App() {
}} }}
> >
<Scrowl /> <Scrowl />
<Controls /> <Sidebar />
</main> </main>
); );
} }

View file

@ -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 }) { export function Expression(prop: { expr: Expr }) {
return ( return (

View file

@ -4,12 +4,6 @@ import { DisplayLineViews } from "./LineView";
export function formatErrorMesage(err: ParseError): string { export function formatErrorMesage(err: ParseError): string {
switch (err.tag) { 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": case "UnexpectedCharacter":
return `Unexpected character: ${formatChar(err.char)}`; return `Unexpected character: ${formatChar(err.char)}`;
@ -20,13 +14,12 @@ export function formatErrorMesage(err: ParseError): string {
return "Expected a number here."; return "Expected a number here.";
case "InvalidNumber": case "InvalidNumber":
return err.reason === "NotFinite" switch (err.reason) {
? "Number is too large or invalid." case "NotFinite":
: "Invalid number format (missing fractional digits?)."; return "Number is too large or invalid.";
case "MissingFractionalDigits":
case "InvalidIdentifier": return "Invalid number format (missing fractional digits?).";
// Handle nested reasons if needed, e.g. "Keyword 'let' cannot be used as an identifier" }
return `Invalid identifier '${err.text}': ${err.reason.tag}`;
case "InvalidEscape": case "InvalidEscape":
switch (err.reason.tag) { 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 "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 // Context specific errors
case "ExpectedExpression": return "Expected an expression here."; case "ExpectedExpression": return "Expected an expression here.";
case "ExpectedSignalExpression": return "Expected a signal expression here.";
case "ExpectedFieldAssignmentSymbol": return "Expected '=' for field assignment."; case "ExpectedFieldAssignmentSymbol": return "Expected '=' for field assignment.";
case "ExpectedPatternAssignmentSymbol": return "Expected '=' for pattern assignment."; case "ExpectedPatternAssignmentSymbol": return "Expected '=' for pattern assignment.";
case "ExpectedPatternBindingSymbol": return "Expected '.' for pattern binding."; 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 "ExpectedRecordOpen": return "Expected '(' to start record.";
case "ExpectedLetBlockOpen": return "Expected '{' to start let-block."; case "ExpectedLetBlockOpen": return "Expected '{' to start let-block.";
case "ExpectedLetBlockClose": return "Expected '}' to close 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 "ExpectedMatchBlockOpen": return "Expected '{' to start match-block.";
case "ExpectedMatchBlockClose": return "Expected '}' to close match-block."; case "ExpectedMatchBlockClose": return "Expected '}' to close match-block.";
case "ExpectedLambdaBlockOpen": return "Expected '{' to start lambda body."; 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 "ExpectedApplySeparator": return "Expected '!' inside 'apply'.";
case "UnexpectedTagPattern": return "Unexpected tag pattern (expected product pattern)."; case "UnexpectedTagPattern": return "Unexpected tag pattern (expected product pattern).";
case "ExpectedPattern": return "Expected a pattern here."; 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."; case "ExpectedRecordField": return "Expected a field name in record pattern.";
default:
return `Unknown error: ${(err as any).tag}`;
} }
} }

View file

@ -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'; import { exprToString, productPatternToString } from './Expr';
export function Val(prop: { value: Value }) { export function Val(prop: { value: Value }) {

View file

@ -1,4 +1,4 @@
import { spawnFunctionDraft } from "./scrowlStore"; import { spawnNewFunctionDraftDigith, spawnNewSignalDraftDigith } from "src/ui/Scrowl/scrowlStore";
type Props = { type Props = {
// TODO // TODO
@ -6,20 +6,14 @@ type Props = {
export function Controls(props: Props) { export function Controls(props: Props) {
return ( return (
<aside <aside>
style={{
position: "sticky",
top: "2rem",
height: "fit-content"
}}
>
<article> <article>
<header>Controls</header> <header>Controls</header>
<div style={{ display: "flex", gap: "0.5rem", "flex-wrap": "wrap" }}> <div style={{ display: "flex", gap: "0.5rem", "flex-wrap": "wrap" }}>
<button <button
class="outline secondary" class="outline secondary"
onClick={spawnFunctionDraft} onClick={spawnNewFunctionDraftDigith}
style={{ padding: "2px 8px", "font-size": "0.8rem", width: "auto" }} style={{ padding: "2px 8px", "font-size": "0.8rem", width: "auto" }}
> >
+fn +fn
@ -27,8 +21,8 @@ export function Controls(props: Props) {
<button <button
class="outline secondary" class="outline secondary"
onClick={spawnNewSignalDraftDigith}
style={{ padding: "2px 8px", "font-size": "0.8rem", width: "auto" }} style={{ padding: "2px 8px", "font-size": "0.8rem", width: "auto" }}
disabled
> >
+signal +signal
</button> </button>

View file

@ -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
}
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View file

@ -1,13 +1,13 @@
import { createSignal, Match, Switch } from 'solid-js'; 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 { eval_start } from 'src/lang/eval/evaluator';
import { Value } from 'src/lang/eval/value'; import { Value } from 'src/lang/eval/value';
import { RuntimeError } from 'src/lang/eval/error'; import { RuntimeError } from 'src/lang/eval/error';
import { SourceText, sourceText } from 'src/lang/parser/source_text'; import { SourceText, sourceText } from 'src/lang/parser/source_text';
import { ParseError, parseExpr } from 'src/lang/parser/parser'; import { ParseError, parseExpr } from 'src/lang/parser/parser';
import { ShowParseError } from './ParseError'; import { ShowParseError } from 'src/ui/Component/ParseError';
import { Val } from './Value'; import { Val } from 'src/ui/Component/Value';
import { CodeEditor } from './CodeEditor'; import { CodeEditor } from 'src/ui/Component/CodeEditor';
namespace ReplResult { namespace ReplResult {
export type Idle = export type Idle =

View 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>
);
}

View 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
View 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,
}
}

View file

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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
View file

@ -0,0 +1,10 @@
export function ProgramMeta(props: {}) {
return (
<aside>
<article>
<header>Controls</header>
</article>
</aside>
);
}

View file

@ -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
View 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>
);
}

View 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
View 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>
);
}

View file

@ -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]);
};

View 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]);
};

View file

@ -98,3 +98,4 @@ export function letValidate<A, T extends Record<string, any>, E, B>(
} }
}; };
} }