use std::any::{Any, TypeId};
use std::fmt;
use std::hash::{Hash, Hasher};
use crate::ast::{Extension, Spanned};
use crate::precedence::BindingPower;
use crate::vocab::Span;
use super::{Render, RenderCtx};
pub trait DynAstExt: Render + Spanned + fmt::Debug {
fn as_any(&self) -> &dyn Any;
fn dyn_clone(&self) -> Box<dyn DynAstExt>;
fn dyn_eq(&self, other: &dyn DynAstExt) -> bool;
fn dyn_hash(&self, state: &mut dyn Hasher);
}
impl<T: Extension + Render + 'static> DynAstExt for T {
fn as_any(&self) -> &dyn Any {
self
}
fn dyn_clone(&self) -> Box<dyn DynAstExt> {
Box::new(self.clone())
}
fn dyn_eq(&self, other: &dyn DynAstExt) -> bool {
other
.as_any()
.downcast_ref::<T>()
.is_some_and(|other| self == other)
}
fn dyn_hash(&self, mut state: &mut dyn Hasher) {
TypeId::of::<T>().hash(&mut state);
self.hash(&mut state);
}
}
pub struct DynExt(Box<dyn DynAstExt>);
impl DynExt {
pub fn new<T: Extension + Render + 'static>(ext: T) -> Self {
DynExt(Box::new(ext))
}
pub fn downcast_ref<T: 'static>(&self) -> Option<&T> {
self.0.as_any().downcast_ref::<T>()
}
}
impl Clone for DynExt {
fn clone(&self) -> Self {
DynExt(self.0.dyn_clone())
}
}
impl fmt::Debug for DynExt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}
impl PartialEq for DynExt {
fn eq(&self, other: &Self) -> bool {
self.0.dyn_eq(&*other.0)
}
}
impl Eq for DynExt {}
impl Hash for DynExt {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.dyn_hash(state);
}
}
impl Spanned for DynExt {
fn span(&self) -> Span {
self.0.span()
}
}
impl Render for DynExt {
fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.render(ctx, f)
}
fn operand_binding_power(&self) -> Option<BindingPower> {
self.0.operand_binding_power()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{BinaryOperator, Expr, NoExt, Statement};
use crate::generated::visit::{self, Visit};
use crate::render::{RenderConfig, RenderCtx, RenderExt as _};
use crate::vocab::{Meta, NodeId, Resolver, Span, Symbol};
fn meta() -> Meta {
Meta::new(Span::SYNTHETIC, NodeId::new(1).expect("non-zero node id"))
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct Tag(u32);
impl Spanned for Tag {
fn span(&self) -> Span {
Span::SYNTHETIC
}
}
impl Render for Tag {
fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "#{}", self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct Marker;
impl Spanned for Marker {
fn span(&self) -> Span {
Span::SYNTHETIC
}
}
impl Render for Marker {
fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("<marker>")
}
}
fn other(ext: DynExt) -> Expr<DynExt> {
Expr::Other { ext, meta: meta() }
}
fn rendered(node: &impl Render) -> String {
struct Empty;
impl Resolver for Empty {
fn try_resolve(&self, _sym: Symbol) -> Option<&str> {
None
}
}
let config = RenderConfig::default();
let ctx = RenderCtx::new(&Empty, "", &config);
node.displayed(&ctx).to_string()
}
#[test]
fn erased_extension_renders_through_the_node_render_path() {
let expr = other(DynExt::new(Tag(7)));
assert_eq!(rendered(&expr), "#7");
}
#[test]
fn heterogeneous_erased_extensions_compose_in_one_tree() {
let expr: Expr<DynExt> = Expr::BinaryOp {
left: Box::new(other(DynExt::new(Tag(1)))),
op: BinaryOperator::Plus,
right: Box::new(other(DynExt::new(Marker))),
meta: meta(),
};
assert_eq!(rendered(&expr), "#1 + <marker>");
}
#[test]
fn node_holding_dynext_derives_eq_and_clone() {
let expr = other(DynExt::new(Tag(7)));
let same = other(DynExt::new(Tag(7)));
let different = other(DynExt::new(Tag(8)));
assert!(
expr == same,
"structurally equal erased nodes compare equal"
);
assert!(expr != different, "different payloads compare unequal");
assert!(
expr == expr.clone(),
"a cloned subtree stays equal to its origin"
);
}
#[test]
fn erased_extension_equality_is_concrete_type_then_value() {
let a = DynExt::new(Tag(1));
let a2 = DynExt::new(Tag(1));
let b = DynExt::new(Tag(2));
let m = DynExt::new(Marker);
assert!(a == a2, "same type, same value compares equal");
assert!(a != b, "same type, different value compares unequal");
assert!(a != m, "different concrete types are never equal");
}
#[test]
fn erased_extension_clone_is_a_deep_typed_clone() {
let original = DynExt::new(Tag(42));
let clone = original.clone();
assert!(original == clone);
assert_eq!(clone.downcast_ref::<Tag>(), Some(&Tag(42)));
}
#[test]
fn erased_extension_hash_agrees_with_equality() {
use std::collections::hash_map::DefaultHasher;
fn hash_of(ext: &DynExt) -> u64 {
let mut hasher = DefaultHasher::new();
ext.hash(&mut hasher);
hasher.finish()
}
assert_eq!(hash_of(&DynExt::new(Tag(1))), hash_of(&DynExt::new(Tag(1))));
}
#[test]
fn visitor_threads_and_downcasts_erased_extensions() {
#[derive(Default)]
struct Collect {
tags: Vec<u32>,
others: usize,
}
impl<'ast> Visit<'ast, DynExt> for Collect {
fn visit_extension(&mut self, node: &'ast DynExt) {
match node.downcast_ref::<Tag>() {
Some(Tag(n)) => self.tags.push(*n),
None => self.others += 1,
}
}
}
let expr: Expr<DynExt> = Expr::BinaryOp {
left: Box::new(other(DynExt::new(Tag(1)))),
op: BinaryOperator::Plus,
right: Box::new(other(DynExt::new(Marker))),
meta: meta(),
};
let mut collect = Collect::default();
collect.visit_expr(&expr);
assert_eq!(collect.tags, vec![1]);
assert_eq!(collect.others, 1, "the non-Tag extension was still visited");
let _ = visit::walk_expr::<Collect, DynExt>; }
#[test]
fn dynamic_hatch_does_not_change_the_static_noext_layout() {
use std::mem::size_of;
assert_eq!(size_of::<Statement<NoExt>>(), size_of::<Statement>());
assert!(size_of::<Statement<NoExt>>() < size_of::<Statement<DynExt>>());
}
}