diff --git a/docs/content/.vitepress/config.ts b/docs/content/.vitepress/config.ts
index cf5b90e..069ad7d 100644
--- a/docs/content/.vitepress/config.ts
+++ b/docs/content/.vitepress/config.ts
@@ -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'),
diff --git a/docs/content/combinators/lookahead.md b/docs/content/combinators/lookahead.md
new file mode 100644
index 0000000..5d884bb
--- /dev/null
+++ b/docs/content/combinators/lookahead.md
@@ -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 }}
+
+## Signature
+
+```ts
+function lookahead(parser: Parser): Parser
+```
+
+## 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'
+}
+```
+:::
diff --git a/src/__tests__/@helpers/index.ts b/src/__tests__/@helpers/index.ts
index a41f551..419b94c 100644
--- a/src/__tests__/@helpers/index.ts
+++ b/src/__tests__/@helpers/index.ts
@@ -39,6 +39,10 @@ export const should = {
expect(a, message).toBe(b)
},
+ beStrictEqual(a: T, b: T, message?: string) {
+ expect(a, message).toStrictEqual(b)
+ },
+
throw(f: () => void) {
expect(f).toThrow()
},
@@ -66,6 +70,7 @@ export const expectedCombinators = [
'chainl',
'choice',
'error',
+ 'lookahead',
'many',
'many1',
'map',
diff --git a/src/__tests__/combinators/lookahead.spec.ts b/src/__tests__/combinators/lookahead.spec.ts
new file mode 100644
index 0000000..b676e31
--- /dev/null
+++ b/src/__tests__/combinators/lookahead.spec.ts
@@ -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'
+ })
+ })
+})
diff --git a/src/combinators.ts b/src/combinators.ts
index a5ec752..bb48453 100644
--- a/src/combinators.ts
+++ b/src/combinators.ts
@@ -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'
diff --git a/src/combinators/lookahead.ts b/src/combinators/lookahead.ts
new file mode 100644
index 0000000..1653462
--- /dev/null
+++ b/src/combinators/lookahead.ts
@@ -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(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 the parser failed, then still advance the pos cursor.
+ case false: {
+ return result
+ }
+ }
+ }
+ }
+}