@gobstones/gobstones-parser
Version:
Gobstones parser
194 lines (172 loc) • 5.93 kB
text/typescript
/* eslint-disable no-underscore-dangle */
/* A SourceReader represents the current position in a source file.
* It keeps track of line and column numbers.
* Methods are non-destructive. For example:
*
* let r = new SourceReader('foo.gbs', 'if\n(True)');
*
* r.peek(); // ~~> 'i'
* r = r.consumeCharacter(); // Note: returns a new file reader.
*
* r.peek(); // ~~> 'f'
* r = r.consumeCharacter();
*
* r.peek(); // ~~> '\n'
* r = r.consumeCharacter('\n');
*
* r.line(); // ~~> 2
*/
export class SourceReader {
private _filename: string;
private _string: string;
private _index: number;
private _line: number;
private _column: number;
private _regions: string[];
public constructor(filename: string, string: string) {
this._filename = filename; // Filename
this._string = string; // Source of the current file
this._index = 0; // Index in the current file
this._line = 1; // Line in the current file
this._column = 1; // Column in the current file
this._regions = []; // Lexical (static) stack of regions
}
public _clone(): SourceReader {
const r = new SourceReader(this._filename, this._string);
r._index = this._index;
r._line = this._line;
r._column = this._column;
r._regions = this._regions;
return r;
}
public get filename(): string {
return this._filename;
}
public get line(): number {
return this._line;
}
public get column(): number {
return this._column;
}
public get region(): string {
if (this._regions.length > 0) {
return this._regions[0];
} else {
return '';
}
}
/* Consume one character */
public consumeCharacter(): SourceReader {
const r = this._clone();
if (r.peek() === '\n') {
r._line++;
r._column = 1;
} else {
r._column++;
}
r._index++;
return r;
}
/* Consume characters from the input, one per each character in the string
* (the contents of the string are ignored). */
public consumeString(string: string): SourceReader {
let r = this._clone();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const _ of string) {
r = r.consumeCharacter();
}
return r;
}
/* Returns the SourceReader after consuming an 'invisible' character.
* Invisible characters affect the index but not the line or column.
*/
public consumeInvisibleCharacter(): SourceReader {
const r = this._clone();
r._index++;
return r;
}
/* Consume 'invisible' characters from the input, one per each character
* in the string */
public consumeInvisibleString(string: string): SourceReader {
// eslint-disable-next-line @typescript-eslint/no-this-alias
let r: SourceReader = this;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const _ of string) {
r = r.consumeInvisibleCharacter();
}
return r;
}
/* Return true if the substring occurs at the current point. */
public startsWith(sub: string | any[]): boolean {
const i = this._index;
const j = this._index + sub.length;
return j <= this._string.length && this._string.substring(i, j) === sub;
}
/* Return true if we have reached the end of the current file */
public eof(): boolean {
return this._index >= this._string.length;
}
/* Return the current character, assuming we have not reached EOF */
public peek(): string {
return this._string[this._index];
}
/* Push a region to the stack of regions (non-destructively) */
public beginRegion(region: string): SourceReader {
const r = this._clone();
r._regions = [region].concat(r._regions);
return r;
}
/* Pop a region from the stack of regions (non-destructively) */
public endRegion(): SourceReader {
const r = this._clone();
if (r._regions.length > 0) {
r._regions = r._regions.slice(1);
}
return r;
}
}
/* Return a source reader that represents an unknown position */
export const UnknownPosition = new SourceReader('(?)', '');
export type Input = string | Record<string, string> | string[];
/* An instance of MultifileReader represents a scanner for reading
* source code from a list of files.
*/
export class MultifileReader {
private _filenames: string[];
private _input: Input;
private _index: number;
/* The 'input' parameter should be either:
* (1) a string. e.g. 'program {}', or
* (2) a map from filenames to strings, e.g.
* {
* 'foo.gbs': 'program { P() }',
* 'bar.gbs': 'procedure P() {}',
* }
*/
public constructor(input: Input) {
if (typeof input === 'string') {
input = { '(?)': input };
}
this._filenames = Object.keys(input);
this._filenames.sort();
this._input = input;
this._index = 0;
}
/* Return true if there are more files */
public moreFiles(): boolean {
return this._index + 1 < this._filenames.length;
}
/* Advance to the next file */
public nextFile(): void {
this._index++;
}
/* Return a SourceReader for the current files */
public readCurrentFile(): SourceReader {
if (this._index < this._filenames.length) {
const filename = this._filenames[this._index];
return new SourceReader(filename, this._input[filename]);
} else {
return new SourceReader('(?)', '');
}
}
}