185 lines
3.9 KiB
TypeScript
185 lines
3.9 KiB
TypeScript
import {
|
|
Command,
|
|
ErrorResult,
|
|
InterpolatedPiece,
|
|
Script,
|
|
SimplifyWord,
|
|
Word,
|
|
} from "./words";
|
|
|
|
/**
|
|
* Parse out a Notcl script into an easier-to-interpret representation.
|
|
* No script is actually executed yet.
|
|
*
|
|
* @param code code to parse
|
|
* @param offset source position of code, if embedded in a larger source document
|
|
* @returns parsed list of commands, or error message on failure
|
|
*/
|
|
export function parse(
|
|
code: string,
|
|
offset = 0
|
|
): [true, Script] | [false, string] {
|
|
try {
|
|
const parser = new Parser(code);
|
|
const script = parser.parseScript();
|
|
parser.expect("EOF");
|
|
|
|
return [true, script];
|
|
} catch (ex) {
|
|
// TODO: report error with error position
|
|
return [false, String(ex)];
|
|
}
|
|
}
|
|
|
|
// ---------------------------
|
|
|
|
// Parser for evaluating Notcl scripts
|
|
|
|
type TokenType =
|
|
| "newline"
|
|
| "whitespace"
|
|
| "semicolon"
|
|
| "{"
|
|
| "}"
|
|
| "["
|
|
| "]"
|
|
| "quote"
|
|
| "backslash"
|
|
| "comment"
|
|
| "text"
|
|
| "EOF"
|
|
| "ERROR";
|
|
|
|
type Token = [TokenType, string, number];
|
|
|
|
const Tokens: [TokenType, RegExp][] = [
|
|
["newline", /(\n)/y],
|
|
["whitespace", /([^\S\n]+)/y],
|
|
["semicolon", /(;)/y],
|
|
["{", /(\{)/y],
|
|
["}", /(\})/y],
|
|
["[", /(\[)/y],
|
|
["]", /(\])/y],
|
|
["quote", /(")/y],
|
|
["backslash", /(\\)/y],
|
|
["comment", /(\#)/y],
|
|
["text", /([^\s\\;\[\]]+)/y],
|
|
];
|
|
|
|
class WipScript {
|
|
script: Command[] = [];
|
|
wipCommand: Word[] = [];
|
|
wipWord: InterpolatedPiece[] = [];
|
|
wordPos: number | undefined = undefined;
|
|
// TODO: thing to fail {}a & ""a
|
|
|
|
startOfWord(): boolean {
|
|
return this.wipWord.length == 0;
|
|
}
|
|
|
|
addWordPiece(piece: InterpolatedPiece, pos?: number) {
|
|
if (this.startOfWord()) {
|
|
this.wordPos = pos;
|
|
}
|
|
this.wipWord.push(piece);
|
|
}
|
|
finishWord() {
|
|
if (this.wipWord.length > 0) {
|
|
this.wipCommand.push(SimplifyWord(this.wipWord, this.wordPos));
|
|
this.wipWord = [];
|
|
this.wordPos = undefined;
|
|
}
|
|
}
|
|
finishCommand() {
|
|
this.finishWord();
|
|
if (this.wipCommand.length > 0) {
|
|
this.script.push(this.wipCommand);
|
|
this.wipCommand = [];
|
|
}
|
|
}
|
|
finishScript(): Script {
|
|
this.finishCommand();
|
|
return this.script;
|
|
}
|
|
}
|
|
|
|
class Parser {
|
|
lastIndex: number = 0;
|
|
next: Token;
|
|
|
|
constructor(public text: string) {
|
|
this.next = this.advance();
|
|
}
|
|
|
|
advance(): Token {
|
|
const startPos = this.lastIndex;
|
|
if (startPos == this.text.length) {
|
|
return (this.next = ["EOF", "<EOF>", startPos]);
|
|
}
|
|
|
|
for (const [type, regex] of Tokens) {
|
|
regex.lastIndex = startPos;
|
|
const matches = regex.exec(this.text);
|
|
if (matches) {
|
|
this.lastIndex = regex.lastIndex;
|
|
return (this.next = [type, matches[1], startPos]);
|
|
}
|
|
}
|
|
|
|
return (this.next = ["ERROR", "Token not matched", startPos]);
|
|
}
|
|
|
|
expect(type: TokenType) {
|
|
if (this.next[0] != type) {
|
|
throw new Error(
|
|
`Expected ${type}, found ${this.next[0]} (${this.next[1]})`
|
|
);
|
|
}
|
|
}
|
|
|
|
parseScript(): Script {
|
|
const wip = new WipScript();
|
|
|
|
while (true) {
|
|
const [type, chars, pos] = this.next;
|
|
switch (type) {
|
|
case "text":
|
|
wip.addWordPiece({ bare: chars }, pos);
|
|
break;
|
|
|
|
case "[": {
|
|
this.advance();
|
|
const script = this.parseScript();
|
|
wip.addWordPiece({ script });
|
|
this.expect("]");
|
|
break;
|
|
}
|
|
|
|
case "whitespace":
|
|
wip.finishWord();
|
|
break;
|
|
|
|
case "newline":
|
|
case "semicolon":
|
|
wip.finishCommand();
|
|
break;
|
|
|
|
case "EOF":
|
|
case "]":
|
|
return wip.finishScript();
|
|
|
|
case "{":
|
|
case "}":
|
|
case "quote":
|
|
case "backslash":
|
|
case "comment":
|
|
case "ERROR":
|
|
throw new Error(`Unhandled case: ${type} (${chars})`);
|
|
default:
|
|
throw new Error(`Unhandled case: ${type satisfies never} (${chars})`);
|
|
}
|
|
|
|
this.advance();
|
|
}
|
|
}
|
|
}
|