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, DiagnosticSeverity, InlayHintInfo,
11    TypeChecker, TypeDiagnostic,
12};
13
14/// Returns `true` if `name` is a builtin recognized by the parser's static
15/// analyzer. Exposed for cross-crate drift tests (see
16/// `crates/harn-vm/tests/builtin_registry_alignment.rs`) and any future
17/// tooling that needs to validate builtin references without running the
18/// VM.
19pub fn is_known_builtin(name: &str) -> bool {
20    builtin_signatures::is_builtin(name)
21}
22
23/// Iterator over every builtin name known to the parser, in alphabetical
24/// order. Enables bidirectional drift checks against the VM's runtime
25/// registry — a parser entry with no runtime counterpart means a stale
26/// signature that should be removed.
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// ── Source pipeline ──────────────────────────────────────────────────
36
37/// Error from a source processing pipeline stage. Wraps the original
38/// error types without allocating a String — use `Display` when you
39/// need text.
40#[derive(Debug)]
41pub enum PipelineError {
42    Lex(harn_lexer::LexerError),
43    Parse(ParserError),
44    /// A type-checking error. Boxed to keep the enum small on the stack
45    /// (TypeDiagnostic contains Vec<FixEdit>). Carries full diagnostic
46    /// for rich error rendering (span, help text, fix edits).
47    TypeCheck(Box<TypeDiagnostic>),
48}
49
50impl std::fmt::Display for PipelineError {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        match self {
53            PipelineError::Lex(e) => e.fmt(f),
54            PipelineError::Parse(e) => e.fmt(f),
55            PipelineError::TypeCheck(diag) => write!(f, "type error: {}", diag.message),
56        }
57    }
58}
59
60impl std::error::Error for PipelineError {}
61
62impl From<harn_lexer::LexerError> for PipelineError {
63    fn from(e: harn_lexer::LexerError) -> Self {
64        PipelineError::Lex(e)
65    }
66}
67
68impl From<ParserError> for PipelineError {
69    fn from(e: ParserError) -> Self {
70        PipelineError::Parse(e)
71    }
72}
73
74impl PipelineError {
75    /// Extract the source span, if any, for diagnostic rendering.
76    pub fn span(&self) -> Option<&harn_lexer::Span> {
77        match self {
78            PipelineError::Lex(e) => match e {
79                harn_lexer::LexerError::UnexpectedCharacter(_, span)
80                | harn_lexer::LexerError::UnterminatedString(span)
81                | harn_lexer::LexerError::UnterminatedBlockComment(span) => Some(span),
82            },
83            PipelineError::Parse(e) => match e {
84                ParserError::Unexpected { span, .. } => Some(span),
85                ParserError::UnexpectedEof { span, .. } => Some(span),
86            },
87            PipelineError::TypeCheck(diag) => diag.span.as_ref(),
88        }
89    }
90}
91
92/// Lex and parse source into an AST.
93pub fn parse_source(source: &str) -> Result<Vec<SNode>, PipelineError> {
94    let mut lexer = harn_lexer::Lexer::new(source);
95    let tokens = lexer.tokenize()?;
96    let mut parser = Parser::new(tokens);
97    Ok(parser.parse()?)
98}
99
100/// Lex, parse, and type-check source. Returns the AST and any type
101/// diagnostics (which may include warnings even on success).
102pub fn check_source(source: &str) -> Result<(Vec<SNode>, Vec<TypeDiagnostic>), PipelineError> {
103    let program = parse_source(source)?;
104    let diagnostics = TypeChecker::new().check(&program);
105    Ok((program, diagnostics))
106}
107
108/// Lex, parse, and type-check, bailing on the first type error.
109/// Returns the AST on success. Useful when callers want to proceed
110/// to compilation only if there are no type errors.
111pub fn check_source_strict(source: &str) -> Result<Vec<SNode>, PipelineError> {
112    let (program, diagnostics) = check_source(source)?;
113    for diag in &diagnostics {
114        if diag.severity == DiagnosticSeverity::Error {
115            return Err(PipelineError::TypeCheck(Box::new(diag.clone())));
116        }
117    }
118    Ok(program)
119}
120
121#[cfg(test)]
122mod pipeline_tests {
123    use super::*;
124
125    #[test]
126    fn parse_source_valid() {
127        let program = parse_source("let x = 1").unwrap();
128        assert!(!program.is_empty());
129    }
130
131    #[test]
132    fn parse_source_lex_error() {
133        let err = parse_source("let x = @").unwrap_err();
134        assert!(matches!(err, PipelineError::Lex(_)));
135        assert!(err.span().is_some());
136        assert!(err.to_string().contains("Unexpected character"));
137    }
138
139    #[test]
140    fn parse_source_parse_error() {
141        let err = parse_source("let = 1").unwrap_err();
142        assert!(matches!(err, PipelineError::Parse(_)));
143        assert!(err.span().is_some());
144    }
145
146    #[test]
147    fn check_source_returns_diagnostics() {
148        let (program, _diagnostics) = check_source("let x = 1").unwrap();
149        assert!(!program.is_empty());
150    }
151
152    #[test]
153    fn check_source_strict_passes_valid_code() {
154        let program = check_source_strict("let x = 1\nlog(x)").unwrap();
155        assert!(!program.is_empty());
156    }
157
158    #[test]
159    fn check_source_strict_catches_lex_error() {
160        let err = check_source_strict("@").unwrap_err();
161        assert!(matches!(err, PipelineError::Lex(_)));
162    }
163
164    #[test]
165    fn pipeline_error_display_is_informative() {
166        let err = parse_source("@").unwrap_err();
167        let msg = err.to_string();
168        assert!(!msg.is_empty());
169        // Should not be a generic "error" — should contain specifics.
170        assert!(msg.contains('@') || msg.contains("Unexpected"));
171    }
172
173    #[test]
174    fn pipeline_error_size_is_bounded() {
175        // TypeCheck variant is boxed; Lex/Parse carry Span + strings inline.
176        // 88 bytes on 64-bit. Guard against accidental growth.
177        assert!(
178            std::mem::size_of::<PipelineError>() <= 96,
179            "PipelineError grew to {} bytes — consider boxing large variants",
180            std::mem::size_of::<PipelineError>()
181        );
182    }
183}