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