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