Files
nub-lang/src/compiler/NubLang/Source.cs
nub31 c79985bc05 ...
2025-07-06 20:56:56 +02:00

274 lines
7.7 KiB
C#

using System.Diagnostics.CodeAnalysis;
namespace NubLang;
/// <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);
}
}