What suffete is
Suffete is a Rust crate that implements the PHP type system as a self-contained, queryable data structure with a comprehensive set of operations.
It has three deliverables:
- A representation. A handle-based, intern-deduplicated, content-addressed data model that can express every PHP type a real-world analyzer needs to express.
- Operations on that representation. Subtyping, overlap, intersection, union, set difference, narrowing, generic substitution, generic inference, expansion of unresolved forms, structural transformations.
- A
Worldtrait. The single abstraction by which the type system asks questions about the user's codebase: "does classDextend classC?", "what does classCdeclare for propertyp?", "what is the upper bound of template parameterTon classC?". An analyzer plugs in its codebase model behind this trait; suffete itself stays codebase-agnostic.
That is the entire surface. There is no parser. There is no AST. There is no notion of a file, a statement, a scope, a control-flow graph, a diagnostic, or a configuration.
What suffete does
Concretely, given two TypeIds and a World, suffete answers:
- $\tau \mathrel{<:} \sigma$ — does every value of type $\tau$ also have type $\sigma$? (refines)
- $\tau \mathrel{\#} \sigma$ — are $\tau$ and $\sigma$ disjoint? (overlaps)
- $\tau \sqcap \sigma$ — what is the greatest lower bound? (meet)
- $\tau \sqcup \sigma$ — what is the least upper bound? (join)
- $\tau \setminus \sigma$ — what remains of $\tau$ after removing $\sigma$? (subtract)
- $\mathit{narrow}(\tau, \pi)$ — given an assertion $\pi$ that holds on top of $\tau$, what is the refined type? (narrow)
It also answers, given a type, simpler structural questions: is this guaranteed truthy? does this contain null? is this a single literal value the analyzer can constant-fold? does this contain a free template parameter anywhere in its tree? — and many more. These are the predicates.
And it offers the build-side primitives: making a new type from elements, walking a type and rebuilding it under a transformation, applying a generic substitution, expanding an alias.
What suffete does not do
It does not parse PHP. There is no lexer, no parser, no docblock interpreter. The analyzer brings types in from somewhere — a parser, a serialised cache, a hand-built fixture — and constructs TypeIds through suffete's builders and prelude.
It does not run a control-flow analysis. The lattice operations are pure functions of their inputs; they do not know which line of code they came from, what branch they are on, or which assertions led to them being asked. The analyzer asks; suffete answers.
It does not produce diagnostics. When refines returns false, the analyzer chooses what message to display, where to point the user, and at what severity. Suffete returns a boolean and a LatticeReport carrying structured side information; the message-writing is the analyzer's job.
It does not maintain a codebase model. Class hierarchies, declared properties, declared methods, template parameter bounds — all of those live in the analyzer's data structures and are queried through World on demand.
What this buys you
Three things, in roughly decreasing order of value to a downstream consumer:
Single source of truth for hard semantics. Subtyping in PHP is not simple. The interaction of generics and intersections, of nullability and the truthiness axes on mixed, of literal collapse with refined ranges, of object shape with HasMethod — these are conditions that every analyzer has to get right and few of them do. If suffete is correct, every analyzer that depends on suffete is correct on those conditions, automatically.
A property-test battery, run for everyone. Suffete is verified against an algebraic-law battery: idempotence, commutativity, associativity, identity, absorption, the GLB and LUB bounds, the soundness interlock between the operations. Every PR runs that battery. When suffete says $\tau \sqcap \sigma$ is some value, you can rely on it being a lower bound — that is checked on thousands of cases per CI run.
Performance you don't have to think about. The interner, the SIMD scans, the canonical-form fast paths, the singleton caches — they are tuned, and they run on every analyzer that depends on suffete, without the analyzer needing to know how. You write refines(t1, t2, world, opts, &mut report); it returns in nanoseconds for typical inputs.
How it relates to the rest of Carthage
Suffete is being designed in isolation so that the type-system contract can be specified, exercised, and benchmarked without an analyzer in the loop. Once it is stable, it is intended to replace the type-system core inside Mago, the Carthage Software PHP toolchain.
Mago is one consumer. Anyone else writing a PHP analyzer in Rust is invited to be another. Suffete will not change its API on Mago's behalf; if it works for Mago, it works for everyone.
See also: Why a separate type system — the rationale for not building this inside an analyzer in the first place.