diff --git a/packages/compiler/src/compiler/codegen.ts b/packages/compiler/src/compiler/codegen.ts index 7d344d3..d365bfe 100644 --- a/packages/compiler/src/compiler/codegen.ts +++ b/packages/compiler/src/compiler/codegen.ts @@ -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(p: Pattern, 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; } diff --git a/packages/compiler/src/compiler/grammar.ts b/packages/compiler/src/compiler/grammar.ts index 1d79902..2550214 100644 --- a/packages/compiler/src/compiler/grammar.ts +++ b/packages/compiler/src/compiler/grammar.ts @@ -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[]): Pattern { + return mapm(this.expr(... extraStops), e => e.length ? succeed(e) : fail); + } + propertyNameExpr(): Pattern { const dq = template`"`; return alt( @@ -271,22 +282,25 @@ export class SyndicateParser { mandatoryIfNotTerminal(o: GenericEventEndpointStatement, p: Pattern): Pattern { return i => { - return (o.terminal) ? option(p)(i) : p(i); + return (o.facetToStop !== 'none') ? option(p)(i) : p(i); }; } // Principal: Turn readonly eventHandlerEndpointStatement: Pattern = 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(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 = 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 = + 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 = diff --git a/packages/compiler/src/syntax/matcher.ts b/packages/compiler/src/syntax/matcher.ts index 4e087bd..67665fe 100644 --- a/packages/compiler/src/syntax/matcher.ts +++ b/packages/compiler/src/syntax/matcher.ts @@ -14,6 +14,8 @@ import { List, ArrayList, atEnd, notAtEnd } from './list'; export type PatternResult = [T, List] | null; export type Pattern = (i: List) => PatternResult; +export type PatternTypeArg

= P extends Pattern ? T : never; + export function match(p: Pattern, 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[]): Pattern { }; } +export function seqTuple[]]>( + ... patterns: Patterns +): Pattern<{ [I in keyof Patterns]: PatternTypeArg } & { 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>>, i]; + }; +} + export function alt(... alts: Pattern[]): Pattern { return i => { for (const a of alts) { diff --git a/packages/compiler/test/compiler.test.ts b/packages/compiler/test/compiler.test.ts new file mode 100644 index 0000000..713e5c9 --- /dev/null +++ b/packages/compiler/test/compiler.test.ts @@ -0,0 +1,141 @@ +/// SPDX-License-Identifier: GPL-3.0-or-later +/// SPDX-FileCopyrightText: Copyright © 2024 Tony Garnock-Jones + +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 = {}): { 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;}) + } + } + }), + }), + }));});`); + }); +});