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
74pub fn is_known_builtin(name: &str) -> bool {
76 builtin_signatures::is_builtin(name)
77}
78
79pub fn is_legacy_ambient_builtin(name: &str) -> bool {
82 legacy_ambient_capabilities_enabled() && is_known_builtin(name)
83}
84
85pub fn known_builtin_names() -> impl Iterator<Item = &'static str> {
88 builtin_signatures::iter_builtin_names()
89}
90
91pub fn known_builtin_metadata() -> impl Iterator<Item = builtin_signatures::BuiltinMetadata> {
92 builtin_signatures::iter_builtin_metadata()
93}
94
95pub fn static_signature_names() -> impl Iterator<Item = &'static str> {
100 builtin_signatures::static_signature_names()
101}
102
103#[derive(Debug)]
106pub enum PipelineError {
107 Lex(harn_lexer::LexerError),
108 Parse(ParserError),
109 TypeCheck(Box<TypeDiagnostic>),
112}
113
114impl std::fmt::Display for PipelineError {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 match self {
117 PipelineError::Lex(e) => e.fmt(f),
118 PipelineError::Parse(e) => e.fmt(f),
119 PipelineError::TypeCheck(diag) => write!(f, "type error: {}", diag.message),
120 }
121 }
122}
123
124impl std::error::Error for PipelineError {}
125
126impl From<harn_lexer::LexerError> for PipelineError {
127 fn from(e: harn_lexer::LexerError) -> Self {
128 PipelineError::Lex(e)
129 }
130}
131
132impl From<ParserError> for PipelineError {
133 fn from(e: ParserError) -> Self {
134 PipelineError::Parse(e)
135 }
136}
137
138impl PipelineError {
139 pub fn span(&self) -> Option<&harn_lexer::Span> {
141 match self {
142 PipelineError::Lex(e) => match e {
143 harn_lexer::LexerError::UnexpectedCharacter(_, span)
144 | harn_lexer::LexerError::UnterminatedString(span)
145 | harn_lexer::LexerError::IntegerLiteralOutOfRange(_, span)
146 | harn_lexer::LexerError::UnterminatedBlockComment(span) => Some(span),
147 },
148 PipelineError::Parse(e) => match e {
149 ParserError::Unexpected { span, .. } => Some(span),
150 ParserError::UnexpectedEof { span, .. } => Some(span),
151 },
152 PipelineError::TypeCheck(diag) => diag.span.as_ref(),
153 }
154 }
155}
156
157pub fn parse_source(source: &str) -> Result<Vec<SNode>, PipelineError> {
159 let mut lexer = harn_lexer::Lexer::new(source);
160 let tokens = lexer.tokenize()?;
161 let mut parser = Parser::new(tokens);
162 Ok(parser.parse()?)
163}
164
165pub fn check_source(source: &str) -> Result<(Vec<SNode>, Vec<TypeDiagnostic>), PipelineError> {
168 let program = parse_source(source)?;
169 let diagnostics = TypeChecker::new().check_with_source(&program, source);
170 Ok((program, diagnostics))
171}
172
173pub fn check_source_strict(source: &str) -> Result<Vec<SNode>, PipelineError> {
175 let (program, diagnostics) = check_source(source)?;
176 for diag in &diagnostics {
177 if diag.severity == DiagnosticSeverity::Error {
178 return Err(PipelineError::TypeCheck(Box::new(diag.clone())));
179 }
180 }
181 Ok(program)
182}
183
184#[cfg(test)]
185mod pipeline_tests {
186 use super::*;
187
188 #[test]
189 fn parse_source_valid() {
190 let program = parse_source("const x = 1").unwrap();
191 assert!(!program.is_empty());
192 }
193
194 #[test]
195 fn parse_source_lex_error() {
196 let err = parse_source("let x = `").unwrap_err();
197 assert!(matches!(err, PipelineError::Lex(_)));
198 assert!(err.span().is_some());
199 assert!(err.to_string().contains("Unexpected character"));
200 }
201
202 #[test]
203 fn parse_source_parse_error() {
204 let err = parse_source("let = 1").unwrap_err();
205 assert!(matches!(err, PipelineError::Parse(_)));
206 assert!(err.span().is_some());
207 }
208
209 #[test]
210 fn check_source_returns_diagnostics() {
211 let (program, _diagnostics) = check_source("const x = 1").unwrap();
212 assert!(!program.is_empty());
213 }
214
215 #[test]
216 fn check_source_strict_passes_valid_code() {
217 let program = check_source_strict("const x = 1\nlog(x)").unwrap();
218 assert!(!program.is_empty());
219 }
220
221 #[test]
222 fn check_source_strict_catches_lex_error() {
223 let err = check_source_strict("`").unwrap_err();
224 assert!(matches!(err, PipelineError::Lex(_)));
225 }
226
227 #[test]
228 fn pipeline_error_display_is_informative() {
229 let err = parse_source("`").unwrap_err();
230 let msg = err.to_string();
231 assert!(!msg.is_empty());
232 assert!(msg.contains('`') || msg.contains("Unexpected"));
233 }
234
235 #[test]
236 fn pipeline_error_size_is_bounded() {
237 assert!(
239 std::mem::size_of::<PipelineError>() <= 96,
240 "PipelineError grew to {} bytes — consider boxing large variants",
241 std::mem::size_of::<PipelineError>()
242 );
243 }
244
245 #[test]
246 fn legacy_hostlib_names_must_resolve_through_the_typed_registry() {
247 assert!(is_registered_legacy_hostlib_name(
248 "hostlib_terminal_session_capture"
249 ));
250 assert!(is_registered_legacy_hostlib_name(
251 "hostlib_code_index_agent_heartbeat"
252 ));
253 assert!(is_registered_legacy_hostlib_name("hostlib_enable"));
254 assert!(!is_registered_legacy_hostlib_name(
255 "hostlib_terminal_session_not_registered"
256 ));
257 assert!(!is_registered_legacy_hostlib_name("hostlib_unknown_ping"));
258 }
259}