Initial signal runtime
This commit is contained in:
parent
8b02e3e7d1
commit
94cb3bd721
7 changed files with 900 additions and 35 deletions
86
src/lang/SIGNAL-EXPERIMENT.md
Normal file
86
src/lang/SIGNAL-EXPERIMENT.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
|
||||
```javascript
|
||||
const Thing = (initState) => ({
|
||||
state: initState,
|
||||
subscribers: [],
|
||||
set(transform) {
|
||||
const state = transform(this.state);
|
||||
this.state = state;
|
||||
this.subscribers.forEach(f => {
|
||||
f(state);
|
||||
});
|
||||
},
|
||||
get() {
|
||||
return this.state;
|
||||
},
|
||||
subscribe(f) {
|
||||
this.subscribers.push(f);
|
||||
},
|
||||
map(transform) {
|
||||
const Y = Thing(this.state);
|
||||
this.subscribe(x => {
|
||||
Y.set(() => transform(x));
|
||||
});
|
||||
|
||||
return Y;
|
||||
},
|
||||
});
|
||||
|
||||
function pair(X, Y) {
|
||||
const Z = Thing([X.get(), Y.get()]);
|
||||
X.subscribe(x => {
|
||||
Z.set(() => [x, Y.get()]);
|
||||
});
|
||||
Y.subscribe(y => {
|
||||
Z.set(() => [X.get(), y]);
|
||||
});
|
||||
return Z;
|
||||
}
|
||||
|
||||
// Comonad lift
|
||||
// Signal(a), (Signal(a) -> b) -> Signal(b)
|
||||
|
||||
// X: Signal(A), f: Signal(A) -> B
|
||||
// Y: Signal(B)
|
||||
// function extend(X, f) {
|
||||
// const y0 = f(X);
|
||||
// const Y = Thing(y0);
|
||||
// X.subscribe(x => {
|
||||
// Y.set(f(???)); I need to somehow feed it a new signal...
|
||||
// // TODO: Ofcourse I can feed it `X` again, but that feels wrong... I thought
|
||||
// });
|
||||
// return Y;
|
||||
// }
|
||||
|
||||
const count = Thing(0);
|
||||
console.log("COUNT EXISTS");
|
||||
|
||||
console.log(count.get())
|
||||
|
||||
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);
|
||||
```
|
||||
485
src/lang/SIGNAL.md
Normal file
485
src/lang/SIGNAL.md
Normal file
|
|
@ -0,0 +1,485 @@
|
|||
|
||||
# Language Design: Two Worlds
|
||||
|
||||
The language into two fragments: Normal Expressions vs Signal Expressions.
|
||||
- The Normal (Values) world is the standard (call-by-value) functional programming.
|
||||
- The Signal (Reactive) world declares connections between time-varying signals.
|
||||
|
||||
The basic constraint is: Normal world has no conception of Signals. We can't name in a normal world a signal, we can't create signals, we can't pass them to functions or return signals from functions.
|
||||
|
||||
# Applicative Structure
|
||||
Basic Signal expression is `@foo` which we can think of as reading (and being dependent on) a signal `foo`.
|
||||
Then we have another Signal expression `let-signal` which is the applicative structure. It is the only construct that connects the Normal world to the Signal world.
|
||||
It allows embedding of Normal Expressions into Signal Expressions. Recall applicative structure
|
||||
```
|
||||
Signal(A), Signal(B), (A, B -> C) -> Signal(C)
|
||||
```
|
||||
we directly adopt this into
|
||||
```
|
||||
let-signal {
|
||||
// SIGNAL WORLD (RHS of :=)
|
||||
// We extract values from the graph.
|
||||
x := signal-expr-1,
|
||||
y := signal-expr-2
|
||||
.
|
||||
// NORMAL WORLD (Body)
|
||||
// We compute a new value using the extracted data.
|
||||
// $x and $y are plain values here.
|
||||
normal-expression($x, $y)
|
||||
}
|
||||
```
|
||||
For example given a number signal `count`, we can create a signal that doubles it by
|
||||
```
|
||||
let-signal {
|
||||
x := @count
|
||||
. *($x, 2)
|
||||
}
|
||||
```
|
||||
|
||||
We also have a constant signal expression `const e` where `e` is a normal expression.
|
||||
|
||||
Note that a normal expression can't contain `@` nor `let-expr`.
|
||||
|
||||
# Top-Level Constructs
|
||||
|
||||
We introduce two new top-level constructs, via keywords `signal` and `fn-signal`.
|
||||
|
||||
The following
|
||||
```
|
||||
signal double {
|
||||
let-signal {
|
||||
x := @count
|
||||
. *($x, 2)
|
||||
}
|
||||
}
|
||||
```
|
||||
defines a new top-level signal named `double` and starts it up.
|
||||
General syntax
|
||||
```
|
||||
signal sigName {
|
||||
signal-expression
|
||||
}
|
||||
```
|
||||
|
||||
We also have parametrised top-level signal expressions. These can be used to define UI components
|
||||
```
|
||||
fn-signal Counter(color) {
|
||||
let-signal {
|
||||
// We must hardwire to global signals or internal state
|
||||
n := @global-count
|
||||
.
|
||||
<div style={ :( color = $color ) }>
|
||||
{ $n }
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
More generally
|
||||
```
|
||||
fn-signal ParametrizedSignalName(x1, x2, x3) {
|
||||
signal-expression // $x1, $x2, $x3 are normal values
|
||||
}
|
||||
```
|
||||
|
||||
Here is how we can define App Root
|
||||
```
|
||||
signal App {
|
||||
let-signal {
|
||||
// SIGNAL WORLD: Instantiate the component
|
||||
// We pass "red" (Normal Expr), not a signal.
|
||||
main-counter := Counter("red")
|
||||
.
|
||||
// NORMAL WORLD: Compose the HTML
|
||||
<div id="app">
|
||||
<h1>My App</h1>
|
||||
{ $main-counter }
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
|
||||
# Runtime Constraints & Implementation
|
||||
|
||||
- Static Topology? We should be able to know the dependency graph between signals at compile-time.
|
||||
- Rank-based Update, topological-srot on graph once at startup. Each node gets a rank (how close it is to the root signal).
|
||||
Updates ar eprocessed via priority-queue - low-rank dependencies are processed earlier
|
||||
|
||||
|
||||
|
||||
# TODO: Linear Types
|
||||
|
||||
What exactly is the meaning of the below?
|
||||
Seems a bit weird that the signal `state` can be accessed multiple times like this.
|
||||
```
|
||||
let-signal {
|
||||
x0 := @state,
|
||||
x1 := @state,
|
||||
. ...
|
||||
}
|
||||
It would make sense to me if we sampled once, then duplicated the value we got...
|
||||
let-signal {
|
||||
x := @state,
|
||||
x0 := $x,
|
||||
x1 := $x,
|
||||
. ...
|
||||
}
|
||||
```
|
||||
But then again, I guess it does make sense to sample twice - but there's no guarantee of getting the same result... e.g.
|
||||
```
|
||||
@ : Signal(A) -> Signal(A), A
|
||||
```
|
||||
the signal read operation may potentially change the signal itself.
|
||||
|
||||
|
||||
|
||||
-----------------------
|
||||
|
||||
This is Solidjs. How does it work?
|
||||
|
||||
```
|
||||
function Counter() {
|
||||
const [ count, setCount ] = createSignal(0);
|
||||
return (
|
||||
<button onClick={ () => setCount(n => n + 1) }>
|
||||
Count: { count() }
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
let-signal foo = x0;
|
||||
@foo // read
|
||||
@foo := x1 // write
|
||||
|
||||
@foo { x . body }
|
||||
|
||||
|
||||
function Counter() {
|
||||
let-signal count = 0;
|
||||
|
||||
<button onClick={ e => @count { c . c + 1 } } >
|
||||
Count: { @count } // how can only this part be re-rendered? How is this communicated?
|
||||
</button>
|
||||
}
|
||||
|
||||
|
||||
|
||||
let count = 0
|
||||
let double = count * 2
|
||||
|
||||
count = 11
|
||||
console.log(double) // 0
|
||||
|
||||
|
||||
|
||||
// ===Count===
|
||||
let-signal count = 0;
|
||||
|
||||
//asignment
|
||||
@count := 1
|
||||
@count := 2
|
||||
|
||||
// subscription
|
||||
@count { x .
|
||||
console.log(x);
|
||||
}
|
||||
|
||||
// POSSIBILITY 1: manual...
|
||||
let-signal double = @count
|
||||
@count { x .
|
||||
@double := 2*x
|
||||
}
|
||||
|
||||
// POSSIBILITY 2: automatic
|
||||
let-signal??? double = 2*@count
|
||||
// but where' sthe dependency on count? Is it just auto?
|
||||
// I guess you can just analyze the code and see that this deopends on the count...
|
||||
// but what if I wanted to just initialize `double` with the current value of the @count signal...
|
||||
//
|
||||
// maybe that's not how signals should be used...
|
||||
// if a signal is used in a thing - that thing must change according to the signal - no other way?
|
||||
```
|
||||
|
||||
|
||||
What can we do?
|
||||
- Creation: We can introduce a new signal
|
||||
- Reading/Subsribing: We can read the current value of a signal
|
||||
- should we then automatically change with that signal?
|
||||
Are we dependent on it?
|
||||
This is like we can read only by subscription.
|
||||
- Updating: If we have the capability, we can update the signal to new value (can be a function of the current value)
|
||||
- ???Subscription: We can subscribe to changes in the signal
|
||||
- Map: We can create new signals that are a function of the original signal:
|
||||
But this is a bit weird... in this output signal we can still set it...
|
||||
Maybe we should be able to create only read-only signals?
|
||||
|
||||
|
||||
Read/Write Signal
|
||||
ReadOnly Signal (from map?)
|
||||
|
||||
What about stuff like `andThen`?
|
||||
|
||||
```
|
||||
Signal(A)
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```
|
||||
type Model
|
||||
create initState: Model
|
||||
type Msg
|
||||
create update : Msg, Model -> Model
|
||||
|
||||
const { dispatchMsg: (Msg) -> Void } = useElm(initState, update)
|
||||
```
|
||||
|
||||
how to call the combination of a signal together with the events?
|
||||
|
||||
|
||||
|
||||
=== Tracking Scopes ===
|
||||
|
||||
When are tracking-scopes created?
|
||||
```
|
||||
// effects
|
||||
createEffect(() => {
|
||||
...
|
||||
})
|
||||
|
||||
// returning jsx from a component
|
||||
// would be better if we had to actually wrap it explicitely into something.
|
||||
return (
|
||||
<div>
|
||||
...
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
|
||||
Nesting tracking-scopes seem complex.
|
||||
We need some sort of a stack for tracking effects.
|
||||
And when a child effect is pushed, it needs to say to its parent (current top-of-stack),
|
||||
that, "hey, I'm yo kid, please clean me up when u refresh"
|
||||
|
||||
|
||||
TODO:
|
||||
|
||||
|
||||
```
|
||||
createEffect(() => {
|
||||
if (showDetails()) {
|
||||
console.log(userID());
|
||||
} else {
|
||||
console.log("Hidden");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
Async is gonna be a pain in the ass too.
|
||||
Basically anything to do with continuations.
|
||||
|
||||
Then I need to worry about `Scheduler`.
|
||||
|
||||
=== Store ===
|
||||
|
||||
How do you call a combination of a signal together with event-source that governs it together with a capability to dispatch it. For example imagine fictional:
|
||||
```
|
||||
type Model = ... // probably-big-record
|
||||
type Msg = ... // probably-big-sum-type
|
||||
let initModel: Model = ...
|
||||
fn update : Model, Msg -> Model
|
||||
|
||||
// now we can make the thing ???
|
||||
let X = Store(initModel, update)
|
||||
```
|
||||
Now from `X` we should be able to get `Signal(Model)`, but we should also get a capability to trigger events - i.e. some sort of dispatch mechanism.
|
||||
So I'm wondering, how this `X` should be called (its type depends both on `Model` and `Msg`). Or should that just be a pair? Like `(Signal(Model), EventSourceDIspatchCapability(Msg))`?
|
||||
|
||||
Apparently this is called a `Store(Model, Msg)`
|
||||
|
||||
Other people call it `Subject` - something that can both be read and written to
|
||||
Other people call it `Atom/Agent`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
signals, managing state
|
||||
storing/updating/reading values reactively
|
||||
|
||||
- current-user
|
||||
- current-page
|
||||
- current-theme
|
||||
|
||||
|
||||
createSignal(initState)
|
||||
|
||||
|
||||
tracking-signal
|
||||
|
||||
signals are reactive
|
||||
they auto-update when their value changes
|
||||
- this is stupid. Ofcourse...
|
||||
|
||||
when a signal is called within a tracking scope,
|
||||
signal adds a dependency
|
||||
|
||||
|
||||
|
||||
=== Reactive Runtime ===
|
||||
|
||||
- Signal Dependency Graph (or DAG)
|
||||
- Effect Ownership Tree
|
||||
- Effect Stack
|
||||
|
||||
for async
|
||||
- Scheduler
|
||||
- Context Restoration (when a thing in Scheduler is ready and needs to be resumed)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
=== Syntax ===
|
||||
Wait a sec... @count returns a Signal object?
|
||||
I'm not sure that I like that...
|
||||
|
||||
+(@count, 1) would be error?
|
||||
|
||||
hmm, to read from a signal, we need to be in a tracking block...
|
||||
|
||||
```
|
||||
track {
|
||||
@count
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
let {
|
||||
double = track { *(@count, 2) } // this returns a signal... interesting!
|
||||
.
|
||||
track {
|
||||
+(@double) // need different syntax I think... `double` is a regular variable...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
What about
|
||||
```
|
||||
let-signal {
|
||||
double = *(@count, 2),
|
||||
.
|
||||
+(@double, 5)
|
||||
}
|
||||
```
|
||||
the w
|
||||
|
||||
|
||||
`track` expression returns a signal, interesting.
|
||||
|
||||
Actually the let-signal could be like a sequential pairing mechanism.
|
||||
|
||||
Signal(A) Signal(B), (A, B -> C) -> Signal(C)
|
||||
|
||||
let-signal {
|
||||
a := signal-expression,
|
||||
b := signal-expression
|
||||
.
|
||||
pure-expression that has a, b as local variables...
|
||||
}
|
||||
|
||||
Mapping:
|
||||
we could have a sing
|
||||
this is like a map
|
||||
```
|
||||
let-signal {
|
||||
x := @count
|
||||
.
|
||||
*($x, 2)
|
||||
} // the following is like the double signal
|
||||
```
|
||||
|
||||
|
||||
|
||||
```
|
||||
let {
|
||||
double = let-signal { x := @count . *($x, 2) }
|
||||
.
|
||||
let-signal {
|
||||
y := $double // fucking shit. `double` here is a local var...
|
||||
. +($y, 1)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
// === parametrised signals ===
|
||||
|
||||
|
||||
```
|
||||
// like a top-level function of type (A, B, C) -> Signal(D)
|
||||
fn-signal Foo(x1, x2, x3) {
|
||||
// signal-expression
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
import { FunctionName, Pattern, VariableName } from "../expr"
|
||||
import { FunctionName, Pattern, SignalName, VariableName } from "../expr"
|
||||
import { Closure, Value, ValueTag } from "./value"
|
||||
|
||||
export type RuntimeError =
|
||||
| { tag: "FunctionLookupFailure", name: FunctionName }
|
||||
| { tag: "FunctionCallArityMismatch", name: FunctionName, expected: number, actual: number }
|
||||
| { tag: "SignalLookupFailure", name: SignalName }
|
||||
| { tag: "SignalDefinitionHasSignalIdWithoutSignal", name: SignalName } // runtime corruption
|
||||
| { tag: "SignalHasCyclicDependency", name: SignalName }
|
||||
| { tag: "ClosureApplicationArityMismatch", closure: Closure, expected: number, actual: number }
|
||||
| { tag: "VariableLookupFailure", name: VariableName }
|
||||
// | { tag: "CellLookupFailure", name: CellName }
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export function eval_start(program: Program, e: Expr): Result<Value> {
|
|||
}
|
||||
|
||||
// may throw `ThrownRuntimeError`
|
||||
function eval_expr(program: Program, env: Env, e: Expr): Value {
|
||||
export function eval_expr(program: Program, env: Env, e: Expr): Value {
|
||||
switch (e.tag) {
|
||||
case "literal":
|
||||
switch (e.literal.tag) {
|
||||
|
|
|
|||
154
src/lang/eval/signal.ts
Normal file
154
src/lang/eval/signal.ts
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
import { SignalExpr } from "../expr";
|
||||
import { Program } from "../program";
|
||||
import { Result, RuntimeError, ThrownRuntimeError } from "./error";
|
||||
import { eval_expr } from "./evaluator";
|
||||
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, 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
|
||||
throw new Error("TODO: Let-signals are hard, skipping for now.");
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// === 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;
|
||||
}
|
||||
|
||||
|
|
@ -3,7 +3,7 @@ import { Span } from "./parser/source_text"
|
|||
// === Identifiers ===
|
||||
export type VariableName = string
|
||||
export type FunctionName = string
|
||||
// type CellName = string
|
||||
export type SignalName = string
|
||||
export type Tag = string
|
||||
export type FieldName = string
|
||||
|
||||
|
|
@ -13,7 +13,6 @@ export type Meta = { span: Span };
|
|||
export type Expr =
|
||||
| { tag: "literal", literal: Literal} & Meta
|
||||
| { tag: "var_use", name: VariableName } & Meta
|
||||
// | { tag: "cell_ref", name: CellName }
|
||||
| { tag: "call", name: FunctionName, args: Expr[] } & Meta
|
||||
| { tag: "let", bindings: ExprBinding[], body: Expr } & Meta
|
||||
| { tag: "tag", tag_name: Tag } & Meta
|
||||
|
|
@ -24,6 +23,11 @@ export type Expr =
|
|||
| { tag: "lambda", parameters: ProductPattern[], body: Expr } & Meta
|
||||
| { tag: "apply", callee: Expr, args: Expr[] } & Meta
|
||||
|
||||
export type SignalExpr =
|
||||
| { tag: "read", name: SignalName } & Meta
|
||||
| { tag: "const", arg: Expr } & Meta
|
||||
| { tag: "let", bindings: SignalBinding[], body: Expr } & Meta
|
||||
|
||||
export type Literal =
|
||||
| { tag: "number", value: number }
|
||||
| { tag: "string", value: string }
|
||||
|
|
@ -33,6 +37,11 @@ export type ExprBinding = {
|
|||
expr: Expr,
|
||||
} & Meta
|
||||
|
||||
export type SignalBinding = {
|
||||
pattern: ProductPattern,
|
||||
expr: SignalExpr,
|
||||
} & Meta
|
||||
|
||||
export type MatchBranch = {
|
||||
pattern: Pattern,
|
||||
body: Expr,
|
||||
|
|
@ -75,6 +84,11 @@ export namespace Expr {
|
|||
export const fieldAssignment = (name: FieldName, expr: Expr, span: Span): FieldAssignment => ({ name, expr, span });
|
||||
}
|
||||
|
||||
export namespace SignalExpr {
|
||||
export const read = (name: SignalName, span: Span): SignalExpr => ({ tag: "read", name, span });
|
||||
export const signalBinding = (pattern: ProductPattern, expr: SignalExpr, span: Span): SignalBinding => ({ pattern, expr, span });
|
||||
}
|
||||
|
||||
export namespace ProductPattern {
|
||||
export const any = (name: VariableName, span: Span): ProductPattern => ({ tag: "any", name, span });
|
||||
export const tuple = (patterns: ProductPattern[], span: Span): ProductPattern => ({ tag: "tuple", patterns, span });
|
||||
|
|
|
|||
|
|
@ -1,35 +1,22 @@
|
|||
import { eval_signal_expression, makeTickSignal, Signal, SignalId, SignalRuntime } from "./eval/signal";
|
||||
import { ThrownRuntimeError } from "./eval/error";
|
||||
import { Value } from "./eval/value";
|
||||
import { Expr, FunctionName, ProductPattern } from "./expr";
|
||||
import { Expr, FunctionName, SignalName, ProductPattern, SignalExpr } from "./expr";
|
||||
import { installPrimitives } from "./primitive";
|
||||
|
||||
export type Timestamp = number;
|
||||
|
||||
export type Program = {
|
||||
function_definitions: Map<FunctionName, FunctionDefinition>,
|
||||
function_definition_order: FunctionName[],
|
||||
// TODO: Perhaps include the story and the environment?
|
||||
// story should be a list of currently viewed bindings
|
||||
// environment should be like the store... maybe call it store! It should map names to values and perhaps expressions that generated the value...
|
||||
// like a reactive cell. This is the analogue of the tiddler.
|
||||
// store: Map<CellName, Cell>
|
||||
function_definitions: Map<FunctionName, FunctionDefinition>,
|
||||
function_definition_order: FunctionName[],
|
||||
|
||||
signal_definitions: Map<SignalName, SignalDefinition>,
|
||||
signal_definition_order: SignalName[],
|
||||
|
||||
signal_runtime: SignalRuntime,
|
||||
};
|
||||
|
||||
// type Cell = {
|
||||
// name: CellName,
|
||||
// expression: Expr,
|
||||
// cached_value?: Value,
|
||||
// status: CellStatus
|
||||
// // TODO: Dependencies? Not sure about this yet...
|
||||
// // Operational Semantics of Cells is gonna be thought up much later.
|
||||
// // dependencies?: Set<CellName>,
|
||||
// }
|
||||
|
||||
// type CellStatus =
|
||||
// | "clean"
|
||||
// | "dirty"
|
||||
// | "error"
|
||||
|
||||
// === Functions ===
|
||||
export type FunctionDefinition =
|
||||
| { tag: "user", def: UserFunctionDefinition }
|
||||
| { tag: "primitive", def: PrimitiveFunctionDefinition }
|
||||
|
|
@ -37,8 +24,8 @@ export type FunctionDefinition =
|
|||
export type UserFunctionDefinition = {
|
||||
// Raw user input (authoritative)
|
||||
name: FunctionName,
|
||||
raw_parameters: string;
|
||||
raw_body: string;
|
||||
raw_parameters: string,
|
||||
raw_body: string,
|
||||
|
||||
// parsed
|
||||
parameters: ProductPattern[],
|
||||
|
|
@ -46,8 +33,8 @@ export type UserFunctionDefinition = {
|
|||
|
||||
|
||||
// metadata
|
||||
created_at: Timestamp;
|
||||
last_modified_at: Timestamp;
|
||||
created_at: Timestamp,
|
||||
last_modified_at: Timestamp,
|
||||
}
|
||||
|
||||
export type PrimitiveFunctionDefinition = {
|
||||
|
|
@ -57,6 +44,29 @@ export type PrimitiveFunctionDefinition = {
|
|||
|
||||
export type Implementation = (args: Value[]) => Value
|
||||
|
||||
// === Signals ===
|
||||
export type SignalDefinition =
|
||||
| { tag: "user", def: UserSignalDefinition }
|
||||
| { tag: "primitive", def: PrimitiveSignalDefinition }
|
||||
|
||||
type UserSignalDefinition = {
|
||||
name: SignalName,
|
||||
raw_body: string,
|
||||
|
||||
body: SignalExpr,
|
||||
is_initializing: boolean,
|
||||
signalId?: SignalId,
|
||||
|
||||
// metadata
|
||||
createdAt: Timestamp,
|
||||
lastModifiedAt: Timestamp,
|
||||
}
|
||||
|
||||
type PrimitiveSignalDefinition = {
|
||||
name: FunctionName,
|
||||
signalId: SignalId,
|
||||
}
|
||||
|
||||
export namespace Program {
|
||||
|
||||
type Error =
|
||||
|
|
@ -92,9 +102,15 @@ export namespace Program {
|
|||
const program: Program = {
|
||||
function_definitions: new Map(),
|
||||
function_definition_order: [],
|
||||
|
||||
signal_runtime: SignalRuntime.make(),
|
||||
signal_definitions: new Map(),
|
||||
signal_definition_order: [],
|
||||
};
|
||||
|
||||
installPrimitives(program);
|
||||
initialize_signal_runtime(program);
|
||||
|
||||
return program;
|
||||
}
|
||||
|
||||
|
|
@ -102,13 +118,120 @@ export namespace Program {
|
|||
export function lookup_function(program: Program, name: FunctionName): FunctionDefinition {
|
||||
const fn = program.function_definitions.get(name);
|
||||
if (fn === undefined) {
|
||||
throw ThrownRuntimeError.error({
|
||||
tag: "FunctionLookupFailure",
|
||||
name,
|
||||
});
|
||||
throw ThrownRuntimeError.error({ tag: "FunctionLookupFailure", name, });
|
||||
}
|
||||
return fn;
|
||||
}
|
||||
// may throw `ThrownRuntimeError`. This is used by evaluator.
|
||||
export function lookup_signal_definition(program: Program, name: SignalName): SignalDefinition {
|
||||
const sigDef = program.signal_definitions.get(name);
|
||||
if (sigDef === undefined) {
|
||||
throw ThrownRuntimeError.error({ tag: "SignalLookupFailure", name, });
|
||||
}
|
||||
return sigDef;
|
||||
}
|
||||
|
||||
export function initialize_signal_runtime(program: Program) {
|
||||
// TODO: Do I really need to initalize everything from the start?
|
||||
install_primitive_signals(program);
|
||||
for (const name of program.signal_definition_order) {
|
||||
const _ = get_or_create_signal(program, name);
|
||||
}
|
||||
}
|
||||
|
||||
function install_primitive_signals(program: Program) {
|
||||
install_primitive_signal(program, "tick",makeTickSignal(1000));
|
||||
}
|
||||
|
||||
function install_primitive_signal(program: Program, name: SignalName, signal: Signal<Value>) {
|
||||
const signalId = attachNewSignal(program, signal);
|
||||
const def: SignalDefinition = {
|
||||
tag: "primitive",
|
||||
def: { name, signalId }
|
||||
};
|
||||
program.signal_definitions.set(name, def);
|
||||
}
|
||||
|
||||
// may throw `ThrownRuntimeError`. This is used during initialization.
|
||||
export function get_or_create_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) {
|
||||
const signal = SignalRuntime.getSignal(program.signal_runtime, def.signalId);
|
||||
if (signal === undefined) {
|
||||
throw ThrownRuntimeError.error({ tag: "SignalDefinitionHasSignalIdWithoutSignal", name, });
|
||||
} else {
|
||||
return signal;
|
||||
}
|
||||
}
|
||||
|
||||
// We need to create the signal for the first time.
|
||||
if (def.is_initializing) {
|
||||
throw ThrownRuntimeError.error({ tag: "SignalHasCyclicDependency", name, });
|
||||
}
|
||||
def.is_initializing = true;
|
||||
|
||||
try {
|
||||
const newSignal = eval_signal_expression(program, def.body);
|
||||
const newId = attachNewSignal(program, newSignal);
|
||||
program.signal_runtime.store.set(newId, newSignal);
|
||||
def.signalId = newId;
|
||||
|
||||
return newSignal;
|
||||
} finally {
|
||||
def.is_initializing = false;
|
||||
}
|
||||
}
|
||||
case "primitive": {
|
||||
const def = sigDef.def;
|
||||
const signal = SignalRuntime.getSignal(program.signal_runtime, def.signalId);
|
||||
if (signal === undefined) {
|
||||
throw ThrownRuntimeError.error({ tag: "SignalDefinitionHasSignalIdWithoutSignal", name, });
|
||||
} else {
|
||||
return signal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function attachNewSignal(program: Program, signal: Signal<Value>): SignalId {
|
||||
const newId = SignalRuntime.generateSignalId(program.signal_runtime);
|
||||
program.signal_runtime.store.set(newId, signal);
|
||||
return newId;
|
||||
}
|
||||
|
||||
// TODO: Is this necessary? Maybe `get_or_create_signal` is sufficient for all cases.
|
||||
// may throw `ThrownRuntimeError`. This is used by evaluator.
|
||||
export function lookup_signal(program: Program, name: SignalName): Signal<Value> {
|
||||
const sigDef = lookup_signal_definition(program, name);
|
||||
|
||||
switch (sigDef.tag) {
|
||||
case "user": {
|
||||
const def = sigDef.def;
|
||||
if (def.signalId === undefined) {
|
||||
throw ThrownRuntimeError.error({ tag: "SignalLookupFailure", name, });
|
||||
}
|
||||
const signal = SignalRuntime.getSignal(program.signal_runtime, def.signalId);
|
||||
if (signal === undefined){
|
||||
throw ThrownRuntimeError.error({ tag: "SignalLookupFailure", name, });
|
||||
} else {
|
||||
return signal;
|
||||
}
|
||||
}
|
||||
case "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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type CreateFunction = {
|
||||
name: FunctionName,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue