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