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