Skip to main content

workshop_rs/
source.rs

1//! Source model: files, positions, and spans.
2//!
3//! Conventions: positions are 1-based, and a span is a half-open interval
4//! (`end` is exclusive). Spans carry a typed [`FileId`] instead of a raw file
5//! index.
6
7use crate::ids::Id;
8
9/// A typed ID referencing a [`SourceFile`] in the program's file arena.
10pub type FileId = Id<SourceFile>;
11
12/// One source file in the program's file registry.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct SourceFile {
15    /// The file name as the frontend reported it (for diagnostics).
16    pub path: String,
17}
18
19impl SourceFile {
20    /// Create a file entry with the given path.
21    pub fn new(path: impl Into<String>) -> Self {
22        SourceFile { path: path.into() }
23    }
24}
25
26/// A 1-based line/column position in a source file.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct Position {
29    pub line: u32,
30    pub col: u32,
31}
32
33impl Position {
34    /// A position at line `line`, column `col` (both 1-based).
35    pub const fn new(line: u32, col: u32) -> Self {
36        Position { line, col }
37    }
38
39    /// Whether this position is valid (1-based).
40    pub const fn is_valid(self) -> bool {
41        self.line >= 1 && self.col >= 1
42    }
43}
44
45/// A half-open, 1-based source interval in one file.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct Span {
48    pub file: FileId,
49    pub start: Position,
50    pub end: Position,
51}
52
53impl Span {
54    /// Create a span in `file` from `start` (inclusive) to `end` (exclusive).
55    pub const fn new(file: FileId, start: Position, end: Position) -> Self {
56        Span { file, start, end }
57    }
58
59    /// Whether the span is structurally valid: both positions are 1-based and
60    /// `end` is not before `start`.
61    pub const fn is_valid(self) -> bool {
62        self.start.is_valid()
63            && self.end.is_valid()
64            && (self.end.line > self.start.line
65                || (self.end.line == self.start.line && self.end.col >= self.start.col))
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::{Position, SourceFile, Span};
72    use crate::ids::Id;
73
74    #[test]
75    fn positions_are_one_based_and_validated() {
76        assert!(Position::new(1, 1).is_valid());
77        assert!(Position::new(10, 24).is_valid());
78        assert!(!Position::new(0, 1).is_valid());
79        assert!(!Position::new(1, 0).is_valid());
80    }
81
82    #[test]
83    fn spans_require_end_not_before_start() {
84        let file = Id::from_index(0);
85        assert!(Span::new(file, Position::new(1, 1), Position::new(1, 5)).is_valid());
86        assert!(Span::new(file, Position::new(1, 1), Position::new(2, 1)).is_valid());
87        assert!(Span::new(file, Position::new(1, 1), Position::new(1, 1)).is_valid());
88        assert!(!Span::new(file, Position::new(1, 5), Position::new(1, 1)).is_valid());
89        assert!(!Span::new(file, Position::new(2, 1), Position::new(1, 1)).is_valid());
90    }
91
92    #[test]
93    fn source_files_carry_paths() {
94        let file = SourceFile::new("source.opy");
95        assert_eq!(file.path, "source.opy");
96    }
97}