1mod ast;
2pub mod ast_json;
3pub mod builtin_signatures;
4pub mod diagnostic;
5pub mod diagnostic_codes;
6mod parser;
7pub mod stdlib_metadata;
8pub mod typechecker;
9pub mod visit;
10
11pub use ast::*;
12pub use diagnostic_codes::{
13 Category as DiagnosticCodeCategory, Code as DiagnosticCode, ParseRepairSafetyError, Repair,
14 RepairId, RepairSafety, RepairTemplate, REPAIR_REGISTRY,
15};
16pub use parser::*;
17pub use stdlib_metadata::{parse_for_span as parse_stdlib_metadata, StdlibMetadata};
18pub use typechecker::{
19 block_definitely_exits, format_type, stmt_definitely_exits, DiagnosticDetails,
20 DiagnosticSeverity, InlayHintInfo, TypeChecker, TypeDiagnostic,
21};
22
23pub fn is_known_builtin(name: &str) -> bool {
25 builtin_signatures::is_builtin(name)
26}
27
28pub fn known_builtin_names() -> impl Iterator<Item = &'static str> {
31 builtin_signatures::iter_builtin_names()
32}
33
34pub fn known_builtin_metadata() -> impl Iterator<Item = builtin_signatures::BuiltinMetadata> {
35 builtin_signatures::iter_builtin_metadata()
36}
37
38#[derive(Debug)]
41pub enum PipelineError {
42 Lex(harn_lexer::LexerError),
43 Parse(ParserError),
44 TypeCheck(Box<TypeDiagnostic>),
47}
48
49impl std::fmt::Display for PipelineError {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 match self {
52 PipelineError::Lex(e) => e.fmt(f),
53 PipelineError::Parse(e) => e.fmt(f),
54 PipelineError::TypeCheck(diag) => write!(f, "type error: {}", diag.message),
55 }
56 }
57}
58
59impl std::error::Error for PipelineError {}
60
61impl From<harn_lexer::LexerError> for PipelineError {
62 fn from(e: harn_lexer::LexerError) -> Self {
63 PipelineError::Lex(e)
64 }
65}
66
67impl From<ParserError> for PipelineError {
68 fn from(e: ParserError) -> Self {
69 PipelineError::Parse(e)
70 }
71}
72
73impl PipelineError {
74 pub fn span(&self) -> Option<&harn_lexer::Span> {
76 match self {
77 PipelineError::Lex(e) => match e {
78 harn_lexer::LexerError::UnexpectedCharacter(_, span)
79 | harn_lexer::LexerError::UnterminatedString(span)
80 | harn_lexer::LexerError::UnterminatedBlockComment(span) => Some(span),
81 },
82 PipelineError::Parse(e) => match e {
83 ParserError::Unexpected { span, .. } => Some(span),
84 ParserError::UnexpectedEof { span, .. } => Some(span),
85 },
86 PipelineError::TypeCheck(diag) => diag.span.as_ref(),
87 }
88 }
89}
90
91pub fn parse_source(source: &str) -> Result<Vec<SNode>, PipelineError> {
93 let mut lexer = harn_lexer::Lexer::new(source);
94 let tokens = lexer.tokenize()?;
95 let mut parser = Parser::new(tokens);
96 Ok(parser.parse()?)
97}
98
99pub fn check_source(source: &str) -> Result<(Vec<SNode>, Vec<TypeDiagnostic>), PipelineError> {
102 let program = parse_source(source)?;
103 let diagnostics = TypeChecker::new().check_with_source(&program, source);
104 Ok((program, diagnostics))
105}
106
107pub fn check_source_strict(source: &str) -> Result<Vec<SNode>, PipelineError> {
109 let (program, diagnostics) = check_source(source)?;
110 for diag in &diagnostics {
111 if diag.severity == DiagnosticSeverity::Error {
112 return Err(PipelineError::TypeCheck(Box::new(diag.clone())));
113 }
114 }
115 Ok(program)
116}
117
118#[cfg(test)]
119mod pipeline_tests {
120 use super::*;
121
122 #[test]
123 fn parse_source_valid() {
124 let program = parse_source("let x = 1").unwrap();
125 assert!(!program.is_empty());
126 }
127
128 #[test]
129 fn parse_source_lex_error() {
130 let err = parse_source("let x = `").unwrap_err();
131 assert!(matches!(err, PipelineError::Lex(_)));
132 assert!(err.span().is_some());
133 assert!(err.to_string().contains("Unexpected character"));
134 }
135
136 #[test]
137 fn parse_source_parse_error() {
138 let err = parse_source("let = 1").unwrap_err();
139 assert!(matches!(err, PipelineError::Parse(_)));
140 assert!(err.span().is_some());
141 }
142
143 #[test]
144 fn check_source_returns_diagnostics() {
145 let (program, _diagnostics) = check_source("let x = 1").unwrap();
146 assert!(!program.is_empty());
147 }
148
149 #[test]
150 fn check_source_strict_passes_valid_code() {
151 let program = check_source_strict("let x = 1\nlog(x)").unwrap();
152 assert!(!program.is_empty());
153 }
154
155 #[test]
156 fn check_source_strict_catches_lex_error() {
157 let err = check_source_strict("`").unwrap_err();
158 assert!(matches!(err, PipelineError::Lex(_)));
159 }
160
161 #[test]
162 fn pipeline_error_display_is_informative() {
163 let err = parse_source("`").unwrap_err();
164 let msg = err.to_string();
165 assert!(!msg.is_empty());
166 assert!(msg.contains('`') || msg.contains("Unexpected"));
167 }
168
169 #[test]
170 fn pipeline_error_size_is_bounded() {
171 assert!(
173 std::mem::size_of::<PipelineError>() <= 96,
174 "PipelineError grew to {} bytes — consider boxing large variants",
175 std::mem::size_of::<PipelineError>()
176 );
177 }
178}