Skip to main content

harn_parser/
lib.rs

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