2020-05-09 22:34:13 +02:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
|
|
import * as path from "path"
|
|
|
|
import * as fs from "fs"
|
|
|
|
|
2020-05-10 11:58:07 +02:00
|
|
|
import { parse, SyntaxError } from "../treegen/parser"
|
|
|
|
import { Syntax, Declaration, NodeDeclaration, TypeDeclaration, EnumDeclaration, TypeNode, NodeField } from "../ast"
|
2020-05-09 23:41:42 +02:00
|
|
|
import { FileWriter } from "../util"
|
2020-05-10 11:58:07 +02:00
|
|
|
import minimist from "minimist"
|
2020-05-09 22:34:13 +02:00
|
|
|
|
2020-05-10 11:58:07 +02:00
|
|
|
const PACKAGE_ROOT = path.join(__dirname, '..', '..');
|
|
|
|
|
|
|
|
const argv = minimist(process.argv.slice(2));
|
|
|
|
|
|
|
|
const jsFilePath = argv['js-file'] ?? 'lib/ast.js';
|
|
|
|
const dtsFilePath = argv['dts-file'] ?? 'src/ast.d.ts';
|
|
|
|
|
|
|
|
for (const filename of argv._) {
|
2020-05-09 22:34:13 +02:00
|
|
|
const contents = fs.readFileSync(filename, 'utf8');
|
|
|
|
let decls: Declaration[];
|
|
|
|
try {
|
2020-05-10 11:58:07 +02:00
|
|
|
decls = parse(contents, { prefix: getFileStem(filename) });
|
2020-05-09 22:34:13 +02:00
|
|
|
} catch (e) {
|
|
|
|
if (e instanceof SyntaxError) {
|
|
|
|
console.error(`${filename}:${e.location.start.line}:${e.location.start.column}: ${e.message}`);
|
|
|
|
process.exit(1);
|
|
|
|
} else {
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
}
|
2020-05-10 11:58:07 +02:00
|
|
|
const { jsFile, dtsFile } = generateAST(decls);
|
|
|
|
fs.writeFileSync(jsFilePath, jsFile, 'utf8');
|
|
|
|
fs.writeFileSync(dtsFilePath, dtsFile, 'utf8');
|
2020-05-09 22:34:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
interface FastStringMap<T> { [key: string]: T }
|
|
|
|
|
2020-05-10 11:58:07 +02:00
|
|
|
function generateAST(decls: Declaration[]) {
|
2020-05-09 23:41:42 +02:00
|
|
|
|
|
|
|
let jsFile = new FileWriter();
|
2020-05-10 11:58:07 +02:00
|
|
|
let dtsFile = new FileWriter();
|
|
|
|
let i;
|
|
|
|
|
|
|
|
// Sort declarations by category
|
2020-05-09 23:41:42 +02:00
|
|
|
|
|
|
|
const nodeDecls: NodeDeclaration[] = decls.filter(decl => decl.type === 'NodeDeclaration') as NodeDeclaration[];
|
|
|
|
const typeDecls: TypeDeclaration[] = decls.filter(decl => decl.type === 'TypeDeclaration') as TypeDeclaration[];
|
|
|
|
const enumDecls: EnumDeclaration[] = decls.filter(decl => decl.type === 'EnumDeclaration') as EnumDeclaration[];
|
2020-05-09 22:34:13 +02:00
|
|
|
|
|
|
|
const declByName: FastStringMap<Declaration> = Object.create(null);
|
2020-05-10 11:58:07 +02:00
|
|
|
i = 0;
|
2020-05-09 22:34:13 +02:00
|
|
|
for (const decl of decls) {
|
2020-05-10 11:58:07 +02:00
|
|
|
decl.index = i++;
|
2020-05-09 22:34:13 +02:00
|
|
|
declByName[decl.name] = decl;
|
|
|
|
}
|
|
|
|
|
2020-05-09 23:41:42 +02:00
|
|
|
// Generate a mapping from parent node to child node
|
|
|
|
// This makes it easy to generate union types for the intermediate nodes.
|
|
|
|
|
|
|
|
const childrenOf: FastStringMap<string[]> = Object.create(null);
|
|
|
|
for (const nodeDecl of nodeDecls) {
|
|
|
|
for (const parentName of nodeDecl.parents) {
|
|
|
|
if (childrenOf[parentName] === undefined) {
|
|
|
|
childrenOf[parentName] = [];
|
|
|
|
}
|
|
|
|
childrenOf[parentName].push(nodeDecl.name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-10 11:58:07 +02:00
|
|
|
// After we're done mappping parents to children, we can use isLeafNode()
|
|
|
|
// to store the nodes we will be iterating most frequently on.
|
|
|
|
|
|
|
|
const leafNodes: NodeDeclaration[] = nodeDecls.filter(decl => isLeafNode(decl.name));
|
|
|
|
|
2020-05-09 23:41:42 +02:00
|
|
|
// Write a JavaScript file that contains all AST definitions.
|
|
|
|
|
2020-05-10 11:58:07 +02:00
|
|
|
jsFile.write(`\nconst NODE_TYPES = {\n`);
|
2020-05-09 23:41:42 +02:00
|
|
|
jsFile.indent();
|
2020-05-10 11:58:07 +02:00
|
|
|
for (const decl of leafNodes) {
|
2020-05-09 23:41:42 +02:00
|
|
|
if (decl.type === 'NodeDeclaration' && isLeafNode(decl.name)) {
|
2020-05-10 11:58:07 +02:00
|
|
|
jsFile.write(`'${decl.name}': {\n`);
|
|
|
|
jsFile.indent();
|
|
|
|
jsFile.write(`index: ${decl.index},\n`);
|
|
|
|
jsFile.write(`fields: new Map([\n`);
|
2020-05-09 23:41:42 +02:00
|
|
|
jsFile.indent();
|
|
|
|
for (const field of getAllFields(decl)) {
|
2020-05-10 11:58:07 +02:00
|
|
|
jsFile.write(`['${field.name}', ${JSON.stringify(jsonify(field.typeNode))}],\n`);
|
2020-05-09 23:41:42 +02:00
|
|
|
}
|
|
|
|
jsFile.dedent();
|
|
|
|
jsFile.write(']),\n');
|
2020-05-10 11:58:07 +02:00
|
|
|
jsFile.dedent();
|
|
|
|
jsFile.write('},\n');
|
2020-05-09 23:41:42 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
jsFile.dedent();
|
2020-05-10 11:58:07 +02:00
|
|
|
jsFile.write('};\n\n');
|
|
|
|
|
|
|
|
jsFile.write(fs.readFileSync(path.join(PACKAGE_ROOT, 'src', 'treegen', 'ast-template.js'), 'utf8'));
|
|
|
|
|
|
|
|
jsFile.write(`if (typeof module !== 'undefined') {\n module.exports = exported;\n}\n\n`)
|
|
|
|
|
|
|
|
// Write corresponding TypeScript declarations
|
|
|
|
|
|
|
|
dtsFile.write(`\nexport const enum SyntaxKind {\n`);
|
|
|
|
for (const decl of leafNodes) {
|
|
|
|
dtsFile.write(` ${decl.name} = ${decl.index}\n`);
|
|
|
|
}
|
|
|
|
dtsFile.write(`}\n\n`);
|
|
|
|
|
|
|
|
for (const decl of leafNodes) {
|
|
|
|
dtsFile.write(`export function create${decl.name}(`);
|
|
|
|
for (const field of getAllFields(decl)) {
|
|
|
|
dtsFile.write(`${field.name}: ${emitTypeScriptType(field.typeNode)}, `);
|
|
|
|
}
|
|
|
|
dtsFile.write(`span: TextSpan | null = null, origNodes: SyntaxRange | null = null);\n`);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
jsFile: jsFile.currentText,
|
|
|
|
dtsFile: dtsFile.currentText,
|
|
|
|
};
|
2020-05-09 23:41:42 +02:00
|
|
|
|
2020-05-10 11:58:07 +02:00
|
|
|
// Below are some useful functions
|
|
|
|
|
|
|
|
function hasDeclarationNamed(name: string): boolean {
|
|
|
|
return name in declByName;
|
|
|
|
}
|
|
|
|
|
|
|
|
function emitTypeScriptType(typeNode: TypeNode): string {
|
2020-05-09 23:41:42 +02:00
|
|
|
if (typeNode.type === 'ReferenceTypeNode') {
|
|
|
|
if (hasDeclarationNamed(typeNode.name)) {
|
|
|
|
return typeNode.name;
|
2020-05-10 11:58:07 +02:00
|
|
|
} else if (typeNode.name === 'Option') {
|
|
|
|
return `${emitTypeScriptType(typeNode.typeArgs[0])} | null`;
|
|
|
|
} else if (typeNode.name === 'Vec') {
|
|
|
|
return `${emitTypeScriptType(typeNode.typeArgs[0])}[]`;
|
2020-05-09 23:41:42 +02:00
|
|
|
} else if (typeNode.name === 'String') {
|
2020-05-10 11:58:07 +02:00
|
|
|
return `string`;
|
|
|
|
} else if (typeNode.name === 'Int') {
|
|
|
|
return `bigint`;
|
2020-05-09 23:41:42 +02:00
|
|
|
} else if (typeNode.name === 'usize') {
|
2020-05-10 11:58:07 +02:00
|
|
|
return `number`;
|
|
|
|
} else if (typeNode.name === 'bool') {
|
|
|
|
return `boolean`;
|
|
|
|
} else {
|
|
|
|
throw new Error(`Could not emit TypeScript type for reference type node named ${typeNode.name}`);
|
2020-05-09 23:41:42 +02:00
|
|
|
}
|
2020-05-10 11:58:07 +02:00
|
|
|
} else if (typeNode.type === 'UnionTypeNode') {
|
|
|
|
return typeNode.elements.map(emitTypeScriptType).join(' | ');
|
2020-05-09 23:41:42 +02:00
|
|
|
}
|
2020-05-10 11:58:07 +02:00
|
|
|
//throw new Error(`Could not emit TypeScript type for type node ${typeNode.type}`);
|
2020-05-09 23:41:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function getAllFields(nodeDecl: NodeDeclaration) {
|
|
|
|
let out: NodeField[] = [];
|
|
|
|
pushAll(out, nodeDecl.fields);
|
|
|
|
for (const parentName of nodeDecl.parents) {
|
|
|
|
const parentDecl = getDeclarationNamed(parentName);
|
|
|
|
if (parentDecl.type !== 'NodeDeclaration') {
|
|
|
|
throw new Error(`Parent declaration '${parentName}' of '${nodeDecl.name}' must be a node declaration.`);
|
|
|
|
}
|
|
|
|
pushAll(out, getAllFields(parentDecl));
|
|
|
|
}
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getDeclarationNamed(name: string): Declaration {
|
|
|
|
const decl = declByName[name];
|
|
|
|
if (decl === undefined) {
|
|
|
|
throw new Error(`Declaration '${name}' was not found in any of the definition files.`);
|
|
|
|
}
|
|
|
|
return decl;
|
|
|
|
}
|
|
|
|
|
|
|
|
function isLeafNode(name: string): boolean {
|
|
|
|
const decl = getDeclarationNamed(name);
|
|
|
|
if (decl.type !== 'NodeDeclaration') {
|
|
|
|
throw new Error(`Declaration '${name}' is not a node declaration.`)
|
|
|
|
}
|
|
|
|
return childrenOf[name] === undefined || childrenOf[name].length === 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
function pushAll<T>(arr: T[], els: T[]): void {
|
|
|
|
for (const el of els) {
|
|
|
|
arr.push(el);
|
|
|
|
}
|
2020-05-09 22:34:13 +02:00
|
|
|
}
|
|
|
|
|
2020-05-10 11:58:07 +02:00
|
|
|
function isNode(value: any): value is Syntax {
|
|
|
|
return typeof value === 'object' && value !== null && value.__IS_NODE;
|
|
|
|
}
|
|
|
|
|
2020-05-09 22:34:13 +02:00
|
|
|
function jsonify(value: any) {
|
|
|
|
|
|
|
|
function visitNode(node: any) {
|
|
|
|
|
|
|
|
const obj: any = {};
|
|
|
|
|
|
|
|
for (const key of Object.keys(node)) {
|
2020-05-10 11:58:07 +02:00
|
|
|
if (key !== 'type' && key !== 'span' && key !== '__IS_NODE') {
|
2020-05-09 22:34:13 +02:00
|
|
|
const value = node[key];
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
obj[key] = value.map(visit);
|
|
|
|
} else {
|
|
|
|
obj[key] = visit(value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return obj;
|
|
|
|
}
|
|
|
|
|
|
|
|
function visit(value: any) {
|
2020-05-10 11:58:07 +02:00
|
|
|
if (isNode(value)) {
|
2020-05-09 22:34:13 +02:00
|
|
|
return visitNode(value);
|
|
|
|
} else {
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return visit(value);
|
|
|
|
}
|
|
|
|
|
2020-05-10 11:58:07 +02:00
|
|
|
function stripSuffix(str: string, suffix: string): string {
|
|
|
|
if (!str.endsWith(suffix)) {
|
|
|
|
return str;
|
|
|
|
}
|
|
|
|
return str.substring(0, str.length-suffix.length);
|
|
|
|
}
|
|
|
|
|
2020-05-09 22:34:13 +02:00
|
|
|
function getFileStem(filepath: string) {
|
|
|
|
return path.basename(filepath).split('.')[0];
|
|
|
|
}
|
|
|
|
|