Skip to main content

harn_parser/
lib.rs

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