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#[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}
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 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#[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 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}