1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan;
4:
5: use PHPStan\Type\BooleanType;
6: use PHPStan\Type\Constant\ConstantBooleanType;
7: use function array_column;
8: use function max;
9: use function min;
10:
11: /**
12: * Three-valued logic used throughout PHPStan's type system.
13: *
14: * Unlike boolean logic, TrinaryLogic has three states: Yes, No, and Maybe.
15: * This is essential for static analysis because type relationships aren't always
16: * certain. For example, a `mixed` type *might* be a string — that's `Maybe`.
17: *
18: * Many Type methods return TrinaryLogic instead of bool because the answer may
19: * depend on runtime values that can't be known statically. Extension developers
20: * encounter TrinaryLogic extensively when querying type properties:
21: *
22: * if ($type->isString()->yes()) {
23: * // Definitely a string
24: * }
25: * if ($type->isString()->maybe()) {
26: * // Could be a string (e.g. mixed)
27: * }
28: * if ($type->isString()->no()) {
29: * // Definitely not a string
30: * }
31: *
32: * TrinaryLogic supports logical operations (and, or, negate) that propagate
33: * uncertainty correctly. It is used as a flyweight — instances are cached and
34: * compared by identity.
35: *
36: * @api
37: * @see https://phpstan.org/developing-extensions/trinary-logic
38: */
39: final class TrinaryLogic
40: {
41:
42: private const YES = 3;
43: private const MAYBE = 1;
44: private const NO = 0;
45:
46: /** @var self[] */
47: private static array $registry = [];
48:
49: private static self $YES;
50:
51: private static self $MAYBE;
52:
53: private static self $NO;
54:
55: private function __construct(private int $value)
56: {
57: }
58:
59: public static function createYes(): self
60: {
61: return self::$YES ??= (self::$registry[self::YES] ??= new self(self::YES));
62: }
63:
64: public static function createNo(): self
65: {
66: return self::$NO ??= (self::$registry[self::NO] ??= new self(self::NO));
67: }
68:
69: public static function createMaybe(): self
70: {
71: return self::$MAYBE ??= (self::$registry[self::MAYBE] ??= new self(self::MAYBE));
72: }
73:
74: public static function createFromBoolean(bool $value): self
75: {
76: $yesNo = $value ? self::YES : self::NO;
77: return self::$registry[$yesNo] ??= new self($yesNo);
78: }
79:
80: private static function create(int $value): self
81: {
82: return self::$registry[$value] ??= new self($value);
83: }
84:
85: /**
86: * @phpstan-assert-if-true =false $this->no()
87: * @phpstan-assert-if-true =false $this->maybe()
88: */
89: public function yes(): bool
90: {
91: return $this->value === self::YES;
92: }
93:
94: /**
95: * @phpstan-assert-if-true =false $this->no()
96: * @phpstan-assert-if-true =false $this->yes()
97: */
98: public function maybe(): bool
99: {
100: return $this->value === self::MAYBE;
101: }
102:
103: /**
104: * @phpstan-assert-if-true =false $this->maybe()
105: * @phpstan-assert-if-true =false $this->yes()
106: */
107: public function no(): bool
108: {
109: return $this->value === self::NO;
110: }
111:
112: public function toBooleanType(): BooleanType
113: {
114: if ($this->value === self::MAYBE) {
115: return new BooleanType();
116: }
117:
118: return new ConstantBooleanType($this->value === self::YES);
119: }
120:
121: public function and(?self $operand = null, self ...$rest): self
122: {
123: $min = $this->value & ($operand !== null ? $operand->value : self::YES);
124: foreach ($rest as $restOperand) {
125: $min &= $restOperand->value;
126: }
127: return self::$registry[$min] ??= new self($min);
128: }
129:
130: /**
131: * @template T
132: * @param T[] $objects
133: * @param callable(T): self $callback
134: */
135: public function lazyAnd(
136: array $objects,
137: callable $callback,
138: ): self
139: {
140: if ($this->value === self::NO) {
141: return $this;
142: }
143:
144: $results = [];
145: foreach ($objects as $object) {
146: $result = $callback($object);
147: if ($result->value === self::NO) {
148: return $result;
149: }
150:
151: $results[] = $result;
152: }
153:
154: return $this->and(...$results);
155: }
156:
157: public function or(?self $operand = null, self ...$rest): self
158: {
159: $max = $this->value | ($operand !== null ? $operand->value : self::NO);
160: foreach ($rest as $restOperand) {
161: $max |= $restOperand->value;
162: }
163: return self::$registry[$max] ??= new self($max);
164: }
165:
166: /**
167: * @template T
168: * @param T[] $objects
169: * @param callable(T): self $callback
170: */
171: public function lazyOr(
172: array $objects,
173: callable $callback,
174: ): self
175: {
176: if ($this->value === self::YES) {
177: return $this;
178: }
179:
180: $results = [];
181: foreach ($objects as $object) {
182: $result = $callback($object);
183: if ($result->value === self::YES) {
184: return $result;
185: }
186:
187: $results[] = $result;
188: }
189:
190: return $this->or(...$results);
191: }
192:
193: /**
194: * Returns the operands' value if they all agree, Maybe if any differ.
195: */
196: public static function extremeIdentity(self ...$operands): self
197: {
198: if ($operands === []) {
199: throw new ShouldNotHappenException();
200: }
201: $operandValues = array_column($operands, 'value');
202: $min = min($operandValues);
203: $max = max($operandValues);
204: return self::create($min === $max ? $min : self::MAYBE);
205: }
206:
207: /**
208: * @template T
209: * @param T[] $objects
210: * @param callable(T): self $callback
211: */
212: public static function lazyExtremeIdentity(
213: array $objects,
214: callable $callback,
215: ): self
216: {
217: if ($objects === []) {
218: throw new ShouldNotHappenException();
219: }
220:
221: $lastResult = null;
222: foreach ($objects as $object) {
223: $result = $callback($object);
224: if ($lastResult === null) {
225: $lastResult = $result;
226: continue;
227: }
228: if ($lastResult->equals($result)) {
229: continue;
230: }
231:
232: return self::createMaybe();
233: }
234:
235: return $lastResult;
236: }
237:
238: /**
239: * Returns Yes if any operand is Yes, otherwise the minimum.
240: */
241: public static function maxMin(self ...$operands): self
242: {
243: if ($operands === []) {
244: throw new ShouldNotHappenException();
245: }
246:
247: $max = self::NO;
248: $min = self::YES;
249: foreach ($operands as $operand) {
250: $max |= $operand->value;
251: $min &= $operand->value;
252: }
253: $maxMin = $max === self::YES ? self::YES : $min;
254:
255: return self::$registry[$maxMin] ??= new self($maxMin);
256: }
257:
258: /**
259: * @template T
260: * @param T[] $objects
261: * @param callable(T): self $callback
262: */
263: public static function lazyMaxMin(
264: array $objects,
265: callable $callback,
266: ): self
267: {
268: $min = self::YES;
269: foreach ($objects as $object) {
270: $result = $callback($object);
271: if ($result->value === self::YES) {
272: return $result;
273: }
274:
275: $min &= $result->value;
276: }
277:
278: return self::$registry[$min] ??= new self($min);
279: }
280:
281: public function negate(): self
282: {
283: // 0b11 >> 0 == 0b11 (3)
284: // 0b11 >> 1 == 0b01 (1)
285: // 0b11 >> 3 == 0b00 (0)
286: return self::create(3 >> $this->value);
287: }
288:
289: public function equals(self $other): bool
290: {
291: return $this === $other;
292: }
293:
294: /**
295: * Returns the stronger of the two values, or null if they are equal (Yes > Maybe > No).
296: */
297: public function compareTo(self $other): ?self
298: {
299: if ($this->value > $other->value) {
300: return $this;
301: } elseif ($other->value > $this->value) {
302: return $other;
303: }
304:
305: return null;
306: }
307:
308: public function describe(): string
309: {
310: static $labels = [
311: self::NO => 'No',
312: self::MAYBE => 'Maybe',
313: self::YES => 'Yes',
314: ];
315:
316: return $labels[$this->value];
317: }
318:
319: }
320: