Lattice options and reports
The lattice operations take two extra parameters beyond the types and the world: LatticeOptions and a &mut LatticeReport. This chapter documents both.
LatticeOptions
The configuration knobs for a single lattice query. All fields default to false ("strict reading"); the analyser sets them based on the file's strictness mode and any per-call overrides.
use suffete::lattice::LatticeOptions;
let opts = LatticeOptions::default();
let opts = opts.with_ignore_null(); // ignore null in the input union
let opts = opts.with_ignore_false(); // ignore false in the input union
let opts = opts.inside_assertion(); // checking inside a runtime assertion
The full set of options. LatticeOptions is a Copy struct with exactly these three bool fields:
| Field | Default | Effect |
|---|---|---|
ignore_null | false | When checking refines(τ, σ), drop null from the input union τ before the per-Element check. Used by nullsafe-aware analyser code that has separately verified non-nullability. |
ignore_false | false | Same for false. Used in int|false style returns the caller has narrowed away from false. |
inside_assertion | false | The refinement is being checked inside a runtime assertion (e.g. assert($x instanceof Foo)). Some rules become more permissive in this mode. |
The flag-setters each take no argument and set their flag to true: .with_ignore_null(), .with_ignore_false(), .inside_assertion(). The type is Copy, so chaining is cheap. Besides LatticeOptions::default(), two constructors derive the flags from a type's FlowFlags: LatticeOptions::of_type(ty) (mirrors ignore_nullable_issues / ignore_falsable_issues) and LatticeOptions::assertion_of_type(ty) (the same, with inside_assertion set).
There is no php_runtime_coerce toggle on LatticeOptions: PHP runtime coercion is never modelled by refines. The join-only merge toggles and literal-collapse thresholds (merge_list_element_types, merge_keyed_array_params, int_literal_collapse_threshold, string_literal_collapse_threshold, and the rest) live on suffete::join::JoinOptions instead, consumed by join::compute_with(elements, &JoinOptions).
LatticeReport
A buffer the lattice writes into during a query. Construct one with LatticeReport::new() and pass &mut report to the operation. It exposes these public fields:
causes: CoercionCauses; a bitset of which special rules fired, suitable for the analyser to surface in a diagnostic. Read it withreport.causes.contains(CoercionCauses::X).replacement: Option<TypeId>; the smallest type that, substituted for the input, would have made the comparison succeed cleanly.replacement_element: Option<ElementId>; the single problematic element when only one atom of a wider union was at fault.bounds: Vec<(TemplateKey, Bound)>; template-parameter bounds that surfaced during the comparison.
Methods include add_cause(c), coerced() -> bool (true iff any cause was recorded), set_replacement, set_replacement_element, and push_bound.
use suffete::lattice::{LatticeOptions, LatticeReport, CoercionCauses};
let mut report = LatticeReport::new();
// ... call lattice operations, passing &mut report ...
if report.causes.contains(CoercionCauses::PHP_RUNTIME_COERCE) {
// The check passed but used a runtime-coercion edge.
// Surface a warning if the analyser's policy requires.
}
if report.causes.contains(CoercionCauses::TEMPLATE_DEFAULT) {
// The check passed but used a default-filled template parameter.
}
CoercionCauses
A u8 bitset. The complete set of bits is:
| Cause | Meaning |
|---|---|
NONE | The empty set (no cause). |
NESTED_MIXED | A mixed nested inside a container was narrowed by the container (e.g. array<string, mixed> into array<string, int>). |
FROM_AS_MIXED | The input was a generic parameter constrained to mixed. |
TRUE_UNION_NARROW | A "true union" kind (mixed, array_key, bool, object, scalar, numeric) was narrowed to a concrete subform. |
PHP_RUNTIME_COERCE | A PHP-runtime coercion edge was used (e.g. int → float). Recorded by the runtime-aware paths, not by any LatticeOptions toggle. |
LITERAL_PROMOTED | A literal-shaped value was accepted where its general form was expected, or vice versa. |
TEMPLATE_DEFAULT | A default-filled template parameter was tolerated. |
OBJECT_ANY_DOWN | object (the unspecified-class element) was accepted where a concrete class was expected. |
The bits are non-zero when the corresponding rule contributed to the answer. The analyser reads the bitset after the query and decides what to do. Besides contains(other), any(), and is_empty(), there are convenience predicates nested_mixed(), from_as_mixed(), true_union_narrow(), php_runtime_coerce(), literal_promoted(), template_default(), and object_any_down().
Reusing reports
LatticeReport::new() is cheap (a zeroed bitset, no allocation until a bound is pushed). There is no in-place reset; to reuse, allocate a fresh report per iteration:
for query in queries {
let mut report = LatticeReport::new();
let _ = lattice::refines(query.a, query.b, &world, opts, &mut report);
// ... handle the result and the report ...
}
FlowFlags
A 16-bit bitset that rides on every TypeId. Stored in the flags field of the handle's u64 representation.
use suffete::FlowFlags;
let f = FlowFlags::EMPTY;
let f = f.with_from_template_default(true);
let f = f.with_possibly_undefined(true);
let f = f.with_ignore_nullable_issues(true);
The full set of flags. Each has a getter and a with_* setter taking a bool:
| Flag | Meaning |
|---|---|
had_template | The type was produced from a template parameter at some point. |
from_template_default | This type-arg was filled with the parameter's default rather than the user's value. The variance check tolerates it (recording CoercionCauses::TEMPLATE_DEFAULT). |
populated | The type has been populated by the analyser's inference. |
possibly_undefined | The value may be undefined at this point. |
possibly_undefined_from_try | The value may be undefined because it was assigned inside a try block. |
ignore_nullable_issues | Suppress null-leak diagnostics for this value. Mirrored into LatticeOptions::ignore_null by of_type. |
ignore_falsable_issues | Suppress false-leak diagnostics for this value. Mirrored into LatticeOptions::ignore_false by of_type. |
nullsafe_null | The null arm here came from a nullsafe (?->) chain. |
by_reference | The value is held by reference. |
reference_free | The value is known not to alias any reference. |
The flags do not affect lattice operations directly ; they are metadata the analyser reads out via TypeId::flags(). The exception is from_template_default, which the lattice's variance check consults.
A worked example
use suffete::{TypeBuilder, prelude::{INT, FLOAT}};
use suffete::{lattice::{self, LatticeOptions, LatticeReport}, world::NullWorld, compatibility};
let int_t = TypeBuilder::new().push(INT).build();
let float_t = TypeBuilder::new().push(FLOAT).build();
let world = NullWorld;
let opts = LatticeOptions::default();
let mut report = LatticeReport::new();
// `refines` is the strict static order: an int is never a float.
assert!(!lattice::refines(int_t, float_t, &world, opts, &mut report));
// The runtime coercion story lives in `compatibility`, not in a
// `refines` option: PHP would coerce an int argument to a float.
assert!(compatibility::runtime_compatible(int_t, float_t, &world, opts, &mut report));
refines(int, float) is always false; there is no option that flips it. The PHP int → float coercion is modelled by compatibility::runtime_compatible and cast, which record CoercionCauses::PHP_RUNTIME_COERCE on the paths that exercise it.
See also: refines, meet, join, subtract, narrow, overlaps for the operations that consume these.