Module activation; batch compilation

This commit is contained in:
Tony Garnock-Jones 2021-01-19 15:13:42 +01:00
parent 8c2729e3d8
commit 7be246a400
16 changed files with 351 additions and 81 deletions

View File

@ -1,8 +1,8 @@
import {
isToken, isTokenType, replace, commaJoin, startPos, fixPos,
isToken, isTokenType, replace, commaJoin, startPos, fixPos, joinItems,
Items, Pattern, Templates, Substitution, TokenType,
SourceMap, StringScanner, LaxReader, CodeWriter, TemplateFunction,
SourceMap, StringScanner, LaxReader, CodeWriter, TemplateFunction, Token,
} from '../syntax/index.js';
import {
FacetAction, Statement,
@ -21,6 +21,9 @@ import {
reactStatement,
bootStatement,
stopStatement,
Identifier,
activationImport,
ActivationImport,
} from './grammar.js';
import {
BootProc,
@ -54,7 +57,18 @@ function receiverFor(s: FacetAction): Substitution {
return (s.implicitFacet) ? 'thisFacet.' : '.';
}
export function expand(tree: Items, moduleType: ModuleType): Items {
export interface ActivationRecord {
activation: ActivationImport;
activationScriptId: Identifier;
}
export interface ExpansionContext {
moduleType: ModuleType;
activationRecords: Array<ActivationRecord>;
hasBootProc: boolean;
}
export function expand(tree: Items, ctx: ExpansionContext): Items {
const macro = new Templates();
function terminalWrap(t: TemplateFunction, isTerminal: boolean, body: Statement): Statement {
@ -73,7 +87,7 @@ export function expand(tree: Items, moduleType: ModuleType): Items {
x(p, (v, t) => t`${receiverFor(v)}${f(v, t)}`);
}
const walk = (tree: Items): Items => expand(tree, moduleType);
const walk = (tree: Items): Items => expand(tree, ctx);
const maybeWalk = (tree?: Items) : Items | undefined => (tree === void 0) ? tree : walk(tree);
xf(duringStatement, (s, t) => {
@ -173,14 +187,29 @@ export function expand(tree: Items, moduleType: ModuleType): Items {
xf(reactStatement, (s, t) => t`addChildFacet(function (thisFacet) {${walk(s.body)}});`);
x(activationImport, (s, t) => {
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(bootStatement, (s, t) => {
switch (moduleType) {
ctx.hasBootProc = true;
const activationStatements = ctx.activationRecords.map(({ activationScriptId: id }) =>
t`thisFacet.activate(${[id]}); `);
const body = t`${joinItems(activationStatements)}${walk(s)}`;
switch (ctx.moduleType) {
case 'es6':
return t`export function ${BootProc}(thisFacet) {${walk(s)}}`;
return t`export function ${BootProc}(thisFacet) {${body}}`;
case 'require':
return t`module.exports.${BootProc} = function (thisFacet) {${walk(s)}};`;
return t`module.exports.${BootProc} = function (thisFacet) {${body}};`;
case 'global':
return t`function ${BootProc}(thisFacet) {${walk(s)}}`;
return t`function ${BootProc}(thisFacet) {${body}}`;
}
});
@ -195,34 +224,67 @@ export function compile(options: CompileOptions): CompilerOutput {
const moduleType = options.module ?? 'es6';
const start = startPos(inputFilename);
const scanner = new StringScanner(start, source);
const reader = new LaxReader(scanner);
let tree = stripShebang(reader.readToEnd());
const end = tree.length > 0 ? tree[tree.length - 1].end : start;
let macro = new Templates();
const end = tree.length > 0 ? tree[tree.length - 1].end : start;
const ctx: ExpansionContext = {
moduleType,
activationRecords: [],
hasBootProc: false,
}
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';
const t = macro.template(fixPos(start));
switch (moduleType) {
case 'es6':
tree = t`import * as __SYNDICATE__ from ${JSON.stringify(runtime)};\n${tree}`;
tree = ts`import * as __SYNDICATE__ from ${JSON.stringify(runtime)};\n${tree}`;
break;
case 'require':
tree = t`const __SYNDICATE__ = require(${JSON.stringify(runtime)});\n${tree}`;
tree = ts`const __SYNDICATE__ = require(${JSON.stringify(runtime)});\n${tree}`;
break;
case 'global':
tree = t`const __SYNDICATE__ = ${runtime};\n${tree}`;
tree = ts`const __SYNDICATE__ = ${runtime};\n${tree}`;
break;
}
}
tree = macro.template(fixPos(end))`${tree}\nif (typeof module !== 'undefined' && ((typeof require === 'undefined' ? {main: void 0} : require).main === module)) __SYNDICATE__.bootModule(${BootProc});`;
const cw = new CodeWriter(inputFilename);
cw.emit(expand(tree, moduleType));
cw.emit(tree);
return {
text: cw.text,

View File

@ -6,7 +6,7 @@ import {
scope, bind, seq, alt, upTo, atom, atomString, group, exec,
repeat, option, withoutSpace, map, mapm, rest, discard,
value, succeed, fail, separatedBy, anything, not,
value, succeed, fail, separatedBy, anything, not, follows,
} from '../syntax/index.js';
import * as Matcher from '../syntax/matcher.js';
import { Path, Skeleton } from './internals.js';
@ -244,6 +244,21 @@ export const bootStatement: Pattern<Statement> =
// Principal: Facet
export const stopStatement = blockFacetAction(atom('stop'));
export interface ActivationImport {
activationKeyword: Identifier;
target: { type: 'import', moduleName: Token } | { type: 'expr', moduleExpr: Expr };
}
// Principal: none
export const activationImport: Pattern<ActivationImport> =
scope(o => seq(bind(o, 'activationKeyword', atom('activate')),
follows(alt<any>(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

View File

@ -56,6 +56,14 @@ export function not<T>(p: Pattern<any>, v: T): Pattern<T> {
return i => p(i) === null ? [v, i] : null;
}
export function follows(p: Pattern<any>): Pattern<undefined> {
return i => {
const r = p(i);
if (r === null) return null;
return [r[0], i];
};
}
export function seq(... patterns: Pattern<any>[]): Pattern<any> {
return i => {
for (const p of patterns) {

View File

@ -53,7 +53,7 @@ export class Templates {
}
}
export function joinItems(itemss: Items[], separator0: Substitution): Items {
export function joinItems(itemss: Items[], separator0: Substitution = ''): Items {
if (itemss.length === 0) return [];
const separator = toItems(separator0, startPos(null));
const acc = itemss[0];

View File

@ -68,6 +68,8 @@ export function _canonicalizeDataflowDependent(i: DataflowDependent): string {
return '' + i.id;
}
export type ActivationScript = Script<void>;
export abstract class Dataspace {
nextId: ActorId = 0;
index = new Skeleton.Index();
@ -77,6 +79,7 @@ export abstract class Dataspace {
runnable: Array<Actor> = [];
pendingTurns: Array<Turn>;
actors: IdentityMap<number, Actor> = new IdentityMap();
activations: IdentitySet<ActivationScript> = new IdentitySet();
constructor(bootProc: Script<void>) {
this.pendingTurns = [new Turn(null, [new Spawn(null, bootProc, new Set())])];
@ -390,6 +393,23 @@ class DeferredTurn extends Action {
}
}
class Activation extends Action {
readonly script: ActivationScript;
readonly name: any;
constructor(script: ActivationScript, name: any) {
super();
this.script = script;
this.name = name;
}
perform(ds: Dataspace, ac: Actor | null): void {
if (ds.activations.has(this.script)) return;
ds.activations.add(this.script);
ds.addActor(this.name, rootFacet => rootFacet.addStartScript(this.script), new Set(), ac);
}
}
export class Turn {
readonly actor: Actor | null;
readonly actions: Array<Action>;
@ -623,9 +643,9 @@ export class Facet {
}
}
ensureNonFacetSetup(what: string, keyword: string) {
ensureNonFacetSetup(what: string) {
if (!this.inScript) {
throw new Error(`Cannot ${what} during facet setup; are you missing \`${keyword} { ... }\`?`);
throw new Error(`Cannot ${what} during facet setup; are you missing \`on start { ... }\`?`);
}
}
@ -635,7 +655,7 @@ export class Facet {
}
send(body: any) {
this.ensureNonFacetSetup('`send`', 'on start');
this.ensureNonFacetSetup('`send`');
this.enqueueScriptAction(new Message(body));
}
@ -645,15 +665,20 @@ export class Facet {
}
spawn(name: any, bootProc: Script<void>, initialAssertions?: Set) {
this.ensureNonFacetSetup('`spawn`', 'on start');
this.ensureNonFacetSetup('`spawn`');
this.enqueueScriptAction(new Spawn(name, bootProc, initialAssertions));
}
deferTurn(continuation: Script<void>) {
this.ensureNonFacetSetup('`deferTurn`', 'on start');
this.ensureNonFacetSetup('`deferTurn`');
this.enqueueScriptAction(new DeferredTurn(this.wrap(continuation)));
}
activate(script: ActivationScript, name?: any) {
this.ensureNonFacetSetup('`activate`');
this.enqueueScriptAction(new Activation(script, name ?? null));
}
scheduleScript(script: Script<void>, priority?: Priority) {
this.actor.scheduleTask(this.wrap(script), priority);
}

View File

@ -16,7 +16,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//---------------------------------------------------------------------------
import { Dataspace, Script } from './dataspace.js';
import { ActivationScript, Dataspace } from './dataspace.js';
export type StopHandler<D extends Dataspace> = (ds: D) => void;
@ -33,7 +33,7 @@ export class Ground extends Dataspace {
stopHandlers: Array<StopHandler<this>> = [];
backgroundTaskCount = 0;
constructor(bootProc: Script<void>) {
constructor(bootProc: ActivationScript) {
super(function (rootFacet) { rootFacet.addStartScript(bootProc); });
if (typeof window !== 'undefined') {
window._ground = this;
@ -118,12 +118,14 @@ export class Ground extends Dataspace {
// if (k) k(g);
// }
export function bootModule(bootProc: Script<void>): Ground {
export function bootModule(bootProc: ActivationScript): Ground {
const g = new Ground(bootProc);
if (typeof document !== 'undefined') {
document.addEventListener('DOMContentLoaded', () => g.start());
} else {
g.start();
}
Ground.laterCall(() => {
if (typeof document !== 'undefined') {
document.addEventListener('DOMContentLoaded', () => g.start());
} else {
g.start();
}
});
return g;
}

View File

@ -1 +1,2 @@
index.js
index.js.map

View File

@ -0,0 +1,13 @@
<!doctype HTML>
<html>
<meta charset=utf-8>
<title>Demo</title>
<script src="node_modules/@syndicate-lang/core/dist/syndicate.js"></script>
<script src="index.js"></script>
<h1>Look in the JavaScript console for output.</h1>
<main id="main">
</main>
<script>
Syndicate.bootModule(Main.__SYNDICATE__bootProc);
</script>
</html>

View File

@ -4,9 +4,10 @@
"description": "Simple syndicatec example",
"main": "index.js",
"scripts": {
"prepare": "npm run compile",
"compile": "npx syndicatec -o index.js --module global --runtime Syndicate index.syndicate.js",
"clean": "rm -f index.js"
"prepare": "npm run compile && npm run rollup",
"compile": "npx syndicatec -d lib -b src 'src/**/*.js'",
"rollup": "npx rollup -c",
"clean": "rm -rf lib/ index.js index.js.map"
},
"author": "Tony Garnock-Jones <tonyg@leastfixedpoint.com>",
"license": "GPL-3.0+",
@ -14,6 +15,8 @@
"@syndicate-lang/core": "file:../../../core"
},
"devDependencies": {
"@syndicate-lang/syndicatec": "file:../.."
"@syndicate-lang/syndicatec": "file:../..",
"rollup": "^2.37.0",
"rollup-plugin-sourcemaps": "^0.6.3"
}
}

View File

@ -0,0 +1,15 @@
import sourcemaps from 'rollup-plugin-sourcemaps';
export default {
input: 'lib/index.js',
plugins: [sourcemaps()],
output: {
file: 'index.js',
format: 'umd',
name: 'Main',
sourcemap: true,
globals: {
'@syndicate-lang/core': 'Syndicate',
},
},
};

View File

@ -16,12 +16,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//---------------------------------------------------------------------------
assertion type BoxState(value);
message type SetBox(newValue);
const N = 100000;
console.time('box-and-client-' + N.toString());
import { BoxState, SetBox, N } from './protocol.js';
boot {
spawn named 'box' {
@ -31,12 +26,4 @@ boot {
console.log('terminated box root facet');
on message SetBox($v) => this.value = v;
}
spawn named 'client' {
on asserted BoxState($v) => send message SetBox(v + 1);
on retracted BoxState(_) => console.log('box gone');
}
thisFacet.actor.dataspace.addStopHandler(() =>
console.timeEnd('box-and-client-' + N.toString()));
}

View File

@ -0,0 +1,26 @@
//---------------------------------------------------------------------------
// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS.
// Copyright (C) 2016-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//---------------------------------------------------------------------------
import { BoxState, SetBox } from './protocol.js';
boot {
spawn named 'client' {
on asserted BoxState($v) => send message SetBox(v + 1);
on retracted BoxState(_) => console.log('box gone');
}
}

View File

@ -0,0 +1,27 @@
//---------------------------------------------------------------------------
// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS.
// Copyright (C) 2016-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//---------------------------------------------------------------------------
import { N } from './protocol.js';
activate import './box.js';
activate import './client.js';
console.time('box-and-client-' + N.toString());
boot {
thisFacet.actor.dataspace.addStopHandler(() =>
console.timeEnd('box-and-client-' + N.toString()));
}

View File

@ -0,0 +1,22 @@
//---------------------------------------------------------------------------
// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS.
// Copyright (C) 2016-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//---------------------------------------------------------------------------
export assertion type BoxState(value);
export message type SetBox(newValue);
export const N = 100000;

View File

@ -16,6 +16,7 @@
"dependencies": {
"@syndicate-lang/compiler": "file:../compiler",
"@syndicate-lang/core": "file:../core",
"glob": "^7.1.6",
"yargs": "^16.2.0"
},
"bin": {

View File

@ -2,6 +2,9 @@ import yargs from 'yargs/yargs';
import { Argv } from 'yargs';
import fs from 'fs';
import path from 'path';
import { glob } from 'glob';
import { compile } from '@syndicate-lang/compiler';
export type ModuleChoice = 'es6' | 'require' | 'global';
@ -9,7 +12,9 @@ const moduleChoices: ReadonlyArray<ModuleChoice> = ['es6', 'require', 'global'];
export type CommandLineArguments = {
input: string | undefined;
output: string | undefined;
outputDirectory?: string | undefined;
rootDirectory?: string;
rename: string | undefined;
map: boolean;
mapExtension?: string;
runtime: string;
@ -22,19 +27,62 @@ function checkModuleChoice<T>(t: T & { module: string }): T & { module: ModuleCh
throw new Error("Illegal --module argument: " + t.module);
}
function makeRenamer(outputDir: string,
rootDir: string,
renamePattern: string | undefined): (f: string) => string
{
const rewrites: Array<(f: string) => (string | null)> =
(renamePattern === void 0 ? [] : renamePattern.split(/,/)).map(p => {
const [ from, to ] = p.split(/:/);
let mFrom = /([^%]*)%([^%]*)/.exec(from);
let mTo = /([^%]*)%([^%]*)/.exec(to);
if (mFrom === null && mTo === null) {
return f => (f === from) ? to : null;
} else if (mFrom === null || mTo === null) {
throw new Error(`Invalid --rename pattern: ${JSON.stringify(p)}`);
} else {
const [fh, ft] = mFrom.slice(1);
const [th, tt] = mTo.slice(1);
return f =>
(f.startsWith(fh) && f.endsWith(ft))
? th + f.substring(fh.length, f.length - ft.length) + tt
: null;
}
});
const relocate = (f: string) => path.join(outputDir, path.relative(rootDir, f));
return f => {
for (const rewrite of rewrites) {
const t = rewrite(f);
if (t !== null) return relocate(t);
}
return relocate(f);
};
}
export function main(argv: string[]) {
const options: CommandLineArguments = checkModuleChoice(yargs(argv)
.command('$0 [input]',
'Compile a single file',
'Compile away Syndicate extensions',
yargs => yargs
.positional('input', {
type: 'string',
description: 'Input filename',
description: 'Input filename or glob (stdin if omitted)',
})
.option('output', {
alias: 'o',
.option('root-directory', {
alias: 'b',
type: 'string',
description: 'Output filename (stdout if omitted)',
description: 'Root directory for input files',
default: '.',
})
.option('output-directory', {
alias: 'd',
type: 'string',
description: 'Output directory (if omitted: stdout if stdin as input, else cwd)',
})
.option('rename', {
type: 'string',
description: 'Rewrite input filenames',
default: '%.syndicate.js:%.js,%.syndicate.ts:%.ts',
})
.option('map', {
type: 'boolean',
@ -59,37 +107,52 @@ export function main(argv: string[]) {
argv => argv)
.argv);
const inputFilename = options.input ?? '/dev/stdin';
const source = fs.readFileSync(inputFilename, 'utf-8');
const rename = makeRenamer(options.outputDirectory ?? '',
options.rootDirectory ?? '.',
options.rename);
const { text, map } = compile({
source,
name: inputFilename,
runtime: options.runtime,
module: options.module,
});
map.sourcesContent = [source];
const STDIN = '/dev/stdin';
function mapDataURL() {
const mapData = Buffer.from(JSON.stringify(map)).toString('base64')
return `data:application/json;base64,${mapData}`;
}
const inputGlob = options.input ?? STDIN;
const inputFilenames = glob.sync(inputGlob);
for (const inputFilename of inputFilenames) {
const outputFilename =
(inputFilename === STDIN) ? '/dev/stdout' :
(inputFilename[0] === '/') ? (() => { throw new Error("Absolute input paths are not supported"); })() :
rename(inputFilename);
if (inputFilenames.indexOf(outputFilename) !== -1) {
throw new Error(`Output from ${JSON.stringify(inputFilename)} would trample on existing input file ${JSON.stringify(outputFilename)}`);
}
const source = fs.readFileSync(inputFilename, 'utf-8');
const { text, map } = compile({
source,
name: inputFilename,
runtime: options.runtime,
module: options.module,
});
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 });
}
if (options.output !== void 0) {
if (!options.map) {
fs.writeFileSync(options.output, text);
} else if (options.mapExtension) {
const mapFilename = options.output + options.mapExtension;
fs.writeFileSync(options.output, text + `\n//# sourceMappingURL=${mapFilename}`);
fs.writeFileSync(outputFilename, text);
} else if (options.mapExtension && inputFilename !== STDIN) {
const mapFilename = outputFilename + options.mapExtension;
fs.writeFileSync(outputFilename, text + `\n//# sourceMappingURL=${mapFilename}`);
fs.writeFileSync(mapFilename, JSON.stringify(map));
} else {
fs.writeFileSync(options.output, text + `\n//# sourceMappingURL=${mapDataURL()}`);
}
} else {
if (!options.map) {
console.log(text);
} else {
console.log(text + `\n//# sourceMappingURL=${mapDataURL()}`);
fs.writeFileSync(outputFilename, text + `\n//# sourceMappingURL=${mapDataURL()}`);
}
}
}