...
This commit is contained in:
274
src/Nub.Lang.Syntax/Source.cs
Normal file
274
src/Nub.Lang.Syntax/Source.cs
Normal file
@@ -0,0 +1,274 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Nub.Lang.Syntax;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a location in source code with line and column information.
|
||||
/// Lines and columns are 1-based to match typical editor conventions.
|
||||
/// </summary>
|
||||
public readonly struct SourceLocation : IEquatable<SourceLocation>, IComparable<SourceLocation>
|
||||
{
|
||||
public SourceLocation(int line, int column)
|
||||
{
|
||||
if (line < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(line), "Line must be >= 1");
|
||||
}
|
||||
|
||||
if (column < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(column), "Column must be >= 1");
|
||||
}
|
||||
|
||||
Line = line;
|
||||
Column = column;
|
||||
}
|
||||
|
||||
public int Line { get; }
|
||||
public int Column { get; }
|
||||
|
||||
public int CompareTo(SourceLocation other)
|
||||
{
|
||||
var lineComparison = Line.CompareTo(other.Line);
|
||||
if (lineComparison == 0)
|
||||
{
|
||||
return Column.CompareTo(other.Column);
|
||||
}
|
||||
|
||||
return lineComparison;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Line}:{Column}";
|
||||
}
|
||||
|
||||
public bool Equals(SourceLocation other)
|
||||
{
|
||||
return Line == other.Line && Column == other.Column;
|
||||
}
|
||||
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is SourceLocation other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Line, Column);
|
||||
}
|
||||
|
||||
public static bool operator ==(SourceLocation left, SourceLocation right) => left.Equals(right);
|
||||
public static bool operator !=(SourceLocation left, SourceLocation right) => !left.Equals(right);
|
||||
public static bool operator <(SourceLocation left, SourceLocation right) => left.CompareTo(right) < 0;
|
||||
public static bool operator >(SourceLocation left, SourceLocation right) => left.CompareTo(right) > 0;
|
||||
public static bool operator <=(SourceLocation left, SourceLocation right) => left.CompareTo(right) <= 0;
|
||||
public static bool operator >=(SourceLocation left, SourceLocation right) => left.CompareTo(right) >= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents source text with a name (typically filename) and content.
|
||||
/// Equality is based on both name and content for better semantics.
|
||||
/// </summary>
|
||||
public struct SourceText : IEquatable<SourceText>
|
||||
{
|
||||
private int _lines = -1;
|
||||
|
||||
public SourceText(string path, string content)
|
||||
{
|
||||
Path = path ?? throw new ArgumentNullException(nameof(path));
|
||||
Content = content ?? throw new ArgumentNullException(nameof(content));
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
public string Content { get; }
|
||||
|
||||
public int LineCount()
|
||||
{
|
||||
if (_lines == -1)
|
||||
{
|
||||
_lines = Content.Split('\n').Length + 1;
|
||||
}
|
||||
|
||||
return _lines;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a specific line from the source text (1-based).
|
||||
/// </summary>
|
||||
public string GetLine(int lineNumber)
|
||||
{
|
||||
if (lineNumber < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(lineNumber));
|
||||
}
|
||||
|
||||
var lines = Content.Split('\n');
|
||||
return lineNumber <= lines.Length ? lines[lineNumber - 1] : string.Empty;
|
||||
}
|
||||
|
||||
public bool Equals(SourceText other)
|
||||
{
|
||||
return Path == other.Path && Content == other.Content;
|
||||
}
|
||||
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is SourceText other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Path, Content);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Path;
|
||||
}
|
||||
|
||||
public static bool operator ==(SourceText left, SourceText right) => left.Equals(right);
|
||||
public static bool operator !=(SourceText left, SourceText right) => !left.Equals(right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a span of source code from a start to end location within a source text.
|
||||
/// </summary>
|
||||
public readonly struct SourceSpan : IEquatable<SourceSpan>
|
||||
{
|
||||
public SourceSpan(SourceText text, SourceLocation start, SourceLocation end)
|
||||
{
|
||||
if (start > end)
|
||||
{
|
||||
throw new ArgumentException("Start location cannot be after end location");
|
||||
}
|
||||
|
||||
if (end.Line > text.LineCount() || end.Line == text.LineCount() && end.Column > text.GetLine(text.LineCount()).Length + 1)
|
||||
{
|
||||
throw new ArgumentException("End location cannot be after text end location");
|
||||
}
|
||||
|
||||
Text = text;
|
||||
Start = start;
|
||||
End = end;
|
||||
}
|
||||
|
||||
public SourceText Text { get; }
|
||||
public SourceLocation Start { get; }
|
||||
public SourceLocation End { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this span represents a single point (start == end).
|
||||
/// </summary>
|
||||
public bool IsEmpty => Start == End;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this span is contained within a single line.
|
||||
/// </summary>
|
||||
public bool IsSingleLine => Start.Line == End.Line;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the text content covered by this span.
|
||||
/// </summary>
|
||||
public string GetText()
|
||||
{
|
||||
if (IsEmpty)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var lines = Text.Content.Split('\n');
|
||||
|
||||
if (IsSingleLine)
|
||||
{
|
||||
var line = lines[Start.Line - 1];
|
||||
var startCol = Math.Min(Start.Column - 1, line.Length);
|
||||
var endCol = Math.Min(End.Column - 1, line.Length);
|
||||
return line.Substring(startCol, Math.Max(0, endCol - startCol));
|
||||
}
|
||||
|
||||
var result = new List<string>();
|
||||
for (var i = Start.Line - 1; i < Math.Min(End.Line, lines.Length); i++)
|
||||
{
|
||||
var line = lines[i];
|
||||
if (i == Start.Line - 1)
|
||||
{
|
||||
result.Add(line[Math.Min(Start.Column - 1, line.Length)..]);
|
||||
}
|
||||
else if (i == End.Line - 1)
|
||||
{
|
||||
result.Add(line[..Math.Min(End.Column - 1, line.Length)]);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add(line);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Join("\n", result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges multiple source spans from the same file into a single span.
|
||||
/// The result spans from the earliest start to the latest end.
|
||||
/// </summary>
|
||||
public static SourceSpan Merge(IEnumerable<SourceSpan> spans)
|
||||
{
|
||||
var spanArray = spans.ToArray();
|
||||
if (spanArray.Length == 0)
|
||||
{
|
||||
throw new ArgumentException("Cannot merge empty collection of spans", nameof(spans));
|
||||
}
|
||||
|
||||
var firstText = spanArray[0].Text;
|
||||
if (spanArray.Any(s => !s.Text.Equals(firstText)))
|
||||
{
|
||||
throw new ArgumentException("All spans must be from the same source text", nameof(spans));
|
||||
}
|
||||
|
||||
var minStart = spanArray.Min(s => s.Start);
|
||||
var maxEnd = spanArray.Max(s => s.End);
|
||||
|
||||
return new SourceSpan(firstText, minStart, maxEnd);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (IsEmpty)
|
||||
{
|
||||
return $"{Start}";
|
||||
}
|
||||
|
||||
if (IsSingleLine)
|
||||
{
|
||||
return Start.Column == End.Column ? $"{Start}" : $"{Start.Line}:{Start.Column}-{End.Column}";
|
||||
}
|
||||
|
||||
return $"{Start}-{End}";
|
||||
}
|
||||
|
||||
public bool Equals(SourceSpan other)
|
||||
{
|
||||
return Text.Equals(other.Text) && Start.Equals(other.Start) && End.Equals(other.End);
|
||||
}
|
||||
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is SourceSpan other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Text, Start, End);
|
||||
}
|
||||
|
||||
public static bool operator ==(SourceSpan left, SourceSpan right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(SourceSpan left, SourceSpan right)
|
||||
{
|
||||
return !left.Equals(right);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user