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::{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, PartialEq, Eq)]
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 lex_error: Option<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 lex_error: 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.lex_error = 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 let entry = self
324 .entries
325 .get_mut(id)
326 .ok_or_else(|| AnalysisError::MissingSource(id.clone()))?;
327 if entry.program.is_none() {
328 if let Some(error) = &entry.lex_error {
329 return Err(AnalysisError::Lex {
330 source: entry.source.clone(),
331 error: error.clone(),
332 });
333 }
334 self.stats.lex_runs += 1;
335 let tokens = Lexer::new(&entry.source).tokenize().map_err(|error| {
336 entry.lex_error = Some(error.clone());
337 AnalysisError::Lex {
338 source: entry.source.clone(),
339 error,
340 }
341 })?;
342 self.stats.parse_runs += 1;
343 let mut parser = Parser::new(tokens);
344 entry.program = Some(parser.parse().map_err(|error| {
345 let mut errors = parser.all_errors().to_vec();
346 if errors.is_empty() {
347 errors.push(error);
348 }
349 errors
350 }));
351 }
352 match entry.program.as_ref().expect("program initialized") {
353 Ok(program) => Ok(ParseOutput {
354 source: entry.source.clone(),
355 program: program.clone(),
356 }),
357 Err(errors) => Err(AnalysisError::Parse {
358 source: entry.source.clone(),
359 errors: errors.clone(),
360 }),
361 }
362 }
363
364 pub fn typecheck(
365 &mut self,
366 id: &SourceId,
367 config: TypeCheckConfig,
368 ) -> Result<TypeCheckOutput, AnalysisError> {
369 let parsed = self.parse(id)?;
370 let key = config.cache_key();
371 if let Some(cached) = self
372 .entries
373 .get(id)
374 .expect("parse verified source entry")
375 .typechecks
376 .get(&key)
377 {
378 return Ok(TypeCheckOutput {
379 source: parsed.source,
380 program: parsed.program,
381 diagnostics: cached.diagnostics.clone(),
382 inlay_hints: cached.inlay_hints.clone(),
383 });
384 }
385
386 self.stats.typecheck_runs += 1;
387 let (diagnostics, inlay_hints) = config
388 .into_checker()
389 .check_with_hints(&parsed.program, &parsed.source);
390 let cached = CachedTypeCheck {
391 diagnostics: diagnostics.clone(),
392 inlay_hints: inlay_hints.clone(),
393 };
394 self.entries
395 .get_mut(id)
396 .expect("parse verified source entry")
397 .typechecks
398 .insert(key, cached);
399 Ok(TypeCheckOutput {
400 source: parsed.source,
401 program: parsed.program,
402 diagnostics,
403 inlay_hints,
404 })
405 }
406}
407
408fn structural_digest<T: serde::Serialize>(value: &T) -> u64 {
409 let mut hasher = StableHasher::default();
410 serde_json::to_writer(HasherWriter(&mut hasher), value)
411 .expect("typecheck cache-key values are JSON-serializable");
412 hasher.finish()
413}
414
415struct HasherWriter<'a>(&'a mut StableHasher);
416
417impl Write for HasherWriter<'_> {
418 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
419 self.0.write(bytes);
420 Ok(bytes.len())
421 }
422
423 fn flush(&mut self) -> std::io::Result<()> {
424 Ok(())
425 }
426}
427
428#[derive(Default)]
429struct StableHasher(u64);
430
431impl Hasher for StableHasher {
432 fn finish(&self) -> u64 {
433 self.0
434 }
435
436 fn write(&mut self, bytes: &[u8]) {
437 let mut hash = if self.0 == 0 {
438 0xcbf29ce484222325u64
439 } else {
440 self.0
441 };
442 for byte in bytes {
443 hash ^= u64::from(*byte);
444 hash = hash.wrapping_mul(0x100000001b3);
445 }
446 self.0 = hash;
447 }
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
453 use crate::DiagnosticSeverity;
454
455 fn source_id() -> SourceId {
456 SourceId::new("test.harn")
457 }
458
459 #[test]
460 fn parse_reuses_cached_program_for_unchanged_source() {
461 let mut db = AnalysisDatabase::new();
462 let id = source_id();
463 assert_eq!(
464 db.set_source(id.clone(), "const x = 1\n".to_string(), SourceVersion(1)),
465 SourceUpdate::Inserted
466 );
467 db.parse(&id).expect("initial parse");
468 db.parse(&id).expect("cached parse");
469 assert_eq!(db.stats().lex_runs, 1);
470 assert_eq!(db.stats().parse_runs, 1);
471
472 assert_eq!(
473 db.set_source(id.clone(), "const x = 1\n".to_string(), SourceVersion(2)),
474 SourceUpdate::Unchanged
475 );
476 db.parse(&id).expect("same digest parse");
477 assert_eq!(db.stats().lex_runs, 1);
478 assert_eq!(db.stats().parse_runs, 1);
479 }
480
481 #[test]
482 fn failed_analysis_is_cached_until_source_changes() {
483 for (source, parses) in [("#", 0), ("const =", 1)] {
484 let mut db = AnalysisDatabase::new();
485 let id = source_id();
486 db.set_source(id.clone(), source.into(), SourceVersion(1));
487 let first = db.parse(&id).unwrap_err();
488 let second = db.parse(&id).unwrap_err();
489 assert_eq!(first, second);
490 assert_eq!(db.stats().lex_runs, 1);
491 assert_eq!(db.stats().parse_runs, parses);
492 db.set_source(id.clone(), "const x = 1\n".into(), SourceVersion(2));
493 assert_eq!(db.parse(&id).unwrap().program.len(), 1);
494 assert_eq!(db.stats().lex_runs, 2);
495 assert_eq!(db.stats().parse_runs, parses + 1);
496 }
497 }
498
499 #[test]
500 fn source_change_invalidates_parse_and_typecheck_outputs() {
501 let mut db = AnalysisDatabase::new();
502 let id = source_id();
503 db.set_source(id.clone(), "const x = 1\n".to_string(), SourceVersion(1));
504 db.typecheck(&id, TypeCheckConfig::new())
505 .expect("initial check");
506 assert_eq!(
507 db.set_source(id.clone(), "const x = 2\n".to_string(), SourceVersion(2)),
508 SourceUpdate::Changed
509 );
510 db.typecheck(&id, TypeCheckConfig::new())
511 .expect("changed check");
512 assert_eq!(db.stats().lex_runs, 2);
513 assert_eq!(db.stats().parse_runs, 2);
514 assert_eq!(db.stats().typecheck_runs, 2);
515 }
516
517 #[test]
518 fn parsed_source_seed_skips_lex_and_parse() {
519 let mut db = AnalysisDatabase::new();
520 let id = source_id();
521 let source = "const x = 1\n".to_string();
522 let mut lexer = Lexer::new(&source);
523 let tokens = lexer.tokenize().expect("tokenize");
524 let mut parser = Parser::new(tokens);
525 let program = parser.parse().expect("parse");
526
527 assert_eq!(
528 db.set_parsed_source(id.clone(), source, SourceVersion(1), program),
529 SourceUpdate::Inserted
530 );
531 db.typecheck(&id, TypeCheckConfig::new())
532 .expect("seeded check");
533 assert_eq!(db.stats().lex_runs, 0);
534 assert_eq!(db.stats().parse_runs, 0);
535 assert_eq!(db.stats().typecheck_runs, 1);
536 }
537
538 #[test]
539 fn typecheck_cache_is_keyed_by_options() {
540 let mut db = AnalysisDatabase::new();
541 let id = source_id();
542 db.set_source(
543 id.clone(),
544 "pipeline main() {\n const x = read_file(\"a\")\n log(x.foo)\n}\n".to_string(),
545 SourceVersion(1),
546 );
547 db.typecheck(&id, TypeCheckConfig::new())
548 .expect("default check");
549 db.typecheck(&id, TypeCheckConfig::new())
550 .expect("cached default check");
551 db.typecheck(&id, TypeCheckConfig::new().with_strict_types(true))
552 .expect("strict check");
553 assert_eq!(db.stats().typecheck_runs, 2);
554 }
555
556 #[test]
557 fn typecheck_diagnostics_are_cached_with_hints() {
558 let mut db = AnalysisDatabase::new();
559 let id = source_id();
560 db.set_source(
561 id.clone(),
562 "pipeline main() {\n const x: int = \"nope\"\n}\n".to_string(),
563 SourceVersion(1),
564 );
565 let first = db.typecheck(&id, TypeCheckConfig::new()).expect("check");
566 let second = db.typecheck(&id, TypeCheckConfig::new()).expect("cached");
567 assert!(first
568 .diagnostics
569 .iter()
570 .any(|diag| diag.severity == DiagnosticSeverity::Error));
571 assert_eq!(first.diagnostics.len(), second.diagnostics.len());
572 assert_eq!(db.stats().typecheck_runs, 1);
573 }
574
575 #[test]
576 fn typecheck_facts_keep_shadowed_inferred_bindings_distinct() {
577 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";
578 let program = crate::parse_source(source).expect("parse");
579
580 let facts = TypeCheckConfig::new().check_with_facts(&program, source);
581 let values = facts
582 .binding_types
583 .iter()
584 .filter(|binding| binding.name == "value")
585 .collect::<Vec<_>>();
586
587 assert_eq!(values.len(), 2, "{:#?}", facts.binding_types);
588 assert_ne!(values[0].span, values[1].span);
589 assert_eq!(crate::format_type(&values[0].type_expr), "HarnessAgent");
590 assert_eq!(crate::format_type(&values[1].type_expr), "string");
591 }
592}