382 lines
9.1 KiB
TypeScript
382 lines
9.1 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;
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
export interface ServiceFunctionStatement extends DefinitionNode {
|
||
|
type: "service_function";
|
||
|
inputs: IServiceFunctionInput[];
|
||
|
name: string;
|
||
|
return_type: string | undefined; // Makes it a notification
|
||
|
}
|
||
|
|
||
|
export interface ServiceStatement extends DefinitionNode {
|
||
|
type: "service";
|
||
|
name: string;
|
||
|
functions: ServiceFunctionStatement[];
|
||
|
}
|
||
|
|
||
|
export type RootStatementNode =
|
||
|
| ImportStatement
|
||
|
| TypeStatement
|
||
|
| ServiceStatement
|
||
|
| EnumStatement;
|
||
|
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();
|
||
|
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 },
|
||
|
};
|
||
|
};
|
||
|
|
||
|
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 parseServiceFunction = (
|
||
|
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();
|
||
|
inputs.push({ name, type });
|
||
|
if (currentToken.value !== ",") break;
|
||
|
eatToken(",");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
eatToken(")");
|
||
|
|
||
|
let return_type = undefined;
|
||
|
if (!notification) {
|
||
|
eatToken(":");
|
||
|
|
||
|
return_type = eatText()[0];
|
||
|
}
|
||
|
|
||
|
eatToken(";");
|
||
|
|
||
|
return {
|
||
|
type: "service_function",
|
||
|
name,
|
||
|
location: {
|
||
|
file,
|
||
|
idx,
|
||
|
},
|
||
|
inputs,
|
||
|
return_type,
|
||
|
};
|
||
|
};
|
||
|
|
||
|
const parseServiceStatement = (): ServiceStatement => {
|
||
|
let idx = eatToken("service");
|
||
|
let [name] = eatText();
|
||
|
eatToken("{");
|
||
|
let functions: ServiceFunctionStatement[] = [];
|
||
|
|
||
|
while (currentToken.type === "text") {
|
||
|
let notification = false;
|
||
|
if (currentToken.value == "notification") {
|
||
|
eatText();
|
||
|
notification = true;
|
||
|
}
|
||
|
functions.push(parseServiceFunction(notification));
|
||
|
}
|
||
|
eatToken("}");
|
||
|
|
||
|
return {
|
||
|
type: "service",
|
||
|
name: name,
|
||
|
functions,
|
||
|
location: { file, idx },
|
||
|
};
|
||
|
};
|
||
|
|
||
|
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();
|
||
|
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;
|
||
|
}
|