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;
12mod parser;
13pub mod stdlib_metadata;
14pub mod typechecker;
15pub mod visit;
16
17pub use ast::*;
18pub use diagnostic_codes::{
19 Category as DiagnosticCodeCategory, Code as DiagnosticCode, ParseRepairSafetyError, Repair,
20 RepairId, RepairSafety, RepairTemplate, REPAIR_REGISTRY,
21};
22pub use namespace_demand::{namespace_import_demands, NamespaceDemand};
23pub use parser::*;
24pub use stdlib_metadata::{
25 parse_for_span as parse_stdlib_metadata, synthesize_example, StdlibMetadata,
26};
27pub use typechecker::{
28 block_definitely_exits, format_type, stmt_definitely_exits, substitute_type_expr,
29 BindingTypeInfo, DiagnosticDetails, DiagnosticSeverity, InlayHintInfo, NamespaceImportBinding,
30 TypeCheckFacts, TypeChecker, TypeDiagnostic,
31};
32
33pub use builtin_signatures::install_builtin_manifest;
34
35pub const HARN_LEGACY_AMBIENT_CAPABILITIES_ENV: &str = "HARN_LEGACY_AMBIENT_CAPABILITIES";
43
44static LEGACY_AMBIENT_CAPABILITIES: std::sync::atomic::AtomicU8 =
53 std::sync::atomic::AtomicU8::new(0);
54
55pub fn legacy_ambient_capabilities_enabled() -> bool {
56 match LEGACY_AMBIENT_CAPABILITIES.load(std::sync::atomic::Ordering::Relaxed) {
57 1 => false,
58 2 => true,
59 _ => refresh_legacy_ambient_capabilities(),
60 }
61}
62
63pub fn refresh_legacy_ambient_capabilities() -> bool {
70 let enabled = std::env::var_os(HARN_LEGACY_AMBIENT_CAPABILITIES_ENV).is_some_and(|value| {
71 value.to_str().is_some_and(|value| {
72 matches!(
73 value.trim().to_ascii_lowercase().as_str(),
74 "1" | "true" | "yes" | "on"
75 )
76 })
77 });
78 LEGACY_AMBIENT_CAPABILITIES.store(
79 if enabled { 2 } else { 1 },
80 std::sync::atomic::Ordering::Relaxed,
81 );
82 enabled
83}
84
85pub fn is_registered_legacy_hostlib_name(name: &str) -> bool {
89 if name == "hostlib_enable" {
90 return true;
91 }
92 harn_builtin_meta::host_capabilities::capability_binding_for_legacy_hostlib_name(name).is_some()
93}
94
95pub fn legacy_builtin_alias_target(name: &str) -> Option<&'static str> {
99 match name {
100 "regex_replace_all" => Some("regex_replace"),
101 "task_current" => Some("runtime_context"),
102 _ => None,
103 }
104}
105
106static DECLARED_HOST_OPERATIONS: std::sync::RwLock<
114 Option<std::collections::HashSet<(String, String)>>,
115> = std::sync::RwLock::new(None);
116
117pub fn install_declared_host_operations<I, C, M>(operations: I)
123where
124 I: IntoIterator<Item = (C, M)>,
125 C: Into<String>,
126 M: Into<String>,
127{
128 let Ok(mut guard) = DECLARED_HOST_OPERATIONS.write() else {
129 return;
130 };
131 let declared = guard.get_or_insert_with(std::collections::HashSet::new);
132 for (capability, method) in operations {
133 declared.insert((capability.into(), method.into()));
134 }
135}
136
137pub fn is_declared_host_operation(capability: &str, method: &str) -> bool {
139 DECLARED_HOST_OPERATIONS
140 .read()
141 .ok()
142 .and_then(|guard| {
143 guard
144 .as_ref()
145 .map(|declared| declared.contains(&(capability.to_string(), method.to_string())))
146 })
147 .unwrap_or(false)
148}
149
150pub fn is_known_builtin(name: &str) -> bool {
152 builtin_signatures::is_builtin(name)
153}
154
155pub fn is_legacy_ambient_builtin(name: &str) -> bool {
158 legacy_ambient_capabilities_enabled() && is_known_builtin(name)
159}
160
161pub fn known_builtin_names() -> impl Iterator<Item = &'static str> {
164 builtin_signatures::iter_builtin_names()
165}
166
167pub fn known_builtin_metadata() -> impl Iterator<Item = builtin_signatures::BuiltinMetadata> {
168 builtin_signatures::iter_builtin_metadata()
169}
170
171pub fn static_signature_names() -> impl Iterator<Item = &'static str> {
176 builtin_signatures::static_signature_names()
177}
178
179#[derive(Debug)]
182pub enum PipelineError {
183 Lex(harn_lexer::LexerError),
184 Parse(ParserError),
185 TypeCheck(Box<TypeDiagnostic>),
188}
189
190impl std::fmt::Display for PipelineError {
191 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192 match self {
193 PipelineError::Lex(e) => e.fmt(f),
194 PipelineError::Parse(e) => e.fmt(f),
195 PipelineError::TypeCheck(diag) => write!(f, "type error: {}", diag.message),
196 }
197 }
198}
199
200impl std::error::Error for PipelineError {}
201
202impl From<harn_lexer::LexerError> for PipelineError {
203 fn from(e: harn_lexer::LexerError) -> Self {
204 PipelineError::Lex(e)
205 }
206}
207
208impl From<ParserError> for PipelineError {
209 fn from(e: ParserError) -> Self {
210 PipelineError::Parse(e)
211 }
212}
213
214impl PipelineError {
215 pub fn span(&self) -> Option<&harn_lexer::Span> {
217 match self {
218 PipelineError::Lex(e) => match e {
219 harn_lexer::LexerError::UnexpectedCharacter(_, span)
220 | harn_lexer::LexerError::UnterminatedString(span)
221 | harn_lexer::LexerError::IntegerLiteralOutOfRange(_, span)
222 | harn_lexer::LexerError::UnterminatedBlockComment(span) => Some(span),
223 },
224 PipelineError::Parse(e) => match e {
225 ParserError::Unexpected { span, .. } => Some(span),
226 ParserError::UnexpectedEof { span, .. } => Some(span),
227 },
228 PipelineError::TypeCheck(diag) => diag.span.as_ref(),
229 }
230 }
231}
232
233pub fn parse_source(source: &str) -> Result<Vec<SNode>, PipelineError> {
235 let mut lexer = harn_lexer::Lexer::new(source);
236 let tokens = lexer.tokenize()?;
237 let mut parser = Parser::new(tokens);
238 Ok(parser.parse()?)
239}
240
241pub fn check_source(source: &str) -> Result<(Vec<SNode>, Vec<TypeDiagnostic>), PipelineError> {
244 let program = parse_source(source)?;
245 let diagnostics = TypeChecker::new().check_with_source(&program, source);
246 Ok((program, diagnostics))
247}
248
249pub fn check_source_strict(source: &str) -> Result<Vec<SNode>, PipelineError> {
251 let (program, diagnostics) = check_source(source)?;
252 for diag in &diagnostics {
253 if diag.severity == DiagnosticSeverity::Error {
254 return Err(PipelineError::TypeCheck(Box::new(diag.clone())));
255 }
256 }
257 Ok(program)
258}
259
260#[cfg(test)]
261mod pipeline_tests {
262 use super::*;
263
264 #[test]
265 fn parse_source_valid() {
266 let program = parse_source("const x = 1").unwrap();
267 assert!(!program.is_empty());
268 }
269
270 #[test]
271 fn parse_source_lex_error() {
272 let err = parse_source("let x = `").unwrap_err();
273 assert!(matches!(err, PipelineError::Lex(_)));
274 assert!(err.span().is_some());
275 assert!(err.to_string().contains("Unexpected character"));
276 }
277
278 #[test]
279 fn parse_source_parse_error() {
280 let err = parse_source("let = 1").unwrap_err();
281 assert!(matches!(err, PipelineError::Parse(_)));
282 assert!(err.span().is_some());
283 }
284
285 #[test]
286 fn check_source_returns_diagnostics() {
287 let (program, _diagnostics) = check_source("const x = 1").unwrap();
288 assert!(!program.is_empty());
289 }
290
291 #[test]
292 fn check_source_strict_passes_valid_code() {
293 let program = check_source_strict("const x = 1\nlog(x)").unwrap();
294 assert!(!program.is_empty());
295 }
296
297 #[test]
298 fn check_source_strict_catches_lex_error() {
299 let err = check_source_strict("`").unwrap_err();
300 assert!(matches!(err, PipelineError::Lex(_)));
301 }
302
303 #[test]
304 fn pipeline_error_display_is_informative() {
305 let err = parse_source("`").unwrap_err();
306 let msg = err.to_string();
307 assert!(!msg.is_empty());
308 assert!(msg.contains('`') || msg.contains("Unexpected"));
309 }
310
311 #[test]
312 fn pipeline_error_size_is_bounded() {
313 assert!(
315 std::mem::size_of::<PipelineError>() <= 96,
316 "PipelineError grew to {} bytes — consider boxing large variants",
317 std::mem::size_of::<PipelineError>()
318 );
319 }
320
321 #[test]
322 fn legacy_hostlib_names_must_resolve_through_the_typed_registry() {
323 assert!(is_registered_legacy_hostlib_name(
324 "hostlib_terminal_session_capture"
325 ));
326 assert!(is_registered_legacy_hostlib_name(
327 "hostlib_code_index_agent_heartbeat"
328 ));
329 assert!(is_registered_legacy_hostlib_name("hostlib_enable"));
330 assert!(!is_registered_legacy_hostlib_name(
331 "hostlib_terminal_session_not_registered"
332 ));
333 assert!(!is_registered_legacy_hostlib_name("hostlib_unknown_ping"));
334 }
335}