From 130cfe8358bd5244ca85c9bda694bd271c087098 Mon Sep 17 00:00:00 2001 From: Vladislav Mamon Date: Mon, 16 Jan 2023 20:40:10 +0300 Subject: [PATCH] feat(combinators/attempt): add `attempt` combinator --- docs/content/.vitepress/config.ts | 1 + docs/content/combinators/attempt.md | 69 +++++++++++++++++++++++ src/__tests__/@helpers/index.ts | 1 + src/__tests__/combinators/attempt.spec.ts | 41 ++++++++++++++ src/combinators.ts | 1 + src/combinators/attempt.ts | 37 ++++++++++++ 6 files changed, 150 insertions(+) create mode 100644 docs/content/combinators/attempt.md create mode 100644 src/__tests__/combinators/attempt.spec.ts create mode 100644 src/combinators/attempt.ts diff --git a/docs/content/.vitepress/config.ts b/docs/content/.vitepress/config.ts index 069ad7d..1f96d4f 100644 --- a/docs/content/.vitepress/config.ts +++ b/docs/content/.vitepress/config.ts @@ -140,6 +140,7 @@ function getSidebar() { Sidebar.item('Primitives and composites', '/primitives-and-composites') ]), Sidebar.group('Combinators', '/combinators', [ + Sidebar.item('attempt', '/attempt'), Sidebar.item('chainl', '/chainl'), Sidebar.item('choice', '/choice'), Sidebar.item('error', '/error'), diff --git a/docs/content/combinators/attempt.md b/docs/content/combinators/attempt.md new file mode 100644 index 0000000..bfd4f75 --- /dev/null +++ b/docs/content/combinators/attempt.md @@ -0,0 +1,69 @@ +--- +title: 'attempt' +kind: 'primitive' +description: "attempt combinator applies parser without consuming any input. It doesn't care if parser succeeds or fails, it won't consume any input." +--- + +# {{ $frontmatter.title }} + +## Signature + +```ts +function attempt(parser: Parser): Parser +``` + +## Description + +`attempt` combinator applies `parser` without consuming any input. It doesn't care if `parser` succeeds or fails, it won't consume any input. + +## Usage + +The example is the same as in the docs for [`lookahead` combinators][lookahead]. Notice how differs the output for the last failing case: `attempt` doesn't consume any input, i.e. it doesn't advance `pos`. + +```ts +const Parser = sequence( + takeLeft(string('hello'), whitespace()), + lookahead(string('let')), + string('lettuce') +) +``` + +::: tip Success +```ts +run(Parser).with('hello lettuce') + +{ + isOk: true, + pos: 13, + value: [ 'hello', 'let', 'lettuce' ] +} +``` +::: + +::: danger Failure +```ts +run(Parser).with('hello let') + +{ + isOk: false, + pos: 9, // [!code warning] + expected: 'lettuce' +} +``` +::: + +::: danger Failure +```ts +run(Parser).with('hello something') + +{ + isOk: false, + pos: 6, // [!code warning] + expected: 'let' +} +``` +::: + + + +[lookahead]: ./lookahead diff --git a/src/__tests__/@helpers/index.ts b/src/__tests__/@helpers/index.ts index 419b94c..b2607d8 100644 --- a/src/__tests__/@helpers/index.ts +++ b/src/__tests__/@helpers/index.ts @@ -67,6 +67,7 @@ export function testSuccess>(input: string, value: } export const expectedCombinators = [ + 'attempt', 'chainl', 'choice', 'error', diff --git a/src/__tests__/combinators/attempt.spec.ts b/src/__tests__/combinators/attempt.spec.ts new file mode 100644 index 0000000..b160111 --- /dev/null +++ b/src/__tests__/combinators/attempt.spec.ts @@ -0,0 +1,41 @@ +import { attempt, sequence, takeLeft } from '@combinators' +import { string, whitespace } from '@parsers' +import { run, should, describe, it } from '@testing' + +describe('attempt', () => { + const parser = sequence( + takeLeft(string('hello'), whitespace()), + attempt(string('let')), + string('lettuce') + ) + + it('should successfully attempt and return pos untouched', () => { + const actual = run(parser, 'hello lettuce') + + should.beStrictEqual(actual, { + isOk: true, + pos: 13, + value: ['hello', 'let', 'lettuce'] + }) + }) + + it('should correctly fail if placed before a failing parser (OOB check)', () => { + const actual = run(parser, 'hello let') + + should.beStrictEqual(actual, { + isOk: false, + pos: 9, + expected: 'lettuce' + }) + }) + + it('should correctly fail if given a failing parser (non-consuming check)', () => { + const actual = run(parser, 'hello const') + + should.beStrictEqual(actual, { + isOk: false, + pos: 6, + expected: 'let' + }) + }) +}) diff --git a/src/combinators.ts b/src/combinators.ts index bb48453..731c5d8 100644 --- a/src/combinators.ts +++ b/src/combinators.ts @@ -1,3 +1,4 @@ +export * from '@combinators/attempt' export * from '@combinators/chain' export * from '@combinators/choice' export * from '@combinators/error' diff --git a/src/combinators/attempt.ts b/src/combinators/attempt.ts new file mode 100644 index 0000000..935b53e --- /dev/null +++ b/src/combinators/attempt.ts @@ -0,0 +1,37 @@ +import type { Parser } from '@types' + +/** + * Applies `parser` without consuming any input. It doesn't care if `parser` succeeds or fails, it + * won't consume any input. + * + * @param parser - Parser to apply + * + * @returns Result of `parser` + */ +export function attempt(parser: Parser): Parser { + return { + parse(input, pos) { + const result = parser.parse(input, pos) + + switch (result.isOk) { + // If parser succeeded, keep the position untouched. + case true: { + return { + isOk: true, + pos, + value: result.value + } + } + + // If parser failed, keep the position untouched as well. + case false: { + return { + isOk: false, + pos, + expected: result.expected + } + } + } + } + } +}