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