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