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