196 lines
6.2 KiB
TypeScript
196 lines
6.2 KiB
TypeScript
import {
|
|
Grammar,
|
|
Parser,
|
|
} from "https://deno.land/x/nearley@2.19.7-deno/mod.ts";
|
|
|
|
import compiledParserGrammar from "./parser.out.ts";
|
|
import {
|
|
CodeBlock,
|
|
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 "CodeBlock":
|
|
this.generateCodeBlock(statement[0]);
|
|
break;
|
|
case "Enum":
|
|
this.generateEnum(statement[0]);
|
|
break;
|
|
case "Node":
|
|
this.generateNode(statement[0]);
|
|
break;
|
|
}
|
|
this.result += "\n";
|
|
}
|
|
return this.result;
|
|
}
|
|
|
|
private generateCodeBlock(block: CodeBlock) {
|
|
this.result += `${block[0]}\n`;
|
|
}
|
|
|
|
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 += ` type: "${enumName}",\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}` +
|
|
` => ({ type: "${enumName}", 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`;
|
|
this.result += ` 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}` +
|
|
` => ({ type: "${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);
|