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