import { isToken, isTokenType, replace, commaJoin, startPos, fixPos, joinItems, laxRead, itemText, match, Items, Pattern, Templates, Substitution, TokenType, SourceMap, CodeWriter, TemplateFunction, Token, SpanIndex, } from '../syntax/index.js'; import { SyndicateParser, SyndicateTypedParser, Identifier, FacetAction, Statement, ActivationImport, FacetFields, Binder, compilePattern, patternText, instantiatePatternToPattern, } from './grammar.js'; import { BootProc, } from './internals.js'; export function stripShebang(items: Items): Items { if ((items.length > 0) && isToken(items[0]) && items[0].text.startsWith('#!')) { while (items.length > 0 && !isTokenType(items[0], TokenType.NEWLINE)) items.shift(); } return items; } export type ModuleType ='es6' | 'require' | 'global'; export interface CompileOptions { source: string, name?: string, runtime?: string, module?: ModuleType, global?: string, typescript?: boolean, } export interface CompilerOutput { text: string, map: SourceMap, targetToSourceMap: SpanIndex; sourceToTargetMap: SpanIndex; } function receiverFor(s: FacetAction): Substitution { return (s.implicitFacet) ? 'thisFacet.' : '.'; } export interface ActivationRecord { activation: ActivationImport; activationScriptId: Identifier; } export class ExpansionContext { readonly parser: SyndicateParser; readonly moduleType: ModuleType; readonly activationRecords: Array = []; hasBootProc: boolean = false; readonly typescript: boolean; _collectedFields: FacetFields | null = null; nextIdNumber = 0; constructor(moduleType: ModuleType, typescript: boolean) { this.parser = typescript ? new SyndicateTypedParser : new SyndicateParser(); this.moduleType = moduleType; this.typescript = typescript; } quasiRandomId(): string { return '__SYNDICATE__id_' + (this.nextIdNumber++); } get collectedFields(): FacetFields { // Allocates a transient array for collected fields in // contexts lacking a surrounding collector - that is, for errors. 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; } } argDecl(t: TemplateFunction, name: Substitution, type: Substitution): Substitution { return (this.typescript) ? t`${name}: ${type}` : name; } } function stringifyId(i: Identifier): Items { return [ { ... i, type: TokenType.STRING, text: JSON.stringify(i.text) } ]; } function facetFieldObjectType( t: TemplateFunction, fs: FacetFields, defaultType?: Substitution): Substitution { function formatBinder(binder: Binder) { const hasType = ((binder.type ?? defaultType) !== void 0); return t`${[binder.id]}${hasType ? ': ': ''}${binder.type ?? defaultType ?? ''}`; } return t`{${commaJoin(fs.map(formatBinder))}}`; } function binderTypeGuard(t: TemplateFunction): (binder: Binder, index: number) => Items { return (binder, index) => { if (binder.id.text[0] === '_') { return t`${`/* Ignoring underscore-prefixed binder ${binder.id.text} */`}`; } const raw = t`__vs[${''+index}]`; const bind = t`const ${[binder.id]} = ${raw};`; if (binder.type === void 0) { return bind; } else { const typeText = itemText(binder.type); switch (typeText) { case 'boolean': case 'string': case 'number': case 'symbol': return t`if (typeof (${raw}) !== ${JSON.stringify(typeText)}) return;\n${bind}`; case 'any': return bind; default: // TODO: something better than this!! throw new Error(`Unhandled binding type: ${JSON.stringify(typeText)}`); } } }; } 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`thisFacet._stop(function (thisFacet) {${body}})` } else { return body; } } function x(p: Pattern, f: (v: T, t: TemplateFunction) => Items) { tree = replace(tree, p, (v, start) => f(v, macro.template(fixPos(start)))); } function xf(p: Pattern, f: (v: T, t: TemplateFunction) => Items) { x(p, (v, t) => t`${receiverFor(v)}${f(v, t)}`); } const walk = (tree: Items): Items => expand(tree, ctx); const maybeWalk = (tree?: Items) : Items | undefined => (tree === void 0) ? tree : walk(tree); xf(ctx.parser.duringStatement, (s, t) => { let spawn0 = match(ctx.parser.spawn, s.body, null); if (spawn0 !== null) { const spawn = spawn0; const id = ctx.quasiRandomId(); const instantiated = patternText(instantiatePatternToPattern(s.pattern)); return t`on asserted ${patternText(s.pattern)} => { const ${id} = __SYNDICATE__.genUuid(); const ${id}_inst = __SYNDICATE__.Instance(${id}); react { stop on asserted ${id}_inst => react { stop on retracted ${id}_inst; stop on retracted :snapshot ${instantiated}; } stop on retracted :snapshot ${instantiated} => react { stop on asserted ${id}_inst; } } spawn ${spawn.isDataspace ? 'dataspace' : []} ${spawn.name === void 0 ? [] : t`named ${spawn.name}`} :asserting ${id}_inst ${joinItems(spawn.initialAssertions.map(e => t`:asserting ${e}`), ' ')} ${joinItems(spawn.parentBinders.map((b, i) => { const init = spawn.parentInits[i]; return t`:let ${[b.id]}${b.type === void 0 ? [] : t`: ${b.type}`} = ${init}`; }), ' ')} { assert ${id}_inst; stop on retracted __SYNDICATE__.Observe(${id}_inst); ${spawn.body} } }`; } else { // TODO: untyped template const sa = compilePattern(s.pattern); return t`withSelfDo(function (thisFacet) { const _Facets = new __SYNDICATE__.Dictionary>(); on asserted ${patternText(s.pattern)} => react { _Facets.set([${commaJoin(sa.captureBinders.map(t=>[t.id]))}], thisFacet); dataflow { } // TODO: horrible hack to keep the facet alive if no other endpoints ${s.body} } on retracted ${patternText(s.pattern)} => { const ${ctx.argDecl(t, '_Key', '__SYNDICATE__.Value[]')} = [${commaJoin(sa.captureBinders.map(t=>[t.id]))}]; _Facets.get(_Key)?._stop(); _Facets.delete(_Key); } });`; } }); xf(ctx.parser.spawn, (s, t) => { // TODO: parentBinders, parentInits 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))}])` : ``; let fieldTypeParam = ctx.typescript ? t`<${facetFieldObjectType(t, s.facetFields)}>` : ''; return t`_spawn${fieldTypeParam}(${maybeWalk(s.name) ?? 'null'}, ${proc}${assertions});`; }); xf(ctx.parser.fieldDeclarationStatement, (s, t) => { ctx.collectField(s.property); return t`declareField(this, ${stringifyId(s.property.id)}, ${maybeWalk(s.init) ?? 'void 0'});`; }); xf(ctx.parser.assertionEndpointStatement, (s, t) => { if (s.test == void 0) { return t`addEndpoint(thisFacet => ({ assertion: ${walk(s.template)}, analysis: null }));`; } else { return t`addEndpoint(thisFacet => (${walk(s.test)}) ? ({ assertion: ${walk(s.template)}, analysis: null }) : ({ assertion: void 0, analysis: null }), ${''+s.isDynamic});`; } }); xf(ctx.parser.dataflowStatement, (s, t) => t`addDataflow(function (thisFacet) {${walk(s.body)}});`); xf(ctx.parser.eventHandlerEndpointStatement, (s, t) => { switch (s.triggerType) { case 'dataflow': 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 (thisFacet) {${walk(s.body)}});`; } case 'asserted': case 'retracted': case 'message': { const sa = compilePattern(s.pattern); const expectedEvt = ({ 'asserted': 'ADDED', 'retracted': 'REMOVED', 'message': 'MESSAGE', })[s.triggerType]; return t`addEndpoint(thisFacet => ({ assertion: __SYNDICATE__.Observe(${walk(sa.assertion)}), analysis: { skeleton: ${walk(sa.skeleton)}, constPaths: ${JSON.stringify(sa.constPaths)}, constVals: [${commaJoin(sa.constVals.map(walk))}], capturePaths: ${JSON.stringify(sa.capturePaths)}, callback: thisFacet.wrap((thisFacet, __Evt, ${ctx.argDecl(t, '__vs', 'Array<__SYNDICATE__.Value>')}) => { if (__Evt === __SYNDICATE__.Skeleton.EventType.${expectedEvt}) { ${joinItems(sa.captureBinders.map(binderTypeGuard(t)), '\n')} thisFacet.scheduleScript(() => {${terminalWrap(t, s.terminal, walk(s.body))}}); } }) } }), ${'' + s.isDynamic});`; } } }); x(ctx.parser.typeDefinitionStatement, (s, t) => { const l = `Symbol.for(${JSON.stringify(s.label.text)})`; const fns = JSON.stringify(s.fields.map(f => f.id.text)); const fs = ctx.typescript ? t`<${facetFieldObjectType(t, s.fields, t`__SYNDICATE__.Value`)}, any>` : ''; return t`const ${[s.label]} = __SYNDICATE__.Record.makeConstructor${fs}()(${maybeWalk(s.wireName) ?? l}, ${fns});`; }); xf(ctx.parser.messageSendStatement, (s, t) => t`_send(${walk(s.expr)});`); xf(ctx.parser.reactStatement, (s, t) => { const body = ctx.withCollectedFields(s.facetFields, () => walk(s.body)); const fieldTypeParam = ctx.typescript ? t`<${facetFieldObjectType(t, s.facetFields)}>` : ''; return t`addChildFacet${fieldTypeParam}(function (thisFacet) {${body}});`; }); x(ctx.parser.activationImport, (s) => { const activationScriptId: Token = { start: s.activationKeyword.start, end: s.activationKeyword.end, text: `__SYNDICATE__activationScript${'' + ctx.activationRecords.length}`, type: TokenType.ATOM }; ctx.activationRecords.push({ activation: s, activationScriptId }); return []; }), 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}(${facetDecl}) {${body}}`; case 'require': return t`module.exports.${BootProc} = function (${facetDecl}) {${body}};`; case 'global': return t`function ${BootProc}(${facetDecl}) {${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); let tree = stripShebang(laxRead(source, { start, extraDelimiters: ':' })); const end = tree.length > 0 ? tree[tree.length - 1].end : start; const macro = new Templates(undefined, { extraDelimiters: ':' }); const ctx = new ExpansionContext(moduleType, typescript); tree = expand(tree, ctx); const ts = macro.template(fixPos(start)); const te = macro.template(fixPos(end)); if (ctx.hasBootProc) { let bp; switch (moduleType) { case 'es6': case 'global': bp = BootProc; break; case 'require': bp = te`module.exports.${BootProc}`; break; } tree = te`${tree}\nif (typeof module !== 'undefined' && ((typeof require === 'undefined' ? {main: void 0} : require).main === module)) __SYNDICATE__.bootModule(${bp});`; } const activationImports = ctx.activationRecords.map(r => { const a = r.activation; const t = macro.template(a.activationKeyword.start); switch (a.target.type) { case 'import': return t`import { ${BootProc} as ${[r.activationScriptId]} } from ${[a.target.moduleName]};\n`; case 'expr': return t`const ${[r.activationScriptId]} = (${a.target.moduleExpr}).${BootProc};\n`; } }); tree = ts`${joinItems(activationImports)}${tree}`; { const runtime = options.runtime ?? '@syndicate-lang/core'; switch (moduleType) { case 'es6': tree = ts`import * as __SYNDICATE__ from ${JSON.stringify(runtime)};\n${tree}`; break; case 'require': tree = ts`const __SYNDICATE__ = require(${JSON.stringify(runtime)});\n${tree}`; break; case 'global': tree = ts`const __SYNDICATE__ = ${runtime};\n${tree}`; break; } } const cw = new CodeWriter(inputFilename); cw.emit(tree); const text = cw.text; return { text, map: cw.map, targetToSourceMap: cw.targetToSourceMap.index(), sourceToTargetMap: cw.sourceToTargetMap.index(), }; }