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