Skip to content

Commit

Permalink
feat(combinators/lookahead): add lookahead combinator
Browse files Browse the repository at this point in the history
  • Loading branch information
norskeld committed Jan 16, 2023
1 parent 9927128 commit 497c7a8
Show file tree
Hide file tree
Showing 6 changed files with 146 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 @@ -143,6 +143,7 @@ function getSidebar() {
Sidebar.item('chainl', '/chainl'),
Sidebar.item('choice', '/choice'),
Sidebar.item('error', '/error'),
Sidebar.item('lookahead', '/lookahead'),
Sidebar.item('many', '/many'),
Sidebar.item('many1', '/many1'),
Sidebar.item('map', '/map'),
Expand Down
65 changes: 65 additions & 0 deletions docs/content/combinators/lookahead.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
title: 'lookahead'
kind: 'primitive'
description: "lookahead combinator applies parser without consuming any input. If parser fails and consumes some input, so does lookahead."
---

# {{ $frontmatter.title }} <Primitive />

## Signature

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

## Description

`lookahead` combinator applies `parser` without consuming any input. If `parser` fails and consumes some input, so does `lookahead`.

## Usage

The example is rather contrived, but it clearly illustrates how the combinator works, allowing one, for example, collect ambiguous results for further processing.

```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,
expected: 'lettuce'
}
```
:::

::: danger Failure
```ts
run(Parser).with('hello something')
{
isOk: false,
pos: 9,
expected: 'let'
}
```
:::
5 changes: 5 additions & 0 deletions src/__tests__/@helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ export const should = {
expect(a, message).toBe(b)
},

beStrictEqual<T = unknown>(a: T, b: T, message?: string) {
expect(a, message).toStrictEqual(b)
},

throw(f: () => void) {
expect(f).toThrow()
},
Expand Down Expand Up @@ -66,6 +70,7 @@ export const expectedCombinators = [
'chainl',
'choice',
'error',
'lookahead',
'many',
'many1',
'map',
Expand Down
41 changes: 41 additions & 0 deletions src/__tests__/combinators/lookahead.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { lookahead, sequence, takeLeft } from '@combinators'
import { string, whitespace } from '@parsers'
import { run, should, describe, it } from '@testing'

describe('lookahead', () => {
const parser = sequence(
takeLeft(string('hello'), whitespace()),
lookahead(string('let')),
string('lettuce')
)

it('should successfully lookahead 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 (consuming check)', () => {
const actual = run(parser, 'hello const')

should.beStrictEqual(actual, {
isOk: false,
pos: 9,
expected: 'let'
})
})
})
1 change: 1 addition & 0 deletions src/combinators.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from '@combinators/chain'
export * from '@combinators/choice'
export * from '@combinators/error'
export * from '@combinators/lookahead'
export * from '@combinators/many'
export * from '@combinators/map'
export * from '@combinators/optional'
Expand Down
33 changes: 33 additions & 0 deletions src/combinators/lookahead.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { Parser } from '@types'

/**
* Applies `parser` without consuming any input. If `parser` fails and consumes some input, so does
* `lookahead`.
*
* @param parser - Parser to apply
*
* @returns Result of `parser`
*/
export function lookahead<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 the parser failed, then still advance the pos cursor.
case false: {
return result
}
}
}
}
}

1 comment on commit 497c7a8

@vercel
Copy link

@vercel vercel bot commented on 497c7a8 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.