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