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 parser;
12pub mod stdlib_metadata;
13pub mod typechecker;
14pub mod visit;
15
16pub use ast::*;
17pub use diagnostic_codes::{
18 Category as DiagnosticCodeCategory, Code as DiagnosticCode, ParseRepairSafetyError, Repair,
19 RepairId, RepairSafety, RepairTemplate, REPAIR_REGISTRY,
20};
21pub use parser::*;
22pub use stdlib_metadata::{
23 parse_for_span as parse_stdlib_metadata, synthesize_example, StdlibMetadata,
24};
25pub use typechecker::{
26 block_definitely_exits, format_type, stmt_definitely_exits, substitute_type_expr,
27 DiagnosticDetails, DiagnosticSeverity, InlayHintInfo, NamespaceImportBinding, TypeChecker,
28 TypeDiagnostic,
29};
30
31pub use builtin_signatures::install_builtin_manifest;
32
33pub const HARN_LEGACY_AMBIENT_CAPABILITIES_ENV: &str = "HARN_LEGACY_AMBIENT_CAPABILITIES";
41
42pub fn legacy_ambient_capabilities_enabled() -> bool {
43 std::env::var(HARN_LEGACY_AMBIENT_CAPABILITIES_ENV).is_ok_and(|value| {
44 matches!(
45 value.trim().to_ascii_lowercase().as_str(),
46 "1" | "true" | "yes" | "on"
47 )
48 })
49}
50
51pub fn is_registered_legacy_hostlib_name(name: &str) -> bool {
55 if name == "hostlib_enable" {
56 return true;
57 }
58 harn_builtin_meta::host_capabilities::capability_binding_for_legacy_hostlib_name(name).is_some()
59}
60
61pub fn legacy_builtin_alias_target(name: &str) -> Option<&'static str> {
65 match name {
66 "regex_replace_all" => Some("regex_replace"),
67 "task_current" => Some("runtime_context"),
68 _ => None,
69 }
70}
71
72pub fn is_known_builtin(name: &str) -> bool {
74 builtin_signatures::is_builtin(name)
75}
76
77pub fn is_legacy_ambient_builtin(name: &str) -> bool {
80 legacy_ambient_capabilities_enabled() && is_known_builtin(name)
81}
82
83pub fn known_builtin_names() -> impl Iterator<Item = &'static str> {
86 builtin_signatures::iter_builtin_names()
87}
88
89pub fn known_builtin_metadata() -> impl Iterator<Item = builtin_signatures::BuiltinMetadata> {
90 builtin_signatures::iter_builtin_metadata()
91}
92
93pub fn static_signature_names() -> impl Iterator<Item = &'static str> {
98 builtin_signatures::static_signature_names()
99}
100
101#[derive(Debug)]
104pub enum PipelineError {
105 Lex(harn_lexer::LexerError),
106 Parse(ParserError),
107 TypeCheck(Box<TypeDiagnostic>),
110}
111
112impl std::fmt::Display for PipelineError {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 match self {
115 PipelineError::Lex(e) => e.fmt(f),
116 PipelineError::Parse(e) => e.fmt(f),
117 PipelineError::TypeCheck(diag) => write!(f, "type error: {}", diag.message),
118 }
119 }
120}
121
122impl std::error::Error for PipelineError {}
123
124impl From<harn_lexer::LexerError> for PipelineError {
125 fn from(e: harn_lexer::LexerError) -> Self {
126 PipelineError::Lex(e)
127 }
128}
129
130impl From<ParserError> for PipelineError {
131 fn from(e: ParserError) -> Self {
132 PipelineError::Parse(e)
133 }
134}
135
136impl PipelineError {
137 pub fn span(&self) -> Option<&harn_lexer::Span> {
139 match self {
140 PipelineError::Lex(e) => match e {
141 harn_lexer::LexerError::UnexpectedCharacter(_, span)
142 | harn_lexer::LexerError::UnterminatedString(span)
143 | harn_lexer::LexerError::IntegerLiteralOutOfRange(_, span)
144 | harn_lexer::LexerError::UnterminatedBlockComment(span) => Some(span),
145 },
146 PipelineError::Parse(e) => match e {
147 ParserError::Unexpected { span, .. } => Some(span),
148 ParserError::UnexpectedEof { span, .. } => Some(span),
149 },
150 PipelineError::TypeCheck(diag) => diag.span.as_ref(),
151 }
152 }
153}
154
155pub fn parse_source(source: &str) -> Result<Vec<SNode>, PipelineError> {
157 let mut lexer = harn_lexer::Lexer::new(source);
158 let tokens = lexer.tokenize()?;
159 let mut parser = Parser::new(tokens);
160 Ok(parser.parse()?)
161}
162
163pub fn check_source(source: &str) -> Result<(Vec<SNode>, Vec<TypeDiagnostic>), PipelineError> {
166 let program = parse_source(source)?;
167 let diagnostics = TypeChecker::new().check_with_source(&program, source);
168 Ok((program, diagnostics))
169}
170
171pub fn check_source_strict(source: &str) -> Result<Vec<SNode>, PipelineError> {
173 let (program, diagnostics) = check_source(source)?;
174 for diag in &diagnostics {
175 if diag.severity == DiagnosticSeverity::Error {
176 return Err(PipelineError::TypeCheck(Box::new(diag.clone())));
177 }
178 }
179 Ok(program)
180}
181
182#[cfg(test)]
183mod pipeline_tests {
184 use super::*;
185
186 #[test]
187 fn parse_source_valid() {
188 let program = parse_source("const x = 1").unwrap();
189 assert!(!program.is_empty());
190 }
191
192 #[test]
193 fn parse_source_lex_error() {
194 let err = parse_source("let x = `").unwrap_err();
195 assert!(matches!(err, PipelineError::Lex(_)));
196 assert!(err.span().is_some());
197 assert!(err.to_string().contains("Unexpected character"));
198 }
199
200 #[test]
201 fn parse_source_parse_error() {
202 let err = parse_source("let = 1").unwrap_err();
203 assert!(matches!(err, PipelineError::Parse(_)));
204 assert!(err.span().is_some());
205 }
206
207 #[test]
208 fn check_source_returns_diagnostics() {
209 let (program, _diagnostics) = check_source("const x = 1").unwrap();
210 assert!(!program.is_empty());
211 }
212
213 #[test]
214 fn check_source_strict_passes_valid_code() {
215 let program = check_source_strict("const x = 1\nlog(x)").unwrap();
216 assert!(!program.is_empty());
217 }
218
219 #[test]
220 fn check_source_strict_catches_lex_error() {
221 let err = check_source_strict("`").unwrap_err();
222 assert!(matches!(err, PipelineError::Lex(_)));
223 }
224
225 #[test]
226 fn pipeline_error_display_is_informative() {
227 let err = parse_source("`").unwrap_err();
228 let msg = err.to_string();
229 assert!(!msg.is_empty());
230 assert!(msg.contains('`') || msg.contains("Unexpected"));
231 }
232
233 #[test]
234 fn pipeline_error_size_is_bounded() {
235 assert!(
237 std::mem::size_of::<PipelineError>() <= 96,
238 "PipelineError grew to {} bytes — consider boxing large variants",
239 std::mem::size_of::<PipelineError>()
240 );
241 }
242
243 #[test]
244 fn legacy_hostlib_names_must_resolve_through_the_typed_registry() {
245 assert!(is_registered_legacy_hostlib_name(
246 "hostlib_terminal_session_capture"
247 ));
248 assert!(is_registered_legacy_hostlib_name(
249 "hostlib_code_index_agent_heartbeat"
250 ));
251 assert!(is_registered_legacy_hostlib_name("hostlib_enable"));
252 assert!(!is_registered_legacy_hostlib_name(
253 "hostlib_terminal_session_not_registered"
254 ));
255 assert!(!is_registered_legacy_hostlib_name("hostlib_unknown_ping"));
256 }
257}