use std::collections::{HashMap, HashSet};
use std::hash::Hasher;
use std::io::Write;
use std::path::Path;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use harn_lexer::{Lexer, LexerError};
use crate::{BindingTypeInfo, InlayHintInfo, PredicateSite, TypeCheckFacts};
use crate::{Parser, ParserError, SNode, TypeChecker, TypeDiagnostic};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SourceId(String);
impl SourceId {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
pub fn path(path: &Path) -> Self {
Self(path.to_string_lossy().into_owned())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SourceVersion(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SourceDigest(u64);
impl SourceDigest {
pub fn from_source(source: &str) -> Self {
let mut hash = 0xcbf29ce484222325u64;
for byte in source.as_bytes() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x100000001b3);
}
Self(hash)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceUpdate {
Inserted,
Changed,
Unchanged,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AnalysisStats {
pub lex_runs: usize,
pub parse_runs: usize,
pub typecheck_runs: usize,
}
static PARSE_RUNS: AtomicUsize = AtomicUsize::new(0);
static TYPECHECK_RUNS: AtomicUsize = AtomicUsize::new(0);
pub(crate) fn record_parse_run() {
PARSE_RUNS.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn record_typecheck_run() {
TYPECHECK_RUNS.fetch_add(1, Ordering::Relaxed);
}
pub fn process_stats() -> AnalysisStats {
AnalysisStats {
lex_runs: harn_lexer::process_tokenize_runs(),
parse_runs: PARSE_RUNS.load(Ordering::Relaxed),
typecheck_runs: TYPECHECK_RUNS.load(Ordering::Relaxed),
}
}
#[derive(Debug, Clone)]
pub struct ParseOutput {
pub source: Arc<str>,
pub program: Arc<Vec<SNode>>,
}
#[derive(Debug, Clone)]
pub struct TypeCheckOutput {
pub source: Arc<str>,
pub program: Arc<Vec<SNode>>,
pub diagnostics: Arc<Vec<TypeDiagnostic>>,
pub inlay_hints: Arc<Vec<InlayHintInfo>>,
pub binding_types: Arc<Vec<BindingTypeInfo>>,
pub predicate_sites: Arc<Vec<PredicateSite>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AnalysisError {
MissingSource(SourceId),
Lex {
source: String,
error: LexerError,
},
Parse {
source: String,
errors: Vec<ParserError>,
},
}
impl AnalysisError {
pub fn source(&self) -> Option<&str> {
match self {
AnalysisError::MissingSource(_) => None,
AnalysisError::Lex { source, .. } | AnalysisError::Parse { source, .. } => Some(source),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct TypeCheckConfig {
pub strict_types: bool,
pub privileged_wire_builtins: bool,
pub imported_names: Option<HashSet<String>>,
pub imported_type_decls: Vec<SNode>,
pub imported_callable_decls: Vec<SNode>,
pub namespace_imports: Vec<(String, crate::NamespaceImportBinding)>,
}
impl TypeCheckConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_strict_types(mut self, strict_types: bool) -> Self {
self.strict_types = strict_types;
self
}
pub fn with_privileged_wire_builtins(mut self, enabled: bool) -> Self {
self.privileged_wire_builtins = enabled;
self
}
pub fn with_imported_names(mut self, imported_names: Option<HashSet<String>>) -> Self {
self.imported_names = imported_names;
self
}
pub fn with_imported_type_decls(mut self, imported_type_decls: Vec<SNode>) -> Self {
self.imported_type_decls = imported_type_decls;
self
}
pub fn with_imported_callable_decls(mut self, imported_callable_decls: Vec<SNode>) -> Self {
self.imported_callable_decls = imported_callable_decls;
self
}
pub fn with_namespace_imports(
mut self,
namespace_imports: Vec<(String, crate::NamespaceImportBinding)>,
) -> Self {
self.namespace_imports = namespace_imports;
self
}
fn cache_key(&self) -> TypeCheckCacheKey {
let mut imported_names = self
.imported_names
.as_ref()
.map(|names| names.iter().cloned().collect::<Vec<_>>());
if let Some(names) = &mut imported_names {
names.sort();
}
let mut namespace_imports: Vec<_> = self.namespace_imports.iter().collect();
namespace_imports.sort_by(|a, b| a.0.cmp(&b.0));
TypeCheckCacheKey {
strict_types: self.strict_types,
privileged_wire_builtins: self.privileged_wire_builtins,
imported_names,
imported_type_decls_digest: structural_digest(&self.imported_type_decls),
imported_callable_decls_digest: structural_digest(&self.imported_callable_decls),
namespace_imports_digest: structural_digest(&namespace_imports),
}
}
fn into_checker(self) -> TypeChecker {
let checker = TypeChecker::with_strict_types(self.strict_types)
.with_privileged_wire_builtins(self.privileged_wire_builtins);
self.apply_imports_to(checker)
}
pub fn apply_imports_to(self, mut checker: TypeChecker) -> TypeChecker {
if let Some(imported) = self.imported_names {
checker = checker.with_imported_names(imported);
}
if !self.imported_type_decls.is_empty() {
checker = checker.with_imported_type_decls(self.imported_type_decls);
}
if !self.imported_callable_decls.is_empty() {
checker = checker.with_imported_callable_decls(self.imported_callable_decls);
}
if !self.namespace_imports.is_empty() {
checker = checker.with_namespace_imports(self.namespace_imports);
}
checker
}
pub fn check_with_facts(&self, program: &[SNode], source: &str) -> TypeCheckFacts {
self.clone()
.into_checker()
.check_with_facts(program, source)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct TypeCheckCacheKey {
strict_types: bool,
privileged_wire_builtins: bool,
imported_names: Option<Vec<String>>,
imported_type_decls_digest: u64,
imported_callable_decls_digest: u64,
namespace_imports_digest: u64,
}
#[derive(Debug, Clone)]
struct CachedTypeCheck {
diagnostics: Arc<Vec<TypeDiagnostic>>,
inlay_hints: Arc<Vec<InlayHintInfo>>,
binding_types: Arc<Vec<BindingTypeInfo>>,
predicate_sites: Arc<Vec<PredicateSite>>,
}
#[derive(Debug, Clone)]
struct SourceEntry {
source: Arc<str>,
version: SourceVersion,
digest: SourceDigest,
lex_error: Option<LexerError>,
program: Option<Result<Arc<Vec<SNode>>, Vec<ParserError>>>,
typechecks: HashMap<TypeCheckCacheKey, CachedTypeCheck>,
}
impl SourceEntry {
fn new(source: Arc<str>, version: SourceVersion, digest: SourceDigest) -> Self {
Self {
source,
version,
digest,
lex_error: None,
program: None,
typechecks: HashMap::new(),
}
}
fn replace_source(&mut self, source: Arc<str>, version: SourceVersion, digest: SourceDigest) {
self.source = source;
self.version = version;
self.digest = digest;
self.lex_error = None;
self.program = None;
self.typechecks.clear();
}
}
#[derive(Debug, Default)]
pub struct AnalysisDatabase {
entries: HashMap<SourceId, SourceEntry>,
stats: AnalysisStats,
}
impl AnalysisDatabase {
pub fn new() -> Self {
Self::default()
}
pub fn stats(&self) -> AnalysisStats {
self.stats.clone()
}
pub fn remove_source(&mut self, id: &SourceId) -> bool {
self.entries.remove(id).is_some()
}
pub fn set_source(
&mut self,
id: SourceId,
source: String,
version: SourceVersion,
) -> SourceUpdate {
let digest = SourceDigest::from_source(&source);
match self.entries.get_mut(&id) {
None => {
self.entries
.insert(id, SourceEntry::new(source.into(), version, digest));
SourceUpdate::Inserted
}
Some(entry) if entry.digest == digest => {
entry.version = version;
SourceUpdate::Unchanged
}
Some(entry) => {
entry.replace_source(source.into(), version, digest);
SourceUpdate::Changed
}
}
}
pub fn set_parsed_source(
&mut self,
id: SourceId,
source: String,
version: SourceVersion,
program: Vec<SNode>,
) -> SourceUpdate {
let digest = SourceDigest::from_source(&source);
match self.entries.get_mut(&id) {
None => {
let mut entry = SourceEntry::new(source.into(), version, digest);
entry.program = Some(Ok(Arc::new(program)));
self.entries.insert(id, entry);
SourceUpdate::Inserted
}
Some(entry) if entry.digest == digest => {
entry.version = version;
entry.program = Some(Ok(Arc::new(program)));
SourceUpdate::Unchanged
}
Some(entry) => {
entry.replace_source(source.into(), version, digest);
entry.program = Some(Ok(Arc::new(program)));
SourceUpdate::Changed
}
}
}
pub fn parse(&mut self, id: &SourceId) -> Result<ParseOutput, AnalysisError> {
let entry = self
.entries
.get_mut(id)
.ok_or_else(|| AnalysisError::MissingSource(id.clone()))?;
if entry.program.is_none() {
if let Some(error) = &entry.lex_error {
return Err(AnalysisError::Lex {
source: entry.source.to_string(),
error: error.clone(),
});
}
self.stats.lex_runs += 1;
let tokens = Lexer::new(&entry.source).tokenize().map_err(|error| {
entry.lex_error = Some(error.clone());
AnalysisError::Lex {
source: entry.source.to_string(),
error,
}
})?;
self.stats.parse_runs += 1;
let mut parser = Parser::new(tokens);
entry.program = Some(parser.parse().map(Arc::new).map_err(|error| {
let mut errors = parser.all_errors().to_vec();
if errors.is_empty() {
errors.push(error);
}
errors
}));
}
match entry.program.as_ref().expect("program initialized") {
Ok(program) => Ok(ParseOutput {
source: Arc::clone(&entry.source),
program: Arc::clone(program),
}),
Err(errors) => Err(AnalysisError::Parse {
source: entry.source.to_string(),
errors: errors.clone(),
}),
}
}
pub fn typecheck(
&mut self,
id: &SourceId,
config: TypeCheckConfig,
) -> Result<TypeCheckOutput, AnalysisError> {
let parsed = self.parse(id)?;
let key = config.cache_key();
if let Some(cached) = self
.entries
.get(id)
.expect("parse verified source entry")
.typechecks
.get(&key)
{
return Ok(TypeCheckOutput::from_cache(parsed, cached));
}
self.stats.typecheck_runs += 1;
let TypeCheckFacts {
diagnostics,
inlay_hints,
binding_types,
predicate_sites,
} = config
.into_checker()
.check_with_facts(&parsed.program, &parsed.source);
let cached = CachedTypeCheck {
diagnostics: Arc::new(diagnostics),
inlay_hints: Arc::new(inlay_hints),
binding_types: Arc::new(binding_types),
predicate_sites: Arc::new(predicate_sites),
};
let output = TypeCheckOutput::from_cache(parsed, &cached);
self.entries
.get_mut(id)
.expect("parse verified source entry")
.typechecks
.insert(key, cached);
Ok(output)
}
}
impl TypeCheckOutput {
fn from_cache(parsed: ParseOutput, cached: &CachedTypeCheck) -> Self {
Self {
source: parsed.source,
program: parsed.program,
diagnostics: Arc::clone(&cached.diagnostics),
inlay_hints: Arc::clone(&cached.inlay_hints),
binding_types: Arc::clone(&cached.binding_types),
predicate_sites: Arc::clone(&cached.predicate_sites),
}
}
}
fn structural_digest<T: serde::Serialize>(value: &T) -> u64 {
let mut hasher = StableHasher::default();
serde_json::to_writer(HasherWriter(&mut hasher), value)
.expect("typecheck cache-key values are JSON-serializable");
hasher.finish()
}
struct HasherWriter<'a>(&'a mut StableHasher);
impl Write for HasherWriter<'_> {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
self.0.write(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[derive(Default)]
struct StableHasher(u64);
impl Hasher for StableHasher {
fn finish(&self) -> u64 {
self.0
}
fn write(&mut self, bytes: &[u8]) {
let mut hash = if self.0 == 0 {
0xcbf29ce484222325u64
} else {
self.0
};
for byte in bytes {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x100000001b3);
}
self.0 = hash;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{DiagnosticSeverity, TypeExpr};
fn source_id() -> SourceId {
SourceId::new("test.harn")
}
#[test]
fn parse_reuses_cached_program_for_unchanged_source() {
let mut db = AnalysisDatabase::new();
let id = source_id();
assert_eq!(
db.set_source(id.clone(), "const x = 1\n".to_string(), SourceVersion(1)),
SourceUpdate::Inserted
);
db.parse(&id).expect("initial parse");
db.parse(&id).expect("cached parse");
assert_eq!(db.stats().lex_runs, 1);
assert_eq!(db.stats().parse_runs, 1);
assert_eq!(
db.set_source(id.clone(), "const x = 1\n".to_string(), SourceVersion(2)),
SourceUpdate::Unchanged
);
db.parse(&id).expect("same digest parse");
assert_eq!(db.stats().lex_runs, 1);
assert_eq!(db.stats().parse_runs, 1);
}
#[test]
fn namespace_signature_changes_invalidate_cached_typechecks() {
let mut db = AnalysisDatabase::new();
let id = source_id();
db.set_source(
id.clone(),
"import * as api from \"api\"\napi.take(\"text\")\n".into(),
SourceVersion(1),
);
let config = |accepts: &str| {
TypeCheckConfig::new().with_namespace_imports(vec![(
"api".into(),
crate::NamespaceImportBinding {
module_path: "api".into(),
members: std::collections::BTreeSet::from(["take".into()]),
member_types: std::collections::BTreeMap::from([(
"take".into(),
TypeExpr::FnType {
params: vec![TypeExpr::Named(accepts.into())],
return_type: Box::new(TypeExpr::Named("nil".into())),
},
)]),
member_param_names: std::collections::BTreeMap::from([(
"take".into(),
vec!["input".into()],
)]),
member_required_params: std::collections::BTreeMap::from([("take".into(), 1)]),
member_type_predicates: Default::default(),
},
)])
};
assert!(db
.typecheck(&id, config("string"))
.unwrap()
.diagnostics
.is_empty());
db.typecheck(&id, config("string")).unwrap();
assert_eq!(
db.stats().typecheck_runs,
1,
"the unchanged contract must hit the cache"
);
assert!(db
.typecheck(&id, config("int"))
.unwrap()
.diagnostics
.iter()
.any(|diagnostic| diagnostic.severity == DiagnosticSeverity::Error));
assert_eq!(db.stats().typecheck_runs, 2);
}
#[test]
fn cached_results_are_shared_not_copied() {
let mut db = AnalysisDatabase::new();
let id = source_id();
db.set_source(id.clone(), "const x = 1\n".to_string(), SourceVersion(1));
let parsed = db.parse(&id).expect("parse");
let checked = db.typecheck(&id, TypeCheckConfig::new()).expect("check");
let again = db
.typecheck(&id, TypeCheckConfig::new())
.expect("cached check");
assert!(Arc::ptr_eq(&parsed.program, &checked.program));
assert!(Arc::ptr_eq(&parsed.source, &again.source));
assert!(Arc::ptr_eq(&checked.binding_types, &again.binding_types));
assert!(Arc::ptr_eq(&checked.diagnostics, &again.diagnostics));
}
#[test]
fn failed_analysis_is_cached_until_source_changes() {
for (source, parses) in [("#", 0), ("const =", 1)] {
let mut db = AnalysisDatabase::new();
let id = source_id();
db.set_source(id.clone(), source.into(), SourceVersion(1));
let first = db.parse(&id).unwrap_err();
let second = db.parse(&id).unwrap_err();
assert_eq!(first, second);
assert_eq!(db.stats().lex_runs, 1);
assert_eq!(db.stats().parse_runs, parses);
db.set_source(id.clone(), "const x = 1\n".into(), SourceVersion(2));
assert_eq!(db.parse(&id).unwrap().program.len(), 1);
assert_eq!(db.stats().lex_runs, 2);
assert_eq!(db.stats().parse_runs, parses + 1);
}
}
#[test]
fn source_change_invalidates_parse_and_typecheck_outputs() {
let mut db = AnalysisDatabase::new();
let id = source_id();
db.set_source(id.clone(), "const x = 1\n".to_string(), SourceVersion(1));
db.typecheck(&id, TypeCheckConfig::new())
.expect("initial check");
assert_eq!(
db.set_source(id.clone(), "const x = 2\n".to_string(), SourceVersion(2)),
SourceUpdate::Changed
);
db.typecheck(&id, TypeCheckConfig::new())
.expect("changed check");
assert_eq!(db.stats().lex_runs, 2);
assert_eq!(db.stats().parse_runs, 2);
assert_eq!(db.stats().typecheck_runs, 2);
}
#[test]
fn parsed_source_seed_skips_lex_and_parse() {
let mut db = AnalysisDatabase::new();
let id = source_id();
let source = "const x = 1\n".to_string();
let mut lexer = Lexer::new(&source);
let tokens = lexer.tokenize().expect("tokenize");
let mut parser = Parser::new(tokens);
let program = parser.parse().expect("parse");
assert_eq!(
db.set_parsed_source(id.clone(), source, SourceVersion(1), program),
SourceUpdate::Inserted
);
db.typecheck(&id, TypeCheckConfig::new())
.expect("seeded check");
assert_eq!(db.stats().lex_runs, 0);
assert_eq!(db.stats().parse_runs, 0);
assert_eq!(db.stats().typecheck_runs, 1);
}
#[test]
fn typecheck_cache_is_keyed_by_options() {
let mut db = AnalysisDatabase::new();
let id = source_id();
db.set_source(
id.clone(),
"pipeline main() {\n const x = read_file(\"a\")\n log(x.foo)\n}\n".to_string(),
SourceVersion(1),
);
db.typecheck(&id, TypeCheckConfig::new())
.expect("default check");
db.typecheck(&id, TypeCheckConfig::new())
.expect("cached default check");
db.typecheck(&id, TypeCheckConfig::new().with_strict_types(true))
.expect("strict check");
assert_eq!(db.stats().typecheck_runs, 2);
}
#[test]
fn typecheck_diagnostics_are_cached_with_hints() {
let mut db = AnalysisDatabase::new();
let id = source_id();
db.set_source(
id.clone(),
"pipeline main() {\n const x: int = \"nope\"\n}\n".to_string(),
SourceVersion(1),
);
let first = db.typecheck(&id, TypeCheckConfig::new()).expect("check");
let second = db.typecheck(&id, TypeCheckConfig::new()).expect("cached");
assert!(first
.diagnostics
.iter()
.any(|diag| diag.severity == DiagnosticSeverity::Error));
assert_eq!(first.diagnostics.len(), second.diagnostics.len());
assert_eq!(db.stats().typecheck_runs, 1);
}
#[test]
fn typecheck_facts_keep_shadowed_inferred_bindings_distinct() {
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";
let program = crate::parse_source(source).expect("parse");
let facts = TypeCheckConfig::new().check_with_facts(&program, source);
let values = facts
.binding_types
.iter()
.filter(|binding| binding.name == "value")
.collect::<Vec<_>>();
assert_eq!(values.len(), 2, "{:#?}", facts.binding_types);
assert_ne!(values[0].span, values[1].span);
assert_eq!(crate::format_type(&values[0].type_expr), "HarnessAgent");
assert_eq!(crate::format_type(&values[1].type_expr), "string");
}
}