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