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