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