Skip to main content

harn_parser/
analysis.rs

1use std::collections::{HashMap, HashSet};
2use std::hash::{Hash, Hasher};
3use std::path::Path;
4
5use harn_lexer::{Lexer, LexerError, Token};
6
7use crate::{InlayHintInfo, TypeCheckFacts};
8use crate::{Parser, ParserError, SNode, TypeChecker, TypeDiagnostic};
9
10/// Stable source identity used by the incremental analysis cache.
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
12pub struct SourceId(String);
13
14impl SourceId {
15    pub fn new(value: impl Into<String>) -> Self {
16        Self(value.into())
17    }
18
19    pub fn path(path: &Path) -> Self {
20        Self(path.to_string_lossy().into_owned())
21    }
22
23    pub fn as_str(&self) -> &str {
24        &self.0
25    }
26}
27
28/// Monotonic caller-owned version for a source input.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
30pub struct SourceVersion(pub u64);
31
32/// Deterministic content digest for a source input.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub struct SourceDigest(u64);
35
36impl SourceDigest {
37    pub fn from_source(source: &str) -> Self {
38        let mut hash = 0xcbf29ce484222325u64;
39        for byte in source.as_bytes() {
40            hash ^= u64::from(*byte);
41            hash = hash.wrapping_mul(0x100000001b3);
42        }
43        Self(hash)
44    }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum SourceUpdate {
49    Inserted,
50    Changed,
51    Unchanged,
52}
53
54#[derive(Debug, Clone, Default, PartialEq, Eq)]
55pub struct AnalysisStats {
56    pub lex_runs: usize,
57    pub parse_runs: usize,
58    pub typecheck_runs: usize,
59}
60
61#[derive(Debug, Clone)]
62pub struct ParseOutput {
63    pub source: String,
64    pub program: Vec<SNode>,
65}
66
67#[derive(Debug, Clone)]
68pub struct TypeCheckOutput {
69    pub source: String,
70    pub program: Vec<SNode>,
71    pub diagnostics: Vec<TypeDiagnostic>,
72    pub inlay_hints: Vec<InlayHintInfo>,
73}
74
75#[derive(Debug, Clone)]
76pub enum AnalysisError {
77    MissingSource(SourceId),
78    Lex {
79        source: String,
80        error: LexerError,
81    },
82    Parse {
83        source: String,
84        errors: Vec<ParserError>,
85    },
86}
87
88impl AnalysisError {
89    pub fn source(&self) -> Option<&str> {
90        match self {
91            AnalysisError::MissingSource(_) => None,
92            AnalysisError::Lex { source, .. } | AnalysisError::Parse { source, .. } => Some(source),
93        }
94    }
95}
96
97#[derive(Debug, Clone, Default)]
98pub struct TypeCheckConfig {
99    pub strict_types: bool,
100    pub privileged_wire_builtins: bool,
101    pub imported_names: Option<HashSet<String>>,
102    pub imported_type_decls: Vec<SNode>,
103    pub imported_callable_decls: Vec<SNode>,
104    pub namespace_imports: Vec<(String, crate::NamespaceImportBinding)>,
105}
106
107impl TypeCheckConfig {
108    pub fn new() -> Self {
109        Self::default()
110    }
111
112    pub fn with_strict_types(mut self, strict_types: bool) -> Self {
113        self.strict_types = strict_types;
114        self
115    }
116
117    pub fn with_privileged_wire_builtins(mut self, enabled: bool) -> Self {
118        self.privileged_wire_builtins = enabled;
119        self
120    }
121
122    pub fn with_imported_names(mut self, imported_names: Option<HashSet<String>>) -> Self {
123        self.imported_names = imported_names;
124        self
125    }
126
127    pub fn with_imported_type_decls(mut self, imported_type_decls: Vec<SNode>) -> Self {
128        self.imported_type_decls = imported_type_decls;
129        self
130    }
131
132    pub fn with_imported_callable_decls(mut self, imported_callable_decls: Vec<SNode>) -> Self {
133        self.imported_callable_decls = imported_callable_decls;
134        self
135    }
136
137    pub fn with_namespace_imports(
138        mut self,
139        namespace_imports: Vec<(String, crate::NamespaceImportBinding)>,
140    ) -> Self {
141        self.namespace_imports = namespace_imports;
142        self
143    }
144
145    fn cache_key(&self) -> TypeCheckCacheKey {
146        let mut imported_names = self
147            .imported_names
148            .as_ref()
149            .map(|names| names.iter().cloned().collect::<Vec<_>>());
150        if let Some(names) = &mut imported_names {
151            names.sort();
152        }
153        let mut namespace_aliases: Vec<String> = self
154            .namespace_imports
155            .iter()
156            .map(|(alias, _)| alias.clone())
157            .collect();
158        namespace_aliases.sort();
159        TypeCheckCacheKey {
160            strict_types: self.strict_types,
161            privileged_wire_builtins: self.privileged_wire_builtins,
162            imported_names,
163            imported_type_decls_digest: debug_digest(&self.imported_type_decls),
164            imported_callable_decls_digest: debug_digest(&self.imported_callable_decls),
165            namespace_imports_digest: debug_digest(&namespace_aliases),
166        }
167    }
168
169    fn build_checker(&self) -> TypeChecker {
170        let mut checker = TypeChecker::with_strict_types(self.strict_types);
171        checker = checker.with_privileged_wire_builtins(self.privileged_wire_builtins);
172        if let Some(imported) = self.imported_names.clone() {
173            checker = checker.with_imported_names(imported);
174        }
175        if !self.imported_type_decls.is_empty() {
176            checker = checker.with_imported_type_decls(self.imported_type_decls.clone());
177        }
178        if !self.imported_callable_decls.is_empty() {
179            checker = checker.with_imported_callable_decls(self.imported_callable_decls.clone());
180        }
181        if !self.namespace_imports.is_empty() {
182            checker = checker.with_namespace_imports(self.namespace_imports.clone());
183        }
184        checker
185    }
186
187    /// Run the configured checker while retaining semantic binding facts.
188    pub fn check_with_facts(&self, program: &[SNode], source: &str) -> TypeCheckFacts {
189        self.build_checker().check_with_facts(program, source)
190    }
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, Hash)]
194struct TypeCheckCacheKey {
195    strict_types: bool,
196    privileged_wire_builtins: bool,
197    imported_names: Option<Vec<String>>,
198    imported_type_decls_digest: u64,
199    imported_callable_decls_digest: u64,
200    namespace_imports_digest: u64,
201}
202
203#[derive(Debug, Clone)]
204struct CachedTypeCheck {
205    diagnostics: Vec<TypeDiagnostic>,
206    inlay_hints: Vec<InlayHintInfo>,
207}
208
209#[derive(Debug, Clone)]
210struct SourceEntry {
211    source: String,
212    version: SourceVersion,
213    digest: SourceDigest,
214    tokens: Option<Result<Vec<Token>, LexerError>>,
215    program: Option<Result<Vec<SNode>, Vec<ParserError>>>,
216    typechecks: HashMap<TypeCheckCacheKey, CachedTypeCheck>,
217}
218
219impl SourceEntry {
220    fn new(source: String, version: SourceVersion, digest: SourceDigest) -> Self {
221        Self {
222            source,
223            version,
224            digest,
225            tokens: None,
226            program: None,
227            typechecks: HashMap::new(),
228        }
229    }
230
231    fn replace_source(&mut self, source: String, version: SourceVersion, digest: SourceDigest) {
232        self.source = source;
233        self.version = version;
234        self.digest = digest;
235        self.tokens = None;
236        self.program = None;
237        self.typechecks.clear();
238    }
239}
240
241/// Persistent query cache for lexing, parsing, and type checking Harn sources.
242#[derive(Debug, Default)]
243pub struct AnalysisDatabase {
244    entries: HashMap<SourceId, SourceEntry>,
245    stats: AnalysisStats,
246}
247
248impl AnalysisDatabase {
249    pub fn new() -> Self {
250        Self::default()
251    }
252
253    pub fn stats(&self) -> AnalysisStats {
254        self.stats.clone()
255    }
256
257    pub fn set_source(
258        &mut self,
259        id: SourceId,
260        source: String,
261        version: SourceVersion,
262    ) -> SourceUpdate {
263        let digest = SourceDigest::from_source(&source);
264        match self.entries.get_mut(&id) {
265            None => {
266                self.entries
267                    .insert(id, SourceEntry::new(source, version, digest));
268                SourceUpdate::Inserted
269            }
270            Some(entry) if entry.digest == digest => {
271                entry.version = version;
272                SourceUpdate::Unchanged
273            }
274            Some(entry) => {
275                entry.replace_source(source, version, digest);
276                SourceUpdate::Changed
277            }
278        }
279    }
280
281    pub fn set_parsed_source(
282        &mut self,
283        id: SourceId,
284        source: String,
285        version: SourceVersion,
286        program: Vec<SNode>,
287    ) -> SourceUpdate {
288        let digest = SourceDigest::from_source(&source);
289        match self.entries.get_mut(&id) {
290            None => {
291                let mut entry = SourceEntry::new(source, version, digest);
292                entry.program = Some(Ok(program));
293                self.entries.insert(id, entry);
294                SourceUpdate::Inserted
295            }
296            Some(entry) if entry.digest == digest => {
297                entry.version = version;
298                entry.program = Some(Ok(program));
299                SourceUpdate::Unchanged
300            }
301            Some(entry) => {
302                entry.replace_source(source, version, digest);
303                entry.program = Some(Ok(program));
304                SourceUpdate::Changed
305            }
306        }
307    }
308
309    pub fn parse(&mut self, id: &SourceId) -> Result<ParseOutput, AnalysisError> {
310        if let Some(entry) = self.entries.get(id) {
311            if let Some(program) = &entry.program {
312                return match program {
313                    Ok(program) => Ok(ParseOutput {
314                        source: entry.source.clone(),
315                        program: program.clone(),
316                    }),
317                    Err(errors) => Err(AnalysisError::Parse {
318                        source: entry.source.clone(),
319                        errors: errors.clone(),
320                    }),
321                };
322            }
323        }
324
325        let mut lexed = false;
326        let mut parsed_now = false;
327        let entry = self.entry_mut(id)?;
328        if entry.tokens.is_none() {
329            lexed = true;
330            let mut lexer = Lexer::new(&entry.source);
331            entry.tokens = Some(lexer.tokenize());
332        }
333        let tokens = match entry.tokens.as_ref().expect("tokens initialized") {
334            Ok(tokens) => tokens.clone(),
335            Err(error) => {
336                let source = entry.source.clone();
337                let error = error.clone();
338                if lexed {
339                    self.stats.lex_runs += 1;
340                }
341                return Err(AnalysisError::Lex { source, error });
342            }
343        };
344
345        if entry.program.is_none() {
346            parsed_now = true;
347            let mut parser = Parser::new(tokens);
348            entry.program = Some(match parser.parse() {
349                Ok(program) => Ok(program),
350                Err(error) => {
351                    let mut errors = parser.all_errors().to_vec();
352                    if errors.is_empty() {
353                        errors.push(error);
354                    }
355                    Err(errors)
356                }
357            });
358        }
359
360        let result = match entry.program.as_ref().expect("program initialized") {
361            Ok(program) => Ok(ParseOutput {
362                source: entry.source.clone(),
363                program: program.clone(),
364            }),
365            Err(errors) => Err(AnalysisError::Parse {
366                source: entry.source.clone(),
367                errors: errors.clone(),
368            }),
369        };
370        if lexed {
371            self.stats.lex_runs += 1;
372        }
373        if parsed_now {
374            self.stats.parse_runs += 1;
375        }
376        result
377    }
378
379    pub fn typecheck(
380        &mut self,
381        id: &SourceId,
382        config: TypeCheckConfig,
383    ) -> Result<TypeCheckOutput, AnalysisError> {
384        let parsed = self.parse(id)?;
385        let key = config.cache_key();
386        if let Some(cached) = self
387            .entries
388            .get(id)
389            .expect("parse verified source entry")
390            .typechecks
391            .get(&key)
392        {
393            return Ok(TypeCheckOutput {
394                source: parsed.source,
395                program: parsed.program,
396                diagnostics: cached.diagnostics.clone(),
397                inlay_hints: cached.inlay_hints.clone(),
398            });
399        }
400
401        self.stats.typecheck_runs += 1;
402        let (diagnostics, inlay_hints) = config
403            .build_checker()
404            .check_with_hints(&parsed.program, &parsed.source);
405        let cached = CachedTypeCheck {
406            diagnostics: diagnostics.clone(),
407            inlay_hints: inlay_hints.clone(),
408        };
409        self.entries
410            .get_mut(id)
411            .expect("parse verified source entry")
412            .typechecks
413            .insert(key, cached);
414        Ok(TypeCheckOutput {
415            source: parsed.source,
416            program: parsed.program,
417            diagnostics,
418            inlay_hints,
419        })
420    }
421
422    fn entry_mut(&mut self, id: &SourceId) -> Result<&mut SourceEntry, AnalysisError> {
423        self.entries
424            .get_mut(id)
425            .ok_or_else(|| AnalysisError::MissingSource(id.clone()))
426    }
427}
428
429fn debug_digest<T: std::fmt::Debug>(value: &T) -> u64 {
430    let mut hasher = StableHasher::default();
431    format!("{value:?}").hash(&mut hasher);
432    hasher.finish()
433}
434
435#[derive(Default)]
436struct StableHasher(u64);
437
438impl Hasher for StableHasher {
439    fn finish(&self) -> u64 {
440        self.0
441    }
442
443    fn write(&mut self, bytes: &[u8]) {
444        let mut hash = if self.0 == 0 {
445            0xcbf29ce484222325u64
446        } else {
447            self.0
448        };
449        for byte in bytes {
450            hash ^= u64::from(*byte);
451            hash = hash.wrapping_mul(0x100000001b3);
452        }
453        self.0 = hash;
454    }
455}
456
457#[cfg(test)]
458mod tests {
459    use super::*;
460    use crate::DiagnosticSeverity;
461
462    fn source_id() -> SourceId {
463        SourceId::new("test.harn")
464    }
465
466    #[test]
467    fn parse_reuses_cached_program_for_unchanged_source() {
468        let mut db = AnalysisDatabase::new();
469        let id = source_id();
470        assert_eq!(
471            db.set_source(id.clone(), "const x = 1\n".to_string(), SourceVersion(1)),
472            SourceUpdate::Inserted
473        );
474        db.parse(&id).expect("initial parse");
475        db.parse(&id).expect("cached parse");
476        assert_eq!(db.stats().lex_runs, 1);
477        assert_eq!(db.stats().parse_runs, 1);
478
479        assert_eq!(
480            db.set_source(id.clone(), "const x = 1\n".to_string(), SourceVersion(2)),
481            SourceUpdate::Unchanged
482        );
483        db.parse(&id).expect("same digest parse");
484        assert_eq!(db.stats().lex_runs, 1);
485        assert_eq!(db.stats().parse_runs, 1);
486    }
487
488    #[test]
489    fn source_change_invalidates_parse_and_typecheck_outputs() {
490        let mut db = AnalysisDatabase::new();
491        let id = source_id();
492        db.set_source(id.clone(), "const x = 1\n".to_string(), SourceVersion(1));
493        db.typecheck(&id, TypeCheckConfig::new())
494            .expect("initial check");
495        assert_eq!(
496            db.set_source(id.clone(), "const x = 2\n".to_string(), SourceVersion(2)),
497            SourceUpdate::Changed
498        );
499        db.typecheck(&id, TypeCheckConfig::new())
500            .expect("changed check");
501        assert_eq!(db.stats().lex_runs, 2);
502        assert_eq!(db.stats().parse_runs, 2);
503        assert_eq!(db.stats().typecheck_runs, 2);
504    }
505
506    #[test]
507    fn parsed_source_seed_skips_lex_and_parse() {
508        let mut db = AnalysisDatabase::new();
509        let id = source_id();
510        let source = "const x = 1\n".to_string();
511        let mut lexer = Lexer::new(&source);
512        let tokens = lexer.tokenize().expect("tokenize");
513        let mut parser = Parser::new(tokens);
514        let program = parser.parse().expect("parse");
515
516        assert_eq!(
517            db.set_parsed_source(id.clone(), source, SourceVersion(1), program),
518            SourceUpdate::Inserted
519        );
520        db.typecheck(&id, TypeCheckConfig::new())
521            .expect("seeded check");
522        assert_eq!(db.stats().lex_runs, 0);
523        assert_eq!(db.stats().parse_runs, 0);
524        assert_eq!(db.stats().typecheck_runs, 1);
525    }
526
527    #[test]
528    fn typecheck_cache_is_keyed_by_options() {
529        let mut db = AnalysisDatabase::new();
530        let id = source_id();
531        db.set_source(
532            id.clone(),
533            "pipeline main() {\n  const x = read_file(\"a\")\n  log(x.foo)\n}\n".to_string(),
534            SourceVersion(1),
535        );
536        db.typecheck(&id, TypeCheckConfig::new())
537            .expect("default check");
538        db.typecheck(&id, TypeCheckConfig::new())
539            .expect("cached default check");
540        db.typecheck(&id, TypeCheckConfig::new().with_strict_types(true))
541            .expect("strict check");
542        assert_eq!(db.stats().typecheck_runs, 2);
543    }
544
545    #[test]
546    fn typecheck_diagnostics_are_cached_with_hints() {
547        let mut db = AnalysisDatabase::new();
548        let id = source_id();
549        db.set_source(
550            id.clone(),
551            "pipeline main() {\n  const x: int = \"nope\"\n}\n".to_string(),
552            SourceVersion(1),
553        );
554        let first = db.typecheck(&id, TypeCheckConfig::new()).expect("check");
555        let second = db.typecheck(&id, TypeCheckConfig::new()).expect("cached");
556        assert!(first
557            .diagnostics
558            .iter()
559            .any(|diag| diag.severity == DiagnosticSeverity::Error));
560        assert_eq!(first.diagnostics.len(), second.diagnostics.len());
561        assert_eq!(db.stats().typecheck_runs, 1);
562    }
563
564    #[test]
565    fn typecheck_facts_keep_shadowed_inferred_bindings_distinct() {
566        let source = "pipeline main(agent: HarnessAgent) {\n  const value = agent\n  if true {\n    const value = \"session\"\n    log(value)\n  }\n  log(value)\n}\n";
567        let program = crate::parse_source(source).expect("parse");
568
569        let facts = TypeCheckConfig::new().check_with_facts(&program, source);
570        let values = facts
571            .binding_types
572            .iter()
573            .filter(|binding| binding.name == "value")
574            .collect::<Vec<_>>();
575
576        assert_eq!(values.len(), 2, "{:#?}", facts.binding_types);
577        assert_ne!(values[0].span, values[1].span);
578        assert_eq!(crate::format_type(&values[0].type_expr), "HarnessAgent");
579        assert_eq!(crate::format_type(&values[1].type_expr), "string");
580    }
581}