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; array: boolean; } export interface IServiceFunctionReturn { type: string; array: boolean; } export type Decorators = Map; 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(); 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 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; }