Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions docs/parsers.md
Original file line number Diff line number Diff line change
Expand Up @@ -992,6 +992,44 @@ Parser<T> When(Func<ParseContext, T, bool> predicate)

To evaluate a condition before a parser is executed use the `If` parser instead.

### WhenFollowedBy

Ensures that the result of the current parser is followed by another parser without consuming its input. This implements positive lookahead.

```c#
Parser<T> WhenFollowedBy<U>(Parser<U> lookahead)
```

Usage:

```c#
// Parse a number only if it's followed by a colon
var parser = Literals.Integer().WhenFollowedBy(Literals.Char(':'));
parser.Parse("42:"); // success, returns 42
parser.Parse("42"); // failure, lookahead doesn't match
```

The lookahead parser is checked at the current position but doesn't consume input. If the lookahead fails, the entire parser fails and the cursor is reset to the beginning.

### WhenNotFollowedBy

Ensures that the result of the current parser is NOT followed by another parser without consuming its input. This implements negative lookahead.

```c#
Parser<T> WhenNotFollowedBy<U>(Parser<U> lookahead)
```

Usage:

```c#
// Parse a number only if it's NOT followed by a colon
var parser = Literals.Integer().WhenNotFollowedBy(Literals.Char(':'));
parser.Parse("42"); // success, returns 42
parser.Parse("42:"); // failure, lookahead matches
```

The lookahead parser is checked at the current position but doesn't consume input. If the lookahead succeeds, the entire parser fails and the cursor is reset to the beginning.

### If (Deprecated)

NB: This parser can be rewritten using `Select` (and `Fail`) which is more flexible and simpler to understand.
Expand Down
10 changes: 10 additions & 0 deletions src/Parlot/Fluent/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,16 @@ public Parser<T> Named(string name)
/// </summary>
public Parser<T> When(Func<ParseContext, T, bool> predicate) => new When<T>(this, predicate);

/// <summary>
/// Builds a parser that ensures the specified parser matches at the current position without consuming input (positive lookahead).
/// </summary>
public Parser<T> WhenFollowedBy<U>(Parser<U> lookahead) => new WhenFollowedBy<T>(this, lookahead.Then<object>(_ => new object()));

/// <summary>
/// Builds a parser that ensures the specified parser does NOT match at the current position without consuming input (negative lookahead).
/// </summary>
public Parser<T> WhenNotFollowedBy<U>(Parser<U> lookahead) => new WhenNotFollowedBy<T>(this, lookahead.Then<object>(_ => new object()));

/// <summary>
/// Builds a parser what returns another one based on the previous result.
/// </summary>
Expand Down
102 changes: 102 additions & 0 deletions src/Parlot/Fluent/WhenFollowedBy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using Parlot.Compilation;
using Parlot.Rewriting;
using System;
using System.Collections.Generic;
#if NET
using System.Linq;
#endif
using System.Linq.Expressions;

namespace Parlot.Fluent;

/// <summary>
/// Ensure the given parser matches at the current position without consuming input (positive lookahead).
/// </summary>
/// <typeparam name="T">The output parser type.</typeparam>
public sealed class WhenFollowedBy<T> : Parser<T>, ICompilable, ISeekable
{
private readonly Parser<T> _parser;
private readonly Parser<object> _lookahead;

public WhenFollowedBy(Parser<T> parser, Parser<object> lookahead)
{
_parser = parser ?? throw new ArgumentNullException(nameof(parser));
_lookahead = lookahead ?? throw new ArgumentNullException(nameof(lookahead));

// Forward ISeekable properties from the main parser
if (_parser is ISeekable seekable)
{
CanSeek = seekable.CanSeek;
ExpectedChars = seekable.ExpectedChars;
SkipWhitespace = seekable.SkipWhitespace;
}
}

public bool CanSeek { get; }

public char[] ExpectedChars { get; } = [];

public bool SkipWhitespace { get; }

public override bool Parse(ParseContext context, ref ParseResult<T> result)
{
context.EnterParser(this);

var start = context.Scanner.Cursor.Position;

// First, parse with the main parser
var mainSuccess = _parser.Parse(context, ref result);

if (!mainSuccess)
{
context.ExitParser(this);
return false;
}

// Save position before lookahead check
var beforeLookahead = context.Scanner.Cursor.Position;

// Now check if the lookahead parser matches at the current position
var lookaheadResult = new ParseResult<object>();
var lookaheadSuccess = _lookahead.Parse(context, ref lookaheadResult);

// Reset position to before the lookahead (it shouldn't consume input)
context.Scanner.Cursor.ResetPosition(beforeLookahead);

// If lookahead failed, fail this parser and reset to start
if (!lookaheadSuccess)
{
context.Scanner.Cursor.ResetPosition(start);
context.ExitParser(this);
return false;
}

context.ExitParser(this);
return true;
}

public CompilationResult Compile(CompilationContext context)
{
var result = context.CreateCompilationResult<T>();

var mainParserCompileResult = _parser.Build(context, requireResult: true);

// For now, don't attempt to compile the lookahead check. Just compile the main parser.
// Compilation support for lookahead can be added later if needed.
// This ensures the parser still benefits from compilation of the main parser.

var parserResult = context.CreateCompilationResult<T>();

// Just add the compiled main parser
foreach (var variable in mainParserCompileResult.Variables)
{
parserResult.Variables.Add(variable);
}

parserResult.Body.AddRange(mainParserCompileResult.Body);

return parserResult;
}

public override string ToString() => $"{_parser} (WhenFollowedBy {_lookahead})";
}
102 changes: 102 additions & 0 deletions src/Parlot/Fluent/WhenNotFollowedBy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using Parlot.Compilation;
using Parlot.Rewriting;
using System;
using System.Collections.Generic;
#if NET
using System.Linq;
#endif
using System.Linq.Expressions;

namespace Parlot.Fluent;

/// <summary>
/// Ensure the given parser does NOT match at the current position without consuming input (negative lookahead).
/// </summary>
/// <typeparam name="T">The output parser type.</typeparam>
public sealed class WhenNotFollowedBy<T> : Parser<T>, ICompilable, ISeekable
{
private readonly Parser<T> _parser;
private readonly Parser<object> _lookahead;

public WhenNotFollowedBy(Parser<T> parser, Parser<object> lookahead)
{
_parser = parser ?? throw new ArgumentNullException(nameof(parser));
_lookahead = lookahead ?? throw new ArgumentNullException(nameof(lookahead));

// Forward ISeekable properties from the main parser
if (_parser is ISeekable seekable)
{
CanSeek = seekable.CanSeek;
ExpectedChars = seekable.ExpectedChars;
SkipWhitespace = seekable.SkipWhitespace;
}
}

public bool CanSeek { get; }

public char[] ExpectedChars { get; } = [];

public bool SkipWhitespace { get; }

public override bool Parse(ParseContext context, ref ParseResult<T> result)
{
context.EnterParser(this);

var start = context.Scanner.Cursor.Position;

// First, parse with the main parser
var mainSuccess = _parser.Parse(context, ref result);

if (!mainSuccess)
{
context.ExitParser(this);
return false;
}

// Save position before lookahead check
var beforeLookahead = context.Scanner.Cursor.Position;

// Now check if the lookahead parser matches at the current position
var lookaheadResult = new ParseResult<object>();
var lookaheadSuccess = _lookahead.Parse(context, ref lookaheadResult);

// Reset position to before the lookahead (it shouldn't consume input)
context.Scanner.Cursor.ResetPosition(beforeLookahead);

// If lookahead succeeded, fail this parser and reset to start
if (lookaheadSuccess)
{
context.Scanner.Cursor.ResetPosition(start);
context.ExitParser(this);
return false;
}

context.ExitParser(this);
return true;
}

public CompilationResult Compile(CompilationContext context)
{
var result = context.CreateCompilationResult<T>();

var mainParserCompileResult = _parser.Build(context, requireResult: true);

// For now, don't attempt to compile the lookahead check. Just compile the main parser.
// Compilation support for lookahead can be added later if needed.
// This ensures the parser still benefits from compilation of the main parser.

var parserResult = context.CreateCompilationResult<T>();

// Just add the compiled main parser
foreach (var variable in mainParserCompileResult.Variables)
{
parserResult.Variables.Add(variable);
}

parserResult.Body.AddRange(mainParserCompileResult.Body);

return parserResult;
}

public override string ToString() => $"{_parser} (WhenNotFollowedBy {_lookahead})";
}
54 changes: 54 additions & 0 deletions test/Parlot.Tests/FluentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,60 @@ public void WhenShouldResetPositionWhenFalse()
Assert.Equal(1235, result1.Item2);
}

[Fact]
public void WhenFollowedByShouldSucceedWhenLookaheadMatches()
{
var parser = Literals.Integer().WhenFollowedBy(Terms.Text("abc"));

Assert.True(parser.TryParse("123abc", out var result1));
Assert.Equal(123, result1);
}

[Fact]
public void WhenFollowedByShouldFailWhenLookaheadDoesNotMatch()
{
var parser = Literals.Integer().WhenFollowedBy(Terms.Text("abc"));

Assert.False(parser.TryParse("123xyz", out var result1));
Assert.Equal(default, result1);
}

[Fact]
public void WhenFollowedByShouldNotConsumeInput()
{
var parser = Literals.Integer().WhenFollowedBy(Literals.Char('x')).And(Literals.Char('x'));

Assert.True(parser.TryParse("123x", out var result1));
Assert.Equal((123, 'x'), result1);
}

[Fact]
public void WhenNotFollowedByShouldSucceedWhenLookaheadDoesNotMatch()
{
var parser = Literals.Integer().WhenNotFollowedBy(Terms.Text("abc"));

Assert.True(parser.TryParse("123xyz", out var result1));
Assert.Equal(123, result1);
}

[Fact]
public void WhenNotFollowedByShouldFailWhenLookaheadMatches()
{
var parser = Literals.Integer().WhenNotFollowedBy(Terms.Text("abc"));

Assert.False(parser.TryParse("123abc", out var result1));
Assert.Equal(default, result1);
}

[Fact]
public void WhenNotFollowedByShouldNotConsumeInput()
{
var parser = Literals.Integer().WhenNotFollowedBy(Literals.Char('x')).And(Literals.Char('y'));

Assert.True(parser.TryParse("123y", out var result1));
Assert.Equal((123, 'y'), result1);
}

[Fact]
public void ShouldCast()
{
Expand Down