Skip to main content

harn_parser/
lib.rs

1mod ast;
2pub mod builtin_signatures;
3pub mod diagnostic;
4mod parser;
5pub mod typechecker;
6pub mod visit;
7
8pub use ast::*;
9pub use parser::*;
10pub use typechecker::{
11    block_definitely_exits, format_type, stmt_definitely_exits, DiagnosticDetails,
12    DiagnosticSeverity, InlayHintInfo, TypeChecker, TypeDiagnostic,
13};
14
15/// Returns `true` if `name` is a builtin recognized by the parser's static analyzer.
16pub fn is_known_builtin(name: &str) -> bool {
17    builtin_signatures::is_builtin(name)
18}
19
20/// Every builtin name known to the parser, alphabetically. Enables bidirectional
21/// drift checks against the VM's runtime registry.
22pub fn known_builtin_names() -> impl Iterator<Item = &'static str> {
23    builtin_signatures::iter_builtin_names()
24}
25
26pub fn known_builtin_metadata() -> impl Iterator<Item = builtin_signatures::BuiltinMetadata> {
27    builtin_signatures::iter_builtin_metadata()
28}
29
30/// Error from a source processing pipeline stage. Wraps the inner error
31/// types so callers can dispatch on the failing stage.
32#[derive(Debug)]
33pub enum PipelineError {
34    Lex(harn_lexer::LexerError),
35    Parse(ParserError),
36    /// Boxed to keep the enum small on the stack — TypeDiagnostic contains
37    /// a Vec<FixEdit>.
38    TypeCheck(Box<TypeDiagnostic>),
39}
40
41impl std::fmt::Display for PipelineError {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        match self {
44            PipelineError::Lex(e) => e.fmt(f),
45            PipelineError::Parse(e) => e.fmt(f),
46            PipelineError::TypeCheck(diag) => write!(f, "type error: {}", diag.message),
47        }
48    }
49}
50
51impl std::error::Error for PipelineError {}
52
53impl From<harn_lexer::LexerError> for PipelineError {
54    fn from(e: harn_lexer::LexerError) -> Self {
55        PipelineError::Lex(e)
56    }
57}
58
59impl From<ParserError> for PipelineError {
60    fn from(e: ParserError) -> Self {
61        PipelineError::Parse(e)
62    }
63}
64
65impl PipelineError {
66    /// Extract the source span, if any, for diagnostic rendering.
67    pub fn span(&self) -> Option<&harn_lexer::Span> {
68        match self {
69            PipelineError::Lex(e) => match e {
70                harn_lexer::LexerError::UnexpectedCharacter(_, span)
71                | harn_lexer::LexerError::UnterminatedString(span)
72                | harn_lexer::LexerError::UnterminatedBlockComment(span) => Some(span),
73            },
74            PipelineError::Parse(e) => match e {
75                ParserError::Unexpected { span, .. } => Some(span),
76                ParserError::UnexpectedEof { span, .. } => Some(span),
77            },
78            PipelineError::TypeCheck(diag) => diag.span.as_ref(),
79        }
80    }
81}
82
83/// Lex and parse source into an AST.
84pub fn parse_source(source: &str) -> Result<Vec<SNode>, PipelineError> {
85    let mut lexer = harn_lexer::Lexer::new(source);
86    let tokens = lexer.tokenize()?;
87    let mut parser = Parser::new(tokens);
88    Ok(parser.parse()?)
89}
90
91/// Lex, parse, and type-check source. Returns the AST and any type
92/// diagnostics (which may include warnings even on success).
93pub fn check_source(source: &str) -> Result<(Vec<SNode>, Vec<TypeDiagnostic>), PipelineError> {
94    let program = parse_source(source)?;
95    let diagnostics = TypeChecker::new().check_with_source(&program, source);
96    Ok((program, diagnostics))
97}
98
99/// Lex, parse, and type-check, bailing on the first type error.
100pub fn check_source_strict(source: &str) -> Result<Vec<SNode>, PipelineError> {
101    let (program, diagnostics) = check_source(source)?;
102    for diag in &diagnostics {
103        if diag.severity == DiagnosticSeverity::Error {
104            return Err(PipelineError::TypeCheck(Box::new(diag.clone())));
105        }
106    }
107    Ok(program)
108}
109
110#[cfg(test)]
111mod pipeline_tests {
112    use super::*;
113
114    #[test]
115    fn parse_source_valid() {
116        let program = parse_source("let x = 1").unwrap();
117        assert!(!program.is_empty());
118    }
119
120    #[test]
121    fn parse_source_lex_error() {
122        let err = parse_source("let x = `").unwrap_err();
123        assert!(matches!(err, PipelineError::Lex(_)));
124        assert!(err.span().is_some());
125        assert!(err.to_string().contains("Unexpected character"));
126    }
127
128    #[test]
129    fn parse_source_parse_error() {
130        let err = parse_source("let = 1").unwrap_err();
131        assert!(matches!(err, PipelineError::Parse(_)));
132        assert!(err.span().is_some());
133    }
134
135    #[test]
136    fn check_source_returns_diagnostics() {
137        let (program, _diagnostics) = check_source("let x = 1").unwrap();
138        assert!(!program.is_empty());
139    }
140
141    #[test]
142    fn check_source_strict_passes_valid_code() {
143        let program = check_source_strict("let x = 1\nlog(x)").unwrap();
144        assert!(!program.is_empty());
145    }
146
147    #[test]
148    fn check_source_strict_catches_lex_error() {
149        let err = check_source_strict("`").unwrap_err();
150        assert!(matches!(err, PipelineError::Lex(_)));
151    }
152
153    #[test]
154    fn pipeline_error_display_is_informative() {
155        let err = parse_source("`").unwrap_err();
156        let msg = err.to_string();
157        assert!(!msg.is_empty());
158        assert!(msg.contains('`') || msg.contains("Unexpected"));
159    }
160
161    #[test]
162    fn pipeline_error_size_is_bounded() {
163        // TypeCheck is boxed; guard against accidental growth of the other variants.
164        assert!(
165            std::mem::size_of::<PipelineError>() <= 96,
166            "PipelineError grew to {} bytes — consider boxing large variants",
167            std::mem::size_of::<PipelineError>()
168        );
169    }
170}