syndicate-js/packages/compiler/src/compiler/codegen.ts

349 lines
13 KiB
TypeScript

/// SPDX-License-Identifier: GPL-3.0-or-later
/// SPDX-FileCopyrightText: Copyright © 2016-2023 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
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<Token>;
sourceToTargetMap: SpanIndex<number>;
}
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<T>(p: Pattern<T>, f: (v: T, t: TemplateFunction) => Items) {
tree = replace(tree, p, (v, start) => f(v, macro.template(fixPos(start))));
}
function xf<T extends TurnAction>(p: Pattern<T>, 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(),
};
}