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:

FieldDefaultEffect
ignore_nullfalseWhen 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_falsefalseSame for false. Used in int|false style returns the caller has narrowed away from false.
inside_assertionfalseThe 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 with report.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:

CauseMeaning
NONEThe empty set (no cause).
NESTED_MIXEDA mixed nested inside a container was narrowed by the container (e.g. array<string, mixed> into array<string, int>).
FROM_AS_MIXEDThe input was a generic parameter constrained to mixed.
TRUE_UNION_NARROWA "true union" kind (mixed, array_key, bool, object, scalar, numeric) was narrowed to a concrete subform.
PHP_RUNTIME_COERCEA PHP-runtime coercion edge was used (e.g. int → float). Recorded by the runtime-aware paths, not by any LatticeOptions toggle.
LITERAL_PROMOTEDA literal-shaped value was accepted where its general form was expected, or vice versa.
TEMPLATE_DEFAULTA default-filled template parameter was tolerated.
OBJECT_ANY_DOWNobject (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:

FlagMeaning
had_templateThe type was produced from a template parameter at some point.
from_template_defaultThis type-arg was filled with the parameter's default rather than the user's value. The variance check tolerates it (recording CoercionCauses::TEMPLATE_DEFAULT).
populatedThe type has been populated by the analyser's inference.
possibly_undefinedThe value may be undefined at this point.
possibly_undefined_from_tryThe value may be undefined because it was assigned inside a try block.
ignore_nullable_issuesSuppress null-leak diagnostics for this value. Mirrored into LatticeOptions::ignore_null by of_type.
ignore_falsable_issuesSuppress false-leak diagnostics for this value. Mirrored into LatticeOptions::ignore_false by of_type.
nullsafe_nullThe null arm here came from a nullsafe (?->) chain.
by_referenceThe value is held by reference.
reference_freeThe 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.