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, PredicateQuestionKind, PredicateQuestionSpec,
32 PredicateSite, PredicateSiteKind, TypeCheckFacts, TypeChecker, 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)]
190#[non_exhaustive]
191pub enum PipelineError {
192 Lex(harn_lexer::LexerError),
193 Parse(ParserError),
194 TypeCheck(Box<TypeDiagnostic>),
197}
198
199impl std::fmt::Display for PipelineError {
200 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201 match self {
202 PipelineError::Lex(e) => e.fmt(f),
203 PipelineError::Parse(e) => e.fmt(f),
204 PipelineError::TypeCheck(diag) => write!(f, "type error: {}", diag.message),
205 }
206 }
207}
208
209impl std::error::Error for PipelineError {}
210
211impl From<harn_lexer::LexerError> for PipelineError {
212 fn from(e: harn_lexer::LexerError) -> Self {
213 PipelineError::Lex(e)
214 }
215}
216
217impl From<ParserError> for PipelineError {
218 fn from(e: ParserError) -> Self {
219 PipelineError::Parse(e)
220 }
221}
222
223impl PipelineError {
224 pub fn span(&self) -> Option<&harn_lexer::Span> {
226 match self {
227 PipelineError::Lex(e) => match e {
228 harn_lexer::LexerError::UnexpectedCharacter(_, span)
229 | harn_lexer::LexerError::UnterminatedString(span)
230 | harn_lexer::LexerError::IntegerLiteralOutOfRange(_, span)
231 | harn_lexer::LexerError::UnterminatedBlockComment(span) => Some(span),
232 },
233 PipelineError::Parse(e) => match e {
234 ParserError::Unexpected { span, .. } => Some(span),
235 ParserError::UnexpectedEof { span, .. } => Some(span),
236 },
237 PipelineError::TypeCheck(diag) => diag.span.as_ref(),
238 }
239 }
240}
241
242pub fn parse_source(source: &str) -> Result<Vec<SNode>, PipelineError> {
244 let mut lexer = harn_lexer::Lexer::new(source);
245 let tokens = lexer.tokenize()?;
246 let mut parser = Parser::new(tokens);
247 Ok(parser.parse()?)
248}
249
250pub fn check_source(source: &str) -> Result<(Vec<SNode>, Vec<TypeDiagnostic>), PipelineError> {
253 let program = parse_source(source)?;
254 let diagnostics = TypeChecker::new().check_with_source(&program, source);
255 Ok((program, diagnostics))
256}
257
258pub fn check_source_strict(source: &str) -> Result<Vec<SNode>, PipelineError> {
260 let (program, diagnostics) = check_source(source)?;
261 for diag in &diagnostics {
262 if diag.severity == DiagnosticSeverity::Error {
263 return Err(PipelineError::TypeCheck(Box::new(diag.clone())));
264 }
265 }
266 Ok(program)
267}
268
269#[cfg(test)]
270mod pipeline_tests {
271 use super::*;
272
273 #[test]
274 fn parse_source_valid() {
275 let program = parse_source("const x = 1").unwrap();
276 assert!(!program.is_empty());
277 }
278
279 #[test]
280 fn parse_source_lex_error() {
281 let err = parse_source("let x = `").unwrap_err();
282 assert!(matches!(err, PipelineError::Lex(_)));
283 assert!(err.span().is_some());
284 assert!(err.to_string().contains("Unexpected character"));
285 }
286
287 #[test]
288 fn parse_source_parse_error() {
289 let err = parse_source("let = 1").unwrap_err();
290 assert!(matches!(err, PipelineError::Parse(_)));
291 assert!(err.span().is_some());
292 }
293
294 #[test]
295 fn check_source_returns_diagnostics() {
296 let (program, _diagnostics) = check_source("const x = 1").unwrap();
297 assert!(!program.is_empty());
298 }
299
300 #[test]
301 fn check_source_strict_passes_valid_code() {
302 let program = check_source_strict("const x = 1\nlog(x)").unwrap();
303 assert!(!program.is_empty());
304 }
305
306 #[test]
307 fn check_source_strict_catches_lex_error() {
308 let err = check_source_strict("`").unwrap_err();
309 assert!(matches!(err, PipelineError::Lex(_)));
310 }
311
312 #[test]
313 fn pipeline_error_display_is_informative() {
314 let err = parse_source("`").unwrap_err();
315 let msg = err.to_string();
316 assert!(!msg.is_empty());
317 assert!(msg.contains('`') || msg.contains("Unexpected"));
318 }
319
320 #[test]
321 fn pipeline_error_size_is_bounded() {
322 assert!(
324 std::mem::size_of::<PipelineError>() <= 96,
325 "PipelineError grew to {} bytes — consider boxing large variants",
326 std::mem::size_of::<PipelineError>()
327 );
328 }
329
330 #[test]
331 fn legacy_hostlib_names_must_resolve_through_the_typed_registry() {
332 assert!(is_registered_legacy_hostlib_name(
333 "hostlib_terminal_session_capture"
334 ));
335 assert!(is_registered_legacy_hostlib_name(
336 "hostlib_code_index_agent_heartbeat"
337 ));
338 assert!(is_registered_legacy_hostlib_name("hostlib_enable"));
339 assert!(!is_registered_legacy_hostlib_name(
340 "hostlib_terminal_session_not_registered"
341 ));
342 assert!(!is_registered_legacy_hostlib_name("hostlib_unknown_ping"));
343 }
344}