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: * @api
13: * @see https://phpstan.org/developing-extensions/trinary-logic
14: */
15: final class TrinaryLogic
16: {
17:
18: private const YES = 1;
19: private const MAYBE = 0;
20: private const NO = -1;
21:
22: /** @var self[] */
23: private static array $registry = [];
24:
25: private function __construct(private int $value)
26: {
27: }
28:
29: public static function createYes(): self
30: {
31: return self::$registry[self::YES] ??= new self(self::YES);
32: }
33:
34: public static function createNo(): self
35: {
36: return self::$registry[self::NO] ??= new self(self::NO);
37: }
38:
39: public static function createMaybe(): self
40: {
41: return self::$registry[self::MAYBE] ??= new self(self::MAYBE);
42: }
43:
44: public static function createFromBoolean(bool $value): self
45: {
46: $yesNo = $value ? self::YES : self::NO;
47: return self::$registry[$yesNo] ??= new self($yesNo);
48: }
49:
50: private static function create(int $value): self
51: {
52: self::$registry[$value] ??= new self($value);
53: return self::$registry[$value];
54: }
55:
56: /**
57: * @phpstan-assert-if-true =false $this->no()
58: * @phpstan-assert-if-true =false $this->maybe()
59: */
60: public function yes(): bool
61: {
62: return $this->value === self::YES;
63: }
64:
65: /**
66: * @phpstan-assert-if-true =false $this->no()
67: * @phpstan-assert-if-true =false $this->yes()
68: */
69: public function maybe(): bool
70: {
71: return $this->value === self::MAYBE;
72: }
73:
74: /**
75: * @phpstan-assert-if-true =false $this->maybe()
76: * @phpstan-assert-if-true =false $this->yes()
77: */
78: public function no(): bool
79: {
80: return $this->value === self::NO;
81: }
82:
83: public function toBooleanType(): BooleanType
84: {
85: if ($this->value === self::MAYBE) {
86: return new BooleanType();
87: }
88:
89: return new ConstantBooleanType($this->value === self::YES);
90: }
91:
92: public function and(self ...$operands): self
93: {
94: $min = $this->value;
95: foreach ($operands as $operand) {
96: if ($operand->value >= $min) {
97: continue;
98: }
99:
100: $min = $operand->value;
101: }
102: return self::create($min);
103: }
104:
105: /**
106: * @template T
107: * @param T[] $objects
108: * @param callable(T): self $callback
109: */
110: public function lazyAnd(
111: array $objects,
112: callable $callback,
113: ): self
114: {
115: if ($this->value === self::NO) {
116: return $this;
117: }
118:
119: $results = [];
120: foreach ($objects as $object) {
121: $result = $callback($object);
122: if ($result->value === self::NO) {
123: return $result;
124: }
125:
126: $results[] = $result;
127: }
128:
129: return $this->and(...$results);
130: }
131:
132: public function or(self ...$operands): self
133: {
134: $max = $this->value;
135: foreach ($operands as $operand) {
136: if ($operand->value < $max) {
137: continue;
138: }
139:
140: $max = $operand->value;
141: }
142: return self::create($max);
143: }
144:
145: /**
146: * @template T
147: * @param T[] $objects
148: * @param callable(T): self $callback
149: */
150: public function lazyOr(
151: array $objects,
152: callable $callback,
153: ): self
154: {
155: if ($this->value === self::YES) {
156: return $this;
157: }
158:
159: $results = [];
160: foreach ($objects as $object) {
161: $result = $callback($object);
162: if ($result->value === self::YES) {
163: return $result;
164: }
165:
166: $results[] = $result;
167: }
168:
169: return $this->or(...$results);
170: }
171:
172: public static function extremeIdentity(self ...$operands): self
173: {
174: if ($operands === []) {
175: throw new ShouldNotHappenException();
176: }
177: $operandValues = array_column($operands, 'value');
178: $min = min($operandValues);
179: $max = max($operandValues);
180: return self::create($min === $max ? $min : self::MAYBE);
181: }
182:
183: /**
184: * @template T
185: * @param T[] $objects
186: * @param callable(T): self $callback
187: */
188: public static function lazyExtremeIdentity(
189: array $objects,
190: callable $callback,
191: ): self
192: {
193: if ($objects === []) {
194: throw new ShouldNotHappenException();
195: }
196:
197: $lastResult = null;
198: foreach ($objects as $object) {
199: $result = $callback($object);
200: if ($lastResult === null) {
201: $lastResult = $result;
202: continue;
203: }
204: if ($lastResult->equals($result)) {
205: continue;
206: }
207:
208: return self::createMaybe();
209: }
210:
211: return $lastResult;
212: }
213:
214: public static function maxMin(self ...$operands): self
215: {
216: if ($operands === []) {
217: throw new ShouldNotHappenException();
218: }
219: $operandValues = array_column($operands, 'value');
220: return self::create(max($operandValues) > 0 ? 1 : min($operandValues));
221: }
222:
223: /**
224: * @template T
225: * @param T[] $objects
226: * @param callable(T): self $callback
227: */
228: public static function lazyMaxMin(
229: array $objects,
230: callable $callback,
231: ): self
232: {
233: $results = [];
234: foreach ($objects as $object) {
235: $result = $callback($object);
236: if ($result->value === self::YES) {
237: return $result;
238: }
239:
240: $results[] = $result;
241: }
242:
243: return self::maxMin(...$results);
244: }
245:
246: public function negate(): self
247: {
248: return self::create(-$this->value);
249: }
250:
251: public function equals(self $other): bool
252: {
253: return $this === $other;
254: }
255:
256: public function compareTo(self $other): ?self
257: {
258: if ($this->value > $other->value) {
259: return $this;
260: } elseif ($other->value > $this->value) {
261: return $other;
262: }
263:
264: return null;
265: }
266:
267: public function describe(): string
268: {
269: static $labels = [
270: self::NO => 'No',
271: self::MAYBE => 'Maybe',
272: self::YES => 'Yes',
273: ];
274:
275: return $labels[$this->value];
276: }
277:
278: }
279: