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;
8pub mod harness_methods;
9mod parser;
10pub mod stdlib_metadata;
11pub mod typechecker;
12pub mod visit;
13
14pub use ast::*;
15pub use diagnostic_codes::{
16    Category as DiagnosticCodeCategory, Code as DiagnosticCode, ParseRepairSafetyError, Repair,
17    RepairId, RepairSafety, RepairTemplate, REPAIR_REGISTRY,
18};
19pub use parser::*;
20pub use stdlib_metadata::{parse_for_span as parse_stdlib_metadata, StdlibMetadata};
21pub use typechecker::{
22    block_definitely_exits, format_type, stmt_definitely_exits, DiagnosticDetails,
23    DiagnosticSeverity, InlayHintInfo, TypeChecker, TypeDiagnostic,
24};
25
26/// Returns `true` if `name` is a builtin recognized by the parser's static analyzer.
27pub fn is_known_builtin(name: &str) -> bool {
28    builtin_signatures::is_builtin(name)
29}
30
31/// Every builtin name known to the parser, alphabetically. Enables bidirectional
32/// drift checks against the VM's runtime registry.
33pub fn known_builtin_names() -> impl Iterator<Item = &'static str> {
34    builtin_signatures::iter_builtin_names()
35}
36
37pub fn known_builtin_metadata() -> impl Iterator<Item = builtin_signatures::BuiltinMetadata> {
38    builtin_signatures::iter_builtin_metadata()
39}
40
41/// Error from a source processing pipeline stage. Wraps the inner error
42/// types so callers can dispatch on the failing stage.
43#[derive(Debug)]
44pub enum PipelineError {
45    Lex(harn_lexer::LexerError),
46    Parse(ParserError),
47    /// Boxed to keep the enum small on the stack — TypeDiagnostic contains
48    /// a Vec<FixEdit>.
49    TypeCheck(Box<TypeDiagnostic>),
50}
51
52impl std::fmt::Display for PipelineError {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        match self {
55            PipelineError::Lex(e) => e.fmt(f),
56            PipelineError::Parse(e) => e.fmt(f),
57            PipelineError::TypeCheck(diag) => write!(f, "type error: {}", diag.message),
58        }
59    }
60}
61
62impl std::error::Error for PipelineError {}
63
64impl From<harn_lexer::LexerError> for PipelineError {
65    fn from(e: harn_lexer::LexerError) -> Self {
66        PipelineError::Lex(e)
67    }
68}
69
70impl From<ParserError> for PipelineError {
71    fn from(e: ParserError) -> Self {
72        PipelineError::Parse(e)
73    }
74}
75
76impl PipelineError {
77    /// Extract the source span, if any, for diagnostic rendering.
78    pub fn span(&self) -> Option<&harn_lexer::Span> {
79        match self {
80            PipelineError::Lex(e) => match e {
81                harn_lexer::LexerError::UnexpectedCharacter(_, span)
82                | harn_lexer::LexerError::UnterminatedString(span)
83                | harn_lexer::LexerError::UnterminatedBlockComment(span) => Some(span),
84            },
85            PipelineError::Parse(e) => match e {
86                ParserError::Unexpected { span, .. } => Some(span),
87                ParserError::UnexpectedEof { span, .. } => Some(span),
88            },
89            PipelineError::TypeCheck(diag) => diag.span.as_ref(),
90        }
91    }
92}
93
94/// Lex and parse source into an AST.
95pub fn parse_source(source: &str) -> Result<Vec<SNode>, PipelineError> {
96    let mut lexer = harn_lexer::Lexer::new(source);
97    let tokens = lexer.tokenize()?;
98    let mut parser = Parser::new(tokens);
99    Ok(parser.parse()?)
100}
101
102/// Lex, parse, and type-check source. Returns the AST and any type
103/// diagnostics (which may include warnings even on success).
104pub fn check_source(source: &str) -> Result<(Vec<SNode>, Vec<TypeDiagnostic>), PipelineError> {
105    let program = parse_source(source)?;
106    let diagnostics = TypeChecker::new().check_with_source(&program, source);
107    Ok((program, diagnostics))
108}
109
110/// Lex, parse, and type-check, bailing on the first type error.
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        assert!(msg.contains('`') || msg.contains("Unexpected"));
170    }
171
172    #[test]
173    fn pipeline_error_size_is_bounded() {
174        // TypeCheck is boxed; guard against accidental growth of the other variants.
175        assert!(
176            std::mem::size_of::<PipelineError>() <= 96,
177            "PipelineError grew to {} bytes — consider boxing large variants",
178            std::mem::size_of::<PipelineError>()
179        );
180    }
181}