mod collect;
mod resolve;
#[cfg(test)]
mod tests;
use std::collections::HashMap;
use crate::diagnostic::{Diagnostic, Severity};
use crate::parser::phenotyper_actions as ast;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TypeId(pub usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct EnumId(pub usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AliasId(pub usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FieldId(pub usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PrimitiveType {
String,
Int64,
Real64,
Bool,
Date,
Time,
DateTime,
}
#[derive(Debug, Clone)]
pub enum Symbol {
Phenotype(TypeId),
PluralCompanion { type_id: TypeId },
Enum(EnumId),
Alias(AliasId),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImportedKind {
Phenotype,
Plural,
Enum,
Alias,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImportedSymbol {
pub namespace: String,
pub name: String,
pub kind: ImportedKind,
}
#[derive(Debug, Clone)]
pub enum ImportResolution {
One(ImportedSymbol),
Ambiguous(Vec<String>),
}
#[derive(Debug, Clone, Default)]
pub struct NamespaceExports {
pub namespace: String,
pub names: HashMap<String, ImportedKind>,
}
#[derive(Debug, Clone)]
pub enum ResolvedSymbol<'a> {
Local(&'a Symbol),
Imported(&'a ImportedSymbol),
Ambiguous(&'a [String]),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Requiredness {
Required,
Optional,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Cardinality {
One,
OneOrMore,
ZeroOrMore,
}
#[derive(Debug, Clone)]
pub struct TypeInfo {
pub id: TypeId,
pub singular_name: String,
pub plural_name: Option<String>,
pub fields: Vec<FieldInfo>,
}
#[derive(Debug, Clone)]
pub struct FieldInfo {
pub id: FieldId,
pub name: String,
pub requiredness: Requiredness,
}
#[derive(Debug, Clone)]
pub struct EnumInfo {
pub id: EnumId,
pub name: String,
pub members: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct AliasInfo {
pub id: AliasId,
pub name: String,
}
#[derive(Debug)]
pub struct SymbolTable {
pub namespace: Vec<String>,
pub types: Vec<TypeInfo>,
pub enums: Vec<EnumInfo>,
pub aliases: Vec<AliasInfo>,
names: HashMap<String, Symbol>,
imports: HashMap<String, ImportResolution>,
}
impl SymbolTable {
pub fn resolve(&self, name: &str) -> Option<&Symbol> {
self.names.get(name)
}
pub fn resolve_any(&self, name: &str) -> Option<ResolvedSymbol<'_>> {
if let Some(sym) = self.names.get(name) {
return Some(ResolvedSymbol::Local(sym));
}
match self.imports.get(name) {
Some(ImportResolution::One(is)) => Some(ResolvedSymbol::Imported(is)),
Some(ImportResolution::Ambiguous(nss)) => Some(ResolvedSymbol::Ambiguous(nss)),
None => None,
}
}
fn imported(&self, name: &str) -> Option<&ImportResolution> {
self.imports.get(name)
}
pub fn exports(&self) -> NamespaceExports {
let mut names = HashMap::new();
for (name, sym) in &self.names {
let kind = match sym {
Symbol::Phenotype(_) => ImportedKind::Phenotype,
Symbol::PluralCompanion { .. } => ImportedKind::Plural,
Symbol::Enum(_) => ImportedKind::Enum,
Symbol::Alias(_) => ImportedKind::Alias,
};
names.insert(name.clone(), kind);
}
NamespaceExports {
namespace: self.namespace.join("/"),
names,
}
}
pub fn resolve_field(&self, type_id: TypeId, field_name: &str) -> Option<&FieldInfo> {
self.types
.get(type_id.0)
.and_then(|ti| ti.fields.iter().find(|f| f.name == field_name))
}
pub fn type_info(&self, id: TypeId) -> Option<&TypeInfo> {
self.types.get(id.0)
}
pub fn enum_info(&self, id: EnumId) -> Option<&EnumInfo> {
self.enums.get(id.0)
}
}
pub fn build(ast: &ast::File, file: &str) -> (SymbolTable, Vec<Diagnostic>) {
build_with_imports(ast, file, &[])
}
pub fn build_with_imports(
ast: &ast::File,
file: &str,
available: &[NamespaceExports],
) -> (SymbolTable, Vec<Diagnostic>) {
let mut diags = Vec::new();
let mut table = SymbolTable {
namespace: ast.ns.path.clone(),
types: Vec::new(),
enums: Vec::new(),
aliases: Vec::new(),
names: HashMap::new(),
imports: HashMap::new(),
};
populate_imports(ast, &mut table, file, available, &mut diags);
collect::collect_symbols(ast, &mut table, file, &mut diags);
resolve::resolve_references(ast, &mut table, file, &mut diags);
(table, diags)
}
fn populate_imports(
ast: &ast::File,
table: &mut SymbolTable,
file: &str,
available: &[NamespaceExports],
diags: &mut Vec<Diagnostic>,
) {
let Some(ref uses) = ast.ns.uses else {
return;
};
let own_ns = table.namespace.join("/");
let mut seen: Vec<String> = Vec::new();
for decl in uses {
let ns = decl.path.join("/");
if ns == own_ns {
diags.push(warning(
file,
format!("`uses {ns};` names this file's own namespace and has no effect"),
));
continue;
}
if seen.contains(&ns) {
diags.push(warning(file, format!("duplicate `uses {ns};`")));
continue;
}
seen.push(ns.clone());
let Some(exports) = available.iter().find(|e| e.namespace == ns) else {
diags.push(error(
file,
format!(
"cannot resolve `uses {ns};` — the namespace is not loaded \
(compile with search roots that contain `{ns}.pht` or `{ns}.md`)"
),
));
continue;
};
for (name, kind) in &exports.names {
match table.imports.get_mut(name) {
None => {
table.imports.insert(
name.clone(),
ImportResolution::One(ImportedSymbol {
namespace: ns.clone(),
name: name.clone(),
kind: *kind,
}),
);
}
Some(ImportResolution::One(existing)) => {
let first_ns = existing.namespace.clone();
table.imports.insert(
name.clone(),
ImportResolution::Ambiguous(vec![first_ns, ns.clone()]),
);
}
Some(ImportResolution::Ambiguous(nss)) => {
nss.push(ns.clone());
}
}
}
}
}
fn error(file: &str, summary: String) -> Diagnostic {
Diagnostic {
severity: Severity::Error,
summary,
file: file.to_string(),
line: 0,
col: 0,
explanation: None,
suggestion: None,
}
}
fn warning(file: &str, summary: String) -> Diagnostic {
Diagnostic {
severity: Severity::Warning,
summary,
file: file.to_string(),
line: 0,
col: 0,
explanation: None,
suggestion: None,
}
}