mir-analyzer 0.1.0

Analysis engine for the mir PHP static analyzer
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
/// PHP built-in stubs — registered into the `Codebase` before user-code analysis.
///
/// Each PHP built-in function is stored as a `FunctionStorage` entry so the
/// normal call-analysis path (argument count checking, return-type inference)
/// works for built-ins without a separate match table.
///
/// Class/interface stubs cover the exception hierarchy, core interfaces
/// (`Throwable`, `Iterator`, `Countable`, `ArrayAccess`, `Stringable`,
/// `Traversable`, `IteratorAggregate`), and `stdClass` / `Closure`.
use std::sync::Arc;

use mir_codebase::storage::{
    ClassStorage, FnParam, FunctionStorage, InterfaceStorage, MethodStorage, Visibility,
};
use mir_codebase::Codebase;
use mir_types::{Atomic, Union};

// ---------------------------------------------------------------------------
// Public entry point
// ---------------------------------------------------------------------------

pub fn load_stubs(codebase: &Codebase) {
    load_functions(codebase);
    load_classes(codebase);
    load_interfaces(codebase);
}

// ---------------------------------------------------------------------------
// Param / type helpers
// ---------------------------------------------------------------------------

#[inline]
fn req(name: &'static str) -> FnParam {
    FnParam {
        name: Arc::from(name),
        ty: None,
        default: None,
        is_variadic: false,
        is_byref: false,
        is_optional: false,
    }
}

#[inline]
fn opt(name: &'static str) -> FnParam {
    FnParam {
        name: Arc::from(name),
        ty: None,
        default: Some(Union::single(Atomic::TNull)),
        is_variadic: false,
        is_byref: false,
        is_optional: true,
    }
}

#[inline]
fn vari(name: &'static str) -> FnParam {
    FnParam {
        name: Arc::from(name),
        ty: None,
        default: None,
        is_variadic: true,
        is_byref: false,
        is_optional: true,
    }
}

#[inline]
fn byref(name: &'static str) -> FnParam {
    FnParam {
        name: Arc::from(name),
        ty: None,
        default: None,
        is_variadic: false,
        is_byref: true,
        is_optional: false,
    }
}

#[inline]
fn byref_opt(name: &'static str) -> FnParam {
    FnParam {
        name: Arc::from(name),
        ty: None,
        default: None,
        is_variadic: false,
        is_byref: true,
        is_optional: true,
    }
}

/// Helper to create a public read-only property stub.
fn stub_prop(name: &'static str, ty: Union) -> mir_codebase::storage::PropertyStorage {
    mir_codebase::storage::PropertyStorage {
        name: Arc::from(name),
        ty: Some(ty),
        inferred_ty: None,
        visibility: mir_codebase::storage::Visibility::Public,
        is_static: false,
        is_readonly: false,
        default: None,
        location: None,
    }
}

/// Register one function into the codebase.
fn reg(codebase: &Codebase, name: &'static str, params: Vec<FnParam>, ret: Union) {
    let fqn: Arc<str> = Arc::from(name);
    codebase.functions.insert(
        fqn.clone(),
        FunctionStorage {
            fqn: fqn.clone(),
            short_name: fqn,
            params,
            return_type: Some(ret),
            inferred_return_type: None,
            template_params: vec![],
            assertions: vec![],
            throws: vec![],
            is_deprecated: false,
            is_pure: false,
            location: None,
        },
    );
}

// ---------------------------------------------------------------------------
// Return-type shorthands
// ---------------------------------------------------------------------------

#[inline] fn t_str()  -> Union { Union::single(Atomic::TString) }
#[inline] fn t_int()  -> Union { Union::single(Atomic::TInt) }
#[inline] fn t_float()-> Union { Union::single(Atomic::TFloat) }
#[inline] fn t_bool() -> Union { Union::single(Atomic::TBool) }
#[inline] fn t_void() -> Union { Union::single(Atomic::TVoid) }
#[inline] fn t_never()-> Union { Union::single(Atomic::TNever) }
#[inline] fn t_ni()   -> Union { Union::single(Atomic::TNonNegativeInt) }

/// string|false
#[inline]
fn t_str_or_false() -> Union {
    let mut u = t_str();
    u.add_type(Atomic::TFalse);
    u
}

/// int|false
#[inline]
fn t_int_or_false() -> Union {
    let mut u = t_int();
    u.add_type(Atomic::TFalse);
    u
}

/// int|string|null  (e.g. array_key_first)
#[inline]
fn t_int_str_null() -> Union {
    let mut u = t_int();
    u.add_type(Atomic::TString);
    u.add_type(Atomic::TNull);
    u
}

/// Generic array<int, mixed>
#[inline]
fn t_array() -> Union {
    Union::single(Atomic::TArray {
        key: Box::new(t_int()),
        value: Box::new(Union::mixed()),
    })
}

/// array|false
#[inline]
fn t_array_or_false() -> Union {
    let mut u = t_array();
    u.add_type(Atomic::TFalse);
    u
}

/// string|array  (str_replace, preg_replace, etc.)
#[inline]
fn t_str_or_array() -> Union {
    let mut u = t_str();
    u.add_type(Atomic::TArray {
        key: Box::new(Union::mixed()),
        value: Box::new(Union::mixed()),
    });
    u
}

/// string|array|null  (preg_replace_callback result)
#[inline]
fn t_str_or_array_or_null() -> Union {
    let mut u = t_str_or_array();
    u.add_type(Atomic::TNull);
    u
}

/// string|int|bool  (version_compare)
#[inline]
fn t_str_or_int_or_bool() -> Union {
    let mut u = t_str();
    u.add_type(Atomic::TInt);
    u.add_type(Atomic::TBool);
    u
}

/// string|false|null (for some mb_ functions)
#[inline]
fn t_str_or_bool() -> Union {
    let mut u = t_str();
    u.add_type(Atomic::TBool);
    u
}

/// int|float
#[inline]
fn t_int_or_float() -> Union {
    let mut u = t_int();
    u.add_type(Atomic::TFloat);
    u
}

/// Named object type
#[inline]
fn t_obj(fqcn: &'static str) -> Union {
    Union::single(Atomic::TNamedObject {
        fqcn: Arc::from(fqcn),
        type_params: vec![],
    })
}

/// Named object | false
#[inline]
fn t_obj_or_false(fqcn: &'static str) -> Union {
    let mut u = t_obj(fqcn);
    u.add_type(Atomic::TFalse);
    u
}

// ---------------------------------------------------------------------------
// Functions
// ---------------------------------------------------------------------------

#[allow(clippy::too_many_lines)]
fn load_functions(codebase: &Codebase) {
    // ---- String ----------------------------------------------------------------

    reg(codebase, "strlen",     vec![req("str")],                              t_ni());
    reg(codebase, "mb_strlen",  vec![req("str"), opt("encoding")],             t_ni());
    reg(codebase, "substr",     vec![req("str"), req("offset"), opt("length")], t_str());
    reg(codebase, "mb_substr",  vec![req("str"), req("start"), opt("length"), opt("encoding")], t_str());
    reg(codebase, "str_replace",  vec![req("search"), req("replace"), req("subject"), opt("count")], t_str_or_array());
    reg(codebase, "str_ireplace", vec![req("search"), req("replace"), req("subject"), opt("count")], t_str_or_array());
    reg(codebase, "substr_replace", vec![req("string"), req("replace"), req("offset"), opt("length")], t_str_or_array());
    reg(codebase, "str_contains",   vec![req("haystack"), req("needle")], t_bool());
    reg(codebase, "str_starts_with",vec![req("haystack"), req("needle")], t_bool());
    reg(codebase, "str_ends_with",  vec![req("haystack"), req("needle")], t_bool());
    reg(codebase, "strpos",   vec![req("haystack"), req("needle"), opt("offset")], t_int_or_false());
    reg(codebase, "strrpos",  vec![req("haystack"), req("needle"), opt("offset")], t_int_or_false());
    reg(codebase, "stripos",  vec![req("haystack"), req("needle"), opt("offset")], t_int_or_false());
    reg(codebase, "strripos", vec![req("haystack"), req("needle"), opt("offset")], t_int_or_false());
    reg(codebase, "mb_strpos",  vec![req("haystack"), req("needle"), opt("offset"), opt("encoding")], t_int_or_false());
    reg(codebase, "mb_strrpos", vec![req("haystack"), req("needle"), opt("offset"), opt("encoding")], t_int_or_false());
    reg(codebase, "strstr",   vec![req("haystack"), req("needle"), opt("before_needle")], t_str_or_false());
    reg(codebase, "stristr",  vec![req("haystack"), req("needle"), opt("before_needle")], t_str_or_false());
    reg(codebase, "strrchr",  vec![req("haystack"), req("needle")], t_str_or_false());
    reg(codebase, "trim",    vec![req("string"), opt("characters")], t_str());
    reg(codebase, "ltrim",   vec![req("string"), opt("characters")], t_str());
    reg(codebase, "rtrim",   vec![req("string"), opt("characters")], t_str());
    reg(codebase, "strtolower", vec![req("string")], t_str());
    reg(codebase, "strtoupper", vec![req("string")], t_str());
    reg(codebase, "ucfirst",   vec![req("string")], t_str());
    reg(codebase, "lcfirst",   vec![req("string")], t_str());
    reg(codebase, "ucwords",   vec![req("string"), opt("separators")], t_str());
    reg(codebase, "mb_strtolower", vec![req("string"), opt("encoding")], t_str());
    reg(codebase, "mb_strtoupper", vec![req("string"), opt("encoding")], t_str());
    reg(codebase, "mb_convert_encoding", vec![req("string"), req("to_encoding"), opt("from_encoding")], t_str());
    reg(codebase, "mb_detect_encoding", vec![req("string"), opt("encodings"), opt("strict")], t_str_or_false());
    reg(codebase, "mb_internal_encoding", vec![opt("encoding")], t_str_or_bool());
    reg(codebase, "mb_str_split", vec![req("string"), opt("length"), opt("encoding")], t_array());
    reg(codebase, "sprintf",  vec![req("format"), vari("values")],  t_str());
    reg(codebase, "vsprintf", vec![req("format"), req("values")],   t_str());
    reg(codebase, "printf",   vec![req("format"), vari("values")],  t_int());
    reg(codebase, "vprintf",  vec![req("format"), req("values")],   t_int());
    reg(codebase, "sscanf",   vec![req("string"), req("format"), vari("vars")], Union::mixed());
    reg(codebase, "fprintf",  vec![req("handle"), req("format"), vari("values")], t_int());
    reg(codebase, "explode",  vec![req("separator"), req("string"), opt("limit")], t_array());
    reg(codebase, "implode",  vec![req("separator"), opt("array")], t_str());
    reg(codebase, "join",     vec![req("separator"), opt("array")], t_str());
    reg(codebase, "str_split",     vec![req("string"), opt("length")], t_array());
    reg(codebase, "str_word_count",vec![req("string"), opt("format"), opt("characters")], Union::mixed());
    reg(codebase, "str_getcsv",    vec![req("string"), opt("separator"), opt("enclosure"), opt("escape")], t_array());
    reg(codebase, "str_pad",  vec![req("string"), req("length"), opt("pad_string"), opt("pad_type")], t_str());
    reg(codebase, "str_repeat",    vec![req("string"), req("times")], t_str());
    reg(codebase, "wordwrap",      vec![req("string"), opt("width"), opt("break"), opt("cut_long_words")], t_str());
    reg(codebase, "chunk_split",   vec![req("string"), opt("length"), opt("separator")], t_str());
    reg(codebase, "nl2br",         vec![req("string"), opt("use_xhtml")], t_str());
    reg(codebase, "number_format", vec![req("num"), opt("decimals"), opt("decimal_separator"), opt("thousands_separator")], t_str());
    reg(codebase, "htmlspecialchars",       vec![req("string"), opt("flags"), opt("encoding"), opt("double_encode")], t_str());
    reg(codebase, "htmlspecialchars_decode",vec![req("string"), opt("flags")], t_str());
    reg(codebase, "htmlentities",           vec![req("string"), opt("flags"), opt("encoding"), opt("double_encode")], t_str());
    reg(codebase, "html_entity_decode",     vec![req("string"), opt("flags"), opt("encoding")], t_str());
    reg(codebase, "strip_tags",    vec![req("string"), opt("allowed_tags")], t_str());
    reg(codebase, "addslashes",    vec![req("string")], t_str());
    reg(codebase, "stripslashes",  vec![req("string")], t_str());
    reg(codebase, "addcslashes",   vec![req("string"), req("characters")], t_str());
    reg(codebase, "stripcslashes", vec![req("string")], t_str());
    reg(codebase, "md5",         vec![req("string"), opt("binary")], t_str());
    reg(codebase, "sha1",        vec![req("string"), opt("binary")], t_str());
    reg(codebase, "base64_encode",vec![req("string")], t_str());
    reg(codebase, "base64_decode",vec![req("string"), opt("strict")], t_str_or_false());
    reg(codebase, "urlencode",   vec![req("string")], t_str());
    reg(codebase, "urldecode",   vec![req("string")], t_str());
    reg(codebase, "rawurlencode",vec![req("string")], t_str());
    reg(codebase, "rawurldecode",vec![req("string")], t_str());
    reg(codebase, "http_build_query", vec![req("data"), opt("numeric_prefix"), opt("arg_separator"), opt("encoding_type")], t_str());
    reg(codebase, "parse_url",   vec![req("url"), opt("component")], Union::mixed());
    reg(codebase, "quoted_printable_encode", vec![req("string")], t_str());
    reg(codebase, "quoted_printable_decode", vec![req("string")], t_str());
    reg(codebase, "hex2bin",     vec![req("string")], t_str_or_false());
    reg(codebase, "bin2hex",     vec![req("string")], t_str());
    reg(codebase, "crc32",       vec![req("string")], t_int());
    reg(codebase, "crypt",       vec![req("string"), opt("salt")], t_str());
    reg(codebase, "password_hash",   vec![req("password"), req("algo"), opt("options")], t_str());
    reg(codebase, "password_verify", vec![req("password"), req("hash")], t_bool());
    reg(codebase, "hash",        vec![req("algo"), req("data"), opt("binary")], t_str());
    reg(codebase, "hash_hmac",   vec![req("algo"), req("data"), req("key"), opt("binary")], t_str());
    reg(codebase, "hash_equals", vec![req("known_string"), req("user_string")], t_bool());
    reg(codebase, "preg_match",        vec![req("pattern"), req("subject"), byref_opt("matches"), opt("flags"), opt("offset")], t_int_or_false());
    reg(codebase, "preg_match_all",    vec![req("pattern"), req("subject"), byref_opt("matches"), opt("flags"), opt("offset")], t_int_or_false());
    reg(codebase, "preg_replace",      vec![req("pattern"), req("replacement"), req("subject"), opt("limit"), opt("count")], t_str_or_array_or_null());
    reg(codebase, "preg_replace_callback",       vec![req("pattern"), req("callback"), req("subject"), opt("limit"), opt("count")], t_str_or_array_or_null());
    reg(codebase, "preg_replace_callback_array", vec![req("pattern"), req("subject"), opt("limit"), opt("count")], t_str_or_array_or_null());
    reg(codebase, "preg_split",  vec![req("pattern"), req("subject"), opt("limit"), opt("flags")], t_array_or_false());
    reg(codebase, "preg_quote",  vec![req("string"), opt("delimiter")], t_str());
    reg(codebase, "substr_count",vec![req("haystack"), req("needle"), opt("offset"), opt("length")], t_int());
    reg(codebase, "strcmp",      vec![req("string1"), req("string2")], t_int());
    reg(codebase, "strcasecmp",  vec![req("string1"), req("string2")], t_int());
    reg(codebase, "strncmp",     vec![req("string1"), req("string2"), req("length")], t_int());
    reg(codebase, "strncasecmp", vec![req("string1"), req("string2"), req("length")], t_int());
    reg(codebase, "strnatcmp",      vec![req("string1"), req("string2")], t_int());
    reg(codebase, "strnatcasecmp",  vec![req("string1"), req("string2")], t_int());
    reg(codebase, "version_compare", vec![req("version1"), req("version2"), opt("operator")], t_str_or_int_or_bool());
    reg(codebase, "similar_text", vec![req("string1"), req("string2"), opt("percent")], t_int());
    reg(codebase, "soundex",     vec![req("string")], t_str());
    reg(codebase, "metaphone",   vec![req("string"), opt("max_phonemes")], t_str());
    reg(codebase, "levenshtein", vec![req("string1"), req("string2")], t_int());
    reg(codebase, "ord",  vec![req("character")], t_int());
    reg(codebase, "chr",  vec![req("codepoint")],  t_str());
    reg(codebase, "ctype_alpha",  vec![req("string")], t_bool());
    reg(codebase, "ctype_digit",  vec![req("string")], t_bool());
    reg(codebase, "ctype_alnum",  vec![req("string")], t_bool());
    reg(codebase, "ctype_space",  vec![req("string")], t_bool());
    reg(codebase, "serialize",   vec![req("value")], t_str());
    reg(codebase, "unserialize", vec![req("data"), opt("options")], Union::mixed());
    reg(codebase, "dirname",     vec![req("path"), opt("levels")], t_str());
    reg(codebase, "basename",    vec![req("path"), opt("suffix")], t_str());
    reg(codebase, "pathinfo",    vec![req("path"), opt("option")], Union::mixed());
    reg(codebase, "realpath",    vec![req("path")], t_str_or_false());
    reg(codebase, "getcwd",      vec![], t_str_or_false());
    reg(codebase, "tempnam",     vec![req("directory"), req("prefix")], t_str_or_false());
    reg(codebase, "sys_get_temp_dir", vec![], t_str());
    reg(codebase, "inet_ntop",   vec![req("ip")], t_str_or_false());
    reg(codebase, "inet_pton",   vec![req("ip")], t_str_or_false());
    reg(codebase, "ip2long",     vec![req("ip")], t_int_or_false());
    reg(codebase, "long2ip",     vec![req("ip")], t_str());
    reg(codebase, "money_format",  vec![req("format"), req("num")], t_str());
    reg(codebase, "base_convert",  vec![req("num"), req("from_base"), req("to_base")], t_str());
    reg(codebase, "bindec",   vec![req("binary_string")], t_float());
    reg(codebase, "octdec",   vec![req("octal_string")],  t_float());
    reg(codebase, "hexdec",   vec![req("hex_string")],    t_float());
    reg(codebase, "decoct",   vec![req("num")], t_str());
    reg(codebase, "dechex",   vec![req("num")], t_str());
    reg(codebase, "decbin",   vec![req("num")], t_str());

    reg(codebase, "pack",   vec![req("format"), vari("values")], t_str_or_false());
    reg(codebase, "unpack", vec![req("format"), req("string"), opt("offset")], t_array_or_false());

    // ---- Array -----------------------------------------------------------------

    reg(codebase, "count",    vec![req("array"), opt("mode")],  t_ni());
    reg(codebase, "sizeof",   vec![req("array"), opt("mode")],  t_ni());
    reg(codebase, "array_keys",    vec![req("array"), opt("filter_value"), opt("strict")], t_array());
    reg(codebase, "array_values",  vec![req("array")], t_array());
    reg(codebase, "array_reverse", vec![req("array"), opt("preserve_keys")], t_array());
    reg(codebase, "array_unique",  vec![req("array"), opt("flags")], t_array());
    reg(codebase, "array_flip",    vec![req("array")], t_array());
    reg(codebase, "array_merge",   vec![vari("arrays")], t_array());
    reg(codebase, "array_slice",   vec![req("array"), req("offset"), opt("length"), opt("preserve_keys")], t_array());
    reg(codebase, "array_map",     vec![req("callback"), req("array"), vari("arrays")], t_array());
    reg(codebase, "array_filter",  vec![req("array"), opt("callback"), opt("mode")], t_array());
    reg(codebase, "array_pop",     vec![byref("array")], Union::mixed());
    reg(codebase, "array_shift",   vec![byref("array")], Union::mixed());
    reg(codebase, "array_push",    vec![byref("array"), vari("values")], t_int());
    reg(codebase, "array_unshift", vec![byref("array"), vari("values")], t_int());
    reg(codebase, "in_array",          vec![req("needle"), req("haystack"), opt("strict")], t_bool());
    reg(codebase, "array_key_exists",  vec![req("key"), req("array")], t_bool());
    reg(codebase, "array_search",      vec![req("needle"), req("haystack"), opt("strict")], Union::mixed());
    reg(codebase, "array_combine",     vec![req("keys"), req("values")], Union::mixed());
    reg(codebase, "array_diff",        vec![req("array"), vari("arrays")], t_array());
    reg(codebase, "array_diff_key",    vec![req("array"), vari("arrays")], t_array());
    reg(codebase, "array_diff_assoc",  vec![req("array"), vari("arrays")], t_array());
    reg(codebase, "array_intersect",        vec![req("array"), vari("arrays")], t_array());
    reg(codebase, "array_intersect_key",    vec![req("array"), vari("arrays")], t_array());
    reg(codebase, "array_intersect_assoc",  vec![req("array"), vari("arrays")], t_array());
    reg(codebase, "array_fill",      vec![req("start_index"), req("count"), req("value")], t_array());
    reg(codebase, "array_fill_keys", vec![req("keys"), req("value")], t_array());
    reg(codebase, "array_pad",       vec![req("array"), req("length"), req("value")], t_array());
    reg(codebase, "array_chunk",     vec![req("array"), req("length"), opt("preserve_keys")], t_array());
    reg(codebase, "array_column",    vec![req("array"), req("column_key"), opt("index_key")], t_array());
    reg(codebase, "array_count_values",   vec![req("array")], t_array());
    reg(codebase, "array_walk",           vec![byref("array"), req("callback"), opt("arg")], t_bool());
    reg(codebase, "array_walk_recursive", vec![byref("array"), req("callback"), opt("arg")], t_bool());
    reg(codebase, "array_splice",  vec![byref("array"), req("offset"), opt("length"), opt("replacement")], t_array());
    reg(codebase, "range",         vec![req("start"), req("end"), opt("step")], t_array());
    reg(codebase, "array_reduce",  vec![req("array"), req("callback"), opt("initial")], Union::mixed());
    reg(codebase, "sort",    vec![byref("array"), opt("flags")], t_bool());
    reg(codebase, "rsort",   vec![byref("array"), opt("flags")], t_bool());
    reg(codebase, "asort",   vec![byref("array"), opt("flags")], t_bool());
    reg(codebase, "arsort",  vec![byref("array"), opt("flags")], t_bool());
    reg(codebase, "ksort",   vec![byref("array"), opt("flags")], t_bool());
    reg(codebase, "krsort",  vec![byref("array"), opt("flags")], t_bool());
    reg(codebase, "usort",   vec![byref("array"), req("callback")], t_bool());
    reg(codebase, "uasort",  vec![byref("array"), req("callback")], t_bool());
    reg(codebase, "uksort",  vec![byref("array"), req("callback")], t_bool());
    reg(codebase, "shuffle", vec![byref("array")], t_bool());
    reg(codebase, "natcasesort", vec![byref("array")], t_bool());
    reg(codebase, "natsort",     vec![byref("array")], t_bool());
    reg(codebase, "array_key_first", vec![req("array")], t_int_str_null());
    reg(codebase, "array_key_last",  vec![req("array")], t_int_str_null());
    reg(codebase, "compact",  vec![vari("var_names")], t_array());
    reg(codebase, "extract",  vec![byref("array"), opt("flags"), opt("prefix")], t_int());
    reg(codebase, "list",     vec![vari("vars")], Union::mixed());

    // ---- Math ------------------------------------------------------------------

    reg(codebase, "abs",   vec![req("num")], Union::mixed());
    reg(codebase, "ceil",  vec![req("num")], t_float());
    reg(codebase, "floor", vec![req("num")], t_float());
    reg(codebase, "round", vec![req("num"), opt("precision"), opt("mode")], t_float());
    reg(codebase, "fmod",  vec![req("num1"), req("num2")], t_float());
    reg(codebase, "fdiv",  vec![req("num1"), req("num2")], t_float());
    reg(codebase, "pow",   vec![req("base"), req("exp")], Union::mixed());
    reg(codebase, "sqrt",  vec![req("num")], t_float());
    reg(codebase, "log",   vec![req("num"), opt("base")], t_float());
    reg(codebase, "log10", vec![req("num")], t_float());
    reg(codebase, "log2",  vec![req("num")], t_float());
    reg(codebase, "exp",   vec![req("num")], t_float());
    reg(codebase, "sin",   vec![req("num")], t_float());
    reg(codebase, "cos",   vec![req("num")], t_float());
    reg(codebase, "tan",   vec![req("num")], t_float());
    reg(codebase, "asin",  vec![req("num")], t_float());
    reg(codebase, "acos",  vec![req("num")], t_float());
    reg(codebase, "atan",  vec![req("num")], t_float());
    reg(codebase, "atan2", vec![req("y"), req("x")], t_float());
    reg(codebase, "sinh",  vec![req("num")], t_float());
    reg(codebase, "cosh",  vec![req("num")], t_float());
    reg(codebase, "tanh",  vec![req("num")], t_float());
    reg(codebase, "deg2rad", vec![req("num")], t_float());
    reg(codebase, "rad2deg", vec![req("num")], t_float());
    reg(codebase, "pi",      vec![], t_float());
    reg(codebase, "hypot",   vec![req("x"), req("y")], t_float());
    reg(codebase, "fabs",    vec![req("num")], t_float());
    reg(codebase, "intdiv",  vec![req("num"), req("divisor")], t_int());
    reg(codebase, "rand",       vec![opt("min"), opt("max")], t_int());
    reg(codebase, "mt_rand",    vec![opt("min"), opt("max")], t_int());
    reg(codebase, "random_int", vec![req("min"), req("max")], t_int());
    reg(codebase, "array_sum",     vec![req("array")], t_int_or_float());
    reg(codebase, "array_product", vec![req("array")], t_int_or_float());
    reg(codebase, "max",  vec![req("value"), vari("values")], Union::mixed());
    reg(codebase, "min",  vec![req("value"), vari("values")], Union::mixed());

    // ---- Type coercion / checking ----------------------------------------------

    reg(codebase, "intval",    vec![req("value"), opt("base")], t_int());
    reg(codebase, "floatval",  vec![req("value")], t_float());
    reg(codebase, "doubleval", vec![req("value")], t_float());
    reg(codebase, "strval",    vec![req("value")], t_str());
    reg(codebase, "boolval",   vec![req("value")], t_bool());
    reg(codebase, "settype",   vec![byref("var"), req("type")], t_bool());
    reg(codebase, "is_string",   vec![req("value")], t_bool());
    reg(codebase, "is_int",      vec![req("value")], t_bool());
    reg(codebase, "is_integer",  vec![req("value")], t_bool());
    reg(codebase, "is_long",     vec![req("value")], t_bool());
    reg(codebase, "is_float",    vec![req("value")], t_bool());
    reg(codebase, "is_double",   vec![req("value")], t_bool());
    reg(codebase, "is_real",     vec![req("value")], t_bool());
    reg(codebase, "is_bool",     vec![req("value")], t_bool());
    reg(codebase, "is_null",     vec![req("value")], t_bool());
    reg(codebase, "is_array",    vec![req("value")], t_bool());
    reg(codebase, "is_object",   vec![req("value")], t_bool());
    reg(codebase, "is_callable", vec![req("value"), opt("syntax_only"), opt("callable_name")], t_bool());
    reg(codebase, "is_numeric",  vec![req("value")], t_bool());
    reg(codebase, "is_resource", vec![req("value")], t_bool());
    reg(codebase, "is_finite",   vec![req("value")], t_bool());
    reg(codebase, "is_infinite", vec![req("value")], t_bool());
    reg(codebase, "is_nan",      vec![req("value")], t_bool());
    reg(codebase, "is_a",          vec![req("object"), req("class"), opt("allow_string")], t_bool());
    reg(codebase, "is_subclass_of",vec![req("object"), req("class"), opt("allow_string")], t_bool());
    reg(codebase, "gettype",        vec![req("value")], t_str());
    reg(codebase, "get_debug_type", vec![req("value")], t_str());
    reg(codebase, "get_class",        vec![opt("object")], t_str_or_false());
    reg(codebase, "get_parent_class", vec![opt("object")], t_str_or_false());
    reg(codebase, "get_called_class", vec![], t_str());
    reg(codebase, "class_exists",     vec![req("class"), opt("autoload")], t_bool());
    reg(codebase, "interface_exists", vec![req("class"), opt("autoload")], t_bool());
    reg(codebase, "trait_exists",     vec![req("class"), opt("autoload")], t_bool());
    reg(codebase, "function_exists",  vec![req("function")], t_bool());
    reg(codebase, "method_exists",    vec![req("object"), req("method")], t_bool());
    reg(codebase, "property_exists",  vec![req("object"), req("property")], t_bool());
    reg(codebase, "defined",          vec![req("constant")], t_bool());
    reg(codebase, "extension_loaded", vec![req("extension")], t_bool());

    // ---- JSON ------------------------------------------------------------------

    reg(codebase, "json_encode", vec![req("value"), opt("flags"), opt("depth")], t_str_or_false());
    reg(codebase, "json_decode", vec![req("json"), opt("associative"), opt("depth"), opt("flags")], Union::mixed());

    // ---- File I/O --------------------------------------------------------------

    reg(codebase, "file_get_contents", vec![req("filename"), opt("use_include_path"), opt("context"), opt("offset"), opt("length")], t_str_or_false());
    reg(codebase, "file_put_contents", vec![req("filename"), req("data"), opt("flags"), opt("context")], t_int_or_false());
    reg(codebase, "fopen",  vec![req("filename"), req("mode"), opt("use_include_path"), opt("context")], t_obj_or_false("resource"));
    reg(codebase, "fread",  vec![req("handle"), req("length")], t_str_or_false());
    reg(codebase, "fwrite", vec![req("handle"), req("string"), opt("length")], t_int_or_false());
    reg(codebase, "fgets",  vec![req("handle"), opt("length")], t_str_or_false());
    reg(codebase, "fgetc",  vec![req("handle")], t_str_or_false());
    reg(codebase, "fgetss", vec![req("handle"), opt("length"), opt("allowable_tags")], t_str_or_false());
    reg(codebase, "fclose", vec![req("handle")], t_bool());
    reg(codebase, "feof",   vec![req("handle")], t_bool());
    reg(codebase, "fseek",  vec![req("handle"), req("offset"), opt("whence")], t_int());
    reg(codebase, "ftell",  vec![req("handle")], t_int_or_false());
    reg(codebase, "rewind", vec![req("handle")], t_bool());
    reg(codebase, "fflush", vec![req("handle")], t_bool());
    reg(codebase, "file",   vec![req("filename"), opt("flags"), opt("context")], t_array_or_false());
    reg(codebase, "glob",   vec![req("pattern"), opt("flags")], t_array_or_false());
    reg(codebase, "scandir",vec![req("directory"), opt("sorting_order"), opt("context")], t_array_or_false());
    reg(codebase, "readdir",vec![opt("dir_handle")], t_str_or_false());
    reg(codebase, "opendir",vec![req("directory"), opt("context")], Union::mixed());
    reg(codebase, "closedir",vec![opt("dir_handle")], t_void());
    reg(codebase, "file_exists",   vec![req("filename")], t_bool());
    reg(codebase, "is_file",       vec![req("filename")], t_bool());
    reg(codebase, "is_dir",        vec![req("filename")], t_bool());
    reg(codebase, "is_readable",   vec![req("filename")], t_bool());
    reg(codebase, "is_writable",   vec![req("filename")], t_bool());
    reg(codebase, "is_executable", vec![req("filename")], t_bool());
    reg(codebase, "is_link",       vec![req("filename")], t_bool());
    reg(codebase, "mkdir",  vec![req("directory"), opt("permissions"), opt("recursive"), opt("context")], t_bool());
    reg(codebase, "rmdir",  vec![req("directory"), opt("context")], t_bool());
    reg(codebase, "unlink", vec![req("filename"), opt("context")], t_bool());
    reg(codebase, "rename", vec![req("from"), req("to"), opt("context")], t_bool());
    reg(codebase, "copy",   vec![req("from"), req("to"), opt("context")], t_bool());
    reg(codebase, "symlink",vec![req("target"), req("link")], t_bool());
    reg(codebase, "link",   vec![req("target"), req("link")], t_bool());
    reg(codebase, "chdir",  vec![req("directory")], t_bool());
    reg(codebase, "chown",  vec![req("filename"), req("user")], t_bool());
    reg(codebase, "chmod",  vec![req("filename"), req("permissions")], t_bool());
    reg(codebase, "filemtime",  vec![req("filename")], t_int_or_false());
    reg(codebase, "fileatime", vec![req("filename")], t_int_or_false());
    reg(codebase, "filectime", vec![req("filename")], t_int_or_false());
    reg(codebase, "filesize",  vec![req("filename")], t_int_or_false());
    reg(codebase, "fileperms", vec![req("filename")], t_int_or_false());
    reg(codebase, "fileinode", vec![req("filename")], t_int_or_false());
    reg(codebase, "stream_get_contents",  vec![req("stream"), opt("length"), opt("offset")], t_str_or_false());
    reg(codebase, "stream_socket_client",vec![req("address"), opt("error_code"), opt("error_message"), opt("timeout"), opt("flags"), opt("context")], t_obj_or_false("resource"));
    reg(codebase, "popen",  vec![req("command"), req("mode")], t_obj_or_false("resource"));

    // ---- Date / time -----------------------------------------------------------

    reg(codebase, "time",        vec![], t_int());
    reg(codebase, "mktime",      vec![opt("hour"), opt("minute"), opt("second"), opt("month"), opt("day"), opt("year")], t_int_or_false());
    reg(codebase, "gmmktime",    vec![opt("hour"), opt("minute"), opt("second"), opt("month"), opt("day"), opt("year")], t_int_or_false());
    reg(codebase, "strtotime",   vec![req("datetime"), opt("baseTimestamp")], t_int_or_false());
    reg(codebase, "date",        vec![req("format"), opt("timestamp")], t_str());
    reg(codebase, "gmdate",      vec![req("format"), opt("timestamp")], t_str());
    reg(codebase, "strftime",    vec![req("format"), opt("timestamp")], t_str_or_false());
    reg(codebase, "gmstrftime",  vec![req("format"), opt("timestamp")], t_str_or_false());
    reg(codebase, "microtime",   vec![opt("as_float")], Union::mixed());
    reg(codebase, "checkdate",   vec![req("month"), req("day"), req("year")], t_bool());
    reg(codebase, "date_create", vec![opt("datetime"), opt("timezone")], t_obj_or_false("DateTime"));
    reg(codebase, "date_format", vec![req("datetime"), req("format")], t_str_or_false());
    reg(codebase, "date_modify", vec![req("object"), req("modifier")], t_obj_or_false("DateTime"));

    // ---- Output control --------------------------------------------------------

    reg(codebase, "ob_start",       vec![opt("callback"), opt("chunk_size"), opt("flags")], t_bool());
    reg(codebase, "ob_end_clean",   vec![], t_bool());
    reg(codebase, "ob_end_flush",   vec![], t_bool());
    reg(codebase, "ob_flush",       vec![], t_bool());
    reg(codebase, "ob_get_clean",   vec![], t_str_or_false());
    reg(codebase, "ob_get_contents",vec![], t_str_or_false());
    reg(codebase, "ob_get_level",   vec![], t_int());
    reg(codebase, "ob_get_length",  vec![], t_int_or_false());
    reg(codebase, "ob_get_status",  vec![opt("full_status")], t_array());
    reg(codebase, "flush",          vec![], t_void());

    // ---- Session ---------------------------------------------------------------

    reg(codebase, "session_start",           vec![opt("options")], t_bool());
    reg(codebase, "session_destroy",         vec![], t_bool());
    reg(codebase, "session_regenerate_id",   vec![opt("delete_old_session")], t_bool());
    reg(codebase, "session_id",              vec![opt("id")], t_str_or_false());
    reg(codebase, "session_name",            vec![opt("name")], t_str_or_false());

    // ---- Headers / cookies -----------------------------------------------------

    reg(codebase, "header",        vec![req("header"), opt("replace"), opt("response_code")], t_void());
    reg(codebase, "headers_sent",  vec![opt("filename"), opt("line")], t_bool());
    reg(codebase, "headers_list",  vec![], t_array());
    reg(codebase, "header_remove", vec![opt("name")], t_void());
    reg(codebase, "setcookie",    vec![req("name"), opt("value"), opt("expires_or_options"), opt("path"), opt("domain"), opt("secure"), opt("httponly")], t_bool());

    // ---- Misc / control-flow ---------------------------------------------------

    reg(codebase, "var_dump",    vec![req("value"), vari("values")], t_void());
    reg(codebase, "print_r",     vec![req("value"), opt("return")], Union::mixed());
    reg(codebase, "var_export",  vec![req("value"), opt("return")], Union::mixed());
    reg(codebase, "die",         vec![opt("status")], t_never());
    reg(codebase, "exit",        vec![opt("status")], t_never());
    reg(codebase, "assert",      vec![req("assertion"), opt("description")], t_bool());
    reg(codebase, "sleep",       vec![req("seconds")], t_int_or_false());
    reg(codebase, "usleep",      vec![req("microseconds")], t_void());
    reg(codebase, "gc_collect_cycles", vec![], t_int());
    reg(codebase, "call_user_func",        vec![req("callback"), vari("args")], Union::mixed());
    reg(codebase, "call_user_func_array",  vec![req("callback"), req("args")],  Union::mixed());
    reg(codebase, "forward_static_call",       vec![req("callback"), vari("args")], Union::mixed());
    reg(codebase, "forward_static_call_array", vec![req("callback"), req("args")], Union::mixed());
    reg(codebase, "func_get_args",  vec![], t_array());
    reg(codebase, "func_get_arg",   vec![req("position")], Union::mixed());
    reg(codebase, "func_num_args",  vec![], t_int());
    reg(codebase, "clone",          vec![req("object")], Union::mixed());
    reg(codebase, "isset",          vec![vari("vars")], t_bool());
    reg(codebase, "empty",          vec![req("var")], t_bool());

    // ---- Error / runtime -------------------------------------------------------

    reg(codebase, "error_log",              vec![req("message"), opt("message_type"), opt("destination"), opt("additional_headers")], t_bool());
    reg(codebase, "trigger_error",          vec![req("message"), opt("error_level")], t_bool());
    reg(codebase, "user_error",             vec![req("message"), opt("error_level")], t_bool());
    reg(codebase, "set_error_handler",      vec![req("callback"), opt("error_levels")], Union::mixed());
    reg(codebase, "set_exception_handler",  vec![req("callback")], Union::mixed());
    reg(codebase, "error_reporting",        vec![opt("error_level")], t_int());
    reg(codebase, "restore_error_handler",  vec![], t_bool());
    reg(codebase, "restore_exception_handler", vec![], t_bool());
    reg(codebase, "ignore_user_abort",      vec![opt("value")], t_int());
    reg(codebase, "register_shutdown_function", vec![req("callback"), vari("args")], t_void());

    // ---- System info -----------------------------------------------------------

    reg(codebase, "ini_set",          vec![req("option"), req("value")], t_str_or_false());
    reg(codebase, "ini_get",          vec![req("option")], t_str_or_false());
    reg(codebase, "php_uname",        vec![opt("mode")], t_str());
    reg(codebase, "phpversion",       vec![opt("extension")], t_str_or_false());
    reg(codebase, "php_sapi_name",    vec![], t_str_or_false());
    reg(codebase, "zend_version",     vec![], t_str());
    reg(codebase, "memory_get_usage", vec![opt("real_usage")], t_int());
    reg(codebase, "memory_get_peak_usage", vec![opt("real_usage")], t_int());
    reg(codebase, "getmypid",         vec![], t_int_or_false());
    reg(codebase, "getmyuid",         vec![], t_int_or_false());
    reg(codebase, "getenv",           vec![opt("varname"), opt("local_only")], Union::mixed());
    reg(codebase, "putenv",           vec![req("assignment")], t_bool());

    // ---- Misc ------------------------------------------------------------------

    reg(codebase, "parse_str",       vec![req("string"), byref("result")], t_void());
    reg(codebase, "parse_ini_file",  vec![req("filename"), opt("process_sections"), opt("scanner_mode")], t_array_or_false());
    reg(codebase, "parse_ini_string",vec![req("ini_string"), opt("process_sections"), opt("scanner_mode")], t_array_or_false());
    reg(codebase, "openssl_encrypt", vec![req("data"), req("cipher_algo"), req("passphrase"), opt("options"), opt("iv"), opt("tag"), opt("aad"), opt("tag_length")], t_str_or_false());
    reg(codebase, "openssl_decrypt", vec![req("data"), req("cipher_algo"), req("passphrase"), opt("options"), opt("iv"), opt("tag"), opt("aad")], t_str_or_false());
    reg(codebase, "curl_init",       vec![opt("url")], t_obj_or_false("CurlHandle"));
    reg(codebase, "curl_setopt",     vec![req("handle"), req("option"), req("value")], t_bool());
    reg(codebase, "curl_exec",       vec![req("handle")], Union::mixed());
    reg(codebase, "curl_close",      vec![req("handle")], t_void());
    reg(codebase, "curl_error",      vec![req("handle")], t_str());
    reg(codebase, "curl_errno",      vec![req("handle")], t_int());
    reg(codebase, "mysqli_connect",  vec![opt("host"), opt("user"), opt("passwd"), opt("db"), opt("port"), opt("socket")], Union::mixed());
    reg(codebase, "mysqli_query",    vec![req("mysql"), req("query"), opt("result_mode")], Union::mixed());
    reg(codebase, "mysqli_fetch_assoc", vec![req("result")], Union::mixed());
    reg(codebase, "mysqli_fetch_row",   vec![req("result")], Union::mixed());
    reg(codebase, "mysqli_fetch_array", vec![req("result"), opt("mode")], Union::mixed());
    reg(codebase, "mysqli_close",    vec![req("mysql")], t_void());
    reg(codebase, "mysqli_real_escape_string", vec![req("mysql"), req("string")], t_str());
    reg(codebase, "pg_connect",      vec![req("connection_string")], Union::mixed());
    reg(codebase, "pg_query",        vec![req("connection_or_query"), opt("query")], Union::mixed());
    reg(codebase, "pg_exec",         vec![req("connection_or_query"), opt("query")], Union::mixed());
    reg(codebase, "pg_fetch_assoc",  vec![req("result"), opt("row")], Union::mixed());
    reg(codebase, "pg_fetch_row",    vec![req("result"), opt("row"), opt("mode")], Union::mixed());
    reg(codebase, "pg_close",        vec![opt("connection")], t_bool());
    reg(codebase, "socket_create",   vec![req("domain"), req("type"), req("protocol")], Union::mixed());

    // ---- Missing array functions -----------------------------------------------
    reg(codebase, "array_replace",         vec![req("array"), vari("replacements")], t_array());
    reg(codebase, "array_replace_recursive",vec![req("array"), vari("replacements")], t_array());
    reg(codebase, "array_find",            vec![req("array"), req("callback")], Union::mixed());
    reg(codebase, "array_find_key",        vec![req("array"), req("callback")], Union::mixed());
    reg(codebase, "array_any",             vec![req("array"), req("callback")], t_bool());
    reg(codebase, "array_all",             vec![req("array"), req("callback")], t_bool());
    reg(codebase, "array_is_list",         vec![req("array")], t_bool());
    reg(codebase, "current",               vec![req("array")], Union::mixed());
    reg(codebase, "next",                  vec![byref("array")], Union::mixed());
    reg(codebase, "prev",                  vec![byref("array")], Union::mixed());
    reg(codebase, "reset",                 vec![byref("array")], Union::mixed());
    reg(codebase, "end",                   vec![byref("array")], Union::mixed());
    reg(codebase, "key",                   vec![req("array")], Union::mixed());
    reg(codebase, "each",                  vec![byref("array")], Union::mixed());
    reg(codebase, "list",                  vec![vari("vars")], Union::mixed());

    // ---- Session ---------------------------------------------------------------
    reg(codebase, "session_status",        vec![], t_int());
    reg(codebase, "session_write_close",   vec![], t_bool());
    reg(codebase, "session_set_cookie_params", vec![req("lifetime_or_options"), opt("path"), opt("domain"), opt("secure"), opt("httponly")], t_bool());
    reg(codebase, "session_get_cookie_params", vec![], t_array());
    reg(codebase, "session_unset",         vec![], t_bool());
    reg(codebase, "session_save_path",     vec![opt("path")], t_str_or_false());

    // ---- Filter ----------------------------------------------------------------
    reg(codebase, "filter_var",        vec![req("value"), opt("filter"), opt("options")], Union::mixed());
    reg(codebase, "filter_input",      vec![req("type"), req("var_name"), opt("filter"), opt("options")], Union::mixed());
    reg(codebase, "filter_has_var",    vec![req("input_type"), req("var_name")], t_bool());
    reg(codebase, "filter_var_array",  vec![req("array"), opt("options"), opt("add_empty")], Union::mixed());
    reg(codebase, "filter_input_array",vec![req("type"), opt("options"), opt("add_empty")], Union::mixed());

    // ---- Iterator / SPL --------------------------------------------------------
    reg(codebase, "iterator_to_array", vec![req("iterator"), opt("preserve_keys")], t_array());
    reg(codebase, "iterator_count",    vec![req("iterator")], t_int());
    reg(codebase, "iterator_apply",    vec![req("iterator"), req("callback"), opt("args")], t_int());

    // ---- String (additional) ---------------------------------------------------
    reg(codebase, "strrev",            vec![req("string")], t_str());
    reg(codebase, "str_word_count",    vec![req("string"), opt("format"), opt("characters")], Union::mixed());
    reg(codebase, "wordwrap",          vec![req("string"), opt("width"), opt("break"), opt("cut_long_words")], t_str());
    reg(codebase, "number_format",     vec![req("num"), opt("decimals"), opt("decimal_separator"), opt("thousands_separator")], t_str());
    reg(codebase, "money_format",      vec![req("format"), req("num")], t_str());
    reg(codebase, "nl2br",             vec![req("string"), opt("use_xhtml")], t_str());
    reg(codebase, "htmlentities",      vec![req("string"), opt("flags"), opt("encoding"), opt("double_encode")], t_str());
    reg(codebase, "quoted_printable_encode", vec![req("string")], t_str());
    reg(codebase, "quoted_printable_decode", vec![req("string")], t_str());
    reg(codebase, "chunk_split",       vec![req("string"), opt("length"), opt("separator")], t_str());
    reg(codebase, "str_contains",      vec![req("haystack"), req("needle")], t_bool());
    reg(codebase, "str_starts_with",   vec![req("haystack"), req("needle")], t_bool());
    reg(codebase, "str_ends_with",     vec![req("haystack"), req("needle")], t_bool());
    reg(codebase, "str_split",         vec![req("string"), opt("length")], t_array());
    reg(codebase, "grapheme_strlen",   vec![req("string")], Union::mixed());
    reg(codebase, "grapheme_substr",   vec![req("string"), req("offset"), opt("length")], Union::mixed());

    // ---- Math (additional) -----------------------------------------------------
    reg(codebase, "number_format",     vec![req("num"), opt("decimals"), opt("decimal_separator"), opt("thousands_separator")], t_str());
    reg(codebase, "random_bytes",      vec![req("length")], t_str());
    reg(codebase, "intdiv",            vec![req("num"), req("divisor")], t_int());

    // ---- Exec / process --------------------------------------------------------
    reg(codebase, "exec",         vec![req("command"), byref_opt("output"), byref_opt("result_code")], t_str_or_false());
    reg(codebase, "system",       vec![req("command"), byref_opt("result_code")], t_str_or_false());
    reg(codebase, "passthru",     vec![req("command"), byref_opt("result_code")], t_void());
    reg(codebase, "shell_exec",   vec![req("command")], Union::mixed());
    reg(codebase, "proc_open",    vec![req("command"), req("descriptor_spec"), byref("pipes"), opt("cwd"), opt("env_vars"), opt("options")], Union::mixed());
    reg(codebase, "proc_close",   vec![req("process")], t_int());
    reg(codebase, "proc_get_status", vec![req("process")], t_array());
    reg(codebase, "pclose",       vec![req("handle")], t_int());

    // ---- File (additional) -----------------------------------------------------
    reg(codebase, "touch",         vec![req("filename"), opt("mtime"), opt("atime")], t_bool());
    reg(codebase, "fputcsv",       vec![req("stream"), req("fields"), opt("separator"), opt("enclosure"), opt("escape"), opt("eol")], t_int_or_false());
    reg(codebase, "fgetcsv",       vec![req("stream"), opt("length"), opt("separator"), opt("enclosure"), opt("escape")], Union::mixed());
    reg(codebase, "tmpfile",       vec![], Union::mixed());
    reg(codebase, "tempnam",       vec![req("directory"), req("prefix")], t_str_or_false());
    reg(codebase, "realpath",      vec![req("path")], t_str_or_false());
    reg(codebase, "pathinfo",      vec![req("path"), opt("options")], Union::mixed());
    reg(codebase, "basename",      vec![req("path"), opt("suffix")], t_str());
    reg(codebase, "dirname",       vec![req("path"), opt("levels")], t_str());
    reg(codebase, "stat",          vec![req("filename")], Union::mixed());
    reg(codebase, "lstat",         vec![req("filename")], Union::mixed());
    reg(codebase, "clearstatcache",vec![opt("clear_realpath_cache"), opt("filename")], t_void());

    // ---- Misc (additional) -----------------------------------------------------
    reg(codebase, "is_scalar",     vec![req("value")], t_bool());
    reg(codebase, "is_iterable",   vec![req("value")], t_bool());
    reg(codebase, "is_countable",  vec![req("value")], t_bool());
    reg(codebase, "unserialize",   vec![req("data"), opt("options")], Union::mixed());
    reg(codebase, "serialize",     vec![req("value")], t_str());
    reg(codebase, "base64_encode", vec![req("string")], t_str());
    reg(codebase, "base64_decode", vec![req("string"), opt("strict")], t_str_or_false());
    reg(codebase, "http_build_query",vec![req("data"), opt("numeric_prefix"), opt("arg_separator"), opt("encoding_type")], t_str());
    reg(codebase, "parse_url",     vec![req("url"), opt("component")], Union::mixed());
    reg(codebase, "urlencode",     vec![req("string")], t_str());
    reg(codebase, "urldecode",     vec![req("string")], t_str());
    reg(codebase, "rawurlencode",  vec![req("string")], t_str());
    reg(codebase, "rawurldecode",  vec![req("string")], t_str());
    reg(codebase, "gzencode",      vec![req("data"), opt("level"), opt("encoding")], t_str_or_false());
    reg(codebase, "gzdecode",      vec![req("data"), opt("max_length")], t_str_or_false());
    reg(codebase, "gzcompress",    vec![req("data"), opt("level"), opt("encoding")], t_str_or_false());
    reg(codebase, "gzuncompress",  vec![req("data"), opt("max_length")], t_str_or_false());
    reg(codebase, "gzinflate",     vec![req("data"), opt("max_length")], t_str_or_false());
    reg(codebase, "gzdeflate",     vec![req("data"), opt("level"), opt("encoding")], t_str_or_false());
    reg(codebase, "crc32",         vec![req("string")], t_int());
    reg(codebase, "md5",           vec![req("string"), opt("binary")], t_str());
    reg(codebase, "sha1",          vec![req("string"), opt("binary")], t_str());
    reg(codebase, "ctype_alpha",   vec![req("text")], t_bool());
    reg(codebase, "ctype_digit",   vec![req("text")], t_bool());
    reg(codebase, "ctype_alnum",   vec![req("text")], t_bool());
    reg(codebase, "ctype_space",   vec![req("text")], t_bool());
    reg(codebase, "ctype_upper",   vec![req("text")], t_bool());
    reg(codebase, "ctype_lower",   vec![req("text")], t_bool());
    reg(codebase, "class_implements", vec![req("object"), opt("autoload")], Union::mixed());
    reg(codebase, "class_parents",    vec![req("object"), opt("autoload")], Union::mixed());
    reg(codebase, "class_uses",       vec![req("object"), opt("autoload")], Union::mixed());
    reg(codebase, "get_object_vars",  vec![req("object")], t_array());
    reg(codebase, "get_class_methods",vec![req("object")], Union::mixed());
    reg(codebase, "spl_autoload_register", vec![opt("callback"), opt("throw"), opt("prepend")], t_void());
    reg(codebase, "spl_object_hash", vec![req("object")], t_str());
    reg(codebase, "spl_object_id",   vec![req("object")], t_int());
    reg(codebase, "intl_error_name", vec![req("error_code")], t_str());

    // ---- PHPUnit-style assertions (often used in test helpers) -----------------
    reg(codebase, "assertEquals",        vec![req("expected"), req("actual"), opt("message")], t_void());
    reg(codebase, "assertSame",          vec![req("expected"), req("actual"), opt("message")], t_void());
    reg(codebase, "assertTrue",          vec![req("condition"), opt("message")], t_void());
    reg(codebase, "assertFalse",         vec![req("condition"), opt("message")], t_void());
    reg(codebase, "assertNull",          vec![req("actual"), opt("message")], t_void());
    reg(codebase, "assertNotNull",       vec![req("actual"), opt("message")], t_void());
    reg(codebase, "assertCount",         vec![req("expected_count"), req("haystack"), opt("message")], t_void());
    reg(codebase, "assertContains",      vec![req("needle"), req("haystack"), opt("message")], t_void());
    reg(codebase, "assertInstanceOf",    vec![req("expected"), req("actual"), opt("message")], t_void());
    reg(codebase, "assertArrayHasKey",   vec![req("key"), req("array"), opt("message")], t_void());
    reg(codebase, "assertStringContainsString", vec![req("needle"), req("haystack"), opt("message")], t_void());

    // ---- Timing / execution control -------------------------------------------
    reg(codebase, "hrtime",            vec![opt("as_number")], Union::mixed());
    reg(codebase, "set_time_limit",    vec![req("seconds")], t_bool());
    reg(codebase, "connection_status", vec![], t_int());
    reg(codebase, "connection_aborted",vec![], t_int());

    // ---- String (more) ---------------------------------------------------------
    reg(codebase, "strtr",             vec![req("string"), req("from_or_pairs"), opt("to")], t_str());
    reg(codebase, "strpbrk",           vec![req("string"), req("characters")], t_str_or_false());
    reg(codebase, "strtok",            vec![req("string_or_token"), opt("token")], t_str_or_false());
    reg(codebase, "iconv",             vec![req("from_encoding"), req("to_encoding"), req("string")], t_str_or_false());
    reg(codebase, "idn_to_ascii",      vec![req("domain"), opt("flags"), opt("variant"), byref_opt("idna_info")], t_str_or_false());
    reg(codebase, "getallheaders",     vec![], t_array_or_false());
    reg(codebase, "get_headers",       vec![req("url"), opt("associative"), opt("context")], Union::mixed());
    reg(codebase, "http_response_code",vec![opt("response_code")], Union::mixed());
    reg(codebase, "http_get_last_response_headers", vec![], Union::mixed());

    // ---- Multibyte string (mb_) ------------------------------------------------
    reg(codebase, "mb_trim",               vec![req("string"), opt("characters")], t_str());
    reg(codebase, "mb_check_encoding",     vec![opt("value"), opt("encoding")], t_bool());
    reg(codebase, "mb_substitute_character",vec![opt("substitute_character")], Union::mixed());
    reg(codebase, "mb_chr",                vec![req("codepoint"), opt("encoding")], t_str_or_false());
    reg(codebase, "mb_list_encodings",     vec![], t_array());
    reg(codebase, "mb_strcut",             vec![req("string"), req("start"), opt("length"), opt("encoding")], t_str());
    reg(codebase, "mb_ucfirst",            vec![req("string"), opt("encoding")], t_str());

    // ---- Array (more) ----------------------------------------------------------
    reg(codebase, "array_multisort",       vec![byref("array"), vari("rest")], t_bool());
    reg(codebase, "array_udiff",           vec![req("array"), req("arrays"), req("value_compare_func")], t_array());
    reg(codebase, "array_uintersect",      vec![req("array"), req("arrays"), req("value_compare_func")], t_array());
    reg(codebase, "array_change_key_case", vec![req("array"), opt("case")], t_array());
    reg(codebase, "array_merge_recursive", vec![vari("arrays")], t_array());
    reg(codebase, "array_rand",            vec![req("array"), opt("num")], Union::mixed());

    // ---- File info (finfo) -----------------------------------------------------
    reg(codebase, "finfo_open",   vec![opt("flags"), opt("magic_database")], t_obj_or_false("finfo"));
    reg(codebase, "finfo_file",   vec![req("finfo"), req("filename"), opt("flags"), opt("context")], t_str_or_false());
    reg(codebase, "finfo_buffer", vec![req("finfo"), req("string"), opt("flags"), opt("context")], t_str_or_false());
    reg(codebase, "finfo_close",  vec![req("finfo")], t_bool());
    reg(codebase, "fstat",        vec![req("stream")], Union::mixed());
    reg(codebase, "readfile",     vec![req("filename"), opt("use_include_path"), opt("context")], t_int_or_false());

    // ---- SimpleXML -------------------------------------------------------------
    reg(codebase, "simplexml_load_string", vec![req("data"), opt("class_name"), opt("options"), opt("namespace_or_prefix"), opt("is_prefix")], Union::mixed());
    reg(codebase, "simplexml_load_file",   vec![req("filename"), opt("class_name"), opt("options"), opt("namespace_or_prefix"), opt("is_prefix")], Union::mixed());
    reg(codebase, "simplexml_import_dom",  vec![req("node"), opt("class_name")], Union::mixed());

    // ---- LibXML ----------------------------------------------------------------
    reg(codebase, "libxml_use_internal_errors",  vec![opt("use_errors")], t_bool());
    reg(codebase, "libxml_get_errors",           vec![], t_array());
    reg(codebase, "libxml_clear_errors",         vec![], t_void());

    // ---- Syslog ----------------------------------------------------------------
    reg(codebase, "openlog",  vec![req("prefix"), req("flags"), req("facility")], t_bool());
    reg(codebase, "closelog", vec![], t_bool());
    reg(codebase, "syslog",   vec![req("priority"), req("message")], t_bool());

    // ---- Misc (more) -----------------------------------------------------------
    reg(codebase, "uniqid",                 vec![opt("prefix"), opt("more_entropy")], t_str());
    reg(codebase, "timezone_open",          vec![req("timezone_id")], t_obj_or_false("DateTimeZone"));
    reg(codebase, "date_timezone_set",      vec![req("object"), req("timezone")], t_obj_or_false("DateTime"));
    reg(codebase, "define",                 vec![req("constant_name"), req("value"), opt("case_insensitive")], t_bool());
    reg(codebase, "debug_backtrace",        vec![opt("options"), opt("limit")], t_array());
    reg(codebase, "error_get_last",         vec![], Union::mixed());
    reg(codebase, "password_get_info",      vec![req("hash")], t_array());
    reg(codebase, "getopt",                 vec![req("short_options"), opt("long_options"), byref_opt("rest_index")], Union::mixed());
    reg(codebase, "preg_last_error_msg",    vec![], t_str());
    reg(codebase, "stream_context_create",  vec![opt("options"), opt("params")], t_obj_or_false("resource"));
    reg(codebase, "mt_getrandmax",          vec![], t_int());
    reg(codebase, "escapeshellarg",         vec![req("arg")], t_str());
    reg(codebase, "escapeshellcmd",         vec![req("command")], t_str());

    // ---- PCNTL (process control) -----------------------------------------------
    reg(codebase, "pcntl_fork",     vec![], t_int());
    reg(codebase, "pcntl_waitpid",  vec![req("process_id"), byref("status"), opt("flags"), opt("resource_usage")], t_int());
    reg(codebase, "pcntl_wexitstatus", vec![req("status")], t_int());

    // ---- MySQLi procedural (additional) ----------------------------------------
    reg(codebase, "mysqli_connect_error", vec![], Union::mixed());
    reg(codebase, "mysqli_connect_errno", vec![], t_int());
    reg(codebase, "mysqli_init",          vec![], Union::mixed());

    // ---- OpenSSL (additional) --------------------------------------------------
    reg(codebase, "openssl_cipher_iv_length", vec![req("cipher_algo")], t_int_or_false());
    reg(codebase, "openssl_error_string",     vec![], t_str_or_false());

    // ---- bzip2 -----------------------------------------------------------------
    reg(codebase, "bzcompress",   vec![req("data"), opt("block_size"), opt("work_factor")], Union::mixed());
    reg(codebase, "bzdecompress", vec![req("data"), opt("use_less_memory")], Union::mixed());

    // ---- Grapheme (intl) -------------------------------------------------------
    reg(codebase, "grapheme_strpos",  vec![req("haystack"), req("needle"), opt("offset")], Union::mixed());
    reg(codebase, "grapheme_strrpos", vec![req("haystack"), req("needle"), opt("offset")], Union::mixed());

    // ---- curl (additional) -----------------------------------------------------
    reg(codebase, "curl_getinfo",     vec![req("handle"), opt("option")], Union::mixed());
    reg(codebase, "curl_setopt_array",vec![req("handle"), req("options")], t_bool());
    reg(codebase, "curl_multi_init",  vec![], Union::mixed());
    reg(codebase, "curl_multi_add_handle", vec![req("multi_handle"), req("handle")], t_int());
    reg(codebase, "curl_multi_exec",  vec![req("multi_handle"), byref("still_running")], t_int());
    reg(codebase, "curl_multi_getcontent", vec![req("handle")], Union::mixed());
    reg(codebase, "curl_multi_remove_handle", vec![req("multi_handle"), req("handle")], t_int());
    reg(codebase, "curl_multi_close", vec![req("multi_handle")], t_void());
    reg(codebase, "curl_reset",       vec![req("handle")], t_void());
}

// ---------------------------------------------------------------------------
// Class stubs
// ---------------------------------------------------------------------------

fn empty_class(fqcn: &'static str, parent: Option<&'static str>, interfaces: Vec<&'static str>) -> ClassStorage {
    ClassStorage {
        fqcn: Arc::from(fqcn),
        short_name: Arc::from(fqcn),
        parent: parent.map(Arc::from),
        interfaces: interfaces.into_iter().map(Arc::from).collect(),
        traits: vec![],
        own_methods: indexmap::IndexMap::new(),
        own_properties: indexmap::IndexMap::new(),
        own_constants: indexmap::IndexMap::new(),
        template_params: vec![],
        is_abstract: false,
        is_final: false,
        is_readonly: false,
        all_methods: indexmap::IndexMap::new(),
        all_parents: vec![],
        is_deprecated: false,
        is_internal: false,
        location: None,
    }
}

fn empty_interface(fqcn: &'static str, extends: Vec<&'static str>) -> InterfaceStorage {
    InterfaceStorage {
        fqcn: Arc::from(fqcn),
        short_name: Arc::from(fqcn),
        extends: extends.into_iter().map(Arc::from).collect(),
        own_methods: indexmap::IndexMap::new(),
        own_constants: indexmap::IndexMap::new(),
        template_params: vec![],
        all_parents: vec![],
        location: None,
    }
}

/// Simple method stub (no params, returns mixed).
fn stub_method(
    class_fqcn: &'static str,
    name: &'static str,
    params: Vec<FnParam>,
    ret: Union,
    vis: Visibility,
) -> (Arc<str>, MethodStorage) {
    let key: Arc<str> = Arc::from(name);
    (
        key.clone(),
        MethodStorage {
            name: key,
            fqcn: Arc::from(class_fqcn),
            params,
            return_type: Some(ret),
            inferred_return_type: None,
            visibility: vis,
            is_static: false,
            is_abstract: false,
            is_final: false,
            is_constructor: false,
            template_params: vec![],
            assertions: vec![],
            throws: vec![],
            is_deprecated: false,
            is_internal: false,
            is_pure: false,
            location: None,
        },
    )
}

fn load_classes(codebase: &Codebase) {
    use Visibility::Public;

    // ---- stdClass ---- (has __get/__set for dynamic property access)
    let mut std_class = empty_class("stdClass", None, vec![]);
    std_class.own_methods.insert(Arc::from("__get"),
        stub_method("stdClass", "__get", vec![req("name")], Union::mixed(), Visibility::Public).1);
    std_class.own_methods.insert(Arc::from("__set"),
        stub_method("stdClass", "__set", vec![req("name"), req("value")], t_void(), Visibility::Public).1);
    std_class.own_methods.insert(Arc::from("__isset"),
        stub_method("stdClass", "__isset", vec![req("name")], t_bool(), Visibility::Public).1);
    codebase.classes.insert(Arc::from("stdClass"), std_class);

    // ---- Closure ----
    let mut closure = empty_class("Closure", None, vec![]);
    closure.own_methods.insert(
        Arc::from("__invoke"),
        stub_method("Closure", "__invoke", vec![vari("args")], Union::mixed(), Public).1,
    );
    closure.own_methods.insert(
        Arc::from("bind"),
        stub_method("Closure", "bind", vec![req("closure"), req("newThis"), opt("newScope")], t_obj_or_false("Closure"), Public).1,
    );
    closure.own_methods.insert(
        Arc::from("bindTo"),
        stub_method("Closure", "bindTo", vec![req("newThis"), opt("newScope")], t_obj_or_false("Closure"), Public).1,
    );
    closure.own_methods.insert(
        Arc::from("call"),
        stub_method("Closure", "call", vec![req("newThis"), vari("args")], Union::mixed(), Public).1,
    );
    codebase.classes.insert(Arc::from("Closure"), closure);

    // ---- Exception hierarchy ----
    let exc_methods: &[(&'static str, Vec<FnParam>, Union)] = &[
        ("__construct",     vec![opt("message"), opt("code"), opt("previous")], t_void()),
        ("getMessage",      vec![], t_str()),
        ("getCode",         vec![], Union::mixed()),
        ("getFile",         vec![], t_str()),
        ("getLine",         vec![], t_int()),
        ("getTrace",        vec![], t_array()),
        ("getTraceAsString",vec![], t_str()),
        ("getPrevious",     vec![], Union::mixed()),
        ("__toString",      vec![], t_str()),
    ];

    for class_name in &[
        "Exception",
        "RuntimeException",
        "LogicException",
        "InvalidArgumentException",
        "BadMethodCallException",
        "BadFunctionCallException",
        "OverflowException",
        "UnderflowException",
        "OutOfRangeException",
        "OutOfBoundsException",
        "RangeException",
        "LengthException",
        "DomainException",
        "UnexpectedValueException",
        "Error",
        "TypeError",
        "ValueError",
        "ArithmeticError",
        "DivisionByZeroError",
        "ParseError",
        // PHP built-in exceptions not in the SPL hierarchy above
        "JsonException",
        "mysqli_sql_exception",
        "PDOException",
    ] {
        let parent = match *class_name {
            "Exception" | "Error" => None,
            "RuntimeException" | "LogicException" => Some("Exception"),
            "TypeError" | "ValueError" | "ArithmeticError" | "ParseError" => Some("Error"),
            "DivisionByZeroError" => Some("ArithmeticError"),
            "InvalidArgumentException" | "OutOfRangeException" | "LengthException"
            | "DomainException" | "BadFunctionCallException" => Some("LogicException"),
            "BadMethodCallException" => Some("BadFunctionCallException"),
            "OverflowException" | "UnderflowException" | "OutOfBoundsException"
            | "UnexpectedValueException" | "JsonException" | "PDOException"
            | "mysqli_sql_exception" => Some("RuntimeException"),
            _ => Some("Exception"),
        };
        let mut cls = empty_class(class_name, parent, vec!["Throwable"]);
        for (method_name, params, ret) in exc_methods {
            let is_ctor = *method_name == "__construct";
            let mut m = stub_method(class_name, method_name, params.clone(), ret.clone(), Public).1;
            m.is_constructor = is_ctor;
            cls.own_methods.insert(Arc::from(*method_name), m);
        }
        codebase.classes.insert(Arc::from(*class_name), cls);
    }

    // ---- Generator ----
    let mut gen = empty_class("Generator", None, vec!["Iterator"]);
    for (name, params, ret) in &[
        ("current",  vec![], Union::mixed()),
        ("key",      vec![], Union::mixed()),
        ("next",     vec![], t_void()),
        ("rewind",   vec![], t_void()),
        ("valid",    vec![], t_bool()),
        ("send",     vec![req("value")], Union::mixed()),
        ("throw",    vec![req("exception")], Union::mixed()),
        ("getReturn",vec![], Union::mixed()),
    ] {
        gen.own_methods.insert(
            Arc::from(*name),
            stub_method("Generator", name, params.clone(), ret.clone(), Public).1,
        );
    }
    codebase.classes.insert(Arc::from("Generator"), gen);

    // ---- DateTime / DateTimeImmutable ----
    for class_name in &["DateTime", "DateTimeImmutable"] {
        let mut dt = empty_class(class_name, None, vec!["DateTimeInterface"]);
        for (name, params, ret) in &[
            ("__construct",  vec![opt("datetime"), opt("timezone")], t_void()),
            ("format",       vec![req("format")], t_str()),
            ("getTimestamp", vec![], t_int()),
            ("modify",       vec![req("modifier")], Union::mixed()),
            ("setDate",      vec![req("year"), req("month"), req("day")], Union::mixed()),
            ("setTime",      vec![req("hour"), req("minute"), opt("second"), opt("microsecond")], Union::mixed()),
            ("diff",         vec![req("targetObject"), opt("absolute")], t_obj("DateInterval")),
            ("add",                vec![req("interval")], Union::mixed()),
            ("sub",                vec![req("interval")], Union::mixed()),
            ("createFromFormat",   vec![req("format"), req("datetime"), opt("timezone")], Union::mixed()),
            ("createFromMutable",  vec![req("object")], Union::mixed()),
            ("createFromImmutable",vec![req("object")], Union::mixed()),
            ("setTimezone",        vec![req("timezone")], Union::mixed()),
            ("setTimestamp",       vec![req("timestamp")], Union::mixed()),
            ("getTimezone",        vec![], Union::mixed()),
        ] {
            let is_ctor = *name == "__construct";
            let mut m = stub_method(class_name, name, params.clone(), ret.clone(), Public).1;
            m.is_constructor = is_ctor;
            dt.own_methods.insert(Arc::from(*name), m);
        }
        codebase.classes.insert(Arc::from(*class_name), dt);
    }

    // ---- SplDoublyLinkedList / SplStack / SplQueue ----
    {
        use Visibility::Public;
        let mut dll = empty_class("SplDoublyLinkedList", None, vec!["Iterator", "Countable", "ArrayAccess"]);
        for (name, params, ret) in &[
            ("push",     vec![req("value")], t_void()),
            ("pop",      vec![], Union::mixed()),
            ("shift",    vec![], Union::mixed()),
            ("unshift",  vec![req("value")], t_void()),
            ("top",      vec![], Union::mixed()),
            ("bottom",   vec![], Union::mixed()),
            ("isEmpty",  vec![], t_bool()),
            ("count",    vec![], t_int()),
            ("valid",    vec![], t_bool()),
            ("current",  vec![], Union::mixed()),
            ("next",     vec![], t_void()),
            ("prev",     vec![], t_void()),
            ("rewind",   vec![], t_void()),
            ("key",      vec![], Union::mixed()),
        ] {
            dll.own_methods.insert(Arc::from(*name),
                stub_method("SplDoublyLinkedList", name, params.clone(), ret.clone(), Public).1);
        }
        codebase.classes.insert(Arc::from("SplDoublyLinkedList"), dll);

        let mut stack = empty_class("SplStack", Some("SplDoublyLinkedList"), vec![]);
        for (name, params, ret) in &[
            ("push",    vec![req("value")], t_void()),
            ("pop",     vec![], Union::mixed()),
            ("top",     vec![], Union::mixed()),
            ("isEmpty", vec![], t_bool()),
            ("count",   vec![], t_int()),
        ] {
            stack.own_methods.insert(Arc::from(*name),
                stub_method("SplStack", name, params.clone(), ret.clone(), Public).1);
        }
        codebase.classes.insert(Arc::from("SplStack"), stack);

        let mut queue = empty_class("SplQueue", Some("SplDoublyLinkedList"), vec![]);
        for (name, params, ret) in &[
            ("enqueue", vec![req("value")], t_void()),
            ("dequeue", vec![], Union::mixed()),
            ("isEmpty", vec![], t_bool()),
            ("count",   vec![], t_int()),
        ] {
            queue.own_methods.insert(Arc::from(*name),
                stub_method("SplQueue", name, params.clone(), ret.clone(), Public).1);
        }
        codebase.classes.insert(Arc::from("SplQueue"), queue);
    }


    // ---- ZipArchive ----
    {
        use Visibility::Public;
        let mut zip = empty_class("ZipArchive", None, vec![]);
        for (name, params, ret) in &[
            ("open",        vec![req("filename"), opt("flags")], Union::mixed()),
            ("close",       vec![], t_bool()),
            ("addFile",     vec![req("filename"), opt("localname")], t_bool()),
            ("addFromString",vec![req("localname"), req("contents")], t_bool()),
            ("addEmptyDir", vec![req("dirname")], t_bool()),
            ("extractTo",   vec![req("destination"), opt("files")], t_bool()),
            ("getNameIndex",vec![req("index")], Union::mixed()),
            ("getStream",   vec![req("name")], Union::mixed()),
            ("statIndex",   vec![req("index"), opt("flags")], Union::mixed()),
            ("locateName",  vec![req("name"), opt("flags")], Union::mixed()),
            ("renameName",  vec![req("from"), req("to")], t_bool()),
            ("deleteName",  vec![req("name")], t_bool()),
            ("count",       vec![], t_int()),
        ] {
            zip.own_methods.insert(Arc::from(*name),
                stub_method("ZipArchive", name, params.clone(), ret.clone(), Public).1);
        }
        for (name, ty) in &[
            ("numFiles", t_int()), ("filename", t_str()), ("comment", t_str()), ("status", t_int()),
        ] {
            zip.own_properties.insert(Arc::from(*name), stub_prop(name, ty.clone()));
        }
        codebase.classes.insert(Arc::from("ZipArchive"), zip);
    }

    // ---- ReflectionClass ----
    {
        use Visibility::Public;
        let mut rc = empty_class("ReflectionClass", None, vec![]);
        for (name, params, ret) in &[
            ("__construct",        vec![req("argument")], t_void()),
            ("getName",            vec![], t_str()),
            ("getMethod",          vec![req("name")], t_obj("ReflectionMethod")),
            ("getMethods",         vec![opt("filter")], t_array()),
            ("getProperties",      vec![opt("filter")], t_array()),
            ("getProperty",        vec![req("name")], t_obj("ReflectionProperty")),
            ("hasMethod",          vec![req("name")], t_bool()),
            ("hasProperty",        vec![req("name")], t_bool()),
            ("newInstanceArgs",    vec![opt("args")], t_obj("object")),
            ("newInstanceWithoutConstructor", vec![], t_obj("object")),
            ("isAbstract",         vec![], t_bool()),
            ("isFinal",            vec![], t_bool()),
            ("isInterface",        vec![], t_bool()),
            ("isInstantiable",     vec![], t_bool()),
            ("implementsInterface",vec![req("interface")], t_bool()),
            ("isSubclassOf",       vec![req("class")], t_bool()),
            ("getInterfaces",      vec![], t_array()),
            ("getParentClass",     vec![], Union::mixed()),
            ("getAttributes",      vec![opt("name"), opt("flags")], t_array()),
            ("getConstants",       vec![], t_array()),
            ("getConstant",        vec![req("name")], Union::mixed()),
            ("getConstructor",     vec![], Union::mixed()),
            ("getFileName",        vec![], Union::mixed()),
            ("isAnonymous",        vec![], t_bool()),
            ("isReadOnly",         vec![], t_bool()),
            ("isEnum",             vec![], t_bool()),
        ] {
            rc.own_methods.insert(Arc::from(*name),
                stub_method("ReflectionClass", name, params.clone(), ret.clone(), Public).1);
        }
        rc.own_properties.insert(Arc::from("name"), stub_prop("name", t_str()));
        codebase.classes.insert(Arc::from("ReflectionClass"), rc);
        let mut rm = empty_class("ReflectionMethod", None, vec![]);
        for (name, params, ret) in &[
            ("getName",            vec![], t_str()),
            ("getParameters",      vec![], t_array()),
            ("getReturnType",      vec![], Union::mixed()),
            ("getDeclaringClass",  vec![], t_obj("ReflectionClass")),
            ("invoke",             vec![req("object")], Union::mixed()),
            ("isPublic",           vec![], t_bool()),
            ("isProtected",        vec![], t_bool()),
            ("isPrivate",          vec![], t_bool()),
            ("isStatic",           vec![], t_bool()),
            ("isAbstract",         vec![], t_bool()),
            ("isFinal",            vec![], t_bool()),
            ("isConstructor",      vec![], t_bool()),
            ("getAttributes",      vec![opt("name"), opt("flags")], t_array()),
            ("setAccessible",      vec![req("accessible")], t_void()),
        ] {
            rm.own_methods.insert(Arc::from(*name),
                stub_method("ReflectionMethod", name, params.clone(), ret.clone(), Public).1);
        }
        codebase.classes.insert(Arc::from("ReflectionMethod"), rm);
        let mut rp = empty_class("ReflectionProperty", None, vec![]);
        for (name, params, ret) in &[
            ("getValue",       vec![opt("object")], Union::mixed()),
            ("setValue",       vec![req("objectOrValue"), opt("value")], t_void()),
            ("getType",        vec![], Union::mixed()),
            ("getReturnType",  vec![], Union::mixed()),
            ("getDeclaringClass", vec![], t_obj("ReflectionClass")),
            ("setAccessible",  vec![req("accessible")], t_void()),
            ("getName",        vec![], t_str()),
            ("isPublic",       vec![], t_bool()),
            ("isStatic",       vec![], t_bool()),
        ] {
            rp.own_methods.insert(Arc::from(*name),
                stub_method("ReflectionProperty", name, params.clone(), ret.clone(), Public).1);
        }
        codebase.classes.insert(Arc::from("ReflectionProperty"), rp);
        let re = empty_class("ReflectionEnum", None, vec![]);
        codebase.classes.insert(Arc::from("ReflectionEnum"), re);
        let mut rf = empty_class("ReflectionFunction", None, vec![]);
        for (name, params, ret) in &[
            ("__construct",   vec![req("function")], t_void()),
            ("getName",       vec![], t_str()),
            ("getParameters", vec![], t_array()),
            ("getReturnType", vec![], Union::mixed()),
            ("getFileName",   vec![], Union::mixed()),
            ("invoke",        vec![opt("args")], Union::mixed()),
        ] {
            rf.own_methods.insert(Arc::from(*name),
                stub_method("ReflectionFunction", name, params.clone(), ret.clone(), Public).1);
        }
        codebase.classes.insert(Arc::from("ReflectionFunction"), rf);
        let mut rp2 = empty_class("ReflectionParameter", None, vec![]);
        for (name, params, ret) in &[
            ("getName",               vec![], t_str()),
            ("getType",               vec![], Union::mixed()),
            ("getDefaultValue",       vec![], Union::mixed()),
            ("isDefaultValueAvailable", vec![], t_bool()),
            ("isOptional",            vec![], t_bool()),
            ("isVariadic",            vec![], t_bool()),
            ("hasDefaultValue",       vec![], t_bool()),
            ("getAttributes",         vec![opt("name"), opt("flags")], t_array()),
            ("getPosition",           vec![], t_int()),
        ] {
            rp2.own_methods.insert(Arc::from(*name),
                stub_method("ReflectionParameter", name, params.clone(), ret.clone(), Public).1);
        }
        codebase.classes.insert(Arc::from("ReflectionParameter"), rp2);
        let mut rnt = empty_class("ReflectionNamedType", None, vec![]);
        for (name, params, ret) in &[
            ("getName",       vec![], t_str()),
            ("allowsNull",    vec![], t_bool()),
            ("isBuiltin",     vec![], t_bool()),
            ("__toString",    vec![], t_str()),
        ] {
            rnt.own_methods.insert(Arc::from(*name),
                stub_method("ReflectionNamedType", name, params.clone(), ret.clone(), Public).1);
        }
        codebase.classes.insert(Arc::from("ReflectionNamedType"), rnt);
        let mut rut = empty_class("ReflectionUnionType", None, vec![]);
        for (name, params, ret) in &[
            ("getTypes",      vec![], t_array()),
            ("allowsNull",    vec![], t_bool()),
        ] {
            rut.own_methods.insert(Arc::from(*name),
                stub_method("ReflectionUnionType", name, params.clone(), ret.clone(), Public).1);
        }
        codebase.classes.insert(Arc::from("ReflectionUnionType"), rut);
    }

    // ---- DOM extension ----
    {
        // (class_name, parent) — PHP DOM inheritance hierarchy
        for (class_name, dom_parent) in &[
            ("DOMNode",                None),
            ("DOMDocument",            Some("DOMNode")),
            ("DOMElement",             Some("DOMNode")),
            ("DOMAttr",                Some("DOMNode")),
            ("DOMCharacterData",       Some("DOMNode")),
            ("DOMText",                Some("DOMCharacterData")),
            ("DOMComment",             Some("DOMCharacterData")),
            ("DOMCdataSection",        Some("DOMText")),
            ("DOMDocumentFragment",    Some("DOMNode")),
            ("DOMDocumentType",        Some("DOMNode")),
            ("DOMProcessingInstruction", Some("DOMNode")),
            ("DOMEntityReference",     Some("DOMNode")),
            ("DOMEntity",              Some("DOMNode")),
            ("DOMNotation",            Some("DOMNode")),
            ("DOMNodeList",            None),
            ("DOMNamedNodeMap",        None),
            ("DOMXPath",               None),
            ("DOMImplementation",      None),
        ] {
            let class_name: &str = *class_name;
            let mut cls = empty_class(class_name, *dom_parent, vec![]);
            // Common DOMNode methods
            for (name, params, ret) in &[
                ("appendChild",     vec![req("node")], t_obj("DOMNode")),
                ("cloneNode",       vec![opt("deep")], t_obj("DOMNode")),
                ("getAttribute",    vec![req("qualifiedName")], t_str()),
                ("setAttribute",    vec![req("qualifiedName"), req("value")], Union::mixed()),
                ("hasAttribute",    vec![req("qualifiedName")], t_bool()),
                ("hasAttributes",   vec![], t_bool()),
                ("hasChildNodes",   vec![], t_bool()),
                ("getNodePath",     vec![], Union::mixed()),
                ("removeAttribute", vec![req("qualifiedName")], t_bool()),
                ("removeChild",     vec![req("child")], t_obj("DOMNode")),
                ("getElementsByTagName", vec![req("qualifiedName")], t_obj("DOMNodeList")),
                ("createElement",   vec![req("localName"), opt("value")], t_obj("DOMElement")),
                ("createTextNode",  vec![req("data")], t_obj("DOMText")),
                ("getElementById",  vec![req("elementId")], Union::mixed()),
                ("loadXML",         vec![req("source"), opt("options")], Union::mixed()),
                ("loadHTML",        vec![req("source"), opt("options")], Union::mixed()),
                ("saveHTML",        vec![opt("node")], t_str_or_false()),
                ("saveXML",         vec![opt("node"), opt("options")], t_str_or_false()),
                ("item",            vec![req("index")], Union::mixed()),
                ("getNamedItem",    vec![req("qualifiedName")], Union::mixed()),
                ("evaluate",        vec![req("expression"), opt("contextNode"), opt("registerNodeNS")], Union::mixed()),
                ("query",           vec![req("expression"), opt("contextNode"), opt("registerNodeNS")], Union::mixed()),
                ("registerNamespace", vec![req("prefix"), req("namespaceURI")], t_bool()),
                ("load",            vec![req("filename"), opt("options")], Union::mixed()),
                ("save",            vec![req("filename"), opt("options")], Union::mixed()),
                ("importNode",      vec![req("node"), opt("deep")], t_obj("DOMNode")),
                ("createFragment",  vec![], t_obj("DOMDocumentFragment")),
                ("createComment",   vec![req("data")], t_obj("DOMComment")),
                ("createElementNS", vec![req("namespace"), req("qualifiedName"), opt("value")], t_obj("DOMElement")),
                ("normalize",       vec![], t_void()),
                ("replaceChild",    vec![req("node"), req("child")], t_obj("DOMNode")),
                ("insertBefore",    vec![req("node"), opt("child")], t_obj("DOMNode")),
            ] {
                cls.own_methods.insert(
                    Arc::from(*name),
                    stub_method(class_name, name, params.clone(), ret.clone(), Public).1,
                );
            }
            // Common properties
            cls.own_properties.insert(Arc::from("nodeValue"), stub_prop("nodeValue", Union::mixed()));
            cls.own_properties.insert(Arc::from("nodeType"), stub_prop("nodeType", t_int()));
            cls.own_properties.insert(Arc::from("nodeName"), stub_prop("nodeName", t_str()));
            cls.own_properties.insert(Arc::from("childNodes"), stub_prop("childNodes", t_obj("DOMNodeList")));
            cls.own_properties.insert(Arc::from("parentNode"), stub_prop("parentNode", Union::mixed()));
            cls.own_properties.insert(Arc::from("firstChild"), stub_prop("firstChild", Union::mixed()));
            cls.own_properties.insert(Arc::from("lastChild"), stub_prop("lastChild", Union::mixed()));
            cls.own_properties.insert(Arc::from("nextSibling"), stub_prop("nextSibling", Union::mixed()));
            cls.own_properties.insert(Arc::from("previousSibling"), stub_prop("previousSibling", Union::mixed()));
            cls.own_properties.insert(Arc::from("ownerDocument"), stub_prop("ownerDocument", Union::mixed()));
            cls.own_properties.insert(Arc::from("tagName"), stub_prop("tagName", t_str()));
            cls.own_properties.insert(Arc::from("localName"), stub_prop("localName", t_str()));
            cls.own_properties.insert(Arc::from("namespaceURI"), stub_prop("namespaceURI", Union::mixed()));
            cls.own_properties.insert(Arc::from("prefix"), stub_prop("prefix", Union::mixed()));
            cls.own_properties.insert(Arc::from("data"), stub_prop("data", t_str()));
            cls.own_properties.insert(Arc::from("value"), stub_prop("value", t_str()));
            cls.own_properties.insert(Arc::from("textContent"), stub_prop("textContent", t_str()));
            cls.own_properties.insert(Arc::from("length"), stub_prop("length", t_int()));
            cls.own_properties.insert(Arc::from("attributes"), stub_prop("attributes", t_obj("DOMNamedNodeMap")));
            cls.own_properties.insert(Arc::from("documentElement"), stub_prop("documentElement", Union::mixed()));
            // Add $name property specifically for DOMAttr (attribute name)
            if class_name == "DOMAttr" {
                cls.own_properties.insert(Arc::from("name"), stub_prop("name", t_str()));
                cls.own_properties.insert(Arc::from("specified"), stub_prop("specified", t_bool()));
                cls.own_properties.insert(Arc::from("ownerElement"), stub_prop("ownerElement", Union::mixed()));
            }
            codebase.classes.insert(Arc::from(class_name), cls);
        }
    }

    // ---- SimpleXML ----
    {
        let mut sxe = empty_class("SimpleXMLElement", None, vec![]);
        for (name, params, ret) in &[
            ("attributes",  vec![opt("namespace"), opt("is_prefix")], Union::mixed()),
            ("children",    vec![opt("namespace"), opt("is_prefix")], Union::mixed()),
            ("xpath",       vec![req("expression")], Union::mixed()),
            ("getName",     vec![], t_str()),
            ("addChild",    vec![req("qualifiedName"), opt("value"), opt("namespace")], Union::mixed()),
            ("addAttribute",vec![req("qualifiedName"), req("value"), opt("namespace")], t_void()),
            ("count",       vec![], t_int()),
            ("asXML",       vec![opt("filename")], Union::mixed()),
        ] {
            sxe.own_methods.insert(Arc::from(*name),
                stub_method("SimpleXMLElement", name, params.clone(), ret.clone(), Public).1);
        }
        codebase.classes.insert(Arc::from("SimpleXMLElement"), sxe);
        let sxe_it = empty_class("SimpleXMLIterator", Some("SimpleXMLElement"), vec![]);
        codebase.classes.insert(Arc::from("SimpleXMLIterator"), sxe_it);
    }

    // ---- Fiber (PHP 8.1) ----
    {
        let mut fiber = empty_class("Fiber", None, vec![]);
        for (name, params, ret) in &[
            ("start",    vec![vari("args")], Union::mixed()),
            ("resume",   vec![opt("value")], Union::mixed()),
            ("getReturn",vec![], Union::mixed()),
            ("isStarted",vec![], t_bool()),
            ("isSuspended",vec![], t_bool()),
            ("isRunning",vec![], t_bool()),
            ("isTerminated",vec![], t_bool()),
            ("getCurrent",vec![], Union::mixed()),
            ("suspend",  vec![opt("value")], Union::mixed()),
        ] {
            fiber.own_methods.insert(Arc::from(*name),
                stub_method("Fiber", name, params.clone(), ret.clone(), Public).1);
        }
        codebase.classes.insert(Arc::from("Fiber"), fiber);
        let fiber_error = empty_class("FiberError", Some("Error"), vec![]);
        codebase.classes.insert(Arc::from("FiberError"), fiber_error);
    }

    // ---- WeakReference / WeakMap ----
    {
        let mut wr = empty_class("WeakReference", None, vec![]);
        wr.own_methods.insert(Arc::from("get"),
            stub_method("WeakReference", "get", vec![], Union::mixed(), Public).1);
        wr.own_methods.insert(Arc::from("create"),
            stub_method("WeakReference", "create", vec![req("object")], t_obj("WeakReference"), Public).1);
        codebase.classes.insert(Arc::from("WeakReference"), wr);
        let wm = empty_class("WeakMap", None, vec![]);
        codebase.classes.insert(Arc::from("WeakMap"), wm);
    }

    // ---- SPL Iterators ----
    {
        use Visibility::Public;
        // SplFileInfo
        let mut sfi = empty_class("SplFileInfo", None, vec![]);
        for (name, params, ret) in &[
            ("getFilename",    vec![], t_str()),
            ("getBasename",    vec![opt("suffix")], t_str()),
            ("getPathname",    vec![], t_str()),
            ("getRealPath",    vec![], Union::mixed()),
            ("getExtension",   vec![], t_str()),
            ("getPath",        vec![], t_str()),
            ("isDir",          vec![], t_bool()),
            ("isFile",         vec![], t_bool()),
            ("isLink",         vec![], t_bool()),
            ("isReadable",     vec![], t_bool()),
            ("isWritable",     vec![], t_bool()),
            ("getSize",        vec![], t_int()),
            ("getMTime",       vec![], t_int()),
            ("openFile",       vec![opt("mode"), opt("useIncludePath"), opt("context")], t_obj("SplFileObject")),
            ("getType",        vec![], t_str()),
            ("getOwner",       vec![], t_int()),
            ("getGroup",       vec![], t_int()),
            ("getPerms",       vec![], t_int()),
            ("getInode",       vec![], t_int()),
        ] {
            sfi.own_methods.insert(Arc::from(*name),
                stub_method("SplFileInfo", name, params.clone(), ret.clone(), Public).1);
        }
        codebase.classes.insert(Arc::from("SplFileInfo"), sfi);
        let sfo = empty_class("SplFileObject", Some("SplFileInfo"), vec![]);
        codebase.classes.insert(Arc::from("SplFileObject"), sfo);

        // ArrayObject
        let ao = empty_class("ArrayObject", None, vec!["IteratorAggregate", "ArrayAccess", "Countable", "Serializable"]);
        codebase.classes.insert(Arc::from("ArrayObject"), ao);

        // ArrayIterator
        let mut ai = empty_class("ArrayIterator", None, vec!["Iterator", "ArrayAccess", "Countable", "Serializable"]);
        for (name, params, ret) in &[
            ("__construct",  vec![opt("array"), opt("flags")], t_void()),
            ("current",      vec![], Union::mixed()),
            ("key",          vec![], Union::mixed()),
            ("next",         vec![], t_void()),
            ("rewind",       vec![], t_void()),
            ("valid",        vec![], t_bool()),
            ("count",        vec![], t_int()),
            ("append",       vec![req("value")], t_void()),
            ("offsetGet",    vec![req("key")], Union::mixed()),
            ("offsetExists", vec![req("key")], t_bool()),
            ("offsetSet",    vec![req("key"), req("value")], t_void()),
            ("offsetUnset",  vec![req("key")], t_void()),
            ("getArrayCopy", vec![], t_array()),
        ] {
            ai.own_methods.insert(Arc::from(*name),
                stub_method("ArrayIterator", name, params.clone(), ret.clone(), Public).1);
        }
        codebase.classes.insert(Arc::from("ArrayIterator"), ai);

        // RecursiveArrayIterator
        let rai = empty_class("RecursiveArrayIterator", Some("ArrayIterator"), vec!["RecursiveIterator"]);
        codebase.classes.insert(Arc::from("RecursiveArrayIterator"), rai);

        // DirectoryIterator
        let di = empty_class("DirectoryIterator", Some("SplFileInfo"), vec!["Iterator"]);
        codebase.classes.insert(Arc::from("DirectoryIterator"), di);

        // FilesystemIterator
        let fsi = empty_class("FilesystemIterator", Some("DirectoryIterator"), vec![]);
        codebase.classes.insert(Arc::from("FilesystemIterator"), fsi);

        // RecursiveDirectoryIterator
        let rdi = empty_class("RecursiveDirectoryIterator", Some("FilesystemIterator"), vec!["RecursiveIterator"]);
        codebase.classes.insert(Arc::from("RecursiveDirectoryIterator"), rdi);

        // RecursiveIteratorIterator
        let mut rii = empty_class("RecursiveIteratorIterator", None, vec!["OuterIterator"]);
        for (name, params, ret) in &[
            ("__construct",      vec![req("iterator"), opt("mode"), opt("flags")], t_void()),
            ("current",          vec![], Union::mixed()),
            ("key",              vec![], Union::mixed()),
            ("next",             vec![], t_void()),
            ("rewind",           vec![], t_void()),
            ("valid",            vec![], t_bool()),
            ("getDepth",         vec![], t_int()),
            ("getSubIterator",   vec![opt("level")], Union::mixed()),
            ("getInnerIterator", vec![], Union::mixed()),
            ("beginIteration",   vec![], t_void()),
            ("endIteration",     vec![], t_void()),
            ("callHasChildren",  vec![], t_bool()),
            ("callGetChildren",  vec![], Union::mixed()),
            ("getMaxDepth",      vec![], Union::mixed()),
            ("setMaxDepth",      vec![opt("maxDepth")], t_void()),
        ] {
            rii.own_methods.insert(Arc::from(*name),
                stub_method("RecursiveIteratorIterator", name, params.clone(), ret.clone(), Public).1);
        }
        codebase.classes.insert(Arc::from("RecursiveIteratorIterator"), rii);

        // SplMinHeap / SplMaxHeap / SplPriorityQueue (SplDoublyLinkedList/SplStack/SplQueue defined above)
        for cls in &["SplMinHeap", "SplMaxHeap"] {
            let c = empty_class(cls, None, vec!["Iterator", "Countable"]);
            codebase.classes.insert(Arc::from(*cls), c);
        }
        let spq = empty_class("SplPriorityQueue", None, vec!["Iterator", "Countable"]);
        codebase.classes.insert(Arc::from("SplPriorityQueue"), spq);
        let sfs = empty_class("SplFixedArray", None, vec!["Iterator", "ArrayAccess", "Countable"]);
        codebase.classes.insert(Arc::from("SplFixedArray"), sfs);
    }

    // ---- DateTimeZone, DateInterval ----
    {
        use Visibility::Public;
        let mut dtz = empty_class("DateTimeZone", None, vec![]);
        for (name, params, ret) in &[
            ("__construct", vec![req("timezone")], t_void()),
            ("getName",     vec![], t_str()),
            ("getOffset",   vec![req("datetime")], t_int()),
            ("getTransitions", vec![opt("timestampBegin"), opt("timestampEnd")], Union::mixed()),
            ("listAbbreviations", vec![], t_array()),
            ("listIdentifiers",   vec![opt("timezoneGroup"), opt("countryCode")], t_array()),
        ] {
            dtz.own_methods.insert(Arc::from(*name),
                stub_method("DateTimeZone", name, params.clone(), ret.clone(), Public).1);
        }
        codebase.classes.insert(Arc::from("DateTimeZone"), dtz);
        let mut di_cls = empty_class("DateInterval", None, vec![]);
        for (name, params, ret) in &[
            ("__construct",    vec![req("duration")], t_void()),
            ("createFromDateString", vec![req("datetime")], Union::mixed()),
            ("format",         vec![req("format")], t_str()),
        ] {
            di_cls.own_methods.insert(Arc::from(*name),
                stub_method("DateInterval", name, params.clone(), ret.clone(), Public).1);
        }
        for prop in &["y", "m", "d", "h", "i", "s", "invert", "days"] {
            di_cls.own_properties.insert(Arc::from(*prop), stub_prop(prop, t_int()));
        }
        codebase.classes.insert(Arc::from("DateInterval"), di_cls);
        let dp = empty_class("DatePeriod", None, vec!["IteratorAggregate"]);
        codebase.classes.insert(Arc::from("DatePeriod"), dp);
    }

    // ---- MySQLi ----
    {
        use Visibility::Public;
        let mut my = empty_class("mysqli", None, vec![]);
        for (name, params, ret) in &[
            ("__construct",   vec![opt("host"), opt("user"), opt("password"), opt("database"), opt("port"), opt("socket")], t_void()),
            ("connect",       vec![opt("host"), opt("user"), opt("password"), opt("database")], t_bool()),
            ("query",         vec![req("query"), opt("result_mode")], Union::mixed()),
            ("prepare",       vec![req("query")], Union::mixed()),
            ("multi_query",   vec![req("query")], t_bool()),
            ("close",         vec![], t_bool()),
            ("real_escape_string", vec![req("string")], t_str()),
            ("error",         vec![], t_str()),
            ("errno",         vec![], t_int()),
            ("select_db",     vec![req("database")], t_bool()),
            ("begin_transaction", vec![], t_bool()),
            ("commit",        vec![], t_bool()),
            ("rollback",      vec![], t_bool()),
            ("set_charset",   vec![req("charset")], t_bool()),
            ("next_result",   vec![], t_bool()),
        ] {
            my.own_methods.insert(Arc::from(*name),
                stub_method("mysqli", name, params.clone(), ret.clone(), Public).1);
        }
        for prop in &["error", "errno", "insert_id", "affected_rows", "num_rows"] {
            my.own_properties.insert(Arc::from(*prop), stub_prop(prop, Union::mixed()));
        }
        codebase.classes.insert(Arc::from("mysqli"), my);
        let mut mys = empty_class("mysqli_stmt", None, vec![]);
        for (name, params, ret) in &[
            ("bind_param",    vec![req("types"), vari("vars")], t_bool()),
            ("bind_result",   vec![vari("vars")], t_bool()),
            ("execute",       vec![opt("params")], t_bool()),
            ("fetch",         vec![], Union::mixed()),
            ("close",         vec![], t_bool()),
            ("get_result",    vec![], Union::mixed()),
            ("error",         vec![], t_str()),
        ] {
            mys.own_methods.insert(Arc::from(*name),
                stub_method("mysqli_stmt", name, params.clone(), ret.clone(), Public).1);
        }
        for prop in &["errno", "error", "insert_id", "affected_rows", "num_rows"] {
            mys.own_properties.insert(Arc::from(*prop), stub_prop(prop, Union::mixed()));
        }
        codebase.classes.insert(Arc::from("mysqli_stmt"), mys);
        let myr = empty_class("mysqli_result", None, vec![]);
        codebase.classes.insert(Arc::from("mysqli_result"), myr);
    }

    // ---- PDO ----
    {
        let mut pdo = empty_class("PDO", None, vec![]);
        for (name, params, ret) in &[
            ("__construct",    vec![req("dsn"), opt("username"), opt("password"), opt("options")], t_void()),
            ("prepare",        vec![req("query"), opt("options")], Union::mixed()),
            ("query",          vec![req("query"), opt("fetchMode")], Union::mixed()),
            ("exec",           vec![req("statement")], Union::mixed()),
            ("quote",          vec![req("string"), opt("type")], Union::mixed()),
            ("lastInsertId",   vec![opt("name")], Union::mixed()),
            ("beginTransaction", vec![], t_bool()),
            ("commit",         vec![], t_bool()),
            ("rollBack",       vec![], t_bool()),
            ("inTransaction",  vec![], t_bool()),
            ("setAttribute",   vec![req("attribute"), req("value")], t_bool()),
            ("getAttribute",   vec![req("attribute")], Union::mixed()),
            ("errorCode",      vec![], Union::mixed()),
            ("errorInfo",      vec![], t_array()),
        ] {
            pdo.own_methods.insert(Arc::from(*name),
                stub_method("PDO", name, params.clone(), ret.clone(), Public).1);
        }
        codebase.classes.insert(Arc::from("PDO"), pdo);

        let mut pdo_stmt = empty_class("PDOStatement", None, vec!["Traversable"]);
        for (name, params, ret) in &[
            ("execute",        vec![opt("params")], Union::mixed()),
            ("fetch",          vec![opt("mode"), opt("cursorOrientation"), opt("cursorOffset")], Union::mixed()),
            ("fetchAll",       vec![opt("mode"), opt("fetchArgument"), opt("constructorArgs")], t_array()),
            ("fetchColumn",    vec![opt("column")], Union::mixed()),
            ("fetchObject",    vec![opt("class"), opt("constructorArgs")], Union::mixed()),
            ("rowCount",       vec![], t_int()),
            ("columnCount",    vec![], t_int()),
            ("bindParam",      vec![req("param"), req("var"), opt("type"), opt("maxLength"), opt("driverOptions")], t_bool()),
            ("bindValue",      vec![req("param"), req("value"), opt("type")], t_bool()),
            ("bindColumn",     vec![req("column"), req("var"), opt("type"), opt("maxLength"), opt("driverOptions")], t_bool()),
            ("closeCursor",    vec![], t_bool()),
            ("errorCode",      vec![], Union::mixed()),
            ("errorInfo",      vec![], t_array()),
            ("getColumnMeta",  vec![req("column")], Union::mixed()),
            ("setFetchMode",   vec![req("mode")], t_bool()),
            ("nextRowset",     vec![], t_bool()),
        ] {
            pdo_stmt.own_methods.insert(Arc::from(*name),
                stub_method("PDOStatement", name, params.clone(), ret.clone(), Public).1);
        }
        codebase.classes.insert(Arc::from("PDOStatement"), pdo_stmt);
    }

    // ---- Misc PHP built-ins ----
    {
        let value_error = empty_class("ValueError", Some("Error"), vec![]);
        codebase.classes.insert(Arc::from("ValueError"), value_error);
        let type_error = empty_class("TypeError", Some("Error"), vec![]);
        codebase.classes.insert(Arc::from("TypeError"), type_error);
        let parse_error = empty_class("ParseError", Some("Error"), vec![]);
        codebase.classes.insert(Arc::from("ParseError"), parse_error);
        let arithmetic_error = empty_class("ArithmeticError", Some("Error"), vec![]);
        codebase.classes.insert(Arc::from("ArithmeticError"), arithmetic_error);
        let division_by_zero = empty_class("DivisionByZeroError", Some("ArithmeticError"), vec![]);
        codebase.classes.insert(Arc::from("DivisionByZeroError"), division_by_zero);
        let unhandled_match = empty_class("UnhandledMatchError", Some("Error"), vec![]);
        codebase.classes.insert(Arc::from("UnhandledMatchError"), unhandled_match);
    }
}

// ---------------------------------------------------------------------------
// Interface stubs
// ---------------------------------------------------------------------------

fn load_interfaces(codebase: &Codebase) {
    use Visibility::Public;

    // Throwable
    let mut throwable = empty_interface("Throwable", vec![]);
    for (name, params, ret) in &[
        ("getMessage",       vec![], t_str()),
        ("getCode",          vec![], Union::mixed()),
        ("getFile",          vec![], t_str()),
        ("getLine",          vec![], t_int()),
        ("getTrace",         vec![], t_array()),
        ("getTraceAsString", vec![], t_str()),
        ("getPrevious",      vec![], Union::mixed()),
        ("__toString",       vec![], t_str()),
    ] {
        throwable.own_methods.insert(
            Arc::from(*name),
            stub_method("Throwable", name, params.clone(), ret.clone(), Public).1,
        );
    }
    codebase.interfaces.insert(Arc::from("Throwable"), throwable);

    // Stringable
    let mut stringable = empty_interface("Stringable", vec![]);
    stringable.own_methods.insert(
        Arc::from("__toString"),
        stub_method("Stringable", "__toString", vec![], t_str(), Public).1,
    );
    codebase.interfaces.insert(Arc::from("Stringable"), stringable);

    // Countable
    let mut countable = empty_interface("Countable", vec![]);
    countable.own_methods.insert(
        Arc::from("count"),
        stub_method("Countable", "count", vec![], t_int(), Public).1,
    );
    codebase.interfaces.insert(Arc::from("Countable"), countable);

    // Traversable (marker, no methods)
    let traversable = empty_interface("Traversable", vec![]);
    codebase.interfaces.insert(Arc::from("Traversable"), traversable);

    // Iterator
    let mut iterator = empty_interface("Iterator", vec!["Traversable"]);
    for (name, params, ret) in &[
        ("current", vec![], Union::mixed()),
        ("key",     vec![], Union::mixed()),
        ("next",    vec![], t_void()),
        ("rewind",  vec![], t_void()),
        ("valid",   vec![], t_bool()),
    ] {
        iterator.own_methods.insert(
            Arc::from(*name),
            stub_method("Iterator", name, params.clone(), ret.clone(), Public).1,
        );
    }
    codebase.interfaces.insert(Arc::from("Iterator"), iterator);

    // IteratorAggregate
    let mut iter_agg = empty_interface("IteratorAggregate", vec!["Traversable"]);
    iter_agg.own_methods.insert(
        Arc::from("getIterator"),
        stub_method("IteratorAggregate", "getIterator", vec![], t_obj("Traversable"), Public).1,
    );
    codebase.interfaces.insert(Arc::from("IteratorAggregate"), iter_agg);

    // ArrayAccess
    let mut aa = empty_interface("ArrayAccess", vec![]);
    for (name, params, ret) in &[
        ("offsetExists", vec![req("offset")], t_bool()),
        ("offsetGet",    vec![req("offset")], Union::mixed()),
        ("offsetSet",    vec![req("offset"), req("value")], t_void()),
        ("offsetUnset",  vec![req("offset")], t_void()),
    ] {
        aa.own_methods.insert(
            Arc::from(*name),
            stub_method("ArrayAccess", name, params.clone(), ret.clone(), Public).1,
        );
    }
    codebase.interfaces.insert(Arc::from("ArrayAccess"), aa);

    // DateTimeInterface
    let mut dti = empty_interface("DateTimeInterface", vec![]);
    for (name, params, ret) in &[
        ("format",       vec![req("format")], t_str()),
        ("getTimestamp", vec![], t_int()),
        ("diff",         vec![req("targetObject"), opt("absolute")], t_obj("DateInterval")),
    ] {
        dti.own_methods.insert(
            Arc::from(*name),
            stub_method("DateTimeInterface", name, params.clone(), ret.clone(), Public).1,
        );
    }
    codebase.interfaces.insert(Arc::from("DateTimeInterface"), dti);

    // JsonSerializable
    let mut json_ser = empty_interface("JsonSerializable", vec![]);
    json_ser.own_methods.insert(
        Arc::from("jsonSerialize"),
        stub_method("JsonSerializable", "jsonSerialize", vec![], Union::mixed(), Public).1,
    );
    codebase.interfaces.insert(Arc::from("JsonSerializable"), json_ser);

    // UnitEnum (PHP 8.1 — base for all enums)
    let unit_enum = empty_interface("UnitEnum", vec![]);
    codebase.interfaces.insert(Arc::from("UnitEnum"), unit_enum);

    // BackedEnum (PHP 8.1 — base for string/int-backed enums)
    let backed_enum = empty_interface("BackedEnum", vec!["UnitEnum"]);
    codebase.interfaces.insert(Arc::from("BackedEnum"), backed_enum);

    // Countable
    let mut countable = empty_interface("Countable", vec![]);
    countable.own_methods.insert(
        Arc::from("count"),
        stub_method("Countable", "count", vec![], t_int(), Public).1,
    );
    codebase.interfaces.insert(Arc::from("Countable"), countable);

    // IteratorAggregate
    let mut iter_agg = empty_interface("IteratorAggregate", vec!["Traversable"]);
    iter_agg.own_methods.insert(
        Arc::from("getIterator"),
        stub_method("IteratorAggregate", "getIterator", vec![], t_obj("Traversable"), Public).1,
    );
    codebase.interfaces.insert(Arc::from("IteratorAggregate"), iter_agg);

    // Serializable
    let serializable = empty_interface("Serializable", vec![]);
    codebase.interfaces.insert(Arc::from("Serializable"), serializable);

    // RecursiveIterator
    let mut recursive_iterator = empty_interface("RecursiveIterator", vec!["Iterator"]);
    for (name, params, ret) in &[
        ("getChildren", vec![], t_obj("RecursiveIterator")),
        ("hasChildren", vec![], t_bool()),
    ] {
        recursive_iterator.own_methods.insert(
            Arc::from(*name),
            stub_method("RecursiveIterator", name, params.clone(), ret.clone(), Public).1,
        );
    }
    codebase.interfaces.insert(Arc::from("RecursiveIterator"), recursive_iterator);

    // OuterIterator
    let outer_iterator = empty_interface("OuterIterator", vec!["Iterator"]);
    codebase.interfaces.insert(Arc::from("OuterIterator"), outer_iterator);

    // Psr\Log\LoggerInterface (commonly used)
    let psr_logger = empty_interface("Psr\\Log\\LoggerInterface", vec![]);
    codebase.interfaces.insert(Arc::from("Psr\\Log\\LoggerInterface"), psr_logger);
}