Skip to main content

harn_lexer/
token.rs

1use std::fmt;
2
3/// A segment of an interpolated string.
4#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
5pub enum StringSegment {
6    Literal(String),
7    /// An interpolated expression with its source position (line, column).
8    Expression(String, usize, usize),
9}
10
11impl fmt::Display for StringSegment {
12    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13        match self {
14            StringSegment::Literal(s) => write!(f, "{s}"),
15            StringSegment::Expression(e, _, _) => write!(f, "${{{e}}}"),
16        }
17    }
18}
19
20/// Source location for error reporting.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
22pub struct Span {
23    /// Byte offset from start of source (inclusive).
24    pub start: usize,
25    /// Byte offset from start of source (exclusive).
26    pub end: usize,
27    /// 1-based line number of start position.
28    pub line: usize,
29    /// 1-based column number of start position.
30    pub column: usize,
31    /// 1-based line number of end position (for multiline span detection).
32    pub end_line: usize,
33}
34
35impl Span {
36    pub fn with_offsets(start: usize, end: usize, line: usize, column: usize) -> Self {
37        Self {
38            start,
39            end,
40            line,
41            column,
42            end_line: line,
43        }
44    }
45
46    /// Create a span covering two spans (from start of `a` to end of `b`).
47    pub fn merge(a: Span, b: Span) -> Span {
48        Span {
49            start: a.start,
50            end: b.end,
51            line: a.line,
52            column: a.column,
53            end_line: b.end_line,
54        }
55    }
56
57    /// A dummy span for synthetic/generated nodes.
58    pub fn dummy() -> Self {
59        Self {
60            start: 0,
61            end: 0,
62            line: 0,
63            column: 0,
64            end_line: 0,
65        }
66    }
67}
68
69impl fmt::Display for Span {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        write!(f, "{}:{}", self.line, self.column)
72    }
73}
74
75/// A machine-applicable text replacement for autofixing diagnostics.
76#[derive(Debug, Clone)]
77pub struct FixEdit {
78    /// The source span to replace.
79    pub span: Span,
80    /// The replacement text (empty string = deletion).
81    pub replacement: String,
82}
83
84impl FixEdit {
85    /// Sort edits right-to-left by start offset and drop any that overlap an
86    /// already-accepted edit, returning the survivors in descending-start
87    /// order — ready to splice right-to-left without invalidating earlier
88    /// offsets. This is the single source of truth for the "apply all fixes,
89    /// drop conflicts" policy that `harn fmt`, `harn lint --fix`, and the LSP
90    /// on-save fixer must agree on byte-for-byte.
91    pub fn dedupe_overlapping(edits: &[FixEdit]) -> Vec<FixEdit> {
92        let mut sorted = edits.to_vec();
93        sorted.sort_by_key(|edit| std::cmp::Reverse(edit.span.start));
94        let mut accepted: Vec<FixEdit> = Vec::new();
95        for edit in sorted {
96            let overlaps = accepted
97                .iter()
98                .any(|prev| edit.span.start < prev.span.end && edit.span.end > prev.span.start);
99            if !overlaps {
100                accepted.push(edit);
101            }
102        }
103        accepted
104    }
105
106    /// Apply `edits` to `source`, dropping overlaps via
107    /// [`Self::dedupe_overlapping`] and splicing right-to-left. Callers that
108    /// also need the accepted-edit list (e.g. to build LSP `TextEdit`s) should
109    /// call `dedupe_overlapping` directly.
110    pub fn apply_all(source: &str, edits: &[FixEdit]) -> String {
111        let mut out = source.to_string();
112        for edit in Self::dedupe_overlapping(edits) {
113            let before = &out[..edit.span.start];
114            let after = &out[edit.span.end..];
115            out = format!("{before}{}{after}", edit.replacement);
116        }
117        out
118    }
119}
120
121#[cfg(test)]
122mod fix_edit_tests {
123    use super::*;
124
125    fn edit(start: usize, end: usize, replacement: &str) -> FixEdit {
126        FixEdit {
127            span: Span::with_offsets(start, end, 1, start + 1),
128            replacement: replacement.to_string(),
129        }
130    }
131
132    #[test]
133    fn apply_all_splices_right_to_left() {
134        // Order-independent input; non-overlapping edits both apply.
135        let out = FixEdit::apply_all("0123456789", &[edit(2, 4, "AB"), edit(6, 8, "CD")]);
136        assert_eq!(out, "01AB45CD89");
137    }
138
139    #[test]
140    fn apply_all_drops_overlapping_edits_descending_start_wins() {
141        // Sorted descending by start, edit(4,8) is accepted and edit(2,6)
142        // overlaps it, so it's dropped — matching fmt/lsp/cli semantics.
143        let out = FixEdit::apply_all("0123456789", &[edit(2, 6, "XXXX"), edit(4, 8, "YYYY")]);
144        assert_eq!(out, "0123YYYY89");
145        assert_eq!(
146            FixEdit::dedupe_overlapping(&[edit(2, 6, "x"), edit(4, 8, "y")]).len(),
147            1
148        );
149    }
150}
151
152/// Canonical list of Harn language keywords. Single source of truth; the lexer's
153/// identifier-to-keyword match must stay in sync (enforced by
154/// `test_keywords_const_covers_lexer`). External tooling should consume this
155/// rather than duplicate it.
156pub const KEYWORDS: &[&str] = &[
157    "ask_user",
158    "break",
159    "catch",
160    "const",
161    "continue",
162    "deadline",
163    "defer",
164    "dual_control",
165    "else",
166    "emit",
167    "enum",
168    "escalate_to",
169    "eval_pack",
170    "exclusive",
171    "extends",
172    "false",
173    "finally",
174    "fn",
175    "for",
176    "from",
177    "guard",
178    "if",
179    "impl",
180    "import",
181    "in",
182    "interface",
183    "let",
184    "match",
185    "mutex",
186    "nil",
187    "override",
188    "parallel",
189    "pipeline",
190    "pub",
191    "request_approval",
192    "require",
193    "retry",
194    "return",
195    "select",
196    "skill",
197    "spawn",
198    "struct",
199    "throw",
200    "throws",
201    "to",
202    "tool",
203    "true",
204    "try",
205    "type",
206    "var",
207    "while",
208    "yield",
209];
210
211/// Token kinds produced by the lexer.
212#[derive(Debug, Clone, PartialEq)]
213pub enum TokenKind {
214    Pipeline,
215    Extends,
216    Override,
217    Let,
218    Const,
219    Var,
220    If,
221    Else,
222    For,
223    In,
224    Match,
225    Retry,
226    Parallel,
227    Return,
228    Import,
229    True,
230    False,
231    Nil,
232    Try,
233    Catch,
234    Throw,
235    /// `throws` — declares a callable's exception channel in its signature
236    /// (`fn f() -> R throws E`). Distinct from `Throw` (the statement).
237    Throws,
238    Finally,
239    Fn,
240    Spawn,
241    While,
242    TypeKw,
243    Enum,
244    EvalPack,
245    Struct,
246    Interface,
247    Emit,
248    Pub,
249    From,
250    To,
251    Tool,
252    Exclusive,
253    Guard,
254    Require,
255    Deadline,
256    Defer,
257    Yield,
258    Mutex,
259    Break,
260    Continue,
261    Select,
262    Impl,
263    Skill,
264    /// First-class HITL primitive: `request_approval(...)`.
265    RequestApproval,
266    /// First-class HITL primitive: `dual_control(...)`.
267    DualControl,
268    /// First-class HITL primitive: `ask_user(...)`.
269    AskUser,
270    /// First-class HITL primitive: `escalate_to(...)`.
271    EscalateTo,
272
273    Identifier(String),
274    StringLiteral(String),
275    InterpolatedString(Vec<StringSegment>),
276    /// Raw string literal `r"..."` — no escape processing, no interpolation.
277    RawStringLiteral(String),
278    IntLiteral(i64),
279    FloatLiteral(f64),
280    /// Duration literal in milliseconds: 500ms, 5s, 30m, 2h, 1d, 1w
281    DurationLiteral(u64),
282
283    Eq,            // ==
284    Neq,           // !=
285    And,           // &&
286    Or,            // ||
287    Pipe,          // |>
288    NilCoal,       // ??
289    Pow,           // **
290    QuestionDot,   // ?.
291    Arrow,         // ->
292    Lte,           // <=
293    Gte,           // >=
294    PlusAssign,    // +=
295    MinusAssign,   // -=
296    StarAssign,    // *=
297    SlashAssign,   // /=
298    PercentAssign, // %=
299
300    Assign,   // =
301    Not,      // !
302    Dot,      // .
303    Plus,     // +
304    Minus,    // -
305    Star,     // *
306    Slash,    // /
307    Percent,  // %
308    Lt,       // <
309    Gt,       // >
310    Question, // ?
311    Bar,      // |  (for union types)
312    Amp,      // &  (for intersection types)
313
314    LBrace,    // {
315    RBrace,    // }
316    LParen,    // (
317    RParen,    // )
318    LBracket,  // [
319    RBracket,  // ]
320    Comma,     // ,
321    Colon,     // :
322    Semicolon, // ;
323    At,        // @ (attribute prefix)
324
325    LineComment {
326        text: String,
327        is_doc: bool,
328    }, // // text or /// text
329    BlockComment {
330        text: String,
331        is_doc: bool,
332    }, // /* text */ or /** text */
333
334    Newline,
335    Eof,
336}
337
338impl fmt::Display for TokenKind {
339    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340        match self {
341            TokenKind::Pipeline => write!(f, "pipeline"),
342            TokenKind::Extends => write!(f, "extends"),
343            TokenKind::Override => write!(f, "override"),
344            TokenKind::Let => write!(f, "let"),
345            TokenKind::Const => write!(f, "const"),
346            TokenKind::Var => write!(f, "var"),
347            TokenKind::If => write!(f, "if"),
348            TokenKind::Else => write!(f, "else"),
349            TokenKind::For => write!(f, "for"),
350            TokenKind::In => write!(f, "in"),
351            TokenKind::Match => write!(f, "match"),
352            TokenKind::Retry => write!(f, "retry"),
353            TokenKind::Parallel => write!(f, "parallel"),
354            TokenKind::Return => write!(f, "return"),
355            TokenKind::Import => write!(f, "import"),
356            TokenKind::True => write!(f, "true"),
357            TokenKind::False => write!(f, "false"),
358            TokenKind::Nil => write!(f, "nil"),
359            TokenKind::Try => write!(f, "try"),
360            TokenKind::Catch => write!(f, "catch"),
361            TokenKind::Throw => write!(f, "throw"),
362            TokenKind::Throws => write!(f, "throws"),
363            TokenKind::Finally => write!(f, "finally"),
364            TokenKind::Fn => write!(f, "fn"),
365            TokenKind::Spawn => write!(f, "spawn"),
366            TokenKind::While => write!(f, "while"),
367            TokenKind::TypeKw => write!(f, "type"),
368            TokenKind::Enum => write!(f, "enum"),
369            TokenKind::EvalPack => write!(f, "eval_pack"),
370            TokenKind::Struct => write!(f, "struct"),
371            TokenKind::Interface => write!(f, "interface"),
372            TokenKind::Emit => write!(f, "emit"),
373            TokenKind::Pub => write!(f, "pub"),
374            TokenKind::From => write!(f, "from"),
375            TokenKind::To => write!(f, "to"),
376            TokenKind::Tool => write!(f, "tool"),
377            TokenKind::Exclusive => write!(f, "exclusive"),
378            TokenKind::Guard => write!(f, "guard"),
379            TokenKind::Require => write!(f, "require"),
380            TokenKind::Deadline => write!(f, "deadline"),
381            TokenKind::Defer => write!(f, "defer"),
382            TokenKind::Yield => write!(f, "yield"),
383            TokenKind::Mutex => write!(f, "mutex"),
384            TokenKind::Break => write!(f, "break"),
385            TokenKind::Continue => write!(f, "continue"),
386            TokenKind::Select => write!(f, "select"),
387            TokenKind::Impl => write!(f, "impl"),
388            TokenKind::Skill => write!(f, "skill"),
389            TokenKind::RequestApproval => write!(f, "request_approval"),
390            TokenKind::DualControl => write!(f, "dual_control"),
391            TokenKind::AskUser => write!(f, "ask_user"),
392            TokenKind::EscalateTo => write!(f, "escalate_to"),
393            TokenKind::Identifier(s) => write!(f, "id({s})"),
394            TokenKind::StringLiteral(s) => write!(f, "str({s})"),
395            TokenKind::InterpolatedString(_) => write!(f, "istr(...)"),
396            TokenKind::RawStringLiteral(s) => write!(f, "rstr({s})"),
397            TokenKind::IntLiteral(n) => write!(f, "int({n})"),
398            TokenKind::FloatLiteral(n) => write!(f, "float({n})"),
399            TokenKind::DurationLiteral(ms) => write!(f, "duration({ms}ms)"),
400            TokenKind::Eq => write!(f, "=="),
401            TokenKind::Neq => write!(f, "!="),
402            TokenKind::And => write!(f, "&&"),
403            TokenKind::Or => write!(f, "||"),
404            TokenKind::Pipe => write!(f, "|>"),
405            TokenKind::NilCoal => write!(f, "??"),
406            TokenKind::Pow => write!(f, "**"),
407            TokenKind::QuestionDot => write!(f, "?."),
408            TokenKind::Arrow => write!(f, "->"),
409            TokenKind::Lte => write!(f, "<="),
410            TokenKind::Gte => write!(f, ">="),
411            TokenKind::PlusAssign => write!(f, "+="),
412            TokenKind::MinusAssign => write!(f, "-="),
413            TokenKind::StarAssign => write!(f, "*="),
414            TokenKind::SlashAssign => write!(f, "/="),
415            TokenKind::PercentAssign => write!(f, "%="),
416            TokenKind::Assign => write!(f, "="),
417            TokenKind::Not => write!(f, "!"),
418            TokenKind::Dot => write!(f, "."),
419            TokenKind::Plus => write!(f, "+"),
420            TokenKind::Minus => write!(f, "-"),
421            TokenKind::Star => write!(f, "*"),
422            TokenKind::Slash => write!(f, "/"),
423            TokenKind::Percent => write!(f, "%"),
424            TokenKind::Lt => write!(f, "<"),
425            TokenKind::Gt => write!(f, ">"),
426            TokenKind::Question => write!(f, "?"),
427            TokenKind::Bar => write!(f, "|"),
428            TokenKind::Amp => write!(f, "&"),
429            TokenKind::LBrace => write!(f, "{{"),
430            TokenKind::RBrace => write!(f, "}}"),
431            TokenKind::LParen => write!(f, "("),
432            TokenKind::RParen => write!(f, ")"),
433            TokenKind::LBracket => write!(f, "["),
434            TokenKind::RBracket => write!(f, "]"),
435            TokenKind::Comma => write!(f, ","),
436            TokenKind::Colon => write!(f, ":"),
437            TokenKind::Semicolon => write!(f, ";"),
438            TokenKind::At => write!(f, "@"),
439            TokenKind::LineComment { text, is_doc } => {
440                let prefix = if *is_doc { "///" } else { "//" };
441                write!(f, "{prefix} {text}")
442            }
443            TokenKind::BlockComment { text, is_doc } => {
444                let prefix = if *is_doc { "/**" } else { "/*" };
445                write!(f, "{prefix} {text} */")
446            }
447            TokenKind::Newline => write!(f, "\\n"),
448            TokenKind::Eof => write!(f, "EOF"),
449        }
450    }
451}
452
453/// A token with its kind and source location.
454#[derive(Debug, Clone, PartialEq)]
455pub struct Token {
456    pub kind: TokenKind,
457    pub span: Span,
458}
459
460impl Token {
461    pub fn with_span(kind: TokenKind, span: Span) -> Self {
462        Self { kind, span }
463    }
464}