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