JsonRPC/src/parser.ts
2023-01-02 16:34:13 +01:00

489 lines
12 KiB
TypeScript

import type { Token } from "./tokenizer";
export interface DefinitionNode {
type: string;
location: {
file: string;
idx: number;
};
}
export interface ImportStatement extends DefinitionNode {
type: "import";
path: string;
}
export interface TypeFieldStatement extends DefinitionNode {
type: "type_field";
name: string;
optional: boolean;
fieldtype: string;
array: boolean;
map?: string;
}
export interface EnumValueStatement extends DefinitionNode {
type: "enum_value";
name: string;
value?: number;
}
export interface EnumStatement extends DefinitionNode {
type: "enum";
name: string;
values: EnumValueStatement[];
}
export interface TypeStatement extends DefinitionNode {
type: "type";
name: string;
fields: TypeFieldStatement[];
}
export interface IServiceFunctionInput {
name: string;
type: string;
array: boolean;
}
export interface IServiceFunctionReturn {
type: string;
array: boolean;
}
export type Decorators = Map<string, string[][]>;
export interface ServiceFunctionStatement extends DefinitionNode {
type: "service_function";
inputs: IServiceFunctionInput[];
name: string;
return_type: IServiceFunctionReturn | undefined; // Makes it a notification
decorators: Decorators;
}
export interface ServiceStatement extends DefinitionNode {
type: "service";
name: string;
functions: ServiceFunctionStatement[];
}
export interface DefineStatement extends DefinitionNode {
type: "define";
key: string;
value: string;
}
export type RootStatementNode =
| ImportStatement
| TypeStatement
| ServiceStatement
| EnumStatement
| DefineStatement;
export type StatementNode =
| RootStatementNode
| TypeFieldStatement
| ServiceFunctionStatement
| EnumValueStatement;
export type Parsed = RootStatementNode[];
export class ParserError extends Error {
token: Token;
constructor(message: string, token: Token) {
super(message);
this.token = token;
}
}
export default function parse(tokens: Token[], file: string): Parsed {
const tokenIterator = tokens[Symbol.iterator]();
let currentToken: Token = tokenIterator.next().value;
let nextToken: Token = tokenIterator.next().value;
const eatToken = (value?: string) => {
if (value && value !== currentToken.value) {
throw new ParserError(
`Unexpected token value, expected '${value}', received '${currentToken.value}'`,
currentToken
);
}
let idx = currentToken.startIdx;
currentToken = nextToken;
nextToken = tokenIterator.next().value;
return idx;
};
const eatText = (): [string, number] => {
checkTypes("text");
let val = currentToken.value;
let idx = currentToken.startIdx;
eatToken();
return [val, idx];
};
const eatNumber = (): number => {
checkTypes("number");
let val = Number(currentToken.value);
if (Number.isNaN(val)) {
throw new ParserError(
`Value cannot be parsed as number! ${currentToken.value}`,
currentToken
);
}
eatToken();
return val;
};
const checkTypes = (...types: string[]) => {
if (types.indexOf(currentToken.type) < 0) {
throw new ParserError(
`Unexpected token value, expected ${types.join(" | ")}, received '${currentToken.value
}'`,
currentToken
);
}
};
// const parseUnionField = (): UnionFieldStatement => {
// let idx = currentToken.startIdx;
// let name = currentToken.value;
// eatToken();
// eatToken(":");
// let [type] = eatText();
// eatToken("=");
// let label = eatNumber();
// eatToken(";");
// return {
// type: "union_field",
// name,
// label,
// fieldtype: type,
// location: { file, idx },
// };
// };
const parseTypeField = (): TypeFieldStatement => {
const idx = currentToken.startIdx;
let name = currentToken.value;
eatToken();
let optional = false;
if (currentToken.type === "questionmark") {
eatToken("?");
optional = true;
}
eatToken(":");
let array = false;
let type: string;
let mapKey: string | undefined = undefined;
if (currentToken.type === "curly_open") {
eatToken("{");
[mapKey] = eatText();
eatToken(",");
[type] = eatText();
eatToken("}");
} else {
[type] = eatText();
if (currentToken.type === "array") {
array = true;
eatToken("[]");
}
}
eatToken(";");
return {
type: "type_field",
name,
fieldtype: type,
array,
map: mapKey,
location: { file, idx },
optional
};
};
const parseTypeStatement = (): TypeStatement => {
const idx = eatToken("type");
let [name] = eatText();
eatToken("{");
let fields: TypeFieldStatement[] = [];
while (currentToken.type === "text" || currentToken.type === "keyword") {
//Keywords can also be field names
fields.push(parseTypeField());
}
eatToken("}");
return {
type: "type",
name,
fields,
location: { file, idx },
};
};
// const parseUnionStatement = (): UnionStatement => {
// const idx = eatToken("union");
// let [name] = eatText();
// eatToken("{");
// let fields: UnionFieldStatement[] = [];
// while (currentToken.type === "text") {
// fields.push(parseUnionField());
// }
// eatToken("}");
// return {
// type: "union",
// name,
// fields,
// location: { file, idx },
// };
// };
const parseImportStatement = (): ImportStatement => {
const idx = eatToken("import");
checkTypes("text", "string");
let path = currentToken.value;
if (currentToken.type === "string") {
path = path.substring(1, path.length - 1);
}
eatToken();
eatToken(";");
return {
type: "import",
path,
location: { file, idx },
};
};
const parseEnumValue = (): EnumValueStatement => {
let [name, idx] = eatText();
let value = undefined;
if (currentToken.type === "equals") {
eatToken("=");
value = eatNumber();
}
return {
type: "enum_value",
name,
value,
location: { file, idx },
};
};
const parseEnumStatement = (): EnumStatement => {
let idx = eatToken("enum");
let [name] = eatText();
eatToken("{");
let values: EnumValueStatement[] = [];
let next = currentToken.type === "text";
while (next) {
values.push(parseEnumValue());
if (currentToken.type === "comma") {
eatToken(",");
next = true;
} else {
next = false;
}
}
eatToken("}");
return {
type: "enum",
name: name,
values: values,
location: { file, idx },
};
};
const parseFunctionDecorator = (decorators: Decorators = new Map) => {
const idx = eatToken("@");
const [decorator] = eatText();
eatToken("(");
let args: string[] = [];
let first = true;
while (currentToken.value !== ")") {
if (first) {
first = false;
} else {
eatToken(",");
}
checkTypes("string");
// if(currentToken.type == "number") {
// let arg = Number(currentToken.value);
// if (Number.isNaN(arg)) {
// throw new ParserError(
// `Value cannot be parsed as number! ${currentToken.value}`,
// currentToken
// );
// }
// }
args.push(currentToken.value.slice(1, -1));
eatToken();
}
eatToken(")");
let dec = decorators.get(decorator) || [];
dec.push(args);
decorators.set(decorator, dec);
return decorators;
}
const parseServiceFunction = (
decorators: Decorators,
notification?: boolean,
): ServiceFunctionStatement => {
const [name, idx] = eatText();
eatToken("(");
let input_streaming: string | undefined = undefined;
let inputs = [];
if (currentToken.value !== ")") {
while (true) {
const [name] = eatText();
eatToken(":");
const [type] = eatText();
let array = false;
if (currentToken.type === "array") {
array = true;
eatToken("[]");
}
inputs.push({ name, type, array });
if (currentToken.value !== ",") break;
eatToken(",");
}
}
eatToken(")");
let return_type: IServiceFunctionReturn | undefined = undefined;
if (!notification) {
eatToken(":");
let [type] = eatText();
let array = false;
if (currentToken.type === "array") {
array = true;
eatToken("[]");
}
return_type = {
type,
array
}
}
eatToken(";");
return {
type: "service_function",
name,
location: {
file,
idx,
},
inputs,
return_type,
decorators
};
};
const parseServiceStatement = (): ServiceStatement => {
let idx = eatToken("service");
let [name] = eatText();
eatToken("{");
let functions: ServiceFunctionStatement[] = [];
while (currentToken.type !== "curly_close") {
let decorators: Decorators = new Map;
while (currentToken.type == "at") {
parseFunctionDecorator(decorators);
}
let notification = false;
if (currentToken.value == "notification") {
eatText();
notification = true;
}
functions.push(parseServiceFunction(decorators, notification));
}
eatToken("}");
return {
type: "service",
name: name,
functions,
location: { file, idx },
};
};
const parseDefine = (): DefineStatement => {
const idx = eatToken("define");
let [key] = eatText()
let value: string = undefined;
if (currentToken.type == "string") {
value = currentToken.value.slice(1, -1);
eatToken();
} else {
[value] = eatText()
}
eatToken(";");
return {
type: "define",
location: { file, idx },
key,
value
}
}
const parseStatement = () => {
if (currentToken.type === "keyword") {
switch (currentToken.value) {
case "type":
return parseTypeStatement();
// case "union":
// return parseUnionStatement();
case "import":
return parseImportStatement();
case "enum":
return parseEnumStatement();
case "service":
return parseServiceStatement();
case "define":
return parseDefine();
default:
throw new ParserError(
`Unknown keyword ${currentToken.value}`,
currentToken
);
}
} else {
throw new ParserError(
`Invalid statement! ${currentToken.value}`,
currentToken
);
}
};
const nodes: RootStatementNode[] = [];
while (currentToken) {
nodes.push(parseStatement());
}
return nodes;
}