From 88b13b11d54559d913a242bdc6f120f07db4f0a7 Mon Sep 17 00:00:00 2001 From: Vladislav Mamon Date: Tue, 17 Jan 2023 01:36:27 +0300 Subject: [PATCH] feat: add spans - Added spans for all parsers and combinators that explicitly return results. - Made some minor improvements here and there. --- src/__tests__/combinators/attempt.spec.ts | 3 +++ src/__tests__/combinators/lookahead.spec.ts | 3 +++ src/__tests__/parsers/tryRun.spec.ts | 2 +- src/combinators/attempt.ts | 4 +++- src/combinators/error.ts | 3 ++- src/combinators/lookahead.ts | 1 + src/combinators/many.ts | 6 ++++-- src/combinators/map.ts | 9 ++++++--- src/combinators/sepBy.ts | 14 +++++++++++--- src/combinators/sequence.ts | 3 ++- src/combinators/until.ts | 4 +++- src/parsers/any.ts | 4 +++- src/parsers/defer.ts | 3 ++- src/parsers/eof.ts | 8 ++++---- src/parsers/noneOf.ts | 7 +++++-- src/parsers/nothing.ts | 3 ++- src/parsers/numbers.ts | 8 +++++++- src/parsers/oneOf.ts | 5 ++++- src/parsers/regexp.ts | 5 ++++- src/parsers/rest.ts | 5 +++-- src/parsers/string.ts | 8 +++++++- src/parsers/tryRun.ts | 6 +++++- src/types.ts | 17 +++++++++++++++-- 23 files changed, 100 insertions(+), 31 deletions(-) diff --git a/src/__tests__/combinators/attempt.spec.ts b/src/__tests__/combinators/attempt.spec.ts index b160111..5bd99cb 100644 --- a/src/__tests__/combinators/attempt.spec.ts +++ b/src/__tests__/combinators/attempt.spec.ts @@ -14,6 +14,7 @@ describe('attempt', () => { should.beStrictEqual(actual, { isOk: true, + span: [0, 13], pos: 13, value: ['hello', 'let', 'lettuce'] }) @@ -24,6 +25,7 @@ describe('attempt', () => { should.beStrictEqual(actual, { isOk: false, + span: [6, 9], pos: 9, expected: 'lettuce' }) @@ -34,6 +36,7 @@ describe('attempt', () => { should.beStrictEqual(actual, { isOk: false, + span: [6, 6], pos: 6, expected: 'let' }) diff --git a/src/__tests__/combinators/lookahead.spec.ts b/src/__tests__/combinators/lookahead.spec.ts index b676e31..09322c5 100644 --- a/src/__tests__/combinators/lookahead.spec.ts +++ b/src/__tests__/combinators/lookahead.spec.ts @@ -14,6 +14,7 @@ describe('lookahead', () => { should.beStrictEqual(actual, { isOk: true, + span: [0, 13], pos: 13, value: ['hello', 'let', 'lettuce'] }) @@ -24,6 +25,7 @@ describe('lookahead', () => { should.beStrictEqual(actual, { isOk: false, + span: [6, 9], pos: 9, expected: 'lettuce' }) @@ -34,6 +36,7 @@ describe('lookahead', () => { should.beStrictEqual(actual, { isOk: false, + span: [6, 9], pos: 9, expected: 'let' }) diff --git a/src/__tests__/parsers/tryRun.spec.ts b/src/__tests__/parsers/tryRun.spec.ts index c702e9a..3f2ed8d 100644 --- a/src/__tests__/parsers/tryRun.spec.ts +++ b/src/__tests__/parsers/tryRun.spec.ts @@ -16,7 +16,7 @@ describe('tryRun', () => { deferred.with(string('deferred')) const actual = () => tryRun(deferred).with('lazy') - const expected = new ParserError({ pos: 8, expected: 'deferred' }) + const expected = new ParserError({ pos: 8, span: [0, 4], expected: 'deferred' }) should.throwError(actual, expected) }) diff --git a/src/combinators/attempt.ts b/src/combinators/attempt.ts index 935b53e..3b3004a 100644 --- a/src/combinators/attempt.ts +++ b/src/combinators/attempt.ts @@ -1,4 +1,4 @@ -import type { Parser } from '@types' +import type { Parser, Span } from '@types' /** * Applies `parser` without consuming any input. It doesn't care if `parser` succeeds or fails, it @@ -18,6 +18,7 @@ export function attempt(parser: Parser): Parser { case true: { return { isOk: true, + span: [pos, pos] as Span, pos, value: result.value } @@ -27,6 +28,7 @@ export function attempt(parser: Parser): Parser { case false: { return { isOk: false, + span: [pos, pos] as Span, pos, expected: result.expected } diff --git a/src/combinators/error.ts b/src/combinators/error.ts index 917c9e6..8dd6441 100644 --- a/src/combinators/error.ts +++ b/src/combinators/error.ts @@ -1,4 +1,4 @@ -import type { Parser } from '@types' +import type { Parser, Span } from '@types' /** * Replaces `parser`'s error message with `expected`. @@ -21,6 +21,7 @@ export function error(parser: Parser, expected: string): Parser { case false: { return { isOk: false, + span: [pos, result.pos] as Span, pos, expected } diff --git a/src/combinators/lookahead.ts b/src/combinators/lookahead.ts index 1653462..b485065 100644 --- a/src/combinators/lookahead.ts +++ b/src/combinators/lookahead.ts @@ -18,6 +18,7 @@ export function lookahead(parser: Parser): Parser { case true: { return { isOk: true, + span: result.span, pos, value: result.value } diff --git a/src/combinators/many.ts b/src/combinators/many.ts index e04eebd..61d8fa5 100644 --- a/src/combinators/many.ts +++ b/src/combinators/many.ts @@ -1,4 +1,4 @@ -import type { Parser, SafeParser } from '@types' +import type { Parser, SucceedingParser, Span } from '@types' /** * Applies `parser` *zero* or more times, collecting its results. Never fails. @@ -7,7 +7,7 @@ import type { Parser, SafeParser } from '@types' * * @returns Array of the returned values of `parser` */ -export function many(parser: Parser): SafeParser> { +export function many(parser: Parser): SucceedingParser> { return { parse(input, pos) { const values: Array = [] @@ -26,6 +26,7 @@ export function many(parser: Parser): SafeParser> { return { isOk: true, + span: [pos, nextPos] as Span, pos: nextPos, value: values } @@ -65,6 +66,7 @@ export function many1(parser: Parser): Parser> { return { isOk: true, + span: [pos, nextPos] as Span, pos: nextPos, value: values } diff --git a/src/combinators/map.ts b/src/combinators/map.ts index 3caf26b..795bc58 100644 --- a/src/combinators/map.ts +++ b/src/combinators/map.ts @@ -1,4 +1,4 @@ -import type { Parser } from '@types' +import type { Parser, Span } from '@types' /** * Applies `fn` to the `parser`'s result. @@ -8,17 +8,20 @@ import type { Parser } from '@types' * * @returns Result of `fn` */ -export function map(parser: Parser, fn: (value: T) => R): Parser { +export function map(parser: Parser, fn: (value: T, span: Span) => R): Parser { return { parse(input, pos) { const result = parser.parse(input, pos) switch (result.isOk) { case true: { + const span = [pos, result.pos] as Span + return { isOk: true, + span, pos: result.pos, - value: fn(result.value) + value: fn(result.value, span) } } diff --git a/src/combinators/sepBy.ts b/src/combinators/sepBy.ts index db91097..efdef30 100644 --- a/src/combinators/sepBy.ts +++ b/src/combinators/sepBy.ts @@ -1,7 +1,7 @@ import { many } from './many' import { sequence } from './sequence' -import type { Parser } from '@types' +import type { Parser, Span } from '@types' /** * Parses *zero* or more occurrences of `parser`, separated by `sep`. Never fails. @@ -23,10 +23,13 @@ export function sepBy(parser: Parser, sep: Parser): Parser> const values = [resultP.value] // If the parsers succeed, concatenate the values sans the separator. - resultS.value.forEach(([, value]) => values.push(value)) + for (const [, value] of resultS.value) { + values.push(value) + } return { isOk: true, + span: [pos, resultS.pos] as Span, pos: resultS.pos, value: values } @@ -34,6 +37,7 @@ export function sepBy(parser: Parser, sep: Parser): Parser> return { isOk: true, + span: [pos, resultP.pos] as Span, pos: resultP.pos, value: [] } @@ -61,10 +65,13 @@ export function sepBy1(parser: Parser, sep: Parser): Parser const values = [resultP.value] // If the parsers succeed, concatenate the values sans the separator. - resultS.value.forEach(([, value]) => values.push(value)) + for (const [, value] of resultS.value) { + values.push(value) + } return { isOk: true, + span: [pos, resultS.pos] as Span, pos: resultS.pos, value: values } @@ -72,6 +79,7 @@ export function sepBy1(parser: Parser, sep: Parser): Parser return { isOk: false, + span: [pos, resultP.pos] as Span, pos: resultP.pos, expected: resultP.expected } diff --git a/src/combinators/sequence.ts b/src/combinators/sequence.ts index d145c63..a103eb1 100644 --- a/src/combinators/sequence.ts +++ b/src/combinators/sequence.ts @@ -1,4 +1,4 @@ -import type { Parser, ToTuple } from '@types' +import type { Parser, Span, ToTuple } from '@types' /** * Applies `ps` parsers in order, until *all* of them succeed. @@ -32,6 +32,7 @@ export function sequence(...ps: Array>): Parser> { return { isOk: true, + span: [pos, nextPos] as Span, pos: nextPos, value: values } diff --git a/src/combinators/until.ts b/src/combinators/until.ts index 62ab5f0..befde0b 100644 --- a/src/combinators/until.ts +++ b/src/combinators/until.ts @@ -1,4 +1,4 @@ -import type { Parser } from '@types' +import type { Parser, Span } from '@types' /** * Applies source `parser`, collects its output, and stops after `terminator` parser succeeds. @@ -23,6 +23,7 @@ export function takeUntil(parser: Parser, terminator: Parser): Parse case true: { return { isOk: true, + span: [pos, resultT.pos] as Span, pos: resultT.pos, value: [values, resultT.value] } @@ -68,6 +69,7 @@ export function skipUntil(parser: Parser, terminator: Parser): Parse case true: { return { isOk: true, + span: [pos, resultT.pos] as Span, pos: resultT.pos, value: resultT.value } diff --git a/src/parsers/any.ts b/src/parsers/any.ts index fce3911..95992dc 100644 --- a/src/parsers/any.ts +++ b/src/parsers/any.ts @@ -1,4 +1,4 @@ -import type { Parser } from '@types' +import type { Parser, Span } from '@types' /** * Parses any single character from the input and returns it. Fails at the end of input. @@ -11,6 +11,7 @@ export function any(): Parser { if (input.length === pos) { return { isOk: false, + span: [pos, pos] as Span, pos, expected: 'any @ reached the end of input' } @@ -21,6 +22,7 @@ export function any(): Parser { return { isOk: true, + span: [pos, nextPos] as Span, pos: nextPos, value } diff --git a/src/parsers/defer.ts b/src/parsers/defer.ts index 439493a..66a680a 100644 --- a/src/parsers/defer.ts +++ b/src/parsers/defer.ts @@ -1,4 +1,4 @@ -import type { Parser } from '@types' +import type { Parser, Span } from '@types' /** * Intersection type to add a method for deferred parser definition. @@ -71,6 +71,7 @@ export function defer(): Deferred { return { isOk: false, + span: [pos, pos] as Span, pos, expected: `Deferred parser wasn't initialized.` } diff --git a/src/parsers/eof.ts b/src/parsers/eof.ts index 128ce9a..536b01c 100644 --- a/src/parsers/eof.ts +++ b/src/parsers/eof.ts @@ -1,4 +1,4 @@ -import type { Parser } from '@types' +import type { Parser, Span } from '@types' /** * Only succeeds at the end of the input. @@ -8,12 +8,11 @@ import type { Parser } from '@types' export function eof(): Parser { return { parse(input, pos) { - const isEof = pos === input.length - - switch (isEof) { + switch (pos === input.length) { case true: { return { isOk: true, + span: [pos, pos] as Span, pos: input.length, value: null } @@ -22,6 +21,7 @@ export function eof(): Parser { case false: { return { isOk: false, + span: [pos, pos] as Span, pos, expected: 'end of input' } diff --git a/src/parsers/noneOf.ts b/src/parsers/noneOf.ts index 431181a..543afc6 100644 --- a/src/parsers/noneOf.ts +++ b/src/parsers/noneOf.ts @@ -1,4 +1,4 @@ -import type { Parser } from '@types' +import type { Parser, Span } from '@types' /** * Ensures that none of the characters in the given string matches the current character. @@ -15,6 +15,7 @@ export function noneOf(chars: string): Parser { if (input.length === pos) { return { isOk: false, + span: [pos, pos] as Span, pos, expected: 'noneOf @ reached the end of input' } @@ -26,6 +27,7 @@ export function noneOf(chars: string): Parser { if (!charset.includes(char)) { return { isOk: true, + span: [pos, nextPos] as Span, pos: nextPos, value: char } @@ -33,7 +35,8 @@ export function noneOf(chars: string): Parser { return { isOk: false, - pos, + span: [pos, pos] as Span, + pos: nextPos, expected: `none of: ${charset.join(', ')}` } } diff --git a/src/parsers/nothing.ts b/src/parsers/nothing.ts index 20e80f3..2562a27 100644 --- a/src/parsers/nothing.ts +++ b/src/parsers/nothing.ts @@ -1,4 +1,4 @@ -import type { Parser } from '@types' +import type { Parser, Span } from '@types' /** * Simply resolves to `null`. @@ -10,6 +10,7 @@ export function nothing(): Parser { parse(_, pos) { return { isOk: true, + span: [pos, pos] as Span, pos, value: null } diff --git a/src/parsers/numbers.ts b/src/parsers/numbers.ts index a8ee0b0..625a768 100644 --- a/src/parsers/numbers.ts +++ b/src/parsers/numbers.ts @@ -1,6 +1,6 @@ import { regexp } from './regexp' -import type { Parser } from '@types' +import type { Parser, Span } from '@types' const HEXADECIMAL_RE = /0[xX][0-9a-fA-F]+/g const BINARY_RE = /0[bB][01]+/g @@ -23,6 +23,7 @@ export function hex(): Parser { case true: { return { isOk: true, + span: [pos, result.pos] as Span, pos: result.pos, value: parseInt(result.value.slice(2), 16) } @@ -50,6 +51,7 @@ export function binary(): Parser { case true: { return { isOk: true, + span: [pos, result.pos] as Span, pos: result.pos, value: parseInt(result.value.slice(2), 2) } @@ -77,6 +79,7 @@ export function octal(): Parser { case true: { return { isOk: true, + span: [pos, result.pos] as Span, pos: result.pos, value: parseInt(result.value.slice(2), 8) } @@ -104,6 +107,7 @@ export function whole(): Parser { case true: { return { isOk: true, + span: [pos, result.pos] as Span, pos: result.pos, value: parseInt(result.value, 10) } @@ -131,6 +135,7 @@ export function integer(): Parser { case true: { return { isOk: true, + span: [pos, result.pos] as Span, pos: result.pos, value: parseInt(result.value, 10) } @@ -160,6 +165,7 @@ export function float(): Parser { case true: { return { isOk: true, + span: [pos, result.pos] as Span, pos: result.pos, value: parseFloat(result.value) } diff --git a/src/parsers/oneOf.ts b/src/parsers/oneOf.ts index 477bc30..0c4c490 100644 --- a/src/parsers/oneOf.ts +++ b/src/parsers/oneOf.ts @@ -1,4 +1,4 @@ -import type { Parser } from '@types' +import type { Parser, Span } from '@types' /** * Ensures that one of the characters in the given string matches the current character. @@ -15,6 +15,7 @@ export function oneOf(chars: string): Parser { if (input.length === pos) { return { isOk: false, + span: [pos, pos] as Span, pos, expected: 'oneOf @ reached the end of input' } @@ -26,6 +27,7 @@ export function oneOf(chars: string): Parser { if (charset.includes(char)) { return { isOk: true, + span: [pos, nextPos] as Span, pos: nextPos, value: char } @@ -33,6 +35,7 @@ export function oneOf(chars: string): Parser { return { isOk: false, + span: [pos, pos] as Span, pos, expected: `one of: ${charset.join(', ')}` } diff --git a/src/parsers/regexp.ts b/src/parsers/regexp.ts index 46e4507..7cc8a60 100644 --- a/src/parsers/regexp.ts +++ b/src/parsers/regexp.ts @@ -1,4 +1,4 @@ -import type { Parser } from '@types' +import type { Parser, Span } from '@types' /** * Parses a string that matches a provided `re` regular expression. Returns the matched string, or @@ -34,12 +34,15 @@ export function regexp(rs: RegExp, expected: string): Parser { return { isOk: true, + span: [pos, index] as Span, pos: index, value: match } } else { return { isOk: false, + // TODO: Can this be improved? Zero-length span for this parser doesn't look helpful. + span: [pos, pos] as Span, pos, expected } diff --git a/src/parsers/rest.ts b/src/parsers/rest.ts index dc5ed16..8c52c87 100644 --- a/src/parsers/rest.ts +++ b/src/parsers/rest.ts @@ -1,15 +1,16 @@ -import type { Parser } from '@types' +import type { Span, SucceedingParser } from '@types' /** * Simply returns the unparsed input as a string. Never fails. * * @returns Rest of the input as a string */ -export function rest(): Parser { +export function rest(): SucceedingParser { return { parse(input, pos) { return { isOk: true, + span: [pos, input.length] as Span, pos: input.length, value: input.substring(pos) } diff --git a/src/parsers/string.ts b/src/parsers/string.ts index 813a4c1..bcf3818 100644 --- a/src/parsers/string.ts +++ b/src/parsers/string.ts @@ -1,4 +1,4 @@ -import type { Parser } from '@types' +import type { Parser, Span } from '@types' import { size } from '@utils/unicode' /** @@ -13,11 +13,13 @@ export function string(match: string): Parser { parse(input, pos) { const nextPos = Math.min(pos + match.length, input.length) const slice = input.substring(pos, nextPos) + const span = [pos, nextPos] as Span switch (slice === match) { case true: { return { isOk: true, + span, pos: nextPos, value: match } @@ -26,6 +28,7 @@ export function string(match: string): Parser { case false: { return { isOk: false, + span, pos: nextPos, expected: match } @@ -47,11 +50,13 @@ export function ustring(match: string): Parser { parse(input, pos) { const nextPos = Math.min(pos + size(match), input.length) const slice = input.substring(pos, nextPos) + const span = [pos, nextPos] as Span switch (slice === match) { case true: { return { isOk: true, + span, pos: nextPos, value: match } @@ -60,6 +65,7 @@ export function ustring(match: string): Parser { case false: { return { isOk: false, + span, pos: nextPos, expected: match } diff --git a/src/parsers/tryRun.ts b/src/parsers/tryRun.ts index b92c08b..65e9044 100644 --- a/src/parsers/tryRun.ts +++ b/src/parsers/tryRun.ts @@ -1,4 +1,4 @@ -import type { Failure, Parser, Success } from '@types' +import type { Failure, Parser, Span, Success } from '@types' /** @internal */ interface Runnable { @@ -10,10 +10,14 @@ type ErrorResult = Omit export class ParserError extends Error { readonly name = 'ParserError' + + readonly span: Span readonly pos: number constructor(res: ErrorResult) { super(res.expected) + + this.span = res.span this.pos = res.pos } } diff --git a/src/types.ts b/src/types.ts index d60d5ec..db3f04e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,19 +1,28 @@ +/** Represents some range in the source input we are parsing or parsed. */ +export type Span = [start: number, end: number] + /** Parsers of this type always succeed, e.g. `many` and `sepBy`. */ -export interface SafeParser { +export interface SucceedingParser { parse(input: string, pos: number): Success } +/** Parsers of this type always fail. */ +export interface FailingParser { + parse(input: string, pos: number): Failure +} + /** Parsers of this type may fail. */ export interface UnsafeParser { parse(input: string, pos: number): Result } /** Parser interface that all parsers and combinators consume and resolve to. */ -export type Parser = SafeParser | UnsafeParser +export type Parser = FailingParser | SucceedingParser | UnsafeParser /** Represents failed execution. */ export type Failure = { readonly isOk: false + readonly span: Span readonly pos: number readonly expected: string } @@ -21,6 +30,7 @@ export type Failure = { /** Represents successful execution. */ export type Success = { readonly isOk: true + readonly span: Span readonly pos: number readonly value: T } @@ -52,6 +62,9 @@ export type ToTuple = T extends [Parser, ...infer Tail] * ```ts * type U = [Parser, Parser, Parser] * type R = ToUnion // type R = string | number | boolean + * + * type U = Array> + * type R = ToUnion // type R = number * ``` */ export type ToUnion = T extends Array>