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 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
33/// Explicit process-level bridge for downstream packages that have not yet
34/// completed the typed `Harness` capability migration.
35///
36/// The bridge is intentionally opt-in and keeps the strict source surface as
37/// the default. Defining the variable is not enough: it must be set to one of
38/// `1`, `true`, `yes`, or `on`, so an empty or `0` value leaves strict
39/// enforcement in place.
40pub 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
51/// Whether an old hostlib wire name projects a method from the authoritative
52/// typed host-capability registry. This keeps compatibility recognition exact:
53/// arbitrary `hostlib_*` spellings remain undefined.
54pub 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
61/// Exact behavior-preserving spellings removed during the typed-Harness
62/// cutover. The compiler lowers these to the canonical manifest name only in
63/// compatibility mode; strict source continues to reject them.
64pub 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
72/// Returns `true` if `name` is a builtin recognized by the parser's static analyzer.
73pub fn is_known_builtin(name: &str) -> bool {
74    builtin_signatures::is_builtin(name)
75}
76
77/// Opt-in ambient bridge: treat a registered builtin as known without a typed
78/// `Harness` capability import.
79pub fn is_legacy_ambient_builtin(name: &str) -> bool {
80    legacy_ambient_capabilities_enabled() && is_known_builtin(name)
81}
82
83/// Every builtin name known to the parser, alphabetically. Enables bidirectional
84/// drift checks against the VM's runtime registry.
85pub 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
93/// Names sourced only from the parser's hand-written static fallback tables
94/// (not the driver-installed `#[harn_builtin]` registry). Lets cross-crate
95/// drift guards assert the static tables don't overlap with macro-published
96/// or `runtime_only` builtins.
97pub fn static_signature_names() -> impl Iterator<Item = &'static str> {
98    builtin_signatures::static_signature_names()
99}
100
101/// Error from a source processing pipeline stage. Wraps the inner error
102/// types so callers can dispatch on the failing stage.
103#[derive(Debug)]
104pub enum PipelineError {
105    Lex(harn_lexer::LexerError),
106    Parse(ParserError),
107    /// Boxed to keep the enum small on the stack — TypeDiagnostic contains
108    /// a Vec<FixEdit>.
109    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    /// Extract the source span, if any, for diagnostic rendering.
138    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
155/// Lex and parse source into an AST.
156pub 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
163/// Lex, parse, and type-check source. Returns the AST and any type
164/// diagnostics (which may include warnings even on success).
165pub 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
171/// Lex, parse, and type-check, bailing on the first type error.
172pub 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        // TypeCheck is boxed; guard against accidental growth of the other variants.
236        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}