Skip to content

Commit

Permalink
Assign location to tokens (#150)
Browse files Browse the repository at this point in the history
- That allow implement parsers which indirectly span across multiple files, like C/C++
  • Loading branch information
kant2002 authored Jan 1, 2024
1 parent a73351a commit ce461fd
Show file tree
Hide file tree
Showing 13 changed files with 104 additions and 20 deletions.
32 changes: 30 additions & 2 deletions Sources/SynKit/Libraries/C.Syntax/CLexer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,30 @@ public CLexer(ICharStream source)
this.source = source;
}

/// <summary>
/// Initializes a new instance of the <see cref="CLexer"/> class.
/// </summary>
/// <param name="sourceFile">The <see cref="ISourceFile"/> to read the source from.</param>
public CLexer(ISourceFile sourceFile)
: this(new TextReaderCharStream(sourceFile))
{
}

/// <summary>
/// Initializes a new instance of the <see cref="CLexer"/> class.
/// </summary>
/// <param name="sourceFile">The <see cref="ISourceFile"/> to read the source from.</param>
public CLexer(SourceFile sourceFile)
: this(new TextReaderCharStream(sourceFile))
{
}

/// <summary>
/// Initializes a new instance of the <see cref="CLexer"/> class.
/// </summary>
/// <param name="reader">The <see cref="TextReader"/> to read the source from.</param>
public CLexer(TextReader reader)
: this(new TextReaderCharStream(reader))
: this(new TextReaderCharStream(new SourceFile("<no-location>", reader)))
{
}

Expand All @@ -74,7 +92,17 @@ public CLexer(TextReader reader)
/// </summary>
/// <param name="source">The text to read.</param>
public CLexer(string source)
: this(new StringReader(source))
: this((ISourceFile)new SourceFile("<no-location>", source))
{
}

/// <summary>
/// Initializes a new instance of the <see cref="CLexer"/> class.
/// </summary>
/// <param name="path">Path to source location.</param>
/// <param name="source">The text to read.</param>
public CLexer(string path, string source)
: this(new SourceFile(path, source))
{
}

Expand Down
12 changes: 8 additions & 4 deletions Sources/SynKit/Libraries/C.Syntax/CToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ namespace Yoakke.SynKit.C.Syntax;
public sealed class CToken : IToken<CTokenType>, IEquatable<CToken>
{
/// <inheritdoc/>
public Text.Range Range { get; }
public Text.Range Range => this.Location.Range;

/// <inheritdoc/>
public Text.Location Location { get; }

/// <inheritdoc/>
public string Text { get; }
Expand Down Expand Up @@ -46,9 +49,9 @@ public sealed class CToken : IToken<CTokenType>, IEquatable<CToken>
/// <param name="logicalText">The original text the <see cref="CToken"/> was parsed from.</param>
/// <param name="kind">The <see cref="CTokenType"/> of the <see cref="CToken"/>.</param>
/// <param name="expandedFrom">The <see cref="CToken"/> that this one was expanded from.</param>
public CToken(Text.Range range, string text, Text.Range logicalRange, string logicalText, CTokenType kind, CToken? expandedFrom)
public CToken(Text.Location range, string text, Text.Range logicalRange, string logicalText, CTokenType kind, CToken? expandedFrom)
{
this.Range = range;
this.Location = range;
this.Text = text;
this.Kind = kind;
this.LogicalRange = logicalRange;
Expand All @@ -64,7 +67,7 @@ public CToken(Text.Range range, string text, Text.Range logicalRange, string log
/// <param name="logicalRange">The logical <see cref="Text.Range"/> of the <see cref="CToken"/> in the source.</param>
/// <param name="logicalText">The original text the <see cref="CToken"/> was parsed from.</param>
/// <param name="kind">The <see cref="CTokenType"/> of the <see cref="CToken"/>.</param>
public CToken(Text.Range range, string text, Text.Range logicalRange, string logicalText, CTokenType kind)
public CToken(Text.Location range, string text, Text.Range logicalRange, string logicalText, CTokenType kind)
: this(range, text, logicalRange, logicalText, kind, null)
{
}
Expand All @@ -82,6 +85,7 @@ public CToken(Text.Range range, string text, Text.Range logicalRange, string log
public bool Equals(CToken? other) =>
other is not null
&& this.Range == other.Range
&& this.Location.File.Path == other.Location.File.Path
&& this.Text == other.Text
&& this.Kind == other.Kind
&& this.LogicalRange == other.LogicalRange
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ partial {{ LexerType.Kind }} {{ LexerType.Name }}
public ICharStream {{ SourceName }} { get; }

public {{ LexerType.Name }}(ICharStream source) { this.{{ SourceName }} = source; }
public {{ LexerType.Name }}(TextReader reader) : this(new TextReaderCharStream(reader)) { }
public {{ LexerType.Name }}(string text) : this(new StringReader(text)) { }
public {{ LexerType.Name }}(TextReader reader) : this(new TextReaderCharStream(new SourceFile("<no-location>", reader))) { }
public {{ LexerType.Name }}(SourceFile reader) : this(new TextReaderCharStream(reader)) { }
public {{ LexerType.Name }}(string path, string text) : this(new SourceFile(path, text)) { }
public {{ LexerType.Name }}(string text) : this(new SourceFile("<no-location>", text)) { }
{{ end }}

{{ # Token parsing implementation }}
Expand Down
6 changes: 3 additions & 3 deletions Sources/SynKit/Libraries/Lexer/CharStreamExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,12 @@ public static string ConsumeText(this ICharStream stream, int length, out Text.R
/// <param name="makeToken">The factory function that receives the source <see cref="Text.Range"/> of the skipped characters
/// and the skipped characters themselves concatenated as a string, and produces an <see cref="IToken"/> from them.</param>
/// <returns>The constructed <see cref="IToken{TKind}"/> returned from <paramref name="makeToken"/>.</returns>
public static TToken ConsumeToken<TToken>(this ICharStream stream, int length, Func<Text.Range, string, TToken> makeToken)
public static TToken ConsumeToken<TToken>(this ICharStream stream, int length, Func<Text.Location, string, TToken> makeToken)
{
var start = stream.Position;
var text = length == 0 ? string.Empty : stream.ConsumeText(length);
var range = new Text.Range(start, stream.Position);
return makeToken(range, text);
return makeToken(new Text.Location(stream.SourceFile, range), text);
}

/// <summary>
Expand All @@ -128,5 +128,5 @@ public static TToken ConsumeToken<TToken>(this ICharStream stream, int length, F
/// <returns>The constructed <see cref="Token{TKind}"/>.</returns>
public static Token<TKind> ConsumeToken<TKind>(this ICharStream stream, TKind kind, int length)
where TKind : notnull =>
stream.ConsumeToken(length, (range, text) => new Token<TKind>(range, text, kind));
stream.ConsumeToken(length, (location, text) => new Token<TKind>(location.Range, location, text, kind));
}
2 changes: 2 additions & 0 deletions Sources/SynKit/Libraries/Lexer/ICharStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ namespace Yoakke.SynKit.Lexer;
/// </summary>
public interface ICharStream : IPeekableStream<char>
{
public ISourceFile SourceFile { get; }

Check warning on line 15 in Sources/SynKit/Libraries/Lexer/ICharStream.cs

View workflow job for this annotation

GitHub Actions / Solution (ubuntu-latest)

Missing XML comment for publicly visible type or member 'ICharStream.SourceFile'

Check warning on line 15 in Sources/SynKit/Libraries/Lexer/ICharStream.cs

View workflow job for this annotation

GitHub Actions / Solution (macos-latest)

Missing XML comment for publicly visible type or member 'ICharStream.SourceFile'

Check warning on line 15 in Sources/SynKit/Libraries/Lexer/ICharStream.cs

View workflow job for this annotation

GitHub Actions / Solution (windows-latest)

Missing XML comment for publicly visible type or member 'ICharStream.SourceFile'

Check warning on line 15 in Sources/SynKit/Libraries/Lexer/ICharStream.cs

View workflow job for this annotation

GitHub Actions / deployment

Missing XML comment for publicly visible type or member 'ICharStream.SourceFile'

Check warning on line 15 in Sources/SynKit/Libraries/Lexer/ICharStream.cs

View workflow job for this annotation

GitHub Actions / deployment

Missing XML comment for publicly visible type or member 'ICharStream.SourceFile'

Check warning on line 15 in Sources/SynKit/Libraries/Lexer/ICharStream.cs

View workflow job for this annotation

GitHub Actions / deployment

Missing XML comment for publicly visible type or member 'ICharStream.SourceFile'

/// <summary>
/// The current <see cref="Text.Position"/> the stream is at.
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions Sources/SynKit/Libraries/Lexer/IToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using System;
using Range = Yoakke.SynKit.Text.Range;
using Location = Yoakke.SynKit.Text.Location;

namespace Yoakke.SynKit.Lexer;

Expand All @@ -18,6 +19,11 @@ public interface IToken : IEquatable<IToken>
/// </summary>
public Range Range { get; }

/// <summary>
/// The <see cref="Text.Location"/> that the token can be found at in the source.
/// </summary>
public Location Location { get; }

/// <summary>
/// The text this token was parsed from.
/// </summary>
Expand Down
26 changes: 24 additions & 2 deletions Sources/SynKit/Libraries/Lexer/TextReaderCharStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,46 @@ public class TextReaderCharStream : ICharStream
/// <summary>
/// The underlying <see cref="TextReader"/>.
/// </summary>
public TextReader Underlying { get; }
public TextReader Underlying => this.SourceFile.Reader;

/// <inheritdoc/>
public Position Position { get; private set; }

/// <inheritdoc/>
public bool IsEnd => !this.TryPeek(out _);

/// <inheritdoc/>
public ISourceFile SourceFile => this.underlying;

private readonly RingBuffer<char> peek = new();
private char prevChar;
private ISourceFile underlying;

/// <summary>
/// Initializes a new instance of the <see cref="TextReaderCharStream"/> class.
/// </summary>
/// <param name="underlying">The unerlying <see cref="ISourceFile"/> to read from.</param>
public TextReaderCharStream(ISourceFile underlying)
{
this.underlying = underlying;
}

/// <summary>
/// Initializes a new instance of the <see cref="TextReaderCharStream"/> class.
/// </summary>
/// <param name="underlying">The unerlying <see cref="SourceFile"/> to read from.</param>
public TextReaderCharStream(SourceFile underlying)
{
this.underlying = underlying;
}

/// <summary>
/// Initializes a new instance of the <see cref="TextReaderCharStream"/> class.
/// </summary>
/// <param name="underlying">The unerlying <see cref="TextReader"/> to read from.</param>
public TextReaderCharStream(TextReader underlying)
{
this.Underlying = underlying;
this.underlying = new SourceFile("<no-location>", underlying);
}

/// <inheritdoc/>
Expand Down
13 changes: 12 additions & 1 deletion Sources/SynKit/Libraries/Lexer/Token.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,30 @@

using System;
using Range = Yoakke.SynKit.Text.Range;
using Location = Yoakke.SynKit.Text.Location;

namespace Yoakke.SynKit.Lexer;

/// <summary>
/// A default implementation for <see cref="IToken{TKind}"/>.
/// </summary>
/// <typeparam name="TKind">The kind type this <see cref="Token{TKind}"/> uses. Usually an enumeration type.</typeparam>
public sealed record Token<TKind>(Range Range, string Text, TKind Kind) : IToken<TKind>, IEquatable<Token<TKind>>
public sealed record Token<TKind>(Range Range, Location Location, string Text, TKind Kind) : IToken<TKind>, IEquatable<Token<TKind>>
where TKind : notnull
{
/// <inheritdoc/>
public bool Equals(IToken? other) => this.Equals(other as Token<TKind>);

/// <inheritdoc/>
public bool Equals(IToken<TKind>? other) => this.Equals(other as Token<TKind>);
public bool Equals(Token<TKind>? other) =>

Check warning on line 23 in Sources/SynKit/Libraries/Lexer/Token.cs

View workflow job for this annotation

GitHub Actions / Solution (ubuntu-latest)

Missing XML comment for publicly visible type or member 'Token<TKind>.Equals(Token<TKind>?)'

Check warning on line 23 in Sources/SynKit/Libraries/Lexer/Token.cs

View workflow job for this annotation

GitHub Actions / Solution (macos-latest)

Missing XML comment for publicly visible type or member 'Token<TKind>.Equals(Token<TKind>?)'

Check warning on line 23 in Sources/SynKit/Libraries/Lexer/Token.cs

View workflow job for this annotation

GitHub Actions / Solution (windows-latest)

Missing XML comment for publicly visible type or member 'Token<TKind>.Equals(Token<TKind>?)'

Check warning on line 23 in Sources/SynKit/Libraries/Lexer/Token.cs

View workflow job for this annotation

GitHub Actions / deployment

Missing XML comment for publicly visible type or member 'Token<TKind>.Equals(Token<TKind>?)'

Check warning on line 23 in Sources/SynKit/Libraries/Lexer/Token.cs

View workflow job for this annotation

GitHub Actions / deployment

Missing XML comment for publicly visible type or member 'Token<TKind>.Equals(Token<TKind>?)'
other is not null
&& this.Range == other.Range
&& this.Location.File.Path == other.Location.File.Path
&& this.Text == other.Text
&& this.Kind.Equals(other.Kind);

/// <inheritdoc/>
public override int GetHashCode() =>
HashCode.Combine(this.Range, this.Text, this.Kind, this.Location.File.Path);
}
6 changes: 5 additions & 1 deletion Sources/SynKit/Libraries/Text/SourceFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class SourceFile : TextReader, ISourceFile
public string Path { get; }

/// <inheritdoc/>
public TextReader Reader => this;
public TextReader Reader => this.underlying;

/// <inheritdoc/>
public int AvailableLines => this.lineStarts.Count;
Expand Down Expand Up @@ -60,6 +60,10 @@ public SourceFile(string path, string source)
{
}
this.index = 0;

// this is to reset state of the reader to the "initial".
// I resort to this hack, since constructor from string pre-scan lines, but did not reset.
this.underlying = new StringReader(source);
}

/// <inheritdoc/>
Expand Down
4 changes: 3 additions & 1 deletion Sources/SynKit/Tests/C.Syntax.Tests/CLexerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ namespace Yoakke.SynKit.C.Syntax.Tests;

public class CLexerTests
{
private static ISourceFile fakeLocation = new Text.SourceFile("<no-location>", "1");

private static Range Rn(int line, int column, int length) => new(new(line, column), length);

private static Range Rn(Position from, Position to) => new(from, to);
Expand All @@ -21,7 +23,7 @@ public class CLexerTests

private static CToken Tok(Kind ty, Range r, string t) => Tok(ty, r, t, r, t);

private static CToken Tok(Kind ty, Range r, string t, Range logicalR, string logicalT) => new(r, t, logicalR, logicalT, ty);
private static CToken Tok(Kind ty, Range r, string t, Range logicalR, string logicalT) => new(new Location(fakeLocation, r), t, logicalR, logicalT, ty);

[Theory]

Expand Down
4 changes: 2 additions & 2 deletions Sources/SynKit/Tests/Lexer.Tests/CharStreamTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ public Token<TokenType> Next()
var result = this.charStream.ConsumeToken(TokenType.Identifier, length);
return result.Text switch
{
"if" => new Token<TokenType>(result.Range, result.Text, TokenType.KwIf),
"else" => new Token<TokenType>(result.Range, result.Text, TokenType.KwElse),
"if" => new Token<TokenType>(result.Range, result.Location, result.Text, TokenType.KwIf),
"else" => new Token<TokenType>(result.Range, result.Location, result.Text, TokenType.KwElse),
_ => result,
};
}
Expand Down
5 changes: 4 additions & 1 deletion Sources/SynKit/Tests/Lexer.Tests/Issue17Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ public ExplicitCtorLexer(string text)
[Fact]
public void ImplicitCtors()
{
Assert.Equal(3, typeof(ImplicitCtorLexer).GetConstructors().Length);
Assert.Equal(5, typeof(ImplicitCtorLexer).GetConstructors().Length);
Assert.NotNull(typeof(ImplicitCtorLexer).GetConstructor(new[] { typeof(string) }));
Assert.NotNull(typeof(ImplicitCtorLexer).GetConstructor(new[] { typeof(string), typeof(string) }));
Assert.NotNull(typeof(ImplicitCtorLexer).GetConstructor(new[] { typeof(TextReader) }));
Assert.NotNull(typeof(ImplicitCtorLexer).GetConstructor(new[] { typeof(Text.SourceFile) }));
Assert.NotNull(typeof(ImplicitCtorLexer).GetConstructor(new[] { typeof(ICharStream) }));
}

Expand All @@ -48,6 +50,7 @@ public void ExplicitCtors()
{
Assert.Single(typeof(ExplicitCtorLexer).GetConstructors());
Assert.NotNull(typeof(ExplicitCtorLexer).GetConstructor(new[] { typeof(string) }));
Assert.Null(typeof(ExplicitCtorLexer).GetConstructor(new[] { typeof(Text.SourceFile) }));
Assert.Null(typeof(ExplicitCtorLexer).GetConstructor(new[] { typeof(TextReader) }));
}
}
2 changes: 1 addition & 1 deletion Sources/SynKit/Tests/Lexer.Tests/TestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Yoakke.SynKit.Lexer.Tests;
public abstract class TestBase<TKind>
where TKind : notnull
{
protected static Token<TKind> Token(string value, TKind tt, Range r) => new(r, value, tt);
protected static Token<TKind> Token(string value, TKind tt, Range r) => new(r, new Location(new SourceFile("<no-location>", "test"), r), value, tt);

protected static Range Range((int Line, int Column) p1, (int Line, int Column) p2) =>
new(new Position(p1.Line, p1.Column), new Position(p2.Line, p2.Column));
Expand Down

0 comments on commit ce461fd

Please sign in to comment.