WIP adding typescript support to compiler; cleanups and fixes

This commit is contained in:
Tony Garnock-Jones 2021-01-19 19:54:48 +01:00
parent 7be246a400
commit a374cbfdf9
19 changed files with 406 additions and 28 deletions

View File

@ -1,5 +1,5 @@
import {
isToken, isTokenType, replace, commaJoin, startPos, fixPos, joinItems,
isToken, isTokenType, replace, commaJoin, startPos, fixPos, joinItems, anonymousTemplate,
Items, Pattern, Templates, Substitution, TokenType,
SourceMap, StringScanner, LaxReader, CodeWriter, TemplateFunction, Token,
@ -46,6 +46,7 @@ export interface CompileOptions {
runtime?: string,
module?: ModuleType,
global?: string,
typescript?: boolean,
}
export interface CompilerOutput {
@ -62,10 +63,26 @@ export interface ActivationRecord {
activationScriptId: Identifier;
}
export interface ExpansionContext {
moduleType: ModuleType;
activationRecords: Array<ActivationRecord>;
hasBootProc: boolean;
export class ExpansionContext {
readonly moduleType: ModuleType;
readonly activationRecords: Array<ActivationRecord> = [];
hasBootProc: boolean = false;
readonly typescript: boolean;
constructor(moduleType: ModuleType,
typescript: boolean)
{
this.moduleType = moduleType;
this.typescript = typescript;
}
argDecl(name: Substitution, type: Substitution): Substitution {
return this.typescript ? anonymousTemplate`${name}: ${type}` : name;
}
get thisFacetDecl(): Substitution {
return this.argDecl('thisFacet', '__SYNDICATE__.Facet');
}
}
export function expand(tree: Items, ctx: ExpansionContext): Items {
@ -73,7 +90,7 @@ export function expand(tree: Items, ctx: ExpansionContext): Items {
function terminalWrap(t: TemplateFunction, isTerminal: boolean, body: Statement): Statement {
if (isTerminal) {
return t`thisFacet._stop(function (thisFacet) {${body}})`
return t`thisFacet._stop(function (${ctx.thisFacetDecl}) {${body}})`
} else {
return body;
}
@ -93,7 +110,7 @@ export function expand(tree: Items, ctx: ExpansionContext): Items {
xf(duringStatement, (s, t) => {
// TODO: spawn during
const sa = compilePattern(s.pattern);
return t`withSelfDo(function (thisFacet) {
return t`withSelfDo(function (${ctx.thisFacetDecl}) {
const _Facets = new __SYNDICATE__.Dictionary();
on asserted ${patternText(s.pattern)} => react {
_Facets.set([${commaJoin(sa.captureIds.map(t=>[t]))}], thisFacet);
@ -109,7 +126,7 @@ export function expand(tree: Items, ctx: ExpansionContext): Items {
});
xf(spawn, (s, t) => {
let proc = t`function (thisFacet) {${walk(s.bootProcBody)}}`;
let proc = t`function (${ctx.thisFacetDecl}) {${walk(s.bootProcBody)}}`;
if (s.isDataspace) proc = t`__SYNDICATE__.inNestedDataspace(${proc})`;
let assertions = (s.initialAssertions.length > 0)
? t`, new __SYNDICATE__.Set([${commaJoin(s.initialAssertions.map(walk))}])`
@ -137,17 +154,17 @@ export function expand(tree: Items, ctx: ExpansionContext): Items {
}
});
xf(dataflowStatement, (s, t) => t`addDataflow(function (thisFacet) {${walk(s.body)}});`);
xf(dataflowStatement, (s, t) => t`addDataflow(function (${ctx.thisFacetDecl}) {${walk(s.body)}});`);
xf(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))} } } });`;
return t`withSelfDo(function (${ctx.thisFacetDecl}) { 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)}});`;
return t`${m}(function (${ctx.thisFacetDecl}) {${walk(s.body)}});`;
}
case 'asserted':
@ -185,7 +202,7 @@ export function expand(tree: Items, ctx: ExpansionContext): Items {
xf(messageSendStatement, (s, t) => t`_send(${walk(s.expr)});`);
xf(reactStatement, (s, t) => t`addChildFacet(function (thisFacet) {${walk(s.body)}});`);
xf(reactStatement, (s, t) => t`addChildFacet(function (${ctx.thisFacetDecl}) {${walk(s.body)}});`);
x(activationImport, (s, t) => {
const activationScriptId: Token = {
@ -205,15 +222,15 @@ export function expand(tree: Items, ctx: ExpansionContext): Items {
const body = t`${joinItems(activationStatements)}${walk(s)}`;
switch (ctx.moduleType) {
case 'es6':
return t`export function ${BootProc}(thisFacet) {${body}}`;
return t`export function ${BootProc}(${ctx.thisFacetDecl}) {${body}}`;
case 'require':
return t`module.exports.${BootProc} = function (thisFacet) {${body}};`;
return t`module.exports.${BootProc} = function (${ctx.thisFacetDecl}) {${body}};`;
case 'global':
return t`function ${BootProc}(thisFacet) {${body}}`;
return t`function ${BootProc}(${ctx.thisFacetDecl}) {${body}}`;
}
});
xf(stopStatement, (s, t) => t`_stop(function (thisFacet) {${walk(s.body)}});`)
xf(stopStatement, (s, t) => t`_stop(function (${ctx.thisFacetDecl}) {${walk(s.body)}});`)
return tree;
}
@ -222,6 +239,7 @@ export function compile(options: CompileOptions): CompilerOutput {
const inputFilename = options.name ?? '/dev/stdin';
const source = options.source;
const moduleType = options.module ?? 'es6';
const typescript = options.typescript ?? false;
const start = startPos(inputFilename);
const scanner = new StringScanner(start, source);
@ -231,11 +249,7 @@ export function compile(options: CompileOptions): CompilerOutput {
let macro = new Templates();
const ctx: ExpansionContext = {
moduleType,
activationRecords: [],
hasBootProc: false,
}
const ctx = new ExpansionContext(moduleType, typescript);
tree = expand(tree, ctx);

View File

@ -17,7 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//---------------------------------------------------------------------------
const { bootModule, Dataspace, Skeleton, Ground, Record, Discard, Capture, Observe } = require('../dist/syndicate.js');
const { bootModule, Dataspace, Skeleton, Ground, Record, Discard, Capture, Observe } = require('..');
const __ = Discard._instance;
const _$ = Capture(__);
@ -83,7 +83,7 @@ function boot(thisFacet) {
});
});
thisFacet.actor.dataspace.addStopHandler(() =>
thisFacet.actor.dataspace.ground().addStopHandler(() =>
console.timeEnd('box-and-client-' + N.toString()));
}

View File

@ -0,0 +1,94 @@
#!/usr/bin/env -S npx ts-node -O '{"module": "commonjs"}'
//---------------------------------------------------------------------------
// @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 { bootModule, Skeleton, Record, Discard, Capture, Observe, Facet, DataflowObservableObject } from '..';
const __ = Discard._instance;
const _$ = Capture(__);
const BoxState = Record.makeConstructor('BoxState', ['value']);
const SetBox = Record.makeConstructor('SetBox', ['newValue']);
const N = 100000;
console.time('box-and-client-' + N.toString());
function boot(thisFacet: Facet) {
thisFacet.spawn('box', function (this: DataflowObservableObject & {
value: number;
}, thisFacet: Facet) {
thisFacet.declareField(this, 'value', 0);
thisFacet.addEndpoint(() => {
// console.log('recomputing published BoxState', this.value);
return { assertion: BoxState(this.value), analysis: null };
});
thisFacet.addDataflow(() => {
// console.log('dataflow saw new value', this.value);
if (this.value === N) {
thisFacet.stop(() => {
console.log('terminated box root facet');
});
}
});
thisFacet.addEndpoint(() => {
let analysis = Skeleton.analyzeAssertion(SetBox(_$));
analysis.callback = thisFacet.wrap((thisFacet, evt, [v]) => {
if (evt === Skeleton.EventType.MESSAGE) {
if (typeof v !== 'number') return;
thisFacet.scheduleScript(() => {
this.value = v;
// console.log('box updated value', v);
});
}
});
return { assertion: Observe(SetBox(_$)), analysis };
});
});
thisFacet.spawn('client', function (thisFacet: Facet) {
thisFacet.addEndpoint(() => {
let analysis = Skeleton.analyzeAssertion(BoxState(_$));
analysis.callback = thisFacet.wrap((thisFacet, evt, [v]) => {
if (evt === Skeleton.EventType.ADDED) {
if (typeof v !== 'number') return;
thisFacet.scheduleScript(() => {
// console.log('client sending SetBox', v + 1);
thisFacet.send(SetBox(v + 1));
});
}
});
return { assertion: Observe(BoxState(_$)), analysis };
});
thisFacet.addEndpoint(() => {
let analysis = Skeleton.analyzeAssertion(BoxState(__));
analysis.callback = thisFacet.wrap((thisFacet, evt, _vs) => {
if (evt === Skeleton.EventType.REMOVED) {
thisFacet.scheduleScript(() => {
console.log('box gone');
});
}
});
return { assertion: Observe(BoxState(__)), analysis };
});
});
thisFacet.actor.dataspace.ground().addStopHandler(() =>
console.timeEnd('box-and-client-' + N.toString()));
}
bootModule(boot);

View File

@ -0,0 +1,2 @@
#!/usr/bin/env node
require('../lib/tsc.js').main(process.argv.slice(2));

View File

@ -1,2 +1,2 @@
#!/usr/bin/env node
require('../lib/index.js').main(process.argv.slice(2));
require('../lib/cli.js').main(process.argv.slice(2));

View File

@ -22,6 +22,6 @@ activate import './client.js';
console.time('box-and-client-' + N.toString());
boot {
thisFacet.actor.dataspace.addStopHandler(() =>
thisFacet.actor.dataspace.ground().addStopHandler(() =>
console.timeEnd('box-and-client-' + N.toString()));
}

View File

@ -0,0 +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

@ -0,0 +1,23 @@
{
"name": "@syndicate-lang/syndicatec-javascript-example",
"version": "0.0.0",
"description": "Simple syndicatec example",
"main": "index.js",
"scripts": {
"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+",
"dependencies": {
"@syndicate-lang/core": "file:../../../core"
},
"devDependencies": {
"@syndicate-lang/syndicatec": "file:../..",
"rollup": "^2.37.0",
"rollup-plugin-sourcemaps": "^0.6.3",
"typescript": "^4.1.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

@ -0,0 +1,29 @@
//---------------------------------------------------------------------------
// @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, N } from './protocol.js';
boot {
spawn named 'box' {
field this.value: number = 0;
assert BoxState(this.value);
stop on (this.value === N)
console.log('terminated box root facet');
on message SetBox($v: number) => this.value = v;
}
}

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: number) => 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.ground().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

@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["ES2017", "DOM"],
"declaration": true,
"baseUrl": "./src",
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./lib",
"esModuleInterop": true,
"moduleResolution": "node",
"module": "commonjs",
"sourceMap": true,
"strict": true
},
"include": ["src/**/*"]
}

View File

@ -20,9 +20,14 @@
"yargs": "^16.2.0"
},
"bin": {
"syndicate-tsc": "./bin/syndicate-tsc.js",
"syndicatec": "./bin/syndicatec.js"
},
"devDependencies": {
"@types/yargs": "^15.0.12"
"@types/yargs": "^15.0.12",
"typescript": "^4.1.3"
},
"peerDependencies": {
"typescript": "^4.1.3"
}
}

View File

@ -1,5 +1,4 @@
import yargs from 'yargs/yargs';
import { Argv } from 'yargs';
import fs from 'fs';
import path from 'path';

View File

@ -1 +0,0 @@
export * from './cli.js';

View File

@ -0,0 +1,91 @@
import yargs from 'yargs/yargs';
import ts from 'typescript';
import crypto from 'crypto';
import { compile } from '@syndicate-lang/compiler';
function reportDiagnostic(diagnostic: ts.Diagnostic) {
if (diagnostic.file) {
let { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!);
let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
console.log(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`);
} else {
console.log(ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"));
}
}
function reportErrorSummary(n: number) {
if (n > 0) {
console.log(`\n - ${n} errors reported`);
}
}
function createProgram(rootNames: readonly string[] | undefined,
options: ts.CompilerOptions | undefined,
host?: ts.CompilerHost,
oldProgram?: ts.EmitAndSemanticDiagnosticsBuilderProgram,
configFileParsingDiagnostics?: readonly ts.Diagnostic[],
projectReferences?: readonly ts.ProjectReference[])
: ts.EmitAndSemanticDiagnosticsBuilderProgram
{
if (host === void 0) {
throw new Error("CompilerHost not present - cannot continue");
}
if (rootNames === void 0) {
console.warn("No Syndicate source files to compile");
}
const oldGetSourceFile = host.getSourceFile;
host.getSourceFile = (fileName: string,
languageVersion: ts.ScriptTarget,
onError?: ((message: string) => void),
shouldCreateNewSourceFile?: boolean): ts.SourceFile | undefined => {
if ((rootNames?.indexOf(fileName) ?? -1) !== -1) {
try {
const inputText = host.readFile(fileName);
if (inputText === void 0) {
onError?.(`Could not read input file ${fileName}`);
return undefined;
}
const expandedText = compile({
source: inputText,
name: fileName,
typescript: true,
}).text;
console.log('\n\n', fileName);
expandedText.split(/\n/).forEach((line, i) => {
console.log(i, line);
});
const sf = ts.createSourceFile(fileName, expandedText, languageVersion, true);
(sf as any).version = crypto.createHash('sha256').update(expandedText).digest('hex');
return sf;
} catch (e) {
onError?.(e.message);
return undefined;
}
} else {
return oldGetSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
}
};
return ts.createEmitAndSemanticDiagnosticsBuilderProgram(rootNames,
options,
host,
oldProgram,
configFileParsingDiagnostics,
projectReferences);
}
export function main(argv: string[]) {
const sbh = ts.createSolutionBuilderHost(ts.sys,
createProgram,
reportDiagnostic,
reportDiagnostic,
reportErrorSummary);
const sb = ts.createSolutionBuilder(sbh, ['.'], {
verbose: true,
});
ts.sys.exit(sb.build());
}