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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
30pub struct SourceVersion(pub u64);
31
32#[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 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#[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}