Skip to main content

harn_parser/
analysis.rs

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