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 lexical;
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, substitute_type_expr,
26 DiagnosticDetails, DiagnosticSeverity, InlayHintInfo, NamespaceImportBinding, TypeChecker,
27 TypeDiagnostic,
28};
29
30pub use builtin_signatures::install_builtin_manifest;
31
32pub const HARN_LEGACY_AMBIENT_CAPABILITIES_ENV: &str = "HARN_LEGACY_AMBIENT_CAPABILITIES";
39
40pub fn legacy_ambient_capabilities_enabled() -> bool {
41 std::env::var(HARN_LEGACY_AMBIENT_CAPABILITIES_ENV).is_ok_and(|value| {
42 matches!(
43 value.trim().to_ascii_lowercase().as_str(),
44 "1" | "true" | "yes" | "on"
45 )
46 })
47}
48
49pub fn is_registered_legacy_hostlib_name(name: &str) -> bool {
53 if name == "hostlib_enable" {
54 return true;
55 }
56 harn_builtin_meta::host_capabilities::capability_binding_for_legacy_hostlib_name(name).is_some()
57}
58
59pub fn legacy_builtin_alias_target(name: &str) -> Option<&'static str> {
63 match name {
64 "regex_replace_all" => Some("regex_replace"),
65 _ => None,
66 }
67}
68
69pub fn is_known_builtin(name: &str) -> bool {
71 builtin_signatures::is_builtin(name)
72}
73
74pub fn is_legacy_ambient_builtin(name: &str) -> bool {
77 legacy_ambient_capabilities_enabled() && is_known_builtin(name)
78}
79
80pub fn known_builtin_names() -> impl Iterator<Item = &'static str> {
83 builtin_signatures::iter_builtin_names()
84}
85
86pub fn known_builtin_metadata() -> impl Iterator<Item = builtin_signatures::BuiltinMetadata> {
87 builtin_signatures::iter_builtin_metadata()
88}
89
90pub fn static_signature_names() -> impl Iterator<Item = &'static str> {
95 builtin_signatures::static_signature_names()
96}
97
98#[derive(Debug)]
101pub enum PipelineError {
102 Lex(harn_lexer::LexerError),
103 Parse(ParserError),
104 TypeCheck(Box<TypeDiagnostic>),
107}
108
109impl std::fmt::Display for PipelineError {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 match self {
112 PipelineError::Lex(e) => e.fmt(f),
113 PipelineError::Parse(e) => e.fmt(f),
114 PipelineError::TypeCheck(diag) => write!(f, "type error: {}", diag.message),
115 }
116 }
117}
118
119impl std::error::Error for PipelineError {}
120
121impl From<harn_lexer::LexerError> for PipelineError {
122 fn from(e: harn_lexer::LexerError) -> Self {
123 PipelineError::Lex(e)
124 }
125}
126
127impl From<ParserError> for PipelineError {
128 fn from(e: ParserError) -> Self {
129 PipelineError::Parse(e)
130 }
131}
132
133impl PipelineError {
134 pub fn span(&self) -> Option<&harn_lexer::Span> {
136 match self {
137 PipelineError::Lex(e) => match e {
138 harn_lexer::LexerError::UnexpectedCharacter(_, span)
139 | harn_lexer::LexerError::UnterminatedString(span)
140 | harn_lexer::LexerError::IntegerLiteralOutOfRange(_, span)
141 | harn_lexer::LexerError::UnterminatedBlockComment(span) => Some(span),
142 },
143 PipelineError::Parse(e) => match e {
144 ParserError::Unexpected { span, .. } => Some(span),
145 ParserError::UnexpectedEof { span, .. } => Some(span),
146 },
147 PipelineError::TypeCheck(diag) => diag.span.as_ref(),
148 }
149 }
150}
151
152pub fn parse_source(source: &str) -> Result<Vec<SNode>, PipelineError> {
154 let mut lexer = harn_lexer::Lexer::new(source);
155 let tokens = lexer.tokenize()?;
156 let mut parser = Parser::new(tokens);
157 Ok(parser.parse()?)
158}
159
160pub fn check_source(source: &str) -> Result<(Vec<SNode>, Vec<TypeDiagnostic>), PipelineError> {
163 let program = parse_source(source)?;
164 let diagnostics = TypeChecker::new().check_with_source(&program, source);
165 Ok((program, diagnostics))
166}
167
168pub fn check_source_strict(source: &str) -> Result<Vec<SNode>, PipelineError> {
170 let (program, diagnostics) = check_source(source)?;
171 for diag in &diagnostics {
172 if diag.severity == DiagnosticSeverity::Error {
173 return Err(PipelineError::TypeCheck(Box::new(diag.clone())));
174 }
175 }
176 Ok(program)
177}
178
179#[cfg(test)]
180mod pipeline_tests {
181 use super::*;
182
183 #[test]
184 fn parse_source_valid() {
185 let program = parse_source("const x = 1").unwrap();
186 assert!(!program.is_empty());
187 }
188
189 #[test]
190 fn parse_source_lex_error() {
191 let err = parse_source("let x = `").unwrap_err();
192 assert!(matches!(err, PipelineError::Lex(_)));
193 assert!(err.span().is_some());
194 assert!(err.to_string().contains("Unexpected character"));
195 }
196
197 #[test]
198 fn parse_source_parse_error() {
199 let err = parse_source("let = 1").unwrap_err();
200 assert!(matches!(err, PipelineError::Parse(_)));
201 assert!(err.span().is_some());
202 }
203
204 #[test]
205 fn check_source_returns_diagnostics() {
206 let (program, _diagnostics) = check_source("const x = 1").unwrap();
207 assert!(!program.is_empty());
208 }
209
210 #[test]
211 fn check_source_strict_passes_valid_code() {
212 let program = check_source_strict("const x = 1\nlog(x)").unwrap();
213 assert!(!program.is_empty());
214 }
215
216 #[test]
217 fn check_source_strict_catches_lex_error() {
218 let err = check_source_strict("`").unwrap_err();
219 assert!(matches!(err, PipelineError::Lex(_)));
220 }
221
222 #[test]
223 fn pipeline_error_display_is_informative() {
224 let err = parse_source("`").unwrap_err();
225 let msg = err.to_string();
226 assert!(!msg.is_empty());
227 assert!(msg.contains('`') || msg.contains("Unexpected"));
228 }
229
230 #[test]
231 fn pipeline_error_size_is_bounded() {
232 assert!(
234 std::mem::size_of::<PipelineError>() <= 96,
235 "PipelineError grew to {} bytes — consider boxing large variants",
236 std::mem::size_of::<PipelineError>()
237 );
238 }
239
240 #[test]
241 fn legacy_hostlib_names_must_resolve_through_the_typed_registry() {
242 assert!(is_registered_legacy_hostlib_name(
243 "hostlib_terminal_session_capture"
244 ));
245 assert!(is_registered_legacy_hostlib_name(
246 "hostlib_code_index_agent_heartbeat"
247 ));
248 assert!(is_registered_legacy_hostlib_name("hostlib_enable"));
249 assert!(!is_registered_legacy_hostlib_name(
250 "hostlib_terminal_session_not_registered"
251 ));
252 assert!(!is_registered_legacy_hostlib_name("hostlib_unknown_ping"));
253 }
254}