Better field typing

This commit is contained in:
Tony Garnock-Jones 2021-01-19 21:05:04 +01:00
parent a374cbfdf9
commit 97aaa5ef5d
3 changed files with 131 additions and 115 deletions

View File

@ -17,7 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
import { bootModule, Skeleton, Record, Discard, Capture, Observe, Facet, DataflowObservableObject } from '..'; import { bootModule, Skeleton, Record, Discard, Capture, Observe, Facet } from '..';
const __ = Discard._instance; const __ = Discard._instance;
const _$ = Capture(__); const _$ = Capture(__);
@ -28,67 +28,66 @@ const N = 100000;
console.time('box-and-client-' + N.toString()); console.time('box-and-client-' + N.toString());
function boot(thisFacet: Facet) { function boot(thisFacet: Facet<{}>) {
thisFacet.spawn('box', function (this: DataflowObservableObject & { thisFacet.spawn<{ value: number }>('box', function (thisFacet) {
value: number; thisFacet.declareField(this, 'value', 0);
}, thisFacet: Facet) { thisFacet.addEndpoint(function () {
thisFacet.declareField(this, 'value', 0); // console.log('recomputing published BoxState', this.value);
thisFacet.addEndpoint(() => { return { assertion: BoxState(this.value), analysis: null };
// console.log('recomputing published BoxState', this.value); });
return { assertion: BoxState(this.value), analysis: null }; thisFacet.addDataflow(function () {
}); // console.log('dataflow saw new value', this.value);
thisFacet.addDataflow(() => { if (this.value === N) {
// console.log('dataflow saw new value', this.value); thisFacet.stop(function () {
if (this.value === N) { console.log('terminated box root facet');
thisFacet.stop(() => { });
console.log('terminated box root facet'); }
});
thisFacet.addEndpoint(function () {
let analysis = Skeleton.analyzeAssertion(SetBox(_$));
analysis.callback = thisFacet.wrap(function (thisFacet, evt, [v]) {
if (evt === Skeleton.EventType.MESSAGE) {
if (typeof v !== 'number') return;
thisFacet.scheduleScript(function () {
this.value = v;
// console.log('box updated value', v);
});
}
});
return { assertion: Observe(SetBox(_$)), analysis };
}); });
}
}); });
thisFacet.addEndpoint(() => {
let analysis = Skeleton.analyzeAssertion(SetBox(_$));
analysis.callback = thisFacet.wrap((thisFacet, evt, [v]) => {
if (evt === Skeleton.EventType.MESSAGE) {
if (typeof v !== 'number') return;
thisFacet.scheduleScript(() => {
this.value = v;
// console.log('box updated value', v);
});
}
});
return { assertion: Observe(SetBox(_$)), analysis };
});
});
thisFacet.spawn('client', function (thisFacet: Facet) { thisFacet.spawn('client', function (thisFacet: Facet<{}>) {
thisFacet.addEndpoint(() => { thisFacet.addEndpoint(function () {
let analysis = Skeleton.analyzeAssertion(BoxState(_$)); let analysis = Skeleton.analyzeAssertion(BoxState(_$));
analysis.callback = thisFacet.wrap((thisFacet, evt, [v]) => { analysis.callback = thisFacet.wrap(function (thisFacet, evt, [v]) {
if (evt === Skeleton.EventType.ADDED) { if (evt === Skeleton.EventType.ADDED) {
if (typeof v !== 'number') return; if (typeof v !== 'number') return;
thisFacet.scheduleScript(() => { thisFacet.scheduleScript(function () {
// console.log('client sending SetBox', v + 1); // console.log('client sending SetBox', v + 1);
thisFacet.send(SetBox(v + 1)); thisFacet.send(SetBox(v + 1));
}); });
} }
}); });
return { assertion: Observe(BoxState(_$)), analysis }; return { assertion: Observe(BoxState(_$)), analysis };
});
thisFacet.addEndpoint(function () {
let analysis = Skeleton.analyzeAssertion(BoxState(__));
analysis.callback = thisFacet.wrap(function (thisFacet, evt, _vs) {
if (evt === Skeleton.EventType.REMOVED) {
thisFacet.scheduleScript(function () {
console.log('box gone');
});
}
});
return { assertion: Observe(BoxState(__)), analysis };
});
}); });
thisFacet.addEndpoint(() => {
let analysis = Skeleton.analyzeAssertion(BoxState(__));
analysis.callback = thisFacet.wrap((thisFacet, evt, _vs) => {
if (evt === Skeleton.EventType.REMOVED) {
thisFacet.scheduleScript(() => {
console.log('box gone');
});
}
});
return { assertion: Observe(BoxState(__)), analysis };
});
});
thisFacet.actor.dataspace.ground().addStopHandler(() => thisFacet.actor.dataspace.ground().addStopHandler(function () {
console.timeEnd('box-and-client-' + N.toString())); console.timeEnd('box-and-client-' + N.toString());
});
} }
bootModule(boot); bootModule(boot);

View File

@ -40,17 +40,18 @@ export type FacetId = ActorId;
export type EndpointId = ActorId; export type EndpointId = ActorId;
export type Task<T> = () => T; export type Task<T> = () => T;
export type Script<T> = (f: Facet) => T; export type Script<T, Fields> = (this: Fields & DataflowObservableObject, f: Facet<Fields>) => T;
export type MaybeValue = Value | undefined; export type MaybeValue = Value | undefined;
export type EndpointSpec = { assertion: MaybeValue, analysis: Skeleton.Analysis | null }; export type EndpointSpec = { assertion: MaybeValue, analysis: Skeleton.Analysis | null };
export type ObserverCallback = (facet: Facet, bindings: Array<Value>) => void; export type ObserverCallback<Fields> =
(this: Fields, facet: Facet<Fields>, bindings: Array<Value>) => void;
export type ObserverCallbacks = { export type ObserverCallbacks<Fields> = {
add?: ObserverCallback; add?: ObserverCallback<Fields>;
del?: ObserverCallback; del?: ObserverCallback<Fields>;
msg?: ObserverCallback; msg?: ObserverCallback<Fields>;
} }
export const DataflowObservableObjectId = Symbol.for('DataflowObservableObjectId'); export const DataflowObservableObjectId = Symbol.for('DataflowObservableObjectId');
@ -63,12 +64,12 @@ export function _canonicalizeDataflowObservable(i: DataflowObservable): string {
return i[0][DataflowObservableObjectId]() + ',' + i[1]; return i[0][DataflowObservableObjectId]() + ',' + i[1];
} }
export type DataflowDependent = Endpoint; export type DataflowDependent = Endpoint<any>;
export function _canonicalizeDataflowDependent(i: DataflowDependent): string { export function _canonicalizeDataflowDependent(i: DataflowDependent): string {
return '' + i.id; return '' + i.id;
} }
export type ActivationScript = Script<void>; export type ActivationScript = Script<void, {}>;
export abstract class Dataspace { export abstract class Dataspace {
nextId: ActorId = 0; nextId: ActorId = 0;
@ -81,7 +82,7 @@ export abstract class Dataspace {
actors: IdentityMap<number, Actor> = new IdentityMap(); actors: IdentityMap<number, Actor> = new IdentityMap();
activations: IdentitySet<ActivationScript> = new IdentitySet(); activations: IdentitySet<ActivationScript> = new IdentitySet();
constructor(bootProc: Script<void>) { constructor(bootProc: Script<void, {}>) {
this.pendingTurns = [new Turn(null, [new Spawn(null, bootProc, new Set())])]; this.pendingTurns = [new Turn(null, [new Spawn(null, bootProc, new Set())])];
} }
@ -129,7 +130,12 @@ export abstract class Dataspace {
}); });
} }
addActor(name: any, bootProc: Script<void>, initialAssertions: Set, parentActor: Actor | null) { addActor<SpawnFields>(
name: any,
bootProc: Script<void, SpawnFields>,
initialAssertions: Set,
parentActor: Actor | null)
{
let ac = new Actor(this, name, initialAssertions, parentActor?.id); let ac = new Actor(this, name, initialAssertions, parentActor?.id);
// debug('Spawn', ac && ac.toString()); // debug('Spawn', ac && ac.toString());
this.applyPatch(ac, ac.adhocAssertions); this.applyPatch(ac, ac.adhocAssertions);
@ -181,7 +187,7 @@ export abstract class Dataspace {
this.index.removeHandler(handler, handler.callback!); this.index.removeHandler(handler, handler.callback!);
} }
endpointHook(_facet: Facet, _endpoint: Endpoint) { endpointHook<Fields>(_facet: Facet<Fields>, _endpoint: Endpoint<Fields>) {
// Subclasses may override // Subclasses may override
} }
} }
@ -190,7 +196,7 @@ export class Actor {
readonly id: ActorId; readonly id: ActorId;
readonly dataspace: Dataspace; readonly dataspace: Dataspace;
readonly name: any; readonly name: any;
rootFacet: Facet | null = null; rootFacet: Facet<any> | null = null;
isRunnable: boolean = false; isRunnable: boolean = false;
readonly pendingTasks: Array<Array<Task<void>>>; readonly pendingTasks: Array<Array<Task<void>>>;
pendingActions: Array<Action>; pendingActions: Array<Action>;
@ -253,11 +259,15 @@ export class Actor {
this.pendingTasks[priority].push(task); this.pendingTasks[priority].push(task);
} }
addFacet(parentFacet: Facet | null, bootProc: Script<void>, checkInScript: boolean = false) { addFacet<ParentFields, ChildFields extends ParentFields>(
parentFacet: Facet<ParentFields> | null,
bootProc: Script<void, ChildFields>,
checkInScript: boolean = false)
{
if (checkInScript && parentFacet && !parentFacet.inScript) { if (checkInScript && parentFacet && !parentFacet.inScript) {
throw new Error("Cannot add facet outside script; are you missing a `react { ... }`?"); throw new Error("Cannot add facet outside script; are you missing a `react { ... }`?");
} }
let f = new Facet(this, parentFacet); let f = new Facet<ChildFields>(this, parentFacet);
f.invokeScript(f => f.withNonScriptContext(() => bootProc.call(f.fields, f))); f.invokeScript(f => f.withNonScriptContext(() => bootProc.call(f.fields, f)));
this.scheduleTask(() => { this.scheduleTask(() => {
if ((parentFacet && !parentFacet.isLive) || f.isInert()) { if ((parentFacet && !parentFacet.isLive) || f.isInert()) {
@ -351,12 +361,12 @@ class Message extends Action {
} }
} }
class Spawn extends Action { class Spawn<Fields> extends Action {
readonly name: any; readonly name: any;
readonly bootProc: Script<void>; readonly bootProc: Script<void, Fields>;
readonly initialAssertions: Set; readonly initialAssertions: Set;
constructor(name: any, bootProc: Script<void>, initialAssertions: Set = new Set()) { constructor(name: any, bootProc: Script<void, Fields>, initialAssertions: Set = new Set()) {
super(); super();
this.name = name; this.name = name;
this.bootProc = bootProc; this.bootProc = bootProc;
@ -406,7 +416,7 @@ class Activation extends Action {
perform(ds: Dataspace, ac: Actor | null): void { perform(ds: Dataspace, ac: Actor | null): void {
if (ds.activations.has(this.script)) return; if (ds.activations.has(this.script)) return;
ds.activations.add(this.script); ds.activations.add(this.script);
ds.addActor(this.name, rootFacet => rootFacet.addStartScript(this.script), new Set(), ac); ds.addActor<{}>(this.name, rootFacet => rootFacet.addStartScript(this.script), new Set(), ac);
} }
} }
@ -424,18 +434,18 @@ export class Turn {
} }
} }
export class Facet { export class Facet<Fields> {
readonly id: FacetId; readonly id: FacetId;
isLive = true; isLive = true;
readonly actor: Actor; readonly actor: Actor;
readonly parent: Facet | null; readonly parent: Facet<any> | null;
readonly endpoints = new IdentityMap<EndpointId, Endpoint>(); readonly endpoints = new IdentityMap<EndpointId, Endpoint<Fields>>();
readonly stopScripts: Array<Script<void>> = []; readonly stopScripts: Array<Script<void, Fields>> = [];
readonly children = new IdentitySet<Facet>(); readonly children = new IdentitySet<Facet<any>>();
readonly fields: any; readonly fields: Fields & DataflowObservableObject;
inScript = true; inScript = true;
constructor(actor: Actor, parent: Facet | null) { constructor(actor: Actor, parent: Facet<any> | null) {
this.id = actor.dataspace.nextId++; this.id = actor.dataspace.nextId++;
this.actor = actor; this.actor = actor;
this.parent = parent; this.parent = parent;
@ -513,11 +523,11 @@ export class Facet {
} }
// This alias exists because of the naive expansion done by the parser. // This alias exists because of the naive expansion done by the parser.
_stop(continuation?: Script<void>) { _stop(continuation?: Script<void, Fields>) {
this.stop(continuation); this.stop(continuation);
} }
stop(continuation?: Script<void>) { stop(continuation?: Script<void, Fields>) {
this.parent!.invokeScript(() => { this.parent!.invokeScript(() => {
this.actor.scheduleTask(() => { this.actor.scheduleTask(() => {
this._terminate(); this._terminate();
@ -529,35 +539,35 @@ export class Facet {
}); });
} }
addStartScript(s: Script<void>) { addStartScript(s: Script<void, Fields>) {
this.ensureFacetSetup('`on start`'); this.ensureFacetSetup('`on start`');
this.scheduleScript(s); this.scheduleScript(s);
} }
addStopScript(s: Script<void>) { addStopScript(s: Script<void, Fields>) {
this.ensureFacetSetup('`on stop`'); this.ensureFacetSetup('`on stop`');
this.stopScripts.push(s); this.stopScripts.push(s);
} }
addEndpoint(updateFun: Script<EndpointSpec>, isDynamic: boolean = true): Endpoint { addEndpoint(updateFun: Script<EndpointSpec, Fields>, isDynamic: boolean = true): Endpoint<Fields> {
const ep = new Endpoint(this, isDynamic, updateFun); const ep = new Endpoint(this, isDynamic, updateFun);
this.actor.dataspace.endpointHook(this, ep); this.actor.dataspace.endpointHook(this, ep);
return ep; return ep;
} }
_addRawObserverEndpoint(specScript: Script<MaybeValue>, callbacks: ObserverCallbacks): Endpoint _addRawObserverEndpoint(specScript: Script<MaybeValue, Fields>, callbacks: ObserverCallbacks<Fields>): Endpoint<Fields>
{ {
return this.addEndpoint(() => { return this.addEndpoint(() => {
const spec = specScript(this); const spec = specScript.call(this.fields, this);
if (spec === void 0) { if (spec === void 0) {
return { assertion: void 0, analysis: null }; return { assertion: void 0, analysis: null };
} else { } else {
const analysis = Skeleton.analyzeAssertion(spec); const analysis = Skeleton.analyzeAssertion(spec);
analysis.callback = this.wrap((facet, evt, vs) => { analysis.callback = this.wrap((facet, evt, vs) => {
switch (evt) { switch (evt) {
case Skeleton.EventType.ADDED: callbacks.add?.(facet, vs); break; case Skeleton.EventType.ADDED: callbacks.add?.call(facet.fields, facet, vs); break;
case Skeleton.EventType.REMOVED: callbacks.del?.(facet, vs); break; case Skeleton.EventType.REMOVED: callbacks.del?.call(facet.fields, facet, vs); break;
case Skeleton.EventType.MESSAGE: callbacks.msg?.(facet, vs); break; case Skeleton.EventType.MESSAGE: callbacks.msg?.call(facet.fields, facet, vs); break;
} }
}); });
return { assertion: Observe(spec), analysis }; return { assertion: Observe(spec), analysis };
@ -565,9 +575,9 @@ export class Facet {
}); });
} }
addObserverEndpoint(specThunk: (facet: Facet) => MaybeValue, callbacks: ObserverCallbacks): Endpoint { addObserverEndpoint(specThunk: (facet: Facet<Fields>) => MaybeValue, callbacks: ObserverCallbacks<Fields>): Endpoint<Fields> {
const scriptify = (f?: ObserverCallback) => const scriptify = (f?: ObserverCallback<Fields>) =>
f && ((facet: Facet, vs: Array<Value>) => f && ((facet: Facet<Fields>, vs: Array<Value>) =>
facet.scheduleScript(() => f.call(facet.fields, facet, vs))); facet.scheduleScript(() => f.call(facet.fields, facet, vs)));
return this._addRawObserverEndpoint(specThunk, { return this._addRawObserverEndpoint(specThunk, {
add: scriptify(callbacks.add), add: scriptify(callbacks.add),
@ -576,7 +586,7 @@ export class Facet {
}); });
} }
addDataflow(subjectFun: Script<void>, priority?: Priority): Endpoint { addDataflow(subjectFun: Script<void, Fields>, priority?: Priority): Endpoint<Fields> {
return this.addEndpoint(() => { return this.addEndpoint(() => {
let subjectId = this.actor.dataspace.dataflow.currentSubjectId; let subjectId = this.actor.dataspace.dataflow.currentSubjectId;
this.scheduleScript(() => { this.scheduleScript(() => {
@ -607,7 +617,7 @@ export class Facet {
return s + ')'; return s + ')';
} }
invokeScript<T>(script: Script<T>, propagateErrors = false): T | undefined { invokeScript<T>(script: Script<T, Fields>, propagateErrors = false): T | undefined {
try { try {
// console.group('Facet', facet && facet.toString()); // console.group('Facet', facet && facet.toString());
return script.call(this.fields, this); return script.call(this.fields, this);
@ -623,11 +633,18 @@ export class Facet {
} }
} }
wrap<T extends Array<any>, R>(fn: (f: Facet, ... args: T) => R): (... args: T) => R { wrap<T extends Array<any>, R>(
fn: (this: Fields & DataflowObservableObject,
f: Facet<Fields>, ... args: T) => R
): (... args: T) => R
{
return (... actuals) => this.invokeScript(f => fn.call(f.fields, f, ... actuals), true)!; return (... actuals) => this.invokeScript(f => fn.call(f.fields, f, ... actuals), true)!;
} }
wrapExternal<T extends Array<any>>(fn: (f: Facet, ... args: T) => void): (... args: T) => void { wrapExternal<T extends Array<any>>(
fn: (this: Fields & DataflowObservableObject,
f: Facet<Fields>, ... args: T) => void
): (... args: T) => void {
const ac = this.actor; const ac = this.actor;
return (... actuals) => { return (... actuals) => {
if (this.isLive) { if (this.isLive) {
@ -660,16 +677,16 @@ export class Facet {
} }
// This alias exists because of the naive expansion done by the parser. // This alias exists because of the naive expansion done by the parser.
_spawn(name: any, bootProc: Script<void>, initialAssertions?: Set) { _spawn<SpawnFields>(name: any, bootProc: Script<void, SpawnFields>, initialAssertions?: Set) {
this.spawn(name, bootProc, initialAssertions); this.spawn(name, bootProc, initialAssertions);
} }
spawn(name: any, bootProc: Script<void>, initialAssertions?: Set) { spawn<SpawnFields>(name: any, bootProc: Script<void, SpawnFields>, initialAssertions?: Set) {
this.ensureNonFacetSetup('`spawn`'); this.ensureNonFacetSetup('`spawn`');
this.enqueueScriptAction(new Spawn(name, bootProc, initialAssertions)); this.enqueueScriptAction(new Spawn(name, bootProc, initialAssertions));
} }
deferTurn(continuation: Script<void>) { deferTurn(continuation: Script<void, Fields>) {
this.ensureNonFacetSetup('`deferTurn`'); this.ensureNonFacetSetup('`deferTurn`');
this.enqueueScriptAction(new DeferredTurn(this.wrap(continuation))); this.enqueueScriptAction(new DeferredTurn(this.wrap(continuation)));
} }
@ -679,7 +696,7 @@ export class Facet {
this.enqueueScriptAction(new Activation(script, name ?? null)); this.enqueueScriptAction(new Activation(script, name ?? null));
} }
scheduleScript(script: Script<void>, priority?: Priority) { scheduleScript(script: Script<void, Fields>, priority?: Priority) {
this.actor.scheduleTask(this.wrap(script), priority); this.actor.scheduleTask(this.wrap(script), priority);
} }
@ -706,22 +723,22 @@ export class Facet {
// delete obj[prop]; // delete obj[prop];
// } // }
addChildFacet(bootProc: Script<void>) { addChildFacet<ChildFields extends Fields>(bootProc: Script<void, ChildFields>) {
this.actor.addFacet(this, bootProc, true); this.actor.addFacet(this, bootProc, true);
} }
withSelfDo(t: Script<void>) { withSelfDo(t: Script<void, Fields>) {
t(this); t.call(this.fields, this);
} }
} }
export class Endpoint { export class Endpoint<Fields> {
readonly id: EndpointId; readonly id: EndpointId;
readonly facet: Facet; readonly facet: Facet<Fields>;
readonly updateFun: Script<EndpointSpec>; readonly updateFun: Script<EndpointSpec, Fields>;
spec: EndpointSpec; spec: EndpointSpec;
constructor(facet: Facet, isDynamic: boolean, updateFun: Script<EndpointSpec>) { constructor(facet: Facet<Fields>, isDynamic: boolean, updateFun: Script<EndpointSpec, Fields>) {
facet.ensureFacetSetup('add endpoint'); facet.ensureFacetSetup('add endpoint');
let ac = facet.actor; let ac = facet.actor;
let ds = ac.dataspace; let ds = ac.dataspace;

View File

@ -28,9 +28,9 @@ import { Ground } from './ground.js';
export const $QuitDataspace = new $Special("quit-dataspace"); export const $QuitDataspace = new $Special("quit-dataspace");
export class NestedDataspace extends Dataspace { export class NestedDataspace extends Dataspace {
readonly outerFacet: Facet; readonly outerFacet: Facet<{}>;
constructor(outerFacet: Facet, bootProc: Script<void>) { constructor(outerFacet: Facet<{}>, bootProc: Script<void, {}>) {
super(bootProc); super(bootProc);
this.outerFacet = outerFacet; this.outerFacet = outerFacet;
} }
@ -42,7 +42,7 @@ export class NestedDataspace extends Dataspace {
} }
} }
endpointHook(facet: Facet, innerEp: Endpoint) { endpointHook<InnerFields>(facet: Facet<InnerFields>, innerEp: Endpoint<InnerFields>) {
super.endpointHook(facet, innerEp); super.endpointHook(facet, innerEp);
const innerAssertion = innerEp.spec.assertion; const innerAssertion = innerEp.spec.assertion;
@ -105,7 +105,7 @@ export class NestedDataspace extends Dataspace {
return net; return net;
} }
hookEndpointLifecycle(innerEp: Endpoint, outerEp: Endpoint) { hookEndpointLifecycle<InnerFields>(innerEp: Endpoint<InnerFields>, outerEp: Endpoint<{}>) {
const _refresh = innerEp.refresh; const _refresh = innerEp.refresh;
innerEp.refresh = function () { innerEp.refresh = function () {
_refresh.call(this); _refresh.call(this);
@ -139,7 +139,7 @@ export class NestedDataspace extends Dataspace {
} }
} }
export function inNestedDataspace(bootProc: Script<void>): Script<void> { export function inNestedDataspace(bootProc: Script<void, {}>): Script<void, {}> {
return outerFacet => { return outerFacet => {
outerFacet.addDataflow(function () {}); outerFacet.addDataflow(function () {});
// ^ eww! Dummy endpoint to keep the root facet of the relay alive. // ^ eww! Dummy endpoint to keep the root facet of the relay alive.