Hierarchy and World

Suffete is codebase-agnostic. It does not know which classes the user has declared, what they extend, what methods and properties they declare, what template parameters they have, what their declared variances are, or what type aliases the user has defined. All of that knowledge lives in the analyser and is exposed to suffete through one trait: World.

use suffete::world::World;

This chapter covers the trait surface and the conventions analyser implementations should follow.

The trait

World is a query interface. Every method takes a &self and returns a value (or None). Suffete never mutates the world; it only reads.

The full surface (the trait has no supertraits and no default methods; every method is required):

pub trait World {
    // Inheritance
    fn descends_from(&self, child: Word, ancestor: Word) -> bool;
    fn uses_trait(&self, class: Word, trait_: Word) -> bool;
    fn inherited_template_argument(
        &self,
        child: Word,
        ancestor: Word,
        position: usize,
    ) -> Option<TypeId>;

    // Templates
    fn template_parameter_arity(&self, class: Word) -> usize;
    fn template_parameter_at(&self, class: Word, position: usize)
        -> Option<TemplateParameter>;
    fn template_parameter_index(&self, class: Word, name: Word)
        -> Option<usize>;

    // Class properties and methods
    fn class_has_method(&self, class: Word, method: Word) -> bool;
    fn class_has_property(&self, class: Word, property: Word) -> bool;
    fn class_property_type(&self, class: Word, property: Word) -> Option<TypeId>;
    fn class_property_count(&self, class: Word) -> usize;
    fn class_property_at(&self, class: Word, position: usize) -> Option<ClassProperty>;
    fn class_constant_type(&self, class: Word, constant: Word) -> Option<TypeId>;
    fn global_constant_type(&self, name: Word) -> Option<TypeId>;

    // Class-like classification
    fn class_like_kind(&self, name: Word) -> Option<ClassLikeKind>;
    fn is_final(&self, name: Word) -> bool;

    // Enums
    fn enum_backing(&self, enum_name: Word) -> Option<EnumBacking>;

    // Aliases
    fn alias_body(&self, class: Word, alias: Word) -> Option<TypeId>;

    // Sealed class-likes
    fn sealed_direct_inheritors(&self, class_like: Word) -> Option<&[Word]>;
    fn sealed_parent_of(&self, child: Word) -> Option<Word>;
}

The exact method list lives in src/world/mod.rs. The classification queries (is_interface, is_trait, is_enum) are not separate methods; ask class_like_kind and compare against ClassLikeKind::{Interface, Trait, Enum}.

What suffete asks the world

The lattice's family rules consult the world wherever it cannot answer from the type alone:

  • Object family (refines, meet, overlaps):

    • descends_from(D, C) for inheritance checks.
    • inherited_template_argument(D, C, i) for variance through inheritance.
    • template_parameter_at(C, i) for declared variance.
    • class_has_method(C, m) for Foo <: has-method<m>.
    • class_has_property(C, p) for Foo <: has-property<p>.
    • class_property_type(C, p) for shape-vs-named subtyping.
    • is_final(C) for finality-aware refines and meet.
  • Enum family:

    • enum_backing(E) for the structural shape of Status::Active's value property.
  • Class-like-string family:

    • descends_from(C, D) for class-string<C> <: class-string<D>.
    • class_like_kind(C) for the kind-axis check (compare against ClassLikeKind::{Interface, Enum, Trait}).
  • Conditional and Derived expansion (in expand):

    • class_constant_type(C, K) for MemberReference.
    • alias_body(C, name) for Alias.
    • The full set of derived-type queries.

The NullWorld

Suffete ships a trivial implementation: NullWorld. It returns:

  • descends_from: only descendant == ancestor.
  • class_has_method, class_has_property: always false.
  • All the others: None or zero or empty.

NullWorld is useful for:

  • Examples and tests where the analyser's codebase model is irrelevant.
  • The lattice's join canonicalisation, which uses NullWorld for the structural-only subsumption check (so that int|float is not collapsed to float via PHP runtime coercion).
use suffete::world::NullWorld;
let world = NullWorld;

Implementing World

The analyser implements World by reading from its codebase model. A sketch:

use suffete::world::{World, TemplateParameter, EnumBacking, Variance};
use suffete::element::payload::ClassLikeKind;
use suffete::TypeId;
use mago_word::Word;

pub struct AnalyserWorld {
    classes: HashMap<Word, ClassInfo>,
    aliases: HashMap<Word, TypeId>,
    // ... other tables ...
}

impl World for AnalyserWorld {
    fn descends_from(&self, d: Word, a: Word) -> bool {
        if d == a { return true; }
        let mut current = self.classes.get(&d);
        while let Some(info) = current {
            if info.parents.contains(&a) { return true; }
            current = info.parents.first().and_then(|p| self.classes.get(p));
        }
        false
    }

    fn class_has_method(&self, c: Word, m: Word) -> bool {
        self.classes.get(&c)
            .map(|info| info.methods.contains_key(&m))
            .unwrap_or(false)
    }

    fn template_parameter_at(&self, c: Word, i: usize) -> Option<TemplateParameter> {
        let info = self.classes.get(&c)?;
        info.template_parameters.get(i).cloned()
    }

    // ... and so on ...
}

The implementation can use any storage strategy ; HashMap, indexed slices, B-trees, lazy-loading from disk. Suffete only cares about the answers.

Performance contract

Suffete calls World methods frequently during lattice operations on object-family types. A refines(IntList, Iterable<int>) involves at least:

  • One descends_from (to confirm IntList implements Iterable).
  • One inherited_template_argument (to get IntList's contribution to Iterable's parameter).
  • One template_parameter_at (for the declared variance).

For an analyser checking thousands of refines calls per file, a slow World is a bottleneck. The conventions:

  • descends_from should be O(1) amortised. Pre-compute the transitive closure of inheritance; cache the answer.
  • class_has_method and class_has_property should be O(1). Use a HashMap.
  • inherited_template_argument should be O(1) amortised. Pre-compute the inheritance bindings.
  • template_parameter_at should be O(1). Index by position into a small vector.

The analyser pays the up-front cost when it ingests the codebase; suffete pays the per-query cost on every lattice call.

Threading

The trait itself imposes no Sync bound, but lattice operations are pure functions and can be called from multiple threads simultaneously, all sharing one &World. To use a World that way, make the implementing type Sync so it allows concurrent reads (typical: read-only after ingestion).

A worked example: descendant lookup

use suffete::world::{World, ClassProperty};
use suffete::{TypeBuilder, ElementId};
use suffete::element::payload::ClassLikeKind;

struct DemoWorld;

impl World for DemoWorld {
    fn descends_from(&self, d: mago_word::Word, a: mago_word::Word) -> bool {
        // For demo: class B descends from class A; everything else is itself only.
        if d == a { return true; }
        if d.as_str() == Some("B") && a.as_str() == Some("A") { return true; }
        false
    }

    // ... other methods stub out to None / false / 0 ...

    fn uses_trait(&self, _: mago_word::Word, _: mago_word::Word) -> bool { false }
    fn template_parameter_arity(&self, _: mago_word::Word) -> usize { 0 }
    fn template_parameter_at(&self, _: mago_word::Word, _: usize)
        -> Option<suffete::world::TemplateParameter> { None }
    fn template_parameter_index(&self, _: mago_word::Word, _: mago_word::Word)
        -> Option<usize> { None }
    fn inherited_template_argument(&self, _: mago_word::Word, _: mago_word::Word, _: usize)
        -> Option<suffete::TypeId> { None }
    fn class_has_method(&self, _: mago_word::Word, _: mago_word::Word) -> bool { false }
    fn class_has_property(&self, _: mago_word::Word, _: mago_word::Word) -> bool { false }
    fn class_property_type(&self, _: mago_word::Word, _: mago_word::Word) -> Option<suffete::TypeId> { None }
    fn class_property_count(&self, _: mago_word::Word) -> usize { 0 }
    fn class_property_at(&self, _: mago_word::Word, _: usize) -> Option<ClassProperty> { None }
    fn class_constant_type(&self, _: mago_word::Word, _: mago_word::Word) -> Option<suffete::TypeId> { None }
    fn global_constant_type(&self, _: mago_word::Word) -> Option<suffete::TypeId> { None }
    fn class_like_kind(&self, _: mago_word::Word) -> Option<ClassLikeKind> { None }
    fn is_final(&self, _: mago_word::Word) -> bool { false }
    fn enum_backing(&self, _: mago_word::Word) -> Option<suffete::world::EnumBacking> { None }
    fn alias_body(&self, _: mago_word::Word, _: mago_word::Word) -> Option<suffete::TypeId> { None }
    fn sealed_direct_inheritors(&self, _: mago_word::Word) -> Option<&[mago_word::Word]> { None }
    fn sealed_parent_of(&self, _: mago_word::Word) -> Option<mago_word::Word> { None }
}

let world = DemoWorld;
let class_a = TypeBuilder::new().push(ElementId::object_named(b"A")).build();
let class_b = TypeBuilder::new().push(ElementId::object_named(b"B")).build();

let mut report = suffete::lattice::LatticeReport::new();
let opts = suffete::lattice::LatticeOptions::default();

assert!(suffete::lattice::refines(class_b, class_a, &world, opts, &mut report));

Hierarchy module

Suffete ships a hierarchy module with helpers for managing class-hierarchy data structures. It's a convenience for analyser implementations that don't already have one. See src/hierarchy/mod.rs for the API.

The hierarchy module is not a World implementation by itself ; it's a kit you can compose into your World. Most analysers will have their own.

See also: Templates in depth for the template-related world methods; Specialise for the inheritance-binding protocol; Expand for the resolution methods on the world.