syndicate-js/packages/compiler/src/syntax/reader.ts

202 lines
7.1 KiB
TypeScript

/// SPDX-License-Identifier: GPL-3.0-or-later
/// SPDX-FileCopyrightText: Copyright © 2016-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
import { TokenType, Token, Group, GroupInProgress, Item, Items, finishGroup } from './tokens.js';
import { Pos, startPos } from './position.js';
import { Scanner, StringScanner } 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<GroupInProgress> = [];
constructor(scanner: Scanner) {
this.scanner = scanner;
}
[Symbol.iterator](): IterableIterator<Item> {
return this;
}
stackTop(): GroupInProgress | 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.open.text === m)) {
if (this.stack.length > 0) {
this.stackTop()!.items.push(t);
return 'continue';
}
} else {
while (this.stack.length > 0) {
const inner = finishGroup(this.stack.pop()!, t.end);
if (inner.open.text === m) {
inner.close = t;
}
if (this.stack.length === 0) {
return inner;
} else {
const outer = this.stackTop()!;
outer.items.push(inner);
if (inner.open.text === m) {
return 'continue';
}
}
}
}
return 'eof';
}
_eofClose(): Token {
return this.scanner.makeToken(this.scanner.mark(), TokenType.CLOSE, '');
}
peek(): Token {
return this.scanner.peek() ?? this._eofClose();
}
drop() {
this.scanner.drop();
}
inTemplateString(): boolean {
const i = this.stackTop();
return (i !== null) && (i.open.text[0] === '`');
}
expectTemplateVariablePart() {
const variablePartOpen = this.peek();
if (variablePartOpen.type !== TokenType.OPEN || variablePartOpen.text !== '{')
{
throw new Error("Internal parser error: template string variable part mismatch");
}
this.drop();
this.stack.push(this.scanner.makeGroupInProgress(variablePartOpen));
}
read(): Item | null {
while (true) {
if (this.inTemplateString()) {
const t = this.scanner.templateConstantFragment() ?? this._eofClose();
switch (t.type) {
case TokenType.CLOSE: // always means eof, in this case
return this.popUntilMatch(t) as Group; // will always be a Group
case TokenType.STRING:
if (t.text[t.text.length - 1] === '`') {
const inner = finishGroup(this.stack.pop()!, t.end);
inner.close = t;
if (this.stack.length === 0) {
return inner;
} else {
this.stackTop()!.items.push(inner);
break;
}
} else {
// Another unclosed fragment. Expect {...} again.
this.stackTop()!.items.push(t);
this.expectTemplateVariablePart();
break;
}
default:
throw new Error("Internal error: LaxReader.read()");
}
} else {
let g = this.stackTop();
const t = this.peek();
switch (t.type) {
case TokenType.STRING:
if (t.text[0] === '`' && t.text[t.text.length - 1] !== '`') {
// Unclosed template string - so expect {...} followed by more
// fragments. Encode this as a group with opener the first constant
// template fragment and closer the last template fragment.
this.drop();
this.stack.push(this.scanner.makeGroupInProgress(t));
// We know, from the implementation of the scanner, that the current
// character is '{'.
this.expectTemplateVariablePart();
break;
} else {
// fall through
}
case TokenType.SPACE:
case TokenType.NEWLINE:
case TokenType.ATOM:
if (g === null) {
this.drop();
return t;
}
if (t.text === ';') {
while ('(['.indexOf(g.open.text) >= 0) {
this.stack.pop();
const outer = this.stackTop();
if (outer === null) {
// do not drop the semicolon here
return finishGroup(g, t.start);
}
outer.items.push(finishGroup(g, t.start));
g = outer;
}
}
this.drop();
g.items.push(t);
break;
case TokenType.OPEN:
this.drop();
this.stack.push(this.scanner.makeGroupInProgress(t));
break;
case TokenType.CLOSE: {
this.drop();
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 };
}
}
}
export interface LaxReadOptions {
start?: Pos,
name?: string,
extraDelimiters?: string,
synthetic?: boolean,
}
export function laxRead(source: string, options: LaxReadOptions = {}): Items {
const start = options.start ?? startPos(options.name ?? null);
const scanner = new StringScanner(start, source, options.synthetic);
if (options.extraDelimiters) scanner.addDelimiters(options.extraDelimiters);
const reader = new LaxReader(scanner);
return reader.readToEnd();
}