Skip to content

Commit

Permalink
feat(combinators/attempt): add attempt combinator
Browse files Browse the repository at this point in the history
  • Loading branch information
norskeld committed Jan 16, 2023
1 parent e5207fd commit 7133746
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/content/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
69 changes: 69 additions & 0 deletions docs/content/combinators/attempt.md
Original file line number Diff line number Diff line change
@@ -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 }} <Primitive />

## Signature

```ts
function attempt<T>(parser: Parser<T>): Parser<T>
```

## 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'
}
```
:::

<!-- Links. -->

[lookahead]: ./lookahead
1 change: 1 addition & 0 deletions src/__tests__/@helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export function testSuccess<T, P extends Parser<unknown>>(input: string, value:
}

export const expectedCombinators = [
'attempt',
'chainl',
'choice',
'error',
Expand Down
41 changes: 41 additions & 0 deletions src/__tests__/combinators/attempt.spec.ts
Original file line number Diff line number Diff line change
@@ -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'
})
})
})
1 change: 1 addition & 0 deletions src/combinators.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from '@combinators/attempt'
export * from '@combinators/chain'
export * from '@combinators/choice'
export * from '@combinators/error'
Expand Down
37 changes: 37 additions & 0 deletions src/combinators/attempt.ts
Original file line number Diff line number Diff line change
@@ -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<T>(parser: Parser<T>): Parser<T> {
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
}
}
}
}
}
}

1 comment on commit 7133746

@vercel
Copy link

@vercel vercel bot commented on 7133746 Jan 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.