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;
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
188#[derive(Debug, Clone, PartialEq, Eq, Hash)]
189struct TypeCheckCacheKey {
190 strict_types: bool,
191 privileged_wire_builtins: bool,
192 imported_names: Option<Vec<String>>,
193 imported_type_decls_digest: u64,
194 imported_callable_decls_digest: u64,
195 namespace_imports_digest: u64,
196}
197
198#[derive(Debug, Clone)]
199struct CachedTypeCheck {
200 diagnostics: Vec<TypeDiagnostic>,
201 inlay_hints: Vec<InlayHintInfo>,
202}
203
204#[derive(Debug, Clone)]
205struct SourceEntry {
206 source: String,
207 version: SourceVersion,
208 digest: SourceDigest,
209 tokens: Option<Result<Vec<Token>, LexerError>>,
210 program: Option<Result<Vec<SNode>, Vec<ParserError>>>,
211 typechecks: HashMap<TypeCheckCacheKey, CachedTypeCheck>,
212}
213
214impl SourceEntry {
215 fn new(source: String, version: SourceVersion, digest: SourceDigest) -> Self {
216 Self {
217 source,
218 version,
219 digest,
220 tokens: None,
221 program: None,
222 typechecks: HashMap::new(),
223 }
224 }
225
226 fn replace_source(&mut self, source: String, version: SourceVersion, digest: SourceDigest) {
227 self.source = source;
228 self.version = version;
229 self.digest = digest;
230 self.tokens = None;
231 self.program = None;
232 self.typechecks.clear();
233 }
234}
235
236#[derive(Debug, Default)]
238pub struct AnalysisDatabase {
239 entries: HashMap<SourceId, SourceEntry>,
240 stats: AnalysisStats,
241}
242
243impl AnalysisDatabase {
244 pub fn new() -> Self {
245 Self::default()
246 }
247
248 pub fn stats(&self) -> AnalysisStats {
249 self.stats.clone()
250 }
251
252 pub fn set_source(
253 &mut self,
254 id: SourceId,
255 source: String,
256 version: SourceVersion,
257 ) -> SourceUpdate {
258 let digest = SourceDigest::from_source(&source);
259 match self.entries.get_mut(&id) {
260 None => {
261 self.entries
262 .insert(id, SourceEntry::new(source, version, digest));
263 SourceUpdate::Inserted
264 }
265 Some(entry) if entry.digest == digest => {
266 entry.version = version;
267 SourceUpdate::Unchanged
268 }
269 Some(entry) => {
270 entry.replace_source(source, version, digest);
271 SourceUpdate::Changed
272 }
273 }
274 }
275
276 pub fn set_parsed_source(
277 &mut self,
278 id: SourceId,
279 source: String,
280 version: SourceVersion,
281 program: Vec<SNode>,
282 ) -> SourceUpdate {
283 let digest = SourceDigest::from_source(&source);
284 match self.entries.get_mut(&id) {
285 None => {
286 let mut entry = SourceEntry::new(source, version, digest);
287 entry.program = Some(Ok(program));
288 self.entries.insert(id, entry);
289 SourceUpdate::Inserted
290 }
291 Some(entry) if entry.digest == digest => {
292 entry.version = version;
293 entry.program = Some(Ok(program));
294 SourceUpdate::Unchanged
295 }
296 Some(entry) => {
297 entry.replace_source(source, version, digest);
298 entry.program = Some(Ok(program));
299 SourceUpdate::Changed
300 }
301 }
302 }
303
304 pub fn parse(&mut self, id: &SourceId) -> Result<ParseOutput, AnalysisError> {
305 if let Some(entry) = self.entries.get(id) {
306 if let Some(program) = &entry.program {
307 return match program {
308 Ok(program) => Ok(ParseOutput {
309 source: entry.source.clone(),
310 program: program.clone(),
311 }),
312 Err(errors) => Err(AnalysisError::Parse {
313 source: entry.source.clone(),
314 errors: errors.clone(),
315 }),
316 };
317 }
318 }
319
320 let mut lexed = false;
321 let mut parsed_now = false;
322 let entry = self.entry_mut(id)?;
323 if entry.tokens.is_none() {
324 lexed = true;
325 let mut lexer = Lexer::new(&entry.source);
326 entry.tokens = Some(lexer.tokenize());
327 }
328 let tokens = match entry.tokens.as_ref().expect("tokens initialized") {
329 Ok(tokens) => tokens.clone(),
330 Err(error) => {
331 let source = entry.source.clone();
332 let error = error.clone();
333 if lexed {
334 self.stats.lex_runs += 1;
335 }
336 return Err(AnalysisError::Lex { source, error });
337 }
338 };
339
340 if entry.program.is_none() {
341 parsed_now = true;
342 let mut parser = Parser::new(tokens);
343 entry.program = Some(match parser.parse() {
344 Ok(program) => Ok(program),
345 Err(error) => {
346 let mut errors = parser.all_errors().to_vec();
347 if errors.is_empty() {
348 errors.push(error);
349 }
350 Err(errors)
351 }
352 });
353 }
354
355 let result = match entry.program.as_ref().expect("program initialized") {
356 Ok(program) => Ok(ParseOutput {
357 source: entry.source.clone(),
358 program: program.clone(),
359 }),
360 Err(errors) => Err(AnalysisError::Parse {
361 source: entry.source.clone(),
362 errors: errors.clone(),
363 }),
364 };
365 if lexed {
366 self.stats.lex_runs += 1;
367 }
368 if parsed_now {
369 self.stats.parse_runs += 1;
370 }
371 result
372 }
373
374 pub fn typecheck(
375 &mut self,
376 id: &SourceId,
377 config: TypeCheckConfig,
378 ) -> Result<TypeCheckOutput, AnalysisError> {
379 let parsed = self.parse(id)?;
380 let key = config.cache_key();
381 if let Some(cached) = self
382 .entries
383 .get(id)
384 .expect("parse verified source entry")
385 .typechecks
386 .get(&key)
387 {
388 return Ok(TypeCheckOutput {
389 source: parsed.source,
390 program: parsed.program,
391 diagnostics: cached.diagnostics.clone(),
392 inlay_hints: cached.inlay_hints.clone(),
393 });
394 }
395
396 self.stats.typecheck_runs += 1;
397 let (diagnostics, inlay_hints) = config
398 .build_checker()
399 .check_with_hints(&parsed.program, &parsed.source);
400 let cached = CachedTypeCheck {
401 diagnostics: diagnostics.clone(),
402 inlay_hints: inlay_hints.clone(),
403 };
404 self.entries
405 .get_mut(id)
406 .expect("parse verified source entry")
407 .typechecks
408 .insert(key, cached);
409 Ok(TypeCheckOutput {
410 source: parsed.source,
411 program: parsed.program,
412 diagnostics,
413 inlay_hints,
414 })
415 }
416
417 fn entry_mut(&mut self, id: &SourceId) -> Result<&mut SourceEntry, AnalysisError> {
418 self.entries
419 .get_mut(id)
420 .ok_or_else(|| AnalysisError::MissingSource(id.clone()))
421 }
422}
423
424fn debug_digest<T: std::fmt::Debug>(value: &T) -> u64 {
425 let mut hasher = StableHasher::default();
426 format!("{value:?}").hash(&mut hasher);
427 hasher.finish()
428}
429
430#[derive(Default)]
431struct StableHasher(u64);
432
433impl Hasher for StableHasher {
434 fn finish(&self) -> u64 {
435 self.0
436 }
437
438 fn write(&mut self, bytes: &[u8]) {
439 let mut hash = if self.0 == 0 {
440 0xcbf29ce484222325u64
441 } else {
442 self.0
443 };
444 for byte in bytes {
445 hash ^= u64::from(*byte);
446 hash = hash.wrapping_mul(0x100000001b3);
447 }
448 self.0 = hash;
449 }
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455 use crate::DiagnosticSeverity;
456
457 fn source_id() -> SourceId {
458 SourceId::new("test.harn")
459 }
460
461 #[test]
462 fn parse_reuses_cached_program_for_unchanged_source() {
463 let mut db = AnalysisDatabase::new();
464 let id = source_id();
465 assert_eq!(
466 db.set_source(id.clone(), "const x = 1\n".to_string(), SourceVersion(1)),
467 SourceUpdate::Inserted
468 );
469 db.parse(&id).expect("initial parse");
470 db.parse(&id).expect("cached parse");
471 assert_eq!(db.stats().lex_runs, 1);
472 assert_eq!(db.stats().parse_runs, 1);
473
474 assert_eq!(
475 db.set_source(id.clone(), "const x = 1\n".to_string(), SourceVersion(2)),
476 SourceUpdate::Unchanged
477 );
478 db.parse(&id).expect("same digest parse");
479 assert_eq!(db.stats().lex_runs, 1);
480 assert_eq!(db.stats().parse_runs, 1);
481 }
482
483 #[test]
484 fn source_change_invalidates_parse_and_typecheck_outputs() {
485 let mut db = AnalysisDatabase::new();
486 let id = source_id();
487 db.set_source(id.clone(), "const x = 1\n".to_string(), SourceVersion(1));
488 db.typecheck(&id, TypeCheckConfig::new())
489 .expect("initial check");
490 assert_eq!(
491 db.set_source(id.clone(), "const x = 2\n".to_string(), SourceVersion(2)),
492 SourceUpdate::Changed
493 );
494 db.typecheck(&id, TypeCheckConfig::new())
495 .expect("changed check");
496 assert_eq!(db.stats().lex_runs, 2);
497 assert_eq!(db.stats().parse_runs, 2);
498 assert_eq!(db.stats().typecheck_runs, 2);
499 }
500
501 #[test]
502 fn parsed_source_seed_skips_lex_and_parse() {
503 let mut db = AnalysisDatabase::new();
504 let id = source_id();
505 let source = "const x = 1\n".to_string();
506 let mut lexer = Lexer::new(&source);
507 let tokens = lexer.tokenize().expect("tokenize");
508 let mut parser = Parser::new(tokens);
509 let program = parser.parse().expect("parse");
510
511 assert_eq!(
512 db.set_parsed_source(id.clone(), source, SourceVersion(1), program),
513 SourceUpdate::Inserted
514 );
515 db.typecheck(&id, TypeCheckConfig::new())
516 .expect("seeded check");
517 assert_eq!(db.stats().lex_runs, 0);
518 assert_eq!(db.stats().parse_runs, 0);
519 assert_eq!(db.stats().typecheck_runs, 1);
520 }
521
522 #[test]
523 fn typecheck_cache_is_keyed_by_options() {
524 let mut db = AnalysisDatabase::new();
525 let id = source_id();
526 db.set_source(
527 id.clone(),
528 "pipeline main() {\n const x = read_file(\"a\")\n log(x.foo)\n}\n".to_string(),
529 SourceVersion(1),
530 );
531 db.typecheck(&id, TypeCheckConfig::new())
532 .expect("default check");
533 db.typecheck(&id, TypeCheckConfig::new())
534 .expect("cached default check");
535 db.typecheck(&id, TypeCheckConfig::new().with_strict_types(true))
536 .expect("strict check");
537 assert_eq!(db.stats().typecheck_runs, 2);
538 }
539
540 #[test]
541 fn typecheck_diagnostics_are_cached_with_hints() {
542 let mut db = AnalysisDatabase::new();
543 let id = source_id();
544 db.set_source(
545 id.clone(),
546 "pipeline main() {\n const x: int = \"nope\"\n}\n".to_string(),
547 SourceVersion(1),
548 );
549 let first = db.typecheck(&id, TypeCheckConfig::new()).expect("check");
550 let second = db.typecheck(&id, TypeCheckConfig::new()).expect("cached");
551 assert!(first
552 .diagnostics
553 .iter()
554 .any(|diag| diag.severity == DiagnosticSeverity::Error));
555 assert_eq!(first.diagnostics.len(), second.diagnostics.len());
556 assert_eq!(db.stats().typecheck_runs, 1);
557 }
558}