using System.Diagnostics.CodeAnalysis; namespace NubLang; /// /// Represents a location in source code with line and column information. /// Lines and columns are 1-based to match typical editor conventions. /// public readonly struct SourceLocation : IEquatable, IComparable { 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; } /// /// Represents source text with a name (typically filename) and content. /// Equality is based on both name and content for better semantics. /// public struct SourceText : IEquatable { 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; } /// /// Gets a specific line from the source text (1-based). /// 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); } /// /// Represents a span of source code from a start to end location within a source text. /// public readonly struct SourceSpan : IEquatable { 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; } /// /// Gets whether this span represents a single point (start == end). /// public bool IsEmpty => Start == End; /// /// Gets whether this span is contained within a single line. /// public bool IsSingleLine => Start.Line == End.Line; /// /// Gets the text content covered by this span. /// 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(); 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); } /// /// Merges multiple source spans from the same file into a single span. /// The result spans from the earliest start to the latest end. /// public static SourceSpan Merge(IEnumerable 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); } }