489 lines
12 KiB
TypeScript
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;
|
|
}
|