Skip to main content

harn_parser/
lib.rs

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