Skip to main content

harn_parser/
lib.rs

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