/// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-FileCopyrightText: Copyright © 2016-2023 Tony Garnock-Jones import { isToken, isTokenType, replace, commaJoin, startPos, fixPos, joinItems, laxRead, itemText, Items, Pattern, Templates, Substitution, TokenType, SourceMap, CodeWriter, TemplateFunction, Token, SpanIndex, match, TokenBase, getRange, Pos, } from '../syntax/index.js'; import { SyndicateParser, SyndicateTypedParser, Identifier, TurnAction, Statement, Binder, compilePattern, SpawnStatement, } from './grammar.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 type ErrorSink = (message: string, start: Pos | undefined, end: Pos | undefined) => void; export interface CompileOptions { source: string, name?: string, runtime?: string, module?: ModuleType, global?: string, typescript?: boolean, emitError: ErrorSink, } export interface CompilerOutput { text: string, map: SourceMap, targetToSourceMap: SpanIndex; sourceToTargetMap: SpanIndex; } export class ExpansionContext { readonly parser: SyndicateParser; readonly moduleType: ModuleType; readonly typescript: boolean; readonly errorEmitter: ErrorSink; nextIdNumber = 0; constructor(moduleType: ModuleType, typescript: boolean, errorEmitter: ErrorSink) { this.parser = typescript ? new SyndicateTypedParser : new SyndicateParser(); this.moduleType = moduleType; this.typescript = typescript; this.errorEmitter = errorEmitter; } quasiRandomId(): string { return '__SYNDICATE__id_' + (this.nextIdNumber++); } argDecl(t: TemplateFunction, name: Substitution, type: Substitution): Items { return (this.typescript) ? t`${name}: ${type}` : t`${name}`; } emitError(m: string, loc: TokenBase) { this.errorEmitter(m, loc.start, loc.end); } } function stringifyId(i: Identifier): Items { return [ { ... i, type: TokenType.STRING, text: JSON.stringify(i.text) } ]; } 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: { const intermediate = t`__v_${''+index}`; return t`const ${intermediate} = ${binder.type}.__from_preserve__(${raw}); if (${intermediate} === void 0) return; const ${[binder.id]} = ${intermediate};`; } } } }; } 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}})` } 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`__SYNDICATE__.Turn.active.${f(v, t)}`); } const walk = (tree: Items): Items => expand(tree, ctx); const maybeWalk = (tree?: Items) : Items | undefined => (tree === void 0) ? tree : walk(tree); // Unfortunately, because of the incredibly naive repeated // traversal of the syntax tree we're doing, the order of the // following transformations matters. xf(ctx.parser.duringStatement, (s, t) => { let spawn = match(ctx.parser.spawn, s.body, null); if (spawn !== null) { if (spawn.linkedToken !== null) { ctx.emitError(`during ... spawn doesn't need "linked", it's always linked`, spawn.linkedToken); } spawn.linkedToken = getRange(s.body); } let body = (spawn === null) ? walk(s.body) : expandSpawn(spawn, t, t`__SYNDICATE__.Turn.activeFacet.preventInertCheck();`); const sa = compilePattern(s.pattern); const assertion = t`__SYNDICATE__.Observe({ pattern: __SYNDICATE__.QuasiValue.finish(${sa.skeleton}), observer: __SYNDICATE__.Turn.ref(__SYNDICATE__.assertionFacetObserver( (${ctx.argDecl(t, '__vs', '__SYNDICATE__.AnyValue')}) => { if (Array.isArray(__vs)) { ${joinItems(sa.captureBinders.map(binderTypeGuard(t)), '\n')} ${body} } } )) })`; if (s.test === void 0) { return t`assertDataflow(() => ({ target: currentSyndicateTarget, assertion: ${assertion} }));`; } else { return t`assertDataflow(() => (${walk(s.test)}) ? ({ target: currentSyndicateTarget, assertion: ${assertion} }) : ({ target: void 0, assertion: void 0 }));`; } }); function expandSpawn(spawn: SpawnStatement, t: TemplateFunction, inject: Items = []): Items { // TODO: parentBinders, parentInits /* let assertions = (s.initialAssertions.length > 0) ? 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__.Turn.active._spawn${spawn.linkedToken ? 'Link': ''}(() => {${n} ${inject} ${walk(spawn.body)} });`; } x(ctx.parser.spawn, expandSpawn); x(ctx.parser.fieldDeclarationStatement, (s, t) => { const ft = ctx.typescript ? t`<${s.field.type ?? '__SYNDICATE__.AnyValue'}>` : ''; return t`const ${[s.field.id]} = __SYNDICATE__.Turn.active.field${ft}(${maybeWalk(s.init) ?? 'void 0'}, ${stringifyId(s.field.id)});`; }); x(ctx.parser.atStatement, (s, t) => { return t`(((${ctx.argDecl(t, 'currentSyndicateTarget', '__SYNDICATE__.Ref')}) => {${walk(s.body)}})(${walk(s.target)}));`; }); x(ctx.parser.createExpression, (s, t) => { return t`__SYNDICATE__.Turn.ref(${walk(s.entity)})`; }); xf(ctx.parser.assertionEndpointStatement, (s, t) => { if (s.isDynamic) { if (s.test === void 0) { return t`assertDataflow(() => ({ target: currentSyndicateTarget, assertion: ${walk(s.template)} }));`; } else { return t`assertDataflow(() => (${walk(s.test)}) ? ({ target: currentSyndicateTarget, assertion: ${walk(s.template)} }) : ({ target: void 0, assertion: void 0 }));`; } } else { if (s.test === void 0) { return t`assert(currentSyndicateTarget, ${walk(s.template)});`; } else { return t`replace(currentSyndicateTarget, void 0, (${walk(s.test)}) ? (${walk(s.template)}) : void 0);`; } } }); xf(ctx.parser.dataflowStatement, (s, t) => t`_dataflow(() => {${walk(s.body)}});`); x(ctx.parser.eventHandlerEndpointStatement, (s, t) => { if (s.triggerType === 'dataflow') { return t`__SYNDICATE__.Turn.active._dataflow(() => { if (${walk(s.predicate)}) { ${terminalWrap(t, s.terminal, walk(s.body))} } });`; } if (s.triggerType === 'stop') { return t`__SYNDICATE__.Turn.activeFacet.onStop(() => {${walk(s.body)}});`; } const sa = compilePattern(s.pattern); const guardBody = (body: Statement) => t`if (Array.isArray(__vs)) { ${joinItems(sa.captureBinders.map(binderTypeGuard(t)), '\n')} ${body} }`; let entity: Items; switch (s.triggerType) { 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)))} } }`; break; case 'retracted': entity = t`__SYNDICATE__.assertionObserver((${ctx.argDecl(t, '__vs', '__SYNDICATE__.AnyValue')}) => { ${guardBody(t`return () => { ${terminalWrap(t, s.terminal, walk(s.body))} };`)} })`; break; case 'message': entity = t`{ message: (${ctx.argDecl(t, '__vs', '__SYNDICATE__.AnyValue')}) => { ${guardBody(terminalWrap(t, s.terminal, walk(s.body)))} } }`; break; } const assertion = t`__SYNDICATE__.Observe({ pattern: __SYNDICATE__.QuasiValue.finish(${sa.skeleton}), observer: __SYNDICATE__.Turn.ref(${entity}), })`; if (s.isDynamic) { return t`__SYNDICATE__.Turn.active.assertDataflow(() => ({ target: currentSyndicateTarget, assertion: ${assertion}, }));`; } else { return t`__SYNDICATE__.Turn.active.replace(currentSyndicateTarget, void 0, ${assertion});`; } }); 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 formatBinder = (b: Binder) => t`${[b.id]}: ${b.type ?? '__SYNDICATE__.AnyValue'}`; const fs = ctx.typescript ? t`<{${commaJoin(s.fields.map(formatBinder))}}, __SYNDICATE__.Ref>` : ''; return t`const ${[s.label]} = __SYNDICATE__.Record.makeConstructor${fs}()(${maybeWalk(s.wireName) ?? l}, ${fns});`; }); xf(ctx.parser.messageSendStatement, (s, t) => t`message(currentSyndicateTarget, ${walk(s.expr)});`); xf(ctx.parser.reactStatement, (s, t) => { return t`facet(() => {${s.body}});`; }); x(ctx.parser.stopStatement, (s, t) => t`__SYNDICATE__.Turn.active._stop(__SYNDICATE__.Turn.activeFacet, () => {${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, options.emitError); tree = expand(tree, ctx); const ts = macro.template(fixPos(start)); // const te = macro.template(fixPos(end)); { 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(), }; }