Skip to main content

harn_parser/
lib.rs

1mod ast;
2pub mod ast_json;
3pub mod builtin_signatures;
4pub mod const_eval;
5pub mod diagnostic;
6pub mod diagnostic_codes;
7mod parser;
8pub mod stdlib_metadata;
9pub mod typechecker;
10pub mod visit;
11
12pub use ast::*;
13pub use diagnostic_codes::{
14    Category as DiagnosticCodeCategory, Code as DiagnosticCode, ParseRepairSafetyError, Repair,
15    RepairId, RepairSafety, RepairTemplate, REPAIR_REGISTRY,
16};
17pub use parser::*;
18pub use stdlib_metadata::{parse_for_span as parse_stdlib_metadata, StdlibMetadata};
19pub use typechecker::{
20    block_definitely_exits, format_type, stmt_definitely_exits, DiagnosticDetails,
21    DiagnosticSeverity, InlayHintInfo, TypeChecker, TypeDiagnostic,
22};
23
24/// Returns `true` if `name` is a builtin recognized by the parser's static analyzer.
25pub fn is_known_builtin(name: &str) -> bool {
26    builtin_signatures::is_builtin(name)
27}
28
29/// Every builtin name known to the parser, alphabetically. Enables bidirectional
30/// drift checks against the VM's runtime registry.
31pub fn known_builtin_names() -> impl Iterator<Item = &'static str> {
32    builtin_signatures::iter_builtin_names()
33}
34
35pub fn known_builtin_metadata() -> impl Iterator<Item = builtin_signatures::BuiltinMetadata> {
36    builtin_signatures::iter_builtin_metadata()
37}
38
39/// Error from a source processing pipeline stage. Wraps the inner error
40/// types so callers can dispatch on the failing stage.
41#[derive(Debug)]
42pub enum PipelineError {
43    Lex(harn_lexer::LexerError),
44    Parse(ParserError),
45    /// Boxed to keep the enum small on the stack — TypeDiagnostic contains
46    /// a Vec<FixEdit>.
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_with_source(&program, source);
105    Ok((program, diagnostics))
106}
107
108/// Lex, parse, and type-check, bailing on the first type error.
109pub fn check_source_strict(source: &str) -> Result<Vec<SNode>, PipelineError> {
110    let (program, diagnostics) = check_source(source)?;
111    for diag in &diagnostics {
112        if diag.severity == DiagnosticSeverity::Error {
113            return Err(PipelineError::TypeCheck(Box::new(diag.clone())));
114        }
115    }
116    Ok(program)
117}
118
119#[cfg(test)]
120mod pipeline_tests {
121    use super::*;
122
123    #[test]
124    fn parse_source_valid() {
125        let program = parse_source("let x = 1").unwrap();
126        assert!(!program.is_empty());
127    }
128
129    #[test]
130    fn parse_source_lex_error() {
131        let err = parse_source("let x = `").unwrap_err();
132        assert!(matches!(err, PipelineError::Lex(_)));
133        assert!(err.span().is_some());
134        assert!(err.to_string().contains("Unexpected character"));
135    }
136
137    #[test]
138    fn parse_source_parse_error() {
139        let err = parse_source("let = 1").unwrap_err();
140        assert!(matches!(err, PipelineError::Parse(_)));
141        assert!(err.span().is_some());
142    }
143
144    #[test]
145    fn check_source_returns_diagnostics() {
146        let (program, _diagnostics) = check_source("let x = 1").unwrap();
147        assert!(!program.is_empty());
148    }
149
150    #[test]
151    fn check_source_strict_passes_valid_code() {
152        let program = check_source_strict("let x = 1\nlog(x)").unwrap();
153        assert!(!program.is_empty());
154    }
155
156    #[test]
157    fn check_source_strict_catches_lex_error() {
158        let err = check_source_strict("`").unwrap_err();
159        assert!(matches!(err, PipelineError::Lex(_)));
160    }
161
162    #[test]
163    fn pipeline_error_display_is_informative() {
164        let err = parse_source("`").unwrap_err();
165        let msg = err.to_string();
166        assert!(!msg.is_empty());
167        assert!(msg.contains('`') || msg.contains("Unexpected"));
168    }
169
170    #[test]
171    fn pipeline_error_size_is_bounded() {
172        // TypeCheck is boxed; guard against accidental growth of the other variants.
173        assert!(
174            std::mem::size_of::<PipelineError>() <= 96,
175            "PipelineError grew to {} bytes — consider boxing large variants",
176            std::mem::size_of::<PipelineError>()
177        );
178    }
179}