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