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 interpolation;
10pub mod lexical;
11mod namespace_demand;
12pub mod param_annotations;
13mod parser;
14pub mod stdlib_metadata;
15pub mod typechecker;
16pub mod visit;
17
18pub use ast::*;
19pub use diagnostic_codes::{
20 Category as DiagnosticCodeCategory, Code as DiagnosticCode, ParseRepairSafetyError, Repair,
21 RepairId, RepairSafety, RepairTemplate, REPAIR_REGISTRY,
22};
23pub use namespace_demand::{namespace_import_demands, NamespaceDemand};
24pub use parser::*;
25pub use stdlib_metadata::{
26 parse_for_span as parse_stdlib_metadata, synthesize_example, StdlibMetadata,
27};
28pub use typechecker::{
29 block_definitely_exits, canonical_predicate_type, format_type, stmt_definitely_exits,
30 substitute_type_expr, BindingTypeInfo, DiagnosticDetails, DiagnosticSeverity, InlayHintInfo,
31 NamespaceImportBinding, PredicateModelRoute, PredicateSite, TypeCheckFacts, TypeChecker,
32 TypeDiagnostic,
33};
34
35pub use builtin_signatures::install_builtin_manifest;
36
37pub const HARN_LEGACY_AMBIENT_CAPABILITIES_ENV: &str = "HARN_LEGACY_AMBIENT_CAPABILITIES";
45
46static LEGACY_AMBIENT_CAPABILITIES: std::sync::atomic::AtomicU8 =
55 std::sync::atomic::AtomicU8::new(0);
56
57#[cfg(test)]
58pub(crate) fn legacy_ambient_capabilities_test_lock() -> &'static std::sync::Mutex<()> {
59 static LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
60 LOCK.get_or_init(|| std::sync::Mutex::new(()))
61}
62
63pub fn legacy_ambient_capabilities_enabled() -> bool {
64 match LEGACY_AMBIENT_CAPABILITIES.load(std::sync::atomic::Ordering::Relaxed) {
65 1 => false,
66 2 => true,
67 _ => refresh_legacy_ambient_capabilities(),
68 }
69}
70
71pub fn refresh_legacy_ambient_capabilities() -> bool {
78 let enabled = std::env::var_os(HARN_LEGACY_AMBIENT_CAPABILITIES_ENV).is_some_and(|value| {
79 value.to_str().is_some_and(|value| {
80 matches!(
81 value.trim().to_ascii_lowercase().as_str(),
82 "1" | "true" | "yes" | "on"
83 )
84 })
85 });
86 LEGACY_AMBIENT_CAPABILITIES.store(
87 if enabled { 2 } else { 1 },
88 std::sync::atomic::Ordering::Relaxed,
89 );
90 enabled
91}
92
93pub fn is_registered_legacy_hostlib_name(name: &str) -> bool {
97 if name == "hostlib_enable" {
98 return true;
99 }
100 harn_builtin_meta::host_capabilities::capability_binding_for_legacy_hostlib_name(name).is_some()
101}
102
103pub fn legacy_builtin_alias_target(name: &str) -> Option<&'static str> {
107 match name {
108 "regex_replace_all" => Some("regex_replace"),
109 "task_current" => Some("runtime_context"),
110 _ => None,
111 }
112}
113
114static DECLARED_HOST_OPERATIONS: std::sync::RwLock<
122 Option<std::collections::HashSet<(String, String)>>,
123> = std::sync::RwLock::new(None);
124
125pub fn install_declared_host_operations<I, C, M>(operations: I)
131where
132 I: IntoIterator<Item = (C, M)>,
133 C: Into<String>,
134 M: Into<String>,
135{
136 let Ok(mut guard) = DECLARED_HOST_OPERATIONS.write() else {
137 return;
138 };
139 let declared = guard.get_or_insert_with(std::collections::HashSet::new);
140 for (capability, method) in operations {
141 declared.insert((capability.into(), method.into()));
142 }
143}
144
145pub fn is_declared_host_operation(capability: &str, method: &str) -> bool {
147 DECLARED_HOST_OPERATIONS
148 .read()
149 .ok()
150 .and_then(|guard| {
151 guard
152 .as_ref()
153 .map(|declared| declared.contains(&(capability.to_string(), method.to_string())))
154 })
155 .unwrap_or(false)
156}
157
158pub fn is_known_builtin(name: &str) -> bool {
160 builtin_signatures::is_builtin(name)
161}
162
163pub fn is_legacy_ambient_builtin(name: &str) -> bool {
166 legacy_ambient_capabilities_enabled() && is_known_builtin(name)
167}
168
169pub fn known_builtin_names() -> impl Iterator<Item = &'static str> {
172 builtin_signatures::iter_builtin_names()
173}
174
175pub fn known_builtin_metadata() -> impl Iterator<Item = builtin_signatures::BuiltinMetadata> {
176 builtin_signatures::iter_builtin_metadata()
177}
178
179pub fn static_signature_names() -> impl Iterator<Item = &'static str> {
184 builtin_signatures::static_signature_names()
185}
186
187#[derive(Debug)]
190pub enum PipelineError {
191 Lex(harn_lexer::LexerError),
192 Parse(ParserError),
193 TypeCheck(Box<TypeDiagnostic>),
196}
197
198impl std::fmt::Display for PipelineError {
199 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200 match self {
201 PipelineError::Lex(e) => e.fmt(f),
202 PipelineError::Parse(e) => e.fmt(f),
203 PipelineError::TypeCheck(diag) => write!(f, "type error: {}", diag.message),
204 }
205 }
206}
207
208impl std::error::Error for PipelineError {}
209
210impl From<harn_lexer::LexerError> for PipelineError {
211 fn from(e: harn_lexer::LexerError) -> Self {
212 PipelineError::Lex(e)
213 }
214}
215
216impl From<ParserError> for PipelineError {
217 fn from(e: ParserError) -> Self {
218 PipelineError::Parse(e)
219 }
220}
221
222impl PipelineError {
223 pub fn span(&self) -> Option<&harn_lexer::Span> {
225 match self {
226 PipelineError::Lex(e) => match e {
227 harn_lexer::LexerError::UnexpectedCharacter(_, span)
228 | harn_lexer::LexerError::UnterminatedString(span)
229 | harn_lexer::LexerError::IntegerLiteralOutOfRange(_, span)
230 | harn_lexer::LexerError::UnterminatedBlockComment(span) => Some(span),
231 },
232 PipelineError::Parse(e) => match e {
233 ParserError::Unexpected { span, .. } => Some(span),
234 ParserError::UnexpectedEof { span, .. } => Some(span),
235 },
236 PipelineError::TypeCheck(diag) => diag.span.as_ref(),
237 }
238 }
239}
240
241pub fn parse_source(source: &str) -> Result<Vec<SNode>, PipelineError> {
243 let mut lexer = harn_lexer::Lexer::new(source);
244 let tokens = lexer.tokenize()?;
245 let mut parser = Parser::new(tokens);
246 Ok(parser.parse()?)
247}
248
249pub fn check_source(source: &str) -> Result<(Vec<SNode>, Vec<TypeDiagnostic>), PipelineError> {
252 let program = parse_source(source)?;
253 let diagnostics = TypeChecker::new().check_with_source(&program, source);
254 Ok((program, diagnostics))
255}
256
257pub fn check_source_strict(source: &str) -> Result<Vec<SNode>, PipelineError> {
259 let (program, diagnostics) = check_source(source)?;
260 for diag in &diagnostics {
261 if diag.severity == DiagnosticSeverity::Error {
262 return Err(PipelineError::TypeCheck(Box::new(diag.clone())));
263 }
264 }
265 Ok(program)
266}
267
268#[cfg(test)]
269mod pipeline_tests {
270 use super::*;
271
272 #[test]
273 fn parse_source_valid() {
274 let program = parse_source("const x = 1").unwrap();
275 assert!(!program.is_empty());
276 }
277
278 #[test]
279 fn parse_source_lex_error() {
280 let err = parse_source("let x = `").unwrap_err();
281 assert!(matches!(err, PipelineError::Lex(_)));
282 assert!(err.span().is_some());
283 assert!(err.to_string().contains("Unexpected character"));
284 }
285
286 #[test]
287 fn parse_source_parse_error() {
288 let err = parse_source("let = 1").unwrap_err();
289 assert!(matches!(err, PipelineError::Parse(_)));
290 assert!(err.span().is_some());
291 }
292
293 #[test]
294 fn check_source_returns_diagnostics() {
295 let (program, _diagnostics) = check_source("const x = 1").unwrap();
296 assert!(!program.is_empty());
297 }
298
299 #[test]
300 fn check_source_strict_passes_valid_code() {
301 let program = check_source_strict("const x = 1\nlog(x)").unwrap();
302 assert!(!program.is_empty());
303 }
304
305 #[test]
306 fn check_source_strict_catches_lex_error() {
307 let err = check_source_strict("`").unwrap_err();
308 assert!(matches!(err, PipelineError::Lex(_)));
309 }
310
311 #[test]
312 fn pipeline_error_display_is_informative() {
313 let err = parse_source("`").unwrap_err();
314 let msg = err.to_string();
315 assert!(!msg.is_empty());
316 assert!(msg.contains('`') || msg.contains("Unexpected"));
317 }
318
319 #[test]
320 fn pipeline_error_size_is_bounded() {
321 assert!(
323 std::mem::size_of::<PipelineError>() <= 96,
324 "PipelineError grew to {} bytes — consider boxing large variants",
325 std::mem::size_of::<PipelineError>()
326 );
327 }
328
329 #[test]
330 fn legacy_hostlib_names_must_resolve_through_the_typed_registry() {
331 assert!(is_registered_legacy_hostlib_name(
332 "hostlib_terminal_session_capture"
333 ));
334 assert!(is_registered_legacy_hostlib_name(
335 "hostlib_code_index_agent_heartbeat"
336 ));
337 assert!(is_registered_legacy_hostlib_name("hostlib_enable"));
338 assert!(!is_registered_legacy_hostlib_name(
339 "hostlib_terminal_session_not_registered"
340 ));
341 assert!(!is_registered_legacy_hostlib_name("hostlib_unknown_ping"));
342 }
343}