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 runtime_stack;
15pub mod stdlib_metadata;
16pub mod typechecker;
17pub mod visit;
18
19pub use ast::*;
20pub use diagnostic_codes::{
21 Category as DiagnosticCodeCategory, Code as DiagnosticCode, ParseRepairSafetyError, Repair,
22 RepairId, RepairSafety, RepairTemplate, REPAIR_REGISTRY,
23};
24pub use namespace_demand::{namespace_import_demands, NamespaceDemand};
25pub use parser::*;
26pub use runtime_stack::{PARSE_STACK_SIZE, RUNTIME_STACK_SIZE};
27pub use stdlib_metadata::{
28 parse_for_span as parse_stdlib_metadata, synthesize_example, StdlibMetadata,
29};
30pub use typechecker::{
31 block_definitely_exits, canonical_predicate_type, format_type, stmt_definitely_exits,
32 substitute_type_expr, BindingTypeInfo, DiagnosticDetails, DiagnosticSeverity, InlayHintInfo,
33 NamespaceImportBinding, PredicateModelRoute, PredicateQuestionKind, PredicateQuestionSpec,
34 PredicateSite, PredicateSiteKind, TypeCheckFacts, TypeChecker, TypeDiagnostic,
35};
36
37pub use builtin_signatures::install_builtin_manifest;
38
39pub const HARN_LEGACY_AMBIENT_CAPABILITIES_ENV: &str = "HARN_LEGACY_AMBIENT_CAPABILITIES";
47
48static LEGACY_AMBIENT_CAPABILITIES: std::sync::atomic::AtomicU8 =
57 std::sync::atomic::AtomicU8::new(0);
58
59#[cfg(test)]
60pub(crate) fn legacy_ambient_capabilities_test_lock() -> &'static std::sync::Mutex<()> {
61 static LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
62 LOCK.get_or_init(|| std::sync::Mutex::new(()))
63}
64
65pub fn legacy_ambient_capabilities_enabled() -> bool {
66 match LEGACY_AMBIENT_CAPABILITIES.load(std::sync::atomic::Ordering::Relaxed) {
67 1 => false,
68 2 => true,
69 _ => refresh_legacy_ambient_capabilities(),
70 }
71}
72
73pub fn refresh_legacy_ambient_capabilities() -> bool {
80 let enabled = std::env::var_os(HARN_LEGACY_AMBIENT_CAPABILITIES_ENV).is_some_and(|value| {
81 value.to_str().is_some_and(|value| {
82 matches!(
83 value.trim().to_ascii_lowercase().as_str(),
84 "1" | "true" | "yes" | "on"
85 )
86 })
87 });
88 LEGACY_AMBIENT_CAPABILITIES.store(
89 if enabled { 2 } else { 1 },
90 std::sync::atomic::Ordering::Relaxed,
91 );
92 enabled
93}
94
95pub fn is_registered_legacy_hostlib_name(name: &str) -> bool {
99 if name == "hostlib_enable" {
100 return true;
101 }
102 harn_builtin_meta::host_capabilities::capability_binding_for_legacy_hostlib_name(name).is_some()
103}
104
105pub fn legacy_builtin_alias_target(name: &str) -> Option<&'static str> {
109 match name {
110 "regex_replace_all" => Some("regex_replace"),
111 "task_current" => Some("runtime_context"),
112 _ => None,
113 }
114}
115
116static DECLARED_HOST_OPERATIONS: std::sync::RwLock<
124 Option<std::collections::HashSet<(String, String)>>,
125> = std::sync::RwLock::new(None);
126
127pub fn install_declared_host_operations<I, C, M>(operations: I)
133where
134 I: IntoIterator<Item = (C, M)>,
135 C: Into<String>,
136 M: Into<String>,
137{
138 let Ok(mut guard) = DECLARED_HOST_OPERATIONS.write() else {
139 return;
140 };
141 let declared = guard.get_or_insert_with(std::collections::HashSet::new);
142 for (capability, method) in operations {
143 declared.insert((capability.into(), method.into()));
144 }
145}
146
147pub fn is_declared_host_operation(capability: &str, method: &str) -> bool {
149 DECLARED_HOST_OPERATIONS
150 .read()
151 .ok()
152 .and_then(|guard| {
153 guard
154 .as_ref()
155 .map(|declared| declared.contains(&(capability.to_string(), method.to_string())))
156 })
157 .unwrap_or(false)
158}
159
160pub fn is_known_builtin(name: &str) -> bool {
162 builtin_signatures::is_builtin(name)
163}
164
165pub fn is_legacy_ambient_builtin(name: &str) -> bool {
168 legacy_ambient_capabilities_enabled() && is_known_builtin(name)
169}
170
171pub fn known_builtin_names() -> impl Iterator<Item = &'static str> {
174 builtin_signatures::iter_builtin_names()
175}
176
177pub fn known_builtin_metadata() -> impl Iterator<Item = builtin_signatures::BuiltinMetadata> {
178 builtin_signatures::iter_builtin_metadata()
179}
180
181pub fn static_signature_names() -> impl Iterator<Item = &'static str> {
186 builtin_signatures::static_signature_names()
187}
188
189#[derive(Debug)]
192#[non_exhaustive]
193pub enum PipelineError {
194 Lex(harn_lexer::LexerError),
195 Parse(ParserError),
196 TypeCheck(Box<TypeDiagnostic>),
199}
200
201impl std::fmt::Display for PipelineError {
202 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203 match self {
204 PipelineError::Lex(e) => e.fmt(f),
205 PipelineError::Parse(e) => e.fmt(f),
206 PipelineError::TypeCheck(diag) => write!(f, "type error: {}", diag.message),
207 }
208 }
209}
210
211impl std::error::Error for PipelineError {}
212
213impl From<harn_lexer::LexerError> for PipelineError {
214 fn from(e: harn_lexer::LexerError) -> Self {
215 PipelineError::Lex(e)
216 }
217}
218
219impl From<ParserError> for PipelineError {
220 fn from(e: ParserError) -> Self {
221 PipelineError::Parse(e)
222 }
223}
224
225impl PipelineError {
226 pub fn span(&self) -> Option<&harn_lexer::Span> {
228 match self {
229 PipelineError::Lex(e) => match e {
230 harn_lexer::LexerError::UnexpectedCharacter(_, span)
231 | harn_lexer::LexerError::UnterminatedString(span)
232 | harn_lexer::LexerError::IntegerLiteralOutOfRange(_, span)
233 | harn_lexer::LexerError::UnterminatedBlockComment(span) => Some(span),
234 },
235 PipelineError::Parse(e) => match e {
236 ParserError::Unexpected { span, .. } => Some(span),
237 ParserError::UnexpectedEof { span, .. } => Some(span),
238 },
239 PipelineError::TypeCheck(diag) => diag.span.as_ref(),
240 }
241 }
242}
243
244pub fn parse_source(source: &str) -> Result<Vec<SNode>, PipelineError> {
246 let mut lexer = harn_lexer::Lexer::new(source);
247 let tokens = lexer.tokenize()?;
248 let mut parser = Parser::new(tokens);
249 Ok(parser.parse()?)
250}
251
252pub fn check_source(source: &str) -> Result<(Vec<SNode>, Vec<TypeDiagnostic>), PipelineError> {
255 let program = parse_source(source)?;
256 let diagnostics = TypeChecker::new().check_with_source(&program, source);
257 Ok((program, diagnostics))
258}
259
260pub fn check_source_strict(source: &str) -> Result<Vec<SNode>, PipelineError> {
262 let (program, diagnostics) = check_source(source)?;
263 for diag in &diagnostics {
264 if diag.severity == DiagnosticSeverity::Error {
265 return Err(PipelineError::TypeCheck(Box::new(diag.clone())));
266 }
267 }
268 Ok(program)
269}
270
271#[cfg(test)]
272mod pipeline_tests {
273 use super::*;
274
275 #[test]
276 fn parse_source_valid() {
277 let program = parse_source("const x = 1").unwrap();
278 assert!(!program.is_empty());
279 }
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}