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:
parent
644891ce76
commit
b4d728ca7a
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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> =
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;})
|
||||
}
|
||||
}
|
||||
}),
|
||||
}),
|
||||
}));});`);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue