From 5b1b535644ddaeb9f28a4bd6a6b3fab1f23edfb9 Mon Sep 17 00:00:00 2001 From: Tony Garnock-Jones Date: Wed, 20 Jan 2021 00:52:40 +0100 Subject: [PATCH] Typescript support --- packages/compiler/src/compiler/codegen.ts | 166 +++-- packages/compiler/src/compiler/grammar.ts | 618 ++++++++++-------- packages/compiler/src/syntax/scanner.ts | 14 +- packages/syndicatec/bin/syndicate-tsc.js | 7 +- packages/syndicatec/bin/syndicatec.js | 8 +- .../syndicatec/examples/javascript/src/box.js | 2 +- .../examples/typescript/package.json | 2 +- .../syndicatec/examples/typescript/src/box.ts | 2 +- .../examples/typescript/tsconfig.json | 2 +- packages/syndicatec/src/cli.ts | 18 +- packages/syndicatec/src/tsc.ts | 14 +- packages/syndicatec/src/util.ts | 7 + 12 files changed, 498 insertions(+), 362 deletions(-) create mode 100644 packages/syndicatec/src/util.ts diff --git a/packages/compiler/src/compiler/codegen.ts b/packages/compiler/src/compiler/codegen.ts index 376644b..1c826ae 100644 --- a/packages/compiler/src/compiler/codegen.ts +++ b/packages/compiler/src/compiler/codegen.ts @@ -1,29 +1,21 @@ import { - isToken, isTokenType, replace, commaJoin, startPos, fixPos, joinItems, anonymousTemplate, + isToken, isTokenType, replace, commaJoin, startPos, fixPos, joinItems, + anonymousTemplate, laxRead, Items, Pattern, Templates, Substitution, TokenType, - SourceMap, StringScanner, LaxReader, CodeWriter, TemplateFunction, Token, + SourceMap, CodeWriter, TemplateFunction, Token, itemText, } from '../syntax/index.js'; import { - FacetAction, Statement, + SyndicateParser, SyndicateTypedParser, + Identifier, + FacetAction, + Statement, + ActivationImport, + FacetFields, + Binder, compilePattern, patternText, - - spawn, - fieldDeclarationStatement, - assertionEndpointStatement, - dataflowStatement, - eventHandlerEndpointStatement, - duringStatement, - typeDefinitionStatement, - messageSendStatement, - reactStatement, - bootStatement, - stopStatement, - Identifier, - activationImport, - ActivationImport, } from './grammar.js'; import { BootProc, @@ -64,14 +56,17 @@ export interface ActivationRecord { } export class ExpansionContext { + readonly parser: SyndicateParser; readonly moduleType: ModuleType; readonly activationRecords: Array = []; hasBootProc: boolean = false; readonly typescript: boolean; + _collectedFields: FacetFields | null = null; constructor(moduleType: ModuleType, typescript: boolean) { + this.parser = typescript ? new SyndicateTypedParser : new SyndicateParser(); this.moduleType = moduleType; this.typescript = typescript; } @@ -80,9 +75,57 @@ export class ExpansionContext { return this.typescript ? anonymousTemplate`${name}: ${type}` : name; } - get thisFacetDecl(): Substitution { - return this.argDecl('thisFacet', '__SYNDICATE__.Facet'); + get collectedFields(): FacetFields { + if (this._collectedFields === null) { + throw new Error("Internal error: this.collectedFields === null"); + } + return this._collectedFields; } + + collectField(f: Binder) { + this.collectedFields.push(f); + } + + withCollectedFields(fs: FacetFields, f: () => T): T { + const oldCollectedFields = this._collectedFields; + try { + this._collectedFields = fs; + return f(); + } finally { + this._collectedFields = oldCollectedFields; + } + } +} + +function stringifyId(i: Identifier): Items { + return [ { ... i, type: TokenType.STRING, text: JSON.stringify(i.text) } ]; +} + +function facetFieldObjectType(t: TemplateFunction, fs: FacetFields): Substitution { + function formatBinder(binder: Binder) { + const hasType = (binder.type !== void 0); + return t`${[binder.id]}${hasType ? ': ': ''}${binder.type ?? ''}`; + } + return t`{${commaJoin(fs.map(formatBinder))}}`; +} + +function binderTypeGuard(t: TemplateFunction): (binder: Binder) => Items { + return (binder) => { + if (binder.type === void 0) { + return t`${`/* ${binder.id.text} is a plain Value */`}`; + } else { + const typeText = itemText(binder.type); + switch (typeText) { + case 'boolean': + case 'string': + case 'number': + case 'symbol': + return t`if (typeof (${[binder.id]}) !== ${JSON.stringify(typeText)}) return;\n`; + default: + throw new Error(`Unhandled binding type: ${JSON.stringify(typeText)}`); + } + } + }; } export function expand(tree: Items, ctx: ExpansionContext): Items { @@ -90,7 +133,7 @@ export function expand(tree: Items, ctx: ExpansionContext): Items { function terminalWrap(t: TemplateFunction, isTerminal: boolean, body: Statement): Statement { if (isTerminal) { - return t`thisFacet._stop(function (${ctx.thisFacetDecl}) {${body}})` + return t`thisFacet._stop(function (thisFacet) {${body}})` } else { return body; } @@ -107,44 +150,42 @@ export function expand(tree: Items, ctx: ExpansionContext): Items { const walk = (tree: Items): Items => expand(tree, ctx); const maybeWalk = (tree?: Items) : Items | undefined => (tree === void 0) ? tree : walk(tree); - xf(duringStatement, (s, t) => { + xf(ctx.parser.duringStatement, (s, t) => { // TODO: spawn during const sa = compilePattern(s.pattern); - return t`withSelfDo(function (${ctx.thisFacetDecl}) { + const body = ctx.withCollectedFields(s.facetFields, () => walk(s.body)); + return t`withSelfDo(function (thisFacet) { const _Facets = new __SYNDICATE__.Dictionary(); on asserted ${patternText(s.pattern)} => react { - _Facets.set([${commaJoin(sa.captureIds.map(t=>[t]))}], thisFacet); + _Facets.set([${commaJoin(sa.captureBinders.map(t=>[t.id]))}], thisFacet); dataflow void 0; // TODO: horrible hack to keep the facet alive if no other endpoints - ${s.body} + ${body} } on retracted ${patternText(s.pattern)} => { - const _Key = [${commaJoin(sa.captureIds.map(t=>[t]))}]; + const _Key = [${commaJoin(sa.captureBinders.map(t=>[t.id]))}]; _Facets.get(_Key)._stop(); _Facets.delete(_Key); } });`; }); - xf(spawn, (s, t) => { - let proc = t`function (${ctx.thisFacetDecl}) {${walk(s.bootProcBody)}}`; + xf(ctx.parser.spawn, (s, t) => { + let body = ctx.withCollectedFields(s.facetFields, () => walk(s.body)); + let proc = t`function (thisFacet) {${body}}`; if (s.isDataspace) proc = t`__SYNDICATE__.inNestedDataspace(${proc})`; let assertions = (s.initialAssertions.length > 0) ? t`, new __SYNDICATE__.Set([${commaJoin(s.initialAssertions.map(walk))}])` : ``; - return t`_spawn(${maybeWalk(s.name) ?? 'null'}, ${proc}${assertions});`; + let fieldTypeParam = ctx.typescript ? t`<${facetFieldObjectType(t, s.facetFields)}>` : ''; + return t`_spawn${fieldTypeParam}(${maybeWalk(s.name) ?? 'null'}, ${proc}${assertions});`; }); - xf(fieldDeclarationStatement, (s, t) => { - const prop = ('name' in s.property) - ? [ { start: s.property.name.start, - end: s.property.name.end, - type: TokenType.STRING, - text: JSON.stringify(s.property.name.text) } ] - : walk(s.property.expr); - return t`declareField(${walk(s.target)}, ${prop}, ${maybeWalk(s.init) ?? 'void 0'});`; + xf(ctx.parser.fieldDeclarationStatement, (s, t) => { + ctx.collectField(s.property); + return t`declareField(this, ${stringifyId(s.property.id)}, ${maybeWalk(s.init) ?? 'void 0'});`; }); - xf(assertionEndpointStatement, (s, t) => { + xf(ctx.parser.assertionEndpointStatement, (s, t) => { if (s.test == void 0) { return t`addEndpoint(thisFacet => ({ assertion: ${walk(s.template)}, analysis: null }));`; } else { @@ -154,17 +195,18 @@ export function expand(tree: Items, ctx: ExpansionContext): Items { } }); - xf(dataflowStatement, (s, t) => t`addDataflow(function (${ctx.thisFacetDecl}) {${walk(s.body)}});`); + xf(ctx.parser.dataflowStatement, (s, t) => + t`addDataflow(function (thisFacet) {${walk(s.body)}});`); - xf(eventHandlerEndpointStatement, (s, t) => { + xf(ctx.parser.eventHandlerEndpointStatement, (s, t) => { switch (s.triggerType) { case 'dataflow': - return t`withSelfDo(function (${ctx.thisFacetDecl}) { dataflow { if (${walk(s.predicate)}) { ${terminalWrap(t, s.terminal, walk(s.body))} } } });`; + return t`withSelfDo(function (thisFacet) { dataflow { if (${walk(s.predicate)}) { ${terminalWrap(t, s.terminal, walk(s.body))} } } });`; case 'start': case 'stop': { const m = s.triggerType === 'start' ? 'addStartScript' : 'addStopScript'; - return t`${m}(function (${ctx.thisFacetDecl}) {${walk(s.body)}});`; + return t`${m}(function (thisFacet) {${walk(s.body)}});`; } case 'asserted': @@ -176,6 +218,8 @@ export function expand(tree: Items, ctx: ExpansionContext): Items { 'retracted': 'REMOVED', 'message': 'MESSAGE', })[s.triggerType]; + const destructure = sa.captureBinders.length === 0 ? '__vs' + : t`[${commaJoin(sa.captureBinders.map(i=>[i.id]))}]`; return t`addEndpoint(thisFacet => ({ assertion: __SYNDICATE__.Observe(${walk(sa.assertion)}), analysis: { @@ -183,8 +227,9 @@ export function expand(tree: Items, ctx: ExpansionContext): Items { constPaths: ${JSON.stringify(sa.constPaths)}, constVals: [${commaJoin(sa.constVals.map(walk))}], capturePaths: ${JSON.stringify(sa.capturePaths)}, - callback: thisFacet.wrap((thisFacet, __Evt, [${commaJoin(sa.captureIds.map(i=>[i]))}]) => { + callback: thisFacet.wrap((thisFacet, __Evt, ${destructure}) => { if (__Evt === __SYNDICATE__.Skeleton.EventType.${expectedEvt}) { +${ctx.typescript ? joinItems(sa.captureBinders.map(binderTypeGuard(t)), '\n') : ''} thisFacet.scheduleScript(() => {${terminalWrap(t, s.terminal, walk(s.body))}}); } }) @@ -194,17 +239,23 @@ export function expand(tree: Items, ctx: ExpansionContext): Items { } }); - x(typeDefinitionStatement, (s, t) => { + x(ctx.parser.typeDefinitionStatement, (s, t) => { const l = JSON.stringify(s.label.text); - const fs = JSON.stringify(s.fields.map(f => f.text)); + const fs = JSON.stringify(s.fields.map(f => f.id.text)); return t`const ${[s.label]} = __SYNDICATE__.Record.makeConstructor(${maybeWalk(s.wireName) ?? l}, ${fs});`; }); - xf(messageSendStatement, (s, t) => t`_send(${walk(s.expr)});`); + xf(ctx.parser.messageSendStatement, (s, t) => t`_send(${walk(s.expr)});`); - xf(reactStatement, (s, t) => t`addChildFacet(function (${ctx.thisFacetDecl}) {${walk(s.body)}});`); + xf(ctx.parser.reactStatement, (s, t) => { + const body = ctx.withCollectedFields(s.facetFields, () => walk(s.body)); + const fieldTypeParam = ctx.typescript + ? t`<${facetFieldObjectType(t, ctx.collectedFields)}, ${facetFieldObjectType(t, s.facetFields)}>` + : ''; + return t`addChildFacet${fieldTypeParam}(function (thisFacet) {${body}});`; + }); - x(activationImport, (s, t) => { + x(ctx.parser.activationImport, (s) => { const activationScriptId: Token = { start: s.activationKeyword.start, end: s.activationKeyword.end, @@ -215,36 +266,39 @@ export function expand(tree: Items, ctx: ExpansionContext): Items { return []; }), - x(bootStatement, (s, t) => { + x(ctx.parser.bootStatement, (s, t) => { ctx.hasBootProc = true; const activationStatements = ctx.activationRecords.map(({ activationScriptId: id }) => t`thisFacet.activate(${[id]}); `); const body = t`${joinItems(activationStatements)}${walk(s)}`; + const facetDecl = ctx.typescript ? 'thisFacet: __SYNDICATE__.Facet<{}>' : 'thisFacet'; switch (ctx.moduleType) { case 'es6': - return t`export function ${BootProc}(${ctx.thisFacetDecl}) {${body}}`; + return t`export function ${BootProc}(${facetDecl}) {${body}}`; case 'require': - return t`module.exports.${BootProc} = function (${ctx.thisFacetDecl}) {${body}};`; + return t`module.exports.${BootProc} = function (${facetDecl}) {${body}};`; case 'global': - return t`function ${BootProc}(${ctx.thisFacetDecl}) {${body}}`; + return t`function ${BootProc}(${facetDecl}) {${body}}`; } }); - xf(stopStatement, (s, t) => t`_stop(function (${ctx.thisFacetDecl}) {${walk(s.body)}});`) + xf(ctx.parser.stopStatement, (s, t) => + t`_stop(function (thisFacet) {${walk(s.body)}});`) return tree; } export function compile(options: CompileOptions): CompilerOutput { const inputFilename = options.name ?? '/dev/stdin'; + + console.info(`Syndicate: compiling ${inputFilename}`); + const source = options.source; const moduleType = options.module ?? 'es6'; const typescript = options.typescript ?? false; const start = startPos(inputFilename); - const scanner = new StringScanner(start, source); - const reader = new LaxReader(scanner); - let tree = stripShebang(reader.readToEnd()); + let tree = stripShebang(laxRead(source, { start, extraDelimiters: ':' })); const end = tree.length > 0 ? tree[tree.length - 1].end : start; let macro = new Templates(); diff --git a/packages/compiler/src/compiler/grammar.ts b/packages/compiler/src/compiler/grammar.ts index dace1c5..1cb54e9 100644 --- a/packages/compiler/src/compiler/grammar.ts +++ b/packages/compiler/src/compiler/grammar.ts @@ -11,129 +11,49 @@ import { import * as Matcher from '../syntax/matcher.js'; import { Path, Skeleton } from './internals.js'; +//--------------------------------------------------------------------------- +// AST types + export type Expr = Items; export type Statement = Items; export type Identifier = Token; - -export const block = (acc: Items) => group('{', map(rest, items => acc.push(... items))); - -export const statementBoundary = alt(atom(';'), Matcher.newline); -export const exprBoundary = alt(atom(';'), atom(','), group('{', discard), Matcher.end); - -export const identifier: Pattern = atom(); - -export function expr(... extraStops: Pattern[]): Pattern { - return withoutSpace(upTo(alt(exprBoundary, ... extraStops))); -} - -export function statement(acc: Items): Pattern { - return alt(block(acc), - withoutSpace(seq(map(upTo(statementBoundary), items => acc.push(... items)), - map(statementBoundary, i => i ? acc.push(i) : void 0)))); -} +export type Type = Items; +export type Binder = { id: Identifier, type?: Type }; export interface FacetAction { implicitFacet: boolean; } -export function facetAction( - pattern: (scope: T) => Pattern): Pattern -{ - 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 type FacetFields = Binder[]; + +export interface FacetProducingAction extends FacetAction { + body: Statement; + facetFields: FacetFields; } -export interface SpawnStatement extends FacetAction { +export interface SpawnStatement extends FacetProducingAction { isDataspace: boolean; name?: Expr; initialAssertions: Expr[]; - parentIds: Identifier[]; + parentBinders: Binder[]; parentInits: Expr[]; - bootProcBody: Statement; } -export const spawn: Pattern & { headerExpr: Pattern } = - Object.assign(facetAction((o: SpawnStatement) => { - o.isDataspace = false; - o.initialAssertions = []; - o.parentIds = []; - o.parentInits = []; - o.bootProcBody = []; - return seq(atom('spawn'), - option(seq(atom('dataspace'), exec(() => o.isDataspace = true))), - option(seq(atom('named'), - bind(o, 'name', spawn.headerExpr))), - repeat(alt(seq(atom(':asserting'), - map(spawn.headerExpr, e => o.initialAssertions.push(e))), - map(scope((l: { id: Identifier, init: Expr }) => - seq(atom(':let'), - bind(l, 'id', identifier), - atom('='), - bind(l, 'init', spawn.headerExpr))), - l => { - o.parentIds.push(l.id); - o.parentInits.push(l.init); - }))), - block(o.bootProcBody)); - }), { - headerExpr: expr(atom(':asserting'), atom(':let')), - }); - export interface FieldDeclarationStatement extends FacetAction { - target: Expr; - property: { name: Identifier } | { expr: Expr }; + property: Binder; init?: Expr; } -// Principal: Dataspace, but only for implementation reasons, so really Facet -export const fieldDeclarationStatement: Pattern = - facetAction(o => { - const prop = alt(seq(atom('.'), map(identifier, name => o.property = {name})), - seq(group('[', map(expr(), expr => o.property = {expr})))); - return seq(atom('field'), - bind(o, 'target', expr(seq(prop, alt(atom('='), statementBoundary)))), - prop, - option(seq(atom('='), bind(o, 'init', expr()))), - statementBoundary); - }); - export interface AssertionEndpointStatement extends FacetAction { isDynamic: boolean, template: Expr, test?: Expr, } -// Principal: Facet -export const assertionEndpointStatement: Pattern = - facetAction(o => { - o.isDynamic = true; - return seq(atom('assert'), - option(map(atom(':snapshot'), _ => o.isDynamic = false)), - bind(o, 'template', expr(seq(atom('when'), group('(', discard)))), - option(seq(atom('when'), group('(', bind(o, 'test', expr())))), - statementBoundary); - }); - export interface StatementFacetAction extends FacetAction { body: Statement; } -export function blockFacetAction(kw: Pattern): Pattern { - return facetAction(o => { - o.body = []; - return seq(kw, block(o.body)); - }); -} - -// Principal: Facet -export const dataflowStatement = blockFacetAction(atom('dataflow')); - export interface GenericEventEndpointStatement extends StatementFacetAction { terminal: boolean; isDynamic: boolean; @@ -156,115 +76,35 @@ export interface AssertionEventEndpointStatement extends GenericEventEndpointSta export type EventHandlerEndpointStatement = DataflowEndpointStatement | PseudoEventEndpointStatement | AssertionEventEndpointStatement; -export function mandatoryIfNotTerminal(o: GenericEventEndpointStatement, p: Pattern): Pattern { - return i => { - return (o.terminal) ? option(p)(i) : p(i); - }; -} - -// Principal: Facet -export const eventHandlerEndpointStatement: Pattern = - facetAction(o => { - o.terminal = false; - o.isDynamic = true; - o.body = []; - return seq(option(map(atom('stop'), _ => o.terminal = true)), - atom('on'), - alt(seq(map(group('(', bind(o as DataflowEndpointStatement, 'predicate', - expr())), - _ => o.triggerType = 'dataflow'), - mandatoryIfNotTerminal(o, statement(o.body))), - mapm(seq(bind(o, 'triggerType', - alt(atomString('start'), atomString('stop'))), - option(statement(o.body))), - v => o.terminal ? fail : succeed(v)), - seq(bind(o, 'triggerType', - alt(atomString('asserted'), - atomString('retracted'), - atomString('message'))), - option(map(atom(':snapshot'), _ => o.isDynamic = false)), - bind(o as AssertionEventEndpointStatement, 'pattern', - valuePattern(atom('=>'))), - mandatoryIfNotTerminal(o, seq(atom('=>'), statement(o.body)))))); - }); - export interface TypeDefinitionStatement { expectedUse: 'message' | 'assertion'; label: Identifier; - fields: Identifier[]; + fields: Binder[]; wireName?: Expr; } -// Principal: none -export const typeDefinitionStatement: Pattern = - scope(o => seq(bind(o, 'expectedUse', alt(atomString('message'), atomString('assertion'))), - atom('type'), - bind(o, 'label', identifier), - group('(', bind(o, 'fields', repeat(identifier, { separator: atom(',') }))), - option(seq(atom('='), - bind(o, 'wireName', withoutSpace(upTo(statementBoundary))))), - statementBoundary)); - export interface MessageSendStatement extends FacetAction { expr: Expr; } -// Principal: Facet -export const messageSendStatement: Pattern = - facetAction(o => seq(atom('send'), - atom('message'), - not(statementBoundary), - bind(o, 'expr', withoutSpace(upTo(statementBoundary))), - statementBoundary)); - -export interface DuringStatement extends FacetAction { +export interface DuringStatement extends FacetProducingAction { pattern: ValuePattern; - body: Statement; } -// Principal: Facet -export const duringStatement: Pattern = - facetAction(o => { - o.body = []; - return seq(atom('during'), - bind(o, 'pattern', valuePattern(atom('=>'))), - seq(atom('=>'), statement(o.body))); - }); - -// Principal: Facet -export const reactStatement = blockFacetAction(atom('react')); - -// Principal: none -export const bootStatement: Pattern = - value(o => { - o.value = []; - return seq(atom('boot'), block(o.value)); - }); - -// Principal: Facet -export const stopStatement = blockFacetAction(atom('stop')); +export interface ReactStatement extends FacetProducingAction { +} export interface ActivationImport { activationKeyword: Identifier; target: { type: 'import', moduleName: Token } | { type: 'expr', moduleExpr: Expr }; } -// Principal: none -export const activationImport: Pattern = - scope(o => seq(bind(o, 'activationKeyword', atom('activate')), - follows(alt(seq(atom('import'), - upTo(seq( - map(atom(void 0, { tokenType: TokenType.STRING }), - n => o.target = { type: 'import', moduleName: n }), - statementBoundary))), - map(expr(), e => o.target = { type: 'expr', moduleExpr: e }))))); - //--------------------------------------------------------------------------- -// Syntax of patterns over Value, used in endpoints +// Value pattern AST types export interface PCapture { type: 'PCapture', - binder: Identifier, + binder: Binder, inner: ValuePattern, } @@ -290,119 +130,335 @@ export interface PArray { export type ValuePattern = PCapture | PDiscard | PConstructor | PConstant | PArray; -const pCaptureId: Pattern = - mapm(identifier, i => i.text.startsWith('$') - ? succeed({ ... i, text: i.text.slice(1) }) - : fail); - -const pDiscard: Pattern = mapm(identifier, i => i.text === '_' ? succeed(void 0) : fail); - -function hasCapturesOrDiscards(e: Expr): boolean { - return foldItems(e, - t => match(alt(pCaptureId, pDiscard), [t], null) !== null, - (_g, b, _k) => b, - bs => bs.some(b => b)); -} - -// $id - capture of discard -// _ - discard -// -// expr(pat, ...) - record ctor -// $id(pat) - nested capture -// [pat, ...] - array pat -// -// expr(expr, ...) - constant -// [expr, ...] - constant -// other - constant - interface RawCall { items: Items; callee: Expr; arguments: Expr[]; } -function pRawCall(... extraStops: Pattern[]): Pattern { - return scope((o: RawCall) => seq(bind(o, 'callee', - expr(seq(group('(', discard), - alt(exprBoundary, ... extraStops)))), - seq(map(anything({ advance: false }), - g => o.items = [... o.callee, g]), - group('(', bind(o, 'arguments', - separatedBy(expr(), atom(','))))))); -} - -function isConstant(o: RawCall) { - return (!(hasCapturesOrDiscards(o.callee) || o.arguments.some(hasCapturesOrDiscards))); -} - -export function valuePattern(... extraStops: Pattern[]): Pattern { - return alt( - scope(o => { - o.type = 'PCapture'; - o.inner = { type: 'PDiscard' }; - return bind(o, 'binder', pCaptureId); - }), - scope(o => map(pDiscard, _ => o.type = 'PDiscard')), - mapm( - pRawCall(... extraStops), - o => { - if (isConstant(o)) { - return succeed({ type: 'PConstant', value: o.items }); - } else if (hasCapturesOrDiscards(o.callee)) { - const r = match(pCaptureId, o.callee, null); - if (r !== null && o.arguments.length === 1) - { - const argPat = match(valuePattern(), o.arguments[0], null); - if (argPat === null) return fail; - return succeed({ - type: 'PCapture', - inner: argPat, - binder: r - }); - } else { - return fail; - } - } else { - const argPats = o.arguments.map(a => match(valuePattern(), a, null)); - if (argPats.some(p => p === null)) return fail; - return succeed({ - type: 'PConstructor', - ctor: o.callee, - arguments: argPats as ValuePattern[] - }); - } - }), - map(expr(), e => ({ type: 'PConstant', value: e })) - ); -} - -export function patternText(p: ValuePattern): Items { - switch (p.type) { - case 'PDiscard': return template`_`; - case 'PConstant': return p.value; - case 'PCapture': - { - const binder = { ... p.binder, text: '$' + p.binder.text }; - if (p.inner.type === 'PDiscard') { - return [binder]; - } else { - return template`${[binder]}(${patternText(p.inner)})`; - } - } - case 'PArray': return template`[${commaJoin(p.elements.map(patternText))}]`; - case 'PConstructor': return template`${p.ctor}(${commaJoin(p.arguments.map(patternText))})`; - } -} - export interface StaticAnalysis { skeleton: Expr; constPaths: Path[]; constVals: Expr[]; capturePaths: Path[]; - captureIds: Identifier[]; + captureBinders: Binder[]; assertion: Expr; } +//--------------------------------------------------------------------------- +// Parsers + +export class SyndicateParser { + block(acc?: Items): Pattern { + return group('{', map(rest, items => (acc?.push(... items), items))); + } + + readonly statementBoundary = alt(atom(';'), Matcher.newline); + readonly exprBoundary = alt(atom(';'), atom(','), group('{', discard), Matcher.end); + + readonly identifier: Pattern = atom(); + get binder(): Pattern { return scope(o => bind(o, 'id', this.identifier)); } + + expr(... extraStops: Pattern[]): Pattern { + return withoutSpace(upTo(alt(this.exprBoundary, ... extraStops))); + } + + readonly type: (... extraStops: Pattern[]) => Pattern = this.expr; + + statement(acc: Items): Pattern { + return alt(this.block(acc), + withoutSpace(seq(map(upTo(this.statementBoundary), + items => acc.push(... items)), + map(this.statementBoundary, + i => i ? acc.push(i) : void 0)))); + } + + facetAction(pattern: (scope: T) => Pattern): Pattern { + 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]]; + }; + } + + readonly headerExpr = this.expr(atom(':asserting'), atom(':let')); + + // Principal: Facet + readonly spawn: Pattern = + this.facetAction(o => { + o.isDataspace = false; + o.initialAssertions = []; + o.parentBinders = []; + o.parentInits = []; + o.body = []; + o.facetFields = []; + return seq(atom('spawn'), + option(seq(atom('dataspace'), exec(() => o.isDataspace = true))), + option(seq(atom('named'), + bind(o, 'name', this.headerExpr))), + repeat(alt(seq(atom(':asserting'), + map(this.headerExpr, e => o.initialAssertions.push(e))), + map(scope((l: { b: Binder, init: Expr }) => + seq(atom(':let'), + bind(l, 'b', this.binder), + atom('='), + bind(l, 'init', this.headerExpr))), + l => { + o.parentBinders.push(l.b); + o.parentInits.push(l.init); + }))), + this.block(o.body)); + }); + + // Principal: Dataspace, but only for implementation reasons, so really Facet + readonly fieldDeclarationStatement: Pattern = + this.facetAction(o => { + return seq(atom('field'), + bind(o, 'property', this.binder), + option(seq(atom('='), bind(o, 'init', this.expr()))), + this.statementBoundary); + }); + + // Principal: Facet + readonly assertionEndpointStatement: Pattern = + this.facetAction(o => { + o.isDynamic = true; + return seq(atom('assert'), + option(map(atom(':snapshot'), _ => o.isDynamic = false)), + bind(o, 'template', this.expr(seq(atom('when'), group('(', discard)))), + option(seq(atom('when'), group('(', bind(o, 'test', this.expr())))), + this.statementBoundary); + }); + + blockFacetAction(kw: Pattern): Pattern { + return this.facetAction(o => { + o.body = []; + return seq(kw, this.block(o.body)); + }); + } + + // Principal: Facet + readonly dataflowStatement = this.blockFacetAction(atom('dataflow')); + + mandatoryIfNotTerminal(o: GenericEventEndpointStatement, p: Pattern): Pattern { + return i => { + return (o.terminal) ? option(p)(i) : p(i); + }; + } + + // Principal: Facet + readonly eventHandlerEndpointStatement: Pattern = + this.facetAction(o => { + o.terminal = false; + o.isDynamic = true; + o.body = []; + return seq(option(map(atom('stop'), _ => o.terminal = true)), + atom('on'), + alt(seq(map(group('(', bind(o as DataflowEndpointStatement, 'predicate', + this.expr())), + _ => o.triggerType = 'dataflow'), + this.mandatoryIfNotTerminal(o, this.statement(o.body))), + mapm(seq(bind(o, 'triggerType', + alt(atomString('start'), atomString('stop'))), + option(this.statement(o.body))), + v => o.terminal ? fail : succeed(v)), + seq(bind(o, 'triggerType', + alt(atomString('asserted'), + atomString('retracted'), + atomString('message'))), + option(map(atom(':snapshot'), _ => o.isDynamic = false)), + bind(o as AssertionEventEndpointStatement, 'pattern', + this.valuePattern(atom('=>'))), + this.mandatoryIfNotTerminal( + o, seq(atom('=>'), this.statement(o.body)))))); + }); + + // Principal: none + readonly typeDefinitionStatement: Pattern = + scope(o => seq(bind(o, 'expectedUse', alt(atomString('message'), atomString('assertion'))), + atom('type'), + bind(o, 'label', this.identifier), + group('(', bind(o, 'fields', + repeat(this.binder, { separator: atom(',') }))), + option(seq(atom('='), + bind(o, 'wireName', withoutSpace(upTo(this.statementBoundary))))), + this.statementBoundary)); + + // Principal: Facet + readonly messageSendStatement: Pattern = + this.facetAction(o => seq(atom('send'), + atom('message'), + not(this.statementBoundary), + bind(o, 'expr', withoutSpace(upTo(this.statementBoundary))), + this.statementBoundary)); + + // Principal: Facet + readonly duringStatement: Pattern = + this.facetAction(o => { + o.body = []; + o.facetFields = []; + return seq(atom('during'), + bind(o, 'pattern', this.valuePattern(atom('=>'))), + seq(atom('=>'), this.statement(o.body))); + }); + + // Principal: Facet + readonly reactStatement: Pattern = + this.facetAction(o => { + o.body = []; + o.facetFields = []; + return seq(atom('react'), this.block(o.body)); + }); + + // Principal: none + readonly bootStatement: Pattern = + value(o => { + o.value = []; + return seq(atom('boot'), this.block(o.value)); + }); + + // Principal: Facet + readonly stopStatement = this.blockFacetAction(atom('stop')); + + // Principal: none + readonly activationImport: Pattern = + scope(o => seq(bind(o, 'activationKeyword', atom('activate')), + follows(alt(seq(atom('import'), + upTo(seq( + map(atom(void 0, { tokenType: TokenType.STRING }), + n => o.target = { + type: 'import', + moduleName: n + }), + this.statementBoundary))), + map(this.expr(), e => o.target = { + type: 'expr', + moduleExpr: e + }))))); + + //--------------------------------------------------------------------------- + // Syntax of patterns over Value, used in endpoints + + readonly pCaptureBinder: Pattern = + mapm(this.binder, i => { + return i.id.text.startsWith('$') + ? succeed({ id: { ... i.id, text: i.id.text.slice(1) }, type: i.type }) + : fail; + }); + + readonly pDiscard: Pattern = + mapm(this.identifier, i => i.text === '_' ? succeed(void 0) : fail); + + hasCapturesOrDiscards(e: Expr): boolean { + return foldItems(e, + t => match(alt(this.pCaptureBinder, this.pDiscard), [t], null) !== null, + (_g, b, _k) => b, + bs => bs.some(b => b)); + } + + // $id - capture of discard + // _ - discard + // + // expr(pat, ...) - record ctor + // $id(pat) - nested capture + // [pat, ...] - array pat + // + // expr(expr, ...) - constant + // [expr, ...] - constant + // other - constant + + pRawCall(... extraStops: Pattern[]): Pattern { + return scope((o: RawCall) => + seq(bind(o, 'callee', + this.expr(seq(group('(', discard), + alt(this.exprBoundary, ... extraStops)))), + seq(map(anything({ advance: false }), + g => o.items = [... o.callee, g]), + group('(', bind(o, 'arguments', + separatedBy(this.expr(), atom(','))))))); + } + + isConstant(o: RawCall): boolean { + return (!(this.hasCapturesOrDiscards(o.callee) || + o.arguments.some(a => this.hasCapturesOrDiscards(a)))); + } + + valuePattern(... extraStops: Pattern[]): Pattern { + return alt( + scope(o => { + o.type = 'PCapture'; + o.inner = { type: 'PDiscard' }; + return bind(o, 'binder', this.pCaptureBinder); + }), + scope(o => map(this.pDiscard, _ => o.type = 'PDiscard')), + mapm( + this.pRawCall(... extraStops), + o => { + if (this.isConstant(o)) { + return succeed({ type: 'PConstant', value: o.items }); + } else if (this.hasCapturesOrDiscards(o.callee)) { + const r = match(this.pCaptureBinder, o.callee, null); + if (r !== null && o.arguments.length === 1) + { + const argPat = match(this.valuePattern(), o.arguments[0], null); + if (argPat === null) return fail; + return succeed({ + type: 'PCapture', + inner: argPat, + binder: r + }); + } else { + return fail; + } + } else { + const argPats = o.arguments.map(a => match(this.valuePattern(), a, null)); + if (argPats.some(p => p === null)) return fail; + return succeed({ + type: 'PConstructor', + ctor: o.callee, + arguments: argPats as ValuePattern[] + }); + } + }), + map(this.expr(), e => ({ type: 'PConstant', value: e })) + ); + } +} + +export class SyndicateTypedParser extends SyndicateParser { + get binder(): Pattern { + return scope(o => seq(bind(o, 'id', this.identifier), + option(seq(atom(':'), + bind(o, 'type', this.type(atom('='))))))); + } +} + +//--------------------------------------------------------------------------- +// Value pattern utilities + +export function patternText(p: ValuePattern): Items { + switch (p.type) { + case 'PDiscard': return template`_`; + case 'PConstant': return p.value; + case 'PCapture': + { + const binderId = { ... p.binder.id, text: '$' + p.binder.id.text }; + const affix = + (p.inner.type === 'PDiscard') ? [] : template`(${patternText(p.inner)})`; + if (p.binder.type !== void 0) { + return template`${[binderId]}:${p.binder.type}${affix}`; + } else { + return template`${[binderId]}${affix}`; + } + } + case 'PArray': return template`[${commaJoin(p.elements.map(patternText))}]`; + case 'PConstructor': return template`${p.ctor}(${commaJoin(p.arguments.map(patternText))})`; + } +} + const eDiscard: Expr = template`(__SYNDICATE__.Discard._instance)`; const eCapture = (e: Expr): Expr => template`(__SYNDICATE__.Capture(${e}))`; @@ -410,7 +466,7 @@ export function compilePattern(pattern: ValuePattern): StaticAnalysis { const constPaths: Path[] = []; const constVals: Expr[] = []; const capturePaths: Path[] = []; - const captureIds: Identifier[] = []; + const captureBinders: Binder[] = []; const currentPath: Path = []; @@ -420,7 +476,7 @@ export function compilePattern(pattern: ValuePattern): StaticAnalysis { return [null, eDiscard]; case 'PCapture': { capturePaths.push(currentPath.slice()); - captureIds.push(pattern.binder); + captureBinders.push(pattern.binder); const [s, a] = walk(pattern.inner); return [s, eCapture(a)]; } @@ -473,7 +529,7 @@ export function compilePattern(pattern: ValuePattern): StaticAnalysis { constPaths, constVals, capturePaths, - captureIds, + captureBinders, assertion, }; } diff --git a/packages/compiler/src/syntax/scanner.ts b/packages/compiler/src/syntax/scanner.ts index 30477af..69343bf 100644 --- a/packages/compiler/src/syntax/scanner.ts +++ b/packages/compiler/src/syntax/scanner.ts @@ -130,8 +130,9 @@ export abstract class Scanner implements IterableIterator { buf = buf + this.shiftChar(); while (true) { ch = this.shiftChar(); - if ((ch === null) ||((ch === '/') && seenStar)) break; + if (ch === null) break; buf = buf + ch; + if ((ch === '/') && seenStar) break; seenStar = (ch === '*'); } return this._collectSpace(buf, start); @@ -168,16 +169,15 @@ export abstract class Scanner implements IterableIterator { case '`': return this._str(false); - case '.': - case ',': - case ';': - return this._punct(TokenType.ATOM); - case '/': return this._maybeComment(); default: - return this._atom(this.mark(), this.shiftChar()!); + if (this.isDelimiter(ch)) { + return this._punct(TokenType.ATOM); + } else { + return this._atom(this.mark(), this.shiftChar()!); + } } } diff --git a/packages/syndicatec/bin/syndicate-tsc.js b/packages/syndicatec/bin/syndicate-tsc.js index 4ad0ded..bb8e43f 100755 --- a/packages/syndicatec/bin/syndicate-tsc.js +++ b/packages/syndicatec/bin/syndicate-tsc.js @@ -1,2 +1,7 @@ #!/usr/bin/env node -require('../lib/tsc.js').main(process.argv.slice(2)); +try { + require('../lib/tsc.js').main(process.argv.slice(2)); +} catch (e) { + console.error(e); + process.exit(1); +} diff --git a/packages/syndicatec/bin/syndicatec.js b/packages/syndicatec/bin/syndicatec.js index fd1e770..2feaf3c 100755 --- a/packages/syndicatec/bin/syndicatec.js +++ b/packages/syndicatec/bin/syndicatec.js @@ -1,2 +1,8 @@ #!/usr/bin/env node -require('../lib/cli.js').main(process.argv.slice(2)); +try { + require('../lib/cli.js').main(process.argv.slice(2)); +} catch (e) { + console.error(e); + process.exit(1); +} + diff --git a/packages/syndicatec/examples/javascript/src/box.js b/packages/syndicatec/examples/javascript/src/box.js index 5b6603e..db8cc45 100644 --- a/packages/syndicatec/examples/javascript/src/box.js +++ b/packages/syndicatec/examples/javascript/src/box.js @@ -20,7 +20,7 @@ import { BoxState, SetBox, N } from './protocol.js'; boot { spawn named 'box' { - field this.value = 0; + field value = 0; assert BoxState(this.value); stop on (this.value === N) console.log('terminated box root facet'); diff --git a/packages/syndicatec/examples/typescript/package.json b/packages/syndicatec/examples/typescript/package.json index cb26bc6..698039b 100644 --- a/packages/syndicatec/examples/typescript/package.json +++ b/packages/syndicatec/examples/typescript/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "prepare": "npm run compile && npm run rollup", - "compile": "npx syndicatec -d lib -b src 'src/**/*.js'", + "compile": "../../bin/syndicate-tsc.js", "rollup": "npx rollup -c", "clean": "rm -rf lib/ index.js index.js.map" }, diff --git a/packages/syndicatec/examples/typescript/src/box.ts b/packages/syndicatec/examples/typescript/src/box.ts index 52eb2ee..959912c 100644 --- a/packages/syndicatec/examples/typescript/src/box.ts +++ b/packages/syndicatec/examples/typescript/src/box.ts @@ -20,7 +20,7 @@ import { BoxState, SetBox, N } from './protocol.js'; boot { spawn named 'box' { - field this.value: number = 0; + field value: number = 0; assert BoxState(this.value); stop on (this.value === N) console.log('terminated box root facet'); diff --git a/packages/syndicatec/examples/typescript/tsconfig.json b/packages/syndicatec/examples/typescript/tsconfig.json index abc7c03..983aa20 100644 --- a/packages/syndicatec/examples/typescript/tsconfig.json +++ b/packages/syndicatec/examples/typescript/tsconfig.json @@ -9,7 +9,7 @@ "declarationDir": "./lib", "esModuleInterop": true, "moduleResolution": "node", - "module": "commonjs", + "module": "es6", "sourceMap": true, "strict": true }, diff --git a/packages/syndicatec/src/cli.ts b/packages/syndicatec/src/cli.ts index 4dce3e3..9015931 100644 --- a/packages/syndicatec/src/cli.ts +++ b/packages/syndicatec/src/cli.ts @@ -5,6 +5,7 @@ import path from 'path'; import { glob } from 'glob'; import { compile } from '@syndicate-lang/compiler'; +import { dataURL, sourceMappingComment} from './util.js'; export type ModuleChoice = 'es6' | 'require' | 'global'; const moduleChoices: ReadonlyArray = ['es6', 'require', 'global']; @@ -18,6 +19,7 @@ export type CommandLineArguments = { mapExtension?: string; runtime: string; module: ModuleChoice; + typed: boolean; } function checkModuleChoice(t: T & { module: string }): T & { module: ModuleChoice } { @@ -97,6 +99,12 @@ export function main(argv: string[]) { description: 'Path to require or import to get the Syndicate runtime', default: '@syndicate-lang/core', }) + .option('typed', { + alias: 't', + type: 'boolean', + description: 'Enable TypeScript-typed translation', + default: false, + }) .option('module', { choices: moduleChoices, type: 'string', @@ -132,14 +140,10 @@ export function main(argv: string[]) { name: inputFilename, runtime: options.runtime, module: options.module, + typescript: options.typed, }); map.sourcesContent = [source]; - function mapDataURL() { - const mapData = Buffer.from(JSON.stringify(map)).toString('base64') - return `data:application/json;base64,${mapData}`; - } - if (inputFilename !== STDIN) { fs.mkdirSync(path.dirname(outputFilename), { recursive: true }); } @@ -148,10 +152,10 @@ export function main(argv: string[]) { fs.writeFileSync(outputFilename, text); } else if (options.mapExtension && inputFilename !== STDIN) { const mapFilename = outputFilename + options.mapExtension; - fs.writeFileSync(outputFilename, text + `\n//# sourceMappingURL=${mapFilename}`); + fs.writeFileSync(outputFilename, text + sourceMappingComment(mapFilename)); fs.writeFileSync(mapFilename, JSON.stringify(map)); } else { - fs.writeFileSync(outputFilename, text + `\n//# sourceMappingURL=${mapDataURL()}`); + fs.writeFileSync(outputFilename, text + sourceMappingComment(dataURL(JSON.stringify(map)))); } } } diff --git a/packages/syndicatec/src/tsc.ts b/packages/syndicatec/src/tsc.ts index 3163db2..d3d4e31 100644 --- a/packages/syndicatec/src/tsc.ts +++ b/packages/syndicatec/src/tsc.ts @@ -3,6 +3,7 @@ import ts from 'typescript'; import crypto from 'crypto'; import { compile } from '@syndicate-lang/compiler'; +import { dataURL, sourceMappingComment } from './util.js'; function reportDiagnostic(diagnostic: ts.Diagnostic) { if (diagnostic.file) { @@ -49,15 +50,18 @@ function createProgram(rootNames: readonly string[] | undefined, onError?.(`Could not read input file ${fileName}`); return undefined; } - const expandedText = compile({ + const { text: baseExpandedText, map: sourceMap } = compile({ source: inputText, name: fileName, typescript: true, - }).text; - console.log('\n\n', fileName); - expandedText.split(/\n/).forEach((line, i) => { - console.log(i, line); }); + sourceMap.sourcesContent = [inputText]; + const expandedText = baseExpandedText + sourceMappingComment(dataURL(JSON.stringify(sourceMap))); + + // console.log('\n\n', fileName); + // expandedText.split(/\n/).forEach((line, i) => { + // console.log(i + 1, line); + // }); const sf = ts.createSourceFile(fileName, expandedText, languageVersion, true); (sf as any).version = crypto.createHash('sha256').update(expandedText).digest('hex'); return sf; diff --git a/packages/syndicatec/src/util.ts b/packages/syndicatec/src/util.ts new file mode 100644 index 0000000..5e38ef5 --- /dev/null +++ b/packages/syndicatec/src/util.ts @@ -0,0 +1,7 @@ +export function dataURL(s: string): string { + return `data:application/json;base64,${Buffer.from(s).toString('base64')}`; +} + +export function sourceMappingComment(url: string): string { + return `\n//# sourceMappingURL=${url}`; +}