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