Skip to main content

harn_parser/
lib.rs

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 lexical;
10mod parser;
11pub mod stdlib_metadata;
12pub mod typechecker;
13pub mod visit;
14
15pub use ast::*;
16pub use diagnostic_codes::{
17    Category as DiagnosticCodeCategory, Code as DiagnosticCode, ParseRepairSafetyError, Repair,
18    RepairId, RepairSafety, RepairTemplate, REPAIR_REGISTRY,
19};
20pub use parser::*;
21pub use stdlib_metadata::{
22    parse_for_span as parse_stdlib_metadata, synthesize_example, StdlibMetadata,
23};
24pub use typechecker::{
25    block_definitely_exits, format_type, stmt_definitely_exits, substitute_type_expr,
26    DiagnosticDetails, DiagnosticSeverity, InlayHintInfo, NamespaceImportBinding, TypeChecker,
27    TypeDiagnostic,
28};
29
30pub use builtin_signatures::install_builtin_manifest;
31
32/// Explicit process-level bridge for downstream packages that have not yet
33/// completed the typed `Harness` capability migration.
34///
35/// The bridge is intentionally opt-in and keeps the strict source surface as
36/// the default. Callers must set the value to exactly `1`; merely defining the
37/// variable is not sufficient.
38pub const HARN_LEGACY_AMBIENT_CAPABILITIES_ENV: &str = "HARN_LEGACY_AMBIENT_CAPABILITIES";
39
40pub fn legacy_ambient_capabilities_enabled() -> bool {
41    std::env::var(HARN_LEGACY_AMBIENT_CAPABILITIES_ENV).is_ok_and(|value| {
42        matches!(
43            value.trim().to_ascii_lowercase().as_str(),
44            "1" | "true" | "yes" | "on"
45        )
46    })
47}
48
49/// Whether an old hostlib wire name projects a method from the authoritative
50/// typed host-capability registry. This keeps compatibility recognition exact:
51/// arbitrary `hostlib_*` spellings remain undefined.
52pub fn is_registered_legacy_hostlib_name(name: &str) -> bool {
53    if name == "hostlib_enable" {
54        return true;
55    }
56    harn_builtin_meta::host_capabilities::capability_binding_for_legacy_hostlib_name(name).is_some()
57}
58
59/// Exact behavior-preserving spellings removed during the typed-Harness
60/// cutover. The compiler lowers these to the canonical manifest name only in
61/// compatibility mode; strict source continues to reject them.
62pub fn legacy_builtin_alias_target(name: &str) -> Option<&'static str> {
63    match name {
64        "regex_replace_all" => Some("regex_replace"),
65        _ => None,
66    }
67}
68
69/// Returns `true` if `name` is a builtin recognized by the parser's static analyzer.
70pub fn is_known_builtin(name: &str) -> bool {
71    builtin_signatures::is_builtin(name)
72}
73
74/// Opt-in ambient bridge: treat a registered builtin as known without a typed
75/// `Harness` capability import.
76pub fn is_legacy_ambient_builtin(name: &str) -> bool {
77    legacy_ambient_capabilities_enabled() && is_known_builtin(name)
78}
79
80/// Every builtin name known to the parser, alphabetically. Enables bidirectional
81/// drift checks against the VM's runtime registry.
82pub fn known_builtin_names() -> impl Iterator<Item = &'static str> {
83    builtin_signatures::iter_builtin_names()
84}
85
86pub fn known_builtin_metadata() -> impl Iterator<Item = builtin_signatures::BuiltinMetadata> {
87    builtin_signatures::iter_builtin_metadata()
88}
89
90/// Names sourced only from the parser's hand-written static fallback tables
91/// (not the driver-installed `#[harn_builtin]` registry). Lets cross-crate
92/// drift guards assert the static tables don't overlap with macro-published
93/// or `runtime_only` builtins.
94pub fn static_signature_names() -> impl Iterator<Item = &'static str> {
95    builtin_signatures::static_signature_names()
96}
97
98/// Error from a source processing pipeline stage. Wraps the inner error
99/// types so callers can dispatch on the failing stage.
100#[derive(Debug)]
101pub enum PipelineError {
102    Lex(harn_lexer::LexerError),
103    Parse(ParserError),
104    /// Boxed to keep the enum small on the stack — TypeDiagnostic contains
105    /// a Vec<FixEdit>.
106    TypeCheck(Box<TypeDiagnostic>),
107}
108
109impl std::fmt::Display for PipelineError {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        match self {
112            PipelineError::Lex(e) => e.fmt(f),
113            PipelineError::Parse(e) => e.fmt(f),
114            PipelineError::TypeCheck(diag) => write!(f, "type error: {}", diag.message),
115        }
116    }
117}
118
119impl std::error::Error for PipelineError {}
120
121impl From<harn_lexer::LexerError> for PipelineError {
122    fn from(e: harn_lexer::LexerError) -> Self {
123        PipelineError::Lex(e)
124    }
125}
126
127impl From<ParserError> for PipelineError {
128    fn from(e: ParserError) -> Self {
129        PipelineError::Parse(e)
130    }
131}
132
133impl PipelineError {
134    /// Extract the source span, if any, for diagnostic rendering.
135    pub fn span(&self) -> Option<&harn_lexer::Span> {
136        match self {
137            PipelineError::Lex(e) => match e {
138                harn_lexer::LexerError::UnexpectedCharacter(_, span)
139                | harn_lexer::LexerError::UnterminatedString(span)
140                | harn_lexer::LexerError::IntegerLiteralOutOfRange(_, span)
141                | harn_lexer::LexerError::UnterminatedBlockComment(span) => Some(span),
142            },
143            PipelineError::Parse(e) => match e {
144                ParserError::Unexpected { span, .. } => Some(span),
145                ParserError::UnexpectedEof { span, .. } => Some(span),
146            },
147            PipelineError::TypeCheck(diag) => diag.span.as_ref(),
148        }
149    }
150}
151
152/// Lex and parse source into an AST.
153pub fn parse_source(source: &str) -> Result<Vec<SNode>, PipelineError> {
154    let mut lexer = harn_lexer::Lexer::new(source);
155    let tokens = lexer.tokenize()?;
156    let mut parser = Parser::new(tokens);
157    Ok(parser.parse()?)
158}
159
160/// Lex, parse, and type-check source. Returns the AST and any type
161/// diagnostics (which may include warnings even on success).
162pub fn check_source(source: &str) -> Result<(Vec<SNode>, Vec<TypeDiagnostic>), PipelineError> {
163    let program = parse_source(source)?;
164    let diagnostics = TypeChecker::new().check_with_source(&program, source);
165    Ok((program, diagnostics))
166}
167
168/// Lex, parse, and type-check, bailing on the first type error.
169pub fn check_source_strict(source: &str) -> Result<Vec<SNode>, PipelineError> {
170    let (program, diagnostics) = check_source(source)?;
171    for diag in &diagnostics {
172        if diag.severity == DiagnosticSeverity::Error {
173            return Err(PipelineError::TypeCheck(Box::new(diag.clone())));
174        }
175    }
176    Ok(program)
177}
178
179#[cfg(test)]
180mod pipeline_tests {
181    use super::*;
182
183    #[test]
184    fn parse_source_valid() {
185        let program = parse_source("const x = 1").unwrap();
186        assert!(!program.is_empty());
187    }
188
189    #[test]
190    fn parse_source_lex_error() {
191        let err = parse_source("let x = `").unwrap_err();
192        assert!(matches!(err, PipelineError::Lex(_)));
193        assert!(err.span().is_some());
194        assert!(err.to_string().contains("Unexpected character"));
195    }
196
197    #[test]
198    fn parse_source_parse_error() {
199        let err = parse_source("let = 1").unwrap_err();
200        assert!(matches!(err, PipelineError::Parse(_)));
201        assert!(err.span().is_some());
202    }
203
204    #[test]
205    fn check_source_returns_diagnostics() {
206        let (program, _diagnostics) = check_source("const x = 1").unwrap();
207        assert!(!program.is_empty());
208    }
209
210    #[test]
211    fn check_source_strict_passes_valid_code() {
212        let program = check_source_strict("const x = 1\nlog(x)").unwrap();
213        assert!(!program.is_empty());
214    }
215
216    #[test]
217    fn check_source_strict_catches_lex_error() {
218        let err = check_source_strict("`").unwrap_err();
219        assert!(matches!(err, PipelineError::Lex(_)));
220    }
221
222    #[test]
223    fn pipeline_error_display_is_informative() {
224        let err = parse_source("`").unwrap_err();
225        let msg = err.to_string();
226        assert!(!msg.is_empty());
227        assert!(msg.contains('`') || msg.contains("Unexpected"));
228    }
229
230    #[test]
231    fn pipeline_error_size_is_bounded() {
232        // TypeCheck is boxed; guard against accidental growth of the other variants.
233        assert!(
234            std::mem::size_of::<PipelineError>() <= 96,
235            "PipelineError grew to {} bytes — consider boxing large variants",
236            std::mem::size_of::<PipelineError>()
237        );
238    }
239
240    #[test]
241    fn legacy_hostlib_names_must_resolve_through_the_typed_registry() {
242        assert!(is_registered_legacy_hostlib_name(
243            "hostlib_terminal_session_capture"
244        ));
245        assert!(is_registered_legacy_hostlib_name(
246            "hostlib_code_index_agent_heartbeat"
247        ));
248        assert!(is_registered_legacy_hostlib_name("hostlib_enable"));
249        assert!(!is_registered_legacy_hostlib_name(
250            "hostlib_terminal_session_not_registered"
251        ));
252        assert!(!is_registered_legacy_hostlib_name("hostlib_unknown_ping"));
253    }
254}