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
+ }
+ }
+ }
+ }
+ }
+}