Answering "is A a subtype of B?"
The most common analyser question. Recipe with the full setup, expansion, and result-handling.
The pattern
use suffete::{TypeId, lattice::{self, LatticeOptions, LatticeReport}, predicates::is_fully_resolved, expand};
use suffete::world::World;
fn is_subtype<W: World>(
sub: TypeId,
sup: TypeId,
world: &W,
) -> (bool, LatticeReport) {
// 1. Expand any unresolved Elements (Alias, Conditional, Derived, ...).
let sub = if is_fully_resolved(sub) { sub } else { expand::expand(sub, world) };
let sup = if is_fully_resolved(sup) { sup } else { expand::expand(sup, world) };
// 2. Configure lattice options.
let opts = LatticeOptions::default();
// 3. Run the lattice query, collecting report side-info.
let mut report = LatticeReport::new();
let result = lattice::refines(sub, sup, world, opts, &mut report);
(result, report)
}
Reading the result
The (bool, LatticeReport) pair is structured for diagnostic emission:
bool == true; the subtype check holds.bool == false; the check failed; the analyser surfaces a type error.report.causes; bitset of which coercion-tolerant rules fired. Surface in the diagnostic if you want to warn (e.g. a default-filled template parameter was tolerated, recorded asCoercionCauses::TEMPLATE_DEFAULT).
Options
refines is always strict at the value level: it does not admit PHP's runtime coercions. int does not refine float, and numeric-string does not refine int, regardless of declare(strict_types). The PHP runtime-coercion story lives elsewhere: see compatibility::runtime_compatible and cast.
The knobs LatticeOptions does expose are narrower:
ignore_null/ignore_false: dropnull/falsefrom the input union before the check, for analyser code that has separately verified non-nullability or non-falsability. Set viaLatticeOptions::default().with_ignore_null().inside_assertion: marks that the check runs inside a runtime assertion, which makes a few rules more permissive.
Worked examples
Direct subtype
use suffete::{TypeBuilder, prelude::{INT, STRING}};
let int_t = TypeBuilder::new().push(INT).build();
let int_or_str = TypeBuilder::new().push(INT).push(STRING).build();
let (ok, _report) = is_subtype(int_t, int_or_str, &world);
assert!(ok);
Int does not refine float
use suffete::{TypeBuilder, prelude::{INT, FLOAT}};
let int_t = TypeBuilder::new().push(INT).build();
let float_t = TypeBuilder::new().push(FLOAT).build();
let (ok, _) = is_subtype(int_t, float_t, &world);
assert!(!ok); // int does not refine float.
Class-hierarchy subtype
use suffete::{TypeBuilder, ElementId};
// Setup (in your World): class B extends A.
let b_t = TypeBuilder::new().push(ElementId::object_named(b"B")).build();
let a_t = TypeBuilder::new().push(ElementId::object_named(b"A")).build();
let (ok, _) = is_subtype(b_t, a_t, &world);
assert!(ok);
Generic subtype with variance
use suffete::element::payload::{ObjectInfo, ObjectFlags};
use suffete::interner::interner;
// World registers Iterator<T> as covariant on T.
let int_iter = TypeBuilder::new().push(interner().intern_object(ObjectInfo {
name: mago_word::word(b"Iterator"),
type_args: Some(interner().intern_type_list(&[suffete::prelude::TYPE_INT])),
flags: ObjectFlags::default(),
})).build();
let mixed_iter = TypeBuilder::new().push(interner().intern_object(ObjectInfo {
name: mago_word::word(b"Iterator"),
type_args: Some(interner().intern_type_list(&[suffete::prelude::TYPE_MIXED])),
flags: ObjectFlags::default(),
})).build();
let (ok, _) = is_subtype(int_iter, mixed_iter, &world);
assert!(ok); // covariance: int <: mixed
Failure with a useful report
let foo = TypeBuilder::new().push(ElementId::object_named(b"Foo")).build();
let bar = TypeBuilder::new().push(ElementId::object_named(b"Bar")).build();
let (ok, report) = is_subtype(foo, bar, &world);
assert!(!ok);
// report contains structured information you can use to construct
// a diagnostic message, like which family of rules failed.
Performance
refines is the hot path. Suffete optimises for the common cases:
- Reflexivity:
refines(t, t) = truein one comparison. - Singleton-vs-singleton: most analyser queries are between one-Element types; the cartesian is degenerate.
- Subsumption shortcut in unions: if any Element on the left refines any Element on the right by the universal axioms (top, bot, placeholder), short-circuit.
Expected cost for a typical analyser query: tens of nanoseconds. Worst-case (deep generics, multi-conjunct intersections, full fan-out coverage): microseconds.
When to expand
Expand exactly once per type, before the first lattice query. The expansion result is itself a TypeId and can be cached per (input, world-version) if the analyser sees it many times.
The is_fully_resolved check is cheap (a tree walk with a short-circuiting predicate), so the recipe above always-checks-then-expands. For analyser code that knows expansion has already happened (e.g. inside a single statement's analysis), skip the check.
When to skip the world
If the question doesn't require the world (e.g. is_subtype(int_or_string, int)), you can pass NullWorld:
use suffete::world::NullWorld;
let (ok, _) = is_subtype(int_or_string, int_t, &NullWorld);
The lattice queries the world only when needed. For fully-structural inputs, the world is never asked.
See also: refines for the rules; overlaps for the symmetric "share a value?" question; LatticeOptions for the
ignore_nullandignore_falseoptions.