Skip to main content

harn_parser/
lib.rs

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