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::IntegerLiteralOutOfRange(_, span)
94                | harn_lexer::LexerError::UnterminatedBlockComment(span) => Some(span),
95            },
96            PipelineError::Parse(e) => match e {
97                ParserError::Unexpected { span, .. } => Some(span),
98                ParserError::UnexpectedEof { span, .. } => Some(span),
99            },
100            PipelineError::TypeCheck(diag) => diag.span.as_ref(),
101        }
102    }
103}
104
105/// Lex and parse source into an AST.
106pub fn parse_source(source: &str) -> Result<Vec<SNode>, PipelineError> {
107    let mut lexer = harn_lexer::Lexer::new(source);
108    let tokens = lexer.tokenize()?;
109    let mut parser = Parser::new(tokens);
110    Ok(parser.parse()?)
111}
112
113/// Lex, parse, and type-check source. Returns the AST and any type
114/// diagnostics (which may include warnings even on success).
115pub fn check_source(source: &str) -> Result<(Vec<SNode>, Vec<TypeDiagnostic>), PipelineError> {
116    let program = parse_source(source)?;
117    let diagnostics = TypeChecker::new().check_with_source(&program, source);
118    Ok((program, diagnostics))
119}
120
121/// Lex, parse, and type-check, bailing on the first type error.
122pub fn check_source_strict(source: &str) -> Result<Vec<SNode>, PipelineError> {
123    let (program, diagnostics) = check_source(source)?;
124    for diag in &diagnostics {
125        if diag.severity == DiagnosticSeverity::Error {
126            return Err(PipelineError::TypeCheck(Box::new(diag.clone())));
127        }
128    }
129    Ok(program)
130}
131
132#[cfg(test)]
133mod pipeline_tests {
134    use super::*;
135
136    #[test]
137    fn parse_source_valid() {
138        let program = parse_source("let x = 1").unwrap();
139        assert!(!program.is_empty());
140    }
141
142    #[test]
143    fn parse_source_lex_error() {
144        let err = parse_source("let x = `").unwrap_err();
145        assert!(matches!(err, PipelineError::Lex(_)));
146        assert!(err.span().is_some());
147        assert!(err.to_string().contains("Unexpected character"));
148    }
149
150    #[test]
151    fn parse_source_parse_error() {
152        let err = parse_source("let = 1").unwrap_err();
153        assert!(matches!(err, PipelineError::Parse(_)));
154        assert!(err.span().is_some());
155    }
156
157    #[test]
158    fn check_source_returns_diagnostics() {
159        let (program, _diagnostics) = check_source("let x = 1").unwrap();
160        assert!(!program.is_empty());
161    }
162
163    #[test]
164    fn check_source_strict_passes_valid_code() {
165        let program = check_source_strict("let x = 1\nlog(x)").unwrap();
166        assert!(!program.is_empty());
167    }
168
169    #[test]
170    fn check_source_strict_catches_lex_error() {
171        let err = check_source_strict("`").unwrap_err();
172        assert!(matches!(err, PipelineError::Lex(_)));
173    }
174
175    #[test]
176    fn pipeline_error_display_is_informative() {
177        let err = parse_source("`").unwrap_err();
178        let msg = err.to_string();
179        assert!(!msg.is_empty());
180        assert!(msg.contains('`') || msg.contains("Unexpected"));
181    }
182
183    #[test]
184    fn pipeline_error_size_is_bounded() {
185        // TypeCheck is boxed; guard against accidental growth of the other variants.
186        assert!(
187            std::mem::size_of::<PipelineError>() <= 96,
188            "PipelineError grew to {} bytes — consider boxing large variants",
189            std::mem::size_of::<PipelineError>()
190        );
191    }
192}