JsonRPC/src/ir.ts

327 lines
10 KiB
TypeScript

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<TypeFieldDefinition>((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<EnumValueDefinition>((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<string>();
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,
};
}