import type { Parsed, StatementNode } from "./parser"; import dbg from "debug"; const log = dbg("app"); const BUILTIN = ["number", "string", "boolean"]; export class IRError extends Error { constructor(public statement: StatementNode, message: string) { super("Error building IR: " + message); } } export interface TypeFieldDefinition { name: string; type: string; array: boolean; map?: string; } export interface TypeDefinition { name: string; depends: string[]; fields: TypeFieldDefinition[]; } export interface EnumValueDefinition { name: string; value: number; } export interface EnumDefinition { name: string; values: EnumValueDefinition[]; } export type ServiceFunctionDecorators = { description: string; parameters: { name: string; description: string; }[]; returns: string; }; export interface ServiceFunctionParamsDefinition { name: string; inputs: { type: string; name: string; array: boolean }[]; return: { type: string; array: boolean } | undefined; decorators: ServiceFunctionDecorators; } export type ServiceFunctionDefinition = ServiceFunctionParamsDefinition; export interface ServiceDefinition { name: string; depends: string[]; functions: ServiceFunctionDefinition[]; } export type Step = [ "type" | "enum" | "service", TypeDefinition | EnumDefinition | ServiceDefinition ]; export type IR = { options: { [key: string]: string }; steps: Step[]; }; export default function get_ir(parsed: Parsed): IR { log("Generatie IR from parse output"); let builtin = [...BUILTIN]; let defined: string[] = []; let types: string[] = []; let enums: string[] = []; // Verifiy and generating steps let steps: Step[] = []; let options = {} as any; parsed.forEach((statement) => { log("Working on statement of type %s", statement.type); if (statement.type == "import") throw new IRError( statement, "Import statements are invalid at this step!" ); if (statement.type === "type") { if (defined.indexOf(statement.name) >= 0) { throw new IRError( statement, `Type ${statement.name} already defined!` ); } let depends: string[] = []; const fields = statement.fields.map((field) => { if (field.type !== "type_field") { throw new IRError(field, "Invalid statement!"); } if (defined.indexOf(field.fieldtype) < 0) { if ( builtin.indexOf(field.fieldtype) < 0 && field.fieldtype !== statement.name ) { throw new IRError( field, `Type ${field.fieldtype} is not defined!` ); } } else { if (depends.indexOf(field.fieldtype) < 0) depends.push(field.fieldtype); } if (field.map && field.map !== "number" && field.map !== "string") { throw new IRError( field, `Type ${field.map} is not valid as map key!` ); } return { name: field.name, type: field.fieldtype, array: field.array, map: field.map, }; }); steps.push([ statement.type, { name: statement.name, depends, fields, }, ]); defined.push(statement.name); types.push(statement.name); } else if (statement.type === "enum") { if (defined.indexOf(statement.name) >= 0) { throw new IRError( statement, `Type ${statement.name} already defined!` ); } let last = -1; let values = statement.values.map((values) => { let value = last + 1; if (values.value) { if (values.value <= last) { throw new IRError( statement, "Enum value must be larger than the previous one!" ); } else { value = values.value; } } last = value; return { name: values.name, value, }; }); steps.push([ "enum", { name: statement.name, values, } as EnumDefinition, ]); defined.push(statement.name); enums.push(statement.name); } else if (statement.type === "service") { if (defined.indexOf(statement.name) >= 0) { throw new IRError( statement, `Type ${statement.name} already defined!` ); } let depends: string[] = []; let alreadyFoundFunctions = new Set(); let functions = statement.functions.map((fnc) => { if (alreadyFoundFunctions.has(fnc.name)) throw new IRError( fnc, `Function with name ${fnc.name} already defined!` ); alreadyFoundFunctions.add(fnc.name); if (fnc.return_type) { if (defined.indexOf(fnc.return_type.type) >= 0) { if (!depends.some((a) => a === fnc.return_type.type)) depends.push(fnc.return_type.type); } else { if ( fnc.return_type.type !== "void" && builtin.indexOf(fnc.return_type.type) < 0 ) { throw new IRError( fnc, `Type ${fnc.return_type.type} is not defined` ); } } } for (const input of fnc.inputs) { if (defined.indexOf(input.type) >= 0) { if (!depends.some((a) => a === input.type)) depends.push(input.type); } else { if (builtin.indexOf(input.type) < 0) { throw new IRError( fnc, `Type ${input.type} is not defined` ); } } } let decorators = {} as ServiceFunctionDecorators; fnc.decorators.forEach((values, key) => { for (const val of values) { switch (key) { case "Description": if (decorators.description) throw new IRError( fnc, `Decorator 'Description' can only be used once!` ); if (val.length != 1) throw new IRError( fnc, `Decorator 'Description' requires exactly one parameter!` ); decorators.description = val[0]; break; case "Returns": if (decorators.returns) throw new IRError( fnc, `Decorator 'Returns' can only be used once!` ); if (val.length != 1) throw new IRError( fnc, `Decorator 'Returns' requires exactly one parameter!` ); decorators.returns = val[0]; break; case "Param": if (!decorators.parameters) decorators.parameters = []; if (val.length != 2) throw new IRError( fnc, `Decorator 'Param' requires exactly two parameters!` ); const [name, description] = val; if (!fnc.inputs.find((e) => e.name == name)) throw new IRError( fnc, `Decorator 'Param' requires the first param to equal the name of a function parameter!` ); if (decorators.parameters.find((e) => e.name == name)) throw new IRError( fnc, `Decorator 'Param' has already been set for the parameter ${name}!` ); decorators.parameters.push({ name, description, }); break; default: throw new IRError( fnc, `Decorator ${key} is not a valid decorator!` ); } } }); return { name: fnc.name, inputs: fnc.inputs, return: fnc.return_type, decorators, } as ServiceFunctionDefinition; }); steps.push([ "service", { name: statement.name, depends, functions, } as ServiceDefinition, ]); } else if (statement.type == "define") { options[statement.key] = statement.value; if ( (statement.key == "use_messagepack" || statement.key == "allow_bytes") && statement.value == "true" ) { builtin.push("bytes"); } } else { throw new IRError(statement, "Invalid statement!"); } }); return { options, steps, }; }