2021-01-14 12:09:53 +00:00
|
|
|
import { TokenType, Token, Group, Item, Items } from './tokens.js';
|
|
|
|
import { Scanner } from './scanner.js';
|
|
|
|
|
|
|
|
function matchingParen(c: string): string | null {
|
|
|
|
switch (c) {
|
|
|
|
case ')': return '(';
|
|
|
|
case ']': return '[';
|
|
|
|
case '}': return '{';
|
|
|
|
default: return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class LaxReader implements IterableIterator<Item> {
|
|
|
|
readonly scanner: Scanner;
|
|
|
|
readonly stack: Array<Group> = [];
|
|
|
|
|
|
|
|
constructor(scanner: Scanner) {
|
|
|
|
this.scanner = scanner;
|
|
|
|
}
|
|
|
|
|
|
|
|
[Symbol.iterator](): IterableIterator<Item> {
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
stackTop(): Group | null {
|
|
|
|
return this.stack[this.stack.length - 1] ?? null;
|
|
|
|
}
|
|
|
|
|
|
|
|
popUntilMatch(t: Token): Group | 'continue' | 'eof' {
|
|
|
|
const m = matchingParen(t.text);
|
|
|
|
|
|
|
|
if (m !== null && !this.stack.some(g => g.start.text === m)) {
|
|
|
|
if (this.stack.length > 0) {
|
2021-01-15 12:38:15 +00:00
|
|
|
this.stackTop()!.items.push(t);
|
2021-01-14 12:09:53 +00:00
|
|
|
return 'continue';
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
while (this.stack.length > 0) {
|
2021-01-15 12:38:15 +00:00
|
|
|
const inner = this.stack.pop()!;
|
2021-01-14 12:09:53 +00:00
|
|
|
if (inner.start.text === m) {
|
|
|
|
inner.end = t;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.stack.length === 0) {
|
|
|
|
return inner;
|
|
|
|
} else {
|
2021-01-15 12:38:15 +00:00
|
|
|
const outer = this.stackTop()!;
|
2021-01-14 12:09:53 +00:00
|
|
|
outer.items.push(inner);
|
|
|
|
if (inner.start.text === m) {
|
|
|
|
return 'continue';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return 'eof';
|
|
|
|
}
|
|
|
|
|
2021-01-15 12:38:15 +00:00
|
|
|
peek(): Token {
|
|
|
|
return this.scanner.peek() ?? this.scanner.makeToken(this.scanner.mark(), TokenType.CLOSE, '');
|
|
|
|
}
|
|
|
|
|
|
|
|
drop() {
|
|
|
|
this.scanner.drop();
|
2021-01-14 12:09:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
read(): Item | null {
|
|
|
|
while (true) {
|
|
|
|
let g = this.stackTop();
|
2021-01-15 12:38:15 +00:00
|
|
|
const t = this.peek();
|
2021-01-14 12:09:53 +00:00
|
|
|
switch (t.type) {
|
|
|
|
case TokenType.SPACE:
|
|
|
|
case TokenType.NEWLINE:
|
|
|
|
case TokenType.ATOM:
|
|
|
|
case TokenType.STRING:
|
2021-01-15 12:38:15 +00:00
|
|
|
if (g === null) {
|
|
|
|
this.drop();
|
|
|
|
return t;
|
|
|
|
}
|
2021-01-14 12:09:53 +00:00
|
|
|
if (t.text === ';') {
|
|
|
|
while ('(['.indexOf(g.start.text) >= 0) {
|
|
|
|
this.stack.pop();
|
2021-01-15 12:38:15 +00:00
|
|
|
const outer = this.stackTop();
|
|
|
|
if (outer === null) {
|
|
|
|
// do not drop the semicolon here
|
|
|
|
return g;
|
|
|
|
}
|
|
|
|
outer.items.push(g);
|
|
|
|
g = outer;
|
2021-01-14 12:09:53 +00:00
|
|
|
}
|
|
|
|
}
|
2021-01-15 12:38:15 +00:00
|
|
|
this.drop();
|
2021-01-14 12:09:53 +00:00
|
|
|
g.items.push(t);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case TokenType.OPEN:
|
2021-01-15 12:38:15 +00:00
|
|
|
this.drop();
|
2021-01-14 12:09:53 +00:00
|
|
|
this.stack.push({ start: t, end: null, items: [] });
|
|
|
|
break;
|
|
|
|
|
|
|
|
case TokenType.CLOSE: {
|
2021-01-15 12:38:15 +00:00
|
|
|
this.drop();
|
2021-01-14 12:09:53 +00:00
|
|
|
const i = this.popUntilMatch(t);
|
|
|
|
if (i === 'eof') return null;
|
|
|
|
if (i === 'continue') break;
|
|
|
|
return i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
readToEnd(): Items {
|
|
|
|
return Array.from(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
next(): IteratorResult<Item> {
|
|
|
|
const i = this.read();
|
|
|
|
if (i === null) {
|
|
|
|
return { done: true, value: null };
|
|
|
|
} else {
|
|
|
|
return { done: false, value: i };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|