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
|
# 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 Normal (Values) world is the standard (call-by-value) functional programming.
|
||||||
- The Signal (Reactive) world declares connections between time-varying signals.
|
- 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 {
|
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).
|
// 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;
|
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 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") {
|
if (res.tag === "failure") {
|
||||||
throw ThrownRuntimeError.error({
|
throw ThrownRuntimeError.error({
|
||||||
tag: "UnableToFindMatchingPattern",
|
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 { Program } from "../program";
|
||||||
import { Result, RuntimeError, ThrownRuntimeError } from "./error";
|
import { Result, RuntimeError, ThrownRuntimeError } from "./error";
|
||||||
import { eval_expr, eval_start } from "./evaluator";
|
import { eval_expr, eval_start } from "./evaluator";
|
||||||
|
import { match_product_pattern } from "./pattern_match";
|
||||||
import { Env, Value } from "./value";
|
import { Env, Value } from "./value";
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -37,7 +39,7 @@ export namespace SignalRuntime {
|
||||||
// 2. runtime, subscriptions, derived signals etc.
|
// 2. runtime, subscriptions, derived signals etc.
|
||||||
|
|
||||||
// may throw `ThrownRuntimeError`
|
// 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) {
|
switch (e.tag) {
|
||||||
case "read":
|
case "read":
|
||||||
return Program.get_or_create_signal(program, e.name);
|
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);
|
const val = eval_expr(program, Env.nil(), e.arg);
|
||||||
return signal(val);
|
return signal(val);
|
||||||
case "let":
|
case "let":
|
||||||
// TODO
|
// TODO: To change this, first look at how `tupleThen` works. It's a simpler more isolated case. Easier to get right.
|
||||||
throw new Error("TODO: Let-signals are hard, skipping for now.");
|
// === 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`
|
// may throw `ThrownRuntimeError`
|
||||||
export function signal_set_value(program: Program, sig: Signal<Value>, e: Expr): Value {
|
export function signal_set_value(program: Program, sig: Signal<Value>, e: Expr): Value {
|
||||||
const value = eval_expr(program, Env.nil(), e);
|
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;
|
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 ===
|
// === 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) {
|
export function frame_insert_mut(frame: EnvFrame, var_name: VariableName, value: Value) {
|
||||||
frame.set(var_name, value);
|
frame.set(var_name, value);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export type SignalExpr =
|
||||||
| { tag: "read", name: SignalName } & Meta
|
| { tag: "read", name: SignalName } & Meta
|
||||||
// TODO: Is `const` necesary?
|
// TODO: Is `const` necesary?
|
||||||
| { tag: "const", arg: Expr } & Meta
|
| { tag: "const", arg: Expr } & Meta
|
||||||
| { tag: "let", bindings: SignalBinding[], body: Expr } & Meta
|
| { tag: "let", bindings: SignalExprBinding[], body: Expr } & Meta
|
||||||
|
|
||||||
export type Literal =
|
export type Literal =
|
||||||
| { tag: "number", value: number }
|
| { tag: "number", value: number }
|
||||||
|
|
@ -38,7 +38,7 @@ export type ExprBinding = {
|
||||||
expr: Expr,
|
expr: Expr,
|
||||||
} & Meta
|
} & Meta
|
||||||
|
|
||||||
export type SignalBinding = {
|
export type SignalExprBinding = {
|
||||||
pattern: ProductPattern,
|
pattern: ProductPattern,
|
||||||
expr: SignalExpr,
|
expr: SignalExpr,
|
||||||
} & Meta
|
} & Meta
|
||||||
|
|
@ -87,7 +87,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): SignalBinding => ({ pattern, expr, span });
|
export const signalBinding = (pattern: ProductPattern, expr: SignalExpr, span: Span): SignalExprBinding => ({ pattern, expr, span });
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace ProductPattern {
|
export namespace ProductPattern {
|
||||||
|
|
|
||||||
|
|
@ -342,7 +342,6 @@ export function isNextTokenExprStart(cursor: Cursor): boolean {
|
||||||
case "signal":
|
case "signal":
|
||||||
case "fn-signal":
|
case "fn-signal":
|
||||||
case "=":
|
case "=":
|
||||||
case "=":
|
|
||||||
case "|":
|
case "|":
|
||||||
case "!":
|
case "!":
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ export namespace Program {
|
||||||
| { tag: "DuplicateSignalName", name: SignalName }
|
| { tag: "DuplicateSignalName", name: SignalName }
|
||||||
| { tag: "SignalNotFound", name: SignalName }
|
| { tag: "SignalNotFound", name: SignalName }
|
||||||
| { tag: "CannotEditPrimitiveSignal", name: SignalName }
|
| { tag: "CannotEditPrimitiveSignal", name: SignalName }
|
||||||
|
| { tag: "CannotEditCell", name: SignalName }
|
||||||
| { tag: "CannotDeletePrimitiveSignal", name: SignalName }
|
| { tag: "CannotDeletePrimitiveSignal", name: SignalName }
|
||||||
| { tag: "PrimitiveSignalAlreadyExists", name: SignalName }
|
| { tag: "PrimitiveSignalAlreadyExists", name: SignalName }
|
||||||
|
|
||||||
|
|
@ -196,7 +197,7 @@ export namespace Program {
|
||||||
def.is_initializing = true;
|
def.is_initializing = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newSignal = eval_signal_expression(program, def.body);
|
const newSignal = eval_signal_expression(program, Env.nil(), def.body);
|
||||||
const newId = attachNewSignal(program, newSignal);
|
const newId = attachNewSignal(program, newSignal);
|
||||||
program.signal_runtime.store.set(newId, newSignal);
|
program.signal_runtime.store.set(newId, newSignal);
|
||||||
def.signalId = newId;
|
def.signalId = newId;
|
||||||
|
|
@ -260,6 +261,18 @@ export namespace Program {
|
||||||
return signal;
|
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": {
|
case "primitive": {
|
||||||
const def = sigDef.def;
|
const def = sigDef.def;
|
||||||
const signal = SignalRuntime.getSignal(program.signal_runtime, def.signalId);
|
const signal = SignalRuntime.getSignal(program.signal_runtime, def.signalId);
|
||||||
|
|
@ -413,6 +426,10 @@ export function updateSignal(
|
||||||
return Result.error({ tag: "CannotEditPrimitiveSignal", name } as any);
|
return Result.error({ tag: "CannotEditPrimitiveSignal", name } as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (existingEntry.tag === "cell") {
|
||||||
|
return Result.error({ tag: "CannotEditCell", name } as any);
|
||||||
|
}
|
||||||
|
|
||||||
const def = existingEntry.def;
|
const def = existingEntry.def;
|
||||||
def.body = body;
|
def.body = body;
|
||||||
def.raw_body = raw_body;
|
def.raw_body = raw_body;
|
||||||
|
|
@ -458,5 +475,90 @@ export function updateSignal(
|
||||||
return Result.ok(undefined);
|
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