Strict tsconfig; more major steps toward ocap style

This commit is contained in:
Tony Garnock-Jones 2021-01-15 13:38:15 +01:00
parent 595a13dfd6
commit 0f6059cd59
17 changed files with 419 additions and 238 deletions

View File

@ -30,7 +30,7 @@ console.time('box-and-client-' + N.toString());
export function boot(thisFacet) { export function boot(thisFacet) {
thisFacet.spawn('box', function (thisFacet) { thisFacet.spawn('box', function (thisFacet) {
thisFacet.actor.dataspace.declareField(this, 'value', 0); thisFacet.declareField(this, 'value', 0);
thisFacet.addEndpoint(() => { thisFacet.addEndpoint(() => {
// console.log('recomputing published BoxState', this.value); // console.log('recomputing published BoxState', this.value);
return { assertion: BoxState(this.value), analysis: null }; return { assertion: BoxState(this.value), analysis: null };

View File

@ -1,5 +1,5 @@
import { import {
Item, Items, Token, Items,
Pattern, Pattern,
scope, bind, seq, alt, upTo, atom, group, exec, scope, bind, seq, alt, upTo, atom, group, exec,
@ -11,7 +11,7 @@ import * as Matcher from '../syntax/matcher.js';
export type Expr = Items; export type Expr = Items;
export type Statement = Items; export type Statement = Items;
export type Identifier = Item; export type Identifier = Token;
export const block = (acc?: Items) => export const block = (acc?: Items) =>
(acc === void 0) (acc === void 0)
@ -21,15 +21,6 @@ export const block = (acc?: Items) =>
export const statementBoundary = alt(atom(';'), Matcher.newline, Matcher.end); export const statementBoundary = alt(atom(';'), Matcher.newline, Matcher.end);
export const exprBoundary = alt(atom(';'), atom(','), group('{', discard), Matcher.end); export const exprBoundary = alt(atom(';'), atom(','), group('{', discard), Matcher.end);
export interface SpawnStatement {
isDataspace: boolean;
name?: Expr;
initialAssertions: Expr[];
parentIds: Identifier[];
parentInits: Expr[];
bootProcBody: Statement;
}
export const identifier: Pattern<Identifier> = atom(); export const identifier: Pattern<Identifier> = atom();
export function expr(... extraStops: Pattern<any>[]): Pattern<Expr> { export function expr(... extraStops: Pattern<any>[]): Pattern<Expr> {
@ -42,8 +33,34 @@ export function statement(acc: Items): Pattern<void> {
map(statementBoundary, i => i ? acc.push(i) : void 0)))); map(statementBoundary, i => i ? acc.push(i) : void 0))));
} }
export interface FacetAction {
implicitFacet: boolean;
}
export function facetAction<I extends FacetAction, T extends I>(
pattern: (scope: T) => Pattern<any>): Pattern<T>
{
return i => {
const scope = Object.create(null);
scope.implicitFacet = true;
const p = seq(option(map(atom('.'), _ => scope.implicitFacet = false)), pattern(scope));
const r = p(i);
if (r === null) return null;
return [scope, r[1]];
};
}
export interface SpawnStatement extends FacetAction {
isDataspace: boolean;
name?: Expr;
initialAssertions: Expr[];
parentIds: Identifier[];
parentInits: Expr[];
bootProcBody: Statement;
}
export const spawn: Pattern<SpawnStatement> & { headerExpr: Pattern<Expr> } = export const spawn: Pattern<SpawnStatement> & { headerExpr: Pattern<Expr> } =
Object.assign(scope((o: SpawnStatement) => { Object.assign(facetAction((o: SpawnStatement) => {
o.isDataspace = false; o.isDataspace = false;
o.initialAssertions = []; o.initialAssertions = [];
o.parentIds = []; o.parentIds = [];
@ -69,38 +86,50 @@ export const spawn: Pattern<SpawnStatement> & { headerExpr: Pattern<Expr> } =
headerExpr: expr(atom(':asserting'), atom(':let')), headerExpr: expr(atom(':asserting'), atom(':let')),
}); });
export interface FieldDeclarationStatement { export interface FieldDeclarationStatement extends FacetAction {
member: Expr; member: Expr;
expr?: Expr; expr?: Expr;
} }
// Principal: Dataspace, but only for implementation reasons, so really Facet
export const fieldDeclarationStatement: Pattern<FieldDeclarationStatement> = export const fieldDeclarationStatement: Pattern<FieldDeclarationStatement> =
scope(o => seq(atom('field'), facetAction(o => seq(atom('field'),
bind(o, 'member', expr(atom('='))), bind(o, 'member', expr(atom('='))),
option(seq(atom('='), bind(o, 'expr', expr()))))); option(seq(atom('='), bind(o, 'expr', expr()))),
statementBoundary));
export interface AssertionEndpointStatement { export interface AssertionEndpointStatement extends FacetAction {
isDynamic: boolean, isDynamic: boolean,
template: Expr, template: Expr,
test?: Expr, test?: Expr,
} }
// Principal: Facet
export const assertionEndpointStatement: Pattern<AssertionEndpointStatement> = export const assertionEndpointStatement: Pattern<AssertionEndpointStatement> =
scope(o => { facetAction(o => {
o.isDynamic = true; o.isDynamic = true;
return seq(atom('assert'), return seq(atom('assert'),
option(map(atom(':snapshot'), _ => o.isDynamic = false)), option(map(atom(':snapshot'), _ => o.isDynamic = false)),
bind(o, 'template', expr(seq(atom('when'), group('(', discard)))), bind(o, 'template', expr(seq(atom('when'), group('(', discard)))),
option(seq(atom('when'), group('(', bind(o, 'test', expr()))))); option(seq(atom('when'), group('(', bind(o, 'test', expr())))),
statementBoundary);
}); });
export const dataflowStatement: Pattern<Statement> = export interface StatementFacetAction extends FacetAction {
value(o => { body: Statement;
o.value = []; }
return seq(atom('dataflow'), statement(o.value));
});
export interface EventHandlerEndpointStatement { export function statementFacetAction(kw: Pattern<any>): Pattern<StatementFacetAction> {
return facetAction(o => {
o.body = [];
return seq(kw, statement(o.body));
});
}
// Principal: Facet
export const dataflowStatement = statementFacetAction(atom('dataflow'));
export interface EventHandlerEndpointStatement extends FacetAction {
terminal: boolean; terminal: boolean;
triggerType: 'dataflow' | 'start' | 'stop' | 'asserted' | 'retracted' | 'message'; triggerType: 'dataflow' | 'start' | 'stop' | 'asserted' | 'retracted' | 'message';
isDynamic: boolean; isDynamic: boolean;
@ -108,8 +137,9 @@ export interface EventHandlerEndpointStatement {
body: Statement; body: Statement;
} }
// Principal: Facet
export const eventHandlerEndpointStatement: Pattern<EventHandlerEndpointStatement> = export const eventHandlerEndpointStatement: Pattern<EventHandlerEndpointStatement> =
scope(o => { facetAction(o => {
o.terminal = false; o.terminal = false;
o.isDynamic = true; o.isDynamic = true;
o.body = []; o.body = [];
@ -125,8 +155,9 @@ export const eventHandlerEndpointStatement: Pattern<EventHandlerEndpointStatemen
atom('message')), atom('message')),
e => e.text)), e => e.text)),
option(map(atom(':snapshot'), _ => o.isDynamic = false)), option(map(atom(':snapshot'), _ => o.isDynamic = false)),
bind(o, 'pattern', expr()), bind(o, 'pattern', expr(atom('=>'))),
option(statement(o.body))))); alt(seq(atom('=>'), statement(o.body)),
statementBoundary))));
}); });
export interface TypeDefinitionStatement { export interface TypeDefinitionStatement {
@ -136,6 +167,7 @@ export interface TypeDefinitionStatement {
wireName?: Expr; wireName?: Expr;
} }
// Principal: none
export const typeDefinitionStatement: Pattern<TypeDefinitionStatement> = export const typeDefinitionStatement: Pattern<TypeDefinitionStatement> =
scope(o => seq(bind(o, 'expectedUse', map(alt(atom('message'), atom('assertion')), e => e.text)), scope(o => seq(bind(o, 'expectedUse', map(alt(atom('message'), atom('assertion')), e => e.text)),
atom('type'), atom('type'),
@ -145,24 +177,39 @@ export const typeDefinitionStatement: Pattern<TypeDefinitionStatement> =
bind(o, 'wireName', withoutSpace(upTo(statementBoundary))))), bind(o, 'wireName', withoutSpace(upTo(statementBoundary))))),
statementBoundary)); statementBoundary));
export const messageSendStatement: Pattern<Expr> = export interface MessageSendStatement extends FacetAction {
value(o => seq(atom('send'), bind(o, 'value', withoutSpace(upTo(statementBoundary))), statementBoundary)); expr: Expr;
}
export interface DuringStatement { // Principal: Facet
export const messageSendStatement: Pattern<MessageSendStatement> =
facetAction(o => seq(atom('send'),
bind(o, 'expr', withoutSpace(upTo(statementBoundary))),
statementBoundary));
export interface DuringStatement extends FacetAction {
pattern: Expr; pattern: Expr;
body: Statement; body: Statement;
} }
// Principal: Facet
export const duringStatement: Pattern<DuringStatement> = export const duringStatement: Pattern<DuringStatement> =
scope(o => { facetAction(o => {
o.body = []; o.body = [];
return seq(atom('during'), return seq(atom('during'),
bind(o, 'pattern', expr()), bind(o, 'pattern', expr()),
statement(o.body)); statement(o.body));
}); });
export const reactStatement: Pattern<Statement> = // Principal: Facet
export const reactStatement = statementFacetAction(atom('react'));
// Principal: none
export const bootStatement: Pattern<Statement> =
value(o => { value(o => {
o.value = []; o.value = [];
return seq(atom('react'), statement(o.value)); return seq(atom('boot'), statement(o.value));
}); });
// Principal: Facet
export const stopStatement = statementFacetAction(atom('stop'));

View File

@ -0,0 +1 @@
export const BootProc = '__SYNDICATE__bootProc';

View File

@ -1,6 +1,8 @@
import fs from 'fs'; import fs from 'fs';
import * as S from '../syntax/index.js'; import * as S from '../syntax/index.js';
import { ArrayList, Substitution } from '../syntax/index.js';
import * as G from './grammar.js'; import * as G from './grammar.js';
import { BootProc } from './internals.js';
export function main(argv: string[]) { export function main(argv: string[]) {
let [ inputFilename ] = argv.slice(2); let [ inputFilename ] = argv.slice(2);
@ -12,6 +14,8 @@ export function main(argv: string[]) {
let tree = reader.readToEnd(); let tree = reader.readToEnd();
let macro = new S.Templates(); let macro = new S.Templates();
tree = macro.template()`import * as __SYNDICATE__ from '@syndicate/core';\n${tree}`;
let passNumber = 1; let passNumber = 1;
let expansionNeeded = true; let expansionNeeded = true;
function expand<T>(p: S.Pattern<T>, f: (t: T) => S.Items) { function expand<T>(p: S.Pattern<T>, f: (t: T) => S.Items) {
@ -20,29 +24,90 @@ export function main(argv: string[]) {
return f(t); return f(t);
}); });
} }
function receiverFor(s: G.FacetAction): Substitution {
return (s.implicitFacet) ? 'thisFacet.' : '.';
}
function expandFacetAction<T extends G.FacetAction>(p: S.Pattern<T>, f: (t: T) => S.Items) {
expand(p, t => macro.template()`${receiverFor(t)}${f(t)}`);
}
while (expansionNeeded) { while (expansionNeeded) {
if (passNumber >= 128) { if (passNumber >= 128) {
throw new Error(`Too many compiler passes (${passNumber})!`); throw new Error(`Too many compiler passes (${passNumber})!`);
} }
expansionNeeded = false; expansionNeeded = false;
expand(G.spawn, expandFacetAction(
s => macro.template()`SPAWN[${s.name ?? []}][${S.joinItems(s.initialAssertions, ', ')}][[${s.bootProcBody}]]`); G.spawn,
expand(G.fieldDeclarationStatement, s => {
s => macro.template()`FIELD[${s.member}][${s.expr ?? []}]`); let proc = macro.template()`function (thisFacet) {${s.bootProcBody}}`;
expand(G.assertionEndpointStatement, if (s.isDataspace) proc = macro.template()`__SYNDICATE__.inNestedDataspace(${proc})`;
s => macro.template()`ASSERT[${''+s.isDynamic}][${s.template}][${s.test ?? []}]`); let assertions = (s.initialAssertions.length > 0)
expand(G.dataflowStatement, ? macro.template()`, new __SYNDICATE__.Set([${S.joinItems(s.initialAssertions, ', ')}])`
e => macro.template()`DATAFLOW[${e}]`); : ``;
expand(G.eventHandlerEndpointStatement, return macro.template()`_spawn(${s.name ?? 'null'}, ${proc}${assertions});`;
s => macro.template()`EVENTHANDLER[${`${s.terminal}/${s.isDynamic}`}][${s.triggerType}][${s.pattern}][${s.body}]`); });
expand(G.typeDefinitionStatement, expandFacetAction(
s => macro.template()`TYPEDEF[${s.expectedUse}][${[s.label]}][${S.joinItems(s.fields.map(f => [f]), ' -- ')}][${s.wireName ?? []}]`); G.fieldDeclarationStatement,
expand(G.messageSendStatement, s => {
e => macro.template()`SEND[${e}]`); function isArrayProp(): [S.Substitution, S.Substitution] | null {
expand(G.duringStatement, if (s.member.length === 0) return null;
s => macro.template()`DURING[${s.pattern}][${s.body}]`); const x = s.member[s.member.length - 1];
expand(G.reactStatement, if (!S.isGroup(x)) return null;
e => macro.template()`REACT[${e}]`); if (x.start.text !== '[') return null;
return [s.member.slice(0, s.member.length - 1), x.items];
}
function isSimpleProp(): [S.Substitution, S.Substitution] | null {
if (s.member.length < 2) return null;
const r = S.value<G.Identifier>(o => S.seq(S.atom('.'), S.bind(o, 'value', G.identifier)))(
new ArrayList(s.member, s.member.length - 2));
if (r === null) return null;
const id = r[0];
return [s.member.slice(0, s.member.length - 2),
[ { start: id.start,
end: id.end,
type: S.TokenType.STRING,
text: JSON.stringify(id.text) } ]];
}
const [obj, prop] = isArrayProp()
?? isSimpleProp()
?? ["__SYNDICATE__.INVALID_PROPERTY_SYNTAX", "''"];
return macro.template()`declareField(${obj}, ${prop}, ${s.expr ?? 'void 0'});`;
});
expandFacetAction(
G.assertionEndpointStatement,
s => macro.template()`addEndpoint(thisFacet => (${s.test ?? 'true'}) && (${s.template}), ${''+s.isDynamic});`);
expandFacetAction(
G.dataflowStatement,
s => macro.template()`addDataflow(function (thisFacet) {${s.body}});`);
expandFacetAction(
G.eventHandlerEndpointStatement,
s => {
return macro.template()`EVENTHANDLER[${`${s.terminal}/${s.isDynamic}`}][${s.triggerType}][${s.pattern ?? []}][${s.body}]`;
});
expandFacetAction(
G.duringStatement,
s => macro.template()`DURING[${s.pattern}][${s.body}]`);
expand(
G.typeDefinitionStatement,
s => {
const l = JSON.stringify(s.label.text);
const fs = JSON.stringify(s.fields.map(f => f.text));
return macro.template()`const ${[s.label]} = __SYNDICATE__.Record.makeConstructor(${s.wireName ?? l}, ${fs});`;
});
expandFacetAction(
G.messageSendStatement,
s => macro.template()`_send(${s.expr});`);
expandFacetAction(
G.reactStatement,
s => macro.template()`addChildFacet(function (thisFacet) {${s.body}});`);
expand(
G.bootStatement,
s => macro.template()`export function ${BootProc}(thisFacet) {${s}}`);
expandFacetAction(
G.stopStatement,
s => macro.template()`_stop(function (thisFacet) {${s.body}});`);
} }
console.log(S.itemText(tree, { color: true, missing: '\x1b[41m□\x1b[0m' })); console.log(S.itemText(tree, { color: true, missing: '\x1b[41m□\x1b[0m' }));

View File

@ -101,10 +101,11 @@ export class Graph<SubjectId, ObjectId> {
} }
} }
defineObservableProperty(obj: object, defineObservableProperty<T, K extends keyof T>(
prop: string, obj: T,
value: any, prop: K,
options: PropertyOptions<ObjectId>) value: T[K],
options: PropertyOptions<ObjectId>)
{ {
const { objectId, noopGuard } = options; const { objectId, noopGuard } = options;
Object.defineProperty(obj, prop, { Object.defineProperty(obj, prop, {
@ -125,9 +126,10 @@ export class Graph<SubjectId, ObjectId> {
return objectId; return objectId;
} }
static newScope(o: object): object { static newScope<T, R extends T>(o: T): R {
function O() {} const Scope: { new (): R, prototype: T } =
O.prototype = o; (function Scope () {}) as unknown as ({ new (): R, prototype: T });
return new O(); Scope.prototype = o;
return new Scope();
} }
} }

View File

@ -82,18 +82,6 @@ export abstract class Dataspace {
this.pendingTurns = [new Turn(null, [new Spawn(null, bootProc, new Set())])]; this.pendingTurns = [new Turn(null, [new Spawn(null, bootProc, new Set())])];
} }
_inScript = true;
withNonScriptContext<T>(task: Task<T>): T {
let savedInScript = this._inScript;
this._inScript = false;
try {
return task();
} finally {
this._inScript = savedInScript;
}
}
abstract start(): this; abstract start(): this;
abstract ground(): Ground; abstract ground(): Ground;
@ -101,29 +89,6 @@ export abstract class Dataspace {
return this.ground().backgroundTask(); return this.ground().backgroundTask();
} }
referenceField(obj: DataflowObservableObject, prop: string) {
if (!(prop in obj)) {
this.dataflow.recordObservation([obj, prop]);
}
return obj[prop];
}
declareField(obj: DataflowObservableObject, prop: string, init: any) {
if (prop in obj) {
obj[prop] = init;
} else {
this.dataflow.defineObservableProperty(obj, prop, init, {
objectId: [obj, prop],
noopGuard: is
});
}
}
deleteField(obj: DataflowObservableObject, prop: string) {
this.dataflow.recordDamage([obj, prop]);
delete obj[prop];
}
runTasks() { // TODO: rename? runTasks() { // TODO: rename?
this.runPendingTasks(); this.runPendingTasks();
this.performPendingActions(); this.performPendingActions();
@ -153,17 +118,15 @@ export abstract class Dataspace {
} }
refreshAssertions() { refreshAssertions() {
this.withNonScriptContext(() => { this.dataflow.repairDamage((ep) => {
this.dataflow.repairDamage((ep) => { let facet = ep.facet;
let facet = ep.facet; if (facet.isLive) { // TODO: necessary test, or tautological?
if (facet.isLive) { // TODO: necessary test, or tautological? facet.invokeScript(f => f.withNonScriptContext(() => ep.refresh()));
facet.invokeScript(() => ep.refresh()); }
}
});
}); });
} }
addActor(name: any, bootProc: Script<void>, initialAssertions: Set, parentActor: Actor | undefined) { addActor(name: any, bootProc: Script<void>, 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);
@ -178,7 +141,7 @@ export abstract class Dataspace {
applyPatch(ac: Actor, delta: Bag) { applyPatch(ac: Actor, delta: Bag) {
// if (!delta.isEmpty()) debug('applyPatch BEGIN', ac && ac.toString()); // if (!delta.isEmpty()) debug('applyPatch BEGIN', ac && ac.toString());
let removals = []; let removals: Array<[number, Value]> = [];
delta.forEach((count, a) => { delta.forEach((count, a) => {
if (count > 0) { if (count > 0) {
// debug('applyPatch +', a && a.toString()); // debug('applyPatch +', a && a.toString());
@ -195,7 +158,7 @@ export abstract class Dataspace {
// if (!delta.isEmpty()) debug('applyPatch END'); // if (!delta.isEmpty()) debug('applyPatch END');
} }
deliverMessage(m: Value, _sendingActor: Actor) { deliverMessage(m: Value, _sendingActor: Actor | null) {
// debug('deliverMessage', sendingActor && sendingActor.toString(), m.toString()); // debug('deliverMessage', sendingActor && sendingActor.toString(), m.toString());
this.index.deliverMessage(m); this.index.deliverMessage(m);
// this.index.deliverMessage(m, (leaf, _m) => { // this.index.deliverMessage(m, (leaf, _m) => {
@ -208,11 +171,11 @@ export abstract class Dataspace {
} }
subscribe(handler: Skeleton.Analysis) { subscribe(handler: Skeleton.Analysis) {
this.index.addHandler(handler, handler.callback); this.index.addHandler(handler, handler.callback!);
} }
unsubscribe(handler: Skeleton.Analysis) { unsubscribe(handler: Skeleton.Analysis) {
this.index.removeHandler(handler, handler.callback); this.index.removeHandler(handler, handler.callback!);
} }
endpointHook(_facet: Facet, _endpoint: Endpoint) { endpointHook(_facet: Facet, _endpoint: Endpoint) {
@ -269,7 +232,7 @@ export class Actor {
let tasks = this.pendingTasks; let tasks = this.pendingTasks;
for (let i = 0; i < Priority._count; i++) { for (let i = 0; i < Priority._count; i++) {
let q = tasks[i]; let q = tasks[i];
if (q.length > 0) return q.shift(); if (q.length > 0) return q.shift()!;
} }
return null; return null;
} }
@ -287,12 +250,12 @@ export class Actor {
this.pendingTasks[priority].push(task); this.pendingTasks[priority].push(task);
} }
addFacet(parentFacet: Facet, bootProc: Script<void>, checkInScript: boolean = false) { addFacet(parentFacet: Facet | null, bootProc: Script<void>, checkInScript: boolean = false) {
if (checkInScript && !this.dataspace._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(this, parentFacet);
f.invokeScript(f => this.dataspace.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()) {
f._terminate(); f._terminate();
@ -352,7 +315,7 @@ export class Actor {
} }
abstract class Action { abstract class Action {
abstract perform(ds: Dataspace, ac: Actor): void; abstract perform(ds: Dataspace, ac: Actor | null): void;
} }
class Patch extends Action { class Patch extends Action {
@ -363,8 +326,8 @@ class Patch extends Action {
this.changes = changes; this.changes = changes;
} }
perform(ds: Dataspace, ac: Actor): void { perform(ds: Dataspace, ac: Actor | null): void {
ds.applyPatch(ac, this.changes); ds.applyPatch(ac!, this.changes);
} }
adjust(a: Value, count: number) { adjust(a: Value, count: number) {
@ -380,7 +343,7 @@ class Message extends Action {
this.body = fromJS(body); this.body = fromJS(body);
} }
perform(ds: Dataspace, ac: Actor): void { perform(ds: Dataspace, ac: Actor | null): void {
ds.deliverMessage(this.body, ac); ds.deliverMessage(this.body, ac);
} }
} }
@ -397,7 +360,7 @@ class Spawn extends Action {
this.initialAssertions = initialAssertions; this.initialAssertions = initialAssertions;
} }
perform(ds: Dataspace, ac: Actor): void { perform(ds: Dataspace, ac: Actor | null): void {
ds.addActor(this.name, this.bootProc, this.initialAssertions, ac); ds.addActor(this.name, this.bootProc, this.initialAssertions, ac);
} }
} }
@ -405,7 +368,8 @@ class Spawn extends Action {
class Quit extends Action { // TODO: rename? Perhaps to Cleanup? class Quit extends Action { // TODO: rename? Perhaps to Cleanup?
// Pseudo-action - not for userland use. // Pseudo-action - not for userland use.
perform(ds: Dataspace, ac: Actor): void { perform(ds: Dataspace, ac: Actor | null): void {
if (ac === null) throw new Error("Internal error: Quit action with null actor");
ds.applyPatch(ac, ac.cleanupChanges); ds.applyPatch(ac, ac.cleanupChanges);
ds.actors.delete(ac.id); ds.actors.delete(ac.id);
// debug('Quit', ac && ac.toString()); // debug('Quit', ac && ac.toString());
@ -420,9 +384,9 @@ class DeferredTurn extends Action {
this.continuation = continuation; this.continuation = continuation;
} }
perform(_ds: Dataspace, ac: Actor): void { perform(_ds: Dataspace, ac: Actor | null): void {
// debug('DeferredTurn', ac && ac.toString()); // debug('DeferredTurn', ac && ac.toString());
ac.scheduleTask(this.continuation); ac!.scheduleTask(this.continuation);
} }
} }
@ -449,6 +413,7 @@ export class Facet {
readonly stopScripts: Array<Script<void>> = []; readonly stopScripts: Array<Script<void>> = [];
readonly children = new IdentitySet<Facet>(); readonly children = new IdentitySet<Facet>();
readonly fields: any; readonly fields: any;
inScript = true;
constructor(actor: Actor, parent: Facet | null) { constructor(actor: Actor, parent: Facet | null) {
this.id = actor.dataspace.nextId++; this.id = actor.dataspace.nextId++;
@ -467,6 +432,16 @@ export class Facet {
this.fields[DataflowObservableObjectId] = () => this.id; this.fields[DataflowObservableObjectId] = () => this.id;
} }
withNonScriptContext<T>(task: Task<T>): T {
let savedInScript = this.inScript;
this.inScript = false;
try {
return task();
} finally {
this.inScript = savedInScript;
}
}
_abort(emitPatches: boolean) { _abort(emitPatches: boolean) {
this.isLive = false; this.isLive = false;
this.children.forEach(child => child._abort(emitPatches)); this.children.forEach(child => child._abort(emitPatches));
@ -517,12 +492,17 @@ export class Facet {
}, Priority.GC); }, Priority.GC);
} }
// This alias exists because of the naive expansion done by the parser.
_stop(continuation?: Script<void>) {
this.stop(continuation);
}
stop(continuation?: Script<void>) { stop(continuation?: Script<void>) {
this.parent.invokeScript(() => { this.parent!.invokeScript(() => {
this.actor.scheduleTask(() => { this.actor.scheduleTask(() => {
this._terminate(); this._terminate();
if (continuation) { if (continuation) {
this.parent.scheduleScript(parent => continuation.call(this.fields, parent)); this.parent!.scheduleScript(parent => continuation.call(this.fields, parent));
// ^ TODO: is this the correct scope to use?? // ^ TODO: is this the correct scope to use??
} }
}); });
@ -539,7 +519,7 @@ export class Facet {
this.stopScripts.push(s); this.stopScripts.push(s);
} }
addEndpoint(updateFun: () => EndpointSpec, isDynamic: boolean = true): Endpoint { addEndpoint(updateFun: Script<EndpointSpec>, isDynamic: boolean = true): Endpoint {
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;
@ -582,7 +562,7 @@ export class Facet {
this.scheduleScript(() => { this.scheduleScript(() => {
if (this.isLive) { if (this.isLive) {
this.actor.dataspace.dataflow.withSubject(subjectId, () => this.actor.dataspace.dataflow.withSubject(subjectId, () =>
subjectFun.call(this.fields)); subjectFun.call(this.fields, this));
} }
}, priority); }, priority);
return { assertion: void 0, analysis: null }; return { assertion: void 0, analysis: null };
@ -624,7 +604,7 @@ 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: (f: Facet, ... 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: (f: Facet, ... args: T) => void): (... args: T) => void {
@ -638,22 +618,32 @@ export class Facet {
} }
ensureFacetSetup(what: string) { ensureFacetSetup(what: string) {
if (this.actor.dataspace._inScript) { if (this.inScript) {
throw new Error(`Cannot ${what} outside facet setup; are you missing \`react { ... }\`?`); throw new Error(`Cannot ${what} outside facet setup; are you missing \`react { ... }\`?`);
} }
} }
ensureNonFacetSetup(what: string, keyword: string) { ensureNonFacetSetup(what: string, keyword: string) {
if (!this.actor.dataspace._inScript) { if (!this.inScript) {
throw new Error(`Cannot ${what} during facet setup; are you missing \`${keyword} { ... }\`?`); throw new Error(`Cannot ${what} during facet setup; are you missing \`${keyword} { ... }\`?`);
} }
} }
// This alias exists because of the naive expansion done by the parser.
_send(body: any) {
this.send(body);
}
send(body: any) { send(body: any) {
this.ensureNonFacetSetup('`send`', 'on start'); this.ensureNonFacetSetup('`send`', 'on start');
this.enqueueScriptAction(new Message(body)); this.enqueueScriptAction(new Message(body));
} }
// This alias exists because of the naive expansion done by the parser.
_spawn(name: any, bootProc: Script<void>, initialAssertions?: Set) {
this.spawn(name, bootProc, initialAssertions);
}
spawn(name: any, bootProc: Script<void>, initialAssertions?: Set) { spawn(name: any, bootProc: Script<void>, initialAssertions?: Set) {
this.ensureNonFacetSetup('`spawn`', 'on start'); this.ensureNonFacetSetup('`spawn`', 'on start');
this.enqueueScriptAction(new Spawn(name, bootProc, initialAssertions)); this.enqueueScriptAction(new Spawn(name, bootProc, initialAssertions));
@ -667,6 +657,33 @@ export class Facet {
scheduleScript(script: Script<void>, priority?: Priority) { scheduleScript(script: Script<void>, priority?: Priority) {
this.actor.scheduleTask(this.wrap(script), priority); this.actor.scheduleTask(this.wrap(script), priority);
} }
declareField<T extends DataflowObservableObject, K extends keyof T & string>(obj: T, prop: K, init: T[K]) {
if (prop in obj) {
obj[prop] = init;
} else {
this.actor.dataspace.dataflow.defineObservableProperty(obj, prop, init, {
objectId: [obj, prop],
noopGuard: is
});
}
}
// referenceField(obj: DataflowObservableObject, prop: string) {
// if (!(prop in obj)) {
// this.actor.dataspace.dataflow.recordObservation([obj, prop]);
// }
// return obj[prop];
// }
// deleteField(obj: DataflowObservableObject, prop: string) {
// this.actor.dataspace.dataflow.recordDamage([obj, prop]);
// delete obj[prop];
// }
addChildFacet(bootProc: Script<void>) {
this.actor.addFacet(this, bootProc, true);
}
} }
export class Endpoint { export class Endpoint {
@ -685,6 +702,7 @@ export class Endpoint {
let initialSpec = ds.dataflow.withSubject(isDynamic ? this : undefined, let initialSpec = ds.dataflow.withSubject(isDynamic ? this : undefined,
() => updateFun.call(facet.fields, facet)); () => updateFun.call(facet.fields, facet));
this._install(initialSpec); this._install(initialSpec);
this.spec = initialSpec; // keeps TypeScript's undefinedness-checker happy
facet.endpoints.set(this.id, this); facet.endpoints.set(this.id, this);
} }

View File

@ -38,6 +38,6 @@ export function randomId(byteCount: number, hexOutput: boolean = false): string
if (hexOutput) { if (hexOutput) {
return Bytes.from(buf).toHex(); return Bytes.from(buf).toHex();
} else { } else {
return _btoa(String.fromCharCode.apply(null, buf)).replace(/=/g,''); return _btoa(String.fromCharCode.apply(null, buf as unknown as number[])).replace(/=/g,'');
} }
} }

View File

@ -16,7 +16,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 { Value } from 'preserves'; import { Value, Record } from 'preserves';
import { $Special } from './special.js'; import { $Special } from './special.js';
import { Dataspace, Facet, Actor, Endpoint, Script } from './dataspace.js'; import { Dataspace, Facet, Actor, Endpoint, Script } from './dataspace.js';
@ -43,44 +43,47 @@ export class NestedDataspace extends Dataspace {
} }
endpointHook(facet: Facet, innerEp: Endpoint) { endpointHook(facet: Facet, innerEp: Endpoint) {
const innerDs = this;
super.endpointHook(facet, innerEp); super.endpointHook(facet, innerEp);
if (Observe.isClassOf(innerEp.spec.assertion) &&
Inbound.isClassOf(innerEp.spec.assertion[0])) const innerAssertion = innerEp.spec.assertion;
{ if (!Observe.isClassOf(innerAssertion)) return;
// We know that innerEp.spec.assertion is an Observe(Inbound(...)). Also, if const wrapper = innerAssertion[0];
// innerEp.spec.analysis exists, it will be consonant with innerEp.spec.assertion. if (!Inbound.isClassOf(wrapper)) return;
// Beware of completely-constant patterns, which cause skeleton to be null!
this.hookEndpointLifecycle(innerEp, this.outerFacet.addEndpoint(() => { // We know that innerAssertion is an Observe(Inbound(...)). Also, if
const assertion = Observe(innerEp.spec.assertion[0][0]); // innerEp.spec.analysis exists, it will be consonant with innerAssertion. Beware of
const h = innerEp.spec.analysis; // completely-constant patterns, which cause skeleton to be null!
const innerCallback = h.callback;
const callback = (innerCallback === void 0) ? void 0 : const innerDs = this;
function (evt: EventType, captures: Array<Value>) { this.hookEndpointLifecycle(innerEp, this.outerFacet.addEndpoint(() => {
innerCallback.call(this, evt, captures); const assertion = Observe(wrapper[0]);
innerDs.start(); const h = innerEp.spec.analysis!;
}; const innerCallback = h.callback;
const analysis: Analysis | null = (h === null) ? null : const callback = (innerCallback === void 0) ? void 0 :
(h.skeleton === void 0 function (evt: EventType, captures: Array<Value>) {
? { innerCallback.call(null, evt, captures);
skeleton: void 0, innerDs.start();
constPaths: h.constPaths, };
constVals: h.constVals.map((v) => v[0]), const analysis: Analysis | null = (h === null) ? null :
capturePaths: h.capturePaths.map((p) => p.slice(1)), (h.skeleton === null
assertion, ? {
callback skeleton: null,
} constPaths: h.constPaths,
: { constVals: h.constVals.map(v => (v as Record)[0]),
skeleton: h.skeleton[1], capturePaths: h.capturePaths.map(p => p.slice(1)),
constPaths: h.constPaths.map((p) => p.slice(1)), assertion,
constVals: h.constVals, callback
capturePaths: h.capturePaths.map((p) => p.slice(1)), }
assertion, : {
callback skeleton: h.skeleton.members[0],
}); constPaths: h.constPaths.map(p => p.slice(1)),
return { assertion, analysis }; constVals: h.constVals,
}, false)); capturePaths: h.capturePaths.map(p => p.slice(1)),
} assertion,
callback
});
return { assertion, analysis };
}, false));
} }
adjustIndex(a: Value, count: number) { adjustIndex(a: Value, count: number) {

View File

@ -17,7 +17,7 @@
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
import { IdentitySet } from './idcoll.js'; import { IdentitySet } from './idcoll.js';
import { is, Value, Record, Set, Dictionary, canonicalString } from 'preserves'; import { is, Value, Record, Set, Dictionary, canonicalString, preserves } from 'preserves';
import { Bag, ChangeDescription } from './bag.js'; import { Bag, ChangeDescription } from './bag.js';
import { Discard, Capture, Observe } from './assertions.js'; import { Discard, Capture, Observe } from './assertions.js';
@ -33,7 +33,8 @@ export enum EventType {
export type HandlerCallback = (eventType: EventType, bindings: Array<Value>) => void; export type HandlerCallback = (eventType: EventType, bindings: Array<Value>) => void;
export type Shape = string; export type Shape = string;
export type Skeleton = null | { shape: Shape, members: Skeleton[] }; export type NonEmptySkeleton = { shape: Shape, members: Skeleton[] };
export type Skeleton = null | NonEmptySkeleton;
export type Path = Array<number>; export type Path = Array<number>;
export interface Analysis { export interface Analysis {
skeleton: Skeleton; skeleton: Skeleton;
@ -58,10 +59,10 @@ export class Index {
constValMap = new Dictionary(); constValMap = new Dictionary();
continuation.cachedAssertions.forEach((a) => { continuation.cachedAssertions.forEach((a) => {
const key = projectPaths(a, constPaths); const key = projectPaths(a, constPaths);
let leaf = constValMap.get(key); let leaf = constValMap!.get(key);
if (!leaf) { if (!leaf) {
leaf = new Leaf(); leaf = new Leaf();
constValMap.set(key, leaf); constValMap!.set(key, leaf);
} }
leaf.cachedAssertions.add(a); leaf.cachedAssertions.add(a);
}); });
@ -76,7 +77,7 @@ export class Index {
if (!handler) { if (!handler) {
const cachedCaptures = new Bag(); const cachedCaptures = new Bag();
leaf.cachedAssertions.forEach((a) => leaf.cachedAssertions.forEach((a) =>
cachedCaptures._items.update(projectPaths(a, capturePaths), n => n + 1, 0)); cachedCaptures._items.update(projectPaths(a, capturePaths), n => n! + 1, 0));
handler = new Handler(cachedCaptures); handler = new Handler(cachedCaptures);
leaf.handlerMap.set(capturePaths, handler); leaf.handlerMap.set(capturePaths, handler);
} }
@ -159,7 +160,7 @@ class Node {
} }
extend(skeleton: Skeleton): Continuation { extend(skeleton: Skeleton): Continuation {
const path = []; const path: Path = [];
function walkNode(node: Node, function walkNode(node: Node,
popCount: number, popCount: number,
@ -279,14 +280,14 @@ class Handler {
} }
} }
function classOf(v: any): string | null { function classOf(v: any): string {
if (Record.isRecord(v)) { if (Record.isRecord(v)) {
const ci = v.getConstructorInfo(); const ci = v.getConstructorInfo();
return canonicalString(ci.label) + '/' + ci.arity; return canonicalString(ci.label) + '/' + ci.arity;
} else if (Array.isArray(v)) { } else if (Array.isArray(v)) {
return '' + v.length; return '' + v.length;
} else { } else {
return null; throw new Error(preserves`Cannot route based on ${v}`);
} }
} }
@ -306,10 +307,10 @@ function projectPaths(v: Value, paths: Array<Path>) {
} }
export function analyzeAssertion(a: Value): Analysis { export function analyzeAssertion(a: Value): Analysis {
const constPaths = []; const constPaths: Path[] = [];
const constVals = []; const constVals: Value[] = [];
const capturePaths = []; const capturePaths: Path[] = [];
const path = []; const path: Path = [];
function walk(a: Value): Skeleton { function walk(a: Value): Skeleton {
if (Capture.isClassOf(a)) { if (Capture.isClassOf(a)) {
@ -328,7 +329,7 @@ export function analyzeAssertion(a: Value): Analysis {
let aa = a as Array<Value>; let aa = a as Array<Value>;
// ^ We know this is safe because it's either Record or Array // ^ We know this is safe because it's either Record or Array
let arity = aa.length; let arity = aa.length;
let result = { shape: cls, members: [] }; let result: NonEmptySkeleton = { shape: cls, members: [] };
path.push(0); path.push(0);
for (let i = 0; i < arity; i++) { for (let i = 0; i < arity; i++) {
path[path.length - 1] = i; path[path.length - 1] = i;
@ -349,7 +350,7 @@ export function analyzeAssertion(a: Value): Analysis {
} }
export function match(p: Value, v: Value): Array<Value> | false { export function match(p: Value, v: Value): Array<Value> | false {
const captures = []; const captures: Array<Value> = [];
function walk(p: Value, v: Value): boolean { function walk(p: Value, v: Value): boolean {
if (Capture.isClassOf(p)) { if (Capture.isClassOf(p)) {
@ -376,8 +377,8 @@ export function match(p: Value, v: Value): Array<Value> | false {
return walk(p, v) ? captures : false; return walk(p, v) ? captures : false;
} }
export function isCompletelyConcrete(p: Value) { export function isCompletelyConcrete(p: Value): boolean {
function walk(p: Value) { function walk(p: Value): boolean {
if (Capture.isClassOf(p)) return false; if (Capture.isClassOf(p)) return false;
if (Discard.isClassOf(p)) return false; if (Discard.isClassOf(p)) return false;
@ -388,8 +389,8 @@ export function isCompletelyConcrete(p: Value) {
return walk(p); return walk(p);
} }
export function withoutCaptures(p: Value) { export function withoutCaptures(p: Value): Value {
function walk(p: Value) { function walk(p: Value): Value {
if (Capture.isClassOf(p)) return walk(p[0]); if (Capture.isClassOf(p)) return walk(p[0]);
if (Discard.isClassOf(p)) return p; if (Discard.isClassOf(p)) return p;

View File

@ -27,8 +27,12 @@ export function push<T>(item: T, rest: Stack<T>): NonEmptyStack<T> {
return { item, rest }; return { item, rest };
} }
export function isEmpty<T>(s: Stack<T>): s is null {
return s === null;
}
export function nonEmpty<T>(s: Stack<T>): s is NonEmptyStack<T> { export function nonEmpty<T>(s: Stack<T>): s is NonEmptyStack<T> {
return s !== empty(); return s !== null;
} }
export function rest<T>(s: Stack<T>): Stack<T> { export function rest<T>(s: Stack<T>): Stack<T> {
@ -46,8 +50,9 @@ export function drop<T>(s: Stack<T>, n: number): Stack<T> {
export function dropNonEmpty<T>(s: NonEmptyStack<T>, n: number): NonEmptyStack<T> { export function dropNonEmpty<T>(s: NonEmptyStack<T>, n: number): NonEmptyStack<T> {
while (n--) { while (n--) {
s = s.rest; const next = s.rest;
if (!nonEmpty(s)) throw new Error("dropNonEmpty popped too far"); if (!nonEmpty(next)) throw new Error("dropNonEmpty popped too far");
s = next;
} }
return s; return s;
} }

View File

@ -12,15 +12,22 @@ export interface SourceMap {
mappings: string; mappings: string;
} }
export interface Mapping { export interface NoSourceMapping {
generatedStartColumn?: number; // zero-based generatedStartColumn: number; // zero-based
sourceIndex?: number; }
sourceStartLine?: number; // zero-based (!!) export interface SourceMapping extends NoSourceMapping {
sourceStartColumn?: number; // zero-based sourceIndex: number;
nameIndex?: number; sourceStartLine: number; // zero-based (!!)
sourceStartColumn: number; // zero-based
}
export interface SourceNameMapping extends SourceMapping {
nameIndex: number;
} }
function encodeMapping(entry: Mapping): Array<number> { export type NonEmptyMapping = NoSourceMapping | SourceMapping | SourceNameMapping;
export type Mapping = {} | NonEmptyMapping;
function encodeMapping(entry: NonEmptyMapping): Array<number> {
const a = [entry.generatedStartColumn]; const a = [entry.generatedStartColumn];
if ('sourceIndex' in entry) { if ('sourceIndex' in entry) {
a.push(entry.sourceIndex); a.push(entry.sourceIndex);
@ -33,11 +40,9 @@ function encodeMapping(entry: Mapping): Array<number> {
return a; return a;
} }
function maybeDelta(newValue: number | undefined, oldValue: number | undefined) { function maybeDelta(newValue: number, oldValue: number | undefined): number {
// console.log('maybeDelta', oldValue, newValue); // console.log('maybeDelta', oldValue, newValue);
return (newValue === void 0) ? void 0 return (oldValue === void 0) ? newValue : newValue - oldValue;
: (oldValue === void 0) ? newValue
: newValue - oldValue;
} }
export class CodeWriter { export class CodeWriter {
@ -45,8 +50,8 @@ export class CodeWriter {
readonly pos: Pos; readonly pos: Pos;
readonly sources: Array<string> = []; readonly sources: Array<string> = [];
readonly chunks: Array<string> = []; readonly chunks: Array<string> = [];
readonly mappings: Array<Array<Mapping>> = []; readonly mappings: Array<Array<NonEmptyMapping>> = [];
previous: Mapping = {}; previous: Partial<SourceNameMapping> = {};
previousPos: Pos | null = null; previousPos: Pos | null = null;
constructor(file: string | null) { constructor(file: string | null) {
@ -113,17 +118,21 @@ export class CodeWriter {
return; return;
} }
const n: Mapping = {}; let n: NonEmptyMapping = {
n.generatedStartColumn = maybeDelta(this.pos.column, this.previous.generatedStartColumn); generatedStartColumn: maybeDelta(this.pos.column, this.previous.generatedStartColumn),
};
this.previous.generatedStartColumn = this.pos.column; this.previous.generatedStartColumn = this.pos.column;
if (p.name !== null) { if (p.name !== null) {
const sourceIndex = this.sourceIndexFor(p.name); const sourceIndex = this.sourceIndexFor(p.name);
n.sourceIndex = maybeDelta(sourceIndex, this.previous.sourceIndex); n = {
... n,
sourceIndex: maybeDelta(sourceIndex, this.previous.sourceIndex),
sourceStartColumn: maybeDelta(p.column, this.previous.sourceStartColumn),
sourceStartLine: maybeDelta(p.line - 1, this.previous.sourceStartLine),
};
this.previous.sourceIndex = sourceIndex; this.previous.sourceIndex = sourceIndex;
n.sourceStartColumn = maybeDelta(p.column, this.previous.sourceStartColumn);
this.previous.sourceStartColumn = p.column; this.previous.sourceStartColumn = p.column;
n.sourceStartLine = maybeDelta(p.line - 1, this.previous.sourceStartLine);
this.previous.sourceStartLine = p.line - 1; this.previous.sourceStartLine = p.line - 1;
} }

View File

@ -5,10 +5,14 @@ export interface List<T> extends Iterable<T> {
toArray(): Array<T>; toArray(): Array<T>;
} }
export function atEnd<T>(xs: List<T>): boolean { export function atEnd<T>(xs: List<T>): xs is (List<T> & { item: null, next: null }) {
return xs.item === null; return xs.item === null;
} }
export function notAtEnd<T>(xs: List<T>): xs is (List<T> & { item: T, next: List<T> }) {
return xs.item !== null;
}
export class ArrayList<T> implements List<T> { export class ArrayList<T> implements List<T> {
readonly items: Array<T>; readonly items: Array<T>;
readonly index: number = 0; readonly index: number = 0;
@ -35,9 +39,13 @@ export class ArrayList<T> implements List<T> {
let i: List<T> = this; let i: List<T> = this;
return { return {
next(): IteratorResult<T> { next(): IteratorResult<T> {
const value = i.item; if (notAtEnd(i)) {
if (!atEnd(i)) i = i.next; const value = i.item;
return { done: atEnd(i), value }; i = i.next;
return { done: false, value };
} else {
return { done: true, value: null };
}
} }
}; };
} }

View File

@ -1,6 +1,6 @@
import { Token, TokenType, Items, Item, isGroup, isToken, isSpace, isTokenType } from './tokens.js'; import { Token, TokenType, Items, Item, isGroup, isToken, isSpace, isTokenType } from './tokens.js';
import { Pos } from './position.js'; import { Pos } from './position.js';
import { List, ArrayList, atEnd } from './list.js'; import { List, ArrayList, atEnd, notAtEnd } from './list.js';
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
// Patterns over Item // Patterns over Item
@ -15,21 +15,24 @@ export const succeed: Pattern<void> = i => [void 0, i];
export const discard: Pattern<void> = _i => [void 0, noItems]; export const discard: Pattern<void> = _i => [void 0, noItems];
export const rest: Pattern<Items> = i => [i.toArray(), noItems]; export const rest: Pattern<Items> = i => [i.toArray(), noItems];
export const end: Pattern<void> = i => atEnd(i) ? [void 0, noItems] : null; export const end: Pattern<void> = i => atEnd(i) ? [void 0, noItems] : null;
export const pos: Pattern<Pos> = i => [isGroup(i.item) ? i.item.start.start : i.item.start, i]; export const pos: Pattern<Pos> = i =>
notAtEnd(i)
? [isGroup(i.item) ? i.item.start.start : i.item.start, i]
: null;
export const newline: Pattern<Item> = i => { export const newline: Pattern<Item> = i => {
while (!atEnd(i) && isTokenType(i.item, TokenType.SPACE)) i = i.next; while (notAtEnd(i) && isTokenType(i.item, TokenType.SPACE)) i = i.next;
if (!isTokenType(i.item, TokenType.NEWLINE)) return null; if (!notAtEnd(i) || !isTokenType(i.item, TokenType.NEWLINE)) return null;
return [i.item, i.next]; return [i.item, i.next];
}; };
export function skipSpace(i: List<Item>): List<Item> { export function skipSpace(i: List<Item>): List<Item> {
while (!atEnd(i) && isSpace(i.item)) i = i.next; while (notAtEnd(i) && isSpace(i.item)) i = i.next;
return i; return i;
} }
export function collectSpace(i: List<Item>, acc: Array<Item>): List<Item> { export function collectSpace(i: List<Item>, acc: Array<Item>): List<Item> {
while (!atEnd(i) && isSpace(i.item)) { while (notAtEnd(i) && isSpace(i.item)) {
acc.push(i.item); acc.push(i.item);
i = i.next; i = i.next;
} }
@ -95,7 +98,7 @@ export function exec(thunk: (i: List<Item>) => void): Pattern<void> {
}; };
} }
export function map<T, R>(p: Pattern<T>, f: (T) => R): Pattern<R> { export function map<T, R>(p: Pattern<T>, f: (t: T) => R): Pattern<R> {
return i => { return i => {
const r = p(i); const r = p(i);
if (r === null) return null; if (r === null) return null;
@ -117,6 +120,7 @@ export interface TokenOptions extends ItemOptions {
export function group<T>(opener: string, items: Pattern<T>, options: GroupOptions = {}): Pattern<T> { export function group<T>(opener: string, items: Pattern<T>, options: GroupOptions = {}): Pattern<T> {
return i => { return i => {
if (options.skipSpace ?? true) i = skipSpace(i); if (options.skipSpace ?? true) i = skipSpace(i);
if (!notAtEnd(i)) return null;
if (!isGroup(i.item)) return null; if (!isGroup(i.item)) return null;
if (i.item.start.text !== opener) return null; if (i.item.start.text !== opener) return null;
const r = items(new ArrayList(i.item.items)); const r = items(new ArrayList(i.item.items));
@ -129,6 +133,7 @@ export function group<T>(opener: string, items: Pattern<T>, options: GroupOption
export function atom(text?: string | undefined, options: TokenOptions = {}): Pattern<Token> { export function atom(text?: string | undefined, options: TokenOptions = {}): Pattern<Token> {
return i => { return i => {
if (options.skipSpace ?? true) i = skipSpace(i); if (options.skipSpace ?? true) i = skipSpace(i);
if (!notAtEnd(i)) return null;
if (!isToken(i.item)) return null; if (!isToken(i.item)) return null;
if (i.item.type !== (options.tokenType ?? TokenType.ATOM)) return null; if (i.item.type !== (options.tokenType ?? TokenType.ATOM)) return null;
if (text !== void 0 && i.item.text !== text) return null; if (text !== void 0 && i.item.text !== text) return null;
@ -139,7 +144,7 @@ export function atom(text?: string | undefined, options: TokenOptions = {}): Pat
export function anything(options: ItemOptions = {}): Pattern<Item> { export function anything(options: ItemOptions = {}): Pattern<Item> {
return i => { return i => {
if (options.skipSpace ?? true) i = skipSpace(i); if (options.skipSpace ?? true) i = skipSpace(i);
if (atEnd(i)) return null; if (!notAtEnd(i)) return null;
return [i.item, i.next]; return [i.item, i.next];
}; };
} }
@ -150,7 +155,7 @@ export function upTo(p: Pattern<any>): Pattern<Items> {
while (true) { while (true) {
const r = p(i); const r = p(i);
if (r !== null) return [acc, i]; if (r !== null) return [acc, i];
if (atEnd(i)) break; if (!notAtEnd(i)) break;
acc.push(i.item); acc.push(i.item);
i = i.next; i = i.next;
} }
@ -202,7 +207,7 @@ export function replace<T>(items: Items,
const walkItems = (items: Items): Items => { const walkItems = (items: Items): Items => {
let i: List<Item> = new ArrayList(items); let i: List<Item> = new ArrayList(items);
const acc: Items = []; const acc: Items = [];
while (!atEnd(i = collectSpace(i, acc))) { while (notAtEnd(i = collectSpace(i, acc))) {
const r = p(i); const r = p(i);
if (r !== null) { if (r !== null) {

View File

@ -31,12 +31,12 @@ export class LaxReader implements IterableIterator<Item> {
if (m !== null && !this.stack.some(g => g.start.text === m)) { if (m !== null && !this.stack.some(g => g.start.text === m)) {
if (this.stack.length > 0) { if (this.stack.length > 0) {
this.stackTop().items.push(t); this.stackTop()!.items.push(t);
return 'continue'; return 'continue';
} }
} else { } else {
while (this.stack.length > 0) { while (this.stack.length > 0) {
const inner = this.stack.pop(); const inner = this.stack.pop()!;
if (inner.start.text === m) { if (inner.start.text === m) {
inner.end = t; inner.end = t;
} }
@ -44,7 +44,7 @@ export class LaxReader implements IterableIterator<Item> {
if (this.stack.length === 0) { if (this.stack.length === 0) {
return inner; return inner;
} else { } else {
const outer = this.stackTop(); const outer = this.stackTop()!;
outer.items.push(inner); outer.items.push(inner);
if (inner.start.text === m) { if (inner.start.text === m) {
return 'continue'; return 'continue';
@ -56,35 +56,50 @@ export class LaxReader implements IterableIterator<Item> {
return 'eof'; return 'eof';
} }
shift(): Token { peek(): Token {
return this.scanner.shift() ?? this.scanner.makeToken(this.scanner.mark(), TokenType.CLOSE, ''); return this.scanner.peek() ?? this.scanner.makeToken(this.scanner.mark(), TokenType.CLOSE, '');
}
drop() {
this.scanner.drop();
} }
read(): Item | null { read(): Item | null {
while (true) { while (true) {
let g = this.stackTop(); let g = this.stackTop();
const t = this.shift(); const t = this.peek();
switch (t.type) { switch (t.type) {
case TokenType.SPACE: case TokenType.SPACE:
case TokenType.NEWLINE: case TokenType.NEWLINE:
case TokenType.ATOM: case TokenType.ATOM:
case TokenType.STRING: case TokenType.STRING:
if (g === null) return t; if (g === null) {
this.drop();
return t;
}
if (t.text === ';') { if (t.text === ';') {
while ('(['.indexOf(g.start.text) >= 0) { while ('(['.indexOf(g.start.text) >= 0) {
this.stack.pop(); this.stack.pop();
this.stackTop().items.push(g); const outer = this.stackTop();
g = this.stackTop(); if (outer === null) {
// do not drop the semicolon here
return g;
}
outer.items.push(g);
g = outer;
} }
} }
this.drop();
g.items.push(t); g.items.push(t);
break; break;
case TokenType.OPEN: case TokenType.OPEN:
this.drop();
this.stack.push({ start: t, end: null, items: [] }); this.stack.push({ start: t, end: null, items: [] });
break; break;
case TokenType.CLOSE: { case TokenType.CLOSE: {
this.drop();
const i = this.popUntilMatch(t); const i = this.popUntilMatch(t);
if (i === 'eof') return null; if (i === 'eof') return null;
if (i === 'continue') break; if (i === 'continue') break;

View File

@ -5,7 +5,7 @@ export abstract class Scanner implements IterableIterator<Token> {
readonly pos: Pos; readonly pos: Pos;
charBuffer: string | null = null; charBuffer: string | null = null;
tokenBuffer: Token | null = null; tokenBuffer: Token | null = null;
delimiters = ' \t\n\r\'"`,;()[]{}/'; delimiters = ' \t\n\r\'"`.,;()[]{}/';
constructor(pos: Pos) { constructor(pos: Pos) {
this.pos = pos; this.pos = pos;
@ -55,19 +55,19 @@ export abstract class Scanner implements IterableIterator<Token> {
} }
_collectSpace(buf = '', start = this.mark()): Token { _collectSpace(buf = '', start = this.mark()): Token {
this._while(ch => this.isSpace(ch), ch => buf = buf + ch); this._while(ch => ch !== null && this.isSpace(ch), ch => buf = buf + ch);
return this.makeToken(start, TokenType.SPACE, buf); return this.makeToken(start, TokenType.SPACE, buf);
} }
_punct(type: TokenType): Token { _punct(type: TokenType): Token {
return this.makeToken(this.mark(), type, this.shiftChar()); return this.makeToken(this.mark(), type, this.shiftChar()!);
} }
_str(forbidNewlines: boolean): Token { _str(forbidNewlines: boolean): Token {
const start = this.mark(); const start = this.mark();
const q = this.shiftChar(); const q = this.shiftChar()!;
let buf = q; let buf = q;
let ch: string; let ch: string | null;
while (true) { while (true) {
ch = this.shiftChar(); ch = this.shiftChar();
if (ch !== null) buf = buf + ch; if (ch !== null) buf = buf + ch;
@ -98,7 +98,7 @@ export abstract class Scanner implements IterableIterator<Token> {
} }
_atom(start = this.mark(), buf = ''): Token { _atom(start = this.mark(), buf = ''): Token {
let ch: string; let ch: string | null;
while (true) { while (true) {
ch = this.peekChar(); ch = this.peekChar();
if (ch === null || this.isDelimiter(ch)) { if (ch === null || this.isDelimiter(ch)) {
@ -111,7 +111,7 @@ export abstract class Scanner implements IterableIterator<Token> {
_maybeComment(): Token { _maybeComment(): Token {
const start = this.mark(); const start = this.mark();
let buf = this.shiftChar(); let buf = this.shiftChar()!;
let ch = this.peekChar(); let ch = this.peekChar();
if (ch === null) return this._collectSpace(buf, start); if (ch === null) return this._collectSpace(buf, start);
switch (ch) { switch (ch) {
@ -162,6 +162,7 @@ export abstract class Scanner implements IterableIterator<Token> {
case '`': case '`':
return this._str(false); return this._str(false);
case '.':
case ',': case ',':
case ';': case ';':
return this._punct(TokenType.ATOM); return this._punct(TokenType.ATOM);
@ -170,7 +171,7 @@ export abstract class Scanner implements IterableIterator<Token> {
return this._maybeComment(); return this._maybeComment();
default: default:
return this._atom(this.mark(), this.shiftChar()); return this._atom(this.mark(), this.shiftChar()!);
} }
} }

View File

@ -7,7 +7,7 @@ export function vlqDecode(s: string): Array<number> {
let shift_amount = 0; let shift_amount = 0;
const buf = []; const buf = [];
for (const ch of s) { for (const ch of s) {
const sextet = inverse_alphabet.get(ch); const sextet = inverse_alphabet.get(ch) ?? 0;
acc |= (sextet & 0x1f) << shift_amount; acc |= (sextet & 0x1f) << shift_amount;
shift_amount += 5; shift_amount += 5;
if (!(sextet & 0x20)) { if (!(sextet & 0x20)) {

View File

@ -10,7 +10,8 @@
"esModuleInterop": true, "esModuleInterop": true,
"moduleResolution": "node", "moduleResolution": "node",
"module": "es6", "module": "es6",
"sourceMap": true "sourceMap": true,
"strict": true
}, },
"include": ["src/**/*"] "include": ["src/**/*"]
} }