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
44pub fn legacy_ambient_capabilities_enabled() -> bool {
45 std::env::var(HARN_LEGACY_AMBIENT_CAPABILITIES_ENV).is_ok_and(|value| {
46 matches!(
47 value.trim().to_ascii_lowercase().as_str(),
48 "1" | "true" | "yes" | "on"
49 )
50 })
51}
52
53pub fn is_registered_legacy_hostlib_name(name: &str) -> bool {
57 if name == "hostlib_enable" {
58 return true;
59 }
60 harn_builtin_meta::host_capabilities::capability_binding_for_legacy_hostlib_name(name).is_some()
61}
62
63pub fn legacy_builtin_alias_target(name: &str) -> Option<&'static str> {
67 match name {
68 "regex_replace_all" => Some("regex_replace"),
69 "task_current" => Some("runtime_context"),
70 _ => None,
71 }
72}
73
74static DECLARED_HOST_OPERATIONS: std::sync::RwLock<
82 Option<std::collections::HashSet<(String, String)>>,
83> = std::sync::RwLock::new(None);
84
85pub fn install_declared_host_operations<I, C, M>(operations: I)
91where
92 I: IntoIterator<Item = (C, M)>,
93 C: Into<String>,
94 M: Into<String>,
95{
96 let Ok(mut guard) = DECLARED_HOST_OPERATIONS.write() else {
97 return;
98 };
99 let declared = guard.get_or_insert_with(std::collections::HashSet::new);
100 for (capability, method) in operations {
101 declared.insert((capability.into(), method.into()));
102 }
103}
104
105pub fn is_declared_host_operation(capability: &str, method: &str) -> bool {
107 DECLARED_HOST_OPERATIONS
108 .read()
109 .ok()
110 .and_then(|guard| {
111 guard
112 .as_ref()
113 .map(|declared| declared.contains(&(capability.to_string(), method.to_string())))
114 })
115 .unwrap_or(false)
116}
117
118pub fn is_known_builtin(name: &str) -> bool {
120 builtin_signatures::is_builtin(name)
121}
122
123pub fn is_legacy_ambient_builtin(name: &str) -> bool {
126 legacy_ambient_capabilities_enabled() && is_known_builtin(name)
127}
128
129pub fn known_builtin_names() -> impl Iterator<Item = &'static str> {
132 builtin_signatures::iter_builtin_names()
133}
134
135pub fn known_builtin_metadata() -> impl Iterator<Item = builtin_signatures::BuiltinMetadata> {
136 builtin_signatures::iter_builtin_metadata()
137}
138
139pub fn static_signature_names() -> impl Iterator<Item = &'static str> {
144 builtin_signatures::static_signature_names()
145}
146
147#[derive(Debug)]
150pub enum PipelineError {
151 Lex(harn_lexer::LexerError),
152 Parse(ParserError),
153 TypeCheck(Box<TypeDiagnostic>),
156}
157
158impl std::fmt::Display for PipelineError {
159 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160 match self {
161 PipelineError::Lex(e) => e.fmt(f),
162 PipelineError::Parse(e) => e.fmt(f),
163 PipelineError::TypeCheck(diag) => write!(f, "type error: {}", diag.message),
164 }
165 }
166}
167
168impl std::error::Error for PipelineError {}
169
170impl From<harn_lexer::LexerError> for PipelineError {
171 fn from(e: harn_lexer::LexerError) -> Self {
172 PipelineError::Lex(e)
173 }
174}
175
176impl From<ParserError> for PipelineError {
177 fn from(e: ParserError) -> Self {
178 PipelineError::Parse(e)
179 }
180}
181
182impl PipelineError {
183 pub fn span(&self) -> Option<&harn_lexer::Span> {
185 match self {
186 PipelineError::Lex(e) => match e {
187 harn_lexer::LexerError::UnexpectedCharacter(_, span)
188 | harn_lexer::LexerError::UnterminatedString(span)
189 | harn_lexer::LexerError::IntegerLiteralOutOfRange(_, span)
190 | harn_lexer::LexerError::UnterminatedBlockComment(span) => Some(span),
191 },
192 PipelineError::Parse(e) => match e {
193 ParserError::Unexpected { span, .. } => Some(span),
194 ParserError::UnexpectedEof { span, .. } => Some(span),
195 },
196 PipelineError::TypeCheck(diag) => diag.span.as_ref(),
197 }
198 }
199}
200
201pub fn parse_source(source: &str) -> Result<Vec<SNode>, PipelineError> {
203 let mut lexer = harn_lexer::Lexer::new(source);
204 let tokens = lexer.tokenize()?;
205 let mut parser = Parser::new(tokens);
206 Ok(parser.parse()?)
207}
208
209pub fn check_source(source: &str) -> Result<(Vec<SNode>, Vec<TypeDiagnostic>), PipelineError> {
212 let program = parse_source(source)?;
213 let diagnostics = TypeChecker::new().check_with_source(&program, source);
214 Ok((program, diagnostics))
215}
216
217pub fn check_source_strict(source: &str) -> Result<Vec<SNode>, PipelineError> {
219 let (program, diagnostics) = check_source(source)?;
220 for diag in &diagnostics {
221 if diag.severity == DiagnosticSeverity::Error {
222 return Err(PipelineError::TypeCheck(Box::new(diag.clone())));
223 }
224 }
225 Ok(program)
226}
227
228#[cfg(test)]
229mod pipeline_tests {
230 use super::*;
231
232 #[test]
233 fn parse_source_valid() {
234 let program = parse_source("const x = 1").unwrap();
235 assert!(!program.is_empty());
236 }
237
238 #[test]
239 fn parse_source_lex_error() {
240 let err = parse_source("let x = `").unwrap_err();
241 assert!(matches!(err, PipelineError::Lex(_)));
242 assert!(err.span().is_some());
243 assert!(err.to_string().contains("Unexpected character"));
244 }
245
246 #[test]
247 fn parse_source_parse_error() {
248 let err = parse_source("let = 1").unwrap_err();
249 assert!(matches!(err, PipelineError::Parse(_)));
250 assert!(err.span().is_some());
251 }
252
253 #[test]
254 fn check_source_returns_diagnostics() {
255 let (program, _diagnostics) = check_source("const x = 1").unwrap();
256 assert!(!program.is_empty());
257 }
258
259 #[test]
260 fn check_source_strict_passes_valid_code() {
261 let program = check_source_strict("const x = 1\nlog(x)").unwrap();
262 assert!(!program.is_empty());
263 }
264
265 #[test]
266 fn check_source_strict_catches_lex_error() {
267 let err = check_source_strict("`").unwrap_err();
268 assert!(matches!(err, PipelineError::Lex(_)));
269 }
270
271 #[test]
272 fn pipeline_error_display_is_informative() {
273 let err = parse_source("`").unwrap_err();
274 let msg = err.to_string();
275 assert!(!msg.is_empty());
276 assert!(msg.contains('`') || msg.contains("Unexpected"));
277 }
278
279 #[test]
280 fn pipeline_error_size_is_bounded() {
281 assert!(
283 std::mem::size_of::<PipelineError>() <= 96,
284 "PipelineError grew to {} bytes — consider boxing large variants",
285 std::mem::size_of::<PipelineError>()
286 );
287 }
288
289 #[test]
290 fn legacy_hostlib_names_must_resolve_through_the_typed_registry() {
291 assert!(is_registered_legacy_hostlib_name(
292 "hostlib_terminal_session_capture"
293 ));
294 assert!(is_registered_legacy_hostlib_name(
295 "hostlib_code_index_agent_heartbeat"
296 ));
297 assert!(is_registered_legacy_hostlib_name("hostlib_enable"));
298 assert!(!is_registered_legacy_hostlib_name(
299 "hostlib_terminal_session_not_registered"
300 ));
301 assert!(!is_registered_legacy_hostlib_name("hostlib_unknown_ping"));
302 }
303}