Signal let-expressions
This commit is contained in:
parent
1ad1c8c442
commit
0014f75a22
7 changed files with 185 additions and 12 deletions
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
# Language Design: Two Worlds
|
||||
|
||||
The language into two fragments: Normal Expressions vs Signal Expressions.
|
||||
The language's expressions are divided 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -77,9 +77,9 @@ export function eval_expr(program: Program, env: Env, e: Expr): Value {
|
|||
function eval_bindings(program: Program, env: Env, bindings: ExprBinding[]): Env {
|
||||
// note that `let { x = 123, y = x + 1 ... } is allowed. Ofcourse later bindings can't be referenced by earlier bindings (i.e. no recursion).
|
||||
let cur_env = env;
|
||||
for (const { pattern: var_name, expr } of bindings) {
|
||||
for (const { pattern, expr } of bindings) {
|
||||
const value = eval_expr(program, cur_env, expr);
|
||||
const res = match_product_pattern(var_name, value);
|
||||
const res = match_product_pattern(pattern, value);
|
||||
if (res.tag === "failure") {
|
||||
throw ThrownRuntimeError.error({
|
||||
tag: "UnableToFindMatchingPattern",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { Expr, SignalExpr, SignalName } from "../expr";
|
||||
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";
|
||||
|
||||
|
||||
|
|
@ -37,7 +39,7 @@ export namespace SignalRuntime {
|
|||
// 2. runtime, subscriptions, derived signals etc.
|
||||
|
||||
// may throw `ThrownRuntimeError`
|
||||
export function eval_signal_expression(program: Program, e: SignalExpr): Signal<Value> {
|
||||
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);
|
||||
|
|
@ -45,11 +47,64 @@ export function eval_signal_expression(program: Program, e: SignalExpr): Signal<
|
|||
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.");
|
||||
// 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);
|
||||
|
|
@ -107,6 +162,19 @@ function pair<A, B>(X: Signal<A>, Y: Signal<B>): Signal<[A, B]> {
|
|||
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 ===
|
||||
|
|
|
|||
|
|
@ -66,6 +66,10 @@ export namespace Env {
|
|||
});
|
||||
}
|
||||
|
||||
export function nil_frame(): EnvFrame {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
export function frame_insert_mut(frame: EnvFrame, var_name: VariableName, value: Value) {
|
||||
frame.set(var_name, value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export type SignalExpr =
|
|||
| { tag: "read", name: SignalName } & Meta
|
||||
// TODO: Is `const` necesary?
|
||||
| { tag: "const", arg: Expr } & Meta
|
||||
| { tag: "let", bindings: SignalBinding[], body: Expr } & Meta
|
||||
| { tag: "let", bindings: SignalExprBinding[], body: Expr } & Meta
|
||||
|
||||
export type Literal =
|
||||
| { tag: "number", value: number }
|
||||
|
|
@ -38,7 +38,7 @@ export type ExprBinding = {
|
|||
expr: Expr,
|
||||
} & Meta
|
||||
|
||||
export type SignalBinding = {
|
||||
export type SignalExprBinding = {
|
||||
pattern: ProductPattern,
|
||||
expr: SignalExpr,
|
||||
} & Meta
|
||||
|
|
@ -87,7 +87,7 @@ export namespace Expr {
|
|||
|
||||
export namespace SignalExpr {
|
||||
export const read = (name: SignalName, span: Span): SignalExpr => ({ tag: "read", name, span });
|
||||
export const signalBinding = (pattern: ProductPattern, expr: SignalExpr, span: Span): SignalBinding => ({ pattern, expr, span });
|
||||
export const signalBinding = (pattern: ProductPattern, expr: SignalExpr, span: Span): SignalExprBinding => ({ pattern, expr, span });
|
||||
}
|
||||
|
||||
export namespace ProductPattern {
|
||||
|
|
|
|||
|
|
@ -342,7 +342,6 @@ export function isNextTokenExprStart(cursor: Cursor): boolean {
|
|||
case "signal":
|
||||
case "fn-signal":
|
||||
case "=":
|
||||
case "=":
|
||||
case "|":
|
||||
case "!":
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ export namespace Program {
|
|||
| { tag: "DuplicateSignalName", name: SignalName }
|
||||
| { tag: "SignalNotFound", name: SignalName }
|
||||
| { tag: "CannotEditPrimitiveSignal", name: SignalName }
|
||||
| { tag: "CannotEditCell", name: SignalName }
|
||||
| { tag: "CannotDeletePrimitiveSignal", name: SignalName }
|
||||
| { tag: "PrimitiveSignalAlreadyExists", name: SignalName }
|
||||
|
||||
|
|
@ -196,7 +197,7 @@ export namespace Program {
|
|||
def.is_initializing = true;
|
||||
|
||||
try {
|
||||
const newSignal = eval_signal_expression(program, def.body);
|
||||
const newSignal = eval_signal_expression(program, Env.nil(), def.body);
|
||||
const newId = attachNewSignal(program, newSignal);
|
||||
program.signal_runtime.store.set(newId, newSignal);
|
||||
def.signalId = newId;
|
||||
|
|
@ -260,6 +261,18 @@ export namespace Program {
|
|||
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);
|
||||
|
|
@ -413,6 +426,10 @@ export function updateSignal(
|
|||
return Result.error({ tag: "CannotEditPrimitiveSignal", name } as any);
|
||||
}
|
||||
|
||||
if (existingEntry.tag === "cell") {
|
||||
return Result.error({ tag: "CannotEditCell", name } as any);
|
||||
}
|
||||
|
||||
const def = existingEntry.def;
|
||||
def.body = body;
|
||||
def.raw_body = raw_body;
|
||||
|
|
@ -458,5 +475,90 @@ export function updateSignal(
|
|||
return Result.ok(undefined);
|
||||
}
|
||||
|
||||
// === Cells ===
|
||||
export type CreateCell = {
|
||||
name: SignalName,
|
||||
body: Expr,
|
||||
raw_body: string,
|
||||
}
|
||||
|
||||
export function registerCell(
|
||||
program: Program,
|
||||
{ name, body, raw_body }: CreateCell
|
||||
): Result<void> {
|
||||
|
||||
if (program.signal_definitions.has(name)) {
|
||||
return Result.error({ tag: "DuplicateSignalName", name });
|
||||
}
|
||||
|
||||
const now: Timestamp = Date.now();
|
||||
|
||||
// TODO: MAY THROW RuntimeError. Should probably switch to `eval_start` - and extend the `Program.Error` with runtime errors.
|
||||
const initialValue = eval_expr(program, Env.nil(), body);
|
||||
|
||||
const sig = signal(initialValue);
|
||||
const signalId = attachNewSignal(program, sig);
|
||||
|
||||
const newCell: CellDefinition = {
|
||||
name,
|
||||
raw_body,
|
||||
body,
|
||||
signalId,
|
||||
createdAt: now,
|
||||
lastModifiedAt: now,
|
||||
};
|
||||
|
||||
program.signal_definitions.set(name, { tag: "cell", def: newCell });
|
||||
program.signal_definition_order.push(name);
|
||||
|
||||
return Result.ok(undefined);
|
||||
}
|
||||
|
||||
export type UpdateCell = {
|
||||
body: Expr,
|
||||
raw_body: string,
|
||||
}
|
||||
|
||||
export function updateCell(
|
||||
program: Program,
|
||||
name: SignalName,
|
||||
{ body, raw_body }: UpdateCell
|
||||
): Result<void> {
|
||||
const existingEntry = program.signal_definitions.get(name);
|
||||
|
||||
if (!existingEntry) {
|
||||
return Result.error({ tag: "SignalNotFound", name } as any);
|
||||
}
|
||||
|
||||
// Ensure we are editing a Cell
|
||||
if (existingEntry.tag !== "cell") {
|
||||
return Result.error({ tag: "CannotEditCell", name } as any);
|
||||
}
|
||||
|
||||
const def = existingEntry.def;
|
||||
|
||||
// TODO: MAY THROW RuntimeError. Should probably switch to `eval_start` - and extend the `Program.Error` with runtime errors.
|
||||
const newValue = eval_expr(program, Env.nil(), body);
|
||||
|
||||
// 2. Find the existing runtime signal
|
||||
if (def.signalId === undefined) {
|
||||
// This should theoretically not happen for cells since we initialize them eagerly,
|
||||
// but good to be safe.
|
||||
return Result.error({ tag: "SignalDefinitionHasSignalIdWithoutSignal", name } as any);
|
||||
}
|
||||
|
||||
const sig = SignalRuntime.getSignal(program.signal_runtime, def.signalId);
|
||||
if (!sig) {
|
||||
return Result.error({ tag: "SignalDefinitionHasSignalIdWithoutSignal", name } as any);
|
||||
}
|
||||
|
||||
sig.set(() => newValue);
|
||||
|
||||
def.body = body;
|
||||
def.raw_body = raw_body;
|
||||
def.lastModifiedAt = Date.now();
|
||||
|
||||
return Result.ok(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue