First Commit
Yes, that is what I do at 31.12.2021 at 22:39 local time...
This commit is contained in:
381
src/parser.ts
Normal file
381
src/parser.ts
Normal file
@ -0,0 +1,381 @@
|
||||
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;
|
||||
}
|
Reference in New Issue
Block a user