360 lines
13 KiB
TypeScript
360 lines
13 KiB
TypeScript
/// SPDX-License-Identifier: GPL-3.0-or-later
|
|
/// SPDX-FileCopyrightText: Copyright © 2016-2024 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' | 'none';
|
|
|
|
export type ErrorSink = (message: string, start: Pos | undefined, end: Pos | undefined) => void;
|
|
|
|
export interface CompileOptions {
|
|
source: string,
|
|
name?: string,
|
|
runtime?: string,
|
|
module?: ModuleType,
|
|
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 facetWrap(t: TemplateFunction, items: Items): Items {
|
|
return t`__SYNDICATE__.Turn.active.facet(() => {${items}})`;
|
|
}
|
|
|
|
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__.Dataspace._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) => {
|
|
const wrap = s.once ? (i: Items) => facetWrap(t, 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))} } });`);
|
|
}
|
|
|
|
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 wrap(t`__SYNDICATE__.Turn.active.assertDataflow(() => ({
|
|
target: currentSyndicateTarget,
|
|
assertion: ${assertion},
|
|
}));`);
|
|
} else {
|
|
return wrap(
|
|
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)});`);
|
|
|
|
x(ctx.parser.reactStatement, (s, t) => facetWrap(t, 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;
|
|
case 'none':
|
|
break;
|
|
default:
|
|
((_: never) => {
|
|
throw new Error(`Unsupported ModuleType: ${moduleType}`);
|
|
})(moduleType);
|
|
}
|
|
}
|
|
|
|
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(),
|
|
};
|
|
}
|