Change `stop` to refer to a lexical facet, not a dynamic facet.

The previous behaviour of `stop` was inappropriate: it always stopped
`Syndicate.Turn.activeFacet`, which is an instance of dynamic binding.

Now, it instead stops the unique lexically-innermost lexically-apparent
facet - the special name `currentSyndicateFacet` - by default. It is an
error if no such facet is lexically apparent (if `currentSyndicateFacet` is
unbound). This makes it similar to `break` and `continue` in structured
programming.

In addition, an expression denoting a facet can now be used with `stop` to
override this default - again, like `break` and `continue` in some
languages.

Finally, `react` can now be preceded by a label, which binds the label as a
variable denoting the newly-created facet (inside the facet's scope).
This commit is contained in:
Tony Garnock-Jones 2024-03-09 23:04:29 +01:00
parent 644891ce76
commit b4d728ca7a
4 changed files with 234 additions and 26 deletions

View File

@ -17,6 +17,7 @@ import {
compilePattern,
SpawnStatement,
FacetToStop,
} from './grammar';
export function stripShebang(items: Items): Items {
@ -116,16 +117,39 @@ function binderTypeGuard(t: TemplateFunction): (binder: Binder, index: number) =
export function expand(tree: Items, ctx: ExpansionContext): Items {
const macro = new Templates(undefined, { extraDelimiters: ':' });
function terminalWrap(t: TemplateFunction, isTerminal: boolean, body: Statement): Statement {
if (isTerminal) {
return t`__SYNDICATE__.Turn.active._stop(__SYNDICATE__.Turn.activeFacet, () => {${body}})`
function terminalWrap(
t: TemplateFunction,
facetToStop: FacetToStop | 'none' | 'once-wrapper',
body: Statement,
): Statement {
if (facetToStop === 'none') {
return walk(body);
} else {
return body;
const toStop =
facetToStop === 'default' ? 'currentSyndicateFacet' :
facetToStop === 'once-wrapper' ? '__once_facet' :
walk(facetToStop);
const resetCurrentSyndicateFacet =
facetToStop === 'once-wrapper' ? [] :
t`const currentSyndicateFacet = __SYNDICATE__.Turn.activeFacet;`;
return t`__SYNDICATE__.Turn.active._stop(${toStop}, () => {${resetCurrentSyndicateFacet}${walk(body)}})`;
}
}
function facetWrap(t: TemplateFunction, items: Items): Items {
return t`__SYNDICATE__.Turn.active.facet(() => {${items}})`;
function facetWrap(
t: TemplateFunction,
facetName: Identifier | 'default' | 'once-wrapper',
items: Items,
): Items {
if (facetName === 'once-wrapper') {
return t`__SYNDICATE__.Turn.active.facet(() => {const __once_facet = __SYNDICATE__.Turn.activeFacet; ${items}});`;
} else {
const defaultLabel = t`const currentSyndicateFacet = __SYNDICATE__.Turn.activeFacet; `;
const customLabel = facetName === 'default'
? []
: t`const ${facetName.text} = currentSyndicateFacet; `;
return t`__SYNDICATE__.Turn.active.facet(() => {${defaultLabel}${customLabel}${items}});`;
}
}
function x<T>(p: Pattern<T>, f: (v: T, t: TemplateFunction) => Items) {
@ -155,7 +179,7 @@ export function expand(tree: Items, ctx: ExpansionContext): Items {
let body = (spawn === null)
? walk(s.body)
: expandSpawn(spawn, t, t`__SYNDICATE__.Turn.activeFacet.preventInertCheck();`);
: expandSpawn(spawn, t, t` __SYNDICATE__.Turn.activeFacet.preventInertCheck();`);
const sa = compilePattern(s.pattern);
const assertion = t`__SYNDICATE__.Observe({
@ -185,8 +209,9 @@ ${joinItems(sa.captureBinders.map(binderTypeGuard(t)), '\n')}
? t`, new __SYNDICATE__.Set([${commaJoin(s.initialAssertions.map(walk))}])`
: ``;
*/
const n = spawn.name === void 0 ? '' : t` __SYNDICATE__.Turn.activeFacet.actor.name = ${walk(spawn.name)};`;
return t`__SYNDICATE__.Dataspace._spawn${spawn.linkedToken ? 'Link': ''}(() => {${n} ${inject} ${walk(spawn.body)} });`;
const f = t` const currentSyndicateFacet = __SYNDICATE__.Turn.activeFacet;`;
const n = spawn.name === void 0 ? '' : t` currentSyndicateFacet.actor.name = ${walk(spawn.name)};`;
return t`__SYNDICATE__.Dataspace._spawn${spawn.linkedToken ? 'Link': ''}(() => {${f}${n}${inject}${walk(spawn.body)}});`;
}
x(ctx.parser.spawn, expandSpawn);
@ -226,10 +251,10 @@ ${joinItems(sa.captureBinders.map(binderTypeGuard(t)), '\n')}
t`_dataflow(() => {${walk(s.body)}});`);
x(ctx.parser.eventHandlerEndpointStatement, (s, t) => {
const wrap = s.once ? (i: Items) => facetWrap(t, i) : (i: Items) => i;
const wrap = s.once ? (i: Items) => facetWrap(t, 'once-wrapper', i) : (i: Items) => i;
if (s.triggerType === 'dataflow') {
return wrap(t`__SYNDICATE__.Turn.active._dataflow(() => { if (${walk(s.predicate)}) { ${terminalWrap(t, s.terminal, walk(s.body))} } });`);
return wrap(t`__SYNDICATE__.Turn.active._dataflow(() => { if (${walk(s.predicate)}) { ${terminalWrap(t, s.facetToStop, s.body)} } });`);
}
if (s.triggerType === 'stop') {
@ -247,19 +272,19 @@ ${joinItems(sa.captureBinders.map(binderTypeGuard(t)), '\n')}
case 'asserted':
entity = t`{
assert: (${ctx.argDecl(t, '__vs', '__SYNDICATE__.AnyValue')}, ${ctx.argDecl(t, '__handle', '__SYNDICATE__.Handle')}) => {
${guardBody(terminalWrap(t, s.terminal, walk(s.body)))}
${guardBody(terminalWrap(t, s.facetToStop, s.body))}
}
}`;
break;
case 'retracted':
entity = t`__SYNDICATE__.assertionObserver((${ctx.argDecl(t, '__vs', '__SYNDICATE__.AnyValue')}) => {
${guardBody(t`return () => { ${terminalWrap(t, s.terminal, walk(s.body))} };`)}
${guardBody(t`return () => { ${terminalWrap(t, s.facetToStop, s.body)} };`)}
})`;
break;
case 'message':
entity = t`{
message: (${ctx.argDecl(t, '__vs', '__SYNDICATE__.AnyValue')}) => {
${guardBody(terminalWrap(t, s.terminal, walk(s.body)))}
${guardBody(terminalWrap(t, s.facetToStop, s.body))}
}
}`;
break;
@ -294,10 +319,9 @@ ${joinItems(sa.captureBinders.map(binderTypeGuard(t)), '\n')}
xf(ctx.parser.messageSendStatement, (s, t) => t`message(currentSyndicateTarget, ${walk(s.expr)});`);
x(ctx.parser.reactStatement, (s, t) => facetWrap(t, s.body));
x(ctx.parser.reactStatement, (s, t) => facetWrap(t, s.label ?? 'default', s.body));
x(ctx.parser.stopStatement, (s, t) =>
t`__SYNDICATE__.Turn.active._stop(__SYNDICATE__.Turn.activeFacet, () => {${walk(s.body)}});`)
x(ctx.parser.stopStatement, (s, t) => t`${terminalWrap(t, s.facetToStop, s.body)};`);
return tree;
}

View File

@ -6,7 +6,7 @@ import {
Pattern,
foldItems, match, anonymousTemplate as template, commaJoin,
scope, bind, seq, alt, upTo, atom, atomString, group,
scope, bind, seq, seqTuple, alt, upTo, atom, atomString, group,
repeat, option, withoutSpace, map, mapm, rest, discard,
value, succeed, fail, separatedOrTerminatedBy, not,
} from '../syntax/index';
@ -50,8 +50,14 @@ export interface StatementTurnAction extends TurnAction {
body: Statement;
}
export type FacetToStop = 'default' | Expr;
export interface StopStatement extends StatementTurnAction {
facetToStop: FacetToStop;
}
export interface GenericEventEndpointStatement extends StatementTurnAction {
terminal: boolean;
facetToStop: FacetToStop | 'none' | 'once-wrapper';
once: boolean;
isDynamic: boolean;
}
@ -90,6 +96,7 @@ export interface DuringStatement extends FacetSetupAction {
}
export interface ReactStatement extends FacetSetupAction {
label: Identifier | null;
}
export interface AtStatement {
@ -184,6 +191,10 @@ export class SyndicateParser {
return withoutSpace(upTo(alt(this.exprBoundary, ... extraStops)));
}
expr1(... extraStops: Pattern<any>[]): Pattern<Expr> {
return mapm(this.expr(... extraStops), e => e.length ? succeed(e) : fail);
}
propertyNameExpr(): Pattern<Expr> {
const dq = template`"`;
return alt<Expr>(
@ -271,22 +282,25 @@ export class SyndicateParser {
mandatoryIfNotTerminal(o: GenericEventEndpointStatement, p: Pattern<any>): Pattern<any> {
return i => {
return (o.terminal) ? option(p)(i) : p(i);
return (o.facetToStop !== 'none') ? option(p)(i) : p(i);
};
}
// Principal: Turn
readonly eventHandlerEndpointStatement: Pattern<EventHandlerEndpointStatement> =
this.turnAction(o => {
o.terminal = false;
o.facetToStop = 'none';
o.once = false;
o.isDynamic = true;
o.body = [];
return seq(alt(seq(option(map(atom('stop'), _ => o.terminal = true)),
return seq(alt(seq(option(seq(atom('stop'),
map(option(this.expr1(atom('on'))), es => {
o.facetToStop = es.length ? es[0] : 'default';
}))),
atom('on')),
map(atom('once'), _ => {
o.terminal = true;
o.once = true;
o.facetToStop = 'once-wrapper';
})),
alt<any>(seq(map(group('(', bind(o as DataflowEndpointStatement, 'predicate',
this.expr())),
@ -294,7 +308,7 @@ export class SyndicateParser {
this.mandatoryIfNotTerminal(o, this.statement(o.body))),
mapm(seq(bind(o, 'triggerType', atomString('stop')),
option(this.statement(o.body))),
v => o.terminal ? fail : succeed(v)),
v => ((o.facetToStop !== 'none') || o.once) ? fail : succeed(v)),
seq(bind(o, 'triggerType',
alt(atomString('asserted'),
atomString('retracted'),
@ -338,12 +352,23 @@ export class SyndicateParser {
// Principal: Turn
readonly reactStatement: Pattern<ReactStatement> =
this.turnAction(o => {
o.label = null;
o.body = [];
return seq(atom('react'), this.block(o.body));
return seq(option(map(seqTuple(this.identifier, atom(':')),
([i, _colon]) => o.label = i)),
atom('react'),
this.block(o.body));
});
// Principal: Turn
readonly stopStatement = this.blockTurnAction(atom('stop'));
readonly stopStatement: Pattern<StopStatement> =
this.turnAction(o => {
o.facetToStop = 'default';
o.body = [];
return seq(atom('stop'),
option(map(this.expr1(), e => o.facetToStop = e)),
alt(this.block(o.body), this.statementBoundary));
});
// Principal: none
readonly atStatement: Pattern<AtStatement> =

View File

@ -14,6 +14,8 @@ import { List, ArrayList, atEnd, notAtEnd } from './list';
export type PatternResult<T> = [T, List<Item>] | null;
export type Pattern<T> = (i: List<Item>) => PatternResult<T>;
export type PatternTypeArg<P> = P extends Pattern<infer T> ? T : never;
export function match<T,F>(p: Pattern<T>, items: Items, failure: F): T | F {
const r = p(new ArrayList(items));
if (r === null) return failure;
@ -78,6 +80,22 @@ export function seq(... patterns: Pattern<any>[]): Pattern<any> {
};
}
export function seqTuple<Patterns extends [...Pattern<any>[]]>(
... patterns: Patterns
): Pattern<{ [I in keyof Patterns]: PatternTypeArg<Patterns[I]> } & { length: Patterns['length'] }>
{
return i => {
const rs = [];
for (const p of patterns) {
const r = p(i);
if (r === null) return null;
rs.push(r[0]);
i = r[1];
}
return [rs as unknown as PatternTypeArg<ReturnType<typeof seqTuple<Patterns>>>, i];
};
}
export function alt<T>(... alts: Pattern<T>[]): Pattern<T> {
return i => {
for (const a of alts) {

View File

@ -0,0 +1,141 @@
/// SPDX-License-Identifier: GPL-3.0-or-later
/// SPDX-FileCopyrightText: Copyright © 2024 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
import { compile, CompileOptions, Syntax } from '../src/index';
import Pos = Syntax.Pos;
import './test-utils';
type Error = { message: string, start: Pos | undefined, end: Pos | undefined };
function translate(source: string, options: Partial<CompileOptions> = {}): { code: string, errors: Error[] } {
const errors: Error[] = [];
const result = compile({
... options,
module: 'none',
source,
emitError: (message, start, end) => errors.push({ message, start, end }),
});
return { code: result.text, errors };
}
function translateNoErrors(source: string): string {
const o = translate(source);
expect(o.errors.length).toBe(0);
return o.code;
}
describe('react', () => {
it('without label', () => {
expect(translateNoErrors(`react { a; b; c; }`)).toBe(
`__SYNDICATE__.Turn.active.facet(() => {const currentSyndicateFacet = __SYNDICATE__.Turn.activeFacet; a; b; c; });`);
});
it('with label', () => {
expect(translateNoErrors(`someLabel: react { a; b; c; }`)).toBe(
`__SYNDICATE__.Turn.active.facet(() => {const currentSyndicateFacet = __SYNDICATE__.Turn.activeFacet; const someLabel = currentSyndicateFacet; a; b; c; });`);
});
});
describe('spawn', () => {
it('without name', () => {
expect(translateNoErrors(`spawn { a; b; c; }`)).toBe(
`__SYNDICATE__.Dataspace._spawn(() => { const currentSyndicateFacet = __SYNDICATE__.Turn.activeFacet; a; b; c; });`);
});
it('with name', () => {
expect(translateNoErrors(`spawn named 'foo' { a; b; c; }`)).toBe(
`__SYNDICATE__.Dataspace._spawn(() => { const currentSyndicateFacet = __SYNDICATE__.Turn.activeFacet; currentSyndicateFacet.actor.name = 'foo'; a; b; c; });`);
});
it('with missing name (known incorrect parsing and codegen)', () => {
// At present, the expr() parser accepts *empty input*. TODO: something better.
expect(translateNoErrors(`spawn named { a; b; c; }`)).toBe(
`__SYNDICATE__.Dataspace._spawn(() => { const currentSyndicateFacet = __SYNDICATE__.Turn.activeFacet; currentSyndicateFacet.actor.name = ; a; b; c; });`);
});
});
describe('stop', () => {
it('non-statement', () => {
expect(translateNoErrors(`stop`)).toBe(
`stop`);
});
it('without facet, without body', () => {
expect(translateNoErrors(`stop;`)).toBe(
`__SYNDICATE__.Turn.active._stop(currentSyndicateFacet, () => {const currentSyndicateFacet = __SYNDICATE__.Turn.activeFacet;});`);
});
it('without facet, empty body', () => {
expect(translateNoErrors(`stop {}`)).toBe(
`__SYNDICATE__.Turn.active._stop(currentSyndicateFacet, () => {const currentSyndicateFacet = __SYNDICATE__.Turn.activeFacet;});`);
});
it('without facet, non-empty body', () => {
expect(translateNoErrors(`stop { a; b; }`)).toBe(
`__SYNDICATE__.Turn.active._stop(currentSyndicateFacet, () => {const currentSyndicateFacet = __SYNDICATE__.Turn.activeFacet; a; b; });`);
});
it('with facet, without body', () => {
expect(translateNoErrors(`stop x.y;`)).toBe(
`__SYNDICATE__.Turn.active._stop(x.y, () => {const currentSyndicateFacet = __SYNDICATE__.Turn.activeFacet;});`);
});
it('with facet, empty body', () => {
expect(translateNoErrors(`stop x.y {}`)).toBe(
`__SYNDICATE__.Turn.active._stop(x.y, () => {const currentSyndicateFacet = __SYNDICATE__.Turn.activeFacet;});`);
});
it('with facet, non-empty body', () => {
expect(translateNoErrors(`stop x.y { a; b; }`)).toBe(
`__SYNDICATE__.Turn.active._stop(x.y, () => {const currentSyndicateFacet = __SYNDICATE__.Turn.activeFacet; a; b; });`);
});
it('nested stop, no labels', () => {
expect(translateNoErrors(`stop { stop; }`)).toBe(
`__SYNDICATE__.Turn.active._stop(currentSyndicateFacet, () => {const currentSyndicateFacet = __SYNDICATE__.Turn.activeFacet; __SYNDICATE__.Turn.active._stop(currentSyndicateFacet, () => {const currentSyndicateFacet = __SYNDICATE__.Turn.activeFacet;}); });`);
});
});
describe('during', () => {
it('stop in body', () => {
expect(translateNoErrors(`during P => { a; stop; b; }`)).toBe(
`__SYNDICATE__.Turn.active.assertDataflow(() => ({ target: currentSyndicateTarget, assertion: __SYNDICATE__.Observe({
pattern: __SYNDICATE__.QuasiValue.finish((__SYNDICATE__.QuasiValue.lit(__SYNDICATE__.fromJS(P)))),
observer: __SYNDICATE__.Turn.ref(__SYNDICATE__.assertionFacetObserver(
(__vs) => {
if (Array.isArray(__vs)) {
a; __SYNDICATE__.Turn.active._stop(currentSyndicateFacet, () => {const currentSyndicateFacet = __SYNDICATE__.Turn.activeFacet;}); b;
}
}
))
}) }));`);
});
});
describe('once', () => {
it('basics with block', () => {
expect(translateNoErrors(`once asserted P => { a; b; }`)).toBe(
`__SYNDICATE__.Turn.active.facet(() => {const __once_facet = __SYNDICATE__.Turn.activeFacet; __SYNDICATE__.Turn.active.assertDataflow(() => ({
target: currentSyndicateTarget,
assertion: __SYNDICATE__.Observe({
pattern: __SYNDICATE__.QuasiValue.finish((__SYNDICATE__.QuasiValue.lit(__SYNDICATE__.fromJS(P)))),
observer: __SYNDICATE__.Turn.ref({
assert: (__vs, __handle) => {
if (Array.isArray(__vs)) {
__SYNDICATE__.Turn.active._stop(__once_facet, () => { a; b; })
}
}
}),
}),
}));});`);
});
it('basics with statement', () => {
expect(translateNoErrors(`once asserted P => x;`)).toBe(
`__SYNDICATE__.Turn.active.facet(() => {const __once_facet = __SYNDICATE__.Turn.activeFacet; __SYNDICATE__.Turn.active.assertDataflow(() => ({
target: currentSyndicateTarget,
assertion: __SYNDICATE__.Observe({
pattern: __SYNDICATE__.QuasiValue.finish((__SYNDICATE__.QuasiValue.lit(__SYNDICATE__.fromJS(P)))),
observer: __SYNDICATE__.Turn.ref({
assert: (__vs, __handle) => {
if (Array.isArray(__vs)) {
__SYNDICATE__.Turn.active._stop(__once_facet, () => {x;})
}
}
}),
}),
}));});`);
});
});