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