use {
crate::core::Ident,
std::{
fmt::{self, Debug},
hash::Hash,
},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum SymbolVisibility {
#[default]
Private,
Public,
}
pub trait Value<I: Ident>:
bluegum::Bluegum + Debug + Clone + PartialEq + Hash + Send + Sync
{
fn span(&self) -> Option<laburnum::Span>;
}
#[derive(Debug, Clone, PartialEq)]
pub enum DefaultValue {
String(String),
Integer(i64),
Float(f64),
Boolean(bool),
}
impl Hash for DefaultValue {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
core::mem::discriminant(self).hash(state);
match self {
| DefaultValue::String(s) => s.hash(state),
| DefaultValue::Integer(i) => i.hash(state),
| DefaultValue::Float(f) => f.to_bits().hash(state),
| DefaultValue::Boolean(b) => b.hash(state),
}
}
}
impl<I: Ident> Value<I> for DefaultValue {
fn span(&self) -> Option<laburnum::Span> {
None
}
}
impl bluegum::Bluegum for DefaultValue {
fn node(&self, b: &mut bluegum::Builder) {
match self {
| DefaultValue::String(s) => b.name("String").field("value", s),
| DefaultValue::Integer(i) => b.name("Integer").field("value", i),
| DefaultValue::Float(f) => b.name("Float").field("value", f),
| DefaultValue::Boolean(v) => b.name("Boolean").field("value", v),
};
}
}
pub trait SymbolPath: Debug + Hash + Clone + Send + Sync {
fn sort_key(&self) -> String;
}
impl SymbolPath for String {
fn sort_key(&self) -> String {
self.clone()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SymbolSortKey(String);
impl SymbolSortKey {
pub fn from_path<P: SymbolPath>(path: &P) -> Self {
Self(path.sort_key())
}
}
impl fmt::Display for SymbolSortKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum Symbol<V: Value<I>, I: Ident, P: SymbolPath> {
Keyword {
name: I,
},
Value {
value: V,
},
Reference {
path: P,
name: Option<I>,
target_path: P,
is_absolute: bool,
nameable: bool,
},
Definition {
name: I,
value: Option<V>,
visibility: SymbolVisibility,
},
Scope {
value: Option<V>,
},
Import {
path: P,
alias: Option<I>,
value: Option<V>,
},
}
impl<V: Value<I>, I: Ident, P: SymbolPath> Symbol<V, I, P> {
pub fn name(&self) -> Option<&I> {
match self {
| Self::Keyword { name, .. } => Some(name),
| Self::Definition { name, .. } => Some(name),
| Self::Value { .. } => None,
| Self::Reference { name, .. } => name.as_ref(),
| Self::Scope { .. } => None,
| Self::Import { alias, .. } => alias.as_ref(),
}
}
pub fn name_span(&self) -> Option<laburnum::Span> {
match self {
| Self::Keyword { name, .. } => name.span(),
| Self::Definition { name, .. } => name.span(),
| Self::Reference { name, .. } => name.as_ref().and_then(|n| n.span()),
| Self::Import { alias, .. } => alias.as_ref().and_then(|a| a.span()),
| Self::Scope { .. } => None,
| Self::Value { .. } => None,
}
}
pub fn has_value(&self) -> bool {
matches!(
self,
Self::Value { .. }
| Self::Definition { value: Some(_), .. }
| Self::Import { value: Some(_), .. }
)
}
pub fn is_absolute_reference(&self) -> bool {
match self {
| Self::Reference { is_absolute, .. } => *is_absolute,
| _ => false,
}
}
pub fn kind(&self) -> &'static str {
match self {
| Self::Definition { .. } => "definition",
| Self::Value { .. } => "value",
| Self::Reference { .. } => "reference",
| Self::Keyword { .. } => "keyword",
| Self::Scope { .. } => "scope",
| Self::Import { .. } => "import",
}
}
pub fn is_nameable(&self) -> bool {
match self {
| Self::Reference { nameable, .. } => *nameable,
| Self::Import { alias, .. } => alias.is_some(),
| _ => true,
}
}
pub fn is_reference(&self) -> bool {
matches!(self, Self::Reference { .. })
}
pub fn is_import(&self) -> bool {
matches!(self, Self::Import { .. })
}
pub fn reference_path(&self) -> Option<&P> {
match self {
| Self::Reference { path, .. } => Some(path),
| _ => None,
}
}
pub fn target_path(&self) -> Option<&P> {
match self {
| Self::Reference { target_path, .. } => Some(target_path),
| _ => None,
}
}
pub fn import_path(&self) -> Option<&P> {
match self {
| Self::Import { path, .. } => Some(path),
| _ => None,
}
}
}
impl<V, I, P> bluegum::Bluegum for Symbol<V, I, P>
where
V: Value<I>,
I: Ident,
P: SymbolPath,
{
fn node(&self, b: &mut bluegum::Builder) {
match self {
| Symbol::Keyword { name } => {
b.name("Keyword").field("name", format!("{name:?}"));
},
| Symbol::Value { value } => {
b.name("Value");
b.add_node("value", value);
},
| Symbol::Definition {
name,
value,
visibility,
} => {
b.name("Definition")
.field("name", format!("{name:?}"))
.field("visibility", format!("{visibility:?}"));
if let Some(v) = value {
b.add_node("value", v);
}
},
| Symbol::Reference {
path,
name,
target_path,
is_absolute,
nameable,
} => {
b.name("Reference")
.field("path", format!("{path:?}"))
.field("target_path", format!("{target_path:?}"))
.field("is_absolute", is_absolute)
.field("nameable", nameable);
if let Some(n) = name {
b.field("name", format!("{n:?}"));
}
},
| Symbol::Scope { value } => {
b.name("Scope");
if let Some(v) = value {
b.add_node("value", v);
}
},
| Symbol::Import { path, alias, value } => {
b.name("Import").field("path", format!("{path:?}"));
if let Some(a) = alias {
b.field("alias", format!("{a:?}"));
}
if let Some(v) = value {
b.add_node("value", v);
}
},
}
}
}
impl<V, I, P> bluegum::BluegumWithState<dyn laburnum::SpanResolver>
for Symbol<V, I, P>
where
V: Value<I>,
I: Ident,
P: SymbolPath,
{
fn node_with_state(
&self,
b: &mut bluegum::Builder,
state: &dyn laburnum::SpanResolver,
) {
let resolve_ident = |ident: &I| -> String {
ident
.span()
.and_then(|s| state.try_resolve_span(&s))
.unwrap_or_else(|| format!("{ident:?}"))
};
match self {
| Symbol::Keyword { name } => {
b.name("Keyword").field("name", resolve_ident(name));
},
| Symbol::Value { value } => {
b.name("Value");
b.add_node("value", value);
},
| Symbol::Definition {
name,
value,
visibility,
} => {
b.name("Definition")
.field("name", resolve_ident(name))
.field("visibility", format!("{visibility:?}"));
if let Some(v) = value {
b.add_node("value", v);
}
},
| Symbol::Reference {
path,
name,
target_path,
is_absolute,
nameable,
} => {
b.name("Reference")
.field("path", path.sort_key())
.field("target_path", target_path.sort_key())
.field("is_absolute", is_absolute)
.field("nameable", nameable);
if let Some(n) = name {
b.field("name", resolve_ident(n));
}
},
| Symbol::Scope { value } => {
b.name("Scope");
if let Some(v) = value {
b.add_node("value", v);
}
},
| Symbol::Import { path, alias, value } => {
b.name("Import").field("path", path.sort_key());
if let Some(a) = alias {
b.field("alias", resolve_ident(a));
}
if let Some(v) = value {
b.add_node("value", v);
}
},
}
}
}
impl<V, I, P, Ps> laburnum::record::CollectReferences<Ps> for Symbol<V, I, P>
where
V: Value<I>,
I: Ident,
P: SymbolPath,
Ps: laburnum::database::storage::Partitions,
{
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::*;
#[test]
fn definition_name() {
let sym = make_definition("foo", None, SymbolVisibility::Private);
let name = sym.name();
assert!(name.is_some());
assert_eq!(name.map(|n| n.as_str()), Some("foo"));
}
#[test]
fn definition_kind() {
let sym = make_definition("foo", None, SymbolVisibility::Private);
assert_eq!(sym.kind(), "definition");
}
#[test]
fn definition_has_value_with_value() {
let sym =
make_definition("foo", Some(DefaultValue::Integer(1)), SymbolVisibility::Private);
assert!(sym.has_value());
}
#[test]
fn definition_has_value_without_value() {
let sym = make_definition("foo", None, SymbolVisibility::Private);
assert!(!sym.has_value());
}
#[test]
fn definition_is_nameable() {
let sym = make_definition("foo", None, SymbolVisibility::Private);
assert!(sym.is_nameable());
}
#[test]
fn definition_visibility() {
let public =
make_definition("foo", None, SymbolVisibility::Public);
let private =
make_definition("bar", None, SymbolVisibility::Private);
match &public {
| Symbol::Definition { visibility, .. } => {
assert_eq!(*visibility, SymbolVisibility::Public);
},
| _ => panic!("expected Definition"),
}
match &private {
| Symbol::Definition { visibility, .. } => {
assert_eq!(*visibility, SymbolVisibility::Private);
},
| _ => panic!("expected Definition"),
}
}
#[test]
fn reference_name_some() {
let sym = make_reference("ref|r", Some("r"), "target", false, true);
let name = sym.name();
assert!(name.is_some());
assert_eq!(name.map(|n| n.as_str()), Some("r"));
}
#[test]
fn reference_name_none() {
let sym = make_reference("ref|anon", None, "target", false, true);
assert!(sym.name().is_none());
}
#[test]
fn reference_kind() {
let sym = make_reference("ref|r", Some("r"), "target", false, true);
assert_eq!(sym.kind(), "reference");
}
#[test]
fn reference_is_reference() {
let sym = make_reference("ref|r", Some("r"), "target", false, true);
assert!(sym.is_reference());
}
#[test]
fn reference_target_path() {
let sym = make_reference("ref|r", Some("r"), "my::path", false, true);
let path = sym.target_path();
assert!(path.is_some());
assert_eq!(path, Some(&"my::path".to_string()));
}
#[test]
fn reference_is_absolute() {
let absolute = make_reference("ref|r", Some("r"), "t", true, true);
let relative = make_reference("ref|r", Some("r"), "t", false, true);
assert!(absolute.is_absolute_reference());
assert!(!relative.is_absolute_reference());
}
#[test]
fn reference_nameable() {
let nameable = make_reference("ref|r", Some("r"), "t", false, true);
let not_nameable = make_reference("ref|r", Some("r"), "t", false, false);
assert!(nameable.is_nameable());
assert!(!not_nameable.is_nameable());
}
#[test]
fn keyword_name() {
let sym = make_keyword("if");
let name = sym.name();
assert!(name.is_some());
assert_eq!(name.map(|n| n.as_str()), Some("if"));
}
#[test]
fn keyword_kind() {
let sym = make_keyword("if");
assert_eq!(sym.kind(), "keyword");
}
#[test]
fn keyword_is_nameable() {
let sym = make_keyword("if");
assert!(sym.is_nameable());
}
#[test]
fn value_kind() {
let sym = make_value(DefaultValue::Integer(42));
assert_eq!(sym.kind(), "value");
}
#[test]
fn value_has_value() {
let sym = make_value(DefaultValue::Integer(42));
assert!(sym.has_value());
}
#[test]
fn value_name_is_none() {
let sym = make_value(DefaultValue::Integer(42));
assert!(sym.name().is_none());
}
#[test]
fn scope_kind() {
let sym = make_scope(None);
assert_eq!(sym.kind(), "scope");
}
#[test]
fn scope_name_is_none() {
let sym = make_scope(None);
assert!(sym.name().is_none());
}
#[test]
fn scope_has_value() {
let with_value = make_scope(Some(DefaultValue::Boolean(true)));
let without_value = make_scope(None);
assert!(!with_value.has_value());
assert!(!without_value.has_value());
}
#[test]
fn import_kind() {
let sym = make_import("std::io", Some("io"), None);
assert_eq!(sym.kind(), "import");
}
#[test]
fn import_is_import() {
let sym = make_import("std::io", Some("io"), None);
assert!(sym.is_import());
}
#[test]
fn import_path() {
let sym = make_import("std::io", Some("io"), None);
let path = sym.import_path();
assert!(path.is_some());
assert_eq!(path, Some(&"std::io".to_string()));
}
#[test]
fn import_name_returns_alias() {
let sym = make_import("std::io", Some("io"), None);
let name = sym.name();
assert!(name.is_some());
assert_eq!(name.map(|n| n.as_str()), Some("io"));
}
#[test]
fn import_name_none_without_alias() {
let sym = make_import("std::io", None, None);
assert!(sym.name().is_none());
}
#[test]
fn import_is_nameable_with_alias() {
let sym = make_import("std::io", Some("io"), None);
assert!(sym.is_nameable());
}
#[test]
fn import_is_nameable_without_alias() {
let sym = make_import("std::io", None, None);
assert!(!sym.is_nameable());
}
#[test]
fn non_reference_is_reference_false() {
let sym = make_definition("foo", None, SymbolVisibility::Private);
assert!(!sym.is_reference());
}
#[test]
fn non_import_is_import_false() {
let sym = make_definition("foo", None, SymbolVisibility::Private);
assert!(!sym.is_import());
}
#[test]
fn non_reference_target_path_none() {
let sym = make_definition("foo", None, SymbolVisibility::Private);
assert!(sym.target_path().is_none());
}
#[test]
fn non_import_import_path_none() {
let sym = make_definition("foo", None, SymbolVisibility::Private);
assert!(sym.import_path().is_none());
}
#[test]
fn default_value_string() {
let v = DefaultValue::String("hello".to_string());
assert_eq!(v, DefaultValue::String("hello".to_string()));
}
#[test]
fn default_value_integer() {
let v = DefaultValue::Integer(42);
assert_eq!(v, DefaultValue::Integer(42));
}
#[test]
fn default_value_float() {
let v = DefaultValue::Float(2.72);
assert_eq!(v, DefaultValue::Float(2.72));
}
#[test]
fn default_value_boolean() {
let v = DefaultValue::Boolean(true);
assert_eq!(v, DefaultValue::Boolean(true));
}
#[test]
fn default_value_span_is_none() {
let values = [
DefaultValue::String("hello".to_string()),
DefaultValue::Integer(42),
DefaultValue::Float(2.72),
DefaultValue::Boolean(true),
];
for v in &values {
assert!(
Value::<crate::StringIdent>::span(v).is_none(),
"span() should be None for {v:?}"
);
}
}
#[test]
fn visibility_default_is_private() {
assert_eq!(SymbolVisibility::default(), SymbolVisibility::Private);
}
#[test]
fn sort_key_from_path() {
let path = "foo::bar".to_string();
let key = SymbolSortKey::from_path(&path);
assert_eq!(format!("{key}"), "foo::bar");
}
#[test]
fn sort_key_display() {
let path = "my::module::path".to_string();
let key = SymbolSortKey::from_path(&path);
assert_eq!(format!("{key}"), "my::module::path");
}
}