ast_generator/ast_generator.ts

186 lines
5.9 KiB
TypeScript

import {
Grammar,
Parser,
} from "https://deno.land/x/nearley@2.19.7-deno/mod.ts";
import compiledParserGrammar from "./parser.out.ts";
import {
Enum,
Name,
Node,
Param,
Param_Named,
Statement,
Type,
} from "./ast.out.ts";
class TypescriptGenerator {
private result = "";
public generate(ast: Statement[]): string {
this.result += "// Generated file by ast_generator\n";
for (const statement of ast) {
switch (statement.kind) {
case "Enum":
this.generateEnum(statement[0]);
break;
case "Node":
this.generateNode(statement[0]);
break;
}
this.result += "\n";
}
return this.result;
}
private generateEnum(enum_: Enum) {
if (this.isShallowEnum(enum_)) {
return this.generateShallowEnumBody(enum_);
} else {
return this.generateEnumBody(enum_);
}
}
private isShallowEnum(enum_: Enum): boolean {
return !enum_.nodes.some((node) => node.params.length > 0);
}
private generateShallowEnumBody(enum_: Enum) {
const name = this.makeName(enum_.name);
const fields = enum_.nodes.map((node) => this.makeName(node.name)).map(
(name) => `${name}: "${name}"`,
).join(", ");
this.result += `export const ${name} = { ${fields} } as const;\n`;
this.result += `export type ${name} = keyof typeof ${name};\n`;
}
private generateEnumBody(enum_: Enum) {
const enumName = this.makeName(enum_.name);
const nodeNames: [string, string][] = [];
for (const node of enum_.nodes) {
const name = this.makeName(node.name);
const fullName = `${enumName}_${name}`;
nodeNames.push([name, fullName]);
this.result += `export type ${fullName} = {\n`;
this.result += ` kind: "${name}",\n`;
this.result += node.params.map((p, i) => this.makeParamField(p, i))
.map((field) => ` ${field},\n`).join("");
this.result += `};\n`;
const fnParams = this.makeFnParams(node.params);
const fields = this.makeFnFields(node.params);
this.result += `export const ${fullName}` +
` = (${fnParams}): ${fullName}` +
` => ({ kind: "${name}", ${fields} });\n`;
}
const typeList = nodeNames
.map(([_, fullName]) => fullName)
.join(" | ");
this.result += `export type ${enumName} = ${typeList};\n`;
const enumFieldFns = nodeNames
.map(([name, fullName]) => `${name}: ${fullName}`)
.join(", ");
this.result +=
`export const ${enumName} = { ${enumFieldFns} } as const;\n`;
}
private makeParamField(param: Param, index: number): string {
switch (param.kind) {
case "Named":
return this.makeNamedParam(param);
case "Unnamed":
return `[${index}]: ${this.makeType(param[0])}`;
}
}
private generateNode(
node: Node,
) {
const name = this.makeName(node.name);
this.result += `export type ${name} = {\n`;
for (
const [param, index] of node.params
.map((v, i) => [v, i] as const)
) {
switch (param.kind) {
case "Named":
this.result += ` ${this.makeNamedParam(param)};\n`;
break;
case "Unnamed": {
const type_ = this.makeType(param[0]);
this.result += ` [${index}]: ${type_};\n`;
break;
}
}
}
this.result += "};\n";
const fnParams = this.makeFnParams(node.params);
const fields = this.makeFnFields(node.params);
this.result += `export const ${name}` +
` = (${fnParams}): ${name}` +
` => ({ ${fields} });\n`;
}
private makeFnParams(params: Param[]): string {
return params
.map((param, index) => {
switch (param.kind) {
case "Named":
return this.makeNamedParam(param);
case "Unnamed":
return `v${index}: ${this.makeType(param[0])}`;
}
})
.join(", ");
}
private makeFnFields(params: Param[]): string {
return params.map((param, index) => {
switch (param.kind) {
case "Named":
return this.makeName(param.name);
case "Unnamed":
return `[${index}]: v${index}`;
}
})
.join(", ");
}
private makeNamedParam(param: Param_Named): string {
const name = this.makeName(param.name);
const type_ = this.makeType(param.type_);
return `${name}: ${type_}`;
}
private makeType(type_: Type): string {
switch (type_.kind) {
case "Name":
return this.makeName(type_[0]);
case "Optional":
return `${this.makeType(type_[0])} | null`;
case "Multiple":
return `${this.makeType(type_[0])}[]`;
}
}
private makeName(name: Name): string {
return name[0];
}
}
const parser = new Parser(Grammar.fromCompiled(compiledParserGrammar));
if (Deno.args.length < 1) {
console.log("generate_ast <file>");
throw new Error("not enough args");
}
if (["-h", "--help"].includes(Deno.args[0])) {
console.log("generate_ast <file>");
Deno.exit(0);
}
const text = await Deno.readTextFile(Deno.args[0]);
parser.feed(text);
const ast = parser.results[0];
// console.log(JSON.stringify(ast, null, "│ "));
const ts = new TypescriptGenerator().generate(ast);
console.log(ts);