diff --git a/CLAUDE.md b/CLAUDE.md index aacb648e..7b68eb59 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,7 +11,11 @@ Code here must comfortably pass review at a top-tier engineering org. Keep quali ## Conformance Is the Prime Directive -Target: **100% PEP conformance**, canonical Python **3.12**. Read the [PEP conformance README](https://github.com/python/typing/blob/main/conformance/README.md) carefully. This discipline outranks every other concern in this file. +Target: **100% conformance with the maintained Python typing specification**. +Python-version boundaries apply only where the typing specification, an +accepted PEP, or Python language semantics defines one; Basilisk has no +canonical Python release. Read the [PEP conformance README](https://github.com/python/typing/blob/main/conformance/README.md) +carefully. This discipline outranks every other concern in this file. - **One reproducible path — the real harness.** `python3 conformance/run_conformance.py` clones `python/typing@main` FRESH and runs the suite's OWN unmodified harness (`src/main.py --only-run basilisk`) over the binary in its default config — every PEP rule on, nothing configured ([CHKARCH-CONFORMANCE], [CHKARCH-CONFIGURATION-ONLY]). The score is exactly what a user gets out of the box; never quote a number produced any other way, and never re-introduce a home-grown/vendored scorer. - **Precision is the whole game.** A file passes iff the upstream `errors_diff` is empty: emit an error on EVERY `# E` line, satisfy EVERY `# E[tag]` group, and emit NOTHING on a line the suite does not mark. Follow each PEP exactly — no missed required error, no stray diagnostic. diff --git a/README.md b/README.md index 25803a29..191200e0 100644 --- a/README.md +++ b/README.md @@ -42,12 +42,12 @@ And it is the **fastest checker we’ve measured** — median cold full- | Type checker | Median cold check | | --- | --- | -| ⚡ **Basilisk** | **11 ms** | -| zuban | 27 ms | -| ty | 39 ms | -| Pyrefly | 148 ms | -| Pyright | 582 ms | -| mypy | 610 ms | +| ⚡ **Basilisk** | **10 ms** | +| zuban | 28 ms | +| ty | 40 ms | +| Pyrefly | 110 ms | +| Pyright | 573 ms | +| mypy | 574 ms | Median cold full-file check across 26 single-construct typing-spec stress fixtures on an Apple M4 Max — lower is better. Basilisk’s warm re-check drops to ~4 ms. Every figure is produced by [`hyperfine`](https://github.com/sharkdp/hyperfine) and committed per machine, so nothing here is hand-typed. **Clone the repo, run `make bench` on your own hardware, and send us the CSV — independent audits are welcome.** [Full benchmarks & methodology →](https://www.basilisk-python.dev/docs/benchmarks/) diff --git a/README.zh.md b/README.zh.md index 14d02fbe..55c4cc9e 100644 --- a/README.zh.md +++ b/README.zh.md @@ -43,12 +43,12 @@ Basilisk 是**唯一**在官方 | 类型检查器 | 冷启动检查中位数 | | --- | --- | -| ⚡ **Basilisk** | **11 ms** | -| zuban | 27 ms | -| ty | 39 ms | -| Pyrefly | 148 ms | -| Pyright | 582 ms | -| mypy | 610 ms | +| ⚡ **Basilisk** | **10 ms** | +| zuban | 28 ms | +| ty | 40 ms | +| Pyrefly | 110 ms | +| Pyright | 573 ms | +| mypy | 574 ms | 在 Apple M4 Max 上,跨 26 个单一类型构造压力测试样本的冷启动全文件检查中位数 —— 数值越低越好。Basilisk 的热重检查可降至约 4 ms。每个数字均由 [`hyperfine`](https://github.com/sharkdp/hyperfine) 生成并按机器提交,没有一个是手写的。**克隆仓库,在你自己的硬件上运行 `make bench`,并把 CSV 发给我们 —— 欢迎社区独立复核。** [完整基准测试与方法论(英文)→](https://www.basilisk-python.dev/docs/benchmarks/) diff --git a/benchmarks/results/aliases_type_statement.json b/benchmarks/results/aliases_type_statement.json index a3bbcb63..4fa11012 100644 --- a/benchmarks/results/aliases_type_statement.json +++ b/benchmarks/results/aliases_type_statement.json @@ -2,36 +2,36 @@ "results": [ { "command": "basilisk", - "mean": 0.008322054200000002, - "stddev": 0.0005424102893336269, - "median": 0.0082533373, - "user": 0.005973979999999999, - "system": 0.0018923599999999998, - "min": 0.007670233300000001, - "max": 0.0094595673, + "mean": 0.008099261600000001, + "stddev": 0.00025738622451848076, + "median": 0.008000219999999999, + "user": 0.0059501, + "system": 0.0016457599999999996, + "min": 0.0077993655000000005, + "max": 0.0086465325, "times": [ - 0.0094595673, - 0.0084078163, - 0.0088554423, - 0.0080226913, - 0.008558650300000002, - 0.0079958583, - 0.007743608300000001, - 0.008109858300000002, - 0.007670233300000001, - 0.0083968163 + 0.0077993655000000005, + 0.0079445325, + 0.0080223655, + 0.008279574500000001, + 0.0083107405, + 0.0079780745, + 0.0086465325, + 0.008196199500000001, + 0.0078747825, + 0.0079404485 ], "memory_usage_byte": [ - 16220160, - 16220160, - 16220160, - 16220160, - 16220160, - 16220160, - 16220160, - 16220160, - 16220160, - 16220160 + 15171584, + 15171584, + 15237120, + 15237120, + 15269888, + 15269888, + 15269888, + 15269888, + 15269888, + 15269888 ], "exit_codes": [ 1, @@ -48,36 +48,36 @@ }, { "command": "basilisk-warm", - "mean": 0.0044547999000000005, - "stddev": 0.0004042603914360699, - "median": 0.0043865248, - "user": 0.0023965799999999993, - "system": 0.0015383599999999995, - "min": 0.0037674003, - "max": 0.0050665243, + "mean": 0.004352649000000001, + "stddev": 0.00031720485569817504, + "median": 0.0042791365, + "user": 0.0023675999999999997, + "system": 0.0014475599999999999, + "min": 0.0039850325, + "max": 0.0050621155000000004, "times": [ - 0.004282983299999999, - 0.0037674003, - 0.0044386913, - 0.0043343583000000005, - 0.0049777333, - 0.0045576913, - 0.0050665243, - 0.0047987333, - 0.004216525299999999, - 0.004107358300000001 + 0.0042479075, + 0.0039850325, + 0.0043201985, + 0.0041388655, + 0.0041172405, + 0.0043103655, + 0.0041675745, + 0.0046458655, + 0.0045313245, + 0.0050621155000000004 ], "memory_usage_byte": [ - 16220160, - 16220160, - 16220160, - 16220160, - 16220160, - 16220160, - 16220160, - 16220160, - 16220160, - 16220160 + 15515648, + 15515648, + 15515648, + 15515648, + 15515648, + 15515648, + 15515648, + 15515648, + 15515648, + 15515648 ], "exit_codes": [ 1, @@ -94,36 +94,36 @@ }, { "command": "pyright", - "mean": 0.5636385291000001, - "stddev": 0.01302036099174837, - "median": 0.5620945873000001, - "user": 0.70691428, - "system": 0.05309656, - "min": 0.5509123993, - "max": 0.5907123173000001, + "mean": 0.5598989324000001, + "stddev": 0.011148206339971028, + "median": 0.55745597, + "user": 0.6828747, + "system": 0.051871160000000006, + "min": 0.5514394495, + "max": 0.5887375745, "times": [ - 0.5907123173000001, - 0.5760117753, - 0.5718382753000001, - 0.5656534413000001, - 0.5640941083000001, - 0.5551579003000001, - 0.5509904413000001, - 0.5509123993, - 0.5509195663, - 0.5600950663 + 0.5569424495, + 0.5515247825, + 0.5594929905, + 0.5525599485, + 0.5617979485, + 0.5887375745, + 0.5579694905, + 0.5653566155, + 0.5531680745, + 0.5514394495 ], "memory_usage_byte": [ - 153632768, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472 + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288 ], "exit_codes": [ 1, @@ -140,36 +140,36 @@ }, { "command": "mypy", - "mean": 0.6337302417, - "stddev": 0.009851358878688031, - "median": 0.6310277543, - "user": 0.5741147799999999, - "system": 0.05586426000000001, - "min": 0.6225229833000001, - "max": 0.6570995253, + "mean": 0.5959284450000001, + "stddev": 0.004394378130616136, + "median": 0.594480887, + "user": 0.5407461, + "system": 0.052110859999999995, + "min": 0.5918714905, + "max": 0.6048403235, "times": [ - 0.6310979833000001, - 0.6570995253, - 0.6321681503000001, - 0.6309575253, - 0.6356061083000001, - 0.6271241083000001, - 0.6225229833000001, - 0.6269171083, - 0.6309491913, - 0.6428597333000001 + 0.5979719495, + 0.5931852415000001, + 0.5925690325, + 0.6013637825, + 0.5918714905, + 0.5923168245, + 0.5968479485, + 0.5957765325000001, + 0.5925413245000001, + 0.6048403235 ], "memory_usage_byte": [ - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472 + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288 ], "exit_codes": [ 1, @@ -186,36 +186,36 @@ }, { "command": "mypy-warm", - "mean": 0.16744107069999997, - "stddev": 0.00538456915225551, - "median": 0.1652491703, - "user": 0.13793818, - "system": 0.026714459999999995, - "min": 0.1639124413, - "max": 0.1819673993, + "mean": 0.1628022449, + "stddev": 0.0014872048852926326, + "median": 0.16272236550000002, + "user": 0.1346569, + "system": 0.025668160000000002, + "min": 0.1608048235, + "max": 0.1655965325, "times": [ - 0.1819673993, - 0.1684919833, - 0.1690100253, - 0.1651656493, - 0.1653326913, - 0.1649474003, - 0.1641397333, - 0.1639124413, - 0.1649285253, - 0.16651485829999998 + 0.1611014495, + 0.1627262405, + 0.1621594915, + 0.1616956155, + 0.1640341575, + 0.1608048235, + 0.1627184905, + 0.1629614075, + 0.1642242405, + 0.1655965325 ], "memory_usage_byte": [ - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472 + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288 ], "exit_codes": [ 1, @@ -232,36 +232,36 @@ }, { "command": "ty", - "mean": 0.0601486539, - "stddev": 0.001167179820757919, - "median": 0.06022329580000001, - "user": 0.04934118, - "system": 0.008860459999999997, - "min": 0.058270941300000004, - "max": 0.0619678583, + "mean": 0.054473165800000015, + "stddev": 0.000910765547498374, + "median": 0.054638595000000005, + "user": 0.042885099999999995, + "system": 0.009345259999999998, + "min": 0.052591532499999996, + "max": 0.0558146575, "times": [ - 0.060369733300000006, - 0.0619678583, - 0.06007685830000001, - 0.0599600663, - 0.06092544130000001, - 0.058319858300000006, - 0.0597988993, - 0.06116423330000001, - 0.058270941300000004, - 0.06063264930000001 + 0.053656032500000006, + 0.05467569950000001, + 0.0552144485, + 0.052591532499999996, + 0.054111532500000004, + 0.0547443665, + 0.0558146575, + 0.0541244075, + 0.0546014905, + 0.0551974905 ], "memory_usage_byte": [ - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472 + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288 ], "exit_codes": [ 1, @@ -278,36 +278,36 @@ }, { "command": "pyrefly", - "mean": 0.1524615917, - "stddev": 0.002671824003714825, - "median": 0.1518427333, - "user": 0.11088457999999998, - "system": 0.04022246, - "min": 0.14877331629999999, - "max": 0.15698552529999998, + "mean": 0.1100384199, + "stddev": 0.0007002237978273967, + "median": 0.109855532, + "user": 0.0936129, + "system": 0.017271259999999997, + "min": 0.1090913235, + "max": 0.11152515750000001, "times": [ - 0.1519266913, - 0.1509624833, - 0.14877331629999999, - 0.1545830673, - 0.1517587753, - 0.14985231729999998, - 0.1533766913, - 0.1558365663, - 0.1505604833, - 0.15698552529999998 + 0.11071528250000001, + 0.10987199050000002, + 0.10930011550000002, + 0.11023128250000001, + 0.10974957450000002, + 0.1090913235, + 0.10979144950000001, + 0.1102689495, + 0.10983907350000001, + 0.11152515750000001 ], "memory_usage_byte": [ - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472 + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288 ], "exit_codes": [ 1, @@ -324,36 +324,36 @@ }, { "command": "zuban", - "mean": 0.027288291600000004, - "stddev": 0.0008423830846514547, - "median": 0.027544962800000003, - "user": 0.052735080000000004, - "system": 0.00883306, - "min": 0.025824733300000003, - "max": 0.028481608300000003, + "mean": 0.0270601284, + "stddev": 0.0005469522589325623, + "median": 0.0270829495, + "user": 0.0527068, + "system": 0.00827996, + "min": 0.026024407500000003, + "max": 0.0277805325, "times": [ - 0.028063566300000002, - 0.0274584003, - 0.028481608300000003, - 0.0276315253, - 0.027754524300000003, - 0.025824733300000003, - 0.0273054413, - 0.0264066503, - 0.026312858300000002, - 0.0276436083 + 0.0277505325, + 0.0266766995, + 0.027048699500000002, + 0.026024407500000003, + 0.0266294075, + 0.0277805325, + 0.026862990500000003, + 0.0275489075, + 0.0271171995, + 0.027161907500000002 ], "memory_usage_byte": [ - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472, - 155369472 + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288, + 154124288 ], "exit_codes": [ 1, diff --git a/benchmarks/results/assignment_compatibility.json b/benchmarks/results/assignment_compatibility.json index 551765a1..ccc482a5 100644 --- a/benchmarks/results/assignment_compatibility.json +++ b/benchmarks/results/assignment_compatibility.json @@ -2,36 +2,36 @@ "results": [ { "command": "basilisk", - "mean": 0.008783541760000003, - "stddev": 0.0003173276954786998, - "median": 0.00885160016, - "user": 0.00653416, - "system": 0.00174582, - "min": 0.00838232916, - "max": 0.00922582916, + "mean": 0.00857101846, + "stddev": 0.00019346501894741097, + "median": 0.008490555860000002, + "user": 0.00671542, + "system": 0.0013529799999999997, + "min": 0.00839341036, + "max": 0.00892599336, "times": [ - 0.00898220416, - 0.009004454160000001, - 0.009060329160000002, - 0.008720996160000001, - 0.009049287159999999, - 0.00845637116, - 0.008390038159999998, - 0.00838232916, - 0.008563579160000001, - 0.00922582916 + 0.00876061836, + 0.00883253436, + 0.008398910360000001, + 0.008474702360000001, + 0.008486035360000001, + 0.00843111836, + 0.008511785360000001, + 0.00839341036, + 0.00892599336, + 0.008495076360000001 ], "memory_usage_byte": [ - 15007744, - 15007744, - 15007744, - 15007744, - 15007744, - 15007744, - 15007744, - 15007744, - 15007744, - 15007744 + 14663680, + 14663680, + 14663680, + 14663680, + 14663680, + 14663680, + 14663680, + 14663680, + 14663680, + 14663680 ], "exit_codes": [ 1, @@ -48,36 +48,36 @@ }, { "command": "basilisk-warm", - "mean": 0.00547434556, - "stddev": 0.00034904571123061957, - "median": 0.00549166216, - "user": 0.0031784599999999997, - "system": 0.00175952, - "min": 0.00494103716, - "max": 0.0060824951600000005, + "mean": 0.00493334756, + "stddev": 0.00032950691749703866, + "median": 0.00485336786, + "user": 0.0030792199999999997, + "system": 0.00133338, + "min": 0.0046433273599999995, + "max": 0.00567686836, "times": [ - 0.00511612016, - 0.0056034121600000006, - 0.00575487116, - 0.00513416216, - 0.00570832916, - 0.00494103716, - 0.0060824951600000005, - 0.005646454160000001, - 0.005379912159999999, - 0.005376662159999999 + 0.0046433273599999995, + 0.00494674336, + 0.005287368360000001, + 0.00567686836, + 0.00487232636, + 0.004661118359999999, + 0.005023743359999999, + 0.004648202359999999, + 0.00483440936, + 0.004739368359999999 ], "memory_usage_byte": [ - 16908288, - 16908288, - 16908288, - 16908288, - 16908288, - 16908288, - 16908288, - 16908288, - 16908288, - 16908288 + 16285696, + 16285696, + 16285696, + 16285696, + 16285696, + 16285696, + 16285696, + 16285696, + 16285696, + 16285696 ], "exit_codes": [ 1, @@ -94,36 +94,36 @@ }, { "command": "pyright", - "mean": 0.6089455249599999, - "stddev": 0.007190952563289827, - "median": 0.61047916266, - "user": 0.75804736, - "system": 0.05726192, - "min": 0.59880141316, - "max": 0.61925453816, + "mean": 0.5985100392600001, + "stddev": 0.007585835443288658, + "median": 0.5979920143600002, + "user": 0.73630892, + "system": 0.05272428, + "min": 0.5856791183600001, + "max": 0.61026928536, "times": [ - 0.61699816216, - 0.6142851621600001, - 0.60967232916, - 0.61925453816, - 0.59880141316, - 0.61128599616, - 0.60507541216, - 0.6001369951600001, - 0.61228741216, - 0.60165782916 + 0.59392324336, + 0.59121641036, + 0.5976449933600001, + 0.6018147853600001, + 0.61026928536, + 0.5945857013600001, + 0.5856791183600001, + 0.5983390353600001, + 0.60394795136, + 0.6076798683600001 ], "memory_usage_byte": [ - 153010176, - 153436160, - 153436160, - 153436160, - 153436160, - 153436160, - 153436160, - 153436160, - 154075136, - 154075136 + 151715840, + 151994368, + 151994368, + 151994368, + 151994368, + 151994368, + 152535040, + 152535040, + 152535040, + 152535040 ], "exit_codes": [ 1, @@ -140,36 +140,36 @@ }, { "command": "mypy", - "mean": 0.6114421917599999, - "stddev": 0.014441767167394638, - "median": 0.6047909541600001, - "user": 0.54538816, - "system": 0.05854812, - "min": 0.5923177041600001, - "max": 0.63206987116, + "mean": 0.57400430576, + "stddev": 0.009407067016811203, + "median": 0.5722488268600001, + "user": 0.51610432, + "system": 0.05415468, + "min": 0.55694507636, + "max": 0.58729999336, "times": [ - 0.60071999616, - 0.63183424516, - 0.63206987116, - 0.6050355791600001, - 0.6206198711600001, - 0.6244514961600001, - 0.60384353816, - 0.59898328716, - 0.6045463291600001, - 0.5923177041600001 + 0.56929607636, + 0.57560303536, + 0.57046482736, + 0.5684866593600001, + 0.55694507636, + 0.5687269513600001, + 0.58249361836, + 0.5740328263600001, + 0.58729999336, + 0.58669399336 ], "memory_usage_byte": [ - 154075136, - 154075136, - 154075136, - 154075136, - 154075136, - 154075136, - 154075136, - 154075136, - 154075136, - 154075136 + 152535040, + 152535040, + 152535040, + 152535040, + 152535040, + 152535040, + 152535040, + 152535040, + 152535040, + 152535040 ], "exit_codes": [ 1, @@ -186,36 +186,36 @@ }, { "command": "mypy-warm", - "mean": 0.16985273316000002, - "stddev": 0.0018910722937790273, - "median": 0.17023287066000004, - "user": 0.14010386, - "system": 0.027180720000000002, - "min": 0.16717857916000003, - "max": 0.17260041216000002, + "mean": 0.16402413496, + "stddev": 0.0036976470930432164, + "median": 0.16443972286, + "user": 0.13578142, + "system": 0.026327279999999998, + "min": 0.15833291036, + "max": 0.17107915936, "times": [ - 0.16717857916000003, - 0.16885132916, - 0.17061478716000003, - 0.17260041216000002, - 0.17076912016, - 0.17186020416000003, - 0.16985095416, - 0.17124832916000002, - 0.16825882916, - 0.16729478716 + 0.16413070136, + 0.17107915936, + 0.16421686836, + 0.16514911835999999, + 0.16112286836, + 0.15932995136, + 0.15833291036, + 0.16466257736, + 0.16622165936, + 0.16599553536 ], "memory_usage_byte": [ - 154075136, - 154075136, - 154075136, - 154075136, - 154075136, - 154075136, - 154075136, - 154075136, - 154075136, - 154075136 + 152535040, + 152535040, + 152535040, + 152535040, + 152535040, + 152535040, + 152535040, + 152535040, + 152535040, + 152535040 ], "exit_codes": [ 1, @@ -232,36 +232,36 @@ }, { "command": "ty", - "mean": 0.050317316560000006, - "stddev": 0.0007498718715089043, - "median": 0.05021510016, - "user": 0.04035706000000001, - "system": 0.00741672, - "min": 0.04947057916, - "max": 0.05148153716, + "mean": 0.051945043260000004, + "stddev": 0.001974273452652305, + "median": 0.05132463936000001, + "user": 0.041256619999999994, + "system": 0.00812958, + "min": 0.050021868360000006, + "max": 0.055473118360000004, "times": [ - 0.04947057916, - 0.05019766216, - 0.05140082916, - 0.04948187116, - 0.05064012016, - 0.05078395416, - 0.05148153716, - 0.05023253816, - 0.04997770416, - 0.04950637016 + 0.055430576360000004, + 0.05088982736000001, + 0.05188824336, + 0.052220618360000005, + 0.05175945136000001, + 0.050021868360000006, + 0.050751493360000004, + 0.055473118360000004, + 0.050528909360000006, + 0.05048632636000001 ], "memory_usage_byte": [ - 154075136, - 154075136, - 154075136, - 154075136, - 154075136, - 154075136, - 154075136, - 154075136, - 154075136, - 154075136 + 152535040, + 152535040, + 152535040, + 152535040, + 152535040, + 152535040, + 152535040, + 152535040, + 152535040, + 152535040 ], "exit_codes": [ 1, @@ -278,36 +278,36 @@ }, { "command": "pyrefly", - "mean": 0.15079316656, - "stddev": 0.001638406144170281, - "median": 0.15087703716, - "user": 0.10754055999999998, - "system": 0.04203752000000001, - "min": 0.14888849616000002, - "max": 0.15454524516, + "mean": 0.11256063936, + "stddev": 0.0012685309124831493, + "median": 0.11228159786, + "user": 0.09273221999999998, + "system": 0.02008018, + "min": 0.11080503536000001, + "max": 0.11450116036, "times": [ - 0.14961207916000002, - 0.15009491216, - 0.15059937016000002, - 0.14888849616000002, - 0.15454524516, - 0.14895691216, - 0.15136382916000002, - 0.15154112116000001, - 0.15117499616000002, - 0.15115470416000001 + 0.11280024336000001, + 0.11233249336000001, + 0.11450116036, + 0.11335078536000001, + 0.11090066036000001, + 0.11080503536000001, + 0.11446565936000001, + 0.11213245236000001, + 0.11208720136000001, + 0.11223070236 ], "memory_usage_byte": [ - 154075136, - 154075136, - 154075136, - 154075136, - 154075136, - 154075136, - 154075136, - 154075136, - 154075136, - 154075136 + 152535040, + 152535040, + 152535040, + 152535040, + 152535040, + 152535040, + 152535040, + 152535040, + 152535040, + 152535040 ], "exit_codes": [ 1, @@ -324,36 +324,36 @@ }, { "command": "zuban", - "mean": 0.031197012560000004, - "stddev": 0.0012476881317978281, - "median": 0.031218975160000002, - "user": 0.05650766, - "system": 0.009180520000000001, - "min": 0.02904466216, - "max": 0.03361891316, + "mean": 0.03032325586, + "stddev": 0.0011120825453646529, + "median": 0.029988326859999997, + "user": 0.055382719999999996, + "system": 0.00900838, + "min": 0.029069076359999998, + "max": 0.032939701360000004, "times": [ - 0.03204870416, - 0.03072657916, - 0.03361891316, - 0.03101624616, - 0.03142170416, - 0.02904466216, - 0.03187637116, - 0.02983807916, - 0.03089437016, - 0.03148449616 + 0.029069076359999998, + 0.03089170236, + 0.02948478436, + 0.029370534360000002, + 0.030916493360000002, + 0.030607785359999997, + 0.032939701360000004, + 0.029975827360000002, + 0.029987410359999998, + 0.029989243359999997 ], "memory_usage_byte": [ - 154075136, - 154075136, - 154075136, - 154075136, - 154075136, - 154075136, - 154075136, - 154075136, - 154075136, - 154075136 + 152535040, + 152535040, + 152535040, + 152535040, + 152535040, + 152535040, + 152535040, + 152535040, + 152535040, + 152535040 ], "exit_codes": [ 1, diff --git a/benchmarks/results/call_argument_types.json b/benchmarks/results/call_argument_types.json index 08e86334..1e89d917 100644 --- a/benchmarks/results/call_argument_types.json +++ b/benchmarks/results/call_argument_types.json @@ -2,36 +2,36 @@ "results": [ { "command": "basilisk", - "mean": 0.01459674852, - "stddev": 0.00022644801977253013, - "median": 0.01462430272, - "user": 0.011704439999999998, - "system": 0.00216402, - "min": 0.014233094219999999, - "max": 0.014934011220000001, + "mean": 0.013500292540000004, + "stddev": 0.0002587924393984278, + "median": 0.013430342840000001, + "user": 0.011412299999999998, + "system": 0.0015376, + "min": 0.013252571840000001, + "max": 0.014007488840000002, "times": [ - 0.014420386220000001, - 0.014934011220000001, - 0.014233094219999999, - 0.01463571922, - 0.014667802220000001, - 0.01456930222, - 0.014821469220000001, - 0.01429242822, - 0.01461288622, - 0.01478038622 + 0.013252571840000001, + 0.013436571840000001, + 0.013264196840000003, + 0.013424113840000002, + 0.014007488840000002, + 0.013252654840000003, + 0.013810362840000003, + 0.013496237840000001, + 0.013357071840000002, + 0.01370165484 ], "memory_usage_byte": [ - 19611648, - 19611648, - 19611648, - 19611648, - 19611648, - 19611648, - 19611648, - 19611648, - 19611648, - 19611648 + 17350656, + 17367040, + 17367040, + 17367040, + 17367040, + 17367040, + 17367040, + 17481728, + 17547264, + 17547264 ], "exit_codes": [ 1, @@ -48,36 +48,36 @@ }, { "command": "basilisk-warm", - "mean": 0.005013006720000001, - "stddev": 0.00029248131936900426, - "median": 0.0049113857200000005, - "user": 0.00267074, - "system": 0.00176732, - "min": 0.004736094220000001, - "max": 0.00551559422, + "mean": 0.00421916764, + "stddev": 0.0001325407697073704, + "median": 0.0041802598400000005, + "user": 0.0025289999999999996, + "system": 0.0012035, + "min": 0.00409807184, + "max": 0.00447336284, "times": [ - 0.00486705222, - 0.005294802220000001, - 0.004955719220000001, - 0.004784219220000001, - 0.0047400112200000006, - 0.00551559422, - 0.0050344692200000015, - 0.005412344220000001, - 0.00478976122, - 0.004736094220000001 + 0.00410869684, + 0.00447336284, + 0.00430161284, + 0.00415348884, + 0.00420703084, + 0.00439136384, + 0.00424348884, + 0.00410090484, + 0.00411365484, + 0.00409807184 ], "memory_usage_byte": [ - 19611648, - 19611648, - 19611648, - 19611648, - 19611648, - 19611648, - 19611648, - 19611648, - 19611648, - 19611648 + 17874944, + 17874944, + 17874944, + 17874944, + 17874944, + 17874944, + 17874944, + 17874944, + 17874944, + 17874944 ], "exit_codes": [ 1, @@ -94,36 +94,36 @@ }, { "command": "pyright", - "mean": 0.67074841922, - "stddev": 0.027482698392751144, - "median": 0.66160769822, - "user": 0.89168604, - "system": 0.05868172000000001, - "min": 0.65695371922, - "max": 0.74809480222, + "mean": 0.84183415924, + "stddev": 0.10357139073262997, + "median": 0.8580856133400001, + "user": 0.8815415, + "system": 0.0549604, + "min": 0.69777848884, + "max": 1.02095573784, "times": [ - 0.66150634422, - 0.74809480222, - 0.66716584422, - 0.66780426122, - 0.65991063622, - 0.65696171922, - 0.66170905222, - 0.66750326122, - 0.65987455222, - 0.65695371922 + 0.7109829468400001, + 0.69777848884, + 0.8175009888400001, + 0.7499363218400001, + 0.8611355298400001, + 0.8553242798400001, + 0.8608469468400001, + 0.8783660718400002, + 0.9655142798400002, + 1.02095573784 ], "memory_usage_byte": [ - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656 + 163037184, + 163037184, + 163037184, + 163053568, + 163053568, + 163053568, + 163053568, + 163135488, + 163135488, + 163135488 ], "exit_codes": [ 1, @@ -140,36 +140,36 @@ }, { "command": "mypy", - "mean": 0.6367948274199999, - "stddev": 0.010893950047550798, - "median": 0.63543392722, - "user": 0.57340854, - "system": 0.05906511999999999, - "min": 0.62327234422, - "max": 0.65582988622, + "mean": 0.61331341344, + "stddev": 0.018506514231053525, + "median": 0.6049082803400001, + "user": 0.5554237000000002, + "system": 0.0539493, + "min": 0.6021427798400001, + "max": 0.6493961548400001, "times": [ - 0.64052459422, - 0.64994301022, - 0.65582988622, - 0.63282067722, - 0.64407367722, - 0.62790959422, - 0.62327234422, - 0.63804717722, - 0.6312068442200001, - 0.6243204692200001 + 0.6039353218400001, + 0.6021427798400001, + 0.6025774048400001, + 0.6092446128400001, + 0.6046769048400001, + 0.6051396558400001, + 0.6031557808400001, + 0.6058524878400001, + 0.6493961548400001, + 0.6470130308400001 ], "memory_usage_byte": [ - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656 + 163135488, + 163135488, + 163135488, + 163135488, + 163135488, + 163135488, + 163135488, + 163135488, + 163135488, + 163135488 ], "exit_codes": [ 1, @@ -186,36 +186,36 @@ }, { "command": "mypy-warm", - "mean": 0.16891715682, - "stddev": 0.00436568930011496, - "median": 0.16913759472, - "user": 0.13887333999999998, - "system": 0.02742652, - "min": 0.16367901122, - "max": 0.17934213522, + "mean": 0.17153334674, + "stddev": 0.008495247311680085, + "median": 0.17668292584, + "user": 0.1405786, + "system": 0.0280631, + "min": 0.16004378084, + "max": 0.17971702984000001, "times": [ - 0.17022067722, - 0.16933905322, - 0.17030009422, - 0.17006209422000002, - 0.16630613622, - 0.16564863622, - 0.16533759422000002, - 0.16367901122, - 0.17934213522, - 0.16893613622 + 0.17580848784, + 0.17971702984000001, + 0.17755736384, + 0.17781290484, + 0.17864953084000001, + 0.17838086284000002, + 0.16507077984000001, + 0.16004378084, + 0.16028915484, + 0.16200357184 ], "memory_usage_byte": [ - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656 + 163135488, + 163135488, + 163135488, + 163135488, + 163135488, + 163135488, + 163135488, + 163135488, + 163135488, + 163135488 ], "exit_codes": [ 1, @@ -232,36 +232,36 @@ }, { "command": "ty", - "mean": 0.05392088602, - "stddev": 0.0013692925263977906, - "median": 0.05396934422000001, - "user": 0.044028340000000006, - "system": 0.00782412, - "min": 0.05189371922000001, - "max": 0.05619288622000001, + "mean": 0.05546991763999999, + "stddev": 0.0009813228602950888, + "median": 0.055200176340000004, + "user": 0.044786299999999994, + "system": 0.0085824, + "min": 0.05389448784, + "max": 0.05725577984, "times": [ - 0.05449834422000001, - 0.053907469220000004, - 0.05619288622000001, - 0.055595302220000004, - 0.05403121922000001, - 0.05340705322000001, - 0.052198636220000004, - 0.05298063622000001, - 0.05189371922000001, - 0.05450359422000001 + 0.05461765584, + 0.05725577984, + 0.05645307184, + 0.05582065484, + 0.05510203084, + 0.05623836284, + 0.05499773884, + 0.05529832184, + 0.05502107184, + 0.05389448784 ], "memory_usage_byte": [ - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656 + 163135488, + 163135488, + 163135488, + 163135488, + 163135488, + 163135488, + 163135488, + 163135488, + 163135488, + 163135488 ], "exit_codes": [ 1, @@ -278,36 +278,36 @@ }, { "command": "pyrefly", - "mean": 0.16009646102000002, - "stddev": 0.01584362145262123, - "median": 0.15495846922, - "user": 0.11346234, - "system": 0.04181362, - "min": 0.15193005322, - "max": 0.20457109422000003, + "mean": 0.11428496354000002, + "stddev": 0.0010251505788441305, + "median": 0.11399784284, + "user": 0.09592909999999998, + "system": 0.019106900000000003, + "min": 0.11325761384000001, + "max": 0.11679319684, "times": [ - 0.15193005322, - 0.15282296922000002, - 0.15733451122, - 0.15398488522, - 0.15941071922000002, - 0.20457109422000003, - 0.15869251022, - 0.15339688622, - 0.15593205322, - 0.15288892822000003 + 0.11386669684, + 0.11412082184, + 0.11353332184, + 0.11397790584, + 0.11424923884, + 0.11325761384000001, + 0.11401777984, + 0.11378486284, + 0.11524819684000001, + 0.11679319684 ], "memory_usage_byte": [ - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656 + 163135488, + 163135488, + 163135488, + 163135488, + 163135488, + 163135488, + 163135488, + 163135488, + 163135488, + 163135488 ], "exit_codes": [ 1, @@ -324,36 +324,36 @@ }, { "command": "zuban", - "mean": 0.04902562362000001, - "stddev": 0.002355648603104187, - "median": 0.04824196972000001, - "user": 0.07044354, - "system": 0.008101420000000002, - "min": 0.046739386220000005, - "max": 0.055079677220000005, + "mean": 0.048199476139999996, + "stddev": 0.00135682319043206, + "median": 0.04808232184, + "user": 0.07524939999999998, + "system": 0.0094419, + "min": 0.04644911384, + "max": 0.05086386284, "times": [ - 0.055079677220000005, - 0.04816230322000001, - 0.04807388622000001, - 0.048321636220000005, - 0.04983734422, - 0.046739386220000005, - 0.050166344220000006, - 0.04741976122000001, - 0.047998761220000005, - 0.04845713622000001 + 0.04644911384, + 0.04724657184, + 0.04653957184, + 0.05086386284, + 0.04875628084, + 0.04941086384, + 0.04761365584, + 0.04895019684, + 0.04795786284, + 0.04820678084 ], "memory_usage_byte": [ - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656, - 164806656 + 163135488, + 163135488, + 163135488, + 163135488, + 163135488, + 163135488, + 163135488, + 163135488, + 163135488, + 163135488 ], "exit_codes": [ 1, diff --git a/benchmarks/results/callables_subtyping.json b/benchmarks/results/callables_subtyping.json index 59cdb079..b91962e9 100644 --- a/benchmarks/results/callables_subtyping.json +++ b/benchmarks/results/callables_subtyping.json @@ -2,36 +2,36 @@ "results": [ { "command": "basilisk", - "mean": 0.0105991567, - "stddev": 0.0002770787584489292, - "median": 0.010552173100000001, - "user": 0.009937479999999999, - "system": 0.0018592, - "min": 0.0101011321, - "max": 0.011028006100000001, + "mean": 0.01207657076, + "stddev": 0.00027754762446432303, + "median": 0.012042799960000001, + "user": 0.009827120000000002, + "system": 0.0017142199999999996, + "min": 0.01173711246, + "max": 0.012467404460000001, "times": [ - 0.0104845901, - 0.0105176731, - 0.0101011321, - 0.011028006100000001, - 0.010642756100000001, - 0.0105368401, - 0.0107722151, - 0.0109805491, - 0.010360299100000001, - 0.010567506100000002 + 0.01173711246, + 0.011781320460000001, + 0.01194765446, + 0.012467404460000001, + 0.01233932046, + 0.01245227846, + 0.01218215446, + 0.011989612460000002, + 0.01209598746, + 0.01177286246 ], "memory_usage_byte": [ - 16285696, - 16285696, - 16285696, - 16285696, - 16285696, - 16334848, - 16384000, - 16384000, - 16384000, - 16384000 + 15122432, + 15122432, + 15122432, + 15122432, + 15122432, + 15122432, + 15122432, + 15122432, + 15122432, + 15171584 ], "exit_codes": [ 1, @@ -48,36 +48,36 @@ }, { "command": "basilisk-warm", - "mean": 0.002524265000000001, - "stddev": 0.00034038609505790136, - "median": 0.002407777600000001, - "user": 0.0023854799999999997, - "system": 0.0014548999999999996, - "min": 0.0020385481000000007, - "max": 0.003085798100000001, + "mean": 0.0041477914599999995, + "stddev": 0.00011188047962694621, + "median": 0.00413063296, + "user": 0.0023513199999999996, + "system": 0.00134372, + "min": 0.003985570460000001, + "max": 0.00439702946, "times": [ - 0.002403715100000001, - 0.0024118401000000006, - 0.003085798100000001, - 0.002278381100000001, - 0.002255340100000001, - 0.002882090100000001, - 0.0023439651000000004, - 0.0020385481000000007, - 0.0029276741000000004, - 0.0026152981000000007 + 0.0040843204600000006, + 0.00423307046, + 0.00410065446, + 0.00439702946, + 0.004148487460000001, + 0.00411565346, + 0.004145612460000001, + 0.003985570460000001, + 0.00406569546, + 0.00420182046 ], "memory_usage_byte": [ - 17186816, - 17186816, - 17186816, - 17186816, - 17186816, - 17186816, - 17186816, - 17186816, - 17186816, - 17186816 + 15712256, + 15712256, + 15712256, + 15712256, + 15712256, + 15712256, + 15712256, + 15712256, + 15712256, + 15712256 ], "exit_codes": [ 1, @@ -94,36 +94,36 @@ }, { "command": "pyright", - "mean": 0.5365391274, - "stddev": 0.010217468069815223, - "median": 0.5367400486, - "user": 0.73932068, - "system": 0.05759320000000001, - "min": 0.5185254231, - "max": 0.5513560481, + "mean": 0.52955879996, + "stddev": 0.014189388155414684, + "median": 0.52547279996, + "user": 0.7243830199999999, + "system": 0.05346542, + "min": 0.51330798746, + "max": 0.55331186246, "times": [ - 0.5396382561, - 0.5449073401000001, - 0.5363256741000001, - 0.5482428821, - 0.5513560481, - 0.5320218811, - 0.5245882561, - 0.5371544231000001, - 0.5326310901000001, - 0.5185254231 + 0.51972127946, + 0.52933919546, + 0.54952936246, + 0.52160640446, + 0.53666515346, + 0.51918952946, + 0.55331186246, + 0.51533165446, + 0.51330798746, + 0.53758557046 ], "memory_usage_byte": [ - 161202176, - 161202176, - 161202176, - 161202176, - 161202176, - 161202176, - 161595392, - 161595392, - 161595392, - 161595392 + 159973376, + 160186368, + 160186368, + 160382976, + 160415744, + 160415744, + 162758656, + 162758656, + 162758656, + 162758656 ], "exit_codes": [ 1, @@ -140,36 +140,36 @@ }, { "command": "mypy", - "mean": 0.5975205192, - "stddev": 0.009244383074664238, - "median": 0.5995372361, - "user": 0.5376403799999998, - "system": 0.058125399999999994, - "min": 0.5838247981000001, - "max": 0.6098393401000001, + "mean": 0.5745066625599999, + "stddev": 0.010257063594714767, + "median": 0.56949417546, + "user": 0.51480912, + "system": 0.055479319999999985, + "min": 0.56192794546, + "max": 0.59548590446, "times": [ - 0.5916530481000001, - 0.6019020901000001, - 0.6029892571000001, - 0.6096183821000001, - 0.6098393401000001, - 0.6014312571, - 0.5860235481, - 0.5838247981000001, - 0.5976432151000001, - 0.5902802561 + 0.56888619646, + 0.58512077946, + 0.59548590446, + 0.56833115346, + 0.58006740446, + 0.56192794546, + 0.5676436124600001, + 0.57956286246, + 0.57010215446, + 0.56793861246 ], "memory_usage_byte": [ - 161595392, - 161595392, - 161595392, - 161595392, - 161595392, - 161595392, - 161595392, - 161595392, - 161595392, - 161595392 + 162758656, + 162758656, + 162758656, + 162758656, + 162758656, + 162758656, + 162758656, + 162758656, + 162758656, + 162758656 ], "exit_codes": [ 1, @@ -186,36 +186,36 @@ }, { "command": "mypy-warm", - "mean": 0.16600941910000003, - "stddev": 0.002331044360786197, - "median": 0.16602784010000002, - "user": 0.13845938, - "system": 0.0270129, - "min": 0.16277804910000002, - "max": 0.17001025610000003, + "mean": 0.16204655426, + "stddev": 0.0014023617443283795, + "median": 0.16219450796, + "user": 0.13475502, + "system": 0.02521602, + "min": 0.16010119646, + "max": 0.16413861246, "times": [ - 0.16277804910000002, - 0.16808067310000002, - 0.16774392310000003, - 0.16716334010000003, - 0.1666300481, - 0.1652420481, - 0.17001025610000003, - 0.16542563210000003, - 0.16360338110000003, - 0.16341684010000002 + 0.16290940446000002, + 0.16121402946000002, + 0.16010119646, + 0.16375502946, + 0.16023157146, + 0.16228086246, + 0.16210815346000002, + 0.16413861246, + 0.16092473746, + 0.16280194546 ], "memory_usage_byte": [ - 161595392, - 161595392, - 161595392, - 161595392, - 161595392, - 161595392, - 161595392, - 161595392, - 161595392, - 161595392 + 162758656, + 162758656, + 162758656, + 162758656, + 162758656, + 162758656, + 162758656, + 162758656, + 162758656, + 162758656 ], "exit_codes": [ 1, @@ -232,36 +232,36 @@ }, { "command": "ty", - "mean": 0.035427485800000005, - "stddev": 0.0007542102835521342, - "median": 0.035240444100000004, - "user": 0.028987579999999995, - "system": 0.0060577999999999995, - "min": 0.034548631100000005, - "max": 0.036601048100000005, + "mean": 0.03827861246, + "stddev": 0.000862772609895564, + "median": 0.038458300460000004, + "user": 0.02939332, + "system": 0.006990720000000001, + "min": 0.03691261246000001, + "max": 0.03981540346000001, "times": [ - 0.0352121321, - 0.0348168821, - 0.034548631100000005, - 0.0346060481, - 0.0352687561, - 0.0353262571, - 0.0351688811, - 0.036469840100000005, - 0.036256382100000005, - 0.036601048100000005 + 0.03854927946, + 0.038569945460000005, + 0.038367321460000006, + 0.03981540346000001, + 0.03691294546, + 0.03691261246000001, + 0.038734070460000006, + 0.03865169646000001, + 0.03830011246000001, + 0.03797273746 ], "memory_usage_byte": [ - 161595392, - 161595392, - 161595392, - 161595392, - 161595392, - 161595392, - 161595392, - 161595392, - 161595392, - 161595392 + 162758656, + 162758656, + 162758656, + 162758656, + 162758656, + 162758656, + 162758656, + 162758656, + 162758656, + 162758656 ], "exit_codes": [ 1, @@ -278,36 +278,36 @@ }, { "command": "pyrefly", - "mean": 0.14186256500000002, - "stddev": 0.001499913721023056, - "median": 0.14202498610000003, - "user": 0.10411688, - "system": 0.038506200000000004, - "min": 0.1395668811, - "max": 0.14460909010000003, + "mean": 0.10736981656, + "stddev": 0.00044663113735869263, + "median": 0.10731365396, + "user": 0.08942022000000001, + "system": 0.01833882, + "min": 0.10668482046000001, + "max": 0.10806202846, "times": [ - 0.14460909010000003, - 0.14191642310000002, - 0.14213354910000003, - 0.14347367410000003, - 0.14041984010000003, - 0.14154350610000002, - 0.1421413401, - 0.1403877561, - 0.14243359010000003, - 0.1395668811 + 0.10806202846, + 0.10668482046000001, + 0.10733594546, + 0.10795807146, + 0.10749586246000001, + 0.10773119546, + 0.10690323746000001, + 0.10715252946, + 0.10708311246, + 0.10729136246 ], "memory_usage_byte": [ - 161595392, - 161595392, - 161595392, - 161595392, - 161595392, - 161595392, - 161595392, - 161595392, - 161595392, - 161595392 + 162758656, + 162758656, + 162758656, + 162758656, + 162758656, + 162758656, + 162758656, + 162758656, + 162758656, + 162758656 ], "exit_codes": [ 1, @@ -324,36 +324,36 @@ }, { "command": "zuban", - "mean": 0.0269738818, - "stddev": 0.002096569755519559, - "median": 0.0265086946, - "user": 0.05505557999999999, - "system": 0.008558300000000001, - "min": 0.025090548100000002, - "max": 0.0323033401, + "mean": 0.028440250060000005, + "stddev": 0.0015891223583477491, + "median": 0.028017695960000002, + "user": 0.05238022, + "system": 0.008690119999999999, + "min": 0.026924528460000003, + "max": 0.031944196460000004, "times": [ - 0.0273273401, - 0.0261684651, - 0.0323033401, - 0.027695465100000004, - 0.0274955901, - 0.025090548100000002, - 0.025491882100000002, - 0.0263039651, - 0.0251487981, - 0.0267134241 + 0.031944196460000004, + 0.026924528460000003, + 0.028953153460000002, + 0.027957487460000003, + 0.027410654460000003, + 0.02715244546, + 0.02828302946, + 0.027358404460000003, + 0.03034069646, + 0.02807790446 ], "memory_usage_byte": [ - 161595392, - 161595392, - 161595392, - 161595392, - 161595392, - 161595392, - 161595392, - 161595392, - 161595392, - 161595392 + 162758656, + 162758656, + 162758656, + 162758656, + 162758656, + 162758656, + 162758656, + 162758656, + 162758656, + 162758656 ], "exit_codes": [ 1, diff --git a/benchmarks/results/classvar_scoping.json b/benchmarks/results/classvar_scoping.json index 4a054841..8426fd5c 100644 --- a/benchmarks/results/classvar_scoping.json +++ b/benchmarks/results/classvar_scoping.json @@ -2,36 +2,36 @@ "results": [ { "command": "basilisk", - "mean": 0.015229792580000002, - "stddev": 0.00033799174582335885, - "median": 0.015096809080000002, - "user": 0.012195439999999998, - "system": 0.0022771000000000007, - "min": 0.014833413080000002, - "max": 0.015816497080000002, + "mean": 0.015284485859999999, + "stddev": 0.00039940996039858573, + "median": 0.01525877296, + "user": 0.012437319999999998, + "system": 0.00216668, + "min": 0.01478379446, + "max": 0.01579812746, "times": [ - 0.015153705080000001, - 0.015816497080000002, - 0.01565991408, - 0.015287205080000003, - 0.015003288080000001, - 0.015039913080000003, - 0.014833413080000002, - 0.015014830080000001, - 0.015559788080000003, - 0.014929372080000005 + 0.01478379446, + 0.01569446146, + 0.01579812746, + 0.01482321046, + 0.01512362746, + 0.01575850246, + 0.01481837746, + 0.01518029346, + 0.015337252459999999, + 0.015527211459999999 ], "memory_usage_byte": [ - 20447232, - 20447232, - 20463616, - 20463616, - 20496384, - 20496384, - 20496384, - 20496384, - 20496384, - 20496384 + 20037632, + 21118976, + 21118976, + 21118976, + 21118976, + 21118976, + 21118976, + 21118976, + 21118976, + 21118976 ], "exit_codes": [ 1, @@ -48,36 +48,36 @@ }, { "command": "basilisk-warm", - "mean": 0.0057537674800000015, - "stddev": 0.000388593430456335, - "median": 0.005837309080000002, - "user": 0.0032489399999999996, - "system": 0.0018800000000000004, - "min": 0.004849871080000002, - "max": 0.006311288080000003, + "mean": 0.00570763546, + "stddev": 0.0003250057248500915, + "median": 0.00561554346, + "user": 0.00330502, + "system": 0.0017855800000000002, + "min": 0.005356668459999998, + "max": 0.006399127459999999, "times": [ - 0.005987788080000002, - 0.005926455080000002, - 0.005708247080000003, - 0.005818830080000002, - 0.005954038080000003, - 0.006311288080000003, - 0.005654205080000002, - 0.005471164080000002, - 0.004849871080000002, - 0.005855788080000002 + 0.006399127459999999, + 0.005882044459999999, + 0.0055537524599999985, + 0.005459043459999998, + 0.0056942524599999985, + 0.0055902934599999995, + 0.0054250854599999995, + 0.005356668459999998, + 0.006075293459999999, + 0.00564079346 ], "memory_usage_byte": [ - 22167552, - 22167552, - 22167552, - 22167552, - 22167552, - 22167552, - 22167552, - 22167552, - 22167552, - 22167552 + 21413888, + 21413888, + 21413888, + 21413888, + 21413888, + 21413888, + 21413888, + 21413888, + 21413888, + 21413888 ], "exit_codes": [ 1, @@ -94,36 +94,36 @@ }, { "command": "pyright", - "mean": 0.62191609288, - "stddev": 0.009087982953672016, - "median": 0.62072935108, - "user": 0.8247729399999999, - "system": 0.05967729999999999, - "min": 0.60933241408, - "max": 0.64160103908, + "mean": 0.60619493996, + "stddev": 0.004835446355768636, + "median": 0.60577704446, + "user": 0.8030060200000001, + "system": 0.05601268, + "min": 0.5982557524600001, + "max": 0.61268025246, "times": [ - 0.64160103908, - 0.61957133008, - 0.62172599708, - 0.62690199708, - 0.61973270508, - 0.6289637890800001, - 0.6221578720800001, - 0.60933241408, - 0.6161921220800001, - 0.61298166308 + 0.60461966946, + 0.61268025246, + 0.60693441946, + 0.60338908546, + 0.61265875246, + 0.60707862746, + 0.5982557524600001, + 0.60342358546, + 0.61114854446, + 0.60176071046 ], "memory_usage_byte": [ - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040 + 164085760, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736 ], "exit_codes": [ 1, @@ -140,36 +140,36 @@ }, { "command": "mypy", - "mean": 0.6393957758800001, - "stddev": 0.011709260188382697, - "median": 0.63607380908, - "user": 0.57457714, - "system": 0.0603804, - "min": 0.62951183008, - "max": 0.67036753808, + "mean": 0.62028249006, + "stddev": 0.01358846497938636, + "median": 0.61393796096, + "user": 0.5593728199999999, + "system": 0.05618848, + "min": 0.6084115854600001, + "max": 0.64823887746, "times": [ - 0.63901395508, - 0.63519353808, - 0.64214566308, - 0.63394549708, - 0.67036753808, - 0.63695408008, - 0.62951183008, - 0.64240195508, - 0.63357749708, - 0.63084620508 + 0.61339246146, + 0.61342954446, + 0.6084115854600001, + 0.61820408546, + 0.61063250246, + 0.61292904446, + 0.61444637746, + 0.62145825246, + 0.64823887746, + 0.64168216946 ], "memory_usage_byte": [ - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040 + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736 ], "exit_codes": [ 1, @@ -186,36 +186,36 @@ }, { "command": "mypy-warm", - "mean": 0.16780342578000002, - "stddev": 0.002401153062719986, - "median": 0.16813362158000003, - "user": 0.13852773999999998, - "system": 0.0268679, - "min": 0.16440228908, - "max": 0.17215508008000002, + "mean": 0.17034606906, + "stddev": 0.006405054226636095, + "median": 0.17274804396000001, + "user": 0.14032912000000003, + "system": 0.027474480000000003, + "min": 0.16031596046000002, + "max": 0.18006871046, "times": [ - 0.16996708008000003, - 0.17215508008000002, - 0.16961812108000002, - 0.16809120508000003, - 0.16521370508000002, - 0.16832195508000003, - 0.16615641308000004, - 0.16593237108000003, - 0.16440228908, - 0.16817603808000003 + 0.17284150246000002, + 0.17368200246, + 0.18006871046, + 0.17490204446000002, + 0.17265458546, + 0.17524104446000002, + 0.16631833546000002, + 0.16257621146, + 0.16486029346000003, + 0.16031596046000002 ], "memory_usage_byte": [ - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040 + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736 ], "exit_codes": [ 1, @@ -232,36 +232,36 @@ }, { "command": "ty", - "mean": 0.056804971979999994, - "stddev": 0.0013439969699490858, - "median": 0.05669930958, - "user": 0.04598994, - "system": 0.007815399999999998, - "min": 0.05483533008, - "max": 0.05979366408, + "mean": 0.05741388575999999, + "stddev": 0.0014873290399186542, + "median": 0.05721927296, + "user": 0.046519920000000006, + "system": 0.00878518, + "min": 0.055030544460000004, + "max": 0.06061204446000001, "times": [ - 0.05606353908, - 0.05689741408000001, - 0.056906872080000004, - 0.05675595508, - 0.05806137208000001, - 0.056289913080000004, - 0.05580299608000001, - 0.05483533008, - 0.05664266408, - 0.05979366408 + 0.05752704446, + 0.05886271046, + 0.056571294460000005, + 0.06061204446000001, + 0.05760779446, + 0.056505918460000006, + 0.05705271046, + 0.05738583546, + 0.055030544460000004, + 0.05698296046 ], "memory_usage_byte": [ - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040 + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736 ], "exit_codes": [ 1, @@ -278,36 +278,36 @@ }, { "command": "pyrefly", - "mean": 0.16944279268000004, - "stddev": 0.0035561779898426586, - "median": 0.16784168408000003, - "user": 0.12205123999999998, - "system": 0.045438400000000004, - "min": 0.16598541408000003, - "max": 0.17625395508000002, + "mean": 0.13348356916, + "stddev": 0.0009041657465304132, + "median": 0.13326069046, + "user": 0.10884692, + "system": 0.025086480000000005, + "min": 0.13203971046000001, + "max": 0.13534233546000002, "times": [ - 0.16699412208000003, - 0.16598541408000003, - 0.17558603908000003, - 0.16764349608000004, - 0.17625395508000002, - 0.16775158008000002, - 0.16940933008000003, - 0.16925095508000002, - 0.16793178808000003, - 0.16762124708000004 + 0.13431871046000002, + 0.13203971046000001, + 0.13283504446000002, + 0.13322679446000002, + 0.13316462746000002, + 0.13309408546, + 0.13329458646, + 0.13406979446, + 0.13534233546000002, + 0.13345000246000002 ], "memory_usage_byte": [ - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040 + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736 ], "exit_codes": [ 1, @@ -324,36 +324,36 @@ }, { "command": "zuban", - "mean": 0.03158271348000001, - "stddev": 0.0006855704043111674, - "median": 0.031597475580000006, - "user": 0.05670773999999999, - "system": 0.008490900000000001, - "min": 0.03066520508, - "max": 0.03280108008, + "mean": 0.03146021916, + "stddev": 0.000682750309005512, + "median": 0.03148221096, + "user": 0.054999620000000006, + "system": 0.00906318, + "min": 0.03038516846, + "max": 0.03287541946, "times": [ - 0.03066520508, - 0.03196237208, - 0.03167278808, - 0.03117874708, - 0.031522163080000005, - 0.03280108008, - 0.030948330080000003, - 0.030843205080000005, - 0.03193595508, - 0.03229728908 + 0.031219294459999998, + 0.03158771146, + 0.03287541946, + 0.03180437746, + 0.03137671046, + 0.03179237746, + 0.03111346146, + 0.03072646046, + 0.03172121046, + 0.03038516846 ], "memory_usage_byte": [ - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040, - 164823040 + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736, + 164724736 ], "exit_codes": [ 1, diff --git a/benchmarks/results/constructors_call_init.json b/benchmarks/results/constructors_call_init.json index 41bc2d5d..6109d247 100644 --- a/benchmarks/results/constructors_call_init.json +++ b/benchmarks/results/constructors_call_init.json @@ -2,36 +2,36 @@ "results": [ { "command": "basilisk", - "mean": 0.00953001394, - "stddev": 0.00018934562341686766, - "median": 0.009520039139999999, - "user": 0.007054859999999999, - "system": 0.0019404999999999995, - "min": 0.00917318464, - "max": 0.00987935164, + "mean": 0.009072832400000001, + "stddev": 0.00013999434447469665, + "median": 0.009096132600000001, + "user": 0.00695558, + "system": 0.00163654, + "min": 0.008781861100000002, + "max": 0.009227403100000002, "times": [ - 0.00955293464, - 0.009399684640000001, - 0.00949226764, - 0.009689309640000001, - 0.00917318464, - 0.00941714264, - 0.00965618564, - 0.00987935164, - 0.00952818464, - 0.00951189364 + 0.008781861100000002, + 0.0089748611, + 0.009107070100000001, + 0.0090851951, + 0.009018195100000001, + 0.0089731111, + 0.009217029100000001, + 0.009214195100000001, + 0.009227403100000002, + 0.0091294031 ], "memory_usage_byte": [ - 14565376, - 14647296, - 14647296, - 14647296, - 14647296, - 14647296, - 14663680, - 14663680, - 14663680, - 14663680 + 14368768, + 14368768, + 14368768, + 14368768, + 14417920, + 14417920, + 14417920, + 14417920, + 14417920, + 14417920 ], "exit_codes": [ 1, @@ -48,36 +48,36 @@ }, { "command": "basilisk-warm", - "mean": 0.00451333904, - "stddev": 0.0004908719970981798, - "median": 0.00454574764, - "user": 0.00232536, - "system": 0.0016003999999999994, - "min": 0.00381797664, - "max": 0.00536205964, + "mean": 0.004094965900000001, + "stddev": 0.0002640517400594059, + "median": 0.0040382161, + "user": 0.00226648, + "system": 0.00138014, + "min": 0.0037961121000000004, + "max": 0.004677445100000001, "times": [ - 0.0038557266400000003, - 0.00460193464, - 0.00457668564, - 0.00381797664, - 0.00478385164, - 0.00508060164, - 0.00421110164, - 0.00451480964, - 0.00536205964, - 0.00432864264 + 0.003932070100000001, + 0.0038995281000000007, + 0.0042871121000000005, + 0.0039685281, + 0.0041079041, + 0.0037961121000000004, + 0.004258361100000001, + 0.004677445100000001, + 0.004153445100000001, + 0.0038691531000000006 ], "memory_usage_byte": [ - 15007744, - 15007744, - 15007744, - 15007744, - 15007744, - 15007744, - 15007744, - 15007744, - 15007744, - 15007744 + 14565376, + 14565376, + 14565376, + 14565376, + 14565376, + 14565376, + 14565376, + 14565376, + 14565376, + 14565376 ], "exit_codes": [ 1, @@ -94,36 +94,36 @@ }, { "command": "pyright", - "mean": 0.63007082664, - "stddev": 0.018413925109370094, - "median": 0.6234842271400001, - "user": 0.81984476, - "system": 0.05942650000000001, - "min": 0.61147326764, - "max": 0.67601343464, + "mean": 0.6132189159, + "stddev": 0.014663182284304955, + "median": 0.6099075076, + "user": 0.7968679799999999, + "system": 0.05220073999999999, + "min": 0.6004884031000001, + "max": 0.6516123201, "times": [ - 0.63808222664, - 0.62099247664, - 0.62396993564, - 0.62299851864, - 0.63843780964, - 0.63223160164, - 0.62016464364, - 0.67601343464, - 0.61634435164, - 0.61147326764 + 0.6145083201, + 0.6101383201, + 0.6050417781, + 0.6004884031000001, + 0.6038645701, + 0.6153833621, + 0.6179451121, + 0.6516123201, + 0.6035302781, + 0.6096766951 ], "memory_usage_byte": [ - 154468352, - 154468352, - 154468352, - 154468352, - 154468352, - 154468352, - 154468352, - 154779648, - 154779648, - 154779648 + 152764416, + 152764416, + 152961024, + 152961024, + 152961024, + 153092096, + 153092096, + 153092096, + 153092096, + 153092096 ], "exit_codes": [ 1, @@ -140,36 +140,36 @@ }, { "command": "mypy", - "mean": 0.62510308064, - "stddev": 0.010092656207054348, - "median": 0.62619043464, - "user": 0.55985446, - "system": 0.0596106, - "min": 0.60976814364, - "max": 0.64032930964, + "mean": 0.5734057993999999, + "stddev": 0.0061510281838725485, + "median": 0.5718572991, + "user": 0.51883638, + "system": 0.05129273999999999, + "min": 0.5659096541, + "max": 0.5833776541000001, "times": [ - 0.60976814364, - 0.63612451864, - 0.62644126764, - 0.62593960164, - 0.64032930964, - 0.6316140596400001, - 0.61650768464, - 0.63196564264, - 0.61736118564, - 0.61497939264 + 0.5782712361000001, + 0.5725791951, + 0.5719669451, + 0.5659096541, + 0.5717476531000001, + 0.5833776541000001, + 0.5706590291, + 0.5831070701000001, + 0.5685815281000001, + 0.5678580291 ], "memory_usage_byte": [ - 154779648, - 154779648, - 154779648, - 154779648, - 154779648, - 154779648, - 154779648, - 154779648, - 154779648, - 154779648 + 153092096, + 153092096, + 153092096, + 153092096, + 153092096, + 153092096, + 153092096, + 153092096, + 153092096, + 153092096 ], "exit_codes": [ 1, @@ -186,36 +186,36 @@ }, { "command": "mypy-warm", - "mean": 0.16825302264000003, - "stddev": 0.00284506262534397, - "median": 0.16880214314000003, - "user": 0.13768246, - "system": 0.028190700000000003, - "min": 0.16369739364000002, - "max": 0.17246076764, + "mean": 0.1560938117, + "stddev": 0.0012619283396270256, + "median": 0.1561219661, + "user": 0.13043228, + "system": 0.02353944, + "min": 0.1541431531, + "max": 0.1581830281, "times": [ - 0.17058372664000002, - 0.17052801864, - 0.16979276764, - 0.17246076764, - 0.16999256064, - 0.16682468564, - 0.16369739364000002, - 0.16563385164, - 0.16781151864000002, - 0.16520493564 + 0.1553345701, + 0.1581830281, + 0.1565486121, + 0.15749932010000003, + 0.15700598610000002, + 0.1563768621, + 0.15495761210000003, + 0.15586707010000003, + 0.1541431531, + 0.15502190310000002 ], "memory_usage_byte": [ - 154779648, - 154779648, - 154779648, - 154779648, - 154779648, - 154779648, - 154779648, - 154779648, - 154779648, - 154779648 + 153092096, + 153092096, + 153092096, + 153092096, + 153092096, + 153092096, + 153092096, + 153092096, + 153092096, + 153092096 ], "exit_codes": [ 1, @@ -232,36 +232,36 @@ }, { "command": "ty", - "mean": 0.03851606844, - "stddev": 0.0012747748385478197, - "median": 0.03840710114, - "user": 0.029727559999999997, - "system": 0.006270199999999999, - "min": 0.03647506064, - "max": 0.04078622664, + "mean": 0.0370143618, + "stddev": 0.0008428055485921932, + "median": 0.037142778099999996, + "user": 0.028703179999999995, + "system": 0.007035939999999999, + "min": 0.0356941541, + "max": 0.0385947781, "times": [ - 0.03784747664, - 0.03966926864, - 0.03938989364, - 0.04078622664, - 0.03907110164, - 0.03813768464, - 0.03802089364, - 0.03867651764, - 0.03708656064, - 0.03647506064 + 0.0356941541, + 0.037190195100000004, + 0.0371615701, + 0.037342695100000003, + 0.0377343201, + 0.036788445100000004, + 0.035894487100000004, + 0.0385947781, + 0.0366189871, + 0.0371239861 ], "memory_usage_byte": [ - 154779648, - 154779648, - 154779648, - 154779648, - 154779648, - 154779648, - 154779648, - 154779648, - 154779648, - 154779648 + 153092096, + 153092096, + 153092096, + 153092096, + 153092096, + 153092096, + 153092096, + 153092096, + 153092096, + 153092096 ], "exit_codes": [ 1, @@ -278,36 +278,36 @@ }, { "command": "pyrefly", - "mean": 0.14240425573999999, - "stddev": 0.0041877383367879455, - "median": 0.14127535164, - "user": 0.10171506, - "system": 0.0396499, - "min": 0.13840522664000002, - "max": 0.15198418464000002, + "mean": 0.1003577282, + "stddev": 0.00026676065138140563, + "median": 0.1003727571, + "user": 0.08445937999999999, + "system": 0.01712514, + "min": 0.0999328611, + "max": 0.1006784451, "times": [ - 0.14016643464, - 0.14028851764, - 0.14011776764, - 0.13840522664000002, - 0.13884260164, - 0.14716210164000001, - 0.15198418464000002, - 0.14226218564, - 0.14248489364, - 0.14232864364 + 0.10014586110000001, + 0.1001717371, + 0.1006749031, + 0.1006784451, + 0.1004421531, + 0.0999328611, + 0.10008519510000001, + 0.1005454451, + 0.1005973201, + 0.1003033611 ], "memory_usage_byte": [ - 154779648, - 154779648, - 154779648, - 154779648, - 154779648, - 154779648, - 154779648, - 154779648, - 154779648, - 154779648 + 153092096, + 153092096, + 153092096, + 153092096, + 153092096, + 153092096, + 153092096, + 153092096, + 153092096, + 153092096 ], "exit_codes": [ 1, @@ -324,36 +324,36 @@ }, { "command": "zuban", - "mean": 0.026246043440000005, - "stddev": 0.001190219616884006, - "median": 0.02598618514, - "user": 0.05168536, - "system": 0.008505200000000001, - "min": 0.025152476640000004, - "max": 0.02942164364, + "mean": 0.026237845200000005, + "stddev": 0.0005130538855302196, + "median": 0.026033570100000004, + "user": 0.05237657999999999, + "system": 0.008523239999999998, + "min": 0.0258328621, + "max": 0.027446404100000002, "times": [ - 0.025670976640000003, - 0.02575997664, - 0.025513518640000003, - 0.026421976640000004, - 0.026394726640000005, - 0.026224434640000002, - 0.026212393639999998, - 0.025152476640000004, - 0.025688310640000003, - 0.02942164364 + 0.026022778100000005, + 0.026362237100000005, + 0.0258328621, + 0.026044362100000003, + 0.026811695100000005, + 0.027446404100000002, + 0.025915654100000005, + 0.0259272361, + 0.026076987100000004, + 0.025938236100000005 ], "memory_usage_byte": [ - 154779648, - 154779648, - 154779648, - 154779648, - 154779648, - 154779648, - 154779648, - 154779648, - 154779648, - 154779648 + 153092096, + 153092096, + 153092096, + 153092096, + 153092096, + 153092096, + 153092096, + 153092096, + 153092096, + 153092096 ], "exit_codes": [ 1, diff --git a/benchmarks/results/coverage.tsv b/benchmarks/results/coverage.tsv index 0eef63a3..ba3fcb91 100644 --- a/benchmarks/results/coverage.tsv +++ b/benchmarks/results/coverage.tsv @@ -61,7 +61,7 @@ final_reassignment zuban 1 500 generics_defaults_specialization basilisk 1 560 generics_defaults_specialization pyright 1 560 generics_defaults_specialization mypy 1 560 -generics_defaults_specialization ty 1 421 +generics_defaults_specialization ty 1 420 generics_defaults_specialization pyrefly 1 560 generics_defaults_specialization zuban 1 560 literals_semantics basilisk 1 576 @@ -73,7 +73,7 @@ literals_semantics zuban 1 576 match_exhaustiveness basilisk 1 500 match_exhaustiveness pyright 0 0 match_exhaustiveness mypy 0 0 -match_exhaustiveness ty 1 500 +match_exhaustiveness ty 0 0 match_exhaustiveness pyrefly 0 0 match_exhaustiveness zuban 0 0 narrowing_typeis basilisk 1 520 @@ -85,7 +85,7 @@ narrowing_typeis zuban 1 520 newtype_definition basilisk 1 2000 newtype_definition pyright 1 2000 newtype_definition mypy 1 2000 -newtype_definition ty 0 0 +newtype_definition ty 1 2000 newtype_definition pyrefly 1 2000 newtype_definition zuban 1 2000 overloads_evaluation basilisk 1 600 @@ -127,7 +127,7 @@ typeddict_key_access zuban 1 500 typeddict_readonly_inheritance basilisk 1 500 typeddict_readonly_inheritance pyright 1 500 typeddict_readonly_inheritance mypy 1 500 -typeddict_readonly_inheritance ty 1 2 +typeddict_readonly_inheritance ty 0 0 typeddict_readonly_inheritance pyrefly 1 500 typeddict_readonly_inheritance zuban 1 500 typeddict_readonly_mutation basilisk 1 500 diff --git a/benchmarks/results/dataclasses_usage.json b/benchmarks/results/dataclasses_usage.json index a2de84c6..8c018861 100644 --- a/benchmarks/results/dataclasses_usage.json +++ b/benchmarks/results/dataclasses_usage.json @@ -2,36 +2,36 @@ "results": [ { "command": "basilisk", - "mean": 0.009248562540000001, - "stddev": 0.00017504025755240194, - "median": 0.00932561724, - "user": 0.007097740000000001, - "system": 0.0017418400000000004, - "min": 0.00892261624, - "max": 0.009392574240000002, + "mean": 0.00918396984, + "stddev": 0.00022237483858728756, + "median": 0.009159978840000001, + "user": 0.00709438, + "system": 0.0016219599999999993, + "min": 0.008749061340000001, + "max": 0.009460644340000001, "times": [ - 0.00936849124, - 0.009194992240000002, - 0.009392574240000002, - 0.009377575240000002, - 0.00895974224, - 0.009293867240000002, - 0.009357367240000001, - 0.00938065824, - 0.00892261624, - 0.009237741240000001 + 0.00911564534, + 0.008749061340000001, + 0.00938706134, + 0.009204312340000001, + 0.008976519340000001, + 0.009082644340000001, + 0.00935297834, + 0.009398519340000001, + 0.009112312340000001, + 0.009460644340000001 ], "memory_usage_byte": [ - 15941632, - 15941632, - 15958016, - 15958016, - 15958016, - 15958016, - 15958016, - 15958016, - 16023552, - 16023552 + 14761984, + 14778368, + 14778368, + 14778368, + 14778368, + 14778368, + 14778368, + 14778368, + 14778368, + 14778368 ], "exit_codes": [ 1, @@ -48,36 +48,36 @@ }, { "command": "basilisk-warm", - "mean": 0.0036610497399999996, - "stddev": 0.00023718312317665726, - "median": 0.00363492874, - "user": 0.0021545400000000004, - "system": 0.0012344399999999998, - "min": 0.00331690824, - "max": 0.0040109492400000005, + "mean": 0.003828728040000001, + "stddev": 0.000266953033547955, + "median": 0.0037294783400000004, + "user": 0.0022416800000000002, + "system": 0.0012526599999999998, + "min": 0.0036035613400000003, + "max": 0.004349186340000001, "times": [ - 0.0036716162400000003, - 0.0033867832400000003, - 0.00388015824, - 0.0038490332400000003, - 0.0035982412399999998, - 0.00348136624, - 0.0040109492400000005, - 0.0038896162399999997, - 0.00352582524, - 0.00331690824 + 0.004349186340000001, + 0.00429514434, + 0.0037688943400000004, + 0.0037258533400000007, + 0.00373310334, + 0.0036917693400000005, + 0.0036035613400000003, + 0.0036744783400000005, + 0.0036393113400000005, + 0.00380597834 ], "memory_usage_byte": [ - 16433152, - 16433152, - 16433152, - 16433152, - 16433152, - 16433152, - 16433152, - 16433152, - 16433152, - 16433152 + 15040512, + 15040512, + 15040512, + 15040512, + 15040512, + 15040512, + 15040512, + 15040512, + 15040512, + 15040512 ], "exit_codes": [ 1, @@ -94,36 +94,36 @@ }, { "command": "pyright", - "mean": 1.62775523754, - "stddev": 0.027809047173525114, - "median": 1.62355932474, - "user": 2.2609718400000003, - "system": 0.10620023999999999, - "min": 1.59274620024, - "max": 1.68391795024, + "mean": 1.57914735754, + "stddev": 0.09371442142375203, + "median": 1.54241372784, + "user": 2.1448232799999998, + "system": 0.08902296, + "min": 1.5229070203400001, + "max": 1.8333694363400002, "times": [ - 1.64004903324, - 1.64455940824, - 1.62738686624, - 1.59906095024, - 1.6197317832399998, - 1.65114590824, - 1.68391795024, - 1.59274620024, - 1.61414149224, - 1.6048127832399999 + 1.8333694363400002, + 1.56383147834, + 1.58516264534, + 1.61068722834, + 1.53254347834, + 1.52325902034, + 1.54634689434, + 1.5229070203400001, + 1.53848056134, + 1.53488581234 ], "memory_usage_byte": [ - 303742976, - 303742976, - 303742976, - 303742976, - 303742976, - 304250880, - 305004544, - 305004544, - 305004544, - 305004544 + 300777472, + 302530560, + 302530560, + 303071232, + 303087616, + 303087616, + 303087616, + 303448064, + 303448064, + 303448064 ], "exit_codes": [ 1, @@ -140,36 +140,36 @@ }, { "command": "mypy", - "mean": 0.6676474290399999, - "stddev": 0.00541762560176907, - "median": 0.66756953324, - "user": 0.6040726399999998, - "system": 0.059788640000000004, - "min": 0.66118778324, - "max": 0.67886111624, + "mean": 0.6406054616400001, + "stddev": 0.02060292003133836, + "median": 0.6343314573400001, + "user": 0.57887138, + "system": 0.05608425999999998, + "min": 0.62053843634, + "max": 0.68743414534, "times": [ - 0.6636479082400001, - 0.6719034502400001, - 0.66375220024, - 0.66597053324, - 0.67886111624, - 0.66209765824, - 0.66925749124, - 0.66916853324, - 0.66118778324, - 0.67062761624 + 0.6400995613400001, + 0.62053843634, + 0.6259345193400001, + 0.6553011453400001, + 0.6285633533400001, + 0.6513303533400001, + 0.68743414534, + 0.6469853533400001, + 0.6257463123400001, + 0.6241214363400001 ], "memory_usage_byte": [ - 305004544, - 305004544, - 305004544, - 305004544, - 305004544, - 305004544, - 305004544, - 305004544, - 305004544, - 305004544 + 303448064, + 303448064, + 303448064, + 303448064, + 303448064, + 303448064, + 303448064, + 303448064, + 303448064, + 303448064 ], "exit_codes": [ 1, @@ -186,36 +186,36 @@ }, { "command": "mypy-warm", - "mean": 0.17004734174, - "stddev": 0.0036104948771879903, - "median": 0.16914188724, - "user": 0.13949154, - "system": 0.028288039999999997, - "min": 0.16591499224, - "max": 0.17542453324, + "mean": 0.15829105774000002, + "stddev": 0.0027674030319975724, + "median": 0.15778547884000002, + "user": 0.13176207999999998, + "system": 0.02434196, + "min": 0.15566877034, + "max": 0.16542106234, "times": [ - 0.16894690824, - 0.16702674224000003, - 0.16933686624000002, - 0.16591499224, - 0.16870915824000002, - 0.16617815824, - 0.16957370024, - 0.17415190824000001, - 0.17542453324, - 0.17521045024 + 0.15784856234000003, + 0.15566877034, + 0.15896314434000003, + 0.15887614534000002, + 0.15570518734000002, + 0.15772239534000002, + 0.15857147834000002, + 0.16542106234, + 0.15732931234000003, + 0.15680451934000003 ], "memory_usage_byte": [ - 305004544, - 305004544, - 305004544, - 305004544, - 305004544, - 305004544, - 305004544, - 305004544, - 305004544, - 305004544 + 303448064, + 303448064, + 303448064, + 303448064, + 303448064, + 303448064, + 303448064, + 303448064, + 303448064, + 303448064 ], "exit_codes": [ 1, @@ -232,36 +232,36 @@ }, { "command": "ty", - "mean": 0.058636720740000016, - "stddev": 0.0009128476087684523, - "median": 0.058324783240000004, - "user": 0.05003863999999999, - "system": 0.00642964, - "min": 0.05748690824, - "max": 0.06043170024000001, + "mean": 0.05790774074000001, + "stddev": 0.001328313574113198, + "median": 0.05773860334000001, + "user": 0.04889378, + "system": 0.00715956, + "min": 0.05596072834000001, + "max": 0.05952102034000001, "times": [ - 0.059238658240000006, - 0.06043170024000001, - 0.05936420024000001, - 0.05821874224, - 0.05926578324000001, - 0.058146783240000006, - 0.05766665824000001, - 0.05748690824, - 0.05811694924000001, - 0.058430824240000005 + 0.056557353340000005, + 0.05947876934000001, + 0.05952102034000001, + 0.05873435334, + 0.059341686340000005, + 0.05762006134000001, + 0.056386228340000004, + 0.057745103340000006, + 0.05773210334000001, + 0.05596072834000001 ], "memory_usage_byte": [ - 305004544, - 305004544, - 305004544, - 305004544, - 305004544, - 305004544, - 305004544, - 305004544, - 305004544, - 305004544 + 303448064, + 303448064, + 303448064, + 303448064, + 303448064, + 303448064, + 303448064, + 303448064, + 303448064, + 303448064 ], "exit_codes": [ 1, @@ -278,36 +278,36 @@ }, { "command": "pyrefly", - "mean": 0.21525690414, - "stddev": 0.004737394605788632, - "median": 0.21322569974, - "user": 0.17201794, - "system": 0.04105664, - "min": 0.21096386724000002, - "max": 0.22537090824000003, + "mean": 0.17050477824000004, + "stddev": 0.001281731553204615, + "median": 0.17006181184000002, + "user": 0.15433998, + "system": 0.01697736, + "min": 0.16947135334000002, + "max": 0.17387397834, "times": [ - 0.21735449124, - 0.21396674124, - 0.21290140824, - 0.21212720024, - 0.22160032524, - 0.22537090824000003, - 0.21096386724000002, - 0.21354999124000001, - 0.21235940824000002, - 0.21237470024000002 + 0.16980322834000003, + 0.16991914434000002, + 0.17387397834, + 0.16947135334000002, + 0.17124635334000002, + 0.17058818634000003, + 0.17009839534, + 0.16979743634000002, + 0.17002522834000003, + 0.17022447834 ], "memory_usage_byte": [ - 305004544, - 305004544, - 305004544, - 305004544, - 305004544, - 305004544, - 305004544, - 305004544, - 305004544, - 305004544 + 303448064, + 303448064, + 303448064, + 303448064, + 303448064, + 303448064, + 303448064, + 303448064, + 303448064, + 303448064 ], "exit_codes": [ 1, @@ -324,36 +324,36 @@ }, { "command": "zuban", - "mean": 0.056884658040000015, - "stddev": 0.0007386016112507322, - "median": 0.05666734574, - "user": 0.08354933999999999, - "system": 0.00872384, - "min": 0.05594415824000001, - "max": 0.05788970024000001, + "mean": 0.05512672844000001, + "stddev": 0.0010212064683410777, + "median": 0.05496352034, + "user": 0.08003668, + "system": 0.00882176, + "min": 0.05410376934, + "max": 0.057783270340000005, "times": [ - 0.05632611624000001, - 0.057868533240000006, - 0.057873491240000005, - 0.056656449240000004, - 0.05621799124000001, - 0.05667824224, - 0.05594415824000001, - 0.05788970024000001, - 0.056424824240000004, - 0.056967074240000005 + 0.054963437340000004, + 0.054373895340000006, + 0.05536452034000001, + 0.055427395340000005, + 0.05410376934, + 0.05477652034, + 0.05499706134000001, + 0.05496360334000001, + 0.05451381134, + 0.057783270340000005 ], "memory_usage_byte": [ - 305004544, - 305004544, - 305004544, - 305004544, - 305004544, - 305004544, - 305004544, - 305004544, - 305004544, - 305004544 + 303448064, + 303448064, + 303448064, + 303448064, + 303448064, + 303448064, + 303448064, + 303448064, + 303448064, + 303448064 ], "exit_codes": [ 1, diff --git a/benchmarks/results/dict_key_hashability.json b/benchmarks/results/dict_key_hashability.json index 1b1f17b4..33ce08a5 100644 --- a/benchmarks/results/dict_key_hashability.json +++ b/benchmarks/results/dict_key_hashability.json @@ -2,36 +2,36 @@ "results": [ { "command": "basilisk", - "mean": 0.01192096006, - "stddev": 0.00023235609991564918, - "median": 0.01193253886, - "user": 0.00936608, - "system": 0.00199604, - "min": 0.01161101836, - "max": 0.01229164336, + "mean": 0.011418759900000002, + "stddev": 0.00020632433853601015, + "median": 0.011413589000000002, + "user": 0.00938396, + "system": 0.00147078, + "min": 0.011165902000000002, + "max": 0.011757360000000001, "times": [ - 0.01163210136, - 0.012146185359999999, - 0.01161101836, - 0.01174035236, - 0.01178701836, - 0.01217526836, - 0.01190835136, - 0.01195672636, - 0.01196093536, - 0.01229164336 + 0.011358818000000001, + 0.011468360000000002, + 0.011654026000000001, + 0.011757360000000001, + 0.011223860000000002, + 0.011254110000000001, + 0.011543693, + 0.011165902000000002, + 0.011208193000000002, + 0.011553277 ], "memory_usage_byte": [ - 19677184, - 20037632, - 20037632, - 20037632, - 20037632, - 20037632, - 20037632, - 20037632, - 20037632, - 20037632 + 18497536, + 18563072, + 18563072, + 18563072, + 18694144, + 18694144, + 18694144, + 18694144, + 18694144, + 18694144 ], "exit_codes": [ 1, @@ -48,36 +48,36 @@ }, { "command": "basilisk-warm", - "mean": 0.005190622559999999, - "stddev": 0.00022140854276894232, - "median": 0.005138684859999999, - "user": 0.00314828, - "system": 0.0016103399999999994, - "min": 0.00490751836, - "max": 0.005535101359999999, + "mean": 0.0046831517, + "stddev": 0.00028822131671803506, + "median": 0.0046407555000000005, + "user": 0.0030681599999999995, + "system": 0.00120958, + "min": 0.004336068, + "max": 0.005393069, "times": [ - 0.00490751836, - 0.00511747636, - 0.005535101359999999, - 0.005398185359999998, - 0.004926018359999999, - 0.0049929773599999994, - 0.005449560359999999, - 0.005302018359999999, - 0.005135726359999999, - 0.00514164336 + 0.004659318000000001, + 0.005393069, + 0.004668277, + 0.004873902000000001, + 0.00458861, + 0.004692652, + 0.004549901, + 0.004336068, + 0.004622193, + 0.004447527 ], "memory_usage_byte": [ - 20742144, - 20742144, - 20742144, - 20742144, - 20742144, - 20742144, - 20742144, - 20742144, - 20742144, - 20742144 + 19054592, + 19054592, + 19054592, + 19054592, + 19054592, + 19054592, + 19054592, + 19054592, + 19054592, + 19054592 ], "exit_codes": [ 1, @@ -94,36 +94,36 @@ }, { "command": "pyright", - "mean": 0.54154610596, - "stddev": 0.010496510646762968, - "median": 0.53786428936, - "user": 0.74178368, - "system": 0.060439339999999994, - "min": 0.53258056036, - "max": 0.56432147736, + "mean": 0.5173818683, + "stddev": 0.004813978465582007, + "median": 0.5179346725, + "user": 0.7139509599999998, + "system": 0.05257638, + "min": 0.506525152, + "max": 0.5245672350000001, "times": [ - 0.55569926836, - 0.54261930936, - 0.53258056036, - 0.53790139336, - 0.53294714336, - 0.54183247636, - 0.53543422736, - 0.56432147736, - 0.53782718536, - 0.53429801836 + 0.5190779850000001, + 0.5143850680000001, + 0.5164063180000001, + 0.506525152, + 0.5210862350000001, + 0.5245672350000001, + 0.5203187770000001, + 0.5177530680000001, + 0.5155825680000001, + 0.5181162770000001 ], "memory_usage_byte": [ - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112 + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472 ], "exit_codes": [ 1, @@ -140,36 +140,36 @@ }, { "command": "mypy", - "mean": 0.6385659432599999, - "stddev": 0.011334392032726178, - "median": 0.63649493486, - "user": 0.57231218, - "system": 0.06037484, - "min": 0.62657943436, - "max": 0.66464185136, + "mean": 0.5925750225, + "stddev": 0.005588280651366137, + "median": 0.5907365475000002, + "user": 0.5361721599999999, + "system": 0.05297278, + "min": 0.5869743180000001, + "max": 0.6040586520000001, "times": [ - 0.63439893536, - 0.65018472736, - 0.66464185136, - 0.63321776836, - 0.63943414336, - 0.62979668436, - 0.62657943436, - 0.63859093436, - 0.63884501836, - 0.62996993536 + 0.5869743180000001, + 0.5900224010000001, + 0.588839693, + 0.5890077760000001, + 0.5948153190000001, + 0.5932263180000001, + 0.591450694, + 0.5875339850000001, + 0.6040586520000001, + 0.5998210690000001 ], "memory_usage_byte": [ - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112 + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472 ], "exit_codes": [ 0, @@ -186,36 +186,36 @@ }, { "command": "mypy-warm", - "mean": 0.16873068066000002, - "stddev": 0.0027810904910740065, - "median": 0.16798545586000002, - "user": 0.13834958, - "system": 0.027679839999999994, - "min": 0.16588197636, - "max": 0.17414972636, + "mean": 0.1573879474, + "stddev": 0.0017534098058863729, + "median": 0.15692271400000002, + "user": 0.13047026, + "system": 0.02448408, + "min": 0.15464102700000001, + "max": 0.16088265100000002, "times": [ - 0.16860860136, - 0.17286760136, - 0.16823581036000002, - 0.16588197636, - 0.16975385136, - 0.16632831036, - 0.16672618436, - 0.16701964336, - 0.16773510136, - 0.17414972636 + 0.159601985, + 0.16088265100000002, + 0.157922652, + 0.15737277600000002, + 0.157056151, + 0.156307152, + 0.156789277, + 0.15670852600000001, + 0.15464102700000001, + 0.156597277 ], "memory_usage_byte": [ - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112 + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472 ], "exit_codes": [ 0, @@ -232,36 +232,36 @@ }, { "command": "ty", - "mean": 0.03905403925999999, - "stddev": 0.0013334368351555527, - "median": 0.03872437286, - "user": 0.03000778, - "system": 0.00658604, - "min": 0.03710489336, - "max": 0.041500726360000004, + "mean": 0.039126730600000006, + "stddev": 0.002243001477755801, + "median": 0.038626547, + "user": 0.029667859999999997, + "system": 0.007154980000000001, + "min": 0.037158818, + "max": 0.044780318, "times": [ - 0.04065635136, - 0.041500726360000004, - 0.03836114336, - 0.03710489336, - 0.03815872736, - 0.03790860136, - 0.03966856036, - 0.03873881036, - 0.03870993536, - 0.03973264336 + 0.038734526, + 0.037158818, + 0.037536818, + 0.037362485, + 0.039092943, + 0.039475985, + 0.03805961, + 0.044780318, + 0.040547235, + 0.038518568 ], "memory_usage_byte": [ - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112 + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472 ], "exit_codes": [ 0, @@ -278,36 +278,36 @@ }, { "command": "pyrefly", - "mean": 0.14338836006000003, - "stddev": 0.0038820710043912573, - "median": 0.14231435136, - "user": 0.10255168, - "system": 0.03956004, - "min": 0.14092297636, - "max": 0.15401485236, + "mean": 0.10351735150000001, + "stddev": 0.0017137606890365884, + "median": 0.10446248450000001, + "user": 0.08606116, + "system": 0.017894680000000003, + "min": 0.099965569, + "max": 0.104927693, "times": [ - 0.14092297636, - 0.14096689336, - 0.15401485236, - 0.14347243536, - 0.14267172636, - 0.14409126836, - 0.14286235236, - 0.14195697636000001, - 0.14152839336, - 0.14139572636 + 0.10152736000000001, + 0.104737693, + 0.10449456800000001, + 0.104697735, + 0.104430401, + 0.104528443, + 0.104927693, + 0.10379511000000001, + 0.10206894300000001, + 0.099965569 ], "memory_usage_byte": [ - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112 + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472 ], "exit_codes": [ 0, @@ -324,36 +324,36 @@ }, { "command": "zuban", - "mean": 0.030254868159999997, - "stddev": 0.0005968774536798066, - "median": 0.03030897636, - "user": 0.05551328, - "system": 0.00808244, - "min": 0.029365935359999998, - "max": 0.03123439336, + "mean": 0.029865739300000006, + "stddev": 0.0004312740603949732, + "median": 0.029888110500000002, + "user": 0.05433175999999999, + "system": 0.008847180000000001, + "min": 0.029332944, + "max": 0.030741193, "times": [ - 0.03054218536, - 0.029604643360000002, - 0.02976168436, - 0.03067368436, - 0.03049355936, - 0.029365935359999998, - 0.02991026836, - 0.03123439336, - 0.03083793436, - 0.03012439336 + 0.029383026, + 0.030221318000000004, + 0.030741193, + 0.029332944, + 0.030003943999999998, + 0.029989819, + 0.029786402000000003, + 0.030019569000000003, + 0.02943586, + 0.029743317999999998 ], "memory_usage_byte": [ - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112, - 169050112 + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472, + 167657472 ], "exit_codes": [ 0, diff --git a/benchmarks/results/enums_member_values.json b/benchmarks/results/enums_member_values.json index a6319289..a51a4dde 100644 --- a/benchmarks/results/enums_member_values.json +++ b/benchmarks/results/enums_member_values.json @@ -2,36 +2,36 @@ "results": [ { "command": "basilisk", - "mean": 0.00781365812, - "stddev": 0.00023727649366424815, - "median": 0.007800062020000001, - "user": 0.0054369, - "system": 0.0018442, - "min": 0.007323395520000001, - "max": 0.008117562520000001, + "mean": 0.007170187640000001, + "stddev": 0.000285775064099644, + "median": 0.00703767104, + "user": 0.00527272, + "system": 0.00151906, + "min": 0.0068966500400000005, + "max": 0.007651442040000001, "times": [ - 0.008117562520000001, - 0.007735770520000001, - 0.007784603520000001, - 0.00809347852, - 0.007703187520000002, - 0.0076361875200000005, - 0.00795331252, - 0.00781552052, - 0.007973562520000001, - 0.007323395520000001 + 0.00736660804, + 0.007021650040000001, + 0.00690594204, + 0.0068966500400000005, + 0.0069646080400000004, + 0.007281234040000001, + 0.00695923404, + 0.00705369204, + 0.00760081604, + 0.007651442040000001 ], "memory_usage_byte": [ - 14417920, - 14417920, - 14417920, - 14417920, - 14417920, - 14417920, - 14417920, - 14417920, - 14417920, - 14417920 + 14303232, + 14303232, + 14303232, + 14303232, + 14303232, + 14303232, + 14303232, + 14303232, + 14303232, + 14303232 ], "exit_codes": [ 1, @@ -48,36 +48,36 @@ }, { "command": "basilisk-warm", - "mean": 0.00408177092, - "stddev": 0.00039313790573543576, - "median": 0.00402968752, - "user": 0.0022340000000000007, - "system": 0.0014221000000000001, - "min": 0.0035927715200000003, - "max": 0.00478552052, + "mean": 0.0037060958400000005, + "stddev": 0.0001405939557004892, + "median": 0.0036835665400000005, + "user": 0.0021838200000000004, + "system": 0.00120186, + "min": 0.0034934000400000005, + "max": 0.003971817040000001, "times": [ - 0.0035927715200000003, - 0.00478552052, - 0.0039268125200000005, - 0.0037360215200000005, - 0.00457606252, - 0.00416331252, - 0.004132562520000001, - 0.00433714552, - 0.003875062520000001, - 0.0036924375200000003 + 0.0037161500400000004, + 0.003971817040000001, + 0.0036591500400000006, + 0.0037042750400000006, + 0.00371052504, + 0.00364640004, + 0.0034934000400000005, + 0.0035889000400000006, + 0.0036628580400000004, + 0.0039074830400000005 ], "memory_usage_byte": [ - 14893056, - 14893056, - 14893056, - 14893056, - 14893056, - 14893056, - 14893056, - 14893056, - 14893056, - 14893056 + 14565376, + 14565376, + 14565376, + 14565376, + 14565376, + 14565376, + 14565376, + 14565376, + 14565376, + 14565376 ], "exit_codes": [ 1, @@ -94,36 +94,36 @@ }, { "command": "pyright", - "mean": 0.5864712792200001, - "stddev": 0.009776989127392943, - "median": 0.5839282295200001, - "user": 0.7482036, - "system": 0.057574400000000005, - "min": 0.5767360625200001, - "max": 0.61054468752, + "mean": 0.5698245834400002, + "stddev": 0.012212488693178007, + "median": 0.5685007125400001, + "user": 0.7311231199999998, + "system": 0.05437776000000001, + "min": 0.5515894840400001, + "max": 0.5910415670400001, "times": [ - 0.58077727152, - 0.57872464552, - 0.5903272285200001, - 0.5855791045200001, - 0.5822773545200001, - 0.5767360625200001, - 0.5810026875200001, - 0.5919780625200001, - 0.58676568752, - 0.61054468752 + 0.5647761080400001, + 0.5725143170400001, + 0.5910415670400001, + 0.5872952330400001, + 0.5573352750400001, + 0.5676215250400001, + 0.5630417750400001, + 0.5515894840400001, + 0.57365065004, + 0.56937990004 ], "memory_usage_byte": [ - 154976256, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456 + 154271744, + 154697728, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984 ], "exit_codes": [ 1, @@ -140,36 +140,36 @@ }, { "command": "mypy", - "mean": 0.5934628958200001, - "stddev": 0.007354575510719818, - "median": 0.5909762915200001, - "user": 0.5319910999999999, - "system": 0.057366799999999996, - "min": 0.5856007295200001, - "max": 0.6069295625200001, + "mean": 0.5496547042400002, + "stddev": 0.0076436239026315056, + "median": 0.5478600665400001, + "user": 0.49518392, + "system": 0.05047035999999999, + "min": 0.5419914830400001, + "max": 0.5697800660400001, "times": [ - 0.59060802052, - 0.60608202052, - 0.5878092705200001, - 0.59134456252, - 0.5915499375200001, - 0.59003647952, - 0.5890287285200001, - 0.59563964652, - 0.6069295625200001, - 0.5856007295200001 + 0.5476001500400001, + 0.5510338170400001, + 0.5697800660400001, + 0.5504316090400001, + 0.5470901910400001, + 0.54541556704, + 0.5506657340400001, + 0.5481199830400001, + 0.5444184420400001, + 0.5419914830400001 ], "memory_usage_byte": [ - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456 + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984 ], "exit_codes": [ 1, @@ -186,36 +186,36 @@ }, { "command": "mypy-warm", - "mean": 0.16746351271999999, - "stddev": 0.0017422627512932549, - "median": 0.16793106302, - "user": 0.1376192, - "system": 0.027343299999999997, - "min": 0.16454627052, - "max": 0.16927306252, + "mean": 0.15587089584, + "stddev": 0.0007876044638320522, + "median": 0.15593248304000001, + "user": 0.13015901999999996, + "system": 0.02375656, + "min": 0.15448106704, + "max": 0.15676306704, "times": [ - 0.16697610452, - 0.16730589652, - 0.16900435452, - 0.16879389652, - 0.16905522952, - 0.16454627052, - 0.16585293752, - 0.16527114552, - 0.16855622952, - 0.16927306252 + 0.15577748304, + 0.15652844204, + 0.15551144104, + 0.15643548304, + 0.15676306704, + 0.15477215004, + 0.15608748304, + 0.15566348404, + 0.15448106704, + 0.15668885804 ], "memory_usage_byte": [ - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456 + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984 ], "exit_codes": [ 1, @@ -232,36 +232,36 @@ }, { "command": "ty", - "mean": 0.04311120402000001, - "stddev": 0.0016602292953381296, - "median": 0.042946291520000006, - "user": 0.03343719999999999, - "system": 0.006898999999999998, - "min": 0.040470437520000006, - "max": 0.04684656252, + "mean": 0.04015831674, + "stddev": 0.0005466521301620236, + "median": 0.040219504540000006, + "user": 0.03163962, + "system": 0.006923759999999999, + "min": 0.03882281604, + "max": 0.04068923304, "times": [ - 0.040470437520000006, - 0.04254464552000001, - 0.04208135352, - 0.042354603520000006, - 0.04359027152000001, - 0.043347937520000004, - 0.044014687520000005, - 0.04684656252, - 0.043547103520000005, - 0.042314437520000005 + 0.04060494204, + 0.04068923304, + 0.04028302504, + 0.040137525040000004, + 0.03991373404, + 0.04050752504, + 0.04056227504, + 0.03990610804, + 0.04015598404, + 0.03882281604 ], "memory_usage_byte": [ - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456 + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984 ], "exit_codes": [ 1, @@ -278,36 +278,36 @@ }, { "command": "pyrefly", - "mean": 0.14603004181999998, - "stddev": 0.0036381156707084895, - "median": 0.14542806202, - "user": 0.10374449999999999, - "system": 0.04058429999999999, - "min": 0.14162539652, - "max": 0.15177722952, + "mean": 0.10033548304000002, + "stddev": 0.0006325641258300311, + "median": 0.10020942054000001, + "user": 0.08456062, + "system": 0.016965760000000003, + "min": 0.09935002504000001, + "max": 0.10130198304000002, "times": [ - 0.14514064552, - 0.14754614652, - 0.14803452052, - 0.15155468752, - 0.14571547852, - 0.14362597952, - 0.15177722952, - 0.14232260452, - 0.14295772952, - 0.14162539652 + 0.09991223304, + 0.10067156604000001, + 0.10012652504000001, + 0.09990760804000001, + 0.10070623304000001, + 0.10123294104000001, + 0.09935002504000001, + 0.09985340004000001, + 0.10029231604000001, + 0.10130198304000002 ], "memory_usage_byte": [ - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456 + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984 ], "exit_codes": [ 1, @@ -324,36 +324,36 @@ }, { "command": "zuban", - "mean": 0.02666313752, - "stddev": 0.001138469963929074, - "median": 0.02635864602, - "user": 0.05246569999999999, - "system": 0.0085479, - "min": 0.02501664552, - "max": 0.02852652152, + "mean": 0.02634561254, + "stddev": 0.0010529544845709405, + "median": 0.026154962540000003, + "user": 0.05154601999999999, + "system": 0.008532360000000001, + "min": 0.024664275040000003, + "max": 0.027965234040000002, "times": [ - 0.02737456252, - 0.02852652152, - 0.028001728520000005, - 0.02592760452, - 0.025644645520000002, - 0.026183562520000002, - 0.025888312520000005, - 0.02653372952, - 0.02753406252, - 0.02501664552 + 0.025386525040000003, + 0.027965234040000002, + 0.027421066040000004, + 0.02565840004, + 0.02662035804, + 0.025838525040000004, + 0.02759181704, + 0.026258150040000004, + 0.024664275040000003, + 0.026051775040000003 ], "memory_usage_byte": [ - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456, - 155795456 + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984, + 156073984 ], "exit_codes": [ 1, diff --git a/benchmarks/results/final_reassignment.json b/benchmarks/results/final_reassignment.json index c190953c..2a684840 100644 --- a/benchmarks/results/final_reassignment.json +++ b/benchmarks/results/final_reassignment.json @@ -2,36 +2,36 @@ "results": [ { "command": "basilisk", - "mean": 0.006933157420000001, - "stddev": 0.00047949352972912986, - "median": 0.00684485282, - "user": 0.0047339800000000005, - "system": 0.0017114600000000002, - "min": 0.006487228320000001, - "max": 0.00799322832, + "mean": 0.00624185656, + "stddev": 0.00010999091586273039, + "median": 0.006221156660000001, + "user": 0.004575919999999999, + "system": 0.0013763, + "min": 0.00613453166, + "max": 0.006531447660000001, "times": [ - 0.00698331132, - 0.00651985332, - 0.006579145320000001, - 0.006527020320000001, - 0.007401728320000001, - 0.006826311320000001, - 0.00799322832, - 0.00686339432, - 0.006487228320000001, - 0.007150353320000001 + 0.006222031660000001, + 0.006215031660000001, + 0.006531447660000001, + 0.006220281660000001, + 0.00616119766, + 0.006171573660000001, + 0.00613453166, + 0.00625065666, + 0.00625740666, + 0.0062544066600000005 ], "memory_usage_byte": [ - 13746176, - 13795328, - 13795328, - 13795328, - 13795328, - 13795328, - 13795328, - 13795328, - 13795328, - 13795328 + 13516800, + 13516800, + 13516800, + 13516800, + 13516800, + 13533184, + 13533184, + 13533184, + 13533184, + 13533184 ], "exit_codes": [ 1, @@ -48,36 +48,36 @@ }, { "command": "basilisk-warm", - "mean": 0.00396519082, - "stddev": 0.0003630184049439581, - "median": 0.0039512073200000004, - "user": 0.00220758, - "system": 0.0013456600000000003, - "min": 0.00342377032, - "max": 0.00447972832, + "mean": 0.003675019060000001, + "stddev": 0.0001596264080478749, + "median": 0.0036629691600000005, + "user": 0.00215922, + "system": 0.0011876999999999999, + "min": 0.0035026986600000002, + "max": 0.0040002816600000005, "times": [ - 0.0041111453200000005, - 0.0037207703200000004, - 0.00434097832, - 0.00391714532, - 0.0037869783200000003, - 0.00437343632, - 0.00342377032, - 0.0035126863200000006, - 0.00447972832, - 0.00398526932 + 0.0035026986600000002, + 0.00361619866, + 0.0037994896600000005, + 0.0037212396600000003, + 0.003709739660000001, + 0.003514489660000001, + 0.0040002816600000005, + 0.003790448660000001, + 0.003554072660000001, + 0.0035415316600000005 ], "memory_usage_byte": [ - 13975552, - 13975552, - 13975552, - 13975552, - 13975552, - 13975552, - 13975552, - 13975552, - 13975552, - 13975552 + 13778944, + 13778944, + 13778944, + 13778944, + 13778944, + 13778944, + 13778944, + 13778944, + 13778944, + 13778944 ], "exit_codes": [ 1, @@ -94,36 +94,36 @@ }, { "command": "pyright", - "mean": 0.46750116172, - "stddev": 0.004035600922867112, - "median": 0.46725143632000005, - "user": 0.5251069800000001, - "system": 0.050800460000000006, - "min": 0.46157777032, - "max": 0.4759711863200001, + "mean": 0.47317310266, + "stddev": 0.03297155997818876, + "median": 0.46328782366, + "user": 0.5094137199999998, + "system": 0.050467899999999996, + "min": 0.45281678166, + "max": 0.56281957366, "times": [ - 0.4759711863200001, - 0.46157777032, - 0.4640245203200001, - 0.46594535332000003, - 0.46691122832000004, - 0.46759164432000005, - 0.47011035332000006, - 0.46974577032000003, - 0.46894293732000003, - 0.46419085332000004 + 0.45633894866, + 0.45281678166, + 0.46616865666, + 0.46040699066, + 0.48430615666000004, + 0.45508540666, + 0.45444628166, + 0.56281957366, + 0.46727440666000003, + 0.47206782366000005 ], "memory_usage_byte": [ - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912 + 145670144, + 145670144, + 145670144, + 145670144, + 145670144, + 145833984, + 145833984, + 145833984, + 145833984, + 145833984 ], "exit_codes": [ 1, @@ -140,36 +140,36 @@ }, { "command": "mypy", - "mean": 0.5905941452200001, - "stddev": 0.011428340903530242, - "median": 0.5905345828199999, - "user": 0.52742398, - "system": 0.05883435999999999, - "min": 0.57657810332, - "max": 0.60850227032, + "mean": 0.54527670646, + "stddev": 0.007085068144939473, + "median": 0.54223557316, + "user": 0.49038872, + "system": 0.0521257, + "min": 0.53904311466, + "max": 0.56196744866, "times": [ - 0.58908747832, - 0.60558518732, - 0.60850227032, - 0.58204285332, - 0.59403747832, - 0.59198168732, - 0.57657810332, - 0.58059489532, - 0.59966835332, - 0.57786314532 + 0.5496796566600001, + 0.5431241976600001, + 0.54661353166, + 0.54099507266, + 0.54134694866, + 0.56196744866, + 0.54981548966, + 0.53904311466, + 0.5396574066600001, + 0.54052419766 ], "memory_usage_byte": [ - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912 + 145833984, + 145833984, + 145833984, + 145833984, + 145833984, + 145833984, + 145833984, + 145833984, + 145833984, + 145833984 ], "exit_codes": [ 1, @@ -186,36 +186,36 @@ }, { "command": "mypy-warm", - "mean": 0.16685818232, - "stddev": 0.0029560320536390267, - "median": 0.16780083232, - "user": 0.13747917999999998, - "system": 0.026918559999999998, - "min": 0.16262693632, - "max": 0.17074256132, + "mean": 0.15600893996, + "stddev": 0.0013215949678217584, + "median": 0.15566623966, + "user": 0.13012472, + "system": 0.0239023, + "min": 0.15475490666, + "max": 0.15891444866, "times": [ - 0.16326081132, - 0.16861981132, - 0.16768285332000002, - 0.16805631232, - 0.16791881132, - 0.17068635332, - 0.16397901932, - 0.16500835332, - 0.16262693632, - 0.17074256132 + 0.15568828166, + 0.15608448966000002, + 0.15494053166000002, + 0.15475490666, + 0.15564419766, + 0.15891444866, + 0.15770949066, + 0.15599844766, + 0.15502457366, + 0.15533003166 ], "memory_usage_byte": [ - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912 + 145833984, + 145833984, + 145833984, + 145833984, + 145833984, + 145833984, + 145833984, + 145833984, + 145833984, + 145833984 ], "exit_codes": [ 1, @@ -232,36 +232,36 @@ }, { "command": "ty", - "mean": 0.02673250352, - "stddev": 0.0006105247916787658, - "median": 0.026819124320000002, - "user": 0.01912058, - "system": 0.00576756, - "min": 0.02555018732, - "max": 0.02753785332, + "mean": 0.026335964860000005, + "stddev": 0.0009116057187929202, + "median": 0.026484989660000003, + "user": 0.018772019999999997, + "system": 0.006270300000000001, + "min": 0.02471228166, + "max": 0.027757281660000004, "times": [ - 0.02753785332, - 0.02723322832, - 0.02711647832, - 0.02659056232, - 0.027037478320000002, - 0.027204895320000002, - 0.02607697832, - 0.02555018732, - 0.02637660332, - 0.02660077032 + 0.026425073660000003, + 0.02711344766, + 0.027757281660000004, + 0.026438697660000003, + 0.026563406660000004, + 0.025985531660000002, + 0.02471228166, + 0.026531281660000003, + 0.025012198660000002, + 0.026820447660000003 ], "memory_usage_byte": [ - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912 + 145833984, + 145833984, + 145833984, + 145833984, + 145833984, + 145833984, + 145833984, + 145833984, + 145833984, + 145833984 ], "exit_codes": [ 1, @@ -278,36 +278,36 @@ }, { "command": "pyrefly", - "mean": 0.13764286992, - "stddev": 0.002254047459198092, - "median": 0.13732712432, - "user": 0.09789298, - "system": 0.038586859999999994, - "min": 0.13527227032, - "max": 0.14189914432, + "mean": 0.09570878996000001, + "stddev": 0.000865678416893945, + "median": 0.09541409366, + "user": 0.08020371999999999, + "system": 0.016812199999999996, + "min": 0.09480115666000001, + "max": 0.09795465666, "times": [ - 0.13723827032, - 0.13602072832, - 0.13642147832, - 0.13755622832, - 0.13527227032, - 0.14137531132, - 0.14189914432, - 0.13750893632, - 0.13741597832, - 0.13572035332000001 + 0.09529765666000001, + 0.09480115666000001, + 0.09554403166, + 0.09531057366000001, + 0.09531494866000001, + 0.09600065666, + 0.09543698966, + 0.09539119766000001, + 0.09603603166, + 0.09795465666 ], "memory_usage_byte": [ - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912 + 145833984, + 145833984, + 145833984, + 145833984, + 145833984, + 145833984, + 145833984, + 145833984, + 145833984, + 145833984 ], "exit_codes": [ 1, @@ -324,36 +324,36 @@ }, { "command": "zuban", - "mean": 0.023993528320000002, - "stddev": 0.0007991544321835844, - "median": 0.02389093682, - "user": 0.051350379999999994, - "system": 0.00810136, - "min": 0.02291718632, - "max": 0.025400769320000002, + "mean": 0.023328381860000004, + "stddev": 0.0008388106599170039, + "median": 0.023478927660000003, + "user": 0.04835111999999999, + "system": 0.0085147, + "min": 0.021465240660000003, + "max": 0.024330072660000004, "times": [ - 0.02400281232, - 0.024030394320000002, - 0.025400769320000002, - 0.02347693632, - 0.02391831132, - 0.02532989532, - 0.02335527032, - 0.02364014532, - 0.02291718632, - 0.02386356232 + 0.023690156660000003, + 0.023906740660000002, + 0.02402815666, + 0.02370874066, + 0.024330072660000004, + 0.023250948660000003, + 0.023267698660000002, + 0.021465240660000003, + 0.023155240660000003, + 0.022480822660000004 ], "memory_usage_byte": [ - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912, - 147750912 + 145833984, + 145833984, + 145833984, + 145833984, + 145833984, + 145833984, + 145833984, + 145833984, + 145833984, + 145833984 ], "exit_codes": [ 1, diff --git a/benchmarks/results/generics_defaults_specialization.json b/benchmarks/results/generics_defaults_specialization.json index baca3e51..abb69f46 100644 --- a/benchmarks/results/generics_defaults_specialization.json +++ b/benchmarks/results/generics_defaults_specialization.json @@ -2,36 +2,36 @@ "results": [ { "command": "basilisk", - "mean": 0.010499286459999998, - "stddev": 0.00025918857659817647, - "median": 0.010487028360000001, - "user": 0.00796114, - "system": 0.0019360999999999996, - "min": 0.01011006936, - "max": 0.010986403359999999, + "mean": 0.009669909079999999, + "stddev": 0.00014289545931219945, + "median": 0.00972507118, + "user": 0.007613839999999999, + "system": 0.0015610800000000005, + "min": 0.00941544668, + "max": 0.00983027968, "times": [ - 0.01032661136, - 0.01033715336, - 0.01050269536, - 0.01011006936, - 0.01052202836, - 0.010986403359999999, - 0.01027494436, - 0.01070961136, - 0.01047136136, - 0.01075198636 + 0.00978423768, + 0.00979778068, + 0.00983027968, + 0.009593321680000001, + 0.009645779680000001, + 0.00972473768, + 0.00972540468, + 0.00945157168, + 0.00941544668, + 0.00973053068 ], "memory_usage_byte": [ - 15433728, - 15613952, - 15613952, - 15613952, - 15613952, - 15613952, - 15613952, - 15613952, - 15613952, - 15613952 + 15122432, + 15122432, + 15122432, + 15122432, + 15122432, + 15122432, + 15122432, + 15122432, + 15122432, + 15122432 ], "exit_codes": [ 1, @@ -48,36 +48,36 @@ }, { "command": "basilisk-warm", - "mean": 0.0042921157599999996, - "stddev": 0.00037308260866825594, - "median": 0.004297174359999999, - "user": 0.0023501399999999997, - "system": 0.0014761999999999996, - "min": 0.00372319536, - "max": 0.00476036136, + "mean": 0.0037947716799999996, + "stddev": 0.00014741463324845927, + "median": 0.00377194668, + "user": 0.00220024, + "system": 0.0012394800000000005, + "min": 0.00363694668, + "max": 0.00416740568, "times": [ - 0.00413786136, - 0.00471936136, - 0.004451612359999999, - 0.00372319536, - 0.00463198636, - 0.00414273636, - 0.00388127836, - 0.00476036136, - 0.004523778359999999, - 0.00394898636 + 0.0038545296799999997, + 0.00363694668, + 0.0037365296799999996, + 0.00416740568, + 0.00379940468, + 0.0038159466799999996, + 0.00381019668, + 0.00368294668, + 0.0037444886799999997, + 0.00369932168 ], "memory_usage_byte": [ - 15859712, - 15859712, - 15859712, - 15859712, - 15859712, - 15859712, - 15859712, - 15859712, - 15859712, - 15859712 + 15171584, + 15171584, + 15171584, + 15171584, + 15171584, + 15171584, + 15171584, + 15171584, + 15171584, + 15171584 ], "exit_codes": [ 1, @@ -94,36 +94,36 @@ }, { "command": "pyright", - "mean": 0.57799486986, - "stddev": 0.016592462381401076, - "median": 0.5722266943600001, - "user": 0.7397257399999998, - "system": 0.0574558, - "min": 0.5629546953600001, - "max": 0.6142039863600001, + "mean": 0.57684011748, + "stddev": 0.018916017829216158, + "median": 0.5746413426800001, + "user": 0.71069744, + "system": 0.052438479999999996, + "min": 0.55140286268, + "max": 0.60887565468, "times": [ - 0.5910696943600001, - 0.5902034453600001, - 0.58227690336, - 0.5634216113600001, - 0.5701466943600001, - 0.5680677783600001, - 0.5632971953600001, - 0.57430669436, - 0.5629546953600001, - 0.6142039863600001 + 0.5692376136800001, + 0.55754877968, + 0.56227640568, + 0.5831832386800001, + 0.59369919668, + 0.5974139876800001, + 0.60887565468, + 0.5800450716800001, + 0.55140286268, + 0.5647183636800001 ], "memory_usage_byte": [ - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920 + 153436160, + 153436160, + 153436160, + 153436160, + 153436160, + 153436160, + 153436160, + 153436160, + 153780224, + 153780224 ], "exit_codes": [ 1, @@ -140,36 +140,36 @@ }, { "command": "mypy", - "mean": 0.6081942907600001, - "stddev": 0.009652798472584231, - "median": 0.6088413403600001, - "user": 0.5427769399999999, - "system": 0.06041289999999999, - "min": 0.5925460283600001, - "max": 0.62116844536, + "mean": 0.5573774716800001, + "stddev": 0.0043165160300078454, + "median": 0.5559096546800001, + "user": 0.50294474, + "system": 0.05120698, + "min": 0.5521987386800001, + "max": 0.56717519668, "times": [ - 0.5966514033600001, - 0.6184696943600001, - 0.61062940336, - 0.6049442783600001, - 0.6104251943600001, - 0.62116844536, - 0.6010308613600001, - 0.6188201123600001, - 0.6072574863600001, - 0.5925460283600001 + 0.5521987386800001, + 0.56148277968, + 0.5552600716800001, + 0.5564170296800001, + 0.5554022796800001, + 0.55498836368, + 0.5540860716800001, + 0.5581031136800001, + 0.56717519668, + 0.5586610716800001 ], "memory_usage_byte": [ - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920 + 153780224, + 153780224, + 153780224, + 153780224, + 153780224, + 153780224, + 153780224, + 153780224, + 153780224, + 153780224 ], "exit_codes": [ 1, @@ -186,36 +186,36 @@ }, { "command": "mypy-warm", - "mean": 0.16698841176, - "stddev": 0.002251915350233279, - "median": 0.16675696586, - "user": 0.13743993999999998, - "system": 0.027184599999999993, - "min": 0.16422261136000002, - "max": 0.17100282036, + "mean": 0.15782925928, + "stddev": 0.0028617429896338527, + "median": 0.15672821768, + "user": 0.13105424, + "system": 0.02451428, + "min": 0.15529128068, + "max": 0.16346432168, "times": [ - 0.16704832036, - 0.17100282036, - 0.16939136136000002, - 0.16751948736, - 0.16889869436000002, - 0.16646561136000002, - 0.16591948736, - 0.16422261136000002, - 0.16429057036, - 0.16512515336 + 0.15585952968, + 0.15914419668000002, + 0.15544915568, + 0.15529128068, + 0.15745111368, + 0.15600532168, + 0.16172857168000002, + 0.15553269668, + 0.16346432168, + 0.15836640468000002 ], "memory_usage_byte": [ - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920 + 153780224, + 153780224, + 153780224, + 153780224, + 153780224, + 153780224, + 153780224, + 153780224, + 153780224, + 153780224 ], "exit_codes": [ 1, @@ -232,36 +232,36 @@ }, { "command": "ty", - "mean": 0.03494051206, - "stddev": 0.0027499313578141483, - "median": 0.03419329936, - "user": 0.02628494, - "system": 0.006283299999999999, - "min": 0.03227348736, - "max": 0.04242236236, + "mean": 0.03368388808, + "stddev": 0.0010117562230387315, + "median": 0.03352315518, + "user": 0.02507854, + "system": 0.00674838, + "min": 0.03188532168, + "max": 0.03509886268, "times": [ - 0.03410069536, - 0.03410469536, - 0.034542278360000005, - 0.035067445360000005, - 0.03227348736, - 0.03387098736, - 0.03360832036, - 0.03513294536, - 0.034281903360000004, - 0.04242236236 + 0.03346465568, + 0.03387173768, + 0.03498073768, + 0.03188532168, + 0.03333690468, + 0.03469732168, + 0.03290082168, + 0.03302086268, + 0.03358165468, + 0.03509886268 ], "memory_usage_byte": [ - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920 + 153780224, + 153780224, + 153780224, + 153780224, + 153780224, + 153780224, + 153780224, + 153780224, + 153780224, + 153780224 ], "exit_codes": [ 1, @@ -278,36 +278,36 @@ }, { "command": "pyrefly", - "mean": 0.14182871586, - "stddev": 0.001183193276052021, - "median": 0.14165309086, - "user": 0.10169264, - "system": 0.0387591, - "min": 0.13969473636000002, - "max": 0.14412011236, + "mean": 0.10094719268, + "stddev": 0.001301755255018565, + "median": 0.10085428018, + "user": 0.08478983999999999, + "system": 0.01676048, + "min": 0.09942365468, + "max": 0.10358023868, "times": [ - 0.14182665336, - 0.14286748636000002, - 0.14120998636, - 0.14121790336, - 0.14134944536000002, - 0.14412011236, - 0.14260373636, - 0.14147952836, - 0.14191757036, - 0.13969473636000002 + 0.10085094668, + 0.09977957168, + 0.10358023868, + 0.10085761368, + 0.10187886368, + 0.09964853068, + 0.10163344668, + 0.10180061368, + 0.10001844668, + 0.09942365468 ], "memory_usage_byte": [ - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920 + 153780224, + 153780224, + 153780224, + 153780224, + 153780224, + 153780224, + 153780224, + 153780224, + 153780224, + 153780224 ], "exit_codes": [ 1, @@ -324,36 +324,36 @@ }, { "command": "zuban", - "mean": 0.02651546976, - "stddev": 0.0006568008305763797, - "median": 0.026457673860000003, - "user": 0.05272143999999999, - "system": 0.0083526, - "min": 0.02577123636, - "max": 0.02773632036, + "mean": 0.025384513180000002, + "stddev": 0.0006571041924682792, + "median": 0.02546619668, + "user": 0.05080874, + "system": 0.008319279999999998, + "min": 0.02426719668, + "max": 0.02628248868, "times": [ - 0.027368111360000003, - 0.025837320360000002, - 0.02602169436, - 0.02603556936, - 0.026552736360000002, - 0.02577123636, - 0.02656665336, - 0.02636261136, - 0.02690244436, - 0.02773632036 + 0.025545613680000002, + 0.02538677968, + 0.02597911268, + 0.02455965468, + 0.02426719668, + 0.026075987680000003, + 0.02628248868, + 0.02487607168, + 0.025314779680000003, + 0.02555744668 ], "memory_usage_byte": [ - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920, - 153681920 + 153780224, + 153780224, + 153780224, + 153780224, + 153780224, + 153780224, + 153780224, + 153780224, + 153780224, + 153780224 ], "exit_codes": [ 1, diff --git a/benchmarks/results/literals_semantics.json b/benchmarks/results/literals_semantics.json index 879f628d..cb1d6447 100644 --- a/benchmarks/results/literals_semantics.json +++ b/benchmarks/results/literals_semantics.json @@ -2,36 +2,36 @@ "results": [ { "command": "basilisk", - "mean": 0.011431239280000002, - "stddev": 0.0003043569714097797, - "median": 0.01131038508, - "user": 0.008831620000000002, - "system": 0.00198622, - "min": 0.011186719080000001, - "max": 0.012002593080000002, + "mean": 0.01065312514, + "stddev": 0.00036028944222118916, + "median": 0.01056758314, + "user": 0.00858144, + "system": 0.00154096, + "min": 0.01021683364, + "max": 0.011398833640000001, "times": [ - 0.01129842708, - 0.012002593080000002, - 0.01195963508, - 0.01150734308, - 0.011322343080000002, - 0.011186719080000001, - 0.011219801080000001, - 0.01120663508, - 0.01135767708, - 0.01125121908 + 0.010430791640000001, + 0.01021683364, + 0.010429208640000001, + 0.011398833640000001, + 0.01110374964, + 0.01057279164, + 0.01058237564, + 0.01056237464, + 0.010837291640000002, + 0.010397000640000001 ], "memory_usage_byte": [ - 15695872, - 15695872, - 15695872, - 15695872, - 15695872, - 15695872, - 15695872, - 15695872, - 15695872, - 15695872 + 14925824, + 14925824, + 14925824, + 14925824, + 14925824, + 14925824, + 14925824, + 14958592, + 14958592, + 14958592 ], "exit_codes": [ 1, @@ -48,36 +48,36 @@ }, { "command": "basilisk-warm", - "mean": 0.00423346448, - "stddev": 0.00027443662680043276, - "median": 0.0042395105800000004, - "user": 0.0023257200000000003, - "system": 0.0014520199999999998, - "min": 0.0037868020800000003, - "max": 0.004599594080000001, + "mean": 0.0038637457400000005, + "stddev": 0.0001245114090555112, + "median": 0.00382062514, + "user": 0.00225434, + "system": 0.0012456600000000002, + "min": 0.00371620864, + "max": 0.004108874639999999, "times": [ - 0.004599594080000001, - 0.0037868020800000003, - 0.00445313508, - 0.004290427080000001, - 0.0038145930800000004, - 0.00418859408, - 0.00437259408, - 0.00451913508, - 0.004123260080000001, - 0.004186510080000001 + 0.0037684996400000003, + 0.0038312496399999997, + 0.00378033364, + 0.0038100006400000005, + 0.00378858264, + 0.0038692496400000004, + 0.00371620864, + 0.00393783264, + 0.004108874639999999, + 0.00402662564 ], "memory_usage_byte": [ - 15728640, - 15728640, - 15728640, - 15728640, - 15728640, - 15728640, - 15728640, - 15728640, - 15728640, - 15728640 + 15204352, + 15204352, + 15204352, + 15204352, + 15204352, + 15204352, + 15204352, + 15204352, + 15204352, + 15204352 ], "exit_codes": [ 1, @@ -94,36 +94,36 @@ }, { "command": "pyright", - "mean": 0.5447929350799999, - "stddev": 0.009385793089029005, - "median": 0.54383065608, - "user": 0.7037490199999998, - "system": 0.059746719999999996, - "min": 0.5257282600800001, - "max": 0.56234226008, + "mean": 0.52648930824, + "stddev": 0.03435534049353881, + "median": 0.51818124964, + "user": 0.6737648399999999, + "system": 0.05012135999999999, + "min": 0.50319533264, + "max": 0.62125141664, "times": [ - 0.54619305208, - 0.56234226008, - 0.54029659308, - 0.5495428020800001, - 0.55229346808, - 0.5448518020800001, - 0.5257282600800001, - 0.54167421808, - 0.54219738508, - 0.54280951008 + 0.51901762464, + 0.51734487464, + 0.52230308364, + 0.5076682086400001, + 0.52452320864, + 0.62125141664, + 0.51341499964, + 0.50319533264, + 0.50640516664, + 0.5297691666400001 ], "memory_usage_byte": [ - 154877952, - 154877952, - 154877952, - 154877952, - 154877952, - 154877952, - 154877952, - 154877952, - 154877952, - 156614656 + 153698304, + 153698304, + 154599424, + 154599424, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000 ], "exit_codes": [ 1, @@ -140,36 +140,36 @@ }, { "command": "mypy", - "mean": 0.60000916418, - "stddev": 0.008441040308467243, - "median": 0.5999624475800001, - "user": 0.5370627199999999, - "system": 0.05845482, - "min": 0.58900042608, - "max": 0.61617026008, + "mean": 0.5506183626400001, + "stddev": 0.0018727783662280508, + "median": 0.55046272914, + "user": 0.49814403999999995, + "system": 0.049892759999999994, + "min": 0.54792720864, + "max": 0.5541694996400001, "times": [ - 0.59077759308, - 0.6033815100800001, - 0.59204917708, - 0.58900042608, - 0.60065555208, - 0.60040788508, - 0.59951701008, - 0.59835671808, - 0.61617026008, - 0.6097755100800001 + 0.54966004164, + 0.55196645764, + 0.54792720864, + 0.54963258364, + 0.5512654166400001, + 0.55178037564, + 0.5541694996400001, + 0.5488067506400001, + 0.54927929164, + 0.55169600064 ], "memory_usage_byte": [ - 156614656, - 156614656, - 156614656, - 156614656, - 156614656, - 156614656, - 156614656, - 156614656, - 156614656, - 156614656 + 155648000, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000 ], "exit_codes": [ 1, @@ -186,36 +186,36 @@ }, { "command": "mypy-warm", - "mean": 0.16935081408, - "stddev": 0.006322090930177662, - "median": 0.16567105158, - "user": 0.13883452000000002, - "system": 0.027750019999999997, - "min": 0.16490159408, - "max": 0.18358021808, + "mean": 0.17685108314000003, + "stddev": 0.023825787341927772, + "median": 0.16359033314000002, + "user": 0.13878653999999999, + "system": 0.030278659999999995, + "min": 0.15521924964000003, + "max": 0.22005854164000002, "times": [ - 0.16526796808, - 0.17119555108, - 0.17694467608, - 0.18358021808, - 0.16962517608, - 0.16593821808, - 0.16490159408, - 0.16535513508, - 0.16529571908000001, - 0.16540388508 + 0.15739974964000003, + 0.15521924964000003, + 0.15766308264, + 0.16330620764, + 0.15996320864000002, + 0.16387445864000003, + 0.18696408264000003, + 0.19884833364, + 0.20521391664000002, + 0.22005854164000002 ], "memory_usage_byte": [ - 156614656, - 156614656, - 156614656, - 156614656, - 156614656, - 156614656, - 156614656, - 156614656, - 156614656, - 156614656 + 155648000, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000 ], "exit_codes": [ 1, @@ -232,36 +232,36 @@ }, { "command": "ty", - "mean": 0.031705088980000004, - "stddev": 0.0009248190050292073, - "median": 0.031714280580000004, - "user": 0.023439319999999996, - "system": 0.00548122, - "min": 0.030455760079999998, - "max": 0.03301359308, + "mean": 0.03926082904, + "stddev": 0.007714906485673177, + "median": 0.035674020640000005, + "user": 0.025880539999999997, + "system": 0.008753559999999999, + "min": 0.03363841664, + "max": 0.05817683364, "times": [ - 0.03301359308, - 0.03089267708, - 0.03217463508, - 0.03235371808, - 0.03288459308, - 0.030455760079999998, - 0.03069017608, - 0.03126609308, - 0.03115717608, - 0.03216246808 + 0.044275291640000006, + 0.044113499640000005, + 0.05817683364, + 0.037653458640000005, + 0.03492016664, + 0.035604124640000005, + 0.034215124640000004, + 0.034267457640000006, + 0.03363841664, + 0.035743916640000005 ], "memory_usage_byte": [ - 156614656, - 156614656, - 156614656, - 156614656, - 156614656, - 156614656, - 156614656, - 156614656, - 156614656, - 156614656 + 155648000, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000 ], "exit_codes": [ 1, @@ -278,36 +278,36 @@ }, { "command": "pyrefly", - "mean": 0.14112715198, - "stddev": 0.0012321093639809054, - "median": 0.14128528158, - "user": 0.10192121999999999, - "system": 0.03867311999999999, - "min": 0.13948742708, - "max": 0.14315480208, + "mean": 0.10544389584, + "stddev": 0.0017834136304592837, + "median": 0.10477343714, + "user": 0.08767674, + "system": 0.01801806, + "min": 0.10376645864, + "max": 0.10833295864, "times": [ - 0.14315480208, - 0.14159309408, - 0.13952380108, - 0.13984284308, - 0.14168655208, - 0.14222742708, - 0.14199830208, - 0.14097746908, - 0.14077980208, - 0.13948742708 + 0.10833295864, + 0.10728133364, + 0.10820070864, + 0.10492037464, + 0.10412341664, + 0.10497616664, + 0.10462649964, + 0.10413062464, + 0.10376645864, + 0.10408041664 ], "memory_usage_byte": [ - 156614656, - 156614656, - 156614656, - 156614656, - 156614656, - 156614656, - 156614656, - 156614656, - 156614656, - 156614656 + 155648000, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000 ], "exit_codes": [ 1, @@ -324,36 +324,36 @@ }, { "command": "zuban", - "mean": 0.02716564758, - "stddev": 0.000946772511783439, - "median": 0.027611635080000002, - "user": 0.052670919999999996, - "system": 0.008590320000000002, - "min": 0.02560513508, - "max": 0.028413260080000002, + "mean": 0.026918554240000003, + "stddev": 0.0006034680184187619, + "median": 0.027053729140000003, + "user": 0.05308454, + "system": 0.00891196, + "min": 0.025998208640000003, + "max": 0.027658125640000003, "times": [ - 0.02610751008, - 0.02560513508, - 0.02766267708, - 0.027625760080000002, - 0.028413260080000002, - 0.027608010080000002, - 0.02738634308, - 0.025865843080000003, - 0.02776667708, - 0.027615260080000002 + 0.02602720864, + 0.027374166640000003, + 0.02648249964, + 0.025998208640000003, + 0.02669658364, + 0.02759754164, + 0.02691454164, + 0.027243749640000002, + 0.027192916640000002, + 0.027658125640000003 ], "memory_usage_byte": [ - 156614656, - 156614656, - 156614656, - 156614656, - 156614656, - 156614656, - 156614656, - 156614656, - 156614656, - 156614656 + 155648000, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000, + 155648000 ], "exit_codes": [ 1, diff --git a/benchmarks/results/match_exhaustiveness.json b/benchmarks/results/match_exhaustiveness.json index 82c1df7a..b5debc78 100644 --- a/benchmarks/results/match_exhaustiveness.json +++ b/benchmarks/results/match_exhaustiveness.json @@ -2,36 +2,36 @@ "results": [ { "command": "basilisk", - "mean": 0.01064612246, - "stddev": 0.0003086820081583417, - "median": 0.01066826396, - "user": 0.00812728, - "system": 0.0019606400000000005, - "min": 0.01008343046, - "max": 0.01125484746, + "mean": 0.0104396273, + "stddev": 0.00022013719132206293, + "median": 0.0104257728, + "user": 0.008183059999999999, + "system": 0.00170156, + "min": 0.0101699188, + "max": 0.0107739608, "times": [ - 0.01062951446, - 0.01073530546, - 0.01082855646, - 0.01038134746, - 0.01078930546, - 0.01070701346, - 0.01125484746, - 0.01047263946, - 0.01057926446, - 0.01008343046 + 0.0106997938, + 0.0103219188, + 0.0107739608, + 0.0106084608, + 0.010197085800000002, + 0.0105296268, + 0.0101699188, + 0.0102671688, + 0.0102665028, + 0.010561835800000002 ], "memory_usage_byte": [ - 17383424, - 17678336, - 17678336, - 17793024, - 17793024, - 17793024, - 17793024, - 17793024, - 17793024, - 17793024 + 16252928, + 16252928, + 16252928, + 16269312, + 16269312, + 16269312, + 16269312, + 16269312, + 16269312, + 16269312 ], "exit_codes": [ 1, @@ -48,36 +48,36 @@ }, { "command": "basilisk-warm", - "mean": 0.00397863906, - "stddev": 0.0004839445963120159, - "median": 0.00400886796, - "user": 0.00222268, - "system": 0.0014241400000000002, - "min": 0.00342947246, - "max": 0.00487547246, + "mean": 0.004120356700000001, + "stddev": 0.00012178776770308268, + "median": 0.0041087733, + "user": 0.00228786, + "system": 0.0013400600000000001, + "min": 0.0039378358, + "max": 0.0042840858, "times": [ - 0.00457293046, - 0.0036576814599999993, - 0.00348505646, - 0.00487547246, - 0.00400609746, - 0.00342947246, - 0.00418647246, - 0.0040116384599999996, - 0.00348180546, - 0.00407976346 + 0.0040290858, + 0.0040912948, + 0.004125044800000001, + 0.0040925018, + 0.0039718358, + 0.0042840858, + 0.0039378358, + 0.0042574198, + 0.0042682938000000005, + 0.004146168800000001 ], "memory_usage_byte": [ - 17793024, - 17793024, - 17793024, - 17793024, - 17793024, - 17793024, - 17793024, - 17793024, - 17793024, - 17793024 + 16564224, + 16564224, + 16564224, + 16564224, + 16564224, + 16564224, + 16564224, + 16564224, + 16564224, + 16564224 ], "exit_codes": [ 1, @@ -94,27 +94,27 @@ }, { "command": "pyright", - "mean": 0.54133201006, - "stddev": 0.006031633183528148, - "median": 0.5409371809600001, - "user": 0.74230788, - "system": 0.05846174, - "min": 0.53275909746, - "max": 0.5509551394600001, + "mean": 0.5471146568, + "stddev": 0.01924376545561796, + "median": 0.5377296898, + "user": 0.75461296, + "system": 0.060522759999999995, + "min": 0.5288297948, + "max": 0.5899912107999999, "times": [ - 0.5459610554600001, - 0.53275909746, - 0.5460149724600001, - 0.53978122246, - 0.53659438946, - 0.5509551394600001, - 0.5472368054600001, - 0.53598218146, - 0.53594209746, - 0.54209313946 + 0.5568816698, + 0.5370099188, + 0.5899912107999999, + 0.5564712108, + 0.5384494608, + 0.5626065028, + 0.5288297948, + 0.5304931278, + 0.5349139187999999, + 0.5354997528 ], "memory_usage_byte": [ - 162873344, + 162463744, 162873344, 162873344, 162873344, @@ -140,24 +140,24 @@ }, { "command": "mypy", - "mean": 0.6238318350600001, - "stddev": 0.010012624174059036, - "median": 0.62197245146, - "user": 0.56082698, - "system": 0.058384139999999994, - "min": 0.61209672246, - "max": 0.64114551446, + "mean": 0.9708153399, + "stddev": 0.2963123801184094, + "median": 1.1089976687999998, + "user": 0.6762345599999999, + "system": 0.07382815999999999, + "min": 0.5910305028, + "max": 1.3553534188, "times": [ - 0.61561159746, - 0.6286408894600001, - 0.61967009746, - 0.61209672246, - 0.62690497246, - 0.64114551446, - 0.62019693046, - 0.6380093064600001, - 0.62374797246, - 0.61229434746 + 1.1236381278, + 1.2211931698, + 1.2196685438, + 1.1181721688, + 1.0998231688, + 1.3553534188, + 0.7793712108, + 0.6046197108, + 0.5952833768, + 0.5910305028 ], "memory_usage_byte": [ 162873344, @@ -186,24 +186,24 @@ }, { "command": "mypy-warm", - "mean": 0.16853441016, - "stddev": 0.002063983381910652, - "median": 0.16925413896000002, - "user": 0.13840497999999996, - "system": 0.027495439999999993, - "min": 0.16485822246, - "max": 0.17048647246, + "mean": 0.1620310566, + "stddev": 0.005058990878174465, + "median": 0.1609202318, + "user": 0.13258386000000003, + "system": 0.027096159999999998, + "min": 0.1572312938, + "max": 0.1749030028, "times": [ - 0.17045813946000002, - 0.17002751446, - 0.17037213946000002, - 0.16907855546, - 0.16802330646000002, - 0.17048647246, - 0.16585297246000003, - 0.16675705646000002, - 0.16942972246000002, - 0.16485822246 + 0.1604267938, + 0.15789696079999999, + 0.1749030028, + 0.1607740028, + 0.1572312938, + 0.1636345438, + 0.1610664608, + 0.1582640028, + 0.1635928358, + 0.1625206688 ], "memory_usage_byte": [ 162873344, @@ -232,24 +232,24 @@ }, { "command": "ty", - "mean": 0.03625212246, - "stddev": 0.0015885825838097166, - "median": 0.03595899296, - "user": 0.027025679999999996, - "system": 0.00618684, - "min": 0.03408447246, - "max": 0.038363097459999995, + "mean": 0.026024252399999996, + "stddev": 0.0019157302541647839, + "median": 0.0256960648, + "user": 0.01705656, + "system": 0.00691286, + "min": 0.0225557938, + "max": 0.0289780858, "times": [ - 0.03573626346, - 0.03762418146, - 0.03533851446, - 0.03739918046, - 0.038363097459999995, - 0.03829963846000001, - 0.036181722459999996, - 0.035262222460000006, - 0.034231931460000004, - 0.03408447246 + 0.0245014608, + 0.0225557938, + 0.0257345858, + 0.0263767528, + 0.0248847108, + 0.0256575438, + 0.0256430438, + 0.0285518358, + 0.0289780858, + 0.0273587108 ], "memory_usage_byte": [ 162873344, @@ -264,38 +264,38 @@ 162873344 ], "exit_codes": [ - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1 + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 ] }, { "command": "pyrefly", - "mean": 0.14793593516000003, - "stddev": 0.0041576905370845855, - "median": 0.14683768146, - "user": 0.10717558, - "system": 0.03993934, - "min": 0.14479288946000002, - "max": 0.15937684746000003, + "mean": 0.10990922730000004, + "stddev": 0.002495659364794504, + "median": 0.10923235680000001, + "user": 0.09170936, + "system": 0.01823196, + "min": 0.10661608580000001, + "max": 0.11366641880000002, "times": [ - 0.14679405646000002, - 0.14688130646, - 0.14717638946, - 0.14479288946000002, - 0.15937684746000003, - 0.14869180646000002, - 0.14665409746000002, - 0.14628526346, - 0.14538822246000002, - 0.14731847246000002 + 0.10777829380000001, + 0.11089012680000002, + 0.11247296080000001, + 0.10661608580000001, + 0.10782679380000001, + 0.1094483358, + 0.11366641880000002, + 0.11318416880000001, + 0.10819271080000001, + 0.10901637780000001 ], "memory_usage_byte": [ 162873344, @@ -324,24 +324,24 @@ }, { "command": "zuban", - "mean": 0.026624884760000002, - "stddev": 0.0008165390451069889, - "median": 0.02670132596, - "user": 0.05302717999999999, - "system": 0.008121340000000001, - "min": 0.02543797246, - "max": 0.02809922246, + "mean": 0.027877943999999998, + "stddev": 0.001244207882231976, + "median": 0.027564752300000002, + "user": 0.05301386, + "system": 0.00893616, + "min": 0.0264108358, + "max": 0.030510001800000002, "times": [ - 0.025692430460000002, - 0.026042264460000002, - 0.02543797246, - 0.027358972460000002, - 0.026960888460000002, - 0.027039972460000002, - 0.02644176346, - 0.02809922246, - 0.026170013460000002, - 0.027005347460000002 + 0.027708293800000002, + 0.0293690018, + 0.0272740858, + 0.0264108358, + 0.0269643358, + 0.0274212108, + 0.030510001800000002, + 0.0280717948, + 0.0281978358, + 0.0268520438 ], "memory_usage_byte": [ 162873344, diff --git a/benchmarks/results/narrowing_typeis.json b/benchmarks/results/narrowing_typeis.json index 9ca38a30..851c4ef7 100644 --- a/benchmarks/results/narrowing_typeis.json +++ b/benchmarks/results/narrowing_typeis.json @@ -2,36 +2,36 @@ "results": [ { "command": "basilisk", - "mean": 0.00949394422, - "stddev": 0.00021293917711549045, - "median": 0.00947565252, - "user": 0.007029139999999999, - "system": 0.0019546999999999993, - "min": 0.00918300602, - "max": 0.00983234002, + "mean": 0.00873180172, + "stddev": 0.0001741404025501327, + "median": 0.00869455182, + "user": 0.006865859999999999, + "system": 0.0014668200000000002, + "min": 0.00855944782, + "max": 0.00907365582, "times": [ - 0.00983234002, - 0.00967704902, - 0.00932996502, - 0.00918300602, - 0.00923217302, - 0.009529382020000001, - 0.00974784002, - 0.009466882020000001, - 0.009456382020000001, - 0.00948442302 + 0.00886965582, + 0.00907365582, + 0.00855944782, + 0.00877286382, + 0.00861623982, + 0.00880353082, + 0.00859619682, + 0.00858615582, + 0.00856636482, + 0.00887390582 ], "memory_usage_byte": [ - 15269888, - 15335424, - 15335424, - 15335424, - 15335424, - 15335424, - 15335424, - 15335424, - 15351808, - 15351808 + 14794752, + 14794752, + 14794752, + 14794752, + 14794752, + 14794752, + 14794752, + 14811136, + 14811136, + 14811136 ], "exit_codes": [ 1, @@ -48,36 +48,36 @@ }, { "command": "basilisk-warm", - "mean": 0.00420097732, - "stddev": 0.0003020227728930571, - "median": 0.004212152520000001, - "user": 0.00232074, - "system": 0.0014593999999999996, - "min": 0.003674090020000001, - "max": 0.004642756020000001, + "mean": 0.0036853225200000005, + "stddev": 0.0001016384823272606, + "median": 0.0036738013200000004, + "user": 0.0022561599999999992, + "system": 0.00115322, + "min": 0.003539114820000001, + "max": 0.0038401138200000007, "times": [ - 0.004287798020000001, - 0.0039490900200000005, - 0.004642756020000001, - 0.00461334002, - 0.00393867302, - 0.004131131020000001, - 0.0041365490200000014, - 0.00428775602, - 0.004348590020000001, - 0.003674090020000001 + 0.0036749058200000006, + 0.0036151558200000007, + 0.0036726968200000002, + 0.0038198638200000004, + 0.0038401138200000007, + 0.0036073648200000007, + 0.003585114820000001, + 0.003748821820000001, + 0.0037500728200000008, + 0.003539114820000001 ], "memory_usage_byte": [ - 15450112, - 15450112, - 15450112, - 15450112, - 15450112, - 15450112, - 15450112, - 15450112, - 15450112, - 15450112 + 15122432, + 15122432, + 15122432, + 15122432, + 15122432, + 15122432, + 15122432, + 15122432, + 15122432, + 15122432 ], "exit_codes": [ 1, @@ -94,36 +94,36 @@ }, { "command": "pyright", - "mean": 0.55246078582, - "stddev": 0.006894345423491821, - "median": 0.54958813152, - "user": 0.70861144, - "system": 0.055668200000000015, - "min": 0.54493734002, - "max": 0.56885363102, + "mean": 0.54951695592, + "stddev": 0.012344794437181622, + "median": 0.54746467632, + "user": 0.69491796, + "system": 0.05546601999999999, + "min": 0.53632728082, + "max": 0.5794496558200001, "times": [ - 0.54928275702, - 0.54851300702, - 0.55287288202, - 0.54493734002, - 0.54839229802, - 0.54947613202, - 0.54970013102, - 0.55895446502, - 0.55362521502, - 0.56885363102 + 0.54103544782, + 0.5794496558200001, + 0.53901307282, + 0.55676994782, + 0.54866336382, + 0.54317698882, + 0.55174307282, + 0.55272473982, + 0.53632728082, + 0.54626598882 ], "memory_usage_byte": [ - 152862720, - 152862720, - 152862720, - 152862720, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096 + 152567808, + 152567808, + 152567808, + 152567808, + 152567808, + 152567808, + 152567808, + 153026560, + 154370048, + 154370048 ], "exit_codes": [ 1, @@ -140,36 +140,36 @@ }, { "command": "mypy", - "mean": 0.6049855775200002, - "stddev": 0.012361334619845505, - "median": 0.60174673602, - "user": 0.5418449399999999, - "system": 0.05854279999999999, - "min": 0.59368271502, - "max": 0.6322412980200001, + "mean": 0.57384534732, + "stddev": 0.008489666404233799, + "median": 0.57093565582, + "user": 0.5136289599999999, + "system": 0.05572342, + "min": 0.56775148882, + "max": 0.59574744682, "times": [ - 0.60068567302, - 0.62096646502, - 0.60458059002, - 0.60069184002, - 0.60454396502, - 0.6322412980200001, - 0.59550625702, - 0.59415534002, - 0.60280163202, - 0.59368271502 + 0.56775148882, + 0.57224065582, + 0.57911003082, + 0.56963065582, + 0.59574744682, + 0.56849932282, + 0.56778298882, + 0.56956544682, + 0.57393411382, + 0.5741913228200001 ], "memory_usage_byte": [ - 153092096, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096 + 154370048, + 154370048, + 154370048, + 154370048, + 154370048, + 154370048, + 154370048, + 154370048, + 154370048, + 154370048 ], "exit_codes": [ 1, @@ -186,36 +186,36 @@ }, { "command": "mypy-warm", - "mean": 0.16729546902, - "stddev": 0.00399288824906959, - "median": 0.16633602751999998, - "user": 0.13782494, - "system": 0.026909899999999997, - "min": 0.16309584002000002, - "max": 0.17619038102, + "mean": 0.16185598912000002, + "stddev": 0.002917208957988442, + "median": 0.16181005132, + "user": 0.13341556000000002, + "system": 0.02645612, + "min": 0.15701457282, + "max": 0.16594769782000002, "times": [ - 0.16412017302, - 0.16453154802, - 0.17619038102, - 0.16887171502, - 0.17076934002000002, - 0.16850850602, - 0.16546042302, - 0.16309584002000002, - 0.16419513202, - 0.16721163202 + 0.16470153082, + 0.16045844782000002, + 0.16027686382, + 0.16594769782000002, + 0.16141882182, + 0.16288390582, + 0.16518228082, + 0.15847448882, + 0.15701457282, + 0.16220128082000002 ], "memory_usage_byte": [ - 153092096, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096 + 154370048, + 154370048, + 154370048, + 154370048, + 154370048, + 154370048, + 154370048, + 154370048, + 154370048, + 154370048 ], "exit_codes": [ 1, @@ -232,36 +232,36 @@ }, { "command": "ty", - "mean": 0.03560461512, - "stddev": 0.0010652992451107024, - "median": 0.03504675652, - "user": 0.026246739999999998, - "system": 0.0065758, - "min": 0.03461846502, - "max": 0.03729821502, + "mean": 0.03355193092, + "stddev": 0.0010431251134067662, + "median": 0.03347611382, + "user": 0.025627659999999997, + "system": 0.00701412, + "min": 0.03223615582, + "max": 0.035536447820000004, "times": [ - 0.03461846502, - 0.03707738202, - 0.03651421502, - 0.03625113202, - 0.03467079802, - 0.03518854802, - 0.03473563202, - 0.03478679902, - 0.03490496502, - 0.03729821502 + 0.035536447820000004, + 0.032894447820000006, + 0.032519697820000006, + 0.032976697820000005, + 0.03223615582, + 0.033537905820000005, + 0.03355932282, + 0.033805322820000004, + 0.03341432182, + 0.035038988820000005 ], "memory_usage_byte": [ - 153092096, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096 + 154370048, + 154370048, + 154370048, + 154370048, + 154370048, + 154370048, + 154370048, + 154370048, + 154370048, + 154370048 ], "exit_codes": [ 1, @@ -278,36 +278,36 @@ }, { "command": "pyrefly", - "mean": 0.14548846922000003, - "stddev": 0.00234481853019062, - "median": 0.14466169402, - "user": 0.10465494, - "system": 0.039903, - "min": 0.14290396502, - "max": 0.14950734002000002, + "mean": 0.10807252652000002, + "stddev": 0.012570938355009553, + "median": 0.10386955132, + "user": 0.08864025999999998, + "system": 0.017264919999999996, + "min": 0.10166903082, + "max": 0.14330086382, "times": [ - 0.14332938202, - 0.14745429902, - 0.14529042302, - 0.14382363202, - 0.14403296502000001, - 0.14290396502, - 0.14386063102000002, - 0.14950734002000002, - 0.14869809002, - 0.14598396502 + 0.10166903082, + 0.10268928082, + 0.10629644782, + 0.10775540582, + 0.10344915582, + 0.10428994682, + 0.14330086382, + 0.10707111382000001, + 0.10207828082, + 0.10212573882 ], "memory_usage_byte": [ - 153092096, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096 + 154370048, + 154370048, + 154370048, + 154370048, + 154370048, + 154370048, + 154370048, + 154370048, + 154370048, + 154370048 ], "exit_codes": [ 1, @@ -324,36 +324,36 @@ }, { "command": "zuban", - "mean": 0.025647144120000003, - "stddev": 0.0006462935366043392, - "median": 0.025785423520000003, - "user": 0.05075423999999999, - "system": 0.008167499999999998, - "min": 0.024608631020000004, - "max": 0.026544631020000004, + "mean": 0.027222893419999998, + "stddev": 0.00259293688373046, + "median": 0.02633890582, + "user": 0.05375396, + "system": 0.00880742, + "min": 0.024810238820000004, + "max": 0.032283405820000007, "times": [ - 0.026323965020000003, - 0.02607567402, - 0.026004173020000004, - 0.026063840020000003, - 0.026544631020000004, - 0.024608631020000004, - 0.025176340020000004, - 0.025566674020000003, - 0.025047173020000004, - 0.025060340020000002 + 0.02579944782, + 0.025381697820000004, + 0.026246738820000004, + 0.03173357282000001, + 0.032283405820000007, + 0.026880113820000003, + 0.026431072820000002, + 0.026207572820000004, + 0.024810238820000004, + 0.026455072820000005 ], "memory_usage_byte": [ - 153092096, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096, - 153092096 + 154370048, + 154370048, + 154370048, + 154370048, + 154370048, + 154370048, + 154370048, + 154370048, + 154370048, + 154370048 ], "exit_codes": [ 1, diff --git a/benchmarks/results/newtype_definition.json b/benchmarks/results/newtype_definition.json index dfd99f1c..14a87a4b 100644 --- a/benchmarks/results/newtype_definition.json +++ b/benchmarks/results/newtype_definition.json @@ -2,36 +2,36 @@ "results": [ { "command": "basilisk", - "mean": 0.0107122091, - "stddev": 0.00022648840877439072, - "median": 0.0107339467, - "user": 0.008144759999999997, - "system": 0.00196984, - "min": 0.0103205297, - "max": 0.0110795297, + "mean": 0.01095078236, + "stddev": 0.0002940317317429078, + "median": 0.01090919916, + "user": 0.00838388, + "system": 0.0019806399999999997, + "min": 0.01052536616, + "max": 0.01143815716, "times": [ - 0.0108035717, - 0.010910071700000001, - 0.010848154700000001, - 0.0103205297, - 0.0105710717, - 0.0106643217, - 0.0104956547, - 0.0110795297, - 0.0108528637, - 0.0105763217 + 0.010866574160000001, + 0.01074349016, + 0.01090394916, + 0.01114636616, + 0.011293449160000002, + 0.01143815716, + 0.01091444916, + 0.01108790716, + 0.01052536616, + 0.010588115160000001 ], "memory_usage_byte": [ - 17104896, - 17104896, - 17104896, - 17104896, - 17104896, - 17104896, - 17104896, - 17104896, - 17104896, - 17104896 + 15728640, + 15728640, + 15745024, + 15745024, + 15745024, + 15745024, + 15745024, + 15745024, + 15745024, + 15745024 ], "exit_codes": [ 1, @@ -48,36 +48,36 @@ }, { "command": "basilisk-warm", - "mean": 0.0058402592000000005, - "stddev": 0.00046800162870781406, - "median": 0.0059697802, - "user": 0.00331906, - "system": 0.0019222399999999998, - "min": 0.0051346137, - "max": 0.0064161127000000005, + "mean": 0.0047759242600000015, + "stddev": 0.00007591928795394074, + "median": 0.00479963666, + "user": 0.0030455800000000004, + "system": 0.0013267400000000003, + "min": 0.00462890816, + "max": 0.0048684491600000015, "times": [ - 0.0053767387, - 0.0051346137, - 0.0059649887, - 0.005307654700000001, - 0.006303279700000001, - 0.006033155700000001, - 0.0059745717, - 0.0055360297, - 0.0063554467, - 0.0064161127000000005 + 0.004832866160000001, + 0.00480469916, + 0.004853616160000001, + 0.00473836616, + 0.0048684491600000015, + 0.0047945741600000005, + 0.00462890816, + 0.004812199160000001, + 0.004729282160000001, + 0.004696282160000001 ], "memory_usage_byte": [ - 17547264, - 17547264, - 17547264, - 17547264, - 17547264, - 17547264, - 17547264, - 17547264, - 17547264, - 17547264 + 17317888, + 17317888, + 17317888, + 17317888, + 17317888, + 17317888, + 17317888, + 17317888, + 17317888, + 17317888 ], "exit_codes": [ 1, @@ -94,36 +94,36 @@ }, { "command": "pyright", - "mean": 0.7306701923000001, - "stddev": 0.0044755634952761796, - "median": 0.7307847587, - "user": 0.87522876, - "system": 0.05860783999999999, - "min": 0.7259638217000001, - "max": 0.7403610297000001, + "mean": 0.71054873646, + "stddev": 0.006376368048894211, + "median": 0.71016015716, + "user": 0.8581096799999999, + "system": 0.05349693999999999, + "min": 0.70040653216, + "max": 0.7201863661600001, "times": [ - 0.7317472377, - 0.7259638217000001, - 0.7260905717, - 0.7324373637, - 0.7321386547000001, - 0.7298222797, - 0.7339278627000001, - 0.7272950717000001, - 0.7403610297000001, - 0.7269180297000001 + 0.7162451151600001, + 0.71484632416, + 0.7201863661600001, + 0.7082712821600001, + 0.70040653216, + 0.7099048241600001, + 0.7162819491600001, + 0.7026293241600001, + 0.71041549016, + 0.70630015716 ], "memory_usage_byte": [ - 152715264, - 152715264, - 152715264, - 152715264, - 152715264, - 152715264, - 152780800, - 152780800, - 152780800, - 152780800 + 152322048, + 152567808, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720 ], "exit_codes": [ 1, @@ -140,36 +140,36 @@ }, { "command": "mypy", - "mean": 0.6432361674000001, - "stddev": 0.009206649005495138, - "median": 0.6404390717, - "user": 0.57933656, - "system": 0.05861164, - "min": 0.6348103217000001, - "max": 0.6614277797000001, + "mean": 0.6166287951600001, + "stddev": 0.004500089964465524, + "median": 0.6159320326600001, + "user": 0.55209638, + "system": 0.057868239999999994, + "min": 0.61030832416, + "max": 0.62589619916, "times": [ - 0.6414130717000001, - 0.6614277797000001, - 0.6394650717, - 0.6348103217000001, - 0.6582071967, - 0.6377576547, - 0.6354805297, - 0.6424969877000001, - 0.6434069047000001, - 0.6378961557 + 0.6154243661600001, + 0.6114158241600001, + 0.61893540816, + 0.6164396991600001, + 0.62056074116, + 0.6151004901600001, + 0.6175342411600001, + 0.62589619916, + 0.61467265816, + 0.61030832416 ], "memory_usage_byte": [ - 152780800, - 152780800, - 152780800, - 152780800, - 152780800, - 152780800, - 152780800, - 152780800, - 152780800, - 152780800 + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720 ], "exit_codes": [ 1, @@ -186,36 +186,36 @@ }, { "command": "mypy-warm", - "mean": 0.1685885133, - "stddev": 0.0027297383575036463, - "median": 0.1683735922, - "user": 0.13922916000000002, - "system": 0.026982739999999998, - "min": 0.1644834877, - "max": 0.1721846137, + "mean": 0.16432273656000002, + "stddev": 0.0035001013915662267, + "median": 0.16338215716, + "user": 0.13565827999999996, + "system": 0.02654664, + "min": 0.15997936616000002, + "max": 0.16995749116000003, "times": [ - 0.1700668217, - 0.1665231557, - 0.1652558217, - 0.1689182377, - 0.1678289467, - 0.1644834877, - 0.1674833217, - 0.1721846137, - 0.17144352969999999, - 0.1716971967 + 0.15997936616000002, + 0.16347169916, + 0.16995749116000003, + 0.16388753216000002, + 0.16210369916000003, + 0.16851090716000003, + 0.16329261516000002, + 0.16044011516, + 0.16860349116, + 0.16298044916 ], "memory_usage_byte": [ - 152780800, - 152780800, - 152780800, - 152780800, - 152780800, - 152780800, - 152780800, - 152780800, - 152780800, - 152780800 + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720 ], "exit_codes": [ 1, @@ -232,82 +232,82 @@ }, { "command": "ty", - "mean": 0.022900796699999997, - "stddev": 0.0010433699542118319, - "median": 0.022718592200000002, - "user": 0.01600576, - "system": 0.005052039999999999, - "min": 0.021420780700000003, - "max": 0.024624363700000002, + "mean": 0.04455511966, + "stddev": 0.0015669396868541244, + "median": 0.04452867766, + "user": 0.03452238, + "system": 0.008377140000000002, + "min": 0.042076282160000006, + "max": 0.046752324160000004, "times": [ - 0.021420780700000003, - 0.0227730717, - 0.022664112700000003, - 0.024624363700000002, - 0.0231006547, - 0.021960738700000003, - 0.0218173217, - 0.022656196700000002, - 0.024068071700000002, - 0.0239226547 + 0.043320949160000004, + 0.04293311516000001, + 0.045018115160000004, + 0.046752324160000004, + 0.04537819916000001, + 0.04598328216000001, + 0.042076282160000006, + 0.04403924016000001, + 0.043734032160000005, + 0.04631565716000001 ], "memory_usage_byte": [ - 152780800, - 152780800, - 152780800, - 152780800, - 152780800, - 152780800, - 152780800, - 152780800, - 152780800, - 152780800 + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720 ], "exit_codes": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 ] }, { "command": "pyrefly", - "mean": 0.15684753840000001, - "stddev": 0.00204180320581218, - "median": 0.15646019719999998, - "user": 0.11335476, - "system": 0.04227434, - "min": 0.1545724467, - "max": 0.1616783217, + "mean": 0.11810431606, + "stddev": 0.001795503371001041, + "median": 0.11825478266, + "user": 0.09741408, + "system": 0.021687639999999994, + "min": 0.11614807416, + "max": 0.12165082416, "times": [ - 0.15495157169999998, - 0.1573293217, - 0.15686815569999998, - 0.1616783217, - 0.15837490469999999, - 0.15578594669999998, - 0.1545724467, - 0.15704032169999999, - 0.1558221547, - 0.15605223869999998 + 0.11970953316, + 0.11620511516, + 0.11695874116, + 0.11849836616000001, + 0.11801119916000001, + 0.11889878316000001, + 0.11615040816000001, + 0.11614807416, + 0.11881211616000001, + 0.12165082416 ], "memory_usage_byte": [ - 152780800, - 152780800, - 152780800, - 152780800, - 152780800, - 152780800, - 152780800, - 152780800, - 152780800, - 152780800 + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720 ], "exit_codes": [ 1, @@ -324,36 +324,36 @@ }, { "command": "zuban", - "mean": 0.035624355100000005, - "stddev": 0.000727017775247652, - "median": 0.0357399262, - "user": 0.061016159999999986, - "system": 0.008937839999999999, - "min": 0.0343306967, - "max": 0.0365091967, + "mean": 0.035267853360000004, + "stddev": 0.0007638898507238098, + "median": 0.03538478266, + "user": 0.05951517999999999, + "system": 0.00965984, + "min": 0.03403240716000001, + "max": 0.036267782160000005, "times": [ - 0.0353585717, - 0.0350758637, - 0.0359044467, - 0.0361562387, - 0.0355754057, - 0.036183696700000004, - 0.0343306967, - 0.0363644877, - 0.0347849467, - 0.0365091967 + 0.034648158160000005, + 0.03419236616000001, + 0.035229574160000006, + 0.03403240716000001, + 0.03560244916000001, + 0.03596678216000001, + 0.035219449160000006, + 0.035539991160000006, + 0.03597957416000001, + 0.036267782160000005 ], "memory_usage_byte": [ - 152780800, - 152780800, - 152780800, - 152780800, - 152780800, - 152780800, - 152780800, - 152780800, - 152780800, - 152780800 + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720, + 152862720 ], "exit_codes": [ 1, diff --git a/benchmarks/results/overloads_evaluation.json b/benchmarks/results/overloads_evaluation.json index 0b41bed0..f822c68b 100644 --- a/benchmarks/results/overloads_evaluation.json +++ b/benchmarks/results/overloads_evaluation.json @@ -2,36 +2,36 @@ "results": [ { "command": "basilisk", - "mean": 0.012478338360000002, - "stddev": 0.00013192038188847074, - "median": 0.012520879960000002, - "user": 0.01005566, - "system": 0.0018857600000000002, - "min": 0.01221625496, - "max": 0.012609838960000001, + "mean": 0.012207098860000001, + "stddev": 0.0002595908055714056, + "median": 0.01216624076, + "user": 0.010045800000000002, + "system": 0.0016873000000000005, + "min": 0.01193590676, + "max": 0.012838073760000002, "times": [ - 0.012399879960000002, - 0.012609838960000001, - 0.012582796960000002, - 0.01260358796, - 0.01221625496, - 0.012518837960000001, - 0.012552213960000001, - 0.012308920960000002, - 0.012468129960000001, - 0.01252292196 + 0.012000782760000002, + 0.01220078276, + 0.012208282760000001, + 0.012274656760000002, + 0.01213169876, + 0.01210665676, + 0.01193590676, + 0.012375573760000001, + 0.011998573760000002, + 0.012838073760000002 ], "memory_usage_byte": [ - 17268736, - 17268736, - 17268736, - 17268736, - 17268736, - 17268736, - 17268736, - 17268736, - 17268736, - 17268736 + 16023552, + 16171008, + 16171008, + 16171008, + 16171008, + 16171008, + 16171008, + 16171008, + 16171008, + 16171008 ], "exit_codes": [ 1, @@ -48,36 +48,36 @@ }, { "command": "basilisk-warm", - "mean": 0.004252134160000001, - "stddev": 0.0003087591181148609, - "median": 0.004188692460000001, - "user": 0.00234486, - "system": 0.0014714600000000004, - "min": 0.0038506719600000013, - "max": 0.004647087960000001, + "mean": 0.00438709056, + "stddev": 0.00021794711485781235, + "median": 0.00434461526, + "user": 0.0024176, + "system": 0.0015907999999999999, + "min": 0.0040627827600000005, + "max": 0.004784115760000001, "times": [ - 0.0038506719600000013, - 0.003956087960000001, - 0.004646046960000001, - 0.004594629960000001, - 0.003977546960000001, - 0.004454629960000001, - 0.004165296960000001, - 0.004017254960000001, - 0.004647087960000001, - 0.004212087960000001 + 0.004784115760000001, + 0.0040627827600000005, + 0.00425378176, + 0.0043471987600000005, + 0.00444253276, + 0.00434203176, + 0.004236865760000001, + 0.00465586576, + 0.00422282376, + 0.004522906760000001 ], "memory_usage_byte": [ - 18071552, - 18071552, - 18071552, - 18071552, - 18071552, - 18071552, - 18071552, - 18071552, - 18071552, - 18071552 + 16400384, + 16400384, + 16400384, + 16400384, + 16400384, + 16400384, + 16400384, + 16400384, + 16400384, + 16400384 ], "exit_codes": [ 1, @@ -94,36 +94,36 @@ }, { "command": "pyright", - "mean": 0.6092199466600001, - "stddev": 0.003563058499913645, - "median": 0.60987987996, - "user": 0.88616316, - "system": 0.05975396000000001, - "min": 0.6033210469600001, - "max": 0.6139253389600001, + "mean": 0.59298739496, + "stddev": 0.006155826252170325, + "median": 0.59183321976, + "user": 0.8629507999999999, + "system": 0.0553415, + "min": 0.58198349076, + "max": 0.60111519876, "times": [ - 0.6042195049600001, - 0.60892200496, - 0.6115222129600001, - 0.6109153379600001, - 0.61083775496, - 0.6033210469600001, - 0.6068383799600001, - 0.6139253389600001, - 0.6085624219600001, - 0.6131354629600001 + 0.59612582476, + 0.59221824076, + 0.58663903176, + 0.60044911576, + 0.59120661576, + 0.60111519876, + 0.59871782476, + 0.59144819876, + 0.58198349076, + 0.58997040776 ], "memory_usage_byte": [ - 162283520, - 162283520, - 162283520, - 162283520, - 162283520, - 162283520, - 162283520, - 162349056, - 162349056, - 162349056 + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352 ], "exit_codes": [ 1, @@ -140,36 +140,36 @@ }, { "command": "mypy", - "mean": 0.6376978339600001, - "stddev": 0.007339600851195098, - "median": 0.6356546924600001, - "user": 0.57443166, - "system": 0.05886226, - "min": 0.62944383796, - "max": 0.6521138379600001, + "mean": 0.60376366166, + "stddev": 0.0041634299885972646, + "median": 0.60420440776, + "user": 0.5450905, + "system": 0.055246099999999985, + "min": 0.59477944976, + "max": 0.61009940676, "times": [ - 0.6427552549600001, - 0.6466250049600001, - 0.6343720459600001, - 0.6379638379600001, - 0.6521138379600001, - 0.62944383796, - 0.6312955879600001, - 0.63693733896, - 0.63364787996, - 0.6318237129600001 + 0.59477944976, + 0.60433649076, + 0.60392919876, + 0.59926707376, + 0.60478786576, + 0.6037403247600001, + 0.61009940676, + 0.60584349076, + 0.6040723247600001, + 0.60678099076 ], "memory_usage_byte": [ - 162349056, - 162349056, - 162349056, - 162349056, - 162349056, - 162349056, - 162349056, - 162349056, - 162349056, - 162349056 + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352 ], "exit_codes": [ 1, @@ -186,36 +186,36 @@ }, { "command": "mypy-warm", - "mean": 0.16675220906, - "stddev": 0.0026869189085075586, - "median": 0.16613058796000002, - "user": 0.13759255999999997, - "system": 0.026611559999999996, - "min": 0.16368750496, - "max": 0.17261704596000002, + "mean": 0.16197871576000003, + "stddev": 0.004256431724316221, + "median": 0.15994249076, + "user": 0.1337895, + "system": 0.026306199999999995, + "min": 0.15713107376000002, + "max": 0.16859078276, "times": [ - 0.16998425496000003, - 0.17261704596000002, - 0.16570675496, - 0.16675221296, - 0.16368750496, - 0.16458871396000002, - 0.16478246396000001, - 0.16714196296000003, - 0.16641575496000002, - 0.16584542096000002 + 0.15713107376000002, + 0.15886961576, + 0.16859078276, + 0.16043019876, + 0.16150407476, + 0.16762624076000002, + 0.15941757376000001, + 0.15945478276, + 0.16755194876, + 0.15921086576000001 ], "memory_usage_byte": [ - 162349056, - 162349056, - 162349056, - 162349056, - 162349056, - 162349056, - 162349056, - 162349056, - 162349056, - 162349056 + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352 ], "exit_codes": [ 1, @@ -232,36 +232,36 @@ }, { "command": "ty", - "mean": 0.05907865496000001, - "stddev": 0.0013348130247827896, - "median": 0.058899525960000004, - "user": 0.048884859999999995, - "system": 0.00803986, - "min": 0.05789425496000001, - "max": 0.06244129596000001, + "mean": 0.060254924060000006, + "stddev": 0.0024959079363607254, + "median": 0.059445594760000005, + "user": 0.0491716, + "system": 0.009103799999999999, + "min": 0.05797478176, + "max": 0.06555799076, "times": [ - 0.05813729696000001, - 0.058936671960000006, - 0.06244129596000001, - 0.059017088960000005, - 0.05927171296000001, - 0.05789425496000001, - 0.05986921296000001, - 0.05797212996000001, - 0.05886237996000001, - 0.05838450496000001 + 0.059155240760000005, + 0.06312553276, + 0.06166711576000001, + 0.058502781760000004, + 0.058293407760000006, + 0.05822744976000001, + 0.05797478176, + 0.06030899076000001, + 0.059735948760000004, + 0.06555799076 ], "memory_usage_byte": [ - 162349056, - 162349056, - 162349056, - 162349056, - 162349056, - 162349056, - 162349056, - 162349056, - 162349056, - 162349056 + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352 ], "exit_codes": [ 1, @@ -278,36 +278,36 @@ }, { "command": "pyrefly", - "mean": 0.15757906756, - "stddev": 0.0018431434002207088, - "median": 0.15708990096000003, - "user": 0.11584016, - "system": 0.04047666, - "min": 0.15489812996000002, - "max": 0.16063796396000002, + "mean": 0.11572912826000001, + "stddev": 0.002062709748617322, + "median": 0.11543359476000001, + "user": 0.0985672, + "system": 0.018146100000000005, + "min": 0.11314924076, + "max": 0.11870228276, "times": [ - 0.15489812996000002, - 0.15691329696000003, - 0.15673542196, - 0.15730754696000002, - 0.15968604596000002, - 0.15978408796000002, - 0.16063796396000002, - 0.15640662996000002, - 0.15615504696000002, - 0.15726650496000003 + 0.11470424076, + 0.11870228276, + 0.11736374076, + 0.11391482376, + 0.11402136576, + 0.11672911576, + 0.11862653276, + 0.11314924076, + 0.11391699076, + 0.11616294876000001 ], "memory_usage_byte": [ - 162349056, - 162349056, - 162349056, - 162349056, - 162349056, - 162349056, - 162349056, - 162349056, - 162349056, - 162349056 + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352 ], "exit_codes": [ 1, @@ -324,36 +324,36 @@ }, { "command": "zuban", - "mean": 0.03474563416, - "stddev": 0.0019868367662274843, - "median": 0.03419879646, - "user": 0.06046236000000001, - "system": 0.00866096, - "min": 0.03349821296, - "max": 0.04032000496, + "mean": 0.033621007460000005, + "stddev": 0.0007162087226587344, + "median": 0.03357484526000001, + "user": 0.05824230000000001, + "system": 0.0087707, + "min": 0.03255124076, + "max": 0.034869115760000007, "times": [ - 0.034226379960000004, - 0.03417121296, - 0.03432404696000001, - 0.03349821296, - 0.03464321396, - 0.034465504960000004, - 0.033850254960000006, - 0.04032000496, - 0.03381471296000001, - 0.034142796960000006 + 0.03313086576, + 0.03344294976000001, + 0.033706740760000006, + 0.03278990676000001, + 0.033374865760000004, + 0.034869115760000007, + 0.03255124076, + 0.03412261576, + 0.03383499076, + 0.034386782760000005 ], "memory_usage_byte": [ - 162349056, - 162349056, - 162349056, - 162349056, - 162349056, - 162349056, - 162349056, - 162349056, - 162349056, - 162349056 + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352, + 162660352 ], "exit_codes": [ 1, diff --git a/benchmarks/results/override_compatibility.json b/benchmarks/results/override_compatibility.json index c30f3a2d..ad94f4d4 100644 --- a/benchmarks/results/override_compatibility.json +++ b/benchmarks/results/override_compatibility.json @@ -2,36 +2,36 @@ "results": [ { "command": "basilisk", - "mean": 0.014336011720000003, - "stddev": 0.00047798255256418896, - "median": 0.014233432220000002, - "user": 0.01154026, - "system": 0.0022455999999999995, - "min": 0.013659327720000003, - "max": 0.015357786720000002, + "mean": 0.0135687827, + "stddev": 0.0005234277858708459, + "median": 0.013622657800000002, + "user": 0.0113393, + "system": 0.0017219799999999997, + "min": 0.012815740800000001, + "max": 0.014593865800000003, "times": [ - 0.014822120720000002, - 0.014297494720000001, - 0.014492203720000003, - 0.015357786720000002, - 0.013924244720000003, - 0.01413661972, - 0.01439257872, - 0.013659327720000003, - 0.014108370720000003, - 0.014169369720000002 + 0.0132190328, + 0.012882782800000001, + 0.012815740800000001, + 0.013510615800000002, + 0.013616282800000002, + 0.013748074800000001, + 0.014593865800000003, + 0.013648365800000001, + 0.0140240328, + 0.013629032800000001 ], "memory_usage_byte": [ - 17743872, - 17743872, - 17743872, - 17743872, - 17743872, - 17743872, - 17743872, - 17743872, - 17776640, - 17776640 + 16842752, + 16891904, + 16891904, + 16891904, + 16891904, + 16891904, + 16891904, + 16891904, + 16891904, + 16891904 ], "exit_codes": [ 1, @@ -48,36 +48,36 @@ }, { "command": "basilisk-warm", - "mean": 0.0036901076200000016, - "stddev": 0.00041978648245809974, - "median": 0.003607140720000001, - "user": 0.00205486, - "system": 0.0012979, - "min": 0.0030577867200000004, - "max": 0.004588244720000001, + "mean": 0.004386836600000001, + "stddev": 0.0004610226143276608, + "median": 0.0042526783, + "user": 0.0022332000000000003, + "system": 0.0016111799999999996, + "min": 0.0040315738, + "max": 0.0055576158, "times": [ - 0.003432578720000001, - 0.003962828720000001, - 0.0030577867200000004, - 0.003465786720000001, - 0.004588244720000001, - 0.0035784947200000012, - 0.003635786720000001, - 0.003956453720000001, - 0.0038234537200000007, - 0.0033996617200000006 + 0.0047127408, + 0.0042387408, + 0.004035448800000001, + 0.004323115800000001, + 0.004051907800000001, + 0.0042666158, + 0.0055576158, + 0.0042111578, + 0.004439448800000001, + 0.0040315738 ], "memory_usage_byte": [ - 17874944, - 17874944, - 17874944, - 17874944, - 17874944, - 17874944, - 17874944, - 17874944, - 17874944, - 17874944 + 16973824, + 16973824, + 16973824, + 16973824, + 16973824, + 16973824, + 16973824, + 16973824, + 16973824, + 16973824 ], "exit_codes": [ 1, @@ -94,36 +94,36 @@ }, { "command": "pyright", - "mean": 0.65495802832, - "stddev": 0.006055648696812404, - "median": 0.65489187022, - "user": 0.9290088599999999, - "system": 0.059956599999999985, - "min": 0.6421847447200001, - "max": 0.66566020272, + "mean": 0.6345935408, + "stddev": 0.007819392341694603, + "median": 0.6358377403, + "user": 0.9059246999999999, + "system": 0.055245880000000004, + "min": 0.6165085328, + "max": 0.6454151158, "times": [ - 0.65305041172, - 0.65446266172, - 0.65068407772, - 0.6589519957200001, - 0.6421847447200001, - 0.65532107872, - 0.6572371207200001, - 0.6578423277200001, - 0.65418566172, - 0.66566020272 + 0.6165085328, + 0.6316734908, + 0.6357373238, + 0.6311494488, + 0.6382789908000001, + 0.6312824498, + 0.6359381568, + 0.6397301158, + 0.6454151158, + 0.6402217828 ], "memory_usage_byte": [ - 164069376, - 164069376, - 164069376, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496 + 161464320, + 161464320, + 161726464, + 161726464, + 161726464, + 163135488, + 163725312, + 163725312, + 163725312, + 163725312 ], "exit_codes": [ 1, @@ -140,36 +140,36 @@ }, { "command": "mypy", - "mean": 0.62401524082, - "stddev": 0.008830935414328986, - "median": 0.62078672422, - "user": 0.56059456, - "system": 0.0592761, - "min": 0.6160117447200001, - "max": 0.6452259947200001, + "mean": 0.5976369782000001, + "stddev": 0.019605766560516037, + "median": 0.5930733238, + "user": 0.533723, + "system": 0.05647338, + "min": 0.5853753658, + "max": 0.6519347408, "times": [ - 0.6203910367200001, - 0.6275157867200001, - 0.6160117447200001, - 0.61945624472, - 0.6452259947200001, - 0.61785407772, - 0.61718707872, - 0.62118241172, - 0.63101870372, - 0.62430932872 + 0.5975118658, + 0.5930444488, + 0.5954889078, + 0.5870852818, + 0.5871349488000001, + 0.5976966578, + 0.5931021988, + 0.5879953658, + 0.5853753658, + 0.6519347408 ], "memory_usage_byte": [ - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496 + 163725312, + 163725312, + 163725312, + 163725312, + 163725312, + 163725312, + 163725312, + 163725312, + 163725312, + 163725312 ], "exit_codes": [ 1, @@ -186,36 +186,36 @@ }, { "command": "mypy-warm", - "mean": 0.16717117832, - "stddev": 0.004248920192579893, - "median": 0.16589157822, - "user": 0.13742116, - "system": 0.0271683, - "min": 0.16307370372000002, - "max": 0.17779453672, + "mean": 0.1629019825, + "stddev": 0.0033214111393864254, + "median": 0.1621395118, + "user": 0.13373439999999998, + "system": 0.02697098, + "min": 0.1589882408, + "max": 0.1680762828, "times": [ - 0.17779453672, - 0.16849066172000002, - 0.16606257872000002, - 0.16481649472, - 0.16778649472, - 0.16307370372000002, - 0.16342899572, - 0.16572057772, - 0.16529220272, - 0.16924553672 + 0.1589882408, + 0.1602989078, + 0.1680226158, + 0.1613617828, + 0.1658268658, + 0.1625571578, + 0.1617218658, + 0.1680762828, + 0.1594795318, + 0.1626865738 ], "memory_usage_byte": [ - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496 + 163725312, + 163725312, + 163725312, + 163725312, + 163725312, + 163725312, + 163725312, + 163725312, + 163725312, + 163725312 ], "exit_codes": [ 1, @@ -232,36 +232,36 @@ }, { "command": "ty", - "mean": 0.04074115752, - "stddev": 0.0007434300812624945, - "median": 0.04061922422, - "user": 0.03215976, - "system": 0.005874, - "min": 0.03919645272, - "max": 0.041974202720000006, + "mean": 0.0407515991, + "stddev": 0.00175082566734765, + "median": 0.0404534908, + "user": 0.031217699999999998, + "system": 0.00716888, + "min": 0.0380298658, + "max": 0.0438106578, "times": [ - 0.040485911720000003, - 0.04057099472, - 0.04131720372, - 0.040287453720000005, - 0.04058028672, - 0.041039036720000005, - 0.03919645272, - 0.041974202720000006, - 0.04065816172, - 0.04130187072000001 + 0.0392280328, + 0.0380298658, + 0.0399665328, + 0.0396322818, + 0.0409218658, + 0.0399851158, + 0.0429649908, + 0.0438106578, + 0.0416133658, + 0.0413632818 ], "memory_usage_byte": [ - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496 + 163725312, + 163725312, + 163725312, + 163725312, + 163725312, + 163725312, + 163725312, + 163725312, + 163725312, + 163725312 ], "exit_codes": [ 1, @@ -278,36 +278,36 @@ }, { "command": "pyrefly", - "mean": 0.15078456161999998, - "stddev": 0.0026569541771308334, - "median": 0.14983351572, - "user": 0.11014746, - "system": 0.039712199999999996, - "min": 0.14791411972000001, - "max": 0.15541045272, + "mean": 0.10923319520000001, + "stddev": 0.0020768637888021434, + "median": 0.10867459530000001, + "user": 0.09198299999999998, + "system": 0.01757228, + "min": 0.10616374080000002, + "max": 0.11212815780000002, "times": [ - 0.14916241172, - 0.15462278672000002, - 0.14791411972000001, - 0.14817603672000002, - 0.15108745372000001, - 0.15282107872, - 0.15541045272, - 0.15030220372, - 0.14936482772, - 0.14898424472000002 + 0.11008436580000001, + 0.10779399080000002, + 0.10772057480000001, + 0.11184682480000001, + 0.10902961580000001, + 0.11212815780000002, + 0.10831957480000001, + 0.1116129908, + 0.10763211580000001, + 0.10616374080000002 ], "memory_usage_byte": [ - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496 + 163725312, + 163725312, + 163725312, + 163725312, + 163725312, + 163725312, + 163725312, + 163725312, + 163725312, + 163725312 ], "exit_codes": [ 1, @@ -324,36 +324,36 @@ }, { "command": "zuban", - "mean": 0.02810011612, - "stddev": 0.0008196593319110885, - "median": 0.027785953720000003, - "user": 0.055444059999999996, - "system": 0.008628600000000002, - "min": 0.027140744720000002, - "max": 0.02973399572, + "mean": 0.0281505657, + "stddev": 0.0012460989279156557, + "median": 0.0280736573, + "user": 0.0524092, + "system": 0.00864348, + "min": 0.0263326158, + "max": 0.0305876578, "times": [ - 0.02870337072, - 0.029129119720000003, - 0.027499370720000003, - 0.02763545372, - 0.02973399572, - 0.027613244720000003, - 0.027728120720000003, - 0.027973953720000003, - 0.027140744720000002, - 0.027843786720000003 + 0.0305876578, + 0.029635240800000004, + 0.0275769488, + 0.028510781800000003, + 0.0280705738, + 0.0263326158, + 0.0268582408, + 0.0275497408, + 0.0280767408, + 0.0283071158 ], "memory_usage_byte": [ - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496, - 164970496 + 163725312, + 163725312, + 163725312, + 163725312, + 163725312, + 163725312, + 163725312, + 163725312, + 163725312, + 163725312 ], "exit_codes": [ 1, diff --git a/benchmarks/results/protocols_definition.json b/benchmarks/results/protocols_definition.json index 8ba8f7a9..a2181dd3 100644 --- a/benchmarks/results/protocols_definition.json +++ b/benchmarks/results/protocols_definition.json @@ -2,36 +2,36 @@ "results": [ { "command": "basilisk", - "mean": 0.008839346459999998, - "stddev": 0.0005278774992401281, - "median": 0.00864985466, - "user": 0.0065386, - "system": 0.0018239200000000001, - "min": 0.008333500159999998, - "max": 0.009724667159999999, + "mean": 0.00837105086, + "stddev": 0.00015885068299772732, + "median": 0.008319075660000001, + "user": 0.006482520000000001, + "system": 0.0014779, + "min": 0.00817647166, + "max": 0.00862751366, "times": [ - 0.008333500159999998, - 0.008395043159999999, - 0.00868841716, - 0.008374584159999999, - 0.008556834159999999, - 0.008797125159999999, - 0.008611292159999999, - 0.00971858416, - 0.009724667159999999, - 0.00919341716 + 0.00828384666, + 0.00862751366, + 0.00825605466, + 0.00858401366, + 0.00819784666, + 0.00829809666, + 0.00817647166, + 0.00848005466, + 0.00834005466, + 0.00846655566 ], "memory_usage_byte": [ - 14876672, - 14876672, - 14876672, - 14876672, - 14876672, - 14876672, - 14876672, - 14876672, - 14876672, - 14876672 + 14581760, + 14614528, + 14614528, + 14614528, + 14614528, + 14614528, + 14614528, + 14614528, + 14614528, + 14614528 ], "exit_codes": [ 1, @@ -48,36 +48,36 @@ }, { "command": "basilisk-warm", - "mean": 0.004177209360000001, - "stddev": 0.00022422887045546415, - "median": 0.00421166766, - "user": 0.0023312000000000003, - "system": 0.00144412, - "min": 0.003846709159999999, - "max": 0.0044481261599999995, + "mean": 0.003798346660000001, + "stddev": 0.00021111621334495156, + "median": 0.0037622216600000005, + "user": 0.00227922, + "system": 0.0012213, + "min": 0.0035584296600000005, + "max": 0.004242971660000001, "times": [ - 0.00438133416, - 0.00408087616, - 0.0038802511599999993, - 0.0043409591599999995, - 0.00396170916, - 0.003846709159999999, - 0.004408793159999999, - 0.0044481261599999995, - 0.00423895916, - 0.004184376159999999 + 0.0035836386600000005, + 0.00366159666, + 0.0035584296600000005, + 0.0036747636600000006, + 0.004242971660000001, + 0.00402555466, + 0.00390151366, + 0.0037819716600000007, + 0.0037424716600000002, + 0.0038105546600000002 ], "memory_usage_byte": [ - 15040512, - 15040512, - 15040512, - 15040512, - 15040512, - 15040512, - 15040512, - 15040512, - 15040512, - 15040512 + 14860288, + 14860288, + 14860288, + 14860288, + 14860288, + 14860288, + 14860288, + 14860288, + 14860288, + 14860288 ], "exit_codes": [ 1, @@ -94,36 +94,36 @@ }, { "command": "pyright", - "mean": 0.5761742634600001, - "stddev": 0.006786283865538624, - "median": 0.57584350116, - "user": 0.7349823999999998, - "system": 0.05532482, - "min": 0.56436362516, - "max": 0.58892087616, + "mean": 0.56296472596, + "stddev": 0.010143682729219746, + "median": 0.56101405516, + "user": 0.7143423199999999, + "system": 0.05108349999999999, + "min": 0.5490009716600001, + "max": 0.5879576376600001, "times": [ - 0.57127287616, - 0.58892087616, - 0.57880270916, - 0.58281525116, - 0.57144620916, - 0.57626025116, - 0.57542675116, - 0.56436362516, - 0.57891483416, - 0.57351925116 + 0.56016705566, + 0.5642465966600001, + 0.5879576376600001, + 0.56054526366, + 0.56241609666, + 0.5685789306600001, + 0.56148284666, + 0.5490009716600001, + 0.5578142636600001, + 0.55743759666 ], "memory_usage_byte": [ - 152289280, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496 + 153059328, + 153059328, + 153059328, + 153059328, + 153059328, + 153059328, + 153059328, + 153059328, + 153141248, + 153141248 ], "exit_codes": [ 1, @@ -140,36 +140,36 @@ }, { "command": "mypy", - "mean": 0.60372008426, - "stddev": 0.011108228274601008, - "median": 0.60191998016, - "user": 0.5399907999999999, - "system": 0.05909151999999999, - "min": 0.5879662921600001, - "max": 0.62942925116, + "mean": 0.5646541757600001, + "stddev": 0.005767274272875748, + "median": 0.5624123256600001, + "user": 0.5068696199999999, + "system": 0.05433189999999999, + "min": 0.55866851366, + "max": 0.5753735966600001, "times": [ - 0.59808900016, - 0.62942925116, - 0.6106990841600001, - 0.59857216816, - 0.60269525116, - 0.5879662921600001, - 0.60558112616, - 0.6011447091600001, - 0.60757733416, - 0.59544662616 + 0.56070151266, + 0.56053047166, + 0.56444342966, + 0.55866851366, + 0.55988309666, + 0.57179726366, + 0.57031922166, + 0.5753735966600001, + 0.5623268466600001, + 0.5624978046600001 ], "memory_usage_byte": [ - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496 + 153141248, + 153141248, + 153141248, + 153141248, + 153141248, + 153141248, + 153141248, + 153141248, + 153141248, + 153141248 ], "exit_codes": [ 1, @@ -186,36 +186,36 @@ }, { "command": "mypy-warm", - "mean": 0.16671325106, - "stddev": 0.0017355590089452415, - "median": 0.16705506366, - "user": 0.13773570000000002, - "system": 0.026765820000000003, - "min": 0.16354150116000002, - "max": 0.16880312616, + "mean": 0.16084299665999996, + "stddev": 0.0019741401470640893, + "median": 0.16157003416, + "user": 0.13316072, + "system": 0.0256237, + "min": 0.15749901266, + "max": 0.16365488766, "times": [ - 0.16682262616000001, - 0.16880312616, - 0.16803362516000003, - 0.16808654316000002, - 0.16354150116000002, - 0.16526275116000003, - 0.16684075116000002, - 0.16448170916000002, - 0.16799050116000003, - 0.16726937616 + 0.16216184666, + 0.16181834666, + 0.16132172166, + 0.16022276366000002, + 0.15933034666, + 0.15749901266, + 0.16234343066, + 0.16365488766, + 0.15818830466, + 0.16188930566 ], "memory_usage_byte": [ - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496 + 153141248, + 153141248, + 153141248, + 153141248, + 153141248, + 153141248, + 153141248, + 153141248, + 153141248, + 153141248 ], "exit_codes": [ 1, @@ -232,36 +232,36 @@ }, { "command": "ty", - "mean": 0.035007096859999996, - "stddev": 0.00156625634167428, - "median": 0.03494468816, - "user": 0.026754100000000003, - "system": 0.00577602, - "min": 0.03274041816, - "max": 0.03870125116, + "mean": 0.03409182586, + "stddev": 0.001548233132678295, + "median": 0.034407346660000004, + "user": 0.026096319999999996, + "system": 0.0066818, + "min": 0.03224626366, + "max": 0.03690134666000001, "times": [ - 0.035712376159999996, - 0.03870125116, - 0.03525466816, - 0.03508083416, - 0.03480854216, - 0.03518700116, - 0.03274041816, - 0.03479854216, - 0.03415341816, - 0.03363391716 + 0.03438826366000001, + 0.03253597166, + 0.03256951266000001, + 0.03224626366, + 0.035874388660000006, + 0.03280159666000001, + 0.03442642966000001, + 0.034550680660000006, + 0.03690134666000001, + 0.03462380466000001 ], "memory_usage_byte": [ - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496 + 153141248, + 153141248, + 153141248, + 153141248, + 153141248, + 153141248, + 153141248, + 153141248, + 153141248, + 153141248 ], "exit_codes": [ 1, @@ -278,36 +278,36 @@ }, { "command": "pyrefly", - "mean": 0.14089808836000003, - "stddev": 0.002741243820625016, - "median": 0.14080835466000002, - "user": 0.10138090000000002, - "system": 0.039121319999999994, - "min": 0.13653479316000003, - "max": 0.14561666716000002, + "mean": 0.10218039666, + "stddev": 0.0024533372958716647, + "median": 0.10113440916, + "user": 0.08528771999999998, + "system": 0.017716100000000002, + "min": 0.09921809666, + "max": 0.10610176366, "times": [ - 0.13963266716000003, - 0.13653479316000003, - 0.13813800116000002, - 0.14196145916000003, - 0.14112070916000002, - 0.14443783416000003, - 0.14049600016000002, - 0.14173616816, - 0.13930658416000002, - 0.14561666716000002 + 0.09971222166, + 0.10121480466, + 0.10267938866000001, + 0.10610176366, + 0.10085117966, + 0.09921809666, + 0.10095922166, + 0.10394784666000001, + 0.10606542966, + 0.10105401366 ], "memory_usage_byte": [ - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496 + 153141248, + 153141248, + 153141248, + 153141248, + 153141248, + 153141248, + 153141248, + 153141248, + 153141248, + 153141248 ], "exit_codes": [ 1, @@ -324,36 +324,36 @@ }, { "command": "zuban", - "mean": 0.027035959160000005, - "stddev": 0.0009116049892588098, - "median": 0.027034063160000003, - "user": 0.05196529999999999, - "system": 0.008396919999999999, - "min": 0.025792459160000003, - "max": 0.02901841716, + "mean": 0.026878275759999997, + "stddev": 0.0012060813516862915, + "median": 0.02626851316, + "user": 0.052439519999999996, + "system": 0.0090382, + "min": 0.02546709666, + "max": 0.02887534666, "times": [ - 0.02631587616, - 0.027250500160000003, - 0.02749345916, - 0.02710104216, - 0.026494668160000002, - 0.025792459160000003, - 0.02696708416, - 0.02901841716, - 0.027639917160000003, - 0.02628616816 + 0.02852188766, + 0.026436096660000002, + 0.02597151366, + 0.02610092966, + 0.027672096660000003, + 0.02604309666, + 0.02599326366, + 0.02887534666, + 0.027701429660000002, + 0.02546709666 ], "memory_usage_byte": [ - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496, - 152682496 + 153141248, + 153141248, + 153141248, + 153141248, + 153141248, + 153141248, + 153141248, + 153141248, + 153141248, + 153141248 ], "exit_codes": [ 1, diff --git a/benchmarks/results/returns_compatibility.json b/benchmarks/results/returns_compatibility.json index 6823aad8..9d8a7eb9 100644 --- a/benchmarks/results/returns_compatibility.json +++ b/benchmarks/results/returns_compatibility.json @@ -2,36 +2,36 @@ "results": [ { "command": "basilisk", - "mean": 0.007373712539999999, - "stddev": 0.0004205409218320298, - "median": 0.00730541704, - "user": 0.00500438, - "system": 0.0018317599999999996, - "min": 0.00680527054, - "max": 0.00791760454, + "mean": 0.00706393762, + "stddev": 0.0002525332098441265, + "median": 0.00708588392, + "user": 0.00508726, + "system": 0.0014969799999999995, + "min": 0.006650841420000001, + "max": 0.00739592442, "times": [ - 0.006971937539999999, - 0.006912103539999999, - 0.007269896539999999, - 0.007884437539999999, - 0.00781114554, - 0.007155687539999999, - 0.00680527054, - 0.00766810454, - 0.00791760454, - 0.00734093754 + 0.007005425420000001, + 0.007291049420000001, + 0.00726584142, + 0.00706505042, + 0.006650841420000001, + 0.006972300420000001, + 0.006662133420000001, + 0.00739592442, + 0.00722409242, + 0.00710671742 ], "memory_usage_byte": [ - 14286848, - 14286848, - 14286848, - 14286848, - 14286848, - 14286848, - 14286848, - 14286848, - 14286848, - 14286848 + 13877248, + 13877248, + 13877248, + 13877248, + 13991936, + 13991936, + 13991936, + 13991936, + 13991936, + 13991936 ], "exit_codes": [ 1, @@ -48,36 +48,36 @@ }, { "command": "basilisk-warm", - "mean": 0.005202737439999999, - "stddev": 0.0002688186606958716, - "median": 0.005198270539999999, - "user": 0.0027556799999999995, - "system": 0.0018314599999999996, - "min": 0.00470581254, - "max": 0.0056035205399999986, + "mean": 0.004545187620000001, + "stddev": 0.0005104254739375768, + "median": 0.004541737420000001, + "user": 0.0026400599999999996, + "system": 0.0013732799999999997, + "min": 0.0039174664200000014, + "max": 0.005454675420000001, "times": [ - 0.005111312539999999, - 0.005137645539999999, - 0.0049091875399999985, - 0.00470581254, - 0.00546685454, - 0.00532193754, - 0.0056035205399999986, - 0.005258895539999999, - 0.0054161455399999985, - 0.00509606254 + 0.00473425842, + 0.004866217420000001, + 0.005454675420000001, + 0.004818175420000001, + 0.0050331334200000005, + 0.004349216420000001, + 0.004181633420000001, + 0.004113008420000001, + 0.00398409142, + 0.0039174664200000014 ], "memory_usage_byte": [ - 14548992, - 14548992, - 14548992, - 14548992, - 14548992, - 14548992, - 14548992, - 14548992, - 14548992, - 14548992 + 14958592, + 14958592, + 14958592, + 14958592, + 14958592, + 14958592, + 14958592, + 14958592, + 14958592, + 14958592 ], "exit_codes": [ 1, @@ -94,36 +94,36 @@ }, { "command": "pyright", - "mean": 0.50339003354, - "stddev": 0.006741863033726673, - "median": 0.50105431304, - "user": 0.59798028, - "system": 0.05468476, - "min": 0.49803060454, - "max": 0.52073214554, + "mean": 0.50312528352, + "stddev": 0.010516875698852499, + "median": 0.50617382092, + "user": 0.5885744599999999, + "system": 0.05528917999999999, + "min": 0.48383667542000003, + "max": 0.51766434142, "times": [ - 0.52073214554, - 0.49965352054000006, - 0.50376456254, - 0.5047742715400001, - 0.5019647715400001, - 0.49803060454, - 0.5001438545400001, - 0.49834893754000004, - 0.49966893754, - 0.5068187295400001 + 0.50694259142, + 0.50540505042, + 0.50127196742, + 0.49130921742000006, + 0.50768130042, + 0.50743084142, + 0.51495250842, + 0.48383667542000003, + 0.51766434142, + 0.4947583414200001 ], "memory_usage_byte": [ - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424 + 151535616, + 151535616, + 151535616, + 151535616, + 151535616, + 151535616, + 151535616, + 151535616, + 151535616, + 152027136 ], "exit_codes": [ 1, @@ -140,36 +140,36 @@ }, { "command": "mypy", - "mean": 0.59167938334, - "stddev": 0.008182455125595402, - "median": 0.58860087504, - "user": 0.52952008, - "system": 0.05793646, - "min": 0.58045197954, - "max": 0.6054341455400001, + "mean": 0.56269984602, + "stddev": 0.004516727786273977, + "median": 0.56374367492, + "user": 0.50411186, + "system": 0.05508367999999999, + "min": 0.55642584242, + "max": 0.5688288004200001, "times": [ - 0.58045197954, - 0.5876730625400001, - 0.5895286875400001, - 0.59436143754, - 0.60433539554, - 0.6054341455400001, - 0.58727581254, - 0.59552277054, - 0.5873251875400001, - 0.5848853545400001 + 0.55644367542, + 0.55841988342, + 0.56573071742, + 0.55642584242, + 0.56300959142, + 0.56447775842, + 0.5604470084200001, + 0.56737021642, + 0.56584496642, + 0.5688288004200001 ], "memory_usage_byte": [ - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424 + 152027136, + 152027136, + 152027136, + 152027136, + 152027136, + 152027136, + 152027136, + 152027136, + 152027136, + 152027136 ], "exit_codes": [ 1, @@ -186,36 +186,36 @@ }, { "command": "mypy-warm", - "mean": 0.16947365004000003, - "stddev": 0.00552326848111201, - "median": 0.16771449954, - "user": 0.13867698, - "system": 0.02809066, - "min": 0.16504772954000002, - "max": 0.18399256254000002, + "mean": 0.16495317502, + "stddev": 0.0053306405963140305, + "median": 0.16396234192000003, + "user": 0.13456056, + "system": 0.02776748, + "min": 0.15763717542, + "max": 0.17574521642000002, "times": [ - 0.16542835454000002, - 0.16989897954000002, - 0.18399256254000002, - 0.16803202054000002, - 0.16739697854000002, - 0.17018914554, - 0.16602564654000002, - 0.16726381254000003, - 0.16504772954000002, - 0.17146127054000002 + 0.17574521642000002, + 0.15763717542, + 0.16289113342, + 0.17115621642, + 0.16279896642, + 0.16440971642000002, + 0.16450430042, + 0.15948629942, + 0.16738775842, + 0.16351496742000002 ], "memory_usage_byte": [ - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424 + 152027136, + 152027136, + 152027136, + 152027136, + 152027136, + 152027136, + 152027136, + 152027136, + 152027136, + 152027136 ], "exit_codes": [ 1, @@ -232,36 +232,36 @@ }, { "command": "ty", - "mean": 0.030980591940000003, - "stddev": 0.0008665239695992524, - "median": 0.03059308354, - "user": 0.02285878, - "system": 0.005744759999999999, - "min": 0.030034812540000004, - "max": 0.03238581254, + "mean": 0.03257077922000001, + "stddev": 0.0010780574203994896, + "median": 0.03237232092, + "user": 0.023236759999999995, + "system": 0.0075286799999999985, + "min": 0.031073758420000003, + "max": 0.034352175420000006, "times": [ - 0.03060993754, - 0.03133027154, - 0.03056247954, - 0.03057622954, - 0.03015406254, - 0.03238581254, - 0.03022297954, - 0.03174477154, - 0.03218456254, - 0.030034812540000004 + 0.03260142542, + 0.031324633420000005, + 0.031073758420000003, + 0.03201017442, + 0.03214321642, + 0.03373100842000001, + 0.034352175420000006, + 0.032654841420000004, + 0.033748966420000005, + 0.032067592420000006 ], "memory_usage_byte": [ - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424 + 152027136, + 152027136, + 152027136, + 152027136, + 152027136, + 152027136, + 152027136, + 152027136, + 152027136, + 152027136 ], "exit_codes": [ 1, @@ -278,36 +278,36 @@ }, { "command": "pyrefly", - "mean": 0.13718183753999996, - "stddev": 0.0016206282409451222, - "median": 0.13745316654, - "user": 0.09856407999999998, - "system": 0.03829336, - "min": 0.13461127054000002, - "max": 0.13933093754, + "mean": 0.10083128331999999, + "stddev": 0.0019209757190007152, + "median": 0.10096715442000001, + "user": 0.08363466, + "system": 0.017664879999999997, + "min": 0.09796955042000001, + "max": 0.10413059142, "times": [ - 0.13933093754, - 0.13788206254000002, - 0.13461127054000002, - 0.13494381254, - 0.13765660354, - 0.13722681254000002, - 0.13848443754, - 0.13724972954, - 0.13879452154000002, - 0.13563818754 + 0.10229946642, + 0.10413059142, + 0.10090380042000001, + 0.09879329942000001, + 0.09924704942, + 0.10103050842, + 0.10291921642, + 0.10110396742, + 0.09796955042000001, + 0.09991538342 ], "memory_usage_byte": [ - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424 + 152027136, + 152027136, + 152027136, + 152027136, + 152027136, + 152027136, + 152027136, + 152027136, + 152027136, + 152027136 ], "exit_codes": [ 1, @@ -324,36 +324,36 @@ }, { "command": "zuban", - "mean": 0.023936325240000004, - "stddev": 0.0006146297302072835, - "median": 0.02371627104, - "user": 0.04965228000000001, - "system": 0.008519760000000001, - "min": 0.02311702154, - "max": 0.025046270540000002, + "mean": 0.02413047102, + "stddev": 0.000751174789384409, + "median": 0.024018466920000002, + "user": 0.04864685999999999, + "system": 0.008475779999999999, + "min": 0.022936467420000002, + "max": 0.025775508420000003, "times": [ - 0.02406202154, - 0.02453802054, - 0.02347881254, - 0.024619520540000002, - 0.02311702154, - 0.02367481254, - 0.02363352154, - 0.02375772954, - 0.025046270540000002, - 0.02343552154 + 0.025775508420000003, + 0.024016133420000002, + 0.023756508420000003, + 0.024676550420000002, + 0.023815883420000003, + 0.024124258420000003, + 0.02365463342, + 0.024020800420000002, + 0.022936467420000002, + 0.024527966420000002 ], "memory_usage_byte": [ - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424, - 152551424 + 152027136, + 152027136, + 152027136, + 152027136, + 152027136, + 152027136, + 152027136, + 152027136, + 152027136, + 152027136 ], "exit_codes": [ 1, diff --git a/benchmarks/results/summary.md b/benchmarks/results/summary.md index 131857ed..fff55729 100644 --- a/benchmarks/results/summary.md +++ b/benchmarks/results/summary.md @@ -5,29 +5,29 @@ Machine: `Apple M4 Max` | fixture | basilisk | basilisk-warm | pyright | mypy | mypy-warm | ty | pyrefly | zuban | |---|---|---|---|---|---|---|---|---| -| aliases_type_statement | 8.3 ms | 4.5 ms | 563.6 ms | 633.7 ms | 167.4 ms | 60.1 ms | 152.5 ms | 27.3 ms | -| assignment_compatibility | 8.8 ms | 5.5 ms | 608.9 ms | 611.4 ms | 169.9 ms | 50.3 ms | 150.8 ms | 31.2 ms | -| call_argument_types | 14.6 ms | 5.0 ms | 670.7 ms | 636.8 ms | 168.9 ms | 53.9 ms | 160.1 ms | 49.0 ms | -| callables_subtyping | 10.6 ms | 2.5 ms | 536.5 ms | 597.5 ms | 166.0 ms | 35.4 ms | 141.9 ms | 27.0 ms | -| classvar_scoping | 15.2 ms | 5.8 ms | 621.9 ms | 639.4 ms | 167.8 ms | 56.8 ms | 169.4 ms | 31.6 ms | -| constructors_call_init | 9.5 ms | 4.5 ms | 630.1 ms | 625.1 ms | 168.3 ms | 38.5 ms | 142.4 ms | 26.2 ms | -| dataclasses_usage | 9.2 ms | 3.7 ms | 1627.8 ms | 667.6 ms | 170.0 ms | 58.6 ms | 215.3 ms | 56.9 ms | -| dict_key_hashability | 11.9 ms | 5.2 ms | 541.5 ms | 638.6 ms | 168.7 ms | 39.1 ms | 143.4 ms | 30.3 ms | -| enums_member_values | 7.8 ms | 4.1 ms | 586.5 ms | 593.5 ms | 167.5 ms | 43.1 ms | 146.0 ms | 26.7 ms | -| final_reassignment | 6.9 ms | 4.0 ms | 467.5 ms | 590.6 ms | 166.9 ms | 26.7 ms | 137.6 ms | 24.0 ms | -| generics_defaults_specialization | 10.5 ms | 4.3 ms | 578.0 ms | 608.2 ms | 167.0 ms | 34.9 ms | 141.8 ms | 26.5 ms | -| literals_semantics | 11.4 ms | 4.2 ms | 544.8 ms | 600.0 ms | 169.4 ms | 31.7 ms | 141.1 ms | 27.2 ms | -| match_exhaustiveness | 10.6 ms | 4.0 ms | 541.3 ms | 623.8 ms | 168.5 ms | 36.3 ms | 147.9 ms | 26.6 ms | -| narrowing_typeis | 9.5 ms | 4.2 ms | 552.5 ms | 605.0 ms | 167.3 ms | 35.6 ms | 145.5 ms | 25.6 ms | -| newtype_definition | 10.7 ms | 5.8 ms | 730.7 ms | 643.2 ms | 168.6 ms | 22.9 ms | 156.8 ms | 35.6 ms | -| overloads_evaluation | 12.5 ms | 4.3 ms | 609.2 ms | 637.7 ms | 166.8 ms | 59.1 ms | 157.6 ms | 34.7 ms | -| override_compatibility | 14.3 ms | 3.7 ms | 655.0 ms | 624.0 ms | 167.2 ms | 40.7 ms | 150.8 ms | 28.1 ms | -| protocols_definition | 8.8 ms | 4.2 ms | 576.2 ms | 603.7 ms | 166.7 ms | 35.0 ms | 140.9 ms | 27.0 ms | -| returns_compatibility | 7.4 ms | 5.2 ms | 503.4 ms | 591.7 ms | 169.5 ms | 31.0 ms | 137.2 ms | 23.9 ms | -| tuples_index | 8.4 ms | 3.7 ms | 563.4 ms | 587.5 ms | 166.5 ms | 32.7 ms | 139.2 ms | 25.7 ms | -| typeddict_key_access | 9.6 ms | 4.0 ms | 630.9 ms | 595.7 ms | 170.7 ms | 37.2 ms | 144.7 ms | 25.7 ms | -| typeddict_readonly_inheritance | 14.2 ms | 4.3 ms | 679.5 ms | 594.6 ms | 168.2 ms | 38.5 ms | 154.0 ms | 25.8 ms | -| typeddict_readonly_mutation | 9.9 ms | 4.4 ms | 635.7 ms | 598.9 ms | 167.4 ms | 43.1 ms | 147.7 ms | 25.7 ms | -| typevar_constraints | 18.2 ms | 5.0 ms | 740.0 ms | 603.2 ms | 168.9 ms | 39.3 ms | 148.6 ms | 32.6 ms | -| undefined_names | 15.2 ms | 4.9 ms | 499.9 ms | 651.6 ms | 170.5 ms | 49.5 ms | 587.3 ms | 35.5 ms | -| unresolved_imports | 11.7 ms | 5.2 ms | 475.3 ms | 688.2 ms | 174.5 ms | 241.7 ms | 565.8 ms | 247.3 ms | +| aliases_type_statement | 8.1 ms | 4.4 ms | 559.9 ms | 595.9 ms | 162.8 ms | 54.5 ms | 110.0 ms | 27.1 ms | +| assignment_compatibility | 8.6 ms | 4.9 ms | 598.5 ms | 574.0 ms | 164.0 ms | 51.9 ms | 112.6 ms | 30.3 ms | +| call_argument_types | 13.5 ms | 4.2 ms | 841.8 ms | 613.3 ms | 171.5 ms | 55.5 ms | 114.3 ms | 48.2 ms | +| callables_subtyping | 12.1 ms | 4.1 ms | 529.6 ms | 574.5 ms | 162.0 ms | 38.3 ms | 107.4 ms | 28.4 ms | +| classvar_scoping | 15.3 ms | 5.7 ms | 606.2 ms | 620.3 ms | 170.3 ms | 57.4 ms | 133.5 ms | 31.5 ms | +| constructors_call_init | 9.1 ms | 4.1 ms | 613.2 ms | 573.4 ms | 156.1 ms | 37.0 ms | 100.4 ms | 26.2 ms | +| dataclasses_usage | 9.2 ms | 3.8 ms | 1579.1 ms | 640.6 ms | 158.3 ms | 57.9 ms | 170.5 ms | 55.1 ms | +| dict_key_hashability | 11.4 ms | 4.7 ms | 517.4 ms | 592.6 ms | 157.4 ms | 39.1 ms | 103.5 ms | 29.9 ms | +| enums_member_values | 7.2 ms | 3.7 ms | 569.8 ms | 549.7 ms | 155.9 ms | 40.2 ms | 100.3 ms | 26.3 ms | +| final_reassignment | 6.2 ms | 3.7 ms | 473.2 ms | 545.3 ms | 156.0 ms | 26.3 ms | 95.7 ms | 23.3 ms | +| generics_defaults_specialization | 9.7 ms | 3.8 ms | 576.8 ms | 557.4 ms | 157.8 ms | 33.7 ms | 100.9 ms | 25.4 ms | +| literals_semantics | 10.7 ms | 3.9 ms | 526.5 ms | 550.6 ms | 176.9 ms | 39.3 ms | 105.4 ms | 26.9 ms | +| match_exhaustiveness | 10.4 ms | 4.1 ms | 547.1 ms | 970.8 ms | 162.0 ms | 26.0 ms | 109.9 ms | 27.9 ms | +| narrowing_typeis | 8.7 ms | 3.7 ms | 549.5 ms | 573.8 ms | 161.9 ms | 33.6 ms | 108.1 ms | 27.2 ms | +| newtype_definition | 11.0 ms | 4.8 ms | 710.5 ms | 616.6 ms | 164.3 ms | 44.6 ms | 118.1 ms | 35.3 ms | +| overloads_evaluation | 12.2 ms | 4.4 ms | 593.0 ms | 603.8 ms | 162.0 ms | 60.3 ms | 115.7 ms | 33.6 ms | +| override_compatibility | 13.6 ms | 4.4 ms | 634.6 ms | 597.6 ms | 162.9 ms | 40.8 ms | 109.2 ms | 28.2 ms | +| protocols_definition | 8.4 ms | 3.8 ms | 563.0 ms | 564.7 ms | 160.8 ms | 34.1 ms | 102.2 ms | 26.9 ms | +| returns_compatibility | 7.1 ms | 4.5 ms | 503.1 ms | 562.7 ms | 165.0 ms | 32.6 ms | 100.8 ms | 24.1 ms | +| tuples_index | 8.7 ms | 3.6 ms | 550.2 ms | 558.1 ms | 159.8 ms | 34.1 ms | 101.8 ms | 25.0 ms | +| typeddict_key_access | 8.9 ms | 3.5 ms | 627.3 ms | 563.3 ms | 160.2 ms | 36.7 ms | 105.4 ms | 26.2 ms | +| typeddict_readonly_inheritance | 13.9 ms | 3.8 ms | 653.3 ms | 562.6 ms | 161.6 ms | 38.0 ms | 114.4 ms | 26.3 ms | +| typeddict_readonly_mutation | 10.3 ms | 4.0 ms | 614.3 ms | 575.0 ms | 179.5 ms | 43.2 ms | 110.0 ms | 26.9 ms | +| typevar_constraints | 17.0 ms | 5.5 ms | 726.8 ms | 568.3 ms | 159.8 ms | 39.9 ms | 110.2 ms | 32.2 ms | +| undefined_names | 14.5 ms | 4.7 ms | 489.9 ms | 611.5 ms | 162.6 ms | 50.4 ms | 539.4 ms | 34.2 ms | +| unresolved_imports | 12.9 ms | 5.0 ms | 456.2 ms | 650.9 ms | 167.8 ms | 264.8 ms | 853.8 ms | 299.0 ms | diff --git a/benchmarks/results/tuples_index.json b/benchmarks/results/tuples_index.json index 934189e0..eab24793 100644 --- a/benchmarks/results/tuples_index.json +++ b/benchmarks/results/tuples_index.json @@ -2,98 +2,38 @@ "results": [ { "command": "basilisk", - "mean": 0.008364091146666667, - "stddev": 0.0002623623843757587, - "median": 0.00832999648, - "user": 0.006447006666666667, - "system": 0.0015240266666666665, - "min": 0.007827267480000002, - "max": 0.009147351480000001, + "mean": 0.008744251640000002, + "stddev": 0.00043972253310791857, + "median": 0.008685626340000002, + "user": 0.00668976, + "system": 0.0015410800000000002, + "min": 0.008120855840000002, + "max": 0.009439146840000003, "times": [ - 0.008561017480000002, - 0.009147351480000001, - 0.008037392480000002, - 0.008856184480000003, - 0.008213600480000001, - 0.007987476480000002, - 0.008270309480000002, - 0.008456392480000002, - 0.008491642480000002, - 0.008545059480000002, - 0.008360309480000001, - 0.008272517480000002, - 0.008634975480000001, - 0.008183600480000002, - 0.008376226480000002, - 0.008498892480000002, - 0.008352934480000003, - 0.008207059480000002, - 0.008598225480000003, - 0.008149809480000002, - 0.008270225480000002, - 0.008414892480000002, - 0.008213642480000002, - 0.008307267480000002, - 0.008352725480000002, - 0.008120267480000002, - 0.007827267480000002, - 0.008682975480000002, - 0.008275225480000002, - 0.008257267480000002 + 0.008688480840000002, + 0.009350605840000002, + 0.008962980840000002, + 0.009439146840000003, + 0.009015480840000001, + 0.008682771840000002, + 0.008422522840000002, + 0.008120855840000002, + 0.008422439840000002, + 0.008337230840000001 ], "memory_usage_byte": [ - 14909440, - 14909440, - 14909440, - 14942208, - 14942208, - 14942208, - 14942208, - 14942208, - 14942208, - 14942208, - 14942208, - 14958592, - 14958592, - 14958592, - 14958592, - 14958592, - 14958592, - 14974976, - 14974976, - 14974976, - 14974976, - 14974976, - 14974976, - 14974976, - 14974976, - 14974976, - 14991360, - 14991360, - 14991360, - 14991360 + 14368768, + 14368768, + 14368768, + 14385152, + 14385152, + 14385152, + 14385152, + 14385152, + 14385152, + 14385152 ], "exit_codes": [ - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, 1, 1, 1, @@ -108,98 +48,38 @@ }, { "command": "basilisk-warm", - "mean": 0.003696570346666667, - "stddev": 0.00039000297552372714, - "median": 0.003675996480000001, - "user": 0.0020870066666666674, - "system": 0.0013084266666666664, - "min": 0.003087683480000001, - "max": 0.004598809480000002, + "mean": 0.003613918240000001, + "stddev": 0.00014071605368511133, + "median": 0.0035996683400000016, + "user": 0.0022027599999999994, + "system": 0.00105198, + "min": 0.003399938840000001, + "max": 0.003832355840000001, "times": [ - 0.0038364344800000017, - 0.003650683480000001, - 0.003087683480000001, - 0.004598809480000002, - 0.003752434480000001, - 0.003273226480000001, - 0.0037896834800000014, - 0.0033083084800000014, - 0.004104225480000001, - 0.0032481844800000014, - 0.003701309480000001, - 0.0037233504800000014, - 0.0031637674800000016, - 0.003840392480000001, - 0.0034648094800000012, - 0.0032873504800000016, - 0.004000934480000001, - 0.003367559480000001, - 0.004129892480000001, - 0.0035128094800000015, - 0.003517434480000001, - 0.004399351480000001, - 0.003503809480000001, - 0.0034625594800000016, - 0.0038707254800000013, - 0.0037397264800000016, - 0.004499976480000001, - 0.003528267480000001, - 0.003440434480000001, - 0.004092975480000002 + 0.0036847308400000016, + 0.0037208558400000013, + 0.0036096888400000016, + 0.003832355840000001, + 0.0035251058400000016, + 0.0037785638400000014, + 0.003555313840000001, + 0.0035896478400000015, + 0.003399938840000001, + 0.003442980840000001 ], "memory_usage_byte": [ - 14991360, - 14991360, - 14991360, - 14991360, - 14991360, - 14991360, - 14991360, - 14991360, - 14991360, - 14991360, - 14991360, - 14991360, - 14991360, - 14991360, - 14991360, - 14991360, - 14991360, - 14991360, - 14991360, - 14991360, - 14991360, - 14991360, - 14991360, - 14991360, - 14991360, - 14991360, - 14991360, - 14991360, - 14991360, - 14991360 + 14532608, + 14532608, + 14532608, + 14532608, + 14532608, + 14532608, + 14532608, + 14532608, + 14532608, + 14532608 ], "exit_codes": [ - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, 1, 1, 1, @@ -214,98 +94,38 @@ }, { "command": "pyright", - "mean": 0.5633750452466668, - "stddev": 0.017124652144273528, - "median": 0.55811889248, - "user": 0.7296561733333331, - "system": 0.05640789333333333, - "min": 0.5517944334800001, - "max": 0.64279905948, + "mean": 0.55021941434, + "stddev": 0.013536968751297283, + "median": 0.54825091834, + "user": 0.71697156, + "system": 0.053762379999999985, + "min": 0.53462618984, + "max": 0.57514339684, "times": [ - 0.5746736424800001, - 0.56156830948, - 0.55576876748, - 0.5572258094800001, - 0.57382426748, - 0.5560079344800001, - 0.56685268448, - 0.5595916014800001, - 0.5552581004800001, - 0.5570081844800001, - 0.5517944334800001, - 0.5525496004800001, - 0.56089122548, - 0.55202493348, - 0.5553642254800001, - 0.56632118348, - 0.5567261424800001, - 0.5521157674800001, - 0.55567330948, - 0.5739786844800001, - 0.5889796004800001, - 0.56256776748, - 0.55408435048, - 0.64279905948, - 0.5600074344800001, - 0.5566007254800001, - 0.56017572548, - 0.5570214764800001, - 0.56478443448, - 0.55901197548 + 0.54317327284, + 0.54952473084, + 0.53603998084, + 0.5384237308400001, + 0.54697710584, + 0.53462618984, + 0.5556559398400001, + 0.56867285584, + 0.57514339684, + 0.55395693984 ], "memory_usage_byte": [ - 153616384, - 153862144, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232 + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016 ], "exit_codes": [ - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, 1, 1, 1, @@ -320,98 +140,38 @@ }, { "command": "mypy", - "mean": 0.5874896050466668, - "stddev": 0.007599497764260622, - "median": 0.5858908714800001, - "user": 0.5254349066666666, - "system": 0.05756496000000001, - "min": 0.5778383504800001, - "max": 0.6042957264800001, + "mean": 0.5581039225400001, + "stddev": 0.006427567755558448, + "median": 0.55687454384, + "user": 0.50081946, + "system": 0.05248797999999999, + "min": 0.55190139784, + "max": 0.57438498084, "times": [ - 0.5791317264800001, - 0.6022154334800001, - 0.5887066014800001, - 0.5778383504800001, - 0.58720455948, - 0.5918219334800001, - 0.5799592254800001, - 0.58179443348, - 0.5829869764800001, - 0.5925026424800001, - 0.6042957264800001, - 0.58563622548, - 0.5861455174800001, - 0.58463351748, - 0.5979148924800001, - 0.5981777674800001, - 0.5931572674800001, - 0.5805243094800001, - 0.5788181014800001, - 0.5867195174800001, - 0.5941546014800001, - 0.5854403924800001, - 0.5898713504800001, - 0.5801620174800001, - 0.6016824764800001, - 0.5828603924800001, - 0.58152551748, - 0.5862189344800001, - 0.57812505948, - 0.5844626834800001 + 0.55283960584, + 0.56257168984, + 0.55485939684, + 0.55717406484, + 0.5556082718400001, + 0.55738831384, + 0.57438498084, + 0.55657502284, + 0.55190139784, + 0.55773648084 ], "memory_usage_byte": [ - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232 + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016 ], "exit_codes": [ - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, 1, 1, 1, @@ -426,98 +186,38 @@ }, { "command": "mypy-warm", - "mean": 0.1665063828133334, - "stddev": 0.0035768957133213587, - "median": 0.16497135098, - "user": 0.13724944, - "system": 0.026813160000000006, - "min": 0.16157947548, - "max": 0.17390168448, + "mean": 0.15984766804, + "stddev": 0.0044626520422011064, + "median": 0.15766975134, + "user": 0.13195716, + "system": 0.025827479999999996, + "min": 0.15575252184000002, + "max": 0.16667548084, "times": [ - 0.16805114248, - 0.17044547548, - 0.16900955948, - 0.17381651748000002, - 0.16653585048, - 0.16869085048000002, - 0.17343547548, - 0.17390168448, - 0.17231118448, - 0.16372793448, - 0.16647980948, - 0.16669451748, - 0.16608243448, - 0.16753655948, - 0.17108730948, - 0.16424439248, - 0.16451155948, - 0.16334114248, - 0.16508189248, - 0.16479368448, - 0.16263414248, - 0.16157947548, - 0.16451280848, - 0.16263172548, - 0.16418535048, - 0.16474585148, - 0.16334255948, - 0.16375301748, - 0.16486080948, - 0.16316676748 + 0.15575252184000002, + 0.15973089784, + 0.16645702184, + 0.15687027184000002, + 0.15753527184000002, + 0.16513448084000001, + 0.15610823084, + 0.15780423084, + 0.16667548084, + 0.15640827184 ], "memory_usage_byte": [ - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232 + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016 ], "exit_codes": [ - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, 1, 1, 1, @@ -532,98 +232,38 @@ }, { "command": "ty", - "mean": 0.03272925364666667, - "stddev": 0.000867702076923556, - "median": 0.03272985098, - "user": 0.024388173333333332, - "system": 0.006190959999999997, - "min": 0.030875017480000006, - "max": 0.03425705848, + "mean": 0.03405484744000001, + "stddev": 0.0014077730515921765, + "median": 0.03379041884, + "user": 0.024482559999999997, + "system": 0.007164479999999999, + "min": 0.032372938840000004, + "max": 0.03695777184, "times": [ - 0.03289951748, - 0.03345614248, - 0.03372472548, - 0.03269197648, - 0.03227905948, - 0.03232130948, - 0.03303868448, - 0.030875017480000006, - 0.03271260048, - 0.03198347648, - 0.03339680948, - 0.03198785148, - 0.03245693448, - 0.03287955948, - 0.034217433480000004, - 0.03425705848, - 0.03267518348, - 0.031068767480000006, - 0.03328485148, - 0.03116180948, - 0.03274710148, - 0.03314722548, - 0.03232626748, - 0.03324380948, - 0.03152555848, - 0.03419785048, - 0.03331501748, - 0.03286305948, - 0.03271255848, - 0.03243039248 + 0.03327527184000001, + 0.03394131484, + 0.03454802184, + 0.03695777184, + 0.03571631384, + 0.03354077284, + 0.03408864784, + 0.032372938840000004, + 0.03246789784000001, + 0.033639522840000005 ], "memory_usage_byte": [ - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232 + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016 ], "exit_codes": [ - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, 1, 1, 1, @@ -638,98 +278,38 @@ }, { "command": "pyrefly", - "mean": 0.13916811327999998, - "stddev": 0.002024203403461963, - "median": 0.13881466298, - "user": 0.09995123999999997, - "system": 0.03856646, - "min": 0.13633264248, - "max": 0.14510447548, + "mean": 0.10183692663999999, + "stddev": 0.0024371823243207547, + "median": 0.10087514734, + "user": 0.08480916000000001, + "system": 0.017879179999999998, + "min": 0.09891998084, + "max": 0.10558773084, "times": [ - 0.14156810148, - 0.13958626748, - 0.13703826748, - 0.13865855948, - 0.13713805948, - 0.13683814248, - 0.13811414248, - 0.13835560048, - 0.13904085148, - 0.13879401748, - 0.14035943348, - 0.13633264248, - 0.13649589248, - 0.13803393448, - 0.13750151748, - 0.13883530848, - 0.13675447548, - 0.13871455948, - 0.13717243348, - 0.13850705948, - 0.14267972648, - 0.13924343448, - 0.13992726748, - 0.14042714248, - 0.14200810048, - 0.14010718348, - 0.14130635048, - 0.14023393348, - 0.14510447548, - 0.14016651748 + 0.10476798084000001, + 0.10060202184, + 0.09891998084, + 0.10016289684, + 0.10379739784, + 0.10558773084, + 0.09997543884, + 0.09945156484, + 0.10114827284, + 0.10395598084 ], "memory_usage_byte": [ - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232 + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016 ], "exit_codes": [ - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, 1, 1, 1, @@ -744,98 +324,38 @@ }, { "command": "zuban", - "mean": 0.02568500341333334, - "stddev": 0.00110096276437041, - "median": 0.025442267480000003, - "user": 0.051902439999999994, - "system": 0.008528793333333335, - "min": 0.02381505848, - "max": 0.028901934480000006, + "mean": 0.02498780984, + "stddev": 0.0009367234805703216, + "median": 0.024895022340000003, + "user": 0.050144060000000004, + "system": 0.00842008, + "min": 0.023609938840000004, + "max": 0.026553063840000002, "times": [ - 0.025199558480000002, - 0.026658850480000003, - 0.025538725480000003, - 0.02473001748, - 0.024485183480000002, - 0.025793017480000003, - 0.024988600480000005, - 0.024293225480000003, - 0.027754226480000005, - 0.028901934480000006, - 0.02537993448, - 0.025680142480000005, - 0.027013475480000004, - 0.026051517480000005, - 0.02503164248, - 0.024856558480000002, - 0.025104392480000005, - 0.025223350480000004, - 0.025839142480000004, - 0.025504600480000004, - 0.025207309480000002, - 0.026475725480000004, - 0.024822850480000002, - 0.02381505848, - 0.02642172548, - 0.027205684480000002, - 0.026979725480000005, - 0.025083683480000004, - 0.02573589248, - 0.024774350480000002 + 0.025535605840000003, + 0.024040646840000002, + 0.026553063840000002, + 0.02466193884, + 0.02409035584, + 0.023609938840000004, + 0.024755855840000004, + 0.025985480840000002, + 0.025034188840000002, + 0.025611022840000004 ], "memory_usage_byte": [ - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232, - 155615232 + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016, + 155222016 ], "exit_codes": [ - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, 1, 1, 1, diff --git a/benchmarks/results/typeddict_key_access.json b/benchmarks/results/typeddict_key_access.json index 8e986f4b..ca33b3df 100644 --- a/benchmarks/results/typeddict_key_access.json +++ b/benchmarks/results/typeddict_key_access.json @@ -2,36 +2,36 @@ "results": [ { "command": "basilisk", - "mean": 0.009552922660000001, - "stddev": 0.00036418097216942404, - "median": 0.00947858476, - "user": 0.007012120000000001, - "system": 0.00201742, - "min": 0.009155189260000002, - "max": 0.010450231260000001, + "mean": 0.008942308239999999, + "stddev": 0.0008380599515642528, + "median": 0.00857227864, + "user": 0.006856000000000001, + "system": 0.0015583399999999996, + "min": 0.00827857114, + "max": 0.01048307114, "times": [ - 0.010450231260000001, - 0.00975552226, - 0.009554855260000002, - 0.00962023126, - 0.009609439260000002, - 0.009402314259999999, - 0.00936635626, - 0.00935002326, - 0.009155189260000002, - 0.00926506426 + 0.00874694514, + 0.00856536214, + 0.00829161314, + 0.00844540414, + 0.00827857114, + 0.01044257014, + 0.01048307114, + 0.009134363139999999, + 0.00845598714, + 0.008579195139999999 ], "memory_usage_byte": [ - 15761408, - 15761408, - 15761408, - 15761408, - 15761408, - 15761408, - 15761408, - 15761408, - 15761408, - 15761408 + 14893056, + 14925824, + 14925824, + 14925824, + 14925824, + 15269888, + 15269888, + 15269888, + 15269888, + 15269888 ], "exit_codes": [ 1, @@ -48,36 +48,36 @@ }, { "command": "basilisk-warm", - "mean": 0.004037097660000001, - "stddev": 0.00034571816986775665, - "median": 0.0039949187600000005, - "user": 0.0022243199999999997, - "system": 0.0014000199999999997, - "min": 0.00357723126, - "max": 0.00468518926, + "mean": 0.0035257374400000006, + "stddev": 0.00012968706584102956, + "median": 0.00356782064, + "user": 0.0021332999999999994, + "system": 0.0011429399999999998, + "min": 0.0032990711399999996, + "max": 0.00368415414, "times": [ - 0.00357723126, - 0.00394568926, - 0.004256897260000001, - 0.00404414826, - 0.00414664826, - 0.00468518926, - 0.0036771052600000007, - 0.0037638142600000006, - 0.00441206426, - 0.00386218926 + 0.0036152371399999993, + 0.00334494514, + 0.0032990711399999996, + 0.00343898714, + 0.00356473714, + 0.0034821961399999998, + 0.0035709041399999996, + 0.0036227791399999995, + 0.00368415414, + 0.0036343631399999997 ], "memory_usage_byte": [ - 15761408, - 15761408, - 15761408, - 15761408, - 15761408, - 15761408, - 15761408, - 15761408, - 15761408, - 15761408 + 15269888, + 15269888, + 15269888, + 15269888, + 15269888, + 15269888, + 15269888, + 15269888, + 15269888, + 15269888 ], "exit_codes": [ 1, @@ -94,36 +94,36 @@ }, { "command": "pyright", - "mean": 0.6308836642600001, - "stddev": 0.006453654658128912, - "median": 0.63205816826, - "user": 0.8929852199999999, - "system": 0.061663619999999995, - "min": 0.6188515642600001, - "max": 0.6421042302600001, + "mean": 0.6273315163400001, + "stddev": 0.014157702050170305, + "median": 0.6216597581400001, + "user": 0.8787318, + "system": 0.05960873999999999, + "min": 0.6143674451400001, + "max": 0.65880015414, "times": [ - 0.6341142312600001, - 0.6188515642600001, - 0.63129906426, - 0.62986585526, - 0.6330908982600001, - 0.6421042302600001, - 0.6338266482600001, - 0.6306553142600001, - 0.6328172722600001, - 0.6222115642600001 + 0.62863619514, + 0.6219802791400001, + 0.6143674451400001, + 0.6213392371400001, + 0.6170202371400001, + 0.65880015414, + 0.64638327914, + 0.62426427914, + 0.6196613201400001, + 0.6208627371400001 ], "memory_usage_byte": [ - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784 + 165494784, + 165576704, + 165576704, + 165576704, + 165576704, + 166674432, + 166674432, + 166674432, + 167280640, + 167280640 ], "exit_codes": [ 1, @@ -140,36 +140,36 @@ }, { "command": "mypy", - "mean": 0.5956626809600001, - "stddev": 0.007557373454938857, - "median": 0.5935523142600001, - "user": 0.53346422, - "system": 0.057501819999999995, - "min": 0.58822814826, - "max": 0.6090882722600001, + "mean": 0.5633062751400001, + "stddev": 0.004746335509241121, + "median": 0.5645701541400001, + "user": 0.5077362999999999, + "system": 0.052812139999999994, + "min": 0.5536921131400001, + "max": 0.56866461314, "times": [ - 0.59182914726, - 0.6090882722600001, - 0.58822814826, - 0.5904899812600001, - 0.59533227326, - 0.5952754812600001, - 0.59068368926, - 0.5889229802600001, - 0.6082467312600001, - 0.5985301052600001 + 0.5608504041400001, + 0.56866461314, + 0.56841877914, + 0.56734911314, + 0.56520361214, + 0.56466173714, + 0.5644785711400001, + 0.5587120291400001, + 0.5536921131400001, + 0.56103177914 ], "memory_usage_byte": [ - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784 + 167280640, + 167280640, + 167280640, + 167280640, + 167280640, + 167280640, + 167280640, + 167280640, + 167280640, + 167280640 ], "exit_codes": [ 1, @@ -186,36 +186,36 @@ }, { "command": "mypy-warm", - "mean": 0.17071649366000002, - "stddev": 0.0030887801449284587, - "median": 0.17015012726, - "user": 0.13948131999999996, - "system": 0.028199019999999998, - "min": 0.16738698126, - "max": 0.17529302326, + "mean": 0.16023016684000002, + "stddev": 0.0028928720876068258, + "median": 0.16054261314, + "user": 0.13228819999999997, + "system": 0.025856639999999997, + "min": 0.15588136214, + "max": 0.16421411314, "times": [ - 0.16772452226, - 0.17487781426000001, - 0.17401131426000002, - 0.17074989826, - 0.17154531426000003, - 0.16738698126, - 0.16815985626000002, - 0.17529302326, - 0.16955035626, - 0.16786585626000003 + 0.15868073814, + 0.16209698714, + 0.15804815414, + 0.15588136214, + 0.16388944614, + 0.16184811214, + 0.16022961314, + 0.16421411314, + 0.16085561314000002, + 0.15655752914 ], "memory_usage_byte": [ - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784 + 167280640, + 167280640, + 167280640, + 167280640, + 167280640, + 167280640, + 167280640, + 167280640, + 167280640, + 167280640 ], "exit_codes": [ 1, @@ -232,36 +232,36 @@ }, { "command": "ty", - "mean": 0.037151172460000007, - "stddev": 0.001536076734920189, - "median": 0.03683470976, - "user": 0.028204519999999993, - "system": 0.0060423199999999995, - "min": 0.035649231260000004, - "max": 0.040617189260000006, + "mean": 0.036652462739999996, + "stddev": 0.0009255604710184119, + "median": 0.03648077914, + "user": 0.0273476, + "system": 0.007088839999999999, + "min": 0.03525098814, + "max": 0.038155279140000005, "times": [ - 0.03679014726, - 0.040617189260000006, - 0.035649231260000004, - 0.03625081426, - 0.035918439260000004, - 0.036879272260000005, - 0.03688764726, - 0.03722310526, - 0.03903789726, - 0.03625798126 + 0.03597273814, + 0.03696790414, + 0.03657452914, + 0.038134821140000004, + 0.038155279140000005, + 0.03633298814, + 0.03638702914, + 0.03592977914, + 0.03525098814, + 0.03681857114 ], "memory_usage_byte": [ - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784 + 167280640, + 167280640, + 167280640, + 167280640, + 167280640, + 167280640, + 167280640, + 167280640, + 167280640, + 167280640 ], "exit_codes": [ 1, @@ -278,36 +278,36 @@ }, { "command": "pyrefly", - "mean": 0.14468039346, - "stddev": 0.001923818045801906, - "median": 0.14457437726000003, - "user": 0.10450422000000001, - "system": 0.039179519999999995, - "min": 0.14199918926000002, - "max": 0.14867681426, + "mean": 0.10538724163999999, + "stddev": 0.0016178868571545386, + "median": 0.10510502914, + "user": 0.08861510000000002, + "system": 0.01685184, + "min": 0.10309761214, + "max": 0.10766469614, "times": [ - 0.14480239826000002, - 0.14199918926000002, - 0.14284935526, - 0.14510127226000002, - 0.14305631426, - 0.14532293926, - 0.14867681426, - 0.14434635626000003, - 0.14418173126000003, - 0.14646756426000002 + 0.10688057114, + 0.10750873714, + 0.10309761214, + 0.10360407014, + 0.10615165414000001, + 0.10452407114000001, + 0.10766469614, + 0.10466194614, + 0.10423094614, + 0.10554811214000001 ], "memory_usage_byte": [ - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784 + 167280640, + 167280640, + 167280640, + 167280640, + 167280640, + 167280640, + 167280640, + 167280640, + 167280640, + 167280640 ], "exit_codes": [ 1, @@ -324,36 +324,36 @@ }, { "command": "zuban", - "mean": 0.025731472460000005, - "stddev": 0.0009530158381685886, - "median": 0.025864751760000002, - "user": 0.05408952, - "system": 0.008489019999999998, - "min": 0.02425623026, - "max": 0.02691810626, + "mean": 0.026189649739999998, + "stddev": 0.0012224815212981248, + "median": 0.025926737140000003, + "user": 0.050288, + "system": 0.00887954, + "min": 0.02478402914, + "max": 0.02859315414, "times": [ - 0.025639856260000003, - 0.025882522260000002, - 0.02646077226, - 0.02425623026, - 0.024610522260000003, - 0.025846981260000002, - 0.024635480260000002, - 0.02690189726, - 0.026162356260000002, - 0.02691810626 + 0.02784223714, + 0.02859315414, + 0.02642490414, + 0.02478402914, + 0.02569961214, + 0.02654948714, + 0.02548282014, + 0.02509732114, + 0.02615386214, + 0.025269070140000002 ], "memory_usage_byte": [ - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784, - 167542784 + 167280640, + 167280640, + 167280640, + 167280640, + 167280640, + 167280640, + 167280640, + 167280640, + 167280640, + 167280640 ], "exit_codes": [ 1, diff --git a/benchmarks/results/typeddict_readonly_inheritance.json b/benchmarks/results/typeddict_readonly_inheritance.json index 0609a6a2..bf603977 100644 --- a/benchmarks/results/typeddict_readonly_inheritance.json +++ b/benchmarks/results/typeddict_readonly_inheritance.json @@ -2,36 +2,36 @@ "results": [ { "command": "basilisk", - "mean": 0.01419525586, - "stddev": 0.00029818552567682896, - "median": 0.014069835360000001, - "user": 0.011592079999999998, - "system": 0.0020095399999999993, - "min": 0.013779459360000003, - "max": 0.014831876360000003, + "mean": 0.013873349820000002, + "stddev": 0.00038051080580953577, + "median": 0.013917608420000002, + "user": 0.01149504, + "system": 0.0018590399999999997, + "min": 0.013370483420000003, + "max": 0.014349358420000003, "times": [ - 0.014392418360000003, - 0.014348751360000002, - 0.014831876360000003, - 0.01397171036, - 0.01438604336, - 0.014067335360000002, - 0.01407233536, - 0.01403825136, - 0.013779459360000003, - 0.014064377360000001 + 0.01409515042, + 0.013740066420000002, + 0.013370483420000003, + 0.013669150420000001, + 0.013451399420000003, + 0.013439399420000001, + 0.014102524420000002, + 0.014268441420000003, + 0.014247524420000001, + 0.014349358420000003 ], "memory_usage_byte": [ - 17252352, - 17252352, - 17252352, - 17252352, - 17252352, - 17252352, - 17252352, - 17252352, - 17252352, - 17252352 + 16220160, + 16220160, + 16236544, + 16236544, + 16285696, + 16285696, + 16285696, + 16285696, + 16285696, + 16285696 ], "exit_codes": [ 1, @@ -48,36 +48,36 @@ }, { "command": "basilisk-warm", - "mean": 0.004282580660000001, - "stddev": 0.0004687334187510116, - "median": 0.004314918360000001, - "user": 0.0022962800000000004, - "system": 0.00151804, - "min": 0.0034951263600000006, - "max": 0.004912334360000001, + "mean": 0.0038235918200000005, + "stddev": 0.00017769159099918669, + "median": 0.00376135842, + "user": 0.0022077399999999997, + "system": 0.0012602399999999997, + "min": 0.0035874414200000003, + "max": 0.0041276084200000005, "times": [ - 0.004912334360000001, - 0.004205918360000001, - 0.0034951263600000006, - 0.004731959360000001, - 0.004817126360000001, - 0.004007751360000001, - 0.004423918360000001, - 0.00448012636, - 0.003904502360000001, - 0.003847043360000001 + 0.0041276084200000005, + 0.00401948342, + 0.00396802542, + 0.0035874414200000003, + 0.0037146084200000004, + 0.00368881642, + 0.00393965042, + 0.00366756742, + 0.00378523342, + 0.00373748342 ], "memory_usage_byte": [ - 17252352, - 17252352, - 17252352, - 17252352, - 17252352, - 17252352, - 17252352, - 17252352, - 17252352, - 17252352 + 16596992, + 16596992, + 16596992, + 16596992, + 16596992, + 16596992, + 16596992, + 16596992, + 16596992, + 16596992 ], "exit_codes": [ 1, @@ -94,36 +94,36 @@ }, { "command": "pyright", - "mean": 0.6794721392600002, - "stddev": 0.007811145132987723, - "median": 0.68004929386, - "user": 0.97114748, - "system": 0.06362314, - "min": 0.6676491683600001, - "max": 0.6903340013600001, + "mean": 0.65332040822, + "stddev": 0.007175844060327018, + "median": 0.65113337942, + "user": 0.94589844, + "system": 0.05645524, + "min": 0.64352514942, + "max": 0.6660330254200001, "times": [ - 0.6677285853600001, - 0.6774752933600001, - 0.68150071036, - 0.6676491683600001, - 0.6880117523600001, - 0.6785978773600001, - 0.67508412636, - 0.68620304336, - 0.68213683436, - 0.6903340013600001 + 0.64796456642, + 0.65618102442, + 0.65753648342, + 0.66261989942, + 0.65105777542, + 0.64636560842, + 0.65071156642, + 0.64352514942, + 0.65120898342, + 0.6660330254200001 ], "memory_usage_byte": [ - 178012160, - 178012160, - 178438144, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752 + 178454528, + 178454528, + 178454528, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352 ], "exit_codes": [ 1, @@ -140,36 +140,36 @@ }, { "command": "mypy", - "mean": 0.5946363597600001, - "stddev": 0.008025175431970554, - "median": 0.5928852518600001, - "user": 0.5327079799999999, - "system": 0.05728443999999999, - "min": 0.5838333763600001, - "max": 0.6119110843600001, + "mean": 0.5626130167200001, + "stddev": 0.002193795773018898, + "median": 0.56198019192, + "user": 0.5076552400000001, + "system": 0.05216124, + "min": 0.56020219142, + "max": 0.5664432744200001, "times": [ - 0.58993191836, - 0.59580891836, - 0.5897093763600001, - 0.5886855013600001, - 0.60078966836, - 0.5984730843600001, - 0.5899615853600001, - 0.6119110843600001, - 0.5838333763600001, - 0.5972590843600001 + 0.56443652542, + 0.56020219142, + 0.56245206642, + 0.5664432744200001, + 0.56579198342, + 0.5622749004200001, + 0.56117915042, + 0.5607988164200001, + 0.56086577542, + 0.56168548342 ], "memory_usage_byte": [ - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752 + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352 ], "exit_codes": [ 1, @@ -186,36 +186,36 @@ }, { "command": "mypy-warm", - "mean": 0.16817201416000002, - "stddev": 0.0028551889767360894, - "median": 0.16767268886, - "user": 0.13784138, - "system": 0.027592839999999997, - "min": 0.16394554336, - "max": 0.17328262736, + "mean": 0.16163195452000004, + "stddev": 0.003811245066326939, + "median": 0.16206346292, + "user": 0.13323624, + "system": 0.026273539999999995, + "min": 0.15609840042, + "max": 0.16750152542000002, "times": [ - 0.16394554336, - 0.16962554336000002, - 0.17328262736, - 0.16493229336000004, - 0.16733250136, - 0.16960516836, - 0.16723641836000003, - 0.16801287636, - 0.17133683436000002, - 0.16641033536 + 0.15902269242, + 0.16562923342, + 0.16154527542, + 0.15825377542000002, + 0.16535235842, + 0.16258165042, + 0.15609840042, + 0.16265915042, + 0.16750152542000002, + 0.15767548342 ], "memory_usage_byte": [ - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752 + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352 ], "exit_codes": [ 1, @@ -232,82 +232,82 @@ }, { "command": "ty", - "mean": 0.03845762256, - "stddev": 0.000797329911955006, - "median": 0.03860273136, - "user": 0.02967768, - "system": 0.006646740000000001, - "min": 0.03664975136, - "max": 0.03972479336, + "mean": 0.038030504119999996, + "stddev": 0.0013473158868680798, + "median": 0.03770725392, + "user": 0.02912364, + "system": 0.00688804, + "min": 0.03659302442, + "max": 0.04026673342, "times": [ - 0.03830004336, - 0.038612377360000005, - 0.03859308536, - 0.03886379336, - 0.037820418360000004, - 0.03664975136, - 0.038882627360000005, - 0.03972479336, - 0.038510751360000005, - 0.03861858436 + 0.04000794142, + 0.03846794142, + 0.04026673342, + 0.03796035842, + 0.03720569142, + 0.03667890042, + 0.03745414942, + 0.03677494242, + 0.03659302442, + 0.03889535842 ], "memory_usage_byte": [ - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752 + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352 ], "exit_codes": [ - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1 + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 ] }, { "command": "pyrefly", - "mean": 0.15404091005999998, - "stddev": 0.002345525448251591, - "median": 0.15387366836, - "user": 0.11246748000000002, - "system": 0.040757739999999994, - "min": 0.15028700136, - "max": 0.15969300136000003, + "mean": 0.11441830821999999, + "stddev": 0.002773579158891702, + "median": 0.11425608742000001, + "user": 0.09665104, + "system": 0.01810514, + "min": 0.11115914942, + "max": 0.12047798342, "times": [ - 0.15969300136000003, - 0.15363446036, - 0.15411287635999998, - 0.15350666836, - 0.15272441836, - 0.15320641835999999, - 0.15475158536, - 0.15422816836000003, - 0.15028700136, - 0.15426450236 + 0.11423119142, + 0.11142098342000001, + 0.11356548342, + 0.11709152442000001, + 0.11234527442, + 0.11428098342000001, + 0.12047798342, + 0.11524385842, + 0.11436665042000001, + 0.11115914942 ], "memory_usage_byte": [ - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752 + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352 ], "exit_codes": [ 1, @@ -324,36 +324,36 @@ }, { "command": "zuban", - "mean": 0.02582361416, - "stddev": 0.0007962173770823873, - "median": 0.02577862686, - "user": 0.051471180000000005, - "system": 0.008199040000000001, - "min": 0.02466562636, - "max": 0.027387376360000004, + "mean": 0.02629736252, + "stddev": 0.0007729512278975448, + "median": 0.02628264992, + "user": 0.05275584, + "system": 0.00852384, + "min": 0.025414191420000003, + "max": 0.027389983420000003, "times": [ - 0.02610437636, - 0.026198585360000002, - 0.02506566836, - 0.02621704336, - 0.026444126360000004, - 0.02535212736, - 0.02466562636, - 0.02545287736, - 0.025348334360000002, - 0.027387376360000004 + 0.027382233420000002, + 0.026424483420000002, + 0.025510525420000003, + 0.025414191420000003, + 0.025526649420000002, + 0.027047942420000002, + 0.027389983420000003, + 0.025678316420000003, + 0.02614081642, + 0.02645848342 ], "memory_usage_byte": [ - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752, - 178634752 + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352, + 179044352 ], "exit_codes": [ 1, diff --git a/benchmarks/results/typeddict_readonly_mutation.json b/benchmarks/results/typeddict_readonly_mutation.json index a5a60676..b834eb99 100644 --- a/benchmarks/results/typeddict_readonly_mutation.json +++ b/benchmarks/results/typeddict_readonly_mutation.json @@ -2,36 +2,36 @@ "results": [ { "command": "basilisk", - "mean": 0.00986859328, - "stddev": 0.00041757448903191694, - "median": 0.009801043180000002, - "user": 0.00754296, - "system": 0.0017598199999999998, - "min": 0.009324751680000002, - "max": 0.010744876680000001, + "mean": 0.010344735160000002, + "stddev": 0.00034651108422337066, + "median": 0.01035418106, + "user": 0.007704259999999999, + "system": 0.0020188999999999997, + "min": 0.009819535059999999, + "max": 0.01095228506, "times": [ - 0.010007626680000001, - 0.010744876680000001, - 0.009498834680000002, - 0.010259334680000001, - 0.009324751680000002, - 0.010003835680000002, - 0.009641959680000001, - 0.009647043680000002, - 0.009602626680000002, - 0.009955042680000002 + 0.009819535059999999, + 0.009978619059999999, + 0.01013353506, + 0.01036099306, + 0.010126243060000002, + 0.01034736906, + 0.010530702060000001, + 0.01095228506, + 0.01074920206, + 0.010448868060000002 ], "memory_usage_byte": [ - 15695872, - 15728640, - 15728640, - 15728640, - 15728640, - 15728640, - 15728640, - 15728640, - 15728640, - 15728640 + 14991360, + 15220736, + 15220736, + 15220736, + 15220736, + 15220736, + 15220736, + 15220736, + 15220736, + 15220736 ], "exit_codes": [ 1, @@ -48,36 +48,36 @@ }, { "command": "basilisk-warm", - "mean": 0.00439371008, - "stddev": 0.00035316327095809676, - "median": 0.00426304368, - "user": 0.0023092600000000005, - "system": 0.0015702199999999996, - "min": 0.00403591868, - "max": 0.00504283468, + "mean": 0.00402505176, + "stddev": 0.00013511833620863183, + "median": 0.00399057656, + "user": 0.00224306, + "system": 0.0013377999999999997, + "min": 0.00381449306, + "max": 0.00427857706, "times": [ - 0.004102293680000001, - 0.004489876680000001, - 0.00403591868, - 0.0042409186800000005, - 0.00504283468, - 0.00461829368, - 0.004042626680000001, - 0.0048769176800000005, - 0.00428516868, - 0.004202251680000001 + 0.00412482706, + 0.00396916006, + 0.00398107706, + 0.00427857706, + 0.00410436906, + 0.0038820350600000005, + 0.00381449306, + 0.00412628506, + 0.00396961806, + 0.00400007606 ], "memory_usage_byte": [ - 16023552, - 16023552, - 16023552, - 16023552, - 16023552, - 16023552, - 16023552, - 16023552, - 16023552, - 16023552 + 15466496, + 15466496, + 15466496, + 15466496, + 15466496, + 15466496, + 15466496, + 15466496, + 15466496, + 15466496 ], "exit_codes": [ 1, @@ -94,36 +94,36 @@ }, { "command": "pyright", - "mean": 0.63574040158, - "stddev": 0.009546784159683611, - "median": 0.63590639768, - "user": 0.90157726, - "system": 0.06251952, - "min": 0.61797783468, - "max": 0.65372737668, + "mean": 0.6142969684599999, + "stddev": 0.009024373086713814, + "median": 0.61397445206, + "user": 0.87601116, + "system": 0.0536051, + "min": 0.60087053506, + "max": 0.62980895106, "times": [ - 0.6295723346800001, - 0.61797783468, - 0.64052337668, - 0.65372737668, - 0.63467179368, - 0.63714100168, - 0.63294025168, - 0.62863341768, - 0.64189100168, - 0.64032562668 + 0.60790103506, + 0.62980895106, + 0.6233969930600001, + 0.61602874406, + 0.61192016006, + 0.60087053506, + 0.60364820206, + 0.62170778506, + 0.6161855350600001, + 0.61150174406 ], "memory_usage_byte": [ - 164888576, - 164888576, - 164888576, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336 + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496 ], "exit_codes": [ 1, @@ -140,36 +140,36 @@ }, { "command": "mypy", - "mean": 0.5988618934800001, - "stddev": 0.006878062271331195, - "median": 0.59837821018, - "user": 0.53589846, - "system": 0.05865292, - "min": 0.5898983346800001, - "max": 0.61407429368, + "mean": 0.57500458526, + "stddev": 0.011788292973393599, + "median": 0.57141359806, + "user": 0.5164402600000001, + "system": 0.05372439999999999, + "min": 0.56164378506, + "max": 0.59511945106, "times": [ - 0.59956625168, - 0.61407429368, - 0.59620329368, - 0.5898983346800001, - 0.5971901686800001, - 0.60145316868, - 0.59371866868, - 0.60209312668, - 0.60243516868, - 0.5919864596800001 + 0.56598466006, + 0.56164378506, + 0.5642984520600001, + 0.5713612020600001, + 0.57243224406, + 0.57146599406, + 0.59511945106, + 0.59378891006, + 0.58324224406, + 0.57070891006 ], "memory_usage_byte": [ - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336 + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496 ], "exit_codes": [ 1, @@ -186,36 +186,36 @@ }, { "command": "mypy-warm", - "mean": 0.16738057258000003, - "stddev": 0.0031974641094376136, - "median": 0.16607854318, - "user": 0.13729555999999998, - "system": 0.02723282, - "min": 0.16346571068, - "max": 0.17514504268, + "mean": 0.17952423086000002, + "stddev": 0.016583780881294075, + "median": 0.17535591006, + "user": 0.14270215999999997, + "system": 0.0308646, + "min": 0.16247653506, + "max": 0.22281707706000003, "times": [ - 0.16590837668, - 0.17514504268, - 0.16936412668, - 0.16581504368, - 0.16346571068, - 0.16560766868, - 0.16615745968, - 0.16599962668, - 0.16801116868, - 0.16833150168 + 0.16247653506, + 0.16899695106, + 0.17215841006000002, + 0.17163728506000003, + 0.18047428506000002, + 0.18638978506000003, + 0.22281707706000003, + 0.17958016006000002, + 0.17569311806000001, + 0.17501870206000003 ], "memory_usage_byte": [ - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336 + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496 ], "exit_codes": [ 1, @@ -232,36 +232,36 @@ }, { "command": "ty", - "mean": 0.04309535568, - "stddev": 0.0014724295229101977, - "median": 0.04352543918, - "user": 0.03250776, - "system": 0.00765782, - "min": 0.03989525168000001, - "max": 0.04529554268, + "mean": 0.04317563106, + "stddev": 0.001493334156596797, + "median": 0.04342453556, + "user": 0.03292616, + "system": 0.007584900000000001, + "min": 0.04066853506, + "max": 0.04472286806, "times": [ - 0.044096709680000006, - 0.043587876680000005, - 0.04267562668, - 0.04299108468000001, - 0.04529554268, - 0.04368312668, - 0.04161045968, - 0.03989525168000001, - 0.04365487668, - 0.04346300168 + 0.04324920206, + 0.04466561806, + 0.04416111906, + 0.04472286806, + 0.04315757706, + 0.04359986906, + 0.04461703506, + 0.04066853506, + 0.04133349306, + 0.04158099406 ], "memory_usage_byte": [ - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336 + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496 ], "exit_codes": [ 1, @@ -278,36 +278,36 @@ }, { "command": "pyrefly", - "mean": 0.14770835558, - "stddev": 0.002122147315571061, - "median": 0.14726383467999998, - "user": 0.10601226000000001, - "system": 0.04063082, - "min": 0.14482962668, - "max": 0.15079200168, + "mean": 0.10997690586, + "stddev": 0.0036025606267433923, + "median": 0.11136143056, + "user": 0.09103616, + "system": 0.018250699999999998, + "min": 0.10556632606000001, + "max": 0.11398278506000001, "times": [ - 0.14482962668, - 0.14628316768, - 0.14730975168, - 0.14598387667999999, - 0.15079200168, - 0.14972758468, - 0.14999533468, - 0.14946112668, - 0.14721791768, - 0.14548316768 + 0.11259153506, + 0.10653736806, + 0.10562199406, + 0.10598645206, + 0.11336957706, + 0.11074674306, + 0.10556632606000001, + 0.11398278506000001, + 0.11197611806, + 0.11339016006000001 ], "memory_usage_byte": [ - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336 + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496 ], "exit_codes": [ 1, @@ -324,36 +324,36 @@ }, { "command": "zuban", - "mean": 0.02571645588, - "stddev": 0.00040045804413496296, - "median": 0.02562875118, - "user": 0.049694760000000004, - "system": 0.00864982, - "min": 0.02508858568, - "max": 0.02643595968, + "mean": 0.02685078516, + "stddev": 0.0009115615179342226, + "median": 0.02686974306, + "user": 0.05058256000000001, + "system": 0.008564899999999997, + "min": 0.025503660060000002, + "max": 0.02875832706, "times": [ - 0.02566470968, - 0.02643595968, - 0.02559279268, - 0.02597558568, - 0.02508858568, - 0.02535045968, - 0.02600591868, - 0.02540412668, - 0.02605579368, - 0.02559062668 + 0.02705341006, + 0.025503660060000002, + 0.026139702060000002, + 0.026341368060000002, + 0.02668607606, + 0.02875832706, + 0.02709282706, + 0.027080744060000002, + 0.026194785060000003, + 0.027656952060000004 ], "memory_usage_byte": [ - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336, - 165134336 + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496, + 164970496 ], "exit_codes": [ 1, diff --git a/benchmarks/results/typevar_constraints.json b/benchmarks/results/typevar_constraints.json index 1c08387a..f45d4d60 100644 --- a/benchmarks/results/typevar_constraints.json +++ b/benchmarks/results/typevar_constraints.json @@ -2,36 +2,36 @@ "results": [ { "command": "basilisk", - "mean": 0.01818984996, - "stddev": 0.00170269968081746, - "median": 0.01768683386, - "user": 0.015081800000000001, - "system": 0.0022966999999999996, - "min": 0.01715964586, - "max": 0.022961853860000003, + "mean": 0.01703933252, + "stddev": 0.0005291569706391897, + "median": 0.016949762120000003, + "user": 0.014596139999999999, + "system": 0.00188942, + "min": 0.01645399112, + "max": 0.01786219912, "times": [ - 0.017659687860000002, - 0.01715964586, - 0.01763085386, - 0.01732856186, - 0.017795728860000002, - 0.022961853860000003, - 0.018287562860000002, - 0.01759852086, - 0.01771397986, - 0.01776210386 + 0.01661403212, + 0.016744783120000002, + 0.016572699120000003, + 0.01645399112, + 0.016487782120000002, + 0.017389157120000002, + 0.01715474112, + 0.01769686612, + 0.01786219912, + 0.01741707412 ], "memory_usage_byte": [ - 18087936, - 18087936, - 18186240, - 18186240, - 18186240, - 18366464, - 18808832, - 18808832, - 18808832, - 18808832 + 17448960, + 17448960, + 17448960, + 17448960, + 17448960, + 17448960, + 17448960, + 17448960, + 17448960, + 17448960 ], "exit_codes": [ 1, @@ -48,36 +48,36 @@ }, { "command": "basilisk-warm", - "mean": 0.004987058260000001, - "stddev": 0.0002307312519501613, - "median": 0.00503175036, - "user": 0.0030829000000000004, - "system": 0.0016003999999999997, - "min": 0.00455197886, - "max": 0.005272395859999999, + "mean": 0.0055338156199999985, + "stddev": 0.0005437353510501325, + "median": 0.005439553119999999, + "user": 0.00325244, + "system": 0.00171772, + "min": 0.0049409491199999996, + "max": 0.0068747401199999985, "times": [ - 0.00478281186, - 0.00491027086, - 0.00455197886, - 0.00522439586, - 0.00501431286, - 0.005210103859999999, - 0.0050742708600000005, - 0.00504918786, - 0.005272395859999999, - 0.00478085386 + 0.0068747401199999985, + 0.005649657119999998, + 0.005574615119999999, + 0.00570282412, + 0.005304491119999999, + 0.005291282119999999, + 0.005729324119999999, + 0.0049409491199999996, + 0.005128407119999999, + 0.00514186612 ], "memory_usage_byte": [ - 20119552, - 20119552, - 20119552, - 20119552, - 20119552, - 20119552, - 20119552, - 20119552, - 20119552, - 20119552 + 18120704, + 18120704, + 18120704, + 18120704, + 18120704, + 18120704, + 18120704, + 18120704, + 18120704, + 18120704 ], "exit_codes": [ 1, @@ -94,36 +94,36 @@ }, { "command": "pyright", - "mean": 0.73998981246, - "stddev": 0.017816214788731972, - "median": 0.73430566686, - "user": 0.9192872999999999, - "system": 0.058351299999999995, - "min": 0.7246610208600001, - "max": 0.7809545208600001, + "mean": 0.7268296616200001, + "stddev": 0.016245748009026874, + "median": 0.72539786562, + "user": 0.9056068399999999, + "system": 0.056107319999999995, + "min": 0.70596599012, + "max": 0.76041869912, "times": [ - 0.73567027086, - 0.75626164586, - 0.73320935386, - 0.7246610208600001, - 0.72612935386, - 0.73170889586, - 0.7809545208600001, - 0.72521827086, - 0.73540197986, - 0.75068281186 + 0.7339073661200001, + 0.72252753212, + 0.73239219912, + 0.76041869912, + 0.74064832412, + 0.71760069912, + 0.71907515812, + 0.70749244912, + 0.7282681991200001, + 0.70596599012 ], "memory_usage_byte": [ - 155385856, - 155385856, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608 + 154877952, + 154877952, + 154877952, + 154877952, + 154877952, + 154877952, + 154877952, + 154877952, + 155893760, + 155893760 ], "exit_codes": [ 1, @@ -140,36 +140,36 @@ }, { "command": "mypy", - "mean": 0.60320532926, - "stddev": 0.008653305017182701, - "median": 0.60058595836, - "user": 0.5391729, - "system": 0.05919749999999999, - "min": 0.59649472886, - "max": 0.6229876868600001, + "mean": 0.56827844082, + "stddev": 0.0041076434132964025, + "median": 0.56725472012, + "user": 0.5127005399999999, + "system": 0.05283871999999999, + "min": 0.56339969912, + "max": 0.57722728312, "times": [ - 0.60020497886, - 0.59767814586, - 0.60210443786, - 0.61443331286, - 0.6229876868600001, - 0.60205106286, - 0.59818452086, - 0.60096693786, - 0.59694747986, - 0.59649472886 + 0.57000903212, + 0.5705842411200001, + 0.56811199112, + 0.56530715812, + 0.56339969912, + 0.57722728312, + 0.56639744912, + 0.5712353651200001, + 0.56490103212, + 0.56561115712 ], "memory_usage_byte": [ - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608 + 155893760, + 155893760, + 155893760, + 155893760, + 155893760, + 155893760, + 155893760, + 155893760, + 155893760, + 155893760 ], "exit_codes": [ 1, @@ -186,36 +186,36 @@ }, { "command": "mypy-warm", - "mean": 0.16888247505999998, - "stddev": 0.0019180243974799669, - "median": 0.16889147886, - "user": 0.1389899, - "system": 0.027557499999999995, - "min": 0.16630377086, - "max": 0.17206535386, + "mean": 0.15981945332000003, + "stddev": 0.0011794727638789226, + "median": 0.15960190762, + "user": 0.13326334, + "system": 0.02455942, + "min": 0.15829703312, + "max": 0.16242494912000002, "times": [ - 0.17206535386, - 0.16630377086, - 0.16647752086, - 0.16888697886, - 0.16889597886, - 0.17128372986, - 0.16897110386, - 0.16815910486, - 0.16743327086, - 0.17034793786 + 0.15903090812, + 0.15991769912, + 0.16043699012, + 0.16060819912000002, + 0.15906174012000002, + 0.15893636612, + 0.16242494912000002, + 0.15829703312, + 0.15928611612000002, + 0.16019453212 ], "memory_usage_byte": [ - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608 + 155893760, + 155893760, + 155893760, + 155893760, + 155893760, + 155893760, + 155893760, + 155893760, + 155893760, + 155893760 ], "exit_codes": [ 1, @@ -232,36 +232,36 @@ }, { "command": "ty", - "mean": 0.03928530016, - "stddev": 0.002088655910227598, - "median": 0.039100187859999996, - "user": 0.029941600000000002, - "system": 0.0071309999999999985, - "min": 0.03683910386, - "max": 0.04467647886, + "mean": 0.03994324072000001, + "stddev": 0.0013917317496935809, + "median": 0.03936174112, + "user": 0.029886440000000004, + "system": 0.007821419999999999, + "min": 0.03874553212, + "max": 0.042369782120000005, "times": [ - 0.03822672886, - 0.03903756286, - 0.03952506286, - 0.03916281286, - 0.03817668786, - 0.04467647886, - 0.03922602086, - 0.03987268786, - 0.03683910386, - 0.03810985486 + 0.039160866120000005, + 0.042156282120000006, + 0.038868824120000006, + 0.039060783120000005, + 0.03874761612, + 0.03874553212, + 0.042369782120000005, + 0.039783865120000005, + 0.04097624012, + 0.039562616120000005 ], "memory_usage_byte": [ - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608 + 155893760, + 155893760, + 155893760, + 155893760, + 155893760, + 155893760, + 155893760, + 155893760, + 155893760, + 155893760 ], "exit_codes": [ 1, @@ -278,36 +278,36 @@ }, { "command": "pyrefly", - "mean": 0.14859303756, - "stddev": 0.0027062249804407123, - "median": 0.14756370886, - "user": 0.10541020000000001, - "system": 0.04220159999999999, - "min": 0.14556339586, - "max": 0.15239239586, + "mean": 0.11021209092000002, + "stddev": 0.002528789321974134, + "median": 0.10954322012, + "user": 0.08995744, + "system": 0.02042322, + "min": 0.10783057412000001, + "max": 0.11562032412, "times": [ - 0.15236747886, - 0.15239239586, - 0.14615356186, - 0.15179447986, - 0.14556339586, - 0.14586418686, - 0.14778181286, - 0.14731956286, - 0.14934789586, - 0.14734560486 + 0.10783057412000001, + 0.11350482412000001, + 0.11070661512, + 0.10838253312000001, + 0.10866540812, + 0.11023057412000001, + 0.10908324112000001, + 0.11562032412, + 0.11000319912, + 0.10809361612000001 ], "memory_usage_byte": [ - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608 + 155893760, + 155893760, + 155893760, + 155893760, + 155893760, + 155893760, + 155893760, + 155893760, + 155893760, + 155893760 ], "exit_codes": [ 1, @@ -324,36 +324,36 @@ }, { "command": "zuban", - "mean": 0.032554041560000004, - "stddev": 0.0007210883254923376, - "median": 0.03263495836, - "user": 0.057584399999999994, - "system": 0.008785999999999999, - "min": 0.03152968786, - "max": 0.03394018686, + "mean": 0.03219219082, + "stddev": 0.0009148761076403306, + "median": 0.032258741120000003, + "user": 0.05704614, + "system": 0.00872102, + "min": 0.030294490120000003, + "max": 0.03348057412, "times": [ - 0.03179972886, - 0.03288510386, - 0.03394018686, - 0.03152968786, - 0.032274478860000004, - 0.03239752086, - 0.03188227086, - 0.03289864586, - 0.03306039586, - 0.03287239586 + 0.03267565812, + 0.03276215812, + 0.030294490120000003, + 0.03178965712, + 0.032261783120000005, + 0.03150719912, + 0.03348057412, + 0.031760740120000006, + 0.033133949120000006, + 0.03225569912 ], "memory_usage_byte": [ - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608, - 155844608 + 155893760, + 155893760, + 155893760, + 155893760, + 155893760, + 155893760, + 155893760, + 155893760, + 155893760, + 155893760 ], "exit_codes": [ 1, diff --git a/benchmarks/results/undefined_names.json b/benchmarks/results/undefined_names.json index 90048508..f597b522 100644 --- a/benchmarks/results/undefined_names.json +++ b/benchmarks/results/undefined_names.json @@ -2,36 +2,36 @@ "results": [ { "command": "basilisk", - "mean": 0.01516418584, - "stddev": 0.00035163579477035287, - "median": 0.015135693840000002, - "user": 0.012599019999999999, - "system": 0.0019771399999999996, - "min": 0.014698401840000002, - "max": 0.01582181984, + "mean": 0.014548134240000002, + "stddev": 0.00033050287219140286, + "median": 0.014471809140000002, + "user": 0.012448879999999999, + "system": 0.0015863599999999995, + "min": 0.014120643140000002, + "max": 0.015061684140000002, "times": [ - 0.01526744384, - 0.015409235840000002, - 0.015515777840000003, - 0.01582181984, - 0.015263110840000002, - 0.015008276840000003, - 0.01486377784, - 0.014698401840000002, - 0.014928277840000002, - 0.014865735840000003 + 0.015002725140000001, + 0.014731143140000002, + 0.01432339214, + 0.015061684140000002, + 0.014786643140000002, + 0.01428410114, + 0.014120643140000002, + 0.014422684140000001, + 0.014227392140000001, + 0.014520934140000002 ], "memory_usage_byte": [ - 18055168, - 18055168, - 18055168, - 18432000, - 18432000, - 18432000, - 18432000, - 18432000, - 18432000, - 18432000 + 18382848, + 18382848, + 18513920, + 18513920, + 18513920, + 18513920, + 18513920, + 18513920, + 18513920, + 18513920 ], "exit_codes": [ 1, @@ -48,36 +48,36 @@ }, { "command": "basilisk-warm", - "mean": 0.00491708184, - "stddev": 0.00017294589519846972, - "median": 0.004960236340000001, - "user": 0.0030735199999999997, - "system": 0.00144624, - "min": 0.0045994858400000005, - "max": 0.005146944840000001, + "mean": 0.00474725504, + "stddev": 0.00019453984864143724, + "median": 0.004753267640000001, + "user": 0.0031111800000000003, + "system": 0.0012612599999999997, + "min": 0.004442934140000001, + "max": 0.005126934140000001, "times": [ - 0.005020568840000001, - 0.00501736084, - 0.00509011084, - 0.005146944840000001, - 0.0045994858400000005, - 0.00487998584, - 0.004975444840000001, - 0.004945027840000001, - 0.00477961084, - 0.00471627784 + 0.004870059140000001, + 0.0047221011400000006, + 0.00476185114, + 0.004442934140000001, + 0.004744684140000001, + 0.004602059140000001, + 0.005126934140000001, + 0.004533559140000001, + 0.004779184140000001, + 0.004889184140000001 ], "memory_usage_byte": [ - 19283968, - 19283968, - 19283968, - 19283968, - 19283968, - 19283968, - 19283968, - 19283968, - 19283968, - 19283968 + 18989056, + 18989056, + 18989056, + 18989056, + 18989056, + 18989056, + 18989056, + 18989056, + 18989056, + 18989056 ], "exit_codes": [ 1, @@ -94,36 +94,36 @@ }, { "command": "pyright", - "mean": 0.49990984824000007, - "stddev": 0.0032205509458932335, - "median": 0.49939602784, - "user": 0.65680502, - "system": 0.05587684, - "min": 0.49612161084, - "max": 0.50612606884, + "mean": 0.48994665494000006, + "stddev": 0.008951229197287844, + "median": 0.48769739264000006, + "user": 0.64774598, + "system": 0.05262935999999999, + "min": 0.47750843414000005, + "max": 0.50383364214, "times": [ - 0.49937331984, - 0.50173681884, - 0.49699427784, - 0.49941873583999996, - 0.50612606884, - 0.49801181884, - 0.50241627684, - 0.49647656884, - 0.49612161084, - 0.50242298584 + 0.5032654751400001, + 0.50383364214, + 0.48706422614000006, + 0.4838153921400001, + 0.48694589314000003, + 0.49120130914000004, + 0.48055872614000006, + 0.47750843414000005, + 0.48833055914000006, + 0.49694289214000004 ], "memory_usage_byte": [ - 158023680, - 158023680, - 158023680, - 158023680, - 158023680, - 158498816, - 158498816, - 158498816, - 158498816, - 158498816 + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608 ], "exit_codes": [ 1, @@ -140,36 +140,36 @@ }, { "command": "mypy", - "mean": 0.6516044400400001, - "stddev": 0.013617076011442934, - "median": 0.64438452734, - "user": 0.58405592, - "system": 0.06074754, - "min": 0.63807773584, - "max": 0.67765523584, + "mean": 0.61152084254, + "stddev": 0.008640570030552967, + "median": 0.60984839264, + "user": 0.5551622799999999, + "system": 0.052511059999999984, + "min": 0.60092389314, + "max": 0.62818285114, "times": [ - 0.67765523584, - 0.67159077684, - 0.64469744384, - 0.65430256984, - 0.6420486938400001, - 0.64279827784, - 0.63807773584, - 0.64407161084, - 0.6582834858400001, - 0.64251856984 + 0.6044652261400001, + 0.6020333511400001, + 0.60958072614, + 0.6161400171400001, + 0.61517289214, + 0.60772401714, + 0.60092389314, + 0.62818285114, + 0.62086939214, + 0.6101160591400001 ], "memory_usage_byte": [ - 158498816, - 158498816, - 158498816, - 158498816, - 158498816, - 158498816, - 158498816, - 158498816, - 158498816, - 158498816 + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608 ], "exit_codes": [ 1, @@ -186,36 +186,36 @@ }, { "command": "mypy-warm", - "mean": 0.17052359824000002, - "stddev": 0.004488622901395636, - "median": 0.16942104784, - "user": 0.14042822, - "system": 0.027661139999999994, - "min": 0.16641156984000002, - "max": 0.18225656884000002, + "mean": 0.16255139254, + "stddev": 0.001373411611315353, + "median": 0.16266683014, + "user": 0.13533618000000003, + "system": 0.02509306, + "min": 0.16073255914, + "max": 0.16550268414000002, "times": [ - 0.16932431884000002, - 0.16797006884000001, - 0.16962140184000002, - 0.16915515284000002, - 0.16951777684000002, - 0.17038211084000002, - 0.16641156984000002, - 0.16756173584000003, - 0.18225656884000002, - 0.17303527784000003 + 0.16314818414, + 0.16073255914, + 0.16275743414000002, + 0.16280489314000002, + 0.16076151714, + 0.16257622614, + 0.16550268414000002, + 0.16155997614, + 0.16310622614, + 0.16256422514000002 ], "memory_usage_byte": [ - 158498816, - 158498816, - 158498816, - 158498816, - 158498816, - 158498816, - 158498816, - 158498816, - 158498816, - 158498816 + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608 ], "exit_codes": [ 1, @@ -232,36 +232,36 @@ }, { "command": "ty", - "mean": 0.04951188574, - "stddev": 0.0006664044511310004, - "median": 0.04953854834, - "user": 0.038913619999999996, - "system": 0.00825884, - "min": 0.048545818840000005, - "max": 0.05078619484, + "mean": 0.050415675640000004, + "stddev": 0.0015251804172897235, + "median": 0.05051555914, + "user": 0.03953138, + "system": 0.008863659999999999, + "min": 0.04814405914, + "max": 0.05352864214, "times": [ - 0.04947361084, - 0.04980877784, - 0.04966427684, - 0.04960348584, - 0.05078619484, - 0.04884073584, - 0.04902315184, - 0.048545818840000005, - 0.05021944384, - 0.04915336084 + 0.05146080914, + 0.049661059140000005, + 0.05352864214, + 0.05078568414, + 0.05059660114, + 0.05041697514, + 0.05043451714, + 0.04814405914, + 0.04838035014, + 0.05074805914 ], "memory_usage_byte": [ - 158498816, - 158498816, - 158498816, - 158498816, - 158498816, - 158498816, - 158498816, - 158498816, - 158498816, - 158498816 + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608 ], "exit_codes": [ 1, @@ -278,36 +278,36 @@ }, { "command": "pyrefly", - "mean": 0.5873364483400001, - "stddev": 0.009943965337129117, - "median": 0.5844237773400001, - "user": 0.5368853200000001, - "system": 0.04669514, - "min": 0.5802214438400001, - "max": 0.61323573584, + "mean": 0.5394357215400001, + "stddev": 0.005350107583891751, + "median": 0.53743903814, + "user": 0.5162165799999999, + "system": 0.02243296, + "min": 0.5340395171400001, + "max": 0.5510534341400001, "times": [ - 0.59229890284, - 0.5888699028400001, - 0.5857104858400001, - 0.61323573584, - 0.58115944484, - 0.58094298584, - 0.58313706884, - 0.58084790184, - 0.5802214438400001, - 0.58694061084 + 0.5510534341400001, + 0.5452106011400001, + 0.53542197514, + 0.5355326841400001, + 0.5383887261400001, + 0.54092551714, + 0.5364893501400001, + 0.5340395171400001, + 0.5358556841400001, + 0.54143972614 ], "memory_usage_byte": [ - 158498816, - 158498816, - 158498816, - 158498816, - 158498816, - 158498816, - 158498816, - 158498816, - 158498816, - 158498816 + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608 ], "exit_codes": [ 1, @@ -324,36 +324,36 @@ }, { "command": "zuban", - "mean": 0.035453148340000004, - "stddev": 0.004038825455974168, - "median": 0.03393225684, - "user": 0.05970942, - "system": 0.009754539999999997, - "min": 0.03290311084, - "max": 0.04651344384, + "mean": 0.034233375940000005, + "stddev": 0.0006742481418849363, + "median": 0.03422683014, + "user": 0.05941808, + "system": 0.009973159999999998, + "min": 0.033145517140000004, + "max": 0.03529189214, "times": [ - 0.03338394484, - 0.04651344384, - 0.03619911084, - 0.03373473584, - 0.03290311084, - 0.03590419384, - 0.03323581884, - 0.03396861084, - 0.03389590284, - 0.03479261084 + 0.03529189214, + 0.03364026814, + 0.034688601140000004, + 0.03432672614, + 0.03405610114, + 0.033145517140000004, + 0.03348310114, + 0.03412693414, + 0.03477105914, + 0.03480355914 ], "memory_usage_byte": [ - 158498816, - 158498816, - 158498816, - 158498816, - 158498816, - 158498816, - 158498816, - 158498816, - 158498816, - 158498816 + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608, + 157892608 ], "exit_codes": [ 1, diff --git a/benchmarks/results/unresolved_imports.json b/benchmarks/results/unresolved_imports.json index 76cdb41e..dc436730 100644 --- a/benchmarks/results/unresolved_imports.json +++ b/benchmarks/results/unresolved_imports.json @@ -2,36 +2,36 @@ "results": [ { "command": "basilisk", - "mean": 0.011652474139999999, - "stddev": 0.00021320845063158808, - "median": 0.01167741214, - "user": 0.007958239999999998, - "system": 0.00316214, - "min": 0.01138420264, - "max": 0.011975536640000001, + "mean": 0.01287298844, + "stddev": 0.0010388168817218567, + "median": 0.012611276040000001, + "user": 0.00791124, + "system": 0.004369099999999999, + "min": 0.01179960954, + "max": 0.014670734539999999, "times": [ - 0.011975536640000001, - 0.01183816164, - 0.01138420264, - 0.01146207864, - 0.01145041164, - 0.01170232864, - 0.01178099464, - 0.01165249564, - 0.01141357764, - 0.01186495364 + 0.01256331754, + 0.014559775540000002, + 0.014670734539999999, + 0.013482026539999999, + 0.01272285854, + 0.01265923454, + 0.012327817540000001, + 0.01209073454, + 0.01179960954, + 0.01185377554 ], "memory_usage_byte": [ - 13434880, - 13434880, - 13434880, - 13434880, - 13434880, - 13434880, - 13434880, - 13434880, - 13434880, - 13434880 + 13860864, + 14336000, + 15515648, + 15515648, + 15515648, + 15515648, + 15515648, + 15515648, + 15515648, + 15515648 ], "exit_codes": [ 1, @@ -48,36 +48,36 @@ }, { "command": "basilisk-warm", - "mean": 0.00521449494, - "stddev": 0.0003540160777284462, - "median": 0.00518307814, - "user": 0.0030960400000000004, - "system": 0.0016795399999999998, - "min": 0.00469266164, - "max": 0.005648244639999999, + "mean": 0.0049663382400000015, + "stddev": 0.0001563070764325784, + "median": 0.00494073454, + "user": 0.0030474400000000002, + "system": 0.0015329, + "min": 0.00474994254, + "max": 0.005276692539999999, "times": [ - 0.005648244639999999, - 0.005033286640000001, - 0.00496349564, - 0.00547336964, - 0.00533286964, - 0.00484453664, - 0.005619328640000001, - 0.00557191164, - 0.00469266164, - 0.00496524464 + 0.00484460854, + 0.00509169254, + 0.005276692539999999, + 0.00489365154, + 0.004842358540000001, + 0.00498781754, + 0.00507315054, + 0.00487031754, + 0.00474994254, + 0.00503315054 ], "memory_usage_byte": [ - 15351808, - 15351808, - 15351808, - 15351808, - 15351808, - 15351808, - 15351808, - 15351808, - 15351808, - 15351808 + 15646720, + 15646720, + 15646720, + 15646720, + 15646720, + 15646720, + 15646720, + 15646720, + 15646720, + 15646720 ], "exit_codes": [ 1, @@ -94,36 +94,36 @@ }, { "command": "pyright", - "mean": 0.4752959867400001, - "stddev": 0.008755684054629823, - "median": 0.47481597464, - "user": 0.5554868400000001, - "system": 0.05681763999999999, - "min": 0.46049882864, - "max": 0.49177499464, + "mean": 0.45624562154000003, + "stddev": 0.0037408163242123116, + "median": 0.45572077554, + "user": 0.50799174, + "system": 0.0511807, + "min": 0.45026285854000003, + "max": 0.46103152654, "times": [ - 0.47161232864, - 0.46671682864, - 0.47060432764, - 0.48046257864, - 0.47578070364, - 0.48092057864, - 0.48073745264, - 0.47385124564000003, - 0.49177499464, - 0.46049882864 + 0.45488427654, + 0.45026285854000003, + 0.45833185854, + 0.45517685854000006, + 0.45099444254000004, + 0.45945019254, + 0.46103152654, + 0.45572006754000005, + 0.45572148354, + 0.46088265054000005 ], "memory_usage_byte": [ - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904 + 146079744, + 146079744, + 146194432, + 146259968, + 146259968, + 146259968, + 146259968, + 146866176, + 147849216, + 147849216 ], "exit_codes": [ 1, @@ -140,36 +140,36 @@ }, { "command": "mypy", - "mean": 0.68821859484, - "stddev": 0.004512959360587782, - "median": 0.68693324464, - "user": 0.55558224, - "system": 0.12861664, - "min": 0.68261082764, - "max": 0.6954220776400001, + "mean": 0.6509248093400001, + "stddev": 0.009855284383514467, + "median": 0.64872844254, + "user": 0.52738054, + "system": 0.11620209999999997, + "min": 0.63881652554, + "max": 0.67126010954, "times": [ - 0.69117266164, - 0.6871461616400001, - 0.69438716164, - 0.6954220776400001, - 0.68261082764, - 0.68429366164, - 0.68338328664, - 0.6861991206400001, - 0.68672032764, - 0.6908506616400001 + 0.64742377654, + 0.63881652554, + 0.64797081754, + 0.64275494254, + 0.64948606754, + 0.64199856754, + 0.65765615054, + 0.6509958595399999, + 0.66088527654, + 0.67126010954 ], "memory_usage_byte": [ - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904 + 147849216, + 147849216, + 147849216, + 147849216, + 147849216, + 147849216, + 147849216, + 147849216, + 147849216, + 147849216 ], "exit_codes": [ 1, @@ -186,36 +186,36 @@ }, { "command": "mypy-warm", - "mean": 0.17448629494000004, - "stddev": 0.002609918805692326, - "median": 0.17368830764, - "user": 0.14330294000000002, - "system": 0.028393839999999997, - "min": 0.17142132864, - "max": 0.17830178664000002, + "mean": 0.16783668414000003, + "stddev": 0.006170041128333176, + "median": 0.16433683804000004, + "user": 0.13795143999999998, + "system": 0.026891500000000002, + "min": 0.16222840054, + "max": 0.17756348454, "times": [ - 0.17395911964000002, - 0.17527995264000001, - 0.17341749564, - 0.17142132864, - 0.17249482864000001, - 0.17716803664000003, - 0.17830178664000002, - 0.17827370264, - 0.17221607764000002, - 0.17233062064000002 + 0.17277194254000003, + 0.17756348454, + 0.17119652554000003, + 0.16476702554000003, + 0.17703960954000003, + 0.16390665054000003, + 0.16295923454000003, + 0.16222840054, + 0.16322831754000003, + 0.16270565054000002 ], "memory_usage_byte": [ - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904 + 147849216, + 147849216, + 147849216, + 147849216, + 147849216, + 147849216, + 147849216, + 147849216, + 147849216, + 147849216 ], "exit_codes": [ 1, @@ -232,36 +232,36 @@ }, { "command": "ty", - "mean": 0.24165603653999995, - "stddev": 0.009111182644098423, - "median": 0.23800557814, - "user": 0.14185284, - "system": 0.09676034000000001, - "min": 0.23386057764, - "max": 0.26323578663999997, + "mean": 0.26480632594000003, + "stddev": 0.0018425017663129312, + "median": 0.26505535904, + "user": 0.14433984, + "system": 0.11772279999999999, + "min": 0.26192348454000003, + "max": 0.26695485854, "times": [ - 0.26323578663999997, - 0.24312078664, - 0.24133899464000003, - 0.25118332764, - 0.23844374464, - 0.23643570364000002, - 0.23756741164, - 0.23671337064, - 0.23466066164000002, - 0.23386057764 + 0.26511377554000004, + 0.26470002554000005, + 0.26695485854, + 0.26499694254, + 0.26685777554, + 0.26659540154000005, + 0.26238523454, + 0.26292002654, + 0.26192348454000003, + 0.26561573454000004 ], "memory_usage_byte": [ - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904 + 147849216, + 147849216, + 147849216, + 147849216, + 147849216, + 147849216, + 147849216, + 147849216, + 147849216, + 147849216 ], "exit_codes": [ 1, @@ -278,36 +278,36 @@ }, { "command": "pyrefly", - "mean": 0.5657920949400002, - "stddev": 0.006828772646766741, - "median": 0.5636867656400001, - "user": 0.39578914000000004, - "system": 0.16780624, - "min": 0.5575602446400001, - "max": 0.57752670364, + "mean": 0.8538434258400001, + "stddev": 0.011819073226278424, + "median": 0.84927894254, + "user": 0.41051234000000003, + "system": 0.4433128, + "min": 0.84211152554, + "max": 0.87754760954, "times": [ - 0.57752670364, - 0.5622104946400001, - 0.55892503664, - 0.56208682864, - 0.5615085366400001, - 0.5575602446400001, - 0.5676889526400001, - 0.57614524464, - 0.5691058706400001, - 0.5651630366400001 + 0.86165856754, + 0.86737381754, + 0.84626744254, + 0.84351023454, + 0.84926994254, + 0.87754760954, + 0.8492879425400001, + 0.84211152554, + 0.84407094254, + 0.85733623354 ], "memory_usage_byte": [ - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904 + 147849216, + 147849216, + 147849216, + 147849216, + 147849216, + 147849216, + 147849216, + 147849216, + 147849216, + 147849216 ], "exit_codes": [ 1, @@ -324,36 +324,36 @@ }, { "command": "zuban", - "mean": 0.24733214064000003, - "stddev": 0.0024673724536486085, - "median": 0.24668930714000004, - "user": 0.26949473999999995, - "system": 0.011424939999999998, - "min": 0.24471199464, - "max": 0.25258141164, + "mean": 0.29896971754, + "stddev": 0.004873815228953436, + "median": 0.29815833854000007, + "user": 0.31860663999999994, + "system": 0.011015599999999999, + "min": 0.29242144254, + "max": 0.30612065054000004, "times": [ - 0.24635099464000002, - 0.24896482864, - 0.24702761964000003, - 0.24821053664, - 0.24930974464000002, - 0.25258141164, - 0.24579432764, - 0.24474778664000002, - 0.24471199464, - 0.24562216164 + 0.30312619254, + 0.30030869254000003, + 0.30590210954, + 0.29827560954000004, + 0.29601044254000003, + 0.30612065054000004, + 0.29242144254, + 0.29804106754000004, + 0.29659669254000004, + 0.29289427554 ], "memory_usage_byte": [ - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904, - 150011904 + 147849216, + 147849216, + 147849216, + 147849216, + 147849216, + 147849216, + 147849216, + 147849216, + 147849216, + 147849216 ], "exit_codes": [ 1, diff --git a/benchmarks/status/darwin-arm64-apple-m4-max.csv b/benchmarks/status/darwin-arm64-apple-m4-max.csv index 9719814b..b697db54 100644 --- a/benchmarks/status/darwin-arm64-apple-m4-max.csv +++ b/benchmarks/status/darwin-arm64-apple-m4-max.csv @@ -3,34 +3,34 @@ # arch: arm64 # os: Darwin 25.5.0 # cores: 14 -# tools: basilisk=basilisk 0.0.0-dev+gcc78164c-dirty, pyright=pyright 1.1.408, mypy=mypy 1.19.1 (compiled: yes), ty=ty 0.0.19 (ae10022c2 2026-02-26), pyrefly=pyrefly 0.54.0, zuban=zuban 0.9.0 +# tools: basilisk=basilisk 0.0.0-dev+g962f2b34-dirty, pyright=pyright 1.1.408, mypy=mypy 1.19.1 (compiled: yes), ty=ty 0.0.19 (ae10022c2 2026-02-26), pyrefly=pyrefly 0.54.0, zuban=zuban 0.9.0 # runs: 10 minimum; noisy Basilisk CV > 15% is remeasured with at least 30 runs (hyperfine mean wall-clock, milliseconds) -# generated: 2026-07-11T14:40:45+1000 +# generated: 2026-07-17T14:23:34+1000 # note: _ms = COLD full-file CLI check from scratch (whole process: startup + stubs + analysis). _diags = error diagnostics the tool reported on that fixture in the measured configuration (error severity only; warnings/notes are not counted) — read every time next to its diags; a tool that reports 0 analyzed the file but flagged no errors there. A blank _ms cell means the tool either was not installed on this machine or failed to analyze that fixture (exit >= 2, e.g. parse abort) and was excluded rather than timed as a crash. Competitor versions are the LATEST official release pulled at the top of every run, so their columns always reflect current upstream, never a pinned build. Only basilisk and mypy have a -warm column (they keep a real cross-run cache): basilisk-warm = --cache result-cache hit; mypy-warm = incremental .mypy_cache hit (cold mypy = --no-incremental). pyright/ty/pyrefly keep NO cross-run result cache (a repeat run = cold), so they are measured cold-only. zuban is also cold-only but its mypy mode DOES reuse a ./.mypy_cache when present (no flag disables it), so we wipe ./.mypy_cache before every timed run to keep the measurement cold. mypy runs with --strict so it performs the strict-mode analysis the fixtures stress (plain mypy reports 'no issues' on the strictness fixtures); zuban runs as `zuban mypy --strict` for the same reason (its default `zuban check` mode skips these strictness rules). This file is ALWAYS rewritten with the latest measured numbers, even on a regression — the CI gate reads the committed baseline, never this working copy, so a slip is recorded here AND fails CI rather than being hidden. fixture,basilisk_ms,basilisk-warm_ms,pyright_ms,mypy_ms,mypy-warm_ms,ty_ms,pyrefly_ms,zuban_ms,basilisk_diags,pyright_diags,mypy_diags,ty_diags,pyrefly_diags,zuban_diags -aliases_type_statement,8.3,4.5,563.6,633.7,167.4,60.1,152.5,27.3,600,850,650,1850,700,600 -assignment_compatibility,8.8,5.5,608.9,611.4,169.9,50.3,150.8,31.2,2000,2000,2000,2000,2000,2000 -call_argument_types,14.6,5.0,670.7,636.8,168.9,53.9,160.1,49.0,998,998,1002,998,998,1000 -callables_subtyping,10.6,2.5,536.5,597.5,166.0,35.4,141.9,27.0,600,600,600,600,600,600 -classvar_scoping,15.2,5.8,621.9,639.4,167.8,56.8,169.4,31.6,2000,2000,2000,2000,4000,2000 -constructors_call_init,9.5,4.5,630.1,625.1,168.3,38.5,142.4,26.2,444,370,296,370,444,518 -dataclasses_usage,9.2,3.7,1627.8,667.6,170.0,58.6,215.3,56.9,500,500,500,500,500,500 -dict_key_hashability,11.9,5.2,541.5,638.6,168.7,39.1,143.4,30.3,2000,2000,0,0,0,0 -enums_member_values,7.8,4.1,586.5,593.5,167.5,43.1,146.0,26.7,480,480,80,480,480,480 -final_reassignment,6.9,4.0,467.5,590.6,166.9,26.7,137.6,24.0,500,500,500,500,500,500 -generics_defaults_specialization,10.5,4.3,578.0,608.2,167.0,34.9,141.8,26.5,560,560,560,421,560,560 -literals_semantics,11.4,4.2,544.8,600.0,169.4,31.7,141.1,27.2,576,576,576,576,576,576 -match_exhaustiveness,10.6,4.0,541.3,623.8,168.5,36.3,147.9,26.6,500,0,0,500,0,0 -narrowing_typeis,9.5,4.2,552.5,605.0,167.3,35.6,145.5,25.6,520,520,520,520,520,520 -newtype_definition,10.7,5.8,730.7,643.2,168.6,22.9,156.8,35.6,2000,2000,2000,0,2000,2000 -overloads_evaluation,12.5,4.3,609.2,637.7,166.8,59.1,157.6,34.7,600,1401,801,600,600,801 -override_compatibility,14.3,3.7,655.0,624.0,167.2,40.7,150.8,28.1,200,200,300,1,200,300 -protocols_definition,8.8,4.2,576.2,603.7,166.7,35.0,140.9,27.0,536,402,402,335,402,402 -returns_compatibility,7.4,5.2,503.4,591.7,169.5,31.0,137.2,23.9,1080,540,540,540,540,540 -tuples_index,8.4,3.7,563.4,587.5,166.5,32.7,139.2,25.7,600,600,600,1800,600,600 -typeddict_key_access,9.6,4.0,630.9,595.7,170.7,37.2,144.7,25.7,500,500,500,500,500,500 -typeddict_readonly_inheritance,14.2,4.3,679.5,594.6,168.2,38.5,154.0,25.8,500,500,500,2,500,500 -typeddict_readonly_mutation,9.9,4.4,635.7,598.9,167.4,43.1,147.7,25.7,500,500,500,500,500,500 -typevar_constraints,18.2,5.0,740.0,603.2,168.9,39.3,148.6,32.6,2000,2000,2000,2000,2000,2000 -undefined_names,15.2,4.9,499.9,651.6,170.5,49.5,587.3,35.5,2000,2000,4000,2000,2000,4000 -unresolved_imports,11.7,5.2,475.3,688.2,174.5,241.7,565.8,247.3,2000,2000,2000,2000,2000,2000 +aliases_type_statement,8.1,4.4,559.9,595.9,162.8,54.5,110.0,27.1,600,850,650,1850,700,600 +assignment_compatibility,8.6,4.9,598.5,574.0,164.0,51.9,112.6,30.3,2000,2000,2000,2000,2000,2000 +call_argument_types,13.5,4.2,841.8,613.3,171.5,55.5,114.3,48.2,998,998,1002,998,998,1000 +callables_subtyping,12.1,4.1,529.6,574.5,162.0,38.3,107.4,28.4,600,600,600,600,600,600 +classvar_scoping,15.3,5.7,606.2,620.3,170.3,57.4,133.5,31.5,2000,2000,2000,2000,4000,2000 +constructors_call_init,9.1,4.1,613.2,573.4,156.1,37.0,100.4,26.2,444,370,296,370,444,518 +dataclasses_usage,9.2,3.8,1579.1,640.6,158.3,57.9,170.5,55.1,500,500,500,500,500,500 +dict_key_hashability,11.4,4.7,517.4,592.6,157.4,39.1,103.5,29.9,2000,2000,0,0,0,0 +enums_member_values,7.2,3.7,569.8,549.7,155.9,40.2,100.3,26.3,480,480,80,480,480,480 +final_reassignment,6.2,3.7,473.2,545.3,156.0,26.3,95.7,23.3,500,500,500,500,500,500 +generics_defaults_specialization,9.7,3.8,576.8,557.4,157.8,33.7,100.9,25.4,560,560,560,420,560,560 +literals_semantics,10.7,3.9,526.5,550.6,176.9,39.3,105.4,26.9,576,576,576,576,576,576 +match_exhaustiveness,10.4,4.1,547.1,970.8,162.0,26.0,109.9,27.9,500,0,0,0,0,0 +narrowing_typeis,8.7,3.7,549.5,573.8,161.9,33.6,108.1,27.2,520,520,520,520,520,520 +newtype_definition,11.0,4.8,710.5,616.6,164.3,44.6,118.1,35.3,2000,2000,2000,2000,2000,2000 +overloads_evaluation,12.2,4.4,593.0,603.8,162.0,60.3,115.7,33.6,600,1401,801,600,600,801 +override_compatibility,13.6,4.4,634.6,597.6,162.9,40.8,109.2,28.2,200,200,300,1,200,300 +protocols_definition,8.4,3.8,563.0,564.7,160.8,34.1,102.2,26.9,536,402,402,335,402,402 +returns_compatibility,7.1,4.5,503.1,562.7,165.0,32.6,100.8,24.1,1080,540,540,540,540,540 +tuples_index,8.7,3.6,550.2,558.1,159.8,34.1,101.8,25.0,600,600,600,1800,600,600 +typeddict_key_access,8.9,3.5,627.3,563.3,160.2,36.7,105.4,26.2,500,500,500,500,500,500 +typeddict_readonly_inheritance,13.9,3.8,653.3,562.6,161.6,38.0,114.4,26.3,500,500,500,0,500,500 +typeddict_readonly_mutation,10.3,4.0,614.3,575.0,179.5,43.2,110.0,26.9,500,500,500,500,500,500 +typevar_constraints,17.0,5.5,726.8,568.3,159.8,39.9,110.2,32.2,2000,2000,2000,2000,2000,2000 +undefined_names,14.5,4.7,489.9,611.5,162.6,50.4,539.4,34.2,2000,2000,4000,2000,2000,4000 +unresolved_imports,12.9,5.0,456.2,650.9,167.8,264.8,853.8,299.0,2000,2000,2000,2000,2000,2000 diff --git a/book/EDITORIAL-BRIEF.md b/book/EDITORIAL-BRIEF.md index da29f7d0..d9008fe4 100644 --- a/book/EDITORIAL-BRIEF.md +++ b/book/EDITORIAL-BRIEF.md @@ -52,9 +52,11 @@ whenever prediction will expose their mental model. ## Scope and versions -- Canonical language target: Python 3.12 - Typing semantics: current Python typing specification, with historical PEPs used for rationale rather than as a substitute for the maintained spec +- Python versions: no edition-wide support target. Mention a version only when + the typing specification, an accepted PEP, or documented runtime behaviour + makes the boundary relevant to the lesson - Basilisk behavior: one named release per book edition - Screenshots: captured from that same release and recorded in `figures.json` - Website: practical companion links may move forward; release-specific claims @@ -93,14 +95,18 @@ repository can resolve it first; a later edition can then add the topic. - Every diagnostic excerpt must be generated from the documented Basilisk release, not composed from memory. - Show file paths when an example spans files. -- Prefer Python 3.12 syntax unless a compatibility comparison is the lesson. +- Use the least version-specific syntax that teaches the lesson. When syntax + has a Python-version floor, name and cite the governing PEP or language + documentation beside it. - Do not add typing syntax purely to make an example look sophisticated. - Explain whether an annotation changes runtime behavior in the demonstrated context. ## Corrections and maintenance -Every edition records its build date, Basilisk version, Python target, example -test result, link-audit result, and screenshot environment. Readers should be -sent to the Basilisk website and repository issue tracker for live corrections; -the final URLs are confirmed before publication. +Every edition records its build date, Basilisk version, interpreter used to run +the examples, example test result, link-audit result, and screenshot +environment. That interpreter record makes the build reproducible; it is not a +Basilisk support boundary. Readers should be sent to the Basilisk website and +repository issue tracker for live corrections; the final URLs are confirmed +before publication. diff --git a/book/Makefile b/book/Makefile index 2a9fb547..2d850535 100644 --- a/book/Makefile +++ b/book/Makefile @@ -3,6 +3,7 @@ PYTHON ?= python3 .PHONY: check check-links render-assets screenshots epub release clean check: + $(PYTHON) -m unittest scripts.test_book_contract $(PYTHON) scripts/check_book.py $(PYTHON) scripts/check_links.py diff --git a/book/OUTLINE.md b/book/OUTLINE.md index ad87cb90..6ab6b5da 100644 --- a/book/OUTLINE.md +++ b/book/OUTLINE.md @@ -55,7 +55,8 @@ evidence of behavior; diagrams explain relationships that screenshots cannot. - The promise and intended reader - Prerequisites: ordinary Python, no prior static-typing expertise -- Python 3.12 as the canonical language target for this edition +- The maintained typing specification as the authority; Python-version + boundaries appear only when a governing PEP or runtime behaviour requires one - How to run, change, and re-check every example - What a type checker can and cannot tell you - Edition, Basilisk release, platform, and screenshot provenance @@ -128,12 +129,11 @@ without confusing annotations with runtime conversion. - Names, values, annotations, and runtime behavior - Built-in scalar and collection types - `T | None`, unions, and literal values -- `object`, `Any`, and the cost of opting out of useful information -- Type aliases that clarify a domain +- Type aliases that clarify a domain without creating a distinct type - Checkpoint: annotate raw readings and normalized readings -- Visuals: runtime/static split; union flow; `Any` information-loss diagram +- Visuals: runtime/static split; union flow; alias equivalence diagram - Source keys: `python-typing-docs`, `python-typing-spec-concepts`, - `cpython-typing-source`, `pep-604` + `python-typing-spec-aliases`, `pep-585`, `pep-604` ### Chapter 5 — Compatibility is the question diff --git a/book/README.md b/book/README.md index 023230ae..7685933c 100644 --- a/book/README.md +++ b/book/README.md @@ -64,8 +64,6 @@ make epub # build and validate the structural EPUB prototype make release # strict checks, external links, EPUB, EPUBCheck ``` -Requirements: Python 3.12+, Pandoc 3+, and EPUBCheck 5+. - ## Drafting rules 1. Treat `book.json` as the one source of chapter order and production targets. diff --git a/book/SOURCE-POLICY.md b/book/SOURCE-POLICY.md index 92bd7776..8c20e79e 100644 --- a/book/SOURCE-POLICY.md +++ b/book/SOURCE-POLICY.md @@ -11,9 +11,11 @@ Use the first applicable level: [Python typing specification](https://typing.python.org/en/latest/spec/), then the official [`python/typing`](https://github.com/python/typing) repository and conformance suite. -2. **Python language and runtime behavior:** versioned - [Python 3.12 documentation](https://docs.python.org/3.12/) and the +2. **Python language and runtime behavior:** the official + [Python documentation](https://docs.python.org/3/) and the [`python/cpython`](https://github.com/python/cpython) source repository. + Use the versioned documentation matching the behaviour only when the claim + actually depends on a Python release. 3. **Design history and accepted changes:** published [Python Enhancement Proposals](https://peps.python.org/). A PEP explains the accepted proposal and rationale; the maintained typing specification wins diff --git a/book/VISUAL-DESIGN-SYSTEM.md b/book/VISUAL-DESIGN-SYSTEM.md index 3dc8b0ac..8d200549 100644 --- a/book/VISUAL-DESIGN-SYSTEM.md +++ b/book/VISUAL-DESIGN-SYSTEM.md @@ -90,7 +90,7 @@ feedback loop. Use one focal point and no more than eight primary elements. - Capture real Basilisk behavior from the release recorded in `metadata.yaml`. - Use a clean fixture from `book/examples/`; never expose personal paths, tokens, unrelated extensions, notifications, or private repository names. -- Record OS, architecture, editor, theme, zoom, Python target, Basilisk version, +- Record OS, architecture, editor, theme, zoom, Python interpreter, Basilisk version, fixture, capture command/manual steps, and source file in `figures.json`. - Crop to the evidence while retaining enough editor/terminal context to orient the reader. @@ -134,4 +134,3 @@ Every ready visual must pass: The release gate also finds missing, unreferenced, and duplicate assets. The book should aim for one meaningful visual every two to three print-equivalent pages, not filler at a fixed interval. - diff --git a/book/assets/diagrams/00-reading-journey.png b/book/assets/diagrams/00-reading-journey.png index b8798180..831facd1 100644 Binary files a/book/assets/diagrams/00-reading-journey.png and b/book/assets/diagrams/00-reading-journey.png differ diff --git a/book/assets/diagrams/01-basilisk-system-map.png b/book/assets/diagrams/01-basilisk-system-map.png index 649ae5f0..3e1aac2f 100644 Binary files a/book/assets/diagrams/01-basilisk-system-map.png and b/book/assets/diagrams/01-basilisk-system-map.png differ diff --git a/book/assets/diagrams/01-basilisk-system-map.svg b/book/assets/diagrams/01-basilisk-system-map.svg index 832488f2..908713f5 100644 --- a/book/assets/diagrams/01-basilisk-system-map.svg +++ b/book/assets/diagrams/01-basilisk-system-map.svg @@ -27,7 +27,7 @@ normalize("18.4") pyproject.toml - Python target · rules + project context · rules project paths diff --git a/book/assets/diagrams/04-alias-domain-name.png b/book/assets/diagrams/04-alias-domain-name.png new file mode 100644 index 00000000..ddac19d6 Binary files /dev/null and b/book/assets/diagrams/04-alias-domain-name.png differ diff --git a/book/assets/diagrams/04-alias-domain-name.svg b/book/assets/diagrams/04-alias-domain-name.svg new file mode 100644 index 00000000..25bcb64c --- /dev/null +++ b/book/assets/diagrams/04-alias-domain-name.svg @@ -0,0 +1,45 @@ + + Type aliases name domain ideas without creating distinct types + RawReading and NormalizedReading appear as domain labels in a function signature, then both expand to the same dictionary type whose ReadingValue expands to string, float, or None. + + + + + + + + + + + + + DOMAIN VOCABULARY + An alias clarifies; it does not separate + Readable names can still expand to exactly the same underlying type. + + + normalize_reading(raw: RawReading) -> NormalizedReading + + + + RawReading + name at the input boundary + + + NormalizedReading + name at the output boundary + + + + + + dict[str, ReadingValue] + + + + ReadingValue + = + + str | float | None + + diff --git a/book/assets/diagrams/04-runtime-static-split.png b/book/assets/diagrams/04-runtime-static-split.png new file mode 100644 index 00000000..f1c6e7a9 Binary files /dev/null and b/book/assets/diagrams/04-runtime-static-split.png differ diff --git a/book/assets/diagrams/04-runtime-static-split.svg b/book/assets/diagrams/04-runtime-static-split.svg new file mode 100644 index 00000000..cb5a4ab3 --- /dev/null +++ b/book/assets/diagrams/04-runtime-static-split.svg @@ -0,0 +1,56 @@ + + Runtime conversion and static annotation are separate + A string passes through an explicit float call to become a floating-point value in the runtime lane. A separate static lane follows inferred string and float types to the expected float annotation. + + + + + + + + + + + + + + + + ANNOTATION ≠ CONVERSION + One line, two kinds of information + The call computes a value. The annotation states an expected type. + + + celsius: float = float(raw_celsius) + + + RUNTIME VALUES + + "21.5" + runtime type: str + + + + float(...) + + + + 21.5 + runtime type: float + + + + STATIC INFORMATION + + raw_celsius + inferred: str + + + float(...) + result: float + + + celsius: float + expected = result + + diff --git a/book/assets/diagrams/04-union-values.png b/book/assets/diagrams/04-union-values.png new file mode 100644 index 00000000..d204956c Binary files /dev/null and b/book/assets/diagrams/04-union-values.png differ diff --git a/book/assets/diagrams/04-union-values.svg b/book/assets/diagrams/04-union-values.svg new file mode 100644 index 00000000..42ed9ae6 --- /dev/null +++ b/book/assets/diagrams/04-union-values.svg @@ -0,0 +1,59 @@ + + A union names several permitted value types + String, float, and None values flow through a gate labelled ReadingValue equals str or float or None. A list reaches a stop outside the gate because it is not a named alternative. + + + + + + + + + + + + + + + + A BOUNDED SET OF POSSIBILITIES + A union permits what it names + It adds alternatives to a contract; it does not perform conversions. + + + + "21.5" + str + + + 21.5 + float + + + None + None + + + + + + + + + + READINGVALUE + str | float | None + Each named alternative passes. + + PEP 604 spelling + Python 3.10+ syntax boundary + + + + + [] · list + + + Not a named alternative + + diff --git a/book/assets/diagrams/09-configuration-resolution.png b/book/assets/diagrams/09-configuration-resolution.png index 4d535742..e7476909 100644 Binary files a/book/assets/diagrams/09-configuration-resolution.png and b/book/assets/diagrams/09-configuration-resolution.png differ diff --git a/book/assets/screenshots/README.md b/book/assets/screenshots/README.md index be065a0e..3bb16195 100644 --- a/book/assets/screenshots/README.md +++ b/book/assets/screenshots/README.md @@ -2,7 +2,8 @@ Only real captures from the Basilisk release named by the book belong here. The figure ledger records the editor/terminal, OS, architecture, theme, zoom, -Python target, Basilisk version, fixture, and capture method. +Python interpreter, Basilisk version, fixture, and capture method. The +interpreter is capture provenance, not a Basilisk support boundary. Use the repository's real capture pipelines where possible. Crop around one interaction, remove private information before capture, and add callouts in a diff --git a/book/book.json b/book/book.json index 17625aa0..52876c7b 100644 --- a/book/book.json +++ b/book/book.json @@ -2,7 +2,6 @@ "schemaVersion": 1, "title": "The Basilisk Book", "status": "structural-prototype", - "canonicalPythonVersion": "3.12", "basiliskRelease": null, "targets": { "words": 29700, @@ -26,7 +25,7 @@ "number": 1, "title": "Meet Basilisk", "file": "manuscript/01-meet-basilisk.md", - "status": "outline", + "status": "draft-complete", "targetWords": 1800, "targetPages": 6, "targetFigures": 2 @@ -59,7 +58,7 @@ "number": 4, "title": "The everyday type vocabulary", "file": "manuscript/04-everyday-type-vocabulary.md", - "status": "outline", + "status": "draft-complete", "targetWords": 2400, "targetPages": 8, "targetFigures": 3 diff --git a/book/evidence.json b/book/evidence.json index 871ae49d..7e8b4469 100644 --- a/book/evidence.json +++ b/book/evidence.json @@ -2,10 +2,56 @@ "schemaVersion": 1, "policy": "A chapter is publishable only when its governing spec, pinned release implementation, and executable evidence agree.", "chapters": [ - {"section": "01", "decision": "withhold-until-verified", "governingSpecs": [], "releaseImplementation": [], "executableEvidence": []}, + { + "section": "01", + "decision": "withhold-until-versioned-release", + "governingSpecs": [ + "docs/specs/CHECKER-ARCHITECTURE-SPEC.md#CHKARCH-COMMANDS", + "docs/specs/CHECKER-ARCHITECTURE-SPEC.md#CHKARCH-VERSION-TARGET", + "docs/specs/LSP-ARCHITECTURE-SPEC.md#LSPARCH-SYSTEM", + "docs/specs/LSP-ARCHITECTURE-SPEC.md#LSPARCH-ARCH-PIPELINE" + ], + "releaseImplementation": [], + "workingTreeImplementation": [ + "crates/basilisk-cli/src/main.rs", + "crates/basilisk-cli/src/pipeline/", + "crates/basilisk-lsp/src/workspace_analysis.rs" + ], + "executableEvidence": [ + "Python 3.14.6 executed the Signal Box normalization call and returned the documented dictionary on 2026-07-17", + "target/release/basilisk check --color never . reported the documented return incompatibility in Signal Box on 2026-07-17", + "target/release/basilisk analyze --color never . reported the configured missing-parameter policy in Signal Box on 2026-07-17" + ], + "blocker": "The current build identifies itself as 0.0.0-PLACEHOLDER, and the Chapter 1 editor screenshot is still planned. Pin a released Basilisk version, rerun the evidence, and capture the real editor surface before setting decision to publish." + }, {"section": "02", "decision": "withhold-until-verified", "governingSpecs": [], "releaseImplementation": [], "executableEvidence": []}, {"section": "03", "decision": "withhold-until-verified", "governingSpecs": [], "releaseImplementation": [], "executableEvidence": []}, - {"section": "04", "decision": "withhold-until-verified", "governingSpecs": [], "releaseImplementation": [], "executableEvidence": []}, + { + "section": "04", + "decision": "withhold-until-versioned-release", + "governingSpecs": [ + "docs/specs/CHECKER-ARCHITECTURE-SPEC.md#CHKARCH-CONFORMANCE", + "docs/specs/CHECKER-ARCHITECTURE-SPEC.md#CHKARCH-VERSION-TARGET", + "https://typing.python.org/en/latest/spec/annotations.html", + "https://typing.python.org/en/latest/spec/concepts.html", + "https://typing.python.org/en/latest/spec/aliases.html" + ], + "releaseImplementation": [], + "workingTreeImplementation": [ + "crates/basilisk-checker/src/types.rs", + "crates/basilisk-checker/src/types_parsing.rs", + "crates/basilisk-checker/src/inference.rs" + ], + "executableEvidence": [ + "Python 3.14.6 ran the Chapter 4 Signal Box checkpoint: 3 tests passed on 2026-07-17", + "target/release/basilisk check --color never src reported no issue for the Chapter 4 checkpoint on 2026-07-17", + "The placeholder release binary rejected the Chapter 4 string-to-float assignment, mixed list element, and list-to-union incompatibilities on 2026-07-17" + ], + "omitted": [ + "The planned object-versus-Any lesson is omitted because the working-tree assignment path currently collapses object into Any; the maintained typing specification distinguishes them." + ], + "blocker": "The current build identifies itself as 0.0.0-PLACEHOLDER. Pin a released Basilisk version, rerun the Chapter 4 static evidence, and capture any release-specific output before setting decision to publish." + }, {"section": "05", "decision": "withhold-until-verified", "governingSpecs": [], "releaseImplementation": [], "executableEvidence": []}, {"section": "06", "decision": "withhold-until-verified", "governingSpecs": [], "releaseImplementation": [], "executableEvidence": []}, {"section": "07", "decision": "withhold-until-verified", "governingSpecs": [], "releaseImplementation": [], "executableEvidence": []}, diff --git a/book/examples/README.md b/book/examples/README.md index f10c855b..6cb8439a 100644 --- a/book/examples/README.md +++ b/book/examples/README.md @@ -1,13 +1,16 @@ # Signal Box example project Signal Box will be the book-owned, executable through-line. It will remain -dependency-light and target Python 3.12. +dependency-light and avoid a project-wide Python-version assumption. A +checkpoint may use a versioned feature only when the lesson names its governing +PEP or language boundary. -The first executable checkpoint now lives in [`signal-box/`](signal-box/). -Chapter 9 uses its explicit annotation policy and deliberately incomplete -functions to capture the real configuration editor and its path preview. Run -the capture reproducibly with `make -C book screenshots` from the repository -root. +Chapter 4's executable checkpoint lives in +[`ch04-type-vocabulary/`](ch04-type-vocabulary/). Chapter 9 uses +[`signal-box/`](signal-box/) with its explicit annotation policy and +deliberately incomplete functions to capture the real configuration editor and +its path preview. Run that capture reproducibly with `make -C book screenshots` +from the repository root. Planned checkpoints: diff --git a/book/examples/ch04-type-vocabulary/README.md b/book/examples/ch04-type-vocabulary/README.md new file mode 100644 index 00000000..379911a8 --- /dev/null +++ b/book/examples/ch04-type-vocabulary/README.md @@ -0,0 +1,22 @@ +# Chapter 4 checkpoint + +This Signal Box snapshot supports Chapter 4's examples for annotations, +parameterized collections, unions, absence, and type aliases. + +The source uses two syntax features with explicit language boundaries: + +- built-in collection parameters such as `dict[str, float]` follow PEP 585 + and require Python 3.9 or later at runtime; +- union expressions such as `str | float | None` follow PEP 604 and require + Python 3.10 or later at runtime. + +Those are syntax boundaries for this checkpoint, not Basilisk-wide support +targets. A project using an earlier interpreter can express the same lessons +with `typing.Dict`, `typing.List`, `typing.Union`, and `typing.Optional`. + +From this directory, run: + +```console +PYTHONPATH=src python3 -m unittest discover -s tests +basilisk check src +``` diff --git a/book/examples/ch04-type-vocabulary/pyproject.toml b/book/examples/ch04-type-vocabulary/pyproject.toml new file mode 100644 index 00000000..29c64252 --- /dev/null +++ b/book/examples/ch04-type-vocabulary/pyproject.toml @@ -0,0 +1,6 @@ +[project] +name = "signal-box-ch04" +version = "0.1.0" + +[tool.basilisk] +include = ["src"] diff --git a/book/examples/ch04-type-vocabulary/src/signal_box/__init__.py b/book/examples/ch04-type-vocabulary/src/signal_box/__init__.py new file mode 100644 index 00000000..5ebe6f23 --- /dev/null +++ b/book/examples/ch04-type-vocabulary/src/signal_box/__init__.py @@ -0,0 +1,5 @@ +"""Signal Box types introduced in Chapter 4.""" + +from .readings import normalize_reading, normalize_temperature + +__all__ = ["normalize_reading", "normalize_temperature"] diff --git a/book/examples/ch04-type-vocabulary/src/signal_box/readings.py b/book/examples/ch04-type-vocabulary/src/signal_box/readings.py new file mode 100644 index 00000000..ca533dd2 --- /dev/null +++ b/book/examples/ch04-type-vocabulary/src/signal_box/readings.py @@ -0,0 +1,20 @@ +"""Everyday type vocabulary for the Chapter 4 checkpoint.""" + +ReadingValue = str | float | None +RawReading = dict[str, ReadingValue] +NormalizedReading = dict[str, ReadingValue] + + +def normalize_temperature(value: ReadingValue) -> float | None: + """Convert one present temperature to a float.""" + if value is None: + return None + return float(value) + + +def normalize_reading(raw: RawReading) -> NormalizedReading: + """Normalize the fields used by the current Signal Box checkpoint.""" + raw_sensor_id = raw.get("sensor_id") + sensor_id = "unknown" if raw_sensor_id is None else str(raw_sensor_id) + celsius = normalize_temperature(raw.get("celsius")) + return {"sensor_id": sensor_id, "celsius": celsius} diff --git a/book/examples/ch04-type-vocabulary/tests/test_readings.py b/book/examples/ch04-type-vocabulary/tests/test_readings.py new file mode 100644 index 00000000..0d1d50a4 --- /dev/null +++ b/book/examples/ch04-type-vocabulary/tests/test_readings.py @@ -0,0 +1,25 @@ +"""Runtime evidence for the Chapter 4 Signal Box checkpoint.""" + +import unittest + +from signal_box.readings import normalize_reading, normalize_temperature + + +class ReadingTests(unittest.TestCase): + """Keep normalization behavior separate from annotation claims.""" + + def test_normalize_temperature_preserves_absence(self) -> None: + self.assertIsNone(normalize_temperature(None)) + + def test_normalize_temperature_converts_text(self) -> None: + self.assertEqual(normalize_temperature("21.5"), 21.5) + + def test_normalize_reading_converts_present_fields(self) -> None: + self.assertEqual( + normalize_reading({"sensor_id": "north-7", "celsius": "21.5"}), + {"sensor_id": "north-7", "celsius": 21.5}, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/book/examples/signal-box/pyproject.toml b/book/examples/signal-box/pyproject.toml index 431d9d5d..c8a57685 100644 --- a/book/examples/signal-box/pyproject.toml +++ b/book/examples/signal-box/pyproject.toml @@ -1,10 +1,8 @@ [project] name = "signal-box" version = "0.1.0" -requires-python = ">=3.12" [tool.basilisk] -python-version = "3.12" include = ["src", "tests"] [tool.basilisk.rules] diff --git a/book/examples/signal-box/src/signal_box/readings.py b/book/examples/signal-box/src/signal_box/readings.py index c4396979..1d7691b8 100644 --- a/book/examples/signal-box/src/signal_box/readings.py +++ b/book/examples/signal-box/src/signal_box/readings.py @@ -1,7 +1,7 @@ """Small deliberately incomplete boundary used by Chapter 9.""" -def normalize_reading(raw): +def normalize_reading(raw) -> None: """Normalize a raw reading after its boundary policy is chosen.""" return { "sensor_id": str(raw["sensor_id"]), diff --git a/book/figures.json b/book/figures.json index 50ceb51c..be94c17c 100644 --- a/book/figures.json +++ b/book/figures.json @@ -101,7 +101,8 @@ "id": "fig-04-runtime-static", "section": "04", "kind": "diagram", - "status": "planned", + "status": "ready", + "master": "assets/diagrams/04-runtime-static-split.svg", "path": "assets/diagrams/04-runtime-static-split.png", "title": "Annotations inform tools without converting values", "alt": "Two aligned lanes separate a Python value's runtime behavior from the static information an annotation gives Basilisk." @@ -110,19 +111,21 @@ "id": "fig-04-unions", "section": "04", "kind": "diagram", - "status": "planned", + "status": "ready", + "master": "assets/diagrams/04-union-values.svg", "path": "assets/diagrams/04-union-values.png", "title": "A union describes several permitted possibilities", "alt": "Example string, float, and None values enter one union-typed boundary while unrelated values stop outside it." }, { - "id": "fig-04-any", + "id": "fig-04-alias", "section": "04", "kind": "diagram", - "status": "planned", - "path": "assets/diagrams/04-any-information-loss.png", - "title": "Any removes information in both directions", - "alt": "A precise type trace becomes an unlabelled channel at Any, illustrating why later operations carry less useful static evidence." + "status": "ready", + "master": "assets/diagrams/04-alias-domain-name.svg", + "path": "assets/diagrams/04-alias-domain-name.png", + "title": "An alias names a domain idea without creating a distinct type", + "alt": "RawReading and NormalizedReading label two application boundaries while both expand to the same dictionary type and remain equivalent." }, { "id": "fig-05-compatibility-gate", diff --git a/book/manuscript/00-how-to-use-this-book.md b/book/manuscript/00-how-to-use-this-book.md index 9257d935..c720b542 100644 --- a/book/manuscript/00-how-to-use-this-book.md +++ b/book/manuscript/00-how-to-use-this-book.md @@ -79,7 +79,7 @@ calculation is correct, or that untyped external data is trustworthy. Python's own documentation states the boundary plainly: > “The Python runtime does not enforce function and variable type -> annotations.” — [Python 3.12 `typing` documentation](https://docs.python.org/3.12/library/typing.html) +> annotations.” — [Python `typing` documentation](https://docs.python.org/3/library/typing.html) An annotation gives a checker information; it does not normally convert or validate the value at runtime. That is why this book keeps separate lanes for @@ -138,12 +138,19 @@ essential instructions from a picture. ## Check the edition before you begin -This working edition targets Python 3.12. Its exact Basilisk release is still -marked **TO BE PINNED BEFORE PUBLICATION** in the edition metadata. Until that -field names a released build, product commands, diagnostics, and screenshots -remain editorial evidence rather than publication claims. The free public -edition will record its Basilisk version, build date, Python target, example -test result, link-audit result, and screenshot environment. +This book does not declare one Python release to be Basilisk's canonical +target. The maintained typing specification is the authority. When a lesson +uses syntax or runtime behaviour that begins with a particular Python release, +the lesson names that boundary and cites the governing PEP or Python +documentation; otherwise the example follows the reader's project environment. + +The exact Basilisk release for this working edition is still marked **TO BE +PINNED BEFORE PUBLICATION** in the edition metadata. Until that field names a +released build, product commands, diagnostics, and screenshots remain +editorial evidence rather than publication claims. The free public edition +will record its Basilisk version, build date, the interpreter used to run the +examples, example test result, link-audit result, and screenshot environment. +The interpreter is a reproducibility fact, not a support boundary. That record is your starting point when the live product moves on. Follow the book with its named release for reproducible results; follow the website when @@ -152,4 +159,3 @@ version change to investigate, not as a reason to silently rewrite the example. You are now ready to begin with the smallest useful distinction: running a Python program and asking a static question about it are not the same act. - diff --git a/book/manuscript/01-meet-basilisk.md b/book/manuscript/01-meet-basilisk.md index 603abb61..165e9c15 100644 --- a/book/manuscript/01-meet-basilisk.md +++ b/book/manuscript/01-meet-basilisk.md @@ -5,44 +5,281 @@ > **Reader promise:** Explain where Basilisk sits between Python source, project > policy, an editor, the command line, and a running program. +Suppose a function says it returns nothing, yet its body returns a formatted +temperature. Python can run the function. A static type checker can reject the +relationship. A test can compare the resulting text with an expected value. +Those outcomes do not contradict one another: each tool asked a different +question. + +That distinction is the foundation for everything that follows. Before fixing +a diagnostic, you need to know what kind of evidence produced it, what facts +the tool could see, and what the result does not establish. + ![Python source and project configuration flow through Basilisk's parser, resolver, and checker to CLI diagnostics and editor language features.](../assets/diagrams/01-basilisk-system-map.png) +*Figure 1.1 — Basilisk analyses source and project context, then presents the +result through command-line and editor surfaces. Running Python, testing, +debugging, and profiling remain separate sources of runtime evidence.* + ## One program, several questions -Open with the difference between asking Python to execute a program and asking -a static analyzer whether one value is compatible with a declared boundary. -Make the limit explicit: static analysis is evidence, not a proof of runtime -correctness. +Begin with a small mistake: + +```python +def format_reading(celsius: float) -> None: + return f"{celsius:.1f} °C" + + +print(format_reading(21.5)) +``` + +The annotation promises `None`, but the function returns a `str`. That is a +static compatibility problem: the returned value does not satisfy the declared +return type. The maintained [Python typing specification](https://typing.python.org/en/latest/spec/type-system.html) +defines static analysis as the primary purpose of the type system. + +Run the file and Python still prints the string: + +```console +$ python reading.py +21.5 °C +``` + +This is expected. Python's [`typing` documentation](https://docs.python.org/3/library/typing.html) +states that the runtime does not enforce function and variable annotations. +The annotation did not convert the string to `None`, block the call, or insert +a hidden validation step. + +A checker instead reads the declaration and the return expression without +needing to execute this call. The smallest honest correction is to make the +declared return agree with the value: + +```python +def format_reading(celsius: float) -> str: + return f"{celsius:.1f} °C" +``` + +Changing `None` to `str` changes the information available to static tools. It +does not change the function's runtime calculation. If the intended contract +really was `None`, the other honest correction would be to stop returning a +value. The checker can expose the disagreement; it cannot choose the intended +design for you. + +When a diagnostic appears, translate it into three facts before editing: the +value the checker found, the type the boundary requires, and the source +expression that connects them. Here the found value is a `str`, the required +type is `None`, and the connecting expression is the `return`. That description +survives changes in message wording, colour, editor theme, and terminal layout. +It also gives you a useful review question: should the implementation change, +or should the declared boundary change? Guessing from the underline alone can +produce a clean check with the wrong contract. + +Nor does a clean static result prove that the temperature is correct, that a +sensor is reachable, or that malformed input has been handled. Execution shows +what happened on one run. A test compares an observed result with an expected +one. Debugging exposes state along one path. Profiling measures where a running +program spent resources. Static analysis adds another line of evidence; it does +not replace the others. ## One engine, several surfaces -Walk the reader through Python source and active configuration, parsing and -resolution, rule selection, diagnostics and actions, the CLI/CI path, and the -language-server/editor path. Keep implementation detail subordinate to what the -reader can observe. +Basilisk receives Python source and project context, then performs three broad +steps. Parsing identifies the program's syntax. Resolution connects names, +imports, declarations, and inferred information. Checking compares those facts +with the active rules. The diagram keeps these steps deliberately broad because +you do not need Basilisk's Rust internals to use the result. + +The command line runs that analysis for a bounded set of files and prints a +result suitable for a person or an automated job. The editor keeps a language +server running so it can reuse knowledge as documents change. The +[Language Server Protocol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/) +standardises messages for features such as diagnostics, hover, completion, and +navigation; the editor renders the response in its own interface. + +Two command-line questions are worth naming now: + +```console +basilisk check . +basilisk analyze . +``` + +`check` asks for the rules that implement the maintained Python typing +specification. `analyze` asks for non-PEP Basilisk rules that the project has +explicitly selected. They use the same source-analysis path but select different +rule scopes. The editor normally presents the configured union as one coherent +diagnostic stream, because you should not have to remember which transport +produced a problem before you can understand it. + +The presentation can differ without changing the underlying relationship. A +terminal can show a path, source span, explanation, and exit status. An editor +can underline the same span and add hover text or an action. Later chapters will +teach those surfaces in detail. For now, remember that neither surface executes +the function merely to decide whether its return satisfies the annotation. + +This shared analysis matters when you move between tools. If the terminal and +editor appear to disagree, do not invent two different meanings for Python +typing. First compare the source revision, project root, configuration, and +Basilisk build each surface is using. A stale editor buffer or a command run +from a different root can change the available evidence even though the type +relationship itself has not changed. Chapter 2 turns those checks into a short +verification routine. + +Runtime tools occupy the other lane: + +```console +python app.py +python -m unittest +``` + +Those commands run Python code. A debugger and profiler also need an observed +execution. Basilisk may connect these activities into one developer workflow, +but connecting the controls does not merge the evidence. A squiggle is not a +test failure, and a passing test does not erase an incompatible declared return. ## Python rules and project policy -Establish the book's central configuration model: the unconfigured baseline -selects Python typing-spec rules. Additional Basilisk policy rules—such as -requiring annotations—are choices enabled individually. Do not call these two -sets “basic” and “strict” modes. +The Python typing specification owns the meaning of Python's type system. It is +a maintained specification; historical PEPs explain how particular features +were accepted, but the maintained text is the authority for current semantics. +Basilisk follows that standard rather than inventing an edition-wide Python +target for this book. + +Python versions still matter when the standard makes them matter. A PEP may +introduce syntax in a particular release, and a `sys.version_info` condition may +make one branch relevant to a project's interpreter. In those cases Basilisk +uses project or environment evidence to apply the stated boundary. That is not +the same as declaring one Python release to be Basilisk's canonical version. + +Project policy answers a separate class of question. The typing specification +can define whether a returned value is compatible with a return annotation +without requiring every function to have an annotation. A team may still want +to require annotations at selected boundaries. That additional choice belongs +in visible project configuration and is handled by opt-in Basilisk rules. + +Signal Box currently makes two such choices: + +```toml +[tool.basilisk] +include = ["src", "tests"] + +[tool.basilisk.rules] +"BSK-0001" = "error" +"BSK-0002" = "error" +``` + +The `include` entry identifies project paths. The two rule entries select +Basilisk policies for missing parameter and return annotations. Removing those +entries changes the opt-in policy; it does not rewrite the Python typing +specification. Conversely, project policy cannot make an incompatible return +become compatible. + +Severity is also policy, not semantics. Reporting a selected rule as an error, +warning, or informational item can change whether a command blocks a workflow +and how prominently an editor displays it. It does not change the meaning of +`str`, `None`, or return compatibility. Keeping that separation clear lets a +team adjust adoption pressure without publishing its own private version of +Python's type system. + +This is why the book avoids labels such as *basic mode* and *strict mode*. +Those labels hide the decision that matters. Ask instead: is this relationship +defined by the Python typing specification, or is this an additional project +rule? Then identify the exact rule and its configured severity. ## Signal Box checkpoint -Open the completed sample without changing it. Identify the project root, -Python files, active configuration, terminal entry point, editor connection, -and runtime entry point. +Open `book/examples/signal-box` without changing it. The current checkpoint is +small enough to map in one glance: + +```text +signal-box/ +├── pyproject.toml +├── src/signal_box/ +│ ├── __init__.py +│ └── readings.py +└── tests/ + └── test_readings.py +``` + +Work through the surfaces in this order: + +1. Treat the directory containing `pyproject.toml` as the project root. Read + the `include` paths and the two explicit policy rules. Notice that the file + does not claim a canonical Python release. +2. Open `src/signal_box/readings.py`. The function declares `-> None` but + returns a dictionary. Before running anything, predict whether this is a + static question, a runtime question, or both. +3. Execute the function directly from the project root: + + ```console + PYTHONPATH=src python - <<'PY' + from signal_box.readings import normalize_reading -## Practice + print(normalize_reading({"sensor_id": "north-7", "celsius": "21.5"})) + PY + ``` -Ask readers to classify five questions as static-analysis, execution, test, -debugging, or profiling questions before the answers are revealed. + Python prints the normalized dictionary. This establishes what that input + did at runtime; it does not make the `-> None` promise accurate. +4. Run `basilisk check .`. Find the return relationship governed by the typing + specification. Read the source span before considering a fix. +5. Run `basilisk analyze .`. The configured annotation policy asks a different + question about the unannotated `raw` parameter. +6. Open the same file in a supported editor with Basilisk connected. Locate the + corresponding source spans there. The editor client starts and communicates + with the language server; there is no separate editor copy of the Python + program. + +Do not fix the fixture yet. The checkpoint is complete when you can point to +the source, project policy, static-analysis commands, editor connection, and +runtime command, and explain what evidence each contributes. + +## Practice: choose the evidence first + +Classify each question before revealing the answers: + +1. Can a `str` be returned where `None` was declared? +2. What dictionary does this sensor payload produce on this run? +3. Does normalization preserve the expected sensor identifier across several + representative inputs? +4. Why did `raw["celsius"]` contain an unexpected value at this breakpoint? +5. Which normalization operation consumes the most CPU under a real workload? + +The answers are, in order: static analysis, execution, testing, debugging, and +profiling. Some investigations use more than one source, but naming the first +question prevents you from demanding evidence that a tool cannot provide. + +For a guided variation, copy `format_reading` into a scratch file and change +its return annotation back to `None`. Predict the static result and runtime +output separately, check both, then restore `str`. Explain why only one result +changed. + +For an independent variation, choose one uncertainty from your own project. +Write it as a single question, name the evidence needed to answer it, and only +then choose a command or editor action. If the question is about a typing +relationship, identify whether it comes from the maintained specification or +an explicit project rule. + +## What changed + +- Static analysis and Python execution answer different questions about the + same source. +- An annotation supplies information; it does not normally enforce or convert + a runtime value. +- Command-line and editor diagnostics are two surfaces over the same analysis + relationships. +- `check` selects Python typing-spec rules; `analyze` selects configured, + non-PEP Basilisk policy rules. +- Python-version boundaries belong to the PEP or runtime behaviour that defines + them, not to a universal Basilisk target. +- Tests, debugging, and profiling remain necessary forms of runtime evidence. + +Chapter 2 turns this map into action: you will verify the installed Basilisk +build, run a bounded check, read the first result, and reach a clean static +baseline without copying unexplained configuration. ## Authoritative sources - [The Python type system](https://typing.python.org/en/latest/spec/type-system.html) -- [Python 3.12 typing documentation](https://docs.python.org/3.12/library/typing.html) +- [Python `typing` documentation](https://docs.python.org/3/library/typing.html) - [Language Server Protocol 3.18](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/) - Continue at the [Basilisk website](https://www.basilisk-python.dev/). - diff --git a/book/manuscript/04-everyday-type-vocabulary.md b/book/manuscript/04-everyday-type-vocabulary.md index a2115fce..f8472393 100644 --- a/book/manuscript/04-everyday-type-vocabulary.md +++ b/book/manuscript/04-everyday-type-vocabulary.md @@ -5,45 +5,351 @@ > **Reader promise:** Read and write the types used at ordinary application > boundaries without confusing an annotation with runtime conversion. -## Values, names, and annotations +A sensor sends the text `"21.5"`. Signal Box needs the number `21.5`. Those +values look related to a person, but Python will not convert one merely because +a name is annotated as `float`. The program needs an explicit conversion; the +annotation gives a static checker a fact to verify. -Use one Signal Box reading to separate the runtime value, the name bound to it, -and the static information written or inferred for that name. Demonstrate that -adding an annotation does not coerce a string into a float. +This chapter builds a small working vocabulary from that distinction. You will +use scalar types, parameterized collections, unions with `None`, and aliases. +These features are defined by the maintained Python typing specification. Two +modern spellings used in the examples have precise PEP boundaries, which are +named where they appear rather than treated as Basilisk-wide version targets. -## Built-in values and collections +## An annotation describes; an expression computes -Cover the scalar and collection forms the running project actually needs. Show -why the element types of a collection carry more useful information than a bare -container name. +Start with a runtime value, the name bound to it, and an annotation: -## Unions and absence +```python +raw_celsius = "21.5" +celsius: float = float(raw_celsius) +``` -Introduce `T | None`, then a small closed union of reading variants. Keep the -focus on permitted possibilities; narrowing comes in Chapter 6. +At runtime, `raw_celsius` refers to a `str`. The call to `float` computes a new +`float` value. The annotation on `celsius` says that the result expected at that +assignment boundary is a `float`. Each part has one job. -## `object` is not `Any` +Remove the conversion and the jobs do not quietly swap places: -Compare the operations a checker can justify after a value becomes `object` -with the information discarded by `Any`. Avoid describing either as merely the -“top type.” +```python +celsius: float = "21.5" +print(type(celsius).__name__) # str +``` -## Aliases name a domain idea +Python binds the string and prints `str`. A checker has reason to reject the +assignment because the expression's type is not assignable to the annotated +type. The [typing specification's annotation rules](https://typing.python.org/en/latest/spec/annotations.html) +use this same idea for variables, parameters, and returns: an annotation states +the expected type, and a value used there must be assignable to it. Python's +[`typing` documentation](https://docs.python.org/3/library/typing.html) is also +explicit that annotations are not enforced by the runtime. -Create one Python 3.12 `type` alias because it makes a boundary easier to read, -not because every long annotation deserves a second name. +![A string becomes a float only through an explicit conversion, while the annotation supplies a separate static expectation.](../assets/diagrams/04-runtime-static-split.png) + +*Figure 4.1 — The `float(...)` call changes the runtime value. The `: float` +annotation gives static tools an expected type; it performs no conversion.* + +Function annotations describe two more boundaries: + +```python +def format_temperature(celsius: float) -> str: + return f"{celsius:.1f} °C" +``` + +The parameter annotation says what values callers may supply at the +`celsius` boundary. The return annotation says what the function promises to +produce. Neither annotation checks whether a sensor is calibrated or whether +the formatted text is desirable. Those remain runtime and application +questions. + +Expected type does not mean “the runtime class name must be spelled exactly +this way.” The typing relation also accounts for compatible subclasses and +other forms of assignability. Chapter 5 gives that relation a full treatment. +For this chapter, the useful reading is simpler: the annotation names the +contract at a boundary, while the value still comes from ordinary Python +evaluation. + +Names without written annotations can still carry inferred static information. +In the first example, a checker can infer `str` for `raw_celsius` from the +literal and `float` for the conversion result. Write annotations where they +make a useful boundary or contract visible; do not annotate every local name +merely to repeat an obvious expression. + +The everyday scalar names mean what ordinary Python suggests: `str` for text, +`int` for integers, `float` for floating-point numbers, `bool` for truth +values, and `bytes` for byte sequences. An annotation using one of those names +does not call its constructor. If conversion is part of the program, keep the +conversion visible in the expression. + +## Collections carry facts about their contents + +Knowing only that a value is a list is rarely enough. Signal Box also needs to +know what each element means: + +```python +sensor_ids: list[str] = ["north-7", "roof-2"] +latest: dict[str, float] = {"north-7": 21.5} +coordinates: tuple[float, float] = (-37.81, 144.96) +``` + +Read these from the outside inward. `list[str]` is a list whose elements are +strings. `dict[str, float]` maps string keys to floating-point values. The +two-item tuple records a float in each position. Supplying the element, key, +and value types lets a checker follow useful facts through indexing, iteration, +and calls. + +The brackets describe different roles for different collections. A list has +one repeating element type. A dictionary has a key type followed by a value +type. This tuple has two fixed positions, each declared separately. Reading the +roles aloud is a useful habit: “dictionary from sensor identifier strings to +temperatures,” not merely “dictionary of strings and floats.” That phrasing +often exposes a reversed key/value annotation before a tool needs to. + +The bracketed types still do not validate a collection at runtime. This code +creates an ordinary list, and Python does not inspect every append against the +annotation: + +```python +sensor_ids: list[str] = [] +sensor_ids.append("north-7") +``` + +A static checker can verify the append from source information. Runtime +validation, if a boundary needs it, is separate code. This is especially +important for external JSON: annotating a decoded dictionary does not prove +that an untrusted payload has the promised keys or value shapes. + +The built-in parameter spelling in this section—`list[str]`, `dict[str, +float]`, and `tuple[float, float]`—comes from +[PEP 585](https://peps.python.org/pep-0585/) and is available at runtime from +Python 3.9. That is a boundary on this syntax, not a Basilisk support target. +Code running on an earlier interpreter can use the equivalent names from +`typing`: + +```python +from typing import Dict, List, Tuple + +sensor_ids: List[str] = ["north-7"] +latest: Dict[str, float] = {"north-7": 21.5} +coordinates: Tuple[float, float] = (-37.81, 144.96) +``` + +The relationship being expressed is the same. The project interpreter decides +which spelling its source can execute. Basilisk should follow that declared +language boundary rather than impose one release on every project. + +## A union lists permitted possibilities + +Some Signal Box fields genuinely have more than one permitted form. A raw +temperature may arrive as text, may already be a float, or may be absent: + +```python +ReadingValue = str | float | None + +first: ReadingValue = "21.5" +second: ReadingValue = 21.5 +missing: ReadingValue = None +``` + +A union is not a sequence of conversion attempts. It says that a value from +any listed alternative is permitted at the boundary. A list is not one of +these alternatives, so `[]` does not satisfy `ReadingValue` merely because the +program could invent a way to convert it. + +Every listed alternative is a valid part of the contract. `None` is not a +comment saying that the data is probably broken, and `str` is not a temporary +exception that the checker may ignore. Code receiving the union must use only +operations justified for all remaining possibilities or gather evidence that +one possibility is present. This is why a union preserves more information +than abandoning the type of the value altogether. + +![String, float, and None values enter a union-typed boundary while a list remains outside the permitted alternatives.](../assets/diagrams/04-union-values.png) + +*Figure 4.2 — A union widens the set of permitted values only to the alternatives +it names. It does not erase the boundary.* + +`float | None` deserves to be read literally: either a float or the value +`None`. It is useful when absence is part of the contract: + +```python +def normalize_temperature(value: str | float | None) -> float | None: + if value is None: + return None + return float(value) +``` + +The function handles absence separately and explicitly converts either present +form. The condition also gives a checker evidence that `value` cannot be +`None` on the final line. Chapter 6 develops that control-flow narrowing in +detail; for now, notice that the declared possibilities and the runtime branches +agree. + +A default of `None` does not remove the need to declare `None` when it is an +accepted value. The maintained [union rules](https://typing.python.org/en/latest/spec/concepts.html) +recommend making that possibility explicit: + +```python +def choose_temperature( + measured: float | None, + fallback: float | None = None, +) -> float | None: + return measured if measured is not None else fallback +``` + +The `X | Y` and `T | None` spellings were introduced by +[PEP 604](https://peps.python.org/pep-0604/) for Python 3.10. On a project whose +interpreter predates that syntax, the equivalent declarations use `Union` and +`Optional`: + +```python +from typing import Optional, Union + +ReadingValue = Union[str, float, None] +MaybeTemperature = Optional[float] +``` + +`Optional[float]` means `Union[float, None]`; it does not mean that a function +parameter has a default value. Syntax changes across PEP boundaries, but the +underlying union relationship remains the lesson. + +## Aliases name domain ideas + +The earlier `ReadingValue` assignment is a type alias. It lets a program name +a recurring type relationship using the language of its domain. Signal Box can +name both sides of its current normalization boundary: + +```python +ReadingValue = str | float | None +RawReading = dict[str, ReadingValue] +NormalizedReading = dict[str, ReadingValue] + + +def normalize_reading(raw: RawReading) -> NormalizedReading: + ... +``` + +This signature is easier to discuss in a review: a raw reading goes in and a +normalized reading comes out. The aliases are still exactly equivalent to +their right-hand types. They create no new runtime classes, perform no +validation, and do not prevent a `RawReading` from being used where the +identical `NormalizedReading` shape is expected. + +That equivalence also means that an alias should not be advertised as a safety +barrier. If two aliases expand to the same type, a checker treats their values +according to that shared type relationship. The benefit is communicative: the +function signature records where data is in the application story. A reviewer +can then decide whether readable naming is sufficient or whether the boundary +needs a genuinely different structure. + +![RawReading and NormalizedReading give domain names to the same dictionary type without creating distinct runtime values.](../assets/diagrams/04-alias-domain-name.png) + +*Figure 4.3 — An alias improves the vocabulary of a contract. It does not make +an equivalent underlying type distinct.* + +That limitation is useful evidence, not a defect to hide. At this checkpoint, +the aliases improve readability while the broad dictionary type treats every +key as having the same union of possible values. Chapter 7 will introduce +structures that can describe required keys and different value types more +precisely. + +The maintained [alias specification](https://typing.python.org/en/latest/spec/aliases.html) +also defines an explicit `type` statement, introduced by +[PEP 695](https://peps.python.org/pep-0695/) for Python 3.12. This chapter does +not need that newer syntax, so it does not use it. A simple alias assignment +teaches the concept without adding an irrelevant language floor. + +Create an alias when the name clarifies a domain boundary or removes distracting +repetition. Do not create one for every short type. `SensorId = str` can be +useful vocabulary, but it does not stop an arbitrary string from being used as +a sensor identifier. When distinctness is the requirement, an alias is the +wrong tool. ## Signal Box checkpoint -Annotate raw and normalized readings, including absent values and a small union, -then confirm which facts are explicit and which Basilisk still infers. +The executable snapshot for this chapter is +`book/examples/ch04-type-vocabulary`: + +```text +ch04-type-vocabulary/ +├── pyproject.toml +├── src/signal_box/ +│ ├── __init__.py +│ └── readings.py +└── tests/ + └── test_readings.py +``` + +Open `src/signal_box/readings.py` and work from the outside inward: + +1. Read `ReadingValue` as the permitted raw field values: text, float, or + absence. +2. Expand `RawReading` and `NormalizedReading` mentally to their dictionary + types. Notice what each alias explains and what it cannot distinguish. +3. In `normalize_temperature`, identify the annotation, the branch that handles + absence, and the expression that actually converts a present value. +4. In `normalize_reading`, identify which local types are written and which are + inferred from `.get(...)`, the conditional expression, and the helper call. +5. Predict the normalized dictionary for the sample input before executing it. + +Write the prediction as two lines: one for runtime values and one for static +facts. The runtime line should say that the text temperature is converted to a +float. The static line should say that the returned dictionary satisfies the +`NormalizedReading` alias. Keeping the two lines separate is the point of the +exercise; neither one can substitute for the other. + +Run the runtime tests and the static check as separate evidence: + +```console +cd book/examples/ch04-type-vocabulary +PYTHONPATH=src python3 -m unittest discover -s tests +basilisk check src +``` + +The tests establish what normalization did for their inputs. The Basilisk check +establishes that the source relationships satisfy the maintained typing rules +implemented by the checked build. Neither result proves that an arbitrary +external dictionary has been validated; the checkpoint has not made that claim. + +For a partially guided variation, add a `humidity` field whose raw value may be +`str`, `float`, or `None`. Reuse `normalize_temperature` under a clearer helper +name if the conversion contract is genuinely identical, add one runtime test, +and predict whether either alias must change before checking. Then introduce a +deliberate mismatch in a scratch copy—put a list where a `ReadingValue` is +expected—and compare Python's runtime behavior with the static result. Remove +the mismatch rather than weakening the alias. + +For an independent variation, choose a boundary in a small module of your own. +Write down the runtime input values first. Then annotate one scalar, one +collection including its contents, and one genuinely absent-or-present value. +If you introduce an alias, explain in one sentence what domain idea it names +and in another what it does *not* enforce. Run the program and the checker +separately. Finally, identify every syntax feature in your annotations that has +a PEP-defined interpreter boundary. Record only those boundaries; do not infer +a project-wide Basilisk target from them. + +## What changed + +- An annotation states expected static information; an expression performs a + runtime conversion. +- Parameters and returns expose function boundaries without proving application + behavior. +- Parameterized collection types preserve facts about elements, keys, values, + and tuple positions. +- A union permits only its named alternatives, and `T | None` makes absence + explicit. +- PEP 585 and PEP 604 syntax boundaries belong to those features, not to a + universal Basilisk target. +- A type alias improves domain vocabulary while remaining equivalent to its + underlying type. + +Chapter 5 will reuse this vocabulary around one recurring question: can this +value be used at that boundary? ## Authoritative sources -- [Type system concepts](https://typing.python.org/en/latest/spec/concepts.html) - [Type annotations](https://typing.python.org/en/latest/spec/annotations.html) +- [Type system concepts and unions](https://typing.python.org/en/latest/spec/concepts.html) - [Type aliases](https://typing.python.org/en/latest/spec/aliases.html) -- [CPython 3.12 typing implementation](https://github.com/python/cpython/blob/3.12/Lib/typing.py) -- Return to the [Basilisk rule reference](https://www.basilisk-python.dev/docs/rules/) - for the diagnostics used in the examples. - +- [PEP 585 — Type Hinting Generics In Standard Collections](https://peps.python.org/pep-0585/) +- [PEP 604 — Allow writing union types as `X | Y`](https://peps.python.org/pep-0604/) +- [PEP 695 — Type Parameter Syntax](https://peps.python.org/pep-0695/) diff --git a/book/manuscript/06-inference-narrowing-paths.md b/book/manuscript/06-inference-narrowing-paths.md index 88ff7b2e..e883ddad 100644 --- a/book/manuscript/06-inference-narrowing-paths.md +++ b/book/manuscript/06-inference-narrowing-paths.md @@ -37,7 +37,6 @@ variant and predict every place that becomes non-exhaustive. - [Type narrowing](https://typing.python.org/en/latest/spec/narrowing.html) - [Unreachable code and exhaustiveness](https://typing.python.org/en/latest/guides/unreachable.html) - [PEP 647 — User-defined type guards](https://peps.python.org/pep-0647/) -- [Python 3.12 match statement](https://docs.python.org/3.12/reference/compound_stmts.html#the-match-statement) +- [Python `match` statement](https://docs.python.org/3/reference/compound_stmts.html#the-match-statement) - Use the [Basilisk rule reference](https://www.basilisk-python.dev/docs/rules/) for the captured narrowing and match diagnostics. - diff --git a/book/manuscript/07-reusable-contracts-structured-data.md b/book/manuscript/07-reusable-contracts-structured-data.md index 80d2f1c9..9e443527 100644 --- a/book/manuscript/07-reusable-contracts-structured-data.md +++ b/book/manuscript/07-reusable-contracts-structured-data.md @@ -29,9 +29,10 @@ class solely for the type checker. ## Preserve a payload with a type parameter -Create one generic page or result container using Python 3.12 type-parameter -syntax. Demonstrate substitution with concrete payloads before using the word -“generic.” +Create one generic page or result container using syntax supported by the +reader's project. If the example uses PEP 695 type-parameter syntax, label its +Python-version boundary. Demonstrate substitution with concrete payloads before +using the word “generic.” ## Overload only when calls truly differ @@ -53,4 +54,3 @@ generic report page. Run the same tests through two storage implementations. - [Overloads](https://typing.python.org/en/latest/spec/overload.html) - Browse related diagnostics in the [Basilisk rule reference](https://www.basilisk-python.dev/docs/rules/). - diff --git a/book/manuscript/09-configure-the-project.md b/book/manuscript/09-configure-the-project.md index 09d028a8..9ae1c932 100644 --- a/book/manuscript/09-configure-the-project.md +++ b/book/manuscript/09-configure-the-project.md @@ -23,7 +23,7 @@ Python's authorities draw a useful boundary around this discussion. The versioned Python documentation says: > “The Python runtime does not enforce function and variable type -> annotations.” — [Python 3.12 `typing` documentation](https://docs.python.org/3.12/library/typing.html) +> annotations.” — [Python `typing` documentation](https://docs.python.org/3/library/typing.html) Changing a Basilisk severity therefore changes static feedback. It does not insert a runtime check, convert a value, or change what Python executes. Your @@ -285,7 +285,7 @@ current catalog. Check both against the Basilisk release used by your project. - [The Python type system](https://typing.python.org/en/latest/spec/type-system.html) - [Python typing specification: annotations](https://typing.python.org/en/latest/spec/annotations.html) - [Python typing specification: type-checker directives](https://typing.python.org/en/latest/spec/directives.html) -- [Python 3.12 `typing` documentation](https://docs.python.org/3.12/library/typing.html) +- [Python `typing` documentation](https://docs.python.org/3/library/typing.html) - [`pyproject.toml` specification](https://packaging.python.org/en/latest/specifications/pyproject-toml/) - [Basilisk configuration guide](https://www.basilisk-python.dev/docs/configuration/) - [Basilisk rule reference](https://www.basilisk-python.dev/docs/rules/) diff --git a/book/manuscript/90-appendices-and-next-steps.md b/book/manuscript/90-appendices-and-next-steps.md index 09c47454..b8913343 100644 --- a/book/manuscript/90-appendices-and-next-steps.md +++ b/book/manuscript/90-appendices-and-next-steps.md @@ -19,15 +19,15 @@ rather than inventing a parallel normative glossary. ## Appendix C — Source map -Map each book topic to the maintained typing specification, Python 3.12 +Map each book topic to the maintained typing specification, Python documentation, CPython source, typeshed, packaging specification, and live Basilisk destination. Explain when a PEP is historical context rather than the current specification. ## Appendix D — Reproducibility record -Record the book revision, build date, Python target, Basilisk release and source -commit, upstream conformance snapshot, example results, screenshot environment, +Record the book revision, build date, Python interpreter used for the examples, +Basilisk release and source commit, upstream conformance snapshot, example results, screenshot environment, image manifest result, link-audit result, Pandoc version, and EPUBCheck version. ## Continue after the last page @@ -36,4 +36,3 @@ Send readers to the [Basilisk website](https://www.basilisk-python.dev/), the [live rule reference](https://www.basilisk-python.dev/docs/rules/), and [release notes](https://www.basilisk-python.dev/docs/releases/). Confirm the final corrections and feedback URL before publication. - diff --git a/book/metadata.yaml b/book/metadata.yaml index 0af47ecc..01a570e2 100644 --- a/book/metadata.yaml +++ b/book/metadata.yaml @@ -12,8 +12,6 @@ description: >- interpreting diagnostics, configuring and adopting projects, and connecting editor, refactoring, debugging, testing, profiling, and CI workflows. edition: "Structural prototype 0.1" -python-version: "3.12" basilisk-version: "TO BE PINNED BEFORE PUBLICATION" website: "https://www.basilisk-python.dev/" ... - diff --git a/book/scripts/test_book_contract.py b/book/scripts/test_book_contract.py new file mode 100644 index 00000000..92ef22b3 --- /dev/null +++ b/book/scripts/test_book_contract.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Regression tests for the book's durable editorial contract.""" + +from __future__ import annotations + +import json +import unittest +from pathlib import Path + + +BOOK_ROOT = Path(__file__).resolve().parents[1] +REPOSITORY_ROOT = BOOK_ROOT.parent + + +class BookContractTests(unittest.TestCase): + """Keep product policy out of edition-specific assumptions.""" + + def test_python_typing_contract_is_pep_first_not_version_pinned(self) -> None: + """The book must not declare one Python release as canonical.""" + book = json.loads((BOOK_ROOT / "book.json").read_text(encoding="utf-8")) + metadata = (BOOK_ROOT / "metadata.yaml").read_text(encoding="utf-8") + editorial_brief = (BOOK_ROOT / "EDITORIAL-BRIEF.md").read_text(encoding="utf-8") + outline = (BOOK_ROOT / "OUTLINE.md").read_text(encoding="utf-8") + repository_instructions = (REPOSITORY_ROOT / "CLAUDE.md").read_text( + encoding="utf-8" + ) + + self.assertNotIn("canonicalPythonVersion", book) + self.assertNotIn('python-version: "3.12"', metadata) + self.assertNotIn("Canonical language target: Python 3.12", editorial_brief) + self.assertNotIn( + "Python 3.12 as the canonical language target for this edition", + outline, + ) + self.assertNotIn("canonical Python **3.12**", repository_instructions) + + +if __name__ == "__main__": + unittest.main() diff --git a/book/sources.json b/book/sources.json index 9c9e27a3..3ce49192 100644 --- a/book/sources.json +++ b/book/sources.json @@ -172,84 +172,68 @@ }, { "key": "python-typing-docs", - "title": "Python 3.12 typing documentation", - "url": "https://docs.python.org/3.12/library/typing.html", + "title": "Python typing documentation", + "url": "https://docs.python.org/3/library/typing.html", "authority": "Python Software Foundation", - "versionScope": "Python 3.12", + "versionScope": "current Python 3 documentation; use a versioned page for version-dependent claims", "topics": ["typing runtime API", "annotation runtime limits"] }, { "key": "python-annotations", - "title": "Python 3.12 annotation best practices", - "url": "https://docs.python.org/3.12/howto/annotations.html", + "title": "Python annotation best practices", + "url": "https://docs.python.org/3/howto/annotations.html", "authority": "Python Software Foundation", - "versionScope": "Python 3.12", + "versionScope": "current Python 3 documentation; use a versioned page for version-dependent claims", "topics": ["annotation evaluation", "annotation access"] }, { "key": "python-match-statement", - "title": "Python 3.12 match statement", - "url": "https://docs.python.org/3.12/reference/compound_stmts.html#the-match-statement", + "title": "Python match statement", + "url": "https://docs.python.org/3/reference/compound_stmts.html#the-match-statement", "authority": "Python Software Foundation", - "versionScope": "Python 3.12", + "versionScope": "current Python 3 documentation; match is version-dependent syntax", "topics": ["pattern matching", "match statement"] }, { "key": "python-dataclasses", - "title": "Python 3.12 dataclasses", - "url": "https://docs.python.org/3.12/library/dataclasses.html", + "title": "Python dataclasses", + "url": "https://docs.python.org/3/library/dataclasses.html", "authority": "Python Software Foundation", - "versionScope": "Python 3.12", + "versionScope": "current Python 3 documentation; use a versioned page for version-dependent claims", "topics": ["dataclass runtime behavior"] }, { "key": "python-enum", - "title": "Python 3.12 enum", - "url": "https://docs.python.org/3.12/library/enum.html", + "title": "Python enum", + "url": "https://docs.python.org/3/library/enum.html", "authority": "Python Software Foundation", - "versionScope": "Python 3.12", + "versionScope": "current Python 3 documentation; use a versioned page for version-dependent claims", "topics": ["enum runtime behavior"] }, { "key": "python-pdb", - "title": "Python 3.12 debugger", - "url": "https://docs.python.org/3.12/library/pdb.html", + "title": "Python debugger", + "url": "https://docs.python.org/3/library/pdb.html", "authority": "Python Software Foundation", - "versionScope": "Python 3.12", + "versionScope": "current Python 3 documentation; use a versioned page for version-dependent claims", "topics": ["debugging concepts"] }, { "key": "python-tracemalloc", - "title": "Python 3.12 tracemalloc", - "url": "https://docs.python.org/3.12/library/tracemalloc.html", + "title": "Python tracemalloc", + "url": "https://docs.python.org/3/library/tracemalloc.html", "authority": "Python Software Foundation", - "versionScope": "Python 3.12", + "versionScope": "current Python 3 documentation; use a versioned page for version-dependent claims", "topics": ["memory allocation tracing"] }, { "key": "python-gc", - "title": "Python 3.12 garbage collector", - "url": "https://docs.python.org/3.12/library/gc.html", + "title": "Python garbage collector", + "url": "https://docs.python.org/3/library/gc.html", "authority": "Python Software Foundation", - "versionScope": "Python 3.12", + "versionScope": "current Python 3 documentation; use a versioned page for version-dependent claims", "topics": ["garbage collection", "gc.garbage"] }, - { - "key": "cpython-typing-source", - "title": "CPython 3.12 typing.py", - "url": "https://github.com/python/cpython/blob/3.12/Lib/typing.py", - "authority": "Python Software Foundation", - "versionScope": "CPython 3.12 branch", - "topics": ["typing runtime implementation"] - }, - { - "key": "cpython-grammar", - "title": "CPython 3.12 grammar", - "url": "https://github.com/python/cpython/blob/3.12/Grammar/python.gram", - "authority": "Python Software Foundation", - "versionScope": "CPython 3.12 branch", - "topics": ["Python syntax"] - }, { "key": "typeshed", "title": "Typeshed", @@ -266,6 +250,14 @@ "versionScope": "historical rationale", "topics": ["py.typed history", "stub distribution history"] }, + { + "key": "pep-585", + "title": "PEP 585 — Type Hinting Generics In Standard Collections", + "url": "https://peps.python.org/pep-0585/", + "authority": "Python Software Foundation", + "versionScope": "accepted in Python 3.9", + "topics": ["built-in generic syntax history"] + }, { "key": "pep-604", "title": "PEP 604 — Union types as X | Y", @@ -428,4 +420,3 @@ } ] } - diff --git a/crates/basilisk-checker/examples/ift_measure.rs b/crates/basilisk-checker/examples/ift_measure.rs new file mode 100644 index 00000000..048e4df6 --- /dev/null +++ b/crates/basilisk-checker/examples/ift_measure.rs @@ -0,0 +1,108 @@ +//! Implements the [NARROWPLAN-CHECKLIST] Stage 2 measurement item: narrowing +//! richness against the utahplt/ifT-benchmark +//! (), whose Python suite +//! (`Pyright/main.py`) is built on `type(x) is C` guards over `@final` +//! classes ([TYPEINF-NARROWING-TYPEOF]). +//! +//! Usage (clone fresh, then measure): +//! ```sh +//! git clone --depth 1 https://github.com/utahplt/ift-benchmark /tmp/ift +//! cargo run --release -p basilisk-checker --example ift_measure -- /tmp/ift/Pyright/main.py +//! ``` +//! +//! Metric (self-measured, stated methodology): for every function in the +//! suite, run parse → resolve → flow analysis and report whether at least +//! one flow-narrowed use is produced. This measures narrowing *richness* +//! (which guard forms produce narrowing at all) — it is NOT the benchmark's +//! own accept/reject score, which needs the full diagnostics pipeline and +//! lands with the Integration stage. + +use std::collections::HashMap; + +use basilisk_checker::narrow::{analyse_function_in, NarrowContext, NarrowEnv}; +use basilisk_checker::types::InferredType; +use ruff_python_ast::Stmt; + +/// Declared parameter types for one resolved function. +fn declared_types( + source: &str, + function: &basilisk_resolver::FunctionInfo, +) -> HashMap { + function + .parameters + .iter() + .filter_map(|param| { + let span = param.annotation_span?; + let start = usize::try_from(span.start).ok()?; + let end = usize::try_from(span.end).ok()?; + let text = source.get(start..end)?; + Some((param.name.clone(), InferredType::from_annotation(text))) + }) + .collect() +} + +fn main() -> Result<(), Box> { + let path = std::env::args() + .nth(1) + .ok_or("usage: ift_measure ")?; + let source = std::fs::read_to_string(&path)?; + + let parsed = basilisk_parser::parse_source(source.clone(), path.clone()) + .map_err(|error| format!("benchmark file must parse: {error}"))?; + let resolved = basilisk_resolver::resolve(&parsed) + .map_err(|error| format!("benchmark file must resolve: {error}"))?; + + // The suite's `@final` classes make negative `type(x) is not C` sound. + let mut ctx = NarrowContext::default(); + for class in &resolved.classes { + let _ = ctx.final_classes.insert(class.name.to_ascii_lowercase()); + } + + let reparsed = ruff_python_parser::parse_module(&source) + .map_err(|error| format!("benchmark file must reparse: {error}"))?; + let bodies: HashMap> = reparsed + .syntax() + .body + .iter() + .filter_map(|stmt| match stmt { + Stmt::FunctionDef(def) => Some((def.name.to_string(), def.body.to_vec())), + _ => None, + }) + .collect(); + + let mut narrowed = Vec::new(); + let mut silent = Vec::new(); + for function in &resolved.functions { + if function.class_name.is_some() { + continue; + } + let Some(body) = bodies.get(&function.name) else { + continue; + }; + let declared = declared_types(&source, function); + let result = analyse_function_in( + body, + NarrowEnv::new(declared), + &function.narrowing_guards, + &ctx, + ); + if result.narrowed_uses.is_empty() && result.unreachable_ranges.is_empty() { + silent.push(function.name.clone()); + } else { + narrowed.push(function.name.clone()); + } + } + + narrowed.sort(); + silent.sort(); + println!("functions with narrowing signal: {}", narrowed.len()); + println!("functions without narrowing signal: {}", silent.len()); + println!("silent: {silent:?}"); + println!( + "RESULT narrowing_signal={} total={} pct={}", + narrowed.len(), + narrowed.len() + silent.len(), + narrowed.len() * 100 / (narrowed.len() + silent.len()).max(1) + ); + Ok(()) +} diff --git a/crates/basilisk-checker/examples/incremental_measure.rs b/crates/basilisk-checker/examples/incremental_measure.rs new file mode 100644 index 00000000..842b7de0 --- /dev/null +++ b/crates/basilisk-checker/examples/incremental_measure.rs @@ -0,0 +1,169 @@ +//! Implements the Stage 1 measurement items of +//! [NARROWPLAN-CHECKLIST](../../../docs/plans/CHECKER-TYPE-NARROWING-INFERENCE-PLAN.md): +//! memory on a 1M+ LOC corpus and p50/p99 keystroke re-check latency over the +//! definition-level Salsa queries ([TYPEINF-TARGET-INCREMENTAL], +//! `crates/basilisk-checker/src/incremental_defs.rs`). +//! +//! Usage (generate the corpus first): +//! ```sh +//! python3 scripts/gen_incremental_corpus.py /tmp/incremental-corpus +//! cargo run --release -p basilisk-checker --example incremental_measure -- \ +//! /tmp/incremental-corpus 200 +//! ``` +//! +//! Methodology (self-measured, reproducible): cold pass runs `definitions` + +//! `definition_type` + `expression_types` + `module_interface` over every +//! file; the keystroke loop then applies single-character body edits to +//! deterministically-chosen files (seeded LCG) and re-runs the same queries +//! for the edited file only, timing each round trip. Memory is the process +//! RSS reported by `ps` after the cold pass and at the end. + +use std::io::Write as _; +use std::time::Instant; + +use basilisk_checker::incremental_defs::{ + definition_type, definitions, expression_types, module_interface, +}; +use basilisk_db::{BasiliskDatabase, Db, SourceFile}; +use salsa::Setter as _; + +/// Deterministic linear congruential generator (no external RNG dependency). +struct Lcg(u64); + +impl Lcg { + fn next(&mut self) -> u64 { + self.0 = self + .0 + .wrapping_mul(6_364_136_223_846_793_005) + .wrapping_add(1_442_695_040_888_963_407); + self.0 >> 16 + } +} + +/// Run every definition-level query for one file, returning the definition count. +fn check_file(db: &dyn Db, file: SourceFile) -> usize { + let defs = definitions(db, file); + for def in defs { + let _ = definition_type(db, *def); + let _ = expression_types(db, *def); + } + let _ = module_interface(db, file); + defs.len() +} + +/// Current process RSS in kilobytes, via `ps` (portable across macOS/Linux). +fn rss_kb() -> Option { + let output = std::process::Command::new("ps") + .args(["-o", "rss=", "-p", &std::process::id().to_string()]) + .output() + .ok()?; + String::from_utf8(output.stdout) + .ok()? + .trim() + .parse::() + .ok() +} + +/// The `pct`-th percentile (nearest-rank) of an ascending-sorted sample. +fn percentile(sorted_micros: &[u128], pct: usize) -> u128 { + let rank = (sorted_micros.len() * pct).div_ceil(100); + sorted_micros + .get(rank.saturating_sub(1)) + .or_else(|| sorted_micros.last()) + .copied() + .unwrap_or(0) +} + +/// Format microseconds as fractional milliseconds without float casts. +fn format_ms(micros: u128) -> String { + format!("{}.{:03}", micros / 1000, micros % 1000) +} + +/// Format kilobytes as fractional megabytes without float casts. +fn format_mb(kb: u64) -> String { + format!("{}.{}", kb / 1024, (kb % 1024) * 10 / 1024) +} + +fn main() -> Result<(), Box> { + let mut args = std::env::args().skip(1); + let corpus_dir = std::path::PathBuf::from( + args.next() + .ok_or("usage: incremental_measure CORPUS_DIR [EDITS]")?, + ); + let edits: usize = args.next().map_or(Ok(200), |raw| raw.parse())?; + + let mut db = BasiliskDatabase::default(); + let mut files: Vec<(SourceFile, String)> = Vec::new(); + let mut total_loc = 0_usize; + for entry in std::fs::read_dir(&corpus_dir)? { + let path = entry?.path(); + if path.extension().is_some_and(|ext| ext == "py") { + let text = std::fs::read_to_string(&path)?; + total_loc += text.lines().count(); + let file = SourceFile::new(&db, path.display().to_string(), text.clone()); + files.push((file, text)); + } + } + files.sort_by_key(|entry| entry.1.len()); + println!("corpus: {} files, {} LOC", files.len(), total_loc); + + // Cold pass. + let cold_start = Instant::now(); + let mut def_count = 0_usize; + for (file, _) in &files { + def_count += check_file(&db, *file); + } + let cold = cold_start.elapsed(); + let rss_after_cold = rss_kb().unwrap_or(0); + println!( + "cold pass: {def_count} definitions in {:.2}s, RSS {} MB", + cold.as_secs_f64(), + format_mb(rss_after_cold) + ); + + // Keystroke loop: append a digit inside a function body line. + let mut rng = Lcg(0x5eed_2026_0718); + let mut latencies: Vec = Vec::with_capacity(edits); + for round in 0..edits { + let index = usize::try_from(rng.next()).unwrap_or(0) % files.len().max(1); + let Some((file, original)) = files.get(index) else { + continue; + }; + // Simulate a keystroke: mutate one arithmetic constant in the text. + let edited = original.replacen("offset * factor", "offset * factor + 1", 1); + let next_text = if round % 2 == 0 { + edited + } else { + original.clone() + }; + + let start = Instant::now(); + let _ = file.set_text(&mut db).to(next_text); + let _ = check_file(&db, *file); + latencies.push(start.elapsed().as_micros()); + } + latencies.sort_unstable(); + + let p50 = percentile(&latencies, 50); + let p99 = percentile(&latencies, 99); + let rss_final = rss_kb().unwrap_or(0); + println!( + "keystroke re-check over {edits} edits: p50 {} ms, p99 {} ms", + format_ms(p50), + format_ms(p99) + ); + println!("final RSS {} MB", format_mb(rss_final)); + + let mut out = std::io::stdout().lock(); + writeln!( + out, + "RESULT files={} loc={} cold_s={:.2} p50_ms={} p99_ms={} rss_mb={}", + files.len(), + total_loc, + cold.as_secs_f64(), + format_ms(p50), + format_ms(p99), + format_mb(rss_final) + )?; + Ok(()) +} diff --git a/crates/basilisk-checker/src/bidir/builtins.rs b/crates/basilisk-checker/src/bidir/builtins.rs new file mode 100644 index 00000000..f4d601d8 --- /dev/null +++ b/crates/basilisk-checker/src/bidir/builtins.rs @@ -0,0 +1,105 @@ +//! Implements [TYPEINF-TARGET-BIDIRECTIONAL] expression inference. See +//! docs/plans/CHECKER-TYPE-NARROWING-INFERENCE-PLAN.md#NARROWPLAN-CHECKLIST. +//! The **centralized** builtin constructor/function/method signature table — +//! the single home the checklist demands instead of rule-local string tables +//! ("centralize builtin constructor/method signatures"). Rules migrate onto +//! this at the Integration stage; new code must never grow a private copy. + +use crate::types::InferredType; + +/// Return type of calling a builtin constructor or function by bare name. +/// +/// Only names whose return type is fixed regardless of arguments are listed — +/// argument-dependent builtins (`abs`, `max`, `next`, …) stay out rather than +/// guessing ([TYPEINF-EXCEEDS-NOUNKNOWN]). +#[must_use] +pub fn builtin_call_return(name: &str) -> Option { + Some(match name { + "int" | "len" | "ord" | "hash" | "id" => InferredType::Int, + "float" => InferredType::Float, + "str" | "repr" | "format" | "chr" | "hex" | "oct" | "bin" | "ascii" | "input" => { + InferredType::Str + } + "bool" | "isinstance" | "issubclass" | "callable" | "hasattr" => InferredType::Bool, + "bytes" | "bytearray" => InferredType::Bytes, + "list" | "sorted" => InferredType::List(Box::new(InferredType::Unknown)), + "dict" => InferredType::Dict( + Box::new(InferredType::Unknown), + Box::new(InferredType::Unknown), + ), + "set" | "frozenset" => InferredType::Set(Box::new(InferredType::Unknown)), + "print" => InferredType::None_, + _ => return None, + }) +} + +/// Return type of calling a method on a receiver of a known builtin type. +/// +/// Covers the argument-independent core of `str`/`list`/`dict`/`set` +/// methods; anything else answers `None` (the caller stays `Unknown`). +#[must_use] +pub fn builtin_method_return(receiver: &InferredType, method: &str) -> Option { + match receiver { + InferredType::Str | InferredType::LiteralString | InferredType::Literal(_) => { + str_method_return(method) + } + InferredType::List(elem) => list_method_return(method, elem), + InferredType::Dict(key, value) => dict_method_return(method, key, value), + InferredType::Set(_) => set_method_return(method), + _ => None, + } +} + +/// `str` methods with argument-independent returns. +fn str_method_return(method: &str) -> Option { + Some(match method { + "upper" | "lower" | "strip" | "lstrip" | "rstrip" | "title" | "capitalize" | "casefold" + | "swapcase" | "replace" | "format" | "join" | "zfill" | "center" | "ljust" | "rjust" + | "expandtabs" | "removeprefix" | "removesuffix" => InferredType::Str, + "split" | "rsplit" | "splitlines" => InferredType::List(Box::new(InferredType::Str)), + "startswith" | "endswith" | "isdigit" | "isalpha" | "isalnum" | "isspace" | "isupper" + | "islower" | "istitle" | "isnumeric" | "isdecimal" | "isidentifier" | "isprintable" + | "isascii" => InferredType::Bool, + "find" | "rfind" | "index" | "rindex" | "count" => InferredType::Int, + "encode" => InferredType::Bytes, + _ => return None, + }) +} + +/// `list` methods with argument-independent returns. +fn list_method_return(method: &str, elem: &InferredType) -> Option { + Some(match method { + "append" | "extend" | "insert" | "remove" | "clear" | "sort" | "reverse" => { + InferredType::None_ + } + "pop" => elem.clone(), + "count" | "index" => InferredType::Int, + "copy" => InferredType::List(Box::new(elem.clone())), + _ => return None, + }) +} + +/// `dict` methods with argument-independent returns. +fn dict_method_return( + method: &str, + key: &InferredType, + value: &InferredType, +) -> Option { + Some(match method { + "keys" => InferredType::List(Box::new(key.clone())), + "values" => InferredType::List(Box::new(value.clone())), + "clear" | "update" => InferredType::None_, + "copy" => InferredType::Dict(Box::new(key.clone()), Box::new(value.clone())), + "get" => InferredType::Optional(Box::new(value.clone())), + _ => return None, + }) +} + +/// `set` methods with argument-independent returns. +fn set_method_return(method: &str) -> Option { + Some(match method { + "add" | "discard" | "remove" | "clear" | "update" => InferredType::None_, + "isdisjoint" | "issubset" | "issuperset" => InferredType::Bool, + _ => return None, + }) +} diff --git a/crates/basilisk-checker/src/bidir/check.rs b/crates/basilisk-checker/src/bidir/check.rs new file mode 100644 index 00000000..e65cb2f4 --- /dev/null +++ b/crates/basilisk-checker/src/bidir/check.rs @@ -0,0 +1,259 @@ +//! Implements [TYPEINF-TARGET-BIDIRECTIONAL]. See docs/specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-TARGET-BIDIRECTIONAL +//! The bidirectional engine — checking mode (`check(e, τ)`), the primary +//! driver. +//! +//! Expected-type context flows top-down into container literals, +//! comprehensions, lambda parameters, and call arguments; only when no +//! checking rule applies does the engine fall back to synthesis plus one +//! recorded `synth(e) <: τ` constraint. + +use ruff_python_ast::Expr; +use ruff_text_size::Ranged; + +use crate::types::InferredType; + +use super::constraints::ConstraintReason; +use super::engine::BidirEngine; +use super::ty::Ty; + +impl BidirEngine { + /// Checking mode: verify `expr` against the `expected` type, threading + /// the expectation into sub-expressions wherever a rule applies. + pub fn check(&mut self, expr: &Expr, expected: &Ty) { + self.check_with_reason(expr, expected, ConstraintReason::ExpectedType); + } + + /// [`Self::check`] with an explicit constraint reason for the fallback + /// (and nested) obligations. + pub(super) fn check_with_reason( + &mut self, + expr: &Expr, + expected: &Ty, + reason: ConstraintReason, + ) { + match (expr, expected) { + (Expr::List(list), Ty::List(elem)) => self.check_elements(&list.elts, elem), + (Expr::Set(set), Ty::Set(elem)) => self.check_elements(&set.elts, elem), + (Expr::Dict(dict), Ty::Dict(key, value)) => self.check_dict(dict, key, value), + (Expr::Tuple(tuple), Ty::Tuple(elems)) => { + self.check_tuple(expr, tuple, elems, expected, reason); + } + (Expr::Lambda(lambda), Ty::Callable(params, ret)) => { + self.check_lambda(lambda, params, ret); + } + (Expr::If(ternary), _) => self.check_ternary(ternary, expected, reason), + (Expr::Named(walrus), _) => self.check_walrus(walrus, expected), + (Expr::Call(call), _) => self.check_call(call, expected), + (Expr::ListComp(comp), Ty::List(elem)) => { + self.check_comprehension(&comp.generators, &comp.elt, elem); + } + (Expr::SetComp(comp), Ty::Set(elem)) => { + self.check_comprehension(&comp.generators, &comp.elt, elem); + } + (Expr::DictComp(comp), Ty::Dict(key, value)) => { + self.check_dict_comprehension(comp, key, value); + } + (_, Ty::Union(alts)) => self.check_against_union(expr, alts, reason), + _ => self.check_fallback(expr, expected, reason), + } + } + + /// Elements of a checked list/set literal are each checked against the + /// expected element type — context threads inward, no fresh variable. + fn check_elements(&mut self, elts: &[Expr], elem: &Ty) { + for elt in elts { + if let Expr::Starred(starred) = elt { + let _ = self.synth(&starred.value); + } else { + self.check_with_reason(elt, elem, ConstraintReason::CollectionElement); + } + } + } + + /// Keys/values of a checked dict literal thread into the expected types. + fn check_dict(&mut self, dict: &ruff_python_ast::ExprDict, key: &Ty, value: &Ty) { + for item in &dict.items { + match &item.key { + Some(key_expr) => { + self.check_with_reason(key_expr, key, ConstraintReason::DictKey); + self.check_with_reason(&item.value, value, ConstraintReason::DictValue); + } + None => { + let _ = self.synth(&item.value); + } + } + } + } + + /// A tuple literal against `tuple[T1, .., Tn]` (pairwise) or the + /// homogeneous `tuple[X, ...]` lift (every element against `X`). + fn check_tuple( + &mut self, + whole: &Expr, + tuple: &ruff_python_ast::ExprTuple, + elems: &[Ty], + expected: &Ty, + reason: ConstraintReason, + ) { + if let Some(homogeneous) = homogeneous_element(elems) { + let homogeneous = homogeneous.clone(); + self.check_elements(&tuple.elts, &homogeneous); + return; + } + let no_spread = tuple.elts.iter().all(|e| !matches!(e, Expr::Starred(_))); + if tuple.elts.len() == elems.len() && no_spread { + let elems = elems.to_vec(); + for (elt, elem) in tuple.elts.iter().zip(&elems) { + self.check_with_reason(elt, elem, ConstraintReason::CollectionElement); + } + return; + } + self.check_fallback(whole, expected, reason); + } + + /// A lambda against `Callable[[P..], R]`: parameters take the expected + /// **input** types (fresh input variables when the expectation is the + /// gradual `Callable[..., R]`), and the body checks against `R`. + fn check_lambda(&mut self, lambda: &ruff_python_ast::ExprLambda, params: &[Ty], ret: &Ty) { + let names = super::engine::lambda_param_names(lambda); + let bound: Vec<(String, Ty)> = names + .iter() + .enumerate() + .map(|(index, name)| { + let ty = params + .get(index) + .cloned() + .unwrap_or_else(|| Ty::Var(self.fresh_input_var())); + (name.clone(), ty) + }) + .collect(); + let body = &lambda.body; + self.scoped(|engine| { + for (name, ty) in &bound { + engine.bind(name, ty.clone()); + } + engine.check_with_reason(body, ret, ConstraintReason::LambdaBody); + }); + } + + /// Both branches of a ternary check against the same expectation. + fn check_ternary( + &mut self, + ternary: &ruff_python_ast::ExprIf, + expected: &Ty, + reason: ConstraintReason, + ) { + let _ = self.synth(&ternary.test); + self.check_with_reason(&ternary.body, expected, reason); + self.check_with_reason(&ternary.orelse, expected, reason); + } + + /// `(name := value)` checks the value and binds the name to it. + fn check_walrus(&mut self, walrus: &ruff_python_ast::ExprNamed, expected: &Ty) { + self.check_with_reason(&walrus.value, expected, ConstraintReason::WalrusValue); + if let Expr::Name(target) = walrus.target.as_ref() { + self.bind(target.id.as_str(), expected.clone()); + } + } + + /// A call in checked position: arguments thread through + /// [`BidirEngine::synth_call`], and the declared return must satisfy the + /// expectation. + fn check_call(&mut self, call: &ruff_python_ast::ExprCall, expected: &Ty) { + let ret = self.synth_call(call); + self.constraints_push( + ret, + expected.clone(), + call.range(), + ConstraintReason::CallReturn, + ); + } + + /// A list/set comprehension in checked position: generators bind, then + /// the element expression checks against the expected element type. + fn check_comprehension( + &mut self, + generators: &[ruff_python_ast::Comprehension], + elt: &Expr, + elem: &Ty, + ) { + let elem = elem.clone(); + self.scoped(|engine| { + engine.bind_generators(generators); + engine.check_with_reason(elt, &elem, ConstraintReason::ComprehensionElement); + }); + } + + /// A dict comprehension in checked position. + fn check_dict_comprehension( + &mut self, + comp: &ruff_python_ast::ExprDictComp, + key: &Ty, + value: &Ty, + ) { + let (key, value) = (key.clone(), value.clone()); + self.scoped(|engine| { + engine.bind_generators(&comp.generators); + if let Some(key_expr) = comp.key.as_deref() { + engine.check_with_reason(key_expr, &key, ConstraintReason::DictKey); + } + engine.check_with_reason(&comp.value, &value, ConstraintReason::DictValue); + }); + } + + /// Against a union: when exactly one alternative structurally matches the + /// literal's shape, thread context into it; otherwise fall back. + fn check_against_union(&mut self, expr: &Expr, alts: &[Ty], reason: ConstraintReason) { + let matching: Vec<&Ty> = alts.iter().filter(|alt| shape_matches(expr, alt)).collect(); + if let [only] = matching.as_slice() { + let only = (*only).clone(); + self.check_with_reason(expr, &only, reason); + return; + } + self.check_fallback(expr, &Ty::Union(alts.to_vec()), reason); + } + + /// No checking rule applies: synthesize and record one obligation. + fn check_fallback(&mut self, expr: &Expr, expected: &Ty, reason: ConstraintReason) { + let ty = self.synth(expr); + self.constraints_push(ty, expected.clone(), expr.range(), reason); + } + + /// Constraint recording, kept private to the engine internals. + fn constraints_push( + &mut self, + sub: Ty, + sup: Ty, + range: ruff_text_size::TextRange, + reason: ConstraintReason, + ) { + self.constraints.push(sub, sup, range, reason); + } + + /// Fresh input-polarity variable (helper keeps borrow scopes tight). + fn fresh_input_var(&mut self) -> super::tyvar::TyVarId { + self.vars.fresh(super::tyvar::Polarity::Input) + } +} + +/// The homogeneous-tuple lift: `tuple[X, ...]` arrives as a two-element list +/// whose second entry is the `Named("...")` marker (matching +/// [`InferredType::Tuple`]'s encoding). +fn homogeneous_element(elems: &[Ty]) -> Option<&Ty> { + match elems { + [elem, Ty::Ground(InferredType::Named(marker))] if marker == "..." => Some(elem), + _ => None, + } +} + +/// Whether an expression's literal shape can only mean one union alternative. +fn shape_matches(expr: &Expr, alt: &Ty) -> bool { + matches!( + (expr, alt), + (Expr::List(_) | Expr::ListComp(_), Ty::List(_)) + | (Expr::Set(_) | Expr::SetComp(_), Ty::Set(_)) + | (Expr::Dict(_) | Expr::DictComp(_), Ty::Dict(_, _)) + | (Expr::Tuple(_), Ty::Tuple(_)) + | (Expr::Lambda(_), Ty::Callable(_, _)) + ) +} diff --git a/crates/basilisk-checker/src/bidir/constraints.rs b/crates/basilisk-checker/src/bidir/constraints.rs new file mode 100644 index 00000000..460212aa --- /dev/null +++ b/crates/basilisk-checker/src/bidir/constraints.rs @@ -0,0 +1,89 @@ +//! Implements [TYPEINF-TARGET-CONSTRAINTS]. See docs/specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-TARGET-CONSTRAINTS +//! Subtype constraints produced by the generation pass. +//! +//! The bidirectional traversal ([`super::engine`]) never decides subtyping on +//! its own: it only *records* `sub <: sup` obligations here, and the separate +//! solver ([`super::solve`]) discharges them — the Pottier–Rémy two-stage +//! split the spec mandates. + +use ruff_text_size::TextRange; + +use super::ty::Ty; + +/// Why a constraint exists — drives the eventual diagnostic wording. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ConstraintReason { + /// An element flowing into a list/set literal's element type. + CollectionElement, + /// A key flowing into a dict literal's key type. + DictKey, + /// A value flowing into a dict literal's value type. + DictValue, + /// A call argument flowing into the callee's declared parameter. + CallArgument, + /// A callee's return flowing into the expression's expected type. + CallReturn, + /// A lambda body flowing into the expected return type. + LambdaBody, + /// A comprehension's element flowing into the expected element type. + ComprehensionElement, + /// An expression checked directly against an expected (annotated) type. + ExpectedType, + /// A walrus (`:=`) value flowing into the expression's expected type. + WalrusValue, +} + +/// One `sub <: sup` obligation with the source location that produced it. +#[derive(Debug, Clone, PartialEq)] +pub struct Constraint { + /// The type that must be assignable… + pub sub: Ty, + /// …to this type. + pub sup: Ty, + /// Source range of the expression that generated the obligation. + pub range: TextRange, + /// Why the obligation exists. + pub reason: ConstraintReason, +} + +/// The ordered set of obligations one generation pass produced. +#[derive(Debug, Clone, Default, PartialEq)] +pub struct ConstraintSet { + constraints: Vec, +} + +impl ConstraintSet { + /// Record one `sub <: sup` obligation. + pub fn push(&mut self, sub: Ty, sup: Ty, range: TextRange, reason: ConstraintReason) { + self.constraints.push(Constraint { + sub, + sup, + range, + reason, + }); + } + + /// The recorded obligations, in generation order. + #[must_use] + pub fn as_slice(&self) -> &[Constraint] { + &self.constraints + } + + /// Number of recorded obligations. + #[must_use] + pub fn len(&self) -> usize { + self.constraints.len() + } + + /// Whether nothing has been recorded. + #[must_use] + pub fn is_empty(&self) -> bool { + self.constraints.is_empty() + } + + /// Consume the set, yielding the obligations for the solver. + #[must_use] + pub fn into_vec(self) -> Vec { + self.constraints + } +} diff --git a/crates/basilisk-checker/src/bidir/engine.rs b/crates/basilisk-checker/src/bidir/engine.rs new file mode 100644 index 00000000..4ceea58d --- /dev/null +++ b/crates/basilisk-checker/src/bidir/engine.rs @@ -0,0 +1,521 @@ +//! Implements [TYPEINF-TARGET-BIDIRECTIONAL]. See docs/specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-TARGET-BIDIRECTIONAL +//! The bidirectional engine — synthesis mode (`synth(e) → τ`). +//! +//! Every AST expression node supports both modes: [`BidirEngine::synth`] +//! infers bottom-up, and [`BidirEngine::check`] (in [`super::check`]) drives +//! top-down with an expected type. Neither mode judges subtyping directly — +//! they only allocate variables and record constraints +//! ([TYPEINF-TARGET-CONSTRAINTS]); [`super::solve`] discharges them. + +use std::collections::HashMap; + +use ruff_python_ast::{Expr, Number, Operator, UnaryOp}; +use ruff_text_size::Ranged; + +use crate::types::{InferredType, LiteralValue}; + +use super::constraints::{ConstraintReason, ConstraintSet}; +use super::solve::{solve, Solution}; +use super::ty::Ty; +use super::tyvar::{Polarity, TyVarStore}; + +/// One inference run's mutable state: variables, recorded constraints, and a +/// stack of lexical scopes (module/function bindings, lambda parameters, +/// comprehension targets). +#[derive(Debug, Default)] +pub struct BidirEngine { + pub(super) vars: TyVarStore, + pub(super) constraints: ConstraintSet, + scopes: Vec>, + /// Lowercased class name → (attribute → type), for plain attribute-load + /// synthesis on user classes (`Point().x`). Empty unless the caller + /// provides module class schemas via [`BidirEngine::set_class_attributes`]. + class_attributes: HashMap>, +} + +impl BidirEngine { + /// Start an engine whose outermost scope holds the given bindings + /// (typically parameter and local annotations). + #[must_use] + pub fn new(globals: HashMap) -> Self { + Self { + vars: TyVarStore::default(), + constraints: ConstraintSet::default(), + scopes: vec![globals], + class_attributes: HashMap::new(), + } + } + + /// Provide the module's class-attribute schemas for plain attribute-load + /// synthesis (`instance.attr`). + pub fn set_class_attributes(&mut self, table: HashMap>) { + self.class_attributes = table; + } + + /// Solve everything recorded so far and return the outcome. + #[must_use] + pub fn finish(self) -> Solution { + solve(self.vars, self.constraints.into_vec()) + } + + /// Allocate a fresh variable — the parameter-inference entry point + /// (issue #317, [`crate::param_infer`]). + pub fn fresh_param_var(&mut self, polarity: Polarity) -> super::tyvar::TyVarId { + self.vars.fresh(polarity) + } + + /// Bind a name in the OUTERMOST scope (module globals / parameters). + pub fn bind_global(&mut self, name: &str, ty: Ty) { + if let Some(scope) = self.scopes.first_mut() { + let _ = scope.insert(name.to_owned(), ty); + } + } + + /// Record call-site evidence: a lower bound directly on a variable. + pub fn add_var_lower_bound(&mut self, var: super::tyvar::TyVarId, ty: Ty) { + self.vars.add_lower(var, ty); + } + + /// Look a name up through the scope stack, innermost first. + pub(super) fn lookup(&self, name: &str) -> Option { + self.scopes + .iter() + .rev() + .find_map(|scope| scope.get(name).cloned()) + } + + /// Bind a name in the innermost scope. + pub(super) fn bind(&mut self, name: &str, ty: Ty) { + if let Some(scope) = self.scopes.last_mut() { + let _ = scope.insert(name.to_owned(), ty); + } + } + + /// Run `body` inside a fresh child scope. + pub(super) fn scoped(&mut self, body: impl FnOnce(&mut Self) -> R) -> R { + self.scopes.push(HashMap::new()); + let result = body(self); + let _ = self.scopes.pop(); + result + } + + /// Synthesis mode: infer a type for `expr` bottom-up, recording + /// constraints for anything that flows into a fresh variable. + pub fn synth(&mut self, expr: &Expr) -> Ty { + match expr { + Expr::NumberLiteral(num) => synth_number(&num.value), + Expr::BooleanLiteral(lit) => { + Ty::Ground(InferredType::Literal(LiteralValue::Bool(lit.value))) + } + Expr::StringLiteral(lit) => Ty::Ground(InferredType::Literal(LiteralValue::Str( + lit.value.to_str().to_owned(), + ))), + Expr::BytesLiteral(_) => Ty::Ground(InferredType::Bytes), + Expr::FString(_) => Ty::Ground(InferredType::Str), + Expr::NoneLiteral(_) => Ty::Ground(InferredType::None_), + Expr::EllipsisLiteral(_) => Ty::Ground(InferredType::Named("ellipsis".to_owned())), + Expr::Name(name) => self.lookup(name.id.as_str()).unwrap_or_else(Ty::unknown), + Expr::List(list) => self.synth_elements(&list.elts, expr, Ty::List), + Expr::Set(set) => self.synth_elements(&set.elts, expr, Ty::Set), + Expr::Dict(dict) => self.synth_dict(dict, expr), + Expr::Tuple(tuple) => self.synth_tuple(tuple), + Expr::Lambda(lambda) => self.synth_lambda(lambda), + Expr::If(ternary) => self.synth_ternary(ternary), + Expr::Named(walrus) => self.synth_walrus(walrus), + Expr::Call(call) => self.synth_call(call), + Expr::Compare(_) => Ty::Ground(InferredType::Bool), + Expr::BoolOp(bool_op) => self.synth_bool_op(&bool_op.values), + Expr::UnaryOp(unary) => self.synth_unary(unary), + Expr::BinOp(bin_op) => self.synth_bin_op(bin_op), + Expr::ListComp(comp) => self.synth_comprehension(&comp.generators, &comp.elt, Ty::List), + Expr::SetComp(comp) => self.synth_comprehension(&comp.generators, &comp.elt, Ty::Set), + Expr::DictComp(comp) => self.synth_dict_comprehension(comp), + Expr::Subscript(subscript) => self.synth_subscript(subscript), + Expr::Attribute(attribute) => self.synth_attribute(attribute), + _ => Ty::unknown(), + } + } + + /// `[e1, ..]` / `{e1, ..}`: elements flow into a fresh output variable — + /// `list[Var{lower=…}]`, settling only at first constraining use + /// (deferred generalization, [TYPEINF-TARGET-CONSTRAINTS]). + fn synth_elements( + &mut self, + elts: &[Expr], + whole: &Expr, + wrap: impl FnOnce(Box) -> Ty, + ) -> Ty { + let elem = self.vars.fresh(Polarity::Output); + for elt in elts { + let ty = self.synth_spread_aware(elt); + self.constraints.push( + ty, + Ty::Var(elem), + elt.range(), + ConstraintReason::CollectionElement, + ); + } + let _ = whole; + wrap(Box::new(Ty::Var(elem))) + } + + /// A starred element spreads an iterable whose element type Stage 0 does + /// not extract — it contributes the conservative `Unknown`. + fn synth_spread_aware(&mut self, elt: &Expr) -> Ty { + if let Expr::Starred(starred) = elt { + let _ = self.synth(&starred.value); + return Ty::unknown(); + } + self.synth(elt) + } + + /// `{k: v, ..}`: keys and values flow into fresh output variables. + fn synth_dict(&mut self, dict: &ruff_python_ast::ExprDict, whole: &Expr) -> Ty { + let key_var = self.vars.fresh(Polarity::Output); + let value_var = self.vars.fresh(Polarity::Output); + for item in &dict.items { + let Some(key) = &item.key else { + // `**spread` merges a mapping Stage 0 does not decompose. + let _ = self.synth(&item.value); + self.constraints.push( + Ty::unknown(), + Ty::Var(key_var), + whole.range(), + ConstraintReason::DictKey, + ); + continue; + }; + let key_ty = self.synth(key); + self.constraints.push( + key_ty, + Ty::Var(key_var), + key.range(), + ConstraintReason::DictKey, + ); + let value_ty = self.synth(&item.value); + self.constraints.push( + value_ty, + Ty::Var(value_var), + item.value.range(), + ConstraintReason::DictValue, + ); + } + Ty::Dict(Box::new(Ty::Var(key_var)), Box::new(Ty::Var(value_var))) + } + + /// `(e1, ..)`: heterogeneous — each element keeps its own type. + fn synth_tuple(&mut self, tuple: &ruff_python_ast::ExprTuple) -> Ty { + Ty::Tuple( + tuple + .elts + .iter() + .map(|elt| self.synth_spread_aware(elt)) + .collect(), + ) + } + + /// `lambda p, ..: body`: parameters become fresh **input** variables in a + /// child scope; the body synthesizes under them. + fn synth_lambda(&mut self, lambda: &ruff_python_ast::ExprLambda) -> Ty { + let names = lambda_param_names(lambda); + let params: Vec = names + .iter() + .map(|_| Ty::Var(self.vars.fresh(Polarity::Input))) + .collect(); + let body = self.scoped(|engine| { + for (name, ty) in names.iter().zip(¶ms) { + engine.bind(name, ty.clone()); + } + engine.synth(&lambda.body) + }); + Ty::Callable(params, Box::new(body)) + } + + /// `a if cond else b`: the union of both branches. + fn synth_ternary(&mut self, ternary: &ruff_python_ast::ExprIf) -> Ty { + let _ = self.synth(&ternary.test); + let body = self.synth(&ternary.body); + let orelse = self.synth(&ternary.orelse); + Ty::Union(vec![body, orelse]) + } + + /// `(name := value)`: the value's type, also bound to `name`. + fn synth_walrus(&mut self, walrus: &ruff_python_ast::ExprNamed) -> Ty { + let value = self.synth(&walrus.value); + if let Expr::Name(target) = walrus.target.as_ref() { + self.bind(target.id.as_str(), value.clone()); + } + value + } + + /// `f(a, ..)`: when the callee synthesizes to a callable, arguments are + /// **checked** against its declared parameters (expected types thread + /// into call arguments — check as the primary driver) and the declared + /// return is the result; otherwise the conservative `Unknown`. + pub(super) fn synth_call(&mut self, call: &ruff_python_ast::ExprCall) -> Ty { + let callee = self.synth(&call.func); + let Ty::Callable(params, ret) = callee else { + for arg in &call.arguments.args { + let _ = self.synth(arg); + } + return self.non_callable_call_result(call, &callee); + }; + for (arg, param) in call.arguments.args.iter().zip(¶ms) { + self.check_with_reason(arg, param, ConstraintReason::CallArgument); + } + for arg in call.arguments.args.iter().skip(params.len()) { + let _ = self.synth(arg); + } + *ret + } + + /// A call whose callee did not synthesize to a `Callable`: builtin + /// functions/constructors (via the centralized table in + /// [`super::builtins`]), builtin METHOD calls on known receivers, and + /// user-class constructors (`Named` callee → `Named` instance). + fn non_callable_call_result(&mut self, call: &ruff_python_ast::ExprCall, callee: &Ty) -> Ty { + // Constructor: a class object used as a callee yields an instance. + // (`Named` deliberately conflates class/instance at Stage 2 — the + // display value is right and no rule enforces it yet.) + if let Ty::Ground(InferredType::Named(name)) = callee { + return Ty::Ground(InferredType::Named(name.clone())); + } + match call.func.as_ref() { + Expr::Name(name) => super::builtins::builtin_call_return(name.id.as_str()) + .map_or_else(Ty::unknown, Ty::Ground), + Expr::Attribute(attribute) => { + let receiver = self.synth(&attribute.value).to_inferred(&self.vars); + super::builtins::builtin_method_return(&receiver, attribute.attr.as_str()) + .map_or_else(Ty::unknown, Ty::Ground) + } + _ => Ty::unknown(), + } + } + + /// `obj.attr`: a plain attribute LOAD — user-class attributes answer + /// from the provided class schemas; anything else stays `Unknown` + /// (method CALLS resolve separately through the builtin table). + fn synth_attribute(&mut self, attribute: &ruff_python_ast::ExprAttribute) -> Ty { + let receiver = self.synth(&attribute.value).to_inferred(&self.vars); + let InferredType::Named(class_name) = receiver else { + return Ty::unknown(); + }; + self.class_attributes + .get(&class_name) + .and_then(|attributes| attributes.get(attribute.attr.as_str())) + .map_or_else(Ty::unknown, |ty| Ty::Ground(ty.clone())) + } + + /// `x[i]`: element extraction for known container shapes. + fn synth_subscript(&mut self, subscript: &ruff_python_ast::ExprSubscript) -> Ty { + let receiver = self.synth(&subscript.value); + let index = self.synth(&subscript.slice); + match (&receiver, &index) { + (Ty::List(elem), _) if !matches!(subscript.slice.as_ref(), Expr::Slice(_)) => { + (**elem).clone() + } + (Ty::List(_), _) => receiver.clone(), + (Ty::Dict(_, value), _) => (**value).clone(), + (Ty::Tuple(elems), Ty::Ground(InferredType::Literal(LiteralValue::Int(position)))) => { + usize::try_from(*position) + .ok() + .and_then(|index| elems.get(index).cloned()) + .unwrap_or_else(Ty::unknown) + } + (Ty::Ground(InferredType::Str | InferredType::LiteralString), _) => { + Ty::Ground(InferredType::Str) + } + _ => Ty::unknown(), + } + } + + /// `a or b`, `a and b`: the union of operand types (truthiness-based + /// refinement is Stage 2 narrowing, not Stage 0). + fn synth_bool_op(&mut self, values: &[Expr]) -> Ty { + Ty::Union(values.iter().map(|value| self.synth(value)).collect()) + } + + /// Unary operators preserve literal precision for integer negation. + fn synth_unary(&mut self, unary: &ruff_python_ast::ExprUnaryOp) -> Ty { + let operand = self.synth(&unary.operand); + match (unary.op, operand) { + (UnaryOp::Not, _) => Ty::Ground(InferredType::Bool), + (UnaryOp::USub, Ty::Ground(InferredType::Literal(LiteralValue::Int(n)))) => { + Ty::Ground(InferredType::Literal(LiteralValue::Int(n.wrapping_neg()))) + } + (UnaryOp::USub | UnaryOp::UAdd | UnaryOp::Invert, ty) => numeric_result(&ty), + } + } + + /// Binary operators: the small conservative table Stage 0 supports. + fn synth_bin_op(&mut self, bin_op: &ruff_python_ast::ExprBinOp) -> Ty { + let left = self.synth(&bin_op.left); + let right = self.synth(&bin_op.right); + bin_op_result(bin_op.op, &left, &right) + } + + /// `[elt for tgt in iter ..]` / `{elt for ..}`. + fn synth_comprehension( + &mut self, + generators: &[ruff_python_ast::Comprehension], + elt: &Expr, + wrap: impl FnOnce(Box) -> Ty, + ) -> Ty { + let elem = self.vars.fresh(Polarity::Output); + self.scoped(|engine| { + engine.bind_generators(generators); + let ty = engine.synth(elt); + engine.constraints.push( + ty, + Ty::Var(elem), + elt.range(), + ConstraintReason::ComprehensionElement, + ); + }); + wrap(Box::new(Ty::Var(elem))) + } + + /// `{k: v for ..}`. + fn synth_dict_comprehension(&mut self, comp: &ruff_python_ast::ExprDictComp) -> Ty { + let key_var = self.vars.fresh(Polarity::Output); + let value_var = self.vars.fresh(Polarity::Output); + self.scoped(|engine| { + engine.bind_generators(&comp.generators); + if let Some(key_expr) = comp.key.as_deref() { + let key = engine.synth(key_expr); + engine.constraints.push( + key, + Ty::Var(key_var), + key_expr.range(), + ConstraintReason::DictKey, + ); + } + let value = engine.synth(&comp.value); + engine.constraints.push( + value, + Ty::Var(value_var), + comp.value.range(), + ConstraintReason::DictValue, + ); + }); + Ty::Dict(Box::new(Ty::Var(key_var)), Box::new(Ty::Var(value_var))) + } + + /// Bind each generator's target from its iterable's element type. + pub(super) fn bind_generators(&mut self, generators: &[ruff_python_ast::Comprehension]) { + for generator in generators { + let iterable = self.synth(&generator.iter); + let element = iteration_element(&iterable); + self.bind_target(&generator.target, &element); + for guard in &generator.ifs { + let _ = self.synth(guard); + } + } + } + + /// Bind a `for` target (name or tuple of names) to an element type. + fn bind_target(&mut self, target: &Expr, element: &Ty) { + match (target, element) { + (Expr::Name(name), _) => self.bind(name.id.as_str(), element.clone()), + (Expr::Tuple(names), Ty::Tuple(elems)) if names.elts.len() == elems.len() => { + for (name, elem) in names.elts.iter().zip(elems) { + self.bind_target(name, elem); + } + } + (Expr::Tuple(names), _) => { + for name in &names.elts { + self.bind_target(name, &Ty::unknown()); + } + } + _ => {} + } + } +} + +/// Number literals keep integer literal precision; floats stay ground. +fn synth_number(value: &Number) -> Ty { + match value { + Number::Int(int) => int.as_i64().map_or(Ty::Ground(InferredType::Int), |n| { + Ty::Ground(InferredType::Literal(LiteralValue::Int(n))) + }), + Number::Float(_) => Ty::Ground(InferredType::Float), + Number::Complex { .. } => Ty::Ground(InferredType::Named("complex".to_owned())), + } +} + +/// What iterating over a value yields, when Stage 0 can tell. +fn iteration_element(iterable: &Ty) -> Ty { + match iterable { + Ty::List(elem) | Ty::Set(elem) => (**elem).clone(), + Ty::Dict(key, _) => (**key).clone(), + Ty::Ground(InferredType::List(elem) | InferredType::Set(elem)) => { + Ty::Ground((**elem).clone()) + } + Ty::Ground(InferredType::Dict(key, _)) => Ty::Ground((**key).clone()), + Ty::Ground(InferredType::Str | InferredType::LiteralString) => { + Ty::Ground(InferredType::Str) + } + _ => Ty::unknown(), + } +} + +/// Positional lambda parameter names, in declaration order. +pub(super) fn lambda_param_names(lambda: &ruff_python_ast::ExprLambda) -> Vec { + lambda.parameters.as_ref().map_or_else(Vec::new, |params| { + params + .posonlyargs + .iter() + .chain(¶ms.args) + .map(|param| param.parameter.name.to_string()) + .collect() + }) +} + +/// Widen a numeric operand to its ground arithmetic result type. +fn numeric_result(ty: &Ty) -> Ty { + match ty { + Ty::Ground( + InferredType::Literal(LiteralValue::Int(_)) | InferredType::Int | InferredType::Bool, + ) => Ty::Ground(InferredType::Int), + Ty::Ground(InferredType::Float) => Ty::Ground(InferredType::Float), + _ => Ty::unknown(), + } +} + +/// The conservative Stage 0 binary-operator result table. +fn bin_op_result(op: Operator, left: &Ty, right: &Ty) -> Ty { + let (left, right) = (numeric_or_seq(left), numeric_or_seq(right)); + match (op, left, right) { + (Operator::Div, OpClass::Int, OpClass::Int) => Ty::Ground(InferredType::Float), + (_, OpClass::Int, OpClass::Int) => Ty::Ground(InferredType::Int), + (_, OpClass::Int | OpClass::Float, OpClass::Int | OpClass::Float) => { + Ty::Ground(InferredType::Float) + } + (Operator::Add, OpClass::Str, OpClass::Str) => Ty::Ground(InferredType::Str), + _ => Ty::unknown(), + } +} + +/// Operand classification for [`bin_op_result`]. +enum OpClass { + Int, + Float, + Str, + Other, +} + +/// Classify an operand for the binary-operator table. +fn numeric_or_seq(ty: &Ty) -> OpClass { + match ty { + Ty::Ground( + InferredType::Int | InferredType::Bool | InferredType::Literal(LiteralValue::Int(_)), + ) => OpClass::Int, + Ty::Ground(InferredType::Float) => OpClass::Float, + Ty::Ground( + InferredType::Str + | InferredType::LiteralString + | InferredType::Literal(LiteralValue::Str(_)), + ) => OpClass::Str, + _ => OpClass::Other, + } +} diff --git a/crates/basilisk-checker/src/bidir/mod.rs b/crates/basilisk-checker/src/bidir/mod.rs new file mode 100644 index 00000000..fd9b9c72 --- /dev/null +++ b/crates/basilisk-checker/src/bidir/mod.rs @@ -0,0 +1,410 @@ +//! Implements [TYPEINF-TARGET-BIDIRECTIONAL] / [TYPEINF-TARGET-CONSTRAINTS]. +//! See docs/specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-TARGET. +//! +//! The Stage 0 bidirectional + constraint inference engine +//! ([NARROWPLAN-CHECKLIST], docs/plans/CHECKER-TYPE-NARROWING-INFERENCE-PLAN.md): +//! +//! - [`engine`] / [`check`] — the two-mode core: `synth(e) → τ` bottom-up and +//! `check(e, τ)` top-down, `check` as the primary driver; +//! - [`constraints`] — the generation pass's `τ₁ <: τ₂` obligations; +//! - [`solve`] — the separate solver (two-stage Pottier–Rémy split); +//! - [`tyvar`] — bounded, polar type variables with deferred generalization; +//! - [`ty`] — the solver language over the existing [`crate::types::InferredType`], +//! whose [`crate::types::InferredType::is_assignable_to`] remains the single +//! ground-subtyping authority. +//! +//! Salsa note ([TYPEINF-TARGET-INCREMENTAL]): the engine is a pure function of +//! one module's AST — it reads no other file and no global state — so running +//! it inside the existing file-level tracked queries +//! ([`crate::incremental::checked_file`]) adds **zero** new Salsa dependency +//! edges. Expected-type threading therefore cannot balloon dependency growth +//! until Stage 1 introduces finer-grained queries, where the checklist's +//! peek-ahead fallback would be reconsidered per construct. + +pub mod builtins; +pub mod check; +pub mod constraints; +pub mod engine; +pub mod solve; +pub mod ty; +pub mod tyvar; + +pub use constraints::{Constraint, ConstraintReason, ConstraintSet}; +pub use engine::BidirEngine; +pub use solve::{solve, Solution, SolveError}; +pub use ty::Ty; +pub use tyvar::{Polarity, TyVarId, TyVarStore}; + +#[cfg(test)] +mod tests { + #![expect( + clippy::expect_used, + reason = "test-only parsing of fixed, known-valid expression fixtures" + )] + + use std::collections::HashMap; + + use crate::types::{InferredType, LiteralValue}; + + use super::{BidirEngine, Solution, Ty}; + + /// Parse a single Python expression for engine tests. + fn parse_expr(source: &str) -> ruff_python_ast::ModExpression { + ruff_python_parser::parse_expression(source) + .map(ruff_python_parser::Parsed::into_syntax) + .expect("test expression must parse") + } + + /// Synthesize a type for `source` under `globals`, then solve. + fn synth_and_solve(source: &str, globals: HashMap) -> (InferredType, Solution) { + let module = parse_expr(source); + let mut engine = BidirEngine::new(globals); + let ty = engine.synth(&module.body); + let solution = engine.finish(); + (ty.to_inferred(&solution.vars), solution) + } + + /// Check `source` against an annotation-shaped expectation, then solve. + fn check_and_solve(source: &str, expected: &InferredType) -> Solution { + let module = parse_expr(source); + let mut engine = BidirEngine::new(HashMap::new()); + engine.check(&module.body, &Ty::from_inferred(expected)); + engine.finish() + } + + /// [TYPEINF-TARGET-CONSTRAINTS]: `[1]` synthesizes to + /// `list[Literal[1]]` via a bounded var — generalization stays deferred, + /// never eagerly widened to `list[int]`. + #[test] + fn list_literal_defers_generalization() { + let (inferred, solution) = synth_and_solve("[1]", HashMap::new()); + assert!(solution.errors.is_empty(), "{:?}", solution.errors); + assert_eq!( + inferred, + InferredType::List(Box::new(InferredType::Literal(LiteralValue::Int(1)))) + ); + } + + /// [TYPEINF-TARGET-BIDIRECTIONAL]: checking `[1, "x"]` against + /// `list[int]` threads the element expectation inward and reports exactly + /// the string element as unsatisfiable. + #[test] + fn checked_list_threads_element_expectation() { + let expected = InferredType::List(Box::new(InferredType::Int)); + let ok = check_and_solve("[1, 2]", &expected); + assert!(ok.errors.is_empty(), "{:?}", ok.errors); + + let bad = check_and_solve("[1, \"x\"]", &expected); + assert_eq!(bad.errors.len(), 1, "{:?}", bad.errors); + } + + /// Nested containers thread context all the way down. + #[test] + fn nested_container_context_threads_inward() { + let expected = + InferredType::List(Box::new(InferredType::List(Box::new(InferredType::Int)))); + let ok = check_and_solve("[[1], [2, 3]]", &expected); + assert!(ok.errors.is_empty(), "{:?}", ok.errors); + + let bad = check_and_solve("[[1], [\"x\"]]", &expected); + assert_eq!(bad.errors.len(), 1, "{:?}", bad.errors); + } + + /// [TYPEINF-TARGET-BIDIRECTIONAL]: a lambda checked against + /// `Callable[[int], int]` binds its parameter to `int` and checks the + /// body under that binding. + #[test] + fn checked_lambda_binds_expected_parameter_types() { + let expected = InferredType::Callable(crate::types::CallableInfo { + param_types: vec![InferredType::Int], + return_type: Box::new(InferredType::Int), + }); + let ok = check_and_solve("lambda x: x + 1", &expected); + assert!(ok.errors.is_empty(), "{:?}", ok.errors); + + let bad_ret = InferredType::Callable(crate::types::CallableInfo { + param_types: vec![InferredType::Int], + return_type: Box::new(InferredType::Str), + }); + let bad = check_and_solve("lambda x: x + 1", &bad_ret); + assert_eq!(bad.errors.len(), 1, "{:?}", bad.errors); + } + + /// Call arguments receive expected types from the callee's declared + /// parameters — context threads into call arguments. + #[test] + fn call_arguments_receive_declared_parameter_context() { + let callee = Ty::from_inferred(&InferredType::Callable(crate::types::CallableInfo { + param_types: vec![InferredType::List(Box::new(InferredType::Int))], + return_type: Box::new(InferredType::Bool), + })); + let globals: HashMap = [("f".to_owned(), callee)].into_iter().collect(); + + let (ret, ok) = synth_and_solve("f([1, 2])", globals.clone()); + assert!(ok.errors.is_empty(), "{:?}", ok.errors); + assert_eq!(ret, InferredType::Bool); + + let (_, bad) = synth_and_solve("f([\"x\"])", globals); + assert_eq!(bad.errors.len(), 1, "{:?}", bad.errors); + } + + /// Comprehensions bind their targets from the iterable and check the + /// element expression against the expected element type. + #[test] + fn checked_comprehension_threads_element_expectation() { + let expected = InferredType::List(Box::new(InferredType::Int)); + let ok = check_and_solve("[x for x in [1, 2]]", &expected); + assert!(ok.errors.is_empty(), "{:?}", ok.errors); + + let bad = check_and_solve("[\"s\" for x in [1, 2]]", &expected); + assert_eq!(bad.errors.len(), 1, "{:?}", bad.errors); + } + + /// Every expression node synthesizes *something* without panicking — + /// unsupported shapes stay `Unknown` ([TYPEINF-EXCEEDS-NOUNKNOWN]). + #[test] + fn synth_is_total_over_expression_nodes() { + let sources = [ + "await x", + "x.attr", + "x[0]", + "x[0:2]", + "(yield)", + "(*x, 1)", + "{**base, \"k\": 1}", + "f\"{x}!\"", + "x if x else y", + "(n := 3)", + "not x", + "-2", + "a @ b", + "{i: str(i) for i in [1]}", + "(i for i in [1])", + "...", + ]; + for source in sources { + let (inferred, solution) = synth_and_solve(source, HashMap::new()); + assert!( + solution.errors.is_empty(), + "{source}: unexpected errors {:?}", + solution.errors + ); + let _ = inferred; + } + } + + /// A ternary in checked position checks both branches; a mismatched + /// branch is caught even when the other satisfies the expectation. + #[test] + fn ternary_checks_both_branches() { + let expected = InferredType::Int; + let bad = check_and_solve("1 if c else \"x\"", &expected); + assert_eq!(bad.errors.len(), 1, "{:?}", bad.errors); + } + + /// Walrus in checked position: value checks against the expectation and + /// the bound name is visible afterwards (within one expression). + #[test] + fn walrus_checks_value_against_expectation() { + let bad = check_and_solve("(n := \"s\")", &InferredType::Int); + assert_eq!(bad.errors.len(), 1, "{:?}", bad.errors); + } + + /// Union expectations route a literal to its single matching alternative. + #[test] + fn union_expectation_picks_the_structural_match() { + let expected = InferredType::Union(vec![ + InferredType::List(Box::new(InferredType::Int)), + InferredType::None_, + ]); + let ok = check_and_solve("[1]", &expected); + assert!(ok.errors.is_empty(), "{:?}", ok.errors); + + let bad = check_and_solve("[\"x\"]", &expected); + assert_eq!(bad.errors.len(), 1, "{:?}", bad.errors); + } + + /// Empty containers satisfy any element expectation — no fabricated + /// errors from `list[Never]` ([TYPEINF-TARGET-GRADUAL]). + #[test] + fn empty_containers_never_error() { + for (source, expected) in [ + ("[]", InferredType::List(Box::new(InferredType::Int))), + ( + "{}", + InferredType::Dict(Box::new(InferredType::Str), Box::new(InferredType::Int)), + ), + ] { + let solution = check_and_solve(source, &expected); + assert!( + solution.errors.is_empty(), + "{source}: {:?}", + solution.errors + ); + } + } + + /// `Expr` coverage guard: `check` accepts every node kind without + /// panicking (falling back to synthesis where no rule applies). + #[test] + fn check_is_total_over_expression_nodes() { + let sources = [ + "x.attr", + "x[i]", + "await x", + "lambda: 1", + "{1, 2}", + "(1, 2)", + "{\"k\": 1}", + "b\"raw\"", + "1.5", + "True", + "None", + "...", + ]; + for source in sources { + let module = parse_expr(source); + let mut engine = BidirEngine::new(HashMap::new()); + engine.check(&module.body, &Ty::any()); + let solution = engine.finish(); + assert!( + solution.errors.is_empty(), + "{source} vs Any: {:?}", + solution.errors + ); + } + } + + /// The homogeneous `tuple[int, ...]` expectation checks every element. + #[test] + fn homogeneous_tuple_expectation() { + let expected = InferredType::Tuple(vec![ + InferredType::Int, + InferredType::Named("...".to_owned()), + ]); + let ok = check_and_solve("(1, 2, 3)", &expected); + assert!(ok.errors.is_empty(), "{:?}", ok.errors); + + let bad = check_and_solve("(1, \"x\")", &expected); + assert_eq!(bad.errors.len(), 1, "{:?}", bad.errors); + } +} + +#[cfg(test)] +mod expression_inference_tests { + #![expect( + clippy::expect_used, + reason = "test-only parsing of fixed, known-valid expression fixtures" + )] + + use std::collections::HashMap; + + use crate::types::{InferredType, LiteralValue}; + + use super::{BidirEngine, Ty}; + + fn synth(source: &str, globals: HashMap) -> InferredType { + let module = ruff_python_parser::parse_expression(source) + .map(ruff_python_parser::Parsed::into_syntax) + .expect("test expression must parse"); + let mut engine = BidirEngine::new(globals); + let ty = engine.synth(&module.body); + let solution = engine.finish(); + ty.to_inferred(&solution.vars) + } + + /// Builtin calls answer from the centralized table + /// ([NARROWPLAN-CHECKLIST]: no rule-local string tables). + #[test] + fn builtin_calls_use_the_central_table() { + assert_eq!(synth("len(x)", HashMap::new()), InferredType::Int); + assert_eq!(synth("str(3)", HashMap::new()), InferredType::Str); + assert_eq!( + synth("isinstance(x, int)", HashMap::new()), + InferredType::Bool + ); + assert_eq!( + synth("sorted(items)", HashMap::new()), + InferredType::List(Box::new(InferredType::Unknown)) + ); + } + + /// Builtin method calls infer from the receiver's synthesized type. + #[test] + fn builtin_methods_infer_from_receiver() { + assert_eq!( + synth("\"a b\".split()", HashMap::new()), + InferredType::List(Box::new(InferredType::Str)) + ); + assert_eq!(synth("\"x\".upper()", HashMap::new()), InferredType::Str); + assert_eq!(synth("[1, 2].count(1)", HashMap::new()), InferredType::Int); + let globals: HashMap = [( + "table".to_owned(), + Ty::from_inferred(&InferredType::Dict( + Box::new(InferredType::Str), + Box::new(InferredType::Int), + )), + )] + .into_iter() + .collect(); + assert_eq!( + synth("table.get(key)", globals), + InferredType::Optional(Box::new(InferredType::Int)) + ); + } + + /// Constructor calls on user classes yield the class instance type. + #[test] + fn constructor_calls_yield_instances() { + let globals: HashMap = [( + "Point".to_owned(), + Ty::Ground(InferredType::Named("point".to_owned())), + )] + .into_iter() + .collect(); + assert_eq!( + synth("Point(1, 2)", globals), + InferredType::Named("point".to_owned()) + ); + } + + /// Subscript inference: list element, dict value, tuple position, str. + #[test] + fn subscripts_extract_element_types() { + let globals: HashMap = [ + ( + "xs".to_owned(), + Ty::from_inferred(&InferredType::List(Box::new(InferredType::Int))), + ), + ( + "d".to_owned(), + Ty::from_inferred(&InferredType::Dict( + Box::new(InferredType::Str), + Box::new(InferredType::Float), + )), + ), + ( + "pair".to_owned(), + Ty::from_inferred(&InferredType::Tuple(vec![ + InferredType::Int, + InferredType::Str, + ])), + ), + ] + .into_iter() + .collect(); + assert_eq!(synth("xs[0]", globals.clone()), InferredType::Int); + assert_eq!(synth("d[k]", globals.clone()), InferredType::Float); + assert_eq!(synth("pair[1]", globals.clone()), InferredType::Str); + assert_eq!( + synth("xs[1:2]", globals), + InferredType::List(Box::new(InferredType::Int)), + "slicing a list yields the list type" + ); + assert_eq!( + synth("[1][0]", HashMap::new()), + InferredType::Literal(LiteralValue::Int(1)), + "literal list subscript keeps deferred-generalization precision" + ); + } +} diff --git a/crates/basilisk-checker/src/bidir/solve.rs b/crates/basilisk-checker/src/bidir/solve.rs new file mode 100644 index 00000000..d891b623 --- /dev/null +++ b/crates/basilisk-checker/src/bidir/solve.rs @@ -0,0 +1,401 @@ +//! Implements [TYPEINF-TARGET-CONSTRAINTS]. See docs/specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-TARGET-CONSTRAINTS +//! The subtype-constraint solver — stage two of the two-stage architecture. +//! +//! A worklist decomposes structural constraints (`list[A] <: list[B]` → +//! `A <: B`), routes variable constraints into bound updates on the +//! [`TyVarStore`], and discharges every ground leaf through the single +//! existing subtyping authority, [`InferredType::is_assignable_to`] +//! ([TYPEINF-SUBTYPING-IMPL]) — the solver never re-implements +//! assignability. Constraints whose shape still involves variables after +//! decomposition are re-verified on the resolved projections at the end. + +use crate::types::InferredType; + +use super::constraints::{Constraint, ConstraintReason}; +use super::ty::Ty; +use super::tyvar::TyVarStore; +use ruff_text_size::TextRange; + +/// Backstop against pathological propagation chains. Generous: real modules +/// produce constraint counts linear in expression count, far below this. +const SOLVE_FUEL: usize = 100_000; + +/// One constraint the solver could not satisfy. +#[derive(Debug, Clone, PartialEq)] +pub struct SolveError { + /// The unsatisfied obligation, projected to ground types for display. + pub sub: InferredType, + /// The expected side of the failed obligation. + pub sup: InferredType, + /// Source range of the offending expression. + pub range: TextRange, + /// Why the obligation existed. + pub reason: ConstraintReason, +} + +/// The outcome of a solve: the variable store with its accumulated bounds +/// (consulted for deferred generalization) plus any unsatisfied constraints. +#[derive(Debug)] +pub struct Solution { + /// Variable bounds accumulated during solving. + pub vars: TyVarStore, + /// Constraints that could not be satisfied. + pub errors: Vec, +} + +/// Solve a constraint set against the variables the generation pass created. +/// +/// Gradual-guarantee posture ([TYPEINF-TARGET-GRADUAL]): running out of fuel +/// or meeting an undecomposable-but-variable-bearing shape never invents an +/// error — residual obligations are re-checked on ground projections, and +/// anything still uncertain resolves through `Unknown`, which +/// [`InferredType::is_assignable_to`] treats as compatible. +#[must_use] +pub fn solve(mut vars: TyVarStore, constraints: Vec) -> Solution { + let mut worklist: Vec = constraints; + let mut residual: Vec = Vec::new(); + let mut errors: Vec = Vec::new(); + let mut fuel = SOLVE_FUEL; + + while let Some(constraint) = worklist.pop() { + if fuel == 0 { + residual.push(constraint); + residual.append(&mut worklist); + break; + } + fuel -= 1; + step( + constraint, + &mut vars, + &mut worklist, + &mut residual, + &mut errors, + ); + } + verify_residual(&vars, residual, &mut errors); + Solution { vars, errors } +} + +/// Process one constraint: update bounds, decompose, or leaf-check. +fn step( + constraint: Constraint, + vars: &mut TyVarStore, + worklist: &mut Vec, + residual: &mut Vec, + errors: &mut Vec, +) { + let Constraint { + sub, + sup, + range, + reason, + } = constraint; + match (sub, sup) { + (Ty::Var(v), sup) => bound_var_above(v, &sup, range, reason, vars, worklist), + (sub, Ty::Var(v)) => bound_var_below(v, &sub, range, reason, vars, worklist), + (sub, sup) if !sub.contains_var() && !sup.contains_var() => { + leaf_check(&sub, &sup, range, reason, vars, errors); + } + (sub, sup) => decompose(sub, sup, range, reason, worklist, residual), + } +} + +/// `Var <: sup`: record the upper bound and propagate to existing lowers. +fn bound_var_above( + var: super::tyvar::TyVarId, + sup: &Ty, + range: TextRange, + reason: ConstraintReason, + vars: &mut TyVarStore, + worklist: &mut Vec, +) { + let lowers: Vec = vars + .get(var) + .map(|data| data.lower.clone()) + .unwrap_or_default(); + vars.add_upper(var, sup.clone()); + for lower in lowers { + worklist.push(Constraint { + sub: lower, + sup: sup.clone(), + range, + reason, + }); + } +} + +/// `sub <: Var`: record the lower bound and propagate to existing uppers. +fn bound_var_below( + var: super::tyvar::TyVarId, + sub: &Ty, + range: TextRange, + reason: ConstraintReason, + vars: &mut TyVarStore, + worklist: &mut Vec, +) { + let uppers: Vec = vars + .get(var) + .map(|data| data.upper.clone()) + .unwrap_or_default(); + vars.add_lower(var, sub.clone()); + for upper in uppers { + worklist.push(Constraint { + sub: sub.clone(), + sup: upper, + range, + reason, + }); + } +} + +/// Split a structural pair into element constraints; shapes the solver does +/// not decompose defer to the residual ground re-check. +fn decompose( + sub: Ty, + sup: Ty, + range: TextRange, + reason: ConstraintReason, + worklist: &mut Vec, + residual: &mut Vec, +) { + match (sub, sup) { + (Ty::List(a), Ty::List(b)) | (Ty::Set(a), Ty::Set(b)) => { + worklist.push(Constraint { + sub: *a, + sup: *b, + range, + reason, + }); + } + (Ty::Dict(ka, va), Ty::Dict(kb, vb)) => { + worklist.push(Constraint { + sub: *ka, + sup: *kb, + range, + reason, + }); + worklist.push(Constraint { + sub: *va, + sup: *vb, + range, + reason, + }); + } + (Ty::Tuple(given), Ty::Tuple(wanted)) if given.len() == wanted.len() => { + for (a, b) in given.into_iter().zip(wanted) { + worklist.push(Constraint { + sub: a, + sup: b, + range, + reason, + }); + } + } + (Ty::Union(alts), sup) => { + for alt in alts { + worklist.push(Constraint { + sub: alt, + sup: sup.clone(), + range, + reason, + }); + } + } + (sub, sup) => push_callable_or_residual(sub, sup, range, reason, worklist, residual), + } +} + +/// Callable variance split; anything else joins the residual set. +fn push_callable_or_residual( + sub: Ty, + sup: Ty, + range: TextRange, + reason: ConstraintReason, + worklist: &mut Vec, + residual: &mut Vec, +) { + match (sub, sup) { + (Ty::Callable(given_params, given_ret), Ty::Callable(wanted_params, wanted_ret)) + if given_params.len() == wanted_params.len() + || given_params.is_empty() + || wanted_params.is_empty() => + { + // Contravariant parameters — skipped entirely when either side is + // the gradual `Callable[..., R]` (empty parameter list), matching + // `is_assignable_to`'s treatment. + for (given, wanted) in given_params.into_iter().zip(wanted_params) { + worklist.push(Constraint { + sub: wanted, + sup: given, + range, + reason, + }); + } + // Covariant return. + worklist.push(Constraint { + sub: *given_ret, + sup: *wanted_ret, + range, + reason, + }); + } + (sub, sup) => residual.push(Constraint { + sub, + sup, + range, + reason, + }), + } +} + +/// Ground leaf: delegate to the single subtyping authority. +fn leaf_check( + sub: &Ty, + sup: &Ty, + range: TextRange, + reason: ConstraintReason, + vars: &TyVarStore, + errors: &mut Vec, +) { + let actual = sub.to_inferred(vars); + let expected = sup.to_inferred(vars); + if !actual.is_assignable_to(&expected) { + errors.push(SolveError { + sub: actual, + sup: expected, + range, + reason, + }); + } +} + +/// Re-verify undecomposed constraints on their ground projections after all +/// bounds settled — variables resolve through deferred generalization first. +fn verify_residual(vars: &TyVarStore, residual: Vec, errors: &mut Vec) { + for constraint in residual { + let actual = constraint.sub.to_inferred(vars); + let expected = constraint.sup.to_inferred(vars); + if !actual.is_assignable_to(&expected) { + errors.push(SolveError { + sub: actual, + sup: expected, + range: constraint.range, + reason: constraint.reason, + }); + } + } +} + +#[cfg(test)] +mod tests { + use super::super::tyvar::Polarity; + use super::*; + use crate::types::LiteralValue; + + fn range() -> TextRange { + TextRange::default() + } + + /// [TYPEINF-TARGET-CONSTRAINTS]: `list[Literal[1]] <: list[int]` + /// decomposes and passes through the numeric tower. + #[test] + fn structural_decomposition_reaches_ground_leaves() { + let mut set = super::super::constraints::ConstraintSet::default(); + set.push( + Ty::List(Box::new(Ty::Ground(InferredType::Literal( + LiteralValue::Int(1), + )))), + Ty::List(Box::new(Ty::Ground(InferredType::Int))), + range(), + ConstraintReason::ExpectedType, + ); + let solution = solve(TyVarStore::default(), set.into_vec()); + assert!(solution.errors.is_empty(), "{:?}", solution.errors); + } + + /// An impossible ground constraint surfaces exactly one error. + #[test] + fn ground_mismatch_is_reported() { + let mut set = super::super::constraints::ConstraintSet::default(); + set.push( + Ty::Ground(InferredType::Str), + Ty::Ground(InferredType::Int), + range(), + ConstraintReason::ExpectedType, + ); + let solution = solve(TyVarStore::default(), set.into_vec()); + assert_eq!(solution.errors.len(), 1); + } + + /// `Literal[1] <: Var` then `Var <: int` propagates: the literal bound is + /// checked against the later upper bound, and the var still resolves to + /// the precise `Literal[1]` (deferred generalization). + #[test] + fn var_bounds_propagate_and_stay_precise() { + let mut vars = TyVarStore::default(); + let var = vars.fresh(Polarity::Output); + let mut set = super::super::constraints::ConstraintSet::default(); + set.push( + Ty::Ground(InferredType::Literal(LiteralValue::Int(1))), + Ty::Var(var), + range(), + ConstraintReason::CollectionElement, + ); + set.push( + Ty::Var(var), + Ty::Ground(InferredType::Int), + range(), + ConstraintReason::ExpectedType, + ); + let solution = solve(vars, set.into_vec()); + assert!(solution.errors.is_empty(), "{:?}", solution.errors); + assert_eq!( + solution.vars.resolve(var), + InferredType::Literal(LiteralValue::Int(1)), + "generalization stays deferred: Literal[1] is not widened to int" + ); + } + + /// A var bounded below by `str` and above by `int` is unsatisfiable. + #[test] + fn conflicting_var_bounds_error() { + let mut vars = TyVarStore::default(); + let var = vars.fresh(Polarity::Output); + let mut set = super::super::constraints::ConstraintSet::default(); + set.push( + Ty::Ground(InferredType::Str), + Ty::Var(var), + range(), + ConstraintReason::CollectionElement, + ); + set.push( + Ty::Var(var), + Ty::Ground(InferredType::Int), + range(), + ConstraintReason::ExpectedType, + ); + let solution = solve(vars, set.into_vec()); + assert_eq!(solution.errors.len(), 1, "{:?}", solution.errors); + } + + /// Callable parameters check contravariantly, returns covariantly. + #[test] + fn callable_variance() { + let sub = Ty::Callable( + vec![Ty::Ground(InferredType::Float)], + Box::new(Ty::Ground(InferredType::Bool)), + ); + let sup = Ty::Callable( + vec![Ty::Ground(InferredType::Int)], + Box::new(Ty::Ground(InferredType::Int)), + ); + let mut set = super::super::constraints::ConstraintSet::default(); + set.push(sub, sup, range(), ConstraintReason::CallArgument); + let solution = solve(TyVarStore::default(), set.into_vec()); + assert!( + solution.errors.is_empty(), + "(float)->bool <: (int)->int must hold: {:?}", + solution.errors + ); + } +} diff --git a/crates/basilisk-checker/src/bidir/ty.rs b/crates/basilisk-checker/src/bidir/ty.rs new file mode 100644 index 00000000..5a36f745 --- /dev/null +++ b/crates/basilisk-checker/src/bidir/ty.rs @@ -0,0 +1,169 @@ +//! Implements [TYPEINF-TARGET-CONSTRAINTS]. See docs/specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-TARGET-CONSTRAINTS +//! The solver-internal type language for the bidirectional engine. +//! +//! [`Ty`] is the constraint solver's view of a type: the existing +//! [`InferredType`] with structure lifted (so containers decompose during +//! solving) plus type variables ([`Ty::Var`]). Ground leaves stay as +//! [`InferredType`] so every leaf subtyping decision delegates to the single +//! existing authority, [`InferredType::is_assignable_to`] — the solver never +//! re-implements assignability. + +use crate::types::{CallableInfo, InferredType}; + +use super::tyvar::{TyVarId, TyVarStore}; + +/// A type as seen by the constraint solver. +/// +/// Structural containers are explicit so `check`/`solve` can decompose them +/// against expected types with variables inside; everything without inner +/// structure stays a [`Ty::Ground`] leaf. +#[derive(Debug, Clone, PartialEq)] +pub enum Ty { + /// A fully-known type with no solver structure and no variables inside. + Ground(InferredType), + /// A bounded, polar type variable — see [`super::tyvar::TyVarStore`]. + Var(TyVarId), + /// `list[T]` with a solver-visible element type. + List(Box), + /// `set[T]` with a solver-visible element type. + Set(Box), + /// `dict[K, V]` with solver-visible key/value types. + Dict(Box, Box), + /// `tuple[T1, ..., Tn]` with solver-visible element types. + Tuple(Vec), + /// A union of alternatives. + Union(Vec), + /// `Callable[[P1..Pn], R]`; an empty parameter list means the gradual + /// `Callable[..., R]` form, matching [`CallableInfo`]. + Callable(Vec, Box), +} + +impl Ty { + /// The `Any` escape hatch as a ground leaf. + #[must_use] + pub const fn any() -> Self { + Ty::Ground(InferredType::Any) + } + + /// The conservative `Unknown` ([TYPEINF-EXCEEDS-NOUNKNOWN]) ground leaf. + #[must_use] + pub const fn unknown() -> Self { + Ty::Ground(InferredType::Unknown) + } + + /// Lift an [`InferredType`] into the solver language, exposing container + /// structure so constraints can decompose it. `Optional[T]` lifts to + /// `Union[T, None]` so the solver has one union form to reason about. + #[must_use] + pub fn from_inferred(inferred: &InferredType) -> Self { + match inferred { + InferredType::List(elem) => Ty::List(Box::new(Self::from_inferred(elem))), + InferredType::Set(elem) => Ty::Set(Box::new(Self::from_inferred(elem))), + InferredType::Dict(key, value) => Ty::Dict( + Box::new(Self::from_inferred(key)), + Box::new(Self::from_inferred(value)), + ), + InferredType::Tuple(elems) => { + Ty::Tuple(elems.iter().map(Self::from_inferred).collect()) + } + InferredType::Union(alts) => Ty::Union(alts.iter().map(Self::from_inferred).collect()), + InferredType::Optional(inner) => Ty::Union(vec![ + Self::from_inferred(inner), + Ty::Ground(InferredType::None_), + ]), + InferredType::Callable(info) => Ty::Callable( + info.param_types.iter().map(Self::from_inferred).collect(), + Box::new(Self::from_inferred(&info.return_type)), + ), + ground => Ty::Ground(ground.clone()), + } + } + + /// Project back to an [`InferredType`], resolving every variable through + /// `vars` (deferred generalization happens here — see + /// [`TyVarStore::resolve`]). + #[must_use] + pub fn to_inferred(&self, vars: &TyVarStore) -> InferredType { + match self { + Ty::Ground(ground) => ground.clone(), + Ty::Var(id) => vars.resolve(*id), + Ty::List(elem) => InferredType::List(Box::new(elem.to_inferred(vars))), + Ty::Set(elem) => InferredType::Set(Box::new(elem.to_inferred(vars))), + Ty::Dict(key, value) => InferredType::Dict( + Box::new(key.to_inferred(vars)), + Box::new(value.to_inferred(vars)), + ), + Ty::Tuple(elems) => { + InferredType::Tuple(elems.iter().map(|e| e.to_inferred(vars)).collect()) + } + Ty::Union(alts) => alts + .iter() + .map(|a| a.to_inferred(vars)) + .fold(InferredType::Never, InferredType::union), + Ty::Callable(params, ret) => InferredType::Callable(CallableInfo { + param_types: params.iter().map(|p| p.to_inferred(vars)).collect(), + return_type: Box::new(ret.to_inferred(vars)), + }), + } + } + + /// Whether any [`Ty::Var`] occurs anywhere in this type. + #[must_use] + pub fn contains_var(&self) -> bool { + match self { + Ty::Ground(_) => false, + Ty::Var(_) => true, + Ty::List(elem) | Ty::Set(elem) => elem.contains_var(), + Ty::Dict(key, value) => key.contains_var() || value.contains_var(), + Ty::Tuple(elems) | Ty::Union(elems) => elems.iter().any(Ty::contains_var), + Ty::Callable(params, ret) => params.iter().any(Ty::contains_var) || ret.contains_var(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::LiteralValue; + + /// [TYPEINF-TARGET-CONSTRAINTS]: lifting exposes container structure and + /// projection inverts it exactly for var-free types. + #[test] + fn lift_then_project_round_trips_var_free_types() { + let cases = [ + InferredType::Int, + InferredType::List(Box::new(InferredType::Str)), + InferredType::Dict(Box::new(InferredType::Str), Box::new(InferredType::Int)), + InferredType::Tuple(vec![InferredType::Int, InferredType::Bool]), + InferredType::Union(vec![InferredType::Int, InferredType::Str]), + InferredType::Literal(LiteralValue::Int(1)), + InferredType::Callable(CallableInfo { + param_types: vec![InferredType::Int], + return_type: Box::new(InferredType::Bool), + }), + ]; + let vars = TyVarStore::default(); + for inferred in cases { + let lifted = Ty::from_inferred(&inferred); + assert!(!lifted.contains_var(), "{inferred:?} lifts without vars"); + assert_eq!(lifted.to_inferred(&vars), inferred, "round trip"); + } + } + + /// `Optional[T]` lifts to the solver's single union form and projects back + /// to a union containing `None`. + #[test] + fn optional_lifts_to_union_with_none() { + let lifted = Ty::from_inferred(&InferredType::Optional(Box::new(InferredType::Int))); + assert!( + matches!(&lifted, Ty::Union(alts) if alts.len() == 2), + "Optional must lift to a two-alternative Ty::Union, got {lifted:?}" + ); + let vars = TyVarStore::default(); + let projected = lifted.to_inferred(&vars); + assert!( + projected.is_assignable_to(&InferredType::Optional(Box::new(InferredType::Int))), + "projection stays Optional-compatible" + ); + } +} diff --git a/crates/basilisk-checker/src/bidir/tyvar.rs b/crates/basilisk-checker/src/bidir/tyvar.rs new file mode 100644 index 00000000..5ff470e0 --- /dev/null +++ b/crates/basilisk-checker/src/bidir/tyvar.rs @@ -0,0 +1,257 @@ +//! Implements [TYPEINF-TARGET-CONSTRAINTS]. See docs/specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-TARGET-CONSTRAINTS +//! Bounded, polar type variables with deferred generalization. +//! +//! Each variable carries explicit lower/upper bounds (Pyright's type-interval +//! idea, Pyrefly's `Var`) and an input/output polarity borrowed from Dolan's +//! algebraic subtyping and Parreaux's Simple-sub — deliberately **without** +//! full biunification. Generalization is deferred: a list literal `[1]` +//! synthesizes `list[Var{lower=Literal[1]}]`, and the variable settles only +//! when [`TyVarStore::resolve`] is forced by a constraining use, preserving +//! `Literal`/generic precision instead of eagerly widening `Literal[1] → int`. + +use crate::types::InferredType; + +use super::ty::Ty; + +/// Identifier of a type variable inside one [`TyVarStore`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct TyVarId(usize); + +impl TyVarId { + /// The variable's index into its store. + #[must_use] + pub fn index(self) -> usize { + self.0 + } +} + +/// The position a variable was introduced in, deciding how it resolves. +/// +/// An **output** (positive/covariant) variable resolves to the union of its +/// lower bounds — what flowed *into* it. An **input** (negative/contravariant) +/// variable resolves to its upper bounds — what is *demanded* of it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Polarity { + /// Positive position: results, container elements being produced. + Output, + /// Negative position: parameters, values being consumed. + Input, +} + +/// One variable's accumulated solving state. +#[derive(Debug, Clone)] +pub struct TyVarData { + /// Types constrained to flow into the variable (`τ <: Var`). + pub lower: Vec, + /// Types the variable must flow into (`Var <: τ`). + pub upper: Vec, + /// Introduction polarity — see [`Polarity`]. + pub polarity: Polarity, +} + +/// Allocation and resolution of type variables for one inference run. +#[derive(Debug, Clone, Default)] +pub struct TyVarStore { + vars: Vec, +} + +impl TyVarStore { + /// Allocate a fresh, unbounded variable with the given polarity. + pub fn fresh(&mut self, polarity: Polarity) -> TyVarId { + let id = self.vars.len(); + self.vars.push(TyVarData { + lower: Vec::new(), + upper: Vec::new(), + polarity, + }); + TyVarId(id) + } + + /// Record `ty <: var` — a new lower bound. Duplicates are dropped. + pub fn add_lower(&mut self, id: TyVarId, ty: Ty) { + if let Some(data) = self.vars.get_mut(id.index()) { + if !data.lower.contains(&ty) { + data.lower.push(ty); + } + } + } + + /// Record `var <: ty` — a new upper bound. Duplicates are dropped. + pub fn add_upper(&mut self, id: TyVarId, ty: Ty) { + if let Some(data) = self.vars.get_mut(id.index()) { + if !data.upper.contains(&ty) { + data.upper.push(ty); + } + } + } + + /// The variable's current state, if it exists in this store. + #[must_use] + pub fn get(&self, id: TyVarId) -> Option<&TyVarData> { + self.vars.get(id.index()) + } + + /// Number of variables allocated so far. + #[must_use] + pub fn len(&self) -> usize { + self.vars.len() + } + + /// Whether no variables have been allocated. + #[must_use] + pub fn is_empty(&self) -> bool { + self.vars.is_empty() + } + + /// Resolve a variable to a concrete [`InferredType`] — the **deferred + /// generalization** point ([TYPEINF-TARGET-CONSTRAINTS]). + /// + /// - Output polarity: the union of the lower bounds (what was produced) — + /// `Var{lower=Literal[1]}` resolves to `Literal[1]`, not `int`. + /// - Input polarity: the first upper bound (what is demanded), since an + /// unconstrained parameter accepts anything. + /// - No bounds at all: the conservative `Unknown` + /// ([TYPEINF-EXCEEDS-NOUNKNOWN]) so downstream rules stay silent — + /// never a guess. + /// + /// Cycles between variables (a bound mentioning the variable being + /// resolved) collapse to `Unknown`, the Stage 0 stand-in for the + /// divergent sentinel planned in [TYPEINF-TARGET-INCREMENTAL]. + #[must_use] + pub fn resolve(&self, id: TyVarId) -> InferredType { + self.resolve_guarded(id, &mut Vec::new()) + } + + fn resolve_guarded(&self, id: TyVarId, visiting: &mut Vec) -> InferredType { + if visiting.contains(&id) { + return InferredType::Unknown; + } + let Some(data) = self.vars.get(id.index()) else { + return InferredType::Unknown; + }; + visiting.push(id); + let resolved = match data.polarity { + Polarity::Output => self.union_of(&data.lower, visiting), + Polarity::Input => self.first_bound(&data.upper, visiting), + }; + let _ = visiting.pop(); + resolved + } + + /// Union of the given bounds, or `Unknown` when there are none. + fn union_of(&self, bounds: &[Ty], visiting: &mut Vec) -> InferredType { + if bounds.is_empty() { + return InferredType::Unknown; + } + bounds + .iter() + .map(|bound| self.project(bound, visiting)) + .fold(InferredType::Never, InferredType::union) + } + + /// First demanded bound, or `Unknown` when nothing is demanded. + fn first_bound(&self, bounds: &[Ty], visiting: &mut Vec) -> InferredType { + bounds + .first() + .map_or(InferredType::Unknown, |bound| self.project(bound, visiting)) + } + + /// Project a bound to ground, resolving nested variables under the cycle + /// guard rather than through [`Ty::to_inferred`] (which would restart an + /// unguarded resolution). + fn project(&self, bound: &Ty, visiting: &mut Vec) -> InferredType { + match bound { + Ty::Var(inner) => self.resolve_guarded(*inner, visiting), + Ty::List(elem) => InferredType::List(Box::new(self.project(elem, visiting))), + Ty::Set(elem) => InferredType::Set(Box::new(self.project(elem, visiting))), + Ty::Dict(key, value) => InferredType::Dict( + Box::new(self.project(key, visiting)), + Box::new(self.project(value, visiting)), + ), + Ty::Tuple(elems) => { + InferredType::Tuple(elems.iter().map(|e| self.project(e, visiting)).collect()) + } + Ty::Union(alts) => alts + .iter() + .map(|a| self.project(a, visiting)) + .fold(InferredType::Never, InferredType::union), + Ty::Callable(params, ret) => InferredType::Callable(crate::types::CallableInfo { + param_types: params.iter().map(|p| self.project(p, visiting)).collect(), + return_type: Box::new(self.project(ret, visiting)), + }), + Ty::Ground(ground) => ground.clone(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::LiteralValue; + + /// [TYPEINF-TARGET-CONSTRAINTS]: deferred generalization — an output var + /// whose only lower bound is `Literal[1]` resolves to `Literal[1]`, never + /// eagerly widened to `int`. + #[test] + fn output_var_preserves_literal_precision() { + let mut store = TyVarStore::default(); + let var = store.fresh(Polarity::Output); + store.add_lower(var, Ty::Ground(InferredType::Literal(LiteralValue::Int(1)))); + assert_eq!( + store.resolve(var), + InferredType::Literal(LiteralValue::Int(1)) + ); + } + + /// Multiple lower bounds union (`[1, "a"]` → `Literal[1] | Literal["a"]`). + #[test] + fn output_var_unions_lower_bounds() { + let mut store = TyVarStore::default(); + let var = store.fresh(Polarity::Output); + store.add_lower(var, Ty::Ground(InferredType::Int)); + store.add_lower(var, Ty::Ground(InferredType::Str)); + let resolved = store.resolve(var); + assert!(InferredType::Int.is_assignable_to(&resolved)); + assert!(InferredType::Str.is_assignable_to(&resolved)); + } + + /// An input var resolves to what is demanded of it; unconstrained vars of + /// either polarity stay `Unknown` — no guessing + /// ([TYPEINF-EXCEEDS-NOUNKNOWN]). + #[test] + fn input_var_takes_upper_bound_and_unbounded_stays_unknown() { + let mut store = TyVarStore::default(); + let input = store.fresh(Polarity::Input); + store.add_upper(input, Ty::Ground(InferredType::Int)); + assert_eq!(store.resolve(input), InferredType::Int); + + let unbounded = store.fresh(Polarity::Output); + assert_eq!(store.resolve(unbounded), InferredType::Unknown); + } + + /// Mutually-referential variables terminate and collapse to `Unknown` + /// instead of recursing forever. + #[test] + fn cyclic_bounds_resolve_to_unknown() { + let mut store = TyVarStore::default(); + let a = store.fresh(Polarity::Output); + let b = store.fresh(Polarity::Output); + store.add_lower(a, Ty::Var(b)); + store.add_lower(b, Ty::Var(a)); + assert_eq!(store.resolve(a), InferredType::Unknown); + } + + /// A structural bound (`list[Var]`) resolves through the nested variable. + #[test] + fn structural_bounds_project_nested_vars() { + let mut store = TyVarStore::default(); + let elem = store.fresh(Polarity::Output); + store.add_lower(elem, Ty::Ground(InferredType::Bool)); + let outer = store.fresh(Polarity::Output); + store.add_lower(outer, Ty::List(Box::new(Ty::Var(elem)))); + assert_eq!( + store.resolve(outer), + InferredType::List(Box::new(InferredType::Bool)) + ); + } +} diff --git a/crates/basilisk-checker/src/incremental_defs.rs b/crates/basilisk-checker/src/incremental_defs.rs new file mode 100644 index 00000000..69fb05fa --- /dev/null +++ b/crates/basilisk-checker/src/incremental_defs.rs @@ -0,0 +1,557 @@ +//! Implements [TYPEINF-TARGET-INCREMENTAL]. See docs/specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-TARGET-INCREMENTAL +//! Definition-level and expression-level Salsa queries — Stage 1 of +//! [NARROWPLAN-CHECKLIST](../../docs/plans/CHECKER-TYPE-NARROWING-INFERENCE-PLAN.md). +//! +//! The file-level queries in [`crate::incremental`] re-check a whole module on +//! any keystroke. This module adds ty-style granularity underneath them: +//! +//! - [`definitions`] parses a file once and creates one **tracked struct** per +//! top-level definition, carrying the definition's own source *slice* (not +//! offsets — an edit that only shifts a definition leaves its slice, and so +//! every dependent memo, untouched); +//! - [`definition_type`] infers one definition's public type from its slice +//! alone — the per-definition unit of re-execution, with **fixpoint cycle +//! recovery** seeded by the divergent/bottom sentinel (`Unknown`) and a hard +//! iteration cap; +//! - [`expression_types`] is the expression-level query: per-expression +//! inferred types (assignment right-hand sides and `return` values) within +//! one definition, for hover/inlay/diagnostic reuse; +//! - [`module_interface`] folds the per-definition types into a compact, +//! `PartialEq` interface — the cross-file dependency boundary: a body-only +//! edit backdates to "interface unchanged" and importers' memos survive. +#![expect( + missing_docs, + reason = "salsa macros generate undocumentable public constructors/accessors/setters" +)] + +use basilisk_db::{Db, SourceFile}; +use ruff_python_ast::{Expr, Stmt}; +use ruff_text_size::Ranged; + +use crate::bidir::BidirEngine; +use crate::types::InferredType; + +/// Hard cap on fixpoint iterations for cyclic definitions. Reached only by +/// pathological oscillation; normal cycles converge in one or two rounds. +const CYCLE_ITERATION_CAP: u32 = 16; + +/// What kind of top-level definition a [`Definition`] is. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum DefKind { + /// A `def`/`async def` at module level. + Function, + /// A `class` at module level. + Class, + /// A module-level assignment (`x = ...` / `x: T = ...`). + Variable, +} + +/// One top-level definition, tracked so downstream queries key on it. +/// +/// `source` is the definition's own text slice: two revisions in which the +/// slice is byte-identical produce an "unchanged" tracked struct, so +/// [`definition_type`]/[`expression_types`] memos survive edits elsewhere in +/// the file — the definition-level early cutoff. +#[salsa::tracked(debug)] +pub struct Definition<'db> { + /// The file this definition belongs to. + pub file: SourceFile, + /// The defined name. + #[returns(ref)] + pub name: String, + /// Definition kind. + pub kind: DefKind, + /// The definition's own source slice (decorators included). + #[returns(ref)] + pub source: String, +} + +/// Tracked query: the top-level definitions of one file. +/// +/// The single whole-file parse per revision lives here; everything downstream +/// is per-definition. +#[salsa::tracked(returns(ref))] +pub fn definitions(db: &dyn Db, file: SourceFile) -> Vec> { + let source = file.text(db); + let Ok(parsed) = ruff_python_parser::parse_module(source) else { + return Vec::new(); + }; + parsed + .syntax() + .body + .iter() + .filter_map(|stmt| create_definition(db, file, source, stmt)) + .collect() +} + +/// Create the tracked struct for one top-level statement, if it defines a name. +fn create_definition<'db>( + db: &'db dyn Db, + file: SourceFile, + source: &str, + stmt: &Stmt, +) -> Option> { + let (name, kind, range) = match stmt { + Stmt::FunctionDef(def) => (def.name.to_string(), DefKind::Function, def.range()), + Stmt::ClassDef(def) => (def.name.to_string(), DefKind::Class, def.range()), + Stmt::Assign(assign) => match assign.targets.as_slice() { + [Expr::Name(target)] => (target.id.to_string(), DefKind::Variable, assign.range()), + _ => return None, + }, + Stmt::AnnAssign(assign) => match assign.target.as_ref() { + Expr::Name(target) => (target.id.to_string(), DefKind::Variable, assign.range()), + _ => return None, + }, + _ => return None, + }; + let slice = source + .get(usize::from(range.start())..usize::from(range.end()))? + .to_owned(); + Some(Definition::new(db, file, name, kind, slice)) +} + +/// Tracked query with **fixpoint cycle recovery**: the public type of one +/// definition, inferred from its slice alone. +/// +/// A definition whose right-hand side is a bare name reference resolves it +/// through its siblings' [`definition_type`] — mutually-referential +/// definitions therefore cycle, and salsa iterates from the divergent/bottom +/// sentinel ([`cycle_initial`] = `Unknown`, [TYPEINF-EXCEEDS-NOUNKNOWN]) +/// until the types stabilise or [`CYCLE_ITERATION_CAP`] falls back to the +/// sentinel. +#[salsa::tracked(cycle_fn = definition_type_cycle_recover, cycle_initial = definition_type_cycle_initial)] +pub fn definition_type<'db>(db: &'db dyn Db, def: Definition<'db>) -> InferredType { + match def.kind(db) { + DefKind::Function => function_type(def.source(db)), + DefKind::Class => InferredType::Named(def.name(db).to_ascii_lowercase()), + DefKind::Variable => variable_type(db, def), + } +} + +/// Divergent/bottom seed for cyclic definition inference. +fn definition_type_cycle_initial<'db>( + _db: &'db dyn Db, + _id: salsa::Id, + _def: Definition<'db>, +) -> InferredType { + InferredType::Unknown +} + +/// Fixpoint step: converged when the newly computed value matches the last +/// provisional one; past the hard cap, settle on the divergent sentinel. +fn definition_type_cycle_recover<'db>( + _db: &'db dyn Db, + cycle: &salsa::Cycle<'_>, + last_provisional: &InferredType, + value: InferredType, + _def: Definition<'db>, +) -> InferredType { + if &value == last_provisional { + value + } else if cycle.iteration() >= CYCLE_ITERATION_CAP { + InferredType::Unknown + } else { + value + } +} + +/// The `Callable` surface of a function definition's slice: declared +/// annotations where present, with an UNANNOTATED return synthesized from +/// the body's `return` expressions — same-module return inference +/// ([NARROWPLAN-CHECKLIST] Stage 2, expression inference). +fn function_type(slice: &str) -> InferredType { + let Ok(parsed) = ruff_python_parser::parse_module(slice) else { + return InferredType::Unknown; + }; + let Some(Stmt::FunctionDef(def)) = parsed.syntax().body.first() else { + return InferredType::Unknown; + }; + let param_types = def + .parameters + .iter() + .map(|param| annotation_type(slice, param.annotation())) + .collect(); + let return_type = match def.returns.as_deref() { + Some(annotation) => annotation_type(slice, Some(annotation)), + None => synthesized_return_type(&def.body), + }; + InferredType::Callable(crate::types::CallableInfo { + param_types, + return_type: Box::new(return_type), + }) +} + +/// Synthesize an unannotated function's return type: the union of its +/// `return` expression types through the bidirectional engine, plus `None` +/// when a bare `return` exists or the body can fall through. +/// +/// Gradual-guarantee note ([TYPEINF-TARGET-GRADUAL]): this type is inferred +/// FROM unannotated code, so when rules consume it (Integration stage) it is +/// display/assist-grade — it must never be enforced as if the user declared +/// it, or removing a return annotation could introduce new errors. The +/// differential harness (`tests/gradual_guarantee_tests.rs`) pins the +/// behavioral invariant. +fn synthesized_return_type(body: &[Stmt]) -> InferredType { + let mut returns = Vec::new(); + let mut has_bare_return = false; + collect_return_exprs(body, &mut returns, &mut has_bare_return); + if returns.is_empty() && !has_bare_return { + // No return statement at all: the function returns None. + return InferredType::None_; + } + let mut engine = BidirEngine::new(std::collections::HashMap::new()); + let types: Vec = returns.iter().map(|expr| engine.synth(expr)).collect(); + let solution = engine.finish(); + let mut result = types + .into_iter() + .map(|ty| ty.to_inferred(&solution.vars)) + .fold(InferredType::Never, InferredType::union); + // Approximate fall-through: unless the body's last statement diverges, + // an implicit `return None` path exists. Conservative in the sound + // direction — a spurious `| None` widens, never fabricates precision. + let last_diverges = matches!(body.last(), Some(Stmt::Return(_) | Stmt::Raise(_))); + if has_bare_return || !last_diverges { + result = InferredType::union(result, InferredType::None_); + } + result +} + +/// Collect `return` expressions (and bare-`return` presence) recursively, +/// stopping at nested function/class boundaries. +fn collect_return_exprs<'a>(stmts: &'a [Stmt], out: &mut Vec<&'a Expr>, bare: &mut bool) { + for stmt in stmts { + match stmt { + Stmt::Return(ret) => match ret.value.as_deref() { + Some(value) => out.push(value), + None => *bare = true, + }, + Stmt::If(node) => { + collect_return_exprs(&node.body, out, bare); + for clause in &node.elif_else_clauses { + collect_return_exprs(&clause.body, out, bare); + } + } + Stmt::For(node) => { + collect_return_exprs(&node.body, out, bare); + collect_return_exprs(&node.orelse, out, bare); + } + Stmt::While(node) => { + collect_return_exprs(&node.body, out, bare); + collect_return_exprs(&node.orelse, out, bare); + } + Stmt::With(node) => collect_return_exprs(&node.body, out, bare), + Stmt::Try(node) => { + collect_return_exprs(&node.body, out, bare); + for handler in &node.handlers { + let ruff_python_ast::ExceptHandler::ExceptHandler(h) = handler; + collect_return_exprs(&h.body, out, bare); + } + collect_return_exprs(&node.orelse, out, bare); + collect_return_exprs(&node.finalbody, out, bare); + } + Stmt::Match(node) => { + for case in &node.cases { + collect_return_exprs(&case.body, out, bare); + } + } + _ => {} + } + } +} + +/// Parse one annotation expression's text, `Unknown` when absent. +fn annotation_type(slice: &str, annotation: Option<&Expr>) -> InferredType { + annotation + .and_then(|expr| { + let range = expr.range(); + slice.get(usize::from(range.start())..usize::from(range.end())) + }) + .map_or(InferredType::Unknown, InferredType::from_annotation) +} + +/// The inferred type of a module-level variable definition. +/// +/// An explicit annotation wins; a bare-name right-hand side resolves through +/// the sibling definition's [`definition_type`] (the cycle edge); anything +/// else synthesizes through the bidirectional engine +/// ([TYPEINF-TARGET-BIDIRECTIONAL]). +fn variable_type<'db>(db: &'db dyn Db, def: Definition<'db>) -> InferredType { + let slice = def.source(db); + let Ok(parsed) = ruff_python_parser::parse_module(slice) else { + return InferredType::Unknown; + }; + match parsed.syntax().body.first() { + Some(Stmt::AnnAssign(assign)) => annotation_type(slice, Some(&assign.annotation)), + // A bare-name alias resolves its sibling directly (the definition + // cycle edge); any other right-hand side synthesizes with the + // module's callables in scope, so same-module call returns + // (`x = f()` → `f`'s declared-or-synthesized return) resolve. + Some(Stmt::Assign(assign)) => match assign.value.as_ref() { + Expr::Name(reference) => sibling_type(db, def, reference.id.as_str()), + value => synth_expression(db, def, value), + }, + _ => InferredType::Unknown, + } +} + +/// Resolve a bare-name right-hand side through the sibling definition — +/// the edge on which definition cycles (and their fixpoint) arise. +fn sibling_type<'db>(db: &'db dyn Db, def: Definition<'db>, name: &str) -> InferredType { + definitions(db, def.file(db)) + .iter() + .find(|sibling| sibling.name(db) == name) + .map_or(InferredType::Unknown, |sibling| { + definition_type(db, *sibling) + }) +} + +/// Synthesize one expression with the module's CALLABLES bound in scope. +/// +/// Sibling functions/classes arrive through [`callable_interface`] — a +/// `PartialEq` value — so a body-only edit to a sibling with a DECLARED +/// signature backdates and this definition's memo survives (the early +/// cutoff the checklist demands); a changed SYNTHESIZED return correctly +/// invalidates. +fn synth_expression<'db>(db: &'db dyn Db, def: Definition<'db>, expr: &Expr) -> InferredType { + let globals: std::collections::HashMap = + callable_interface(db, def.file(db)) + .0 + .iter() + .map(|(name, ty)| (name.clone(), crate::bidir::Ty::from_inferred(ty))) + .collect(); + let mut engine = BidirEngine::new(globals); + engine.set_class_attributes( + class_attribute_interface(db, def.file(db)) + .0 + .iter() + .map(|(class, attrs)| (class.clone(), attrs.iter().cloned().collect())) + .collect(), + ); + let ty = engine.synth(expr); + let solution = engine.finish(); + ty.to_inferred(&solution.vars) +} + +/// A module's class-attribute schemas — the value of +/// [`class_attribute_interface`]. `PartialEq` gives backdating: attribute +/// edits that leave the schema unchanged never invalidate consumers. +#[derive(Debug, Clone, Default, PartialEq)] +pub struct ClassAttributeInterface(pub Vec<(String, Vec<(String, InferredType)>)>); + +/// Tracked query: lowercased class name → annotated class-level attributes, +/// from each class definition's slice (plain attribute-load inference, +/// [NARROWPLAN-CHECKLIST] expression inference). `self.attr` assignments in +/// `__init__` are a follow-up; class-level `AnnAssign` covers the declared +/// schema surface. +#[salsa::tracked(returns(ref))] +pub fn class_attribute_interface(db: &dyn Db, file: SourceFile) -> ClassAttributeInterface { + ClassAttributeInterface( + definitions(db, file) + .iter() + .filter(|def| def.kind(db) == DefKind::Class) + .filter_map(|def| { + let slice = def.source(db); + let attrs = class_level_attributes(slice)?; + Some((def.name(db).to_ascii_lowercase(), attrs)) + }) + .collect(), + ) +} + +/// Parse a class slice and collect its class-level annotated attributes. +fn class_level_attributes(slice: &str) -> Option> { + let parsed = ruff_python_parser::parse_module(slice).ok()?; + let Some(Stmt::ClassDef(class)) = parsed.syntax().body.first() else { + return None; + }; + let attrs: Vec<(String, InferredType)> = class + .body + .iter() + .filter_map(|stmt| match stmt { + Stmt::AnnAssign(assign) => match assign.target.as_ref() { + Expr::Name(name) => Some(( + name.id.to_string(), + annotation_type(slice, Some(&assign.annotation)), + )), + _ => None, + }, + _ => None, + }) + .collect(); + Some(attrs) +} + +/// Tracked query: the `(name, type)` interface of the module's FUNCTIONS and +/// CLASSES only — the backdating boundary variable inference reads its +/// callables through (variables are excluded to keep variable↔variable +/// resolution on the direct, cycle-recovered [`definition_type`] edge). +#[salsa::tracked(returns(ref))] +pub fn callable_interface(db: &dyn Db, file: SourceFile) -> ModuleInterface { + ModuleInterface( + definitions(db, file) + .iter() + .filter(|def| matches!(def.kind(db), DefKind::Function | DefKind::Class)) + .map(|def| (def.name(db).clone(), definition_type(db, *def))) + .collect(), + ) +} + +/// One expression-level inference result within a definition. +/// +/// Offsets are relative to the definition's **slice**, so results stay valid +/// (and memos stay warm) when the definition merely moves within its file. +#[derive(Debug, Clone, PartialEq)] +pub struct ExpressionType { + /// Start offset within the definition slice. + pub start: u32, + /// End offset within the definition slice. + pub end: u32, + /// The inferred type of the expression. + pub inferred: InferredType, +} + +/// Tracked query: expression-level types within one definition — assignment +/// right-hand sides and `return` values, the positions hover/inlay hints and +/// diagnostics consume ([TYPEINF-TARGET-INCREMENTAL] expression granularity). +#[salsa::tracked(returns(ref))] +pub fn expression_types<'db>(db: &'db dyn Db, def: Definition<'db>) -> Vec { + let slice = def.source(db); + let Ok(parsed) = ruff_python_parser::parse_module(slice) else { + return Vec::new(); + }; + let mut engine = BidirEngine::new(std::collections::HashMap::new()); + let mut recorded: Vec<(u32, u32, crate::bidir::Ty)> = Vec::new(); + collect_stmt_expressions(&mut engine, &parsed.syntax().body, &mut recorded); + let solution = engine.finish(); + recorded + .into_iter() + .map(|(start, end, ty)| ExpressionType { + start, + end, + inferred: ty.to_inferred(&solution.vars), + }) + .collect() +} + +/// Walk statements, synthesizing assignment RHS and `return` expressions. +fn collect_stmt_expressions( + engine: &mut BidirEngine, + stmts: &[Stmt], + out: &mut Vec<(u32, u32, crate::bidir::Ty)>, +) { + for stmt in stmts { + match stmt { + Stmt::Assign(assign) => record(engine, &assign.value, out), + Stmt::AnnAssign(assign) => { + if let Some(value) = assign.value.as_deref() { + record(engine, value, out); + } + } + Stmt::Return(ret) => { + if let Some(value) = ret.value.as_deref() { + record(engine, value, out); + } + } + Stmt::FunctionDef(def) => collect_stmt_expressions(engine, &def.body, out), + Stmt::ClassDef(def) => collect_stmt_expressions(engine, &def.body, out), + Stmt::If(node) => { + collect_stmt_expressions(engine, &node.body, out); + for clause in &node.elif_else_clauses { + collect_stmt_expressions(engine, &clause.body, out); + } + } + Stmt::For(node) => collect_stmt_expressions(engine, &node.body, out), + Stmt::While(node) => collect_stmt_expressions(engine, &node.body, out), + Stmt::With(node) => collect_stmt_expressions(engine, &node.body, out), + _ => {} + } + } +} + +/// Record one synthesized expression, slice-relative. +fn record(engine: &mut BidirEngine, expr: &Expr, out: &mut Vec<(u32, u32, crate::bidir::Ty)>) { + let ty = engine.synth(expr); + let range = expr.range(); + out.push((range.start().into(), range.end().into(), ty)); +} + +/// A module's compact inference interface: `(name, type)` per definition. +/// +/// `PartialEq` makes this the **early-cutoff boundary** for cross-file work: a +/// body-only edit re-runs [`module_interface`], salsa backdates the unchanged +/// value, and importers reading it are never re-executed. +#[derive(Debug, Clone, Default, PartialEq)] +pub struct ModuleInterface(pub Vec<(String, InferredType)>); + +/// Tracked query: the module's interface, the cross-file dependency boundary +/// ([TYPEINF-TARGET-INCREMENTAL] — Pyrefly's "Interface" idea). +#[salsa::tracked(returns(ref))] +pub fn module_interface(db: &dyn Db, file: SourceFile) -> ModuleInterface { + ModuleInterface( + definitions(db, file) + .iter() + .map(|def| (def.name(db).clone(), definition_type(db, *def))) + .collect(), + ) +} + +/// Tracked query: the flow-narrowed name uses within one function +/// definition — the **Salsa-backed use-def map** of +/// [TYPEINF-TARGET-NARROWING] at definition granularity. +/// +/// Re-parses and re-resolves the definition's own slice (so the query +/// depends on nothing but the tracked `source` field: an edit elsewhere in +/// the file leaves this memo untouched), seeds the narrowing environment +/// from the parameter annotations, and runs the flow walker +/// ([`crate::narrow::analyse_function`]) with its branch frames and +/// `phi`-joins over the resolver-collected guards. +#[salsa::tracked(returns(ref))] +pub fn narrowed_uses<'db>( + db: &'db dyn Db, + def: Definition<'db>, +) -> Vec { + if def.kind(db) != DefKind::Function { + return Vec::new(); + } + let slice = def.source(db); + let Ok(parsed) = basilisk_parser::parse_source(slice.clone(), "def.py".to_owned()) else { + return Vec::new(); + }; + let Ok(resolved) = basilisk_resolver::resolve(&parsed) else { + return Vec::new(); + }; + let Some(function) = resolved.functions.first() else { + return Vec::new(); + }; + let declared = declared_parameter_types(slice, function); + let Ok(reparsed) = ruff_python_parser::parse_module(slice) else { + return Vec::new(); + }; + let Some(Stmt::FunctionDef(function_def)) = reparsed.syntax().body.first() else { + return Vec::new(); + }; + crate::narrow::analyse_function( + &function_def.body, + crate::narrow::NarrowEnv::new(declared), + &function.narrowing_guards, + ) + .narrowed_uses +} + +/// Parameter name → declared type, from a function's annotation spans. +fn declared_parameter_types( + slice: &str, + function: &basilisk_resolver::FunctionInfo, +) -> std::collections::HashMap { + function + .parameters + .iter() + .filter_map(|param| { + let span = param.annotation_span?; + let start = usize::try_from(span.start).ok()?; + let end = usize::try_from(span.end).ok()?; + let text = slice.get(start..end)?; + Some((param.name.clone(), InferredType::from_annotation(text))) + }) + .collect() +} diff --git a/crates/basilisk-checker/src/inference.rs b/crates/basilisk-checker/src/inference.rs index 1d062878..412b62ac 100644 --- a/crates/basilisk-checker/src/inference.rs +++ b/crates/basilisk-checker/src/inference.rs @@ -38,6 +38,49 @@ pub fn infer_rhs(rhs: &RhsKind) -> InferredType { } } +/// Returns `true` when the CURRENT engine fully determines a usable declared +/// type from this RHS alone — i.e. [`infer_rhs`] produces a type with no +/// `Unknown`/`Never` component and no widening guess. +/// +/// Implements [TYPEINF-EXCEEDS-REQUIRED]: a missing-annotation rule +/// (BSK-0001/BSK-0002) must never fire where this returns `true`, and must +/// keep firing where it returns `false`. The predicate is deliberately exactly +/// as strong as today's inference and no stronger: +/// +/// - scalar literals (`int`/`float`/`str`/`bool`/`bytes`) determine their type; +/// - non-empty containers of determining elements determine theirs; +/// - `None` does NOT determine a declared type (`T | None` needs `T`); +/// - empty containers do NOT (element types unknown); +/// - calls, lambdas, names, and arbitrary expressions do NOT +/// ([TYPEINF-EXCEEDS-NOUNKNOWN] keeps them `Unknown`). +#[must_use] +pub fn rhs_fully_determines_type(rhs: &RhsKind) -> bool { + match rhs { + RhsKind::IntLiteral + | RhsKind::FloatLiteral + | RhsKind::StrLiteral + | RhsKind::BoolLiteral + | RhsKind::BytesLiteral => true, + RhsKind::List(elements) | RhsKind::Set(elements) | RhsKind::Tuple(elements) => { + !elements.is_empty() && elements.iter().all(rhs_fully_determines_type) + } + RhsKind::Dict(pairs) => { + !pairs.is_empty() + && pairs + .iter() + .all(|(k, v)| rhs_fully_determines_type(k) && rhs_fully_determines_type(v)) + } + RhsKind::NoneValue + | RhsKind::EmptyList + | RhsKind::EmptyDict + | RhsKind::CallExpr + | RhsKind::KnownCall(_) + | RhsKind::TypeCall + | RhsKind::Lambda + | RhsKind::Other => false, + } +} + /// Checks if a variable assignment is valid given its annotation and inferred RHS type. /// /// # Errors @@ -175,3 +218,79 @@ pub fn infer_flow_union_types( result } + +#[cfg(test)] +mod tests { + use super::rhs_fully_determines_type; + use basilisk_resolver::RhsKind; + + /// [TYPEINF-EXCEEDS-REQUIRED]: scalar literals fully determine a declared + /// type; the annotation rules must stay silent on them. + #[test] + fn scalar_literals_determine_the_type() { + for kind in [ + RhsKind::IntLiteral, + RhsKind::FloatLiteral, + RhsKind::StrLiteral, + RhsKind::BoolLiteral, + RhsKind::BytesLiteral, + ] { + assert!( + rhs_fully_determines_type(&kind), + "{kind:?} must determine its type" + ); + } + } + + /// [TYPEINF-EXCEEDS-REQUIRED]: `None`, empty containers, calls, lambdas, + /// and arbitrary expressions do NOT determine a usable declared type — the + /// annotation rules must keep firing there. + #[test] + fn non_determining_kinds_keep_the_rules_firing() { + for kind in [ + RhsKind::NoneValue, + RhsKind::EmptyList, + RhsKind::EmptyDict, + RhsKind::CallExpr, + RhsKind::KnownCall(Box::new(RhsKind::IntLiteral)), + RhsKind::TypeCall, + RhsKind::Lambda, + RhsKind::Other, + ] { + assert!( + !rhs_fully_determines_type(&kind), + "{kind:?} must NOT determine a type" + ); + } + } + + /// Containers determine their type iff non-empty and every element (or + /// key/value pair) determines its own. + #[test] + fn containers_recurse_and_reject_unknown_elements() { + assert!(rhs_fully_determines_type(&RhsKind::List(vec![ + RhsKind::IntLiteral, + RhsKind::IntLiteral, + ]))); + assert!(rhs_fully_determines_type(&RhsKind::Tuple(vec![ + RhsKind::StrLiteral, + RhsKind::BoolLiteral, + ]))); + assert!(rhs_fully_determines_type(&RhsKind::Dict(vec![( + RhsKind::StrLiteral, + RhsKind::IntLiteral, + )]))); + // An uninferable element poisons the whole container. + assert!(!rhs_fully_determines_type(&RhsKind::List(vec![ + RhsKind::IntLiteral, + RhsKind::CallExpr, + ]))); + assert!(!rhs_fully_determines_type(&RhsKind::Dict(vec![( + RhsKind::StrLiteral, + RhsKind::Other, + )]))); + // Empty collections carry no element information. + assert!(!rhs_fully_determines_type(&RhsKind::List(vec![]))); + assert!(!rhs_fully_determines_type(&RhsKind::Dict(vec![]))); + } +} diff --git a/crates/basilisk-checker/src/lib.rs b/crates/basilisk-checker/src/lib.rs index 8c3e824f..88c18907 100644 --- a/crates/basilisk-checker/src/lib.rs +++ b/crates/basilisk-checker/src/lib.rs @@ -28,6 +28,7 @@ //! when configuration resolves it to a non-disabled severity //! ([CHKARCH-COMMANDS]). +pub mod bidir; pub mod cached; pub mod collection_inference; pub mod context; @@ -35,7 +36,10 @@ pub mod diagnostic; pub mod exports; pub mod imports; pub mod incremental; +pub mod incremental_defs; pub mod inference; +pub mod narrow; +pub mod param_infer; pub mod rule_catalog; pub mod rule_tags; pub mod rules; @@ -44,6 +48,7 @@ pub mod suppression; mod suppression_audit; pub mod types; pub mod types_parsing; +pub(crate) mod types_star_tuples; pub use cached::CachedDiagnostic; pub use diagnostic::{Diagnostic, ErrorCode, Severity}; diff --git a/crates/basilisk-checker/src/narrow/env.rs b/crates/basilisk-checker/src/narrow/env.rs new file mode 100644 index 00000000..9c3c6c9e --- /dev/null +++ b/crates/basilisk-checker/src/narrow/env.rs @@ -0,0 +1,240 @@ +//! Implements [TYPEINF-TARGET-NARROWING]. See docs/specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-TARGET-NARROWING +//! The scoped narrowing environment: branch push, complement, join, and +//! nested-function boundaries ([NARROWPLAN-CHECKLIST] Stage 2, flow analysis). + +use std::collections::HashMap; + +use crate::types::InferredType; + +/// One branch frame's narrowed bindings, layered over the frames below it. +type Frame = HashMap; + +/// A flow-scoped narrowing environment for one function body. +/// +/// Layers of [`Frame`]s model control flow: entering a branch pushes a frame, +/// leaving pops it; a lookup walks frames innermost-first and falls back to +/// the declared (base) types. Narrowing NEVER changes the declared type used +/// for assignment validation — [`NarrowEnv::declared`] always answers from +/// the base layer ([NARROWPLAN-CHECKLIST]: "assignment narrowing without +/// changing the declared type"). +/// +/// Nested functions get a **fresh** environment ([`NarrowEnv::nested`]): +/// narrowing facts from the enclosing body do not flow in (a closure may run +/// after the narrow was invalidated), matching the boundary rule the spec and +/// every reference checker apply. +#[derive(Debug, Clone, Default)] +pub struct NarrowEnv { + declared: Frame, + /// Whole-scope narrowing facts (`assert`, post-early-exit complements, + /// assignment narrowing outside any branch) — layered over `declared` + /// WITHOUT mutating it, so the declared anchor survives. + scope: Frame, + frames: Vec, +} + +impl NarrowEnv { + /// An environment seeded with the declared parameter/local types. + #[must_use] + pub fn new(declared: HashMap) -> Self { + Self { + declared, + scope: Frame::new(), + frames: Vec::new(), + } + } + + /// A fresh environment for a nested function: declared types only — + /// no narrowing crosses the function boundary. + #[must_use] + pub fn nested(&self, declared: HashMap) -> Self { + let _ = self; + Self::new(declared) + } + + /// The narrowed type currently visible for `name`, innermost frame first, + /// then whole-scope facts, falling back to the declared type. + #[must_use] + pub fn lookup(&self, name: &str) -> Option { + self.frames + .iter() + .rev() + .find_map(|frame| frame.get(name)) + .or_else(|| self.scope.get(name)) + .or_else(|| self.declared.get(name)) + .cloned() + } + + /// The DECLARED type for `name` — narrowing never touches this; it is + /// what assignment compatibility keeps validating against. + #[must_use] + pub fn declared(&self, name: &str) -> Option<&InferredType> { + self.declared.get(name) + } + + /// Enter a branch: subsequent narrows apply only inside it. + pub fn push_branch(&mut self) { + self.frames.push(Frame::new()); + } + + /// Leave a branch, returning its narrowed bindings for a later + /// [`NarrowEnv::join`] at the control-flow merge. + pub fn pop_branch(&mut self) -> HashMap { + self.frames.pop().unwrap_or_default() + } + + /// Record a narrowing fact in the innermost open branch (or, outside any + /// branch, as a whole-scope fact — the `assert` case). The declared + /// layer is NEVER written: assignment validation keeps its anchor. + pub fn narrow(&mut self, name: &str, ty: InferredType) { + let frame = match self.frames.last_mut() { + Some(frame) => frame, + None => &mut self.scope, + }; + let _ = frame.insert(name.to_owned(), ty); + } + + /// Join two branch outcomes at a control-flow merge (`phi`): a name + /// narrowed in BOTH branches flows on as the union of the two; a name + /// narrowed in only one branch falls back to its declared type (the other + /// path never narrowed it), so nothing over-narrows past the merge. + pub fn join( + &mut self, + then_branch: HashMap, + else_branch: HashMap, + ) { + let mut else_branch = else_branch; + for (name, then_ty) in then_branch { + if let Some(else_ty) = else_branch.remove(&name) { + self.narrow(&name, InferredType::union(then_ty, else_ty)); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::narrow::set_ops::{intersect, subtract}; + + fn optional_int_env() -> NarrowEnv { + NarrowEnv::new( + [( + "x".to_owned(), + InferredType::Optional(Box::new(InferredType::Int)), + )] + .into_iter() + .collect(), + ) + } + + /// Branch narrowing is visible inside the branch and gone after popping. + #[test] + fn branch_narrowing_is_scoped() { + let mut env = optional_int_env(); + env.push_branch(); + let narrowed = subtract( + &env.lookup("x").unwrap_or(InferredType::Unknown), + &InferredType::None_, + ); + env.narrow("x", narrowed); + assert_eq!(env.lookup("x"), Some(InferredType::Int)); + let _ = env.pop_branch(); + assert_eq!( + env.lookup("x"), + Some(InferredType::Optional(Box::new(InferredType::Int))) + ); + } + + /// The declared type is never mutated by branch narrowing — + /// assignment validation keeps its anchor. + #[test] + fn declared_type_survives_narrowing() { + let mut env = optional_int_env(); + env.push_branch(); + env.narrow("x", InferredType::Int); + assert_eq!( + env.declared("x"), + Some(&InferredType::Optional(Box::new(InferredType::Int))) + ); + } + + /// A join unions both branches' narrowed types; one-sided narrows drop. + #[test] + fn join_unions_both_branches() { + let mut env = NarrowEnv::new( + [( + "x".to_owned(), + InferredType::Union(vec![ + InferredType::Int, + InferredType::Str, + InferredType::None_, + ]), + )] + .into_iter() + .collect(), + ); + + env.push_branch(); + env.narrow("x", InferredType::Int); + let then_branch = env.pop_branch(); + + env.push_branch(); + env.narrow("x", InferredType::Str); + let else_branch = env.pop_branch(); + + env.join(then_branch, else_branch); + let joined = env.lookup("x").unwrap_or(InferredType::Unknown); + assert!(InferredType::Int.is_assignable_to(&joined)); + assert!(InferredType::Str.is_assignable_to(&joined)); + assert!( + !InferredType::None_.is_assignable_to(&joined), + "both branches eliminated None, so the merge keeps it eliminated" + ); + } + + /// A name narrowed in only one branch reverts at the merge. + #[test] + fn one_sided_narrowing_reverts_at_merge() { + let mut env = optional_int_env(); + env.push_branch(); + env.narrow("x", InferredType::Int); + let then_branch = env.pop_branch(); + env.join(then_branch, HashMap::new()); + assert_eq!( + env.lookup("x"), + Some(InferredType::Optional(Box::new(InferredType::Int))) + ); + } + + /// Nested functions start from declared types only. + #[test] + fn nested_function_boundary_drops_narrows() { + let mut env = optional_int_env(); + env.narrow("x", InferredType::Int); + let inner = env.nested( + [( + "x".to_owned(), + InferredType::Optional(Box::new(InferredType::Int)), + )] + .into_iter() + .collect(), + ); + assert_eq!( + inner.lookup("x"), + Some(InferredType::Optional(Box::new(InferredType::Int))) + ); + } + + /// Whole-scope narrowing (assert) applies without an open branch and + /// composes with intersect. + #[test] + fn assert_narrows_the_whole_scope() { + let mut env = optional_int_env(); + let narrowed = intersect( + &env.lookup("x").unwrap_or(InferredType::Unknown), + &InferredType::Int, + ); + env.narrow("x", narrowed); + assert_eq!(env.lookup("x"), Some(InferredType::Int)); + } +} diff --git a/crates/basilisk-checker/src/narrow/flow.rs b/crates/basilisk-checker/src/narrow/flow.rs new file mode 100644 index 00000000..1f81d204 --- /dev/null +++ b/crates/basilisk-checker/src/narrow/flow.rs @@ -0,0 +1,443 @@ +//! Implements [TYPEINF-TARGET-NARROWING]. See docs/specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-TARGET-NARROWING +//! The statement-level flow walker: drives a [`NarrowEnv`] through a function +//! body, applying resolver-collected guards at branches (positive frame in +//! the `if` body, complement in the `else`, `phi`-join at the merge), +//! persisting the complement after a diverging branch (early exit), applying +//! `assert` narrowing whole-scope, and modelling assignment narrowing without +//! ever touching the declared type ([NARROWPLAN-CHECKLIST] Stage 2). + +use std::collections::HashMap; + +use basilisk_resolver::NarrowingGuard; +use ruff_python_ast::{Expr, Stmt}; +use ruff_text_size::Ranged; + +use crate::bidir::BidirEngine; +use crate::types::InferredType; + +use super::env::NarrowEnv; +use super::guards::{guard_outcomes_in, GuardOutcome}; + +/// One narrowed name-use site: the location and the type visible there. +#[derive(Debug, Clone, PartialEq)] +pub struct NarrowedUse { + /// The variable read at this site. + pub name: String, + /// Byte offset of the use within the analysed body's source. + pub start: u32, + /// End byte offset of the use. + pub end: u32, + /// The flow-narrowed type at this point. + pub narrowed: InferredType, +} + +/// The outcome of walking one function body. +#[derive(Debug, Default)] +pub struct FlowResult { + /// Every `Name` read whose flow-narrowed type differs from its declared + /// type — the sites hover/diagnostics consume. + pub narrowed_uses: Vec, + /// Body ranges of branches whose guard narrows a variable to `Never` — + /// **inference-driven reachability** ([TYPEINF-TARGET-NARROWING]): the + /// branch is unreachable because the type lattice proves the guard can + /// never hold, not because a syntactic idiom matched. + pub unreachable_ranges: Vec<(u32, u32)>, +} + +/// Walk `body` under `env`'s declared types, consuming the function's +/// resolver-collected `guards` (matched to `if`/`assert` statements by +/// test-expression span). +#[must_use] +pub fn analyse_function(body: &[Stmt], env: NarrowEnv, guards: &[NarrowingGuard]) -> FlowResult { + analyse_function_in(body, env, guards, &super::guards::NarrowContext::default()) +} + +/// [`analyse_function`] with module facts (`TypedDict` schemas) available to +/// the guard interpreter. +#[must_use] +pub fn analyse_function_in( + body: &[Stmt], + env: NarrowEnv, + guards: &[NarrowingGuard], + ctx: &super::guards::NarrowContext, +) -> FlowResult { + let mut walker = FlowWalker { + env, + guards_by_span: guards + .iter() + .map(|guard| ((guard.span.start, guard.span.end), guard)) + .collect(), + ctx, + result: FlowResult::default(), + }; + walker.walk_stmts(body); + walker.result +} + +/// Internal walker state. +struct FlowWalker<'g> { + env: NarrowEnv, + guards_by_span: HashMap<(u32, u32), &'g NarrowingGuard>, + ctx: &'g super::guards::NarrowContext, + result: FlowResult, +} + +impl FlowWalker<'_> { + fn walk_stmts(&mut self, stmts: &[Stmt]) { + for stmt in stmts { + self.walk_stmt(stmt); + } + } + + fn walk_stmt(&mut self, stmt: &Stmt) { + match stmt { + Stmt::If(node) => self.walk_if(node), + Stmt::Assert(node) => self.walk_assert(node), + Stmt::Assign(node) => self.walk_assign(node), + Stmt::AnnAssign(node) => { + if let Some(value) = node.value.as_deref() { + self.record_uses(value); + } + } + Stmt::Return(node) => { + if let Some(value) = node.value.as_deref() { + self.record_uses(value); + } + } + Stmt::Expr(node) => self.record_uses(&node.value), + // Loop bodies: narrowing inside must not escape — walk within a + // discarded branch frame ([TYPEINF-NARROWING-SCOPE]). + Stmt::For(node) => { + self.record_uses(&node.iter); + self.walk_discarded(&node.body); + self.walk_discarded(&node.orelse); + } + Stmt::While(node) => { + self.record_uses(&node.test); + self.walk_discarded(&node.body); + self.walk_discarded(&node.orelse); + } + Stmt::With(node) => self.walk_stmts(&node.body), + Stmt::Try(node) => self.walk_try(node), + Stmt::Match(node) => self.walk_match(node), + // Everything else — including nested functions/classes, which + // are a narrowing BOUNDARY (enclosing narrows must not flow into + // a closure that may run later) — is not walked. + _ => {} + } + } + + /// `if : ...` — positive branch, complement branch, then join; a + /// diverging branch persists the OTHER branch's narrowing (early exit). + fn walk_if(&mut self, node: &ruff_python_ast::StmtIf) { + self.record_uses(&node.test); + let outcome = self.lookup_outcome(node.test.range()); + let then_diverges = diverges(&node.body); + let else_body: Vec<&Stmt> = node + .elif_else_clauses + .iter() + .filter(|clause| clause.test.is_none()) + .flat_map(|clause| clause.body.iter()) + .collect(); + let else_diverges = !else_body.is_empty() && diverges_refs(&else_body); + + let then_frame = self.walk_branch(&node.body, outcome.as_ref(), true); + let else_frame = self.walk_else(&node.elif_else_clauses, outcome.as_ref()); + + match (&outcome, then_diverges, else_diverges) { + // `if x is None: return` — the complement holds afterwards. + (Some(out), true, false) => self.env.narrow(&out.variable, out.negative.clone()), + (Some(out), false, true) => self.env.narrow(&out.variable, out.positive.clone()), + _ => self.env.join(then_frame, else_frame), + } + } + + /// Walk one branch under a guard polarity, returning its frame. + fn walk_branch( + &mut self, + body: &[Stmt], + outcome: Option<&GuardOutcome>, + positive: bool, + ) -> HashMap { + self.env.push_branch(); + if let Some(out) = outcome { + let ty = if positive { + out.positive.clone() + } else { + out.negative.clone() + }; + if ty == InferredType::Never { + if let Some(range) = body_range(body) { + self.result.unreachable_ranges.push(range); + } + } + self.env.narrow(&out.variable, ty); + } + self.walk_stmts(body); + self.env.pop_branch() + } + + /// Walk `elif`/`else` clauses; only the plain `else` receives the + /// complement (an `elif` has its own guard, looked up on its own test). + fn walk_else( + &mut self, + clauses: &[ruff_python_ast::ElifElseClause], + outcome: Option<&GuardOutcome>, + ) -> HashMap { + let mut merged = HashMap::new(); + for clause in clauses { + match &clause.test { + Some(test) => { + self.record_uses(test); + let elif_outcome = self.lookup_outcome(test.range()); + let _ = self.walk_branch(&clause.body, elif_outcome.as_ref(), true); + } + None => { + merged = self.walk_branch(&clause.body, outcome, false); + } + } + } + merged + } + + /// `assert ` — whole-scope narrowing from this point on. + fn walk_assert(&mut self, node: &ruff_python_ast::StmtAssert) { + self.record_uses(&node.test); + if let Some(outcome) = self.lookup_outcome(node.range()) { + self.env.narrow(&outcome.variable, outcome.positive); + } + } + + /// `x = expr` — assignment narrowing: the flow type of `x` becomes the + /// synthesized type of `expr`; the DECLARED type (what assignment + /// validation checks against) is untouched by design. + fn walk_assign(&mut self, node: &ruff_python_ast::StmtAssign) { + self.record_uses(&node.value); + if let [Expr::Name(target)] = node.targets.as_slice() { + let ty = synth_type(&node.value); + self.env.narrow(target.id.as_str(), ty); + } + } + + /// `match : case ...` — per-case narrowing of the subject + /// ([TYPEINF-NARROWING-MATCH]): each case body walks in its own branch + /// frame with the subject intersected with the case's pattern type. + fn walk_match(&mut self, node: &ruff_python_ast::StmtMatch) { + self.record_uses(&node.subject); + let range = node.range(); + let key = (u32::from(range.start()), u32::from(range.end())); + let match_guard = self.guards_by_span.get(&key).copied(); + for case in &node.cases { + self.env.push_branch(); + self.narrow_match_case(match_guard, case); + self.walk_stmts(&case.body); + let _ = self.env.pop_branch(); + } + self.narrow_after_match(match_guard, node); + } + + /// Implied-else exhaustiveness: when every case body diverges, reaching + /// past the `match` means no case matched — the subject loses every + /// covered pattern type ([TYPEINF-NARROWING-MATCH]). + fn narrow_after_match( + &mut self, + match_guard: Option<&NarrowingGuard>, + node: &ruff_python_ast::StmtMatch, + ) { + let Some(guard) = match_guard.filter(|guard| !guard.in_loop) else { + return; + }; + let basilisk_resolver::NarrowingGuardKind::Match { + variable, + cases, + has_wildcard, + } = &guard.kind + else { + return; + }; + let all_diverge = node.cases.iter().all(|case| diverges(&case.body)); + if *has_wildcard || !all_diverge { + return; + } + let Some(current) = self.env.lookup(variable) else { + return; + }; + let covered = cases + .iter() + .map(|case| InferredType::from_annotation(&case.pattern_type)) + .fold(InferredType::Never, InferredType::union); + self.env + .narrow(variable, super::set_ops::subtract(¤t, &covered)); + } + + /// Apply one case's pattern narrowing, when the resolver captured it. + fn narrow_match_case( + &mut self, + match_guard: Option<&NarrowingGuard>, + case: &ruff_python_ast::MatchCase, + ) { + let Some(guard) = match_guard.filter(|guard| !guard.in_loop) else { + return; + }; + let basilisk_resolver::NarrowingGuardKind::Match { + variable, cases, .. + } = &guard.kind + else { + return; + }; + let Some(pattern) = matching_case(cases, case) else { + return; + }; + let Some(current) = self.env.lookup(variable) else { + return; + }; + let target = InferredType::from_annotation(&pattern.pattern_type); + let narrowed = super::set_ops::intersect(¤t, &target); + // A `Never` here usually means a VALUE pattern (`case 1:`) whose text + // is not a type — never fabricate unreachability from that. + if narrowed != InferredType::Never { + self.env.narrow(variable, narrowed); + } + } + + /// Try/except: handler and else/final bodies walk in discarded frames — + /// exceptions make mid-body narrowing unreliable. + fn walk_try(&mut self, node: &ruff_python_ast::StmtTry) { + self.walk_discarded(&node.body); + for handler in &node.handlers { + let ruff_python_ast::ExceptHandler::ExceptHandler(h) = handler; + self.walk_discarded(&h.body); + } + self.walk_discarded(&node.orelse); + self.walk_stmts(&node.finalbody); + } + + /// Walk statements inside a frame whose narrowing is thrown away. + fn walk_discarded(&mut self, stmts: &[Stmt]) { + self.env.push_branch(); + self.walk_stmts(stmts); + let _ = self.env.pop_branch(); + } + + /// Find the guard whose collected span matches this range. + fn lookup_outcome(&self, range: ruff_text_size::TextRange) -> Option { + let key = (u32::from(range.start()), u32::from(range.end())); + let guard = self.guards_by_span.get(&key)?; + let current = self.env.lookup(variable_of(guard))?; + guard_outcomes_in(guard, ¤t, self.ctx) + } + + /// Record every narrowed `Name` read inside `expr`. + fn record_uses(&mut self, expr: &Expr) { + let mut names = Vec::new(); + collect_name_reads(expr, &mut names); + for (name, start, end) in names { + let Some(narrowed) = self.env.lookup(&name) else { + continue; + }; + if self.env.declared(&name) == Some(&narrowed) { + continue; + } + self.result.narrowed_uses.push(NarrowedUse { + name, + start, + end, + narrowed, + }); + } + } +} + +/// The variable a guard narrows (guards produced by the resolver always +/// target exactly one name). +fn variable_of(guard: &NarrowingGuard) -> &str { + variable_of_kind(&guard.kind) +} + +/// Recurse into an `assert`'s inner guard kind for its variable. +fn variable_of_kind(kind: &basilisk_resolver::NarrowingGuardKind) -> &str { + use basilisk_resolver::NarrowingGuardKind as K; + match kind { + K::IsInstance { variable, .. } + | K::IsNone { variable, .. } + | K::Truthiness { variable, .. } + | K::Assignment { variable, .. } + | K::TypeGuard { variable, .. } + | K::TypeIs { variable, .. } + | K::Match { variable, .. } + | K::IsSubclass { variable, .. } + | K::EqualsLiteral { variable, .. } + | K::InLiterals { variable, .. } + | K::HasAttr { variable, .. } + | K::TypeOfIs { variable, .. } + | K::KeyInDict { variable, .. } => variable, + K::Assert { inner } => variable_of_kind(inner), + } +} + +/// Find the resolver case entry matching an AST case, by body span. +fn matching_case<'c>( + cases: &'c [basilisk_resolver::MatchCaseNarrowing], + case: &ruff_python_ast::MatchCase, +) -> Option<&'c basilisk_resolver::MatchCaseNarrowing> { + let start = u32::from(case.body.first()?.range().start()); + let end = u32::from(case.body.last()?.range().end()); + cases + .iter() + .find(|entry| entry.body_span.start == start && entry.body_span.end == end) +} + +/// The byte range spanned by a statement list, when non-empty. +fn body_range(body: &[Stmt]) -> Option<(u32, u32)> { + let start = u32::from(body.first()?.range().start()); + let end = u32::from(body.last()?.range().end()); + Some((start, end)) +} + +/// Whether a statement list definitely diverges (ends in `return`/`raise`/ +/// `continue`/`break`) — the early-exit signal for complement persistence. +fn diverges(stmts: &[Stmt]) -> bool { + matches!( + stmts.last(), + Some(Stmt::Return(_) | Stmt::Raise(_) | Stmt::Continue(_) | Stmt::Break(_)) + ) +} + +/// [`diverges`] over a collected reference list (the `else` clause bodies). +fn diverges_refs(stmts: &[&Stmt]) -> bool { + matches!( + stmts.last(), + Some(Stmt::Return(_) | Stmt::Raise(_) | Stmt::Continue(_) | Stmt::Break(_)) + ) +} + +/// Synthesize an expression's type through the bidirectional engine. +fn synth_type(expr: &Expr) -> InferredType { + let mut engine = BidirEngine::new(HashMap::new()); + let ty = engine.synth(expr); + let solution = engine.finish(); + ty.to_inferred(&solution.vars) +} + +/// Visitor collecting every `Name` read with its range. +struct NameReads<'a>(&'a mut Vec<(String, u32, u32)>); + +impl ruff_python_ast::visitor::Visitor<'_> for NameReads<'_> { + fn visit_expr(&mut self, expr: &Expr) { + if let Expr::Name(name) = expr { + let range = name.range(); + self.0.push(( + name.id.to_string(), + u32::from(range.start()), + u32::from(range.end()), + )); + } + ruff_python_ast::visitor::walk_expr(self, expr); + } +} + +/// Collect `(name, start, end)` for every `Name` read in an expression. +fn collect_name_reads(expr: &Expr, out: &mut Vec<(String, u32, u32)>) { + use ruff_python_ast::visitor::Visitor as _; + NameReads(out).visit_expr(expr); +} diff --git a/crates/basilisk-checker/src/narrow/guards.rs b/crates/basilisk-checker/src/narrow/guards.rs new file mode 100644 index 00000000..83b5aa6d --- /dev/null +++ b/crates/basilisk-checker/src/narrow/guards.rs @@ -0,0 +1,548 @@ +//! Implements [TYPEINF-TARGET-NARROWING]. See docs/specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-TARGET-NARROWING +//! Consumption of the resolver's collected narrowing guards +//! ([`NarrowingGuardKind`]) into positive/negative environment updates — +//! `isinstance`, `is None`, truthiness, `TypeGuard`, `TypeIs`, `assert`, and +//! `match` ([NARROWPLAN-CHECKLIST] Stage 2, "consume resolver guards"). + +use std::collections::{HashMap, HashSet}; + +use basilisk_resolver::{NarrowingGuard, NarrowingGuardKind}; + +use crate::types::{InferredType, LiteralValue}; + +use super::set_ops::{intersect, subtract}; + +/// The key sets of one `TypedDict`, for `"key" in td` narrowing. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct TypedDictKeys { + /// Every declared key (required or not). + pub all: HashSet, + /// Keys that are always present (`Required`/total). + pub required: HashSet, +} + +/// Module facts the guard interpreter may consult. Keys are the LOWERCASED +/// class names, matching how annotations lower to +/// [`InferredType::Named`] via [`InferredType::from_annotation`]. +#[derive(Debug, Clone, Default)] +pub struct NarrowContext { + /// `TypedDict` name → key sets. + pub typeddict_keys: HashMap, + /// `@final` class names — the classes whose `type(x) is not C` branch + /// may soundly exclude `C` ([TYPEINF-NARROWING-TYPEOF]). + pub final_classes: std::collections::HashSet, +} + +/// What one guard does to one variable in each branch. +#[derive(Debug, Clone, PartialEq)] +pub struct GuardOutcome { + /// The variable the guard narrows. + pub variable: String, + /// The type in the positive (guard-true) branch. + pub positive: InferredType, + /// The type in the negative (guard-false) branch. + pub negative: InferredType, + /// Whether the positive narrowing outlives any branch (the `assert` form). + pub whole_scope: bool, +} + +/// Interpret one resolver guard against the variable's current type. +/// +/// Guards collected inside loops (`guard.in_loop`) return `None`: their +/// narrowing must not persist past the loop body +/// ([TYPEINF-NARROWING-SCOPE]), and Stage 2's statement-order environment +/// applies them only when flow-accurate. +#[must_use] +pub fn guard_outcomes(guard: &NarrowingGuard, current: &InferredType) -> Option { + guard_outcomes_in(guard, current, &NarrowContext::default()) +} + +/// [`guard_outcomes`] with module facts (`TypedDict` schemas) available. +#[must_use] +pub fn guard_outcomes_in( + guard: &NarrowingGuard, + current: &InferredType, + ctx: &NarrowContext, +) -> Option { + if guard.in_loop { + return None; + } + outcome_for_kind(&guard.kind, current, false, ctx) +} + +/// Interpret a guard kind (recursing through `assert`). +fn outcome_for_kind( + kind: &NarrowingGuardKind, + current: &InferredType, + whole_scope: bool, + ctx: &NarrowContext, +) -> Option { + match kind { + NarrowingGuardKind::IsInstance { + variable, + type_names, + .. + } => { + let guard_type = union_of_names(type_names); + Some(GuardOutcome { + variable: variable.clone(), + positive: intersect(current, &guard_type), + negative: subtract(current, &guard_type), + whole_scope, + }) + } + NarrowingGuardKind::IsNone { + variable, + is_positive, + .. + } => Some(none_outcome(variable, current, *is_positive, whole_scope)), + NarrowingGuardKind::Truthiness { variable, .. } => Some(GuardOutcome { + variable: variable.clone(), + // Truthiness eliminates `None` in the truthy branch; the falsy + // branch stays unchanged (falsy ints/strs are still ints/strs — + // narrowing there needs literal-level falsy modelling, kept + // conservative for now). + positive: subtract(current, &InferredType::None_), + negative: current.clone(), + whole_scope, + }), + NarrowingGuardKind::TypeGuard { + variable, + guard_type, + .. + } => Some(GuardOutcome { + variable: variable.clone(), + positive: InferredType::from_annotation(guard_type), + // PEP 647: TypeGuard narrows the positive branch ONLY. + negative: current.clone(), + whole_scope, + }), + NarrowingGuardKind::TypeIs { + variable, + guard_type, + .. + } => { + let narrowed_to = InferredType::from_annotation(guard_type); + Some(GuardOutcome { + variable: variable.clone(), + // PEP 742: TypeIs narrows BOTH branches. + positive: intersect(current, &narrowed_to), + negative: subtract(current, &narrowed_to), + whole_scope, + }) + } + NarrowingGuardKind::Assert { inner } => outcome_for_kind(inner, current, true, ctx), + extended => extended_outcome_for_kind(extended, current, whole_scope, ctx), + } +} + +/// Interpretation of the extended guards ([NARROWPLAN-CHECKLIST] Stage 2, +/// "extend guard support") — split from [`outcome_for_kind`] purely to keep +/// both dispatchers small. +fn extended_outcome_for_kind( + kind: &NarrowingGuardKind, + current: &InferredType, + whole_scope: bool, + ctx: &NarrowContext, +) -> Option { + match kind { + // Implements [TYPEINF-NARROWING-EQ-LITERAL]: `x == ` narrows + // positively to the literal; the complement removes exactly that + // literal member (equality never excludes a broader type). + NarrowingGuardKind::EqualsLiteral { + variable, + literal_text, + is_positive, + .. + } => { + let literal = parse_guard_literal(literal_text)?; + Some(equality_outcome( + variable, + current, + &literal, + *is_positive, + whole_scope, + )) + } + // Implements [TYPEINF-NARROWING-IN-LITERAL]: `x in ("a", "b")`. + NarrowingGuardKind::InLiterals { + variable, + literal_texts, + is_positive, + .. + } => { + let literals: Vec = literal_texts + .iter() + .map(|text| parse_guard_literal(text)) + .collect::>>()?; + let union = literals + .into_iter() + .fold(InferredType::Never, InferredType::union); + Some(equality_outcome( + variable, + current, + &union, + *is_positive, + whole_scope, + )) + } + // Implements [TYPEINF-NARROWING-TYPEDDICT-KEY]: `"key" in td` keeps + // union members whose schema declares the key; the complement drops + // members where the key is REQUIRED (always present). + NarrowingGuardKind::KeyInDict { + variable, + key, + is_positive, + .. + } => Some(key_in_dict_outcome( + variable, + current, + key, + *is_positive, + whole_scope, + ctx, + )), + // Implements [TYPEINF-NARROWING-TYPEOF]: `type(x) is C` implies + // `isinstance(x, C)` positively; the negative branch excludes `C` + // only when `C` is `@final` (a subclass may still be `is not C`). + NarrowingGuardKind::TypeOfIs { + variable, + type_name, + is_positive, + .. + } => { + let class_type = InferredType::from_annotation(type_name); + let matched = intersect(current, &class_type); + let excluded = if ctx.final_classes.contains(&type_name.to_ascii_lowercase()) { + subtract(current, &class_type) + } else { + current.clone() + }; + let (positive, negative) = if *is_positive { + (matched, excluded) + } else { + (excluded, matched) + }; + Some(GuardOutcome { + variable: variable.clone(), + positive, + negative, + whole_scope, + }) + } + // [TYPEINF-NARROWING-ISSUBCLASS] / [TYPEINF-NARROWING-HASATTR]: + // extraction is live; interpretation is deliberately IDENTITY until + // `type[...]` object modelling and synthetic-protocol intersections + // land (Stage 2 shared-subtyping work) — never a guessed narrowing. + NarrowingGuardKind::IsSubclass { variable, .. } + | NarrowingGuardKind::HasAttr { variable, .. } => Some(GuardOutcome { + variable: variable.clone(), + positive: current.clone(), + negative: current.clone(), + whole_scope, + }), + // Assignment and match narrowing flow through dedicated paths (the + // bidirectional engine's walrus/assign handling and per-case match + // environments) rather than a two-branch outcome. Core kinds were + // handled by [`outcome_for_kind`] before delegating here. + _ => None, + } +} + +/// `x == lit` / `x in lits` branches: intersect positively, remove exactly +/// the literal members negatively; `!=`/`not in` swaps the branches. +fn equality_outcome( + variable: &str, + current: &InferredType, + literals: &InferredType, + is_positive: bool, + whole_scope: bool, +) -> GuardOutcome { + let matched = intersect(current, literals); + let removed = subtract(current, literals); + let (positive, negative) = if is_positive { + (matched, removed) + } else { + (removed, matched) + }; + GuardOutcome { + variable: variable.to_owned(), + positive, + negative, + whole_scope, + } +} + +/// `"key" in td` narrowing over a union of `TypedDict`-typed members. +fn key_in_dict_outcome( + variable: &str, + current: &InferredType, + key: &str, + is_positive: bool, + whole_scope: bool, + ctx: &NarrowContext, +) -> GuardOutcome { + let members = union_members(current); + let has_key = members + .iter() + .filter(|member| member_declares_key(member, key, ctx) != Some(false)) + .cloned() + .fold(InferredType::Never, InferredType::union); + let lacks_key = members + .iter() + .filter(|member| member_requires_key(member, key, ctx) != Some(true)) + .cloned() + .fold(InferredType::Never, InferredType::union); + let (positive, negative) = if is_positive { + (has_key, lacks_key) + } else { + (lacks_key, has_key) + }; + GuardOutcome { + variable: variable.to_owned(), + positive: non_never_or(positive, current), + negative: non_never_or(negative, current), + whole_scope, + } +} + +/// Whether a member's `TypedDict` schema declares `key`; `None` = unknown +/// (not a known `TypedDict` — kept in both branches, conservative). +fn member_declares_key(member: &InferredType, key: &str, ctx: &NarrowContext) -> Option { + let InferredType::Named(name) = member else { + return None; + }; + let keys = ctx.typeddict_keys.get(name)?; + Some(keys.all.contains(key)) +} + +/// Whether a member's schema marks `key` as always-present. +fn member_requires_key(member: &InferredType, key: &str, ctx: &NarrowContext) -> Option { + let InferredType::Named(name) = member else { + return None; + }; + let keys = ctx.typeddict_keys.get(name)?; + Some(keys.required.contains(key)) +} + +/// The members of a union/optional, or the type itself. +fn union_members(ty: &InferredType) -> Vec { + match ty { + InferredType::Union(members) => members.clone(), + InferredType::Optional(inner) => vec![(**inner).clone(), InferredType::None_], + other => vec![other.clone()], + } +} + +/// An empty (Never) filter result falls back to the unfiltered type — an +/// unknown-schema situation must not fabricate unreachability. +fn non_never_or(filtered: InferredType, current: &InferredType) -> InferredType { + if filtered == InferredType::Never { + current.clone() + } else { + filtered + } +} + +/// Case-preserving literal parser for equality/membership guard texts — +/// deliberately NOT [`InferredType::from_annotation`], which lowercases and +/// would corrupt string literal values. +fn parse_guard_literal(text: &str) -> Option { + if let Some(inner) = text.strip_prefix('"').and_then(|t| t.strip_suffix('"')) { + return Some(InferredType::Literal(LiteralValue::Str(inner.to_owned()))); + } + match text { + "True" => Some(InferredType::Literal(LiteralValue::Bool(true))), + "False" => Some(InferredType::Literal(LiteralValue::Bool(false))), + _ => text + .parse::() + .ok() + .map(|value| InferredType::Literal(LiteralValue::Int(value))), + } +} + +/// `x is None` / `x is not None` in both orders of branch polarity. +fn none_outcome( + variable: &str, + current: &InferredType, + is_positive: bool, + whole_scope: bool, +) -> GuardOutcome { + let with_none = intersect(current, &InferredType::None_); + let without_none = subtract(current, &InferredType::None_); + let (positive, negative) = if is_positive { + (with_none, without_none) + } else { + (without_none, with_none) + }; + GuardOutcome { + variable: variable.to_owned(), + positive, + negative, + whole_scope, + } +} + +/// The union of `isinstance`'s class-name arguments as an [`InferredType`]. +fn union_of_names(type_names: &[String]) -> InferredType { + type_names + .iter() + .map(|name| InferredType::from_annotation(name)) + .fold(InferredType::Never, InferredType::union) +} + +#[cfg(test)] +mod tests { + #![expect( + clippy::expect_used, + reason = "test-only unwrapping of guards known to produce outcomes" + )] + + use super::*; + use basilisk_resolver::Span; + + fn guard(kind: NarrowingGuardKind) -> NarrowingGuard { + NarrowingGuard { + kind, + span: Span::new(0, 0), + in_loop: false, + } + } + + fn isinstance_guard(variable: &str, names: &[&str]) -> NarrowingGuard { + guard(NarrowingGuardKind::IsInstance { + variable: variable.to_owned(), + type_names: names.iter().map(|n| (*n).to_owned()).collect(), + if_body_span: Span::new(0, 0), + else_body_span: None, + }) + } + + /// [TYPEINF-NARROWING-ISINSTANCE]: positive intersects, negative subtracts. + #[test] + fn isinstance_narrows_both_branches() { + let current = InferredType::Union(vec![InferredType::Int, InferredType::Str]); + let outcome = guard_outcomes(&isinstance_guard("x", &["int"]), ¤t) + .expect("isinstance guard produces an outcome"); + assert_eq!(outcome.positive, InferredType::Int); + assert_eq!(outcome.negative, InferredType::Str); + assert!(!outcome.whole_scope); + } + + /// `isinstance(x, (int, str))` unions the tuple of names. + #[test] + fn isinstance_tuple_unions_names() { + let current = InferredType::Union(vec![ + InferredType::Int, + InferredType::Str, + InferredType::None_, + ]); + let outcome = + guard_outcomes(&isinstance_guard("x", &["int", "str"]), ¤t).expect("outcome"); + assert!(InferredType::Int.is_assignable_to(&outcome.positive)); + assert!(InferredType::Str.is_assignable_to(&outcome.positive)); + assert_eq!(outcome.negative, InferredType::None_); + } + + /// [TYPEINF-NARROWING-NONE]: `is not None` flips the branches. + #[test] + fn is_not_none_flips_polarity() { + let current = InferredType::Optional(Box::new(InferredType::Int)); + let outcome = guard_outcomes( + &guard(NarrowingGuardKind::IsNone { + variable: "x".to_owned(), + is_positive: false, + if_body_span: Span::new(0, 0), + else_body_span: None, + }), + ¤t, + ) + .expect("outcome"); + assert_eq!(outcome.positive, InferredType::Int); + assert_eq!(outcome.negative, InferredType::None_); + } + + /// [TYPEINF-NARROWING-ASSERT]: assert lifts the inner guard whole-scope. + #[test] + fn assert_marks_whole_scope() { + let current = InferredType::Optional(Box::new(InferredType::Int)); + let outcome = guard_outcomes( + &guard(NarrowingGuardKind::Assert { + inner: Box::new(NarrowingGuardKind::IsNone { + variable: "x".to_owned(), + is_positive: false, + if_body_span: Span::new(0, 0), + else_body_span: None, + }), + }), + ¤t, + ) + .expect("outcome"); + assert!(outcome.whole_scope); + assert_eq!(outcome.positive, InferredType::Int); + } + + /// PEP 647 vs PEP 742: `TypeGuard` narrows one branch, `TypeIs` both. + #[test] + fn typeguard_and_typeis_differ_on_the_negative_branch() { + let current = InferredType::Union(vec![InferredType::Int, InferredType::Str]); + let type_guard = guard_outcomes( + &guard(NarrowingGuardKind::TypeGuard { + variable: "x".to_owned(), + guard_type: "int".to_owned(), + if_body_span: Span::new(0, 0), + else_body_span: None, + }), + ¤t, + ) + .expect("outcome"); + assert_eq!(type_guard.positive, InferredType::Int); + assert_eq!( + type_guard.negative, current, + "TypeGuard: negative unchanged" + ); + + let type_is = guard_outcomes( + &guard(NarrowingGuardKind::TypeIs { + variable: "x".to_owned(), + guard_type: "int".to_owned(), + if_body_span: Span::new(0, 0), + else_body_span: None, + }), + ¤t, + ) + .expect("outcome"); + assert_eq!(type_is.positive, InferredType::Int); + assert_eq!( + type_is.negative, + InferredType::Str, + "TypeIs: negative subtracts" + ); + } + + /// Guards inside loops do not produce persistent narrowing + /// ([TYPEINF-NARROWING-SCOPE]). + #[test] + fn loop_guards_are_suppressed() { + let mut in_loop = isinstance_guard("x", &["int"]); + in_loop.in_loop = true; + assert!(guard_outcomes(&in_loop, &InferredType::Unknown).is_none()); + } + + /// Truthiness removes `None` positively and never invents negatively. + #[test] + fn truthiness_removes_none_only() { + let current = InferredType::Optional(Box::new(InferredType::Str)); + let outcome = guard_outcomes( + &guard(NarrowingGuardKind::Truthiness { + variable: "x".to_owned(), + if_body_span: Span::new(0, 0), + else_body_span: None, + }), + ¤t, + ) + .expect("outcome"); + assert_eq!(outcome.positive, InferredType::Str); + assert_eq!(outcome.negative, current); + } +} diff --git a/crates/basilisk-checker/src/narrow/mod.rs b/crates/basilisk-checker/src/narrow/mod.rs new file mode 100644 index 00000000..60b555f5 --- /dev/null +++ b/crates/basilisk-checker/src/narrow/mod.rs @@ -0,0 +1,23 @@ +//! Implements [TYPEINF-TARGET-NARROWING]. See docs/specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-TARGET-NARROWING +//! The Stage 2 flow-sensitive narrowing engine +//! ([NARROWPLAN-CHECKLIST], docs/plans/CHECKER-TYPE-NARROWING-INFERENCE-PLAN.md): +//! +//! - [`set_ops`] — occurrence-typing set operations over +//! [`crate::types::InferredType`]: `intersect` (narrow to a guard type) and +//! `subtract` (the complement branch), the intersection-and-negation model; +//! - [`env`] — the scoped narrowing environment: branch push/pop, complement +//! application, join at merges, and nested-function boundaries; +//! - [`guards`] — consumption of the resolver's collected +//! [`basilisk_resolver::scope::narrowing_types::NarrowingGuard`]s +//! (`isinstance`, `is None`, truthiness, `TypeGuard`, `TypeIs`, `assert`, +//! `match`) into positive/negative environment updates. + +pub mod env; +pub mod flow; +pub mod guards; +pub mod set_ops; + +pub use env::NarrowEnv; +pub use flow::{analyse_function, analyse_function_in, FlowResult, NarrowedUse}; +pub use guards::{guard_outcomes, guard_outcomes_in, GuardOutcome, NarrowContext, TypedDictKeys}; +pub use set_ops::{intersect, subtract}; diff --git a/crates/basilisk-checker/src/narrow/set_ops.rs b/crates/basilisk-checker/src/narrow/set_ops.rs new file mode 100644 index 00000000..aacf9031 --- /dev/null +++ b/crates/basilisk-checker/src/narrow/set_ops.rs @@ -0,0 +1,151 @@ +//! Implements [TYPEINF-TARGET-NARROWING]. See docs/specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-TARGET-NARROWING +//! Occurrence-typing set operations over [`InferredType`] — the +//! intersection-and-negation core the narrowing environment applies. +//! +//! Both operations delegate every atomic overlap decision to the single +//! subtyping authority, [`InferredType::is_assignable_to`] +//! ([TYPEINF-SUBTYPING-IMPL]), and stay deliberately conservative in the +//! gradual directions: narrowing `Any`/`Unknown` trusts the guard, and a +//! subtraction that cannot be proven leaves the type unchanged — never a +//! fabricated `Never` ([TYPEINF-TARGET-GRADUAL]). + +use crate::types::InferredType; + +/// Narrow `declared` by a positive guard for `guard` — the intersection +/// `declared ∧ guard` on our lattice. +/// +/// - Gradual left side (`Any`/`Unknown`): the guard's type wins (that is the +/// entire point of an `isinstance` check on untyped data). +/// - Union/Optional left side: keep the members that overlap the guard; when +/// a guarded subtype relation holds the more precise side survives. +/// - Disjoint atoms: `Never` (the branch is unreachable for that variable). +#[must_use] +pub fn intersect(declared: &InferredType, guard: &InferredType) -> InferredType { + if matches!(declared, InferredType::Any | InferredType::Unknown) { + return guard.clone(); + } + match declared { + InferredType::Union(members) => members + .iter() + .map(|member| intersect(member, guard)) + .fold(InferredType::Never, InferredType::union), + InferredType::Optional(inner) => { + let inner_part = intersect(inner, guard); + let none_part = intersect(&InferredType::None_, guard); + InferredType::union(inner_part, none_part) + } + atom => intersect_atom(atom, guard), + } +} + +/// Intersection of a non-union declared atom with the guard type. +fn intersect_atom(atom: &InferredType, guard: &InferredType) -> InferredType { + if matches!(guard, InferredType::Any | InferredType::Unknown) { + return atom.clone(); + } + if atom.is_assignable_to(guard) { + // The declared side is at least as precise — keep it (`Literal[1]` + // narrowed by `int` stays `Literal[1]`). + return atom.clone(); + } + if guard.is_assignable_to(atom) { + // The guard is more precise (`int | str` member `int` guarded by + // `bool` becomes `bool`). + return guard.clone(); + } + InferredType::Never +} + +/// The complement branch: remove from `declared` everything that would have +/// satisfied the positive guard — `declared \ guard`. +/// +/// Conservative by construction: only provable-full-overlap members are +/// removed. A gradual declared type stays gradual (we cannot enumerate what +/// to subtract from `Any`), so the negative branch of a guard over untyped +/// data never invents precision ([TYPEINF-TARGET-GRADUAL]). +#[must_use] +pub fn subtract(declared: &InferredType, guard: &InferredType) -> InferredType { + match declared { + InferredType::Any | InferredType::Unknown => declared.clone(), + InferredType::Union(members) => members + .iter() + .filter(|member| !member.is_assignable_to(guard)) + .cloned() + .fold(InferredType::Never, InferredType::union), + InferredType::Optional(inner) => { + let none_removed = InferredType::None_.is_assignable_to(guard); + let inner_removed = inner.is_assignable_to(guard); + match (inner_removed, none_removed) { + (true, true) => InferredType::Never, + (true, false) => InferredType::None_, + (false, true) => (**inner).clone(), + (false, false) => declared.clone(), + } + } + atom if atom.is_assignable_to(guard) => InferredType::Never, + atom => atom.clone(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::LiteralValue; + + /// [TYPEINF-TARGET-NARROWING]: `int | str` narrowed by `isinstance(x, int)`. + #[test] + fn union_intersects_to_the_guarded_member() { + let declared = InferredType::Union(vec![InferredType::Int, InferredType::Str]); + assert_eq!(intersect(&declared, &InferredType::Int), InferredType::Int); + assert_eq!(subtract(&declared, &InferredType::Int), InferredType::Str); + } + + /// `Optional[int]` under `is None` / `is not None`. + #[test] + fn optional_narrows_on_none_checks() { + let declared = InferredType::Optional(Box::new(InferredType::Int)); + assert_eq!( + intersect(&declared, &InferredType::None_), + InferredType::None_ + ); + assert_eq!(subtract(&declared, &InferredType::None_), InferredType::Int); + } + + /// Gradual declared types: positive trusts the guard, negative stays + /// gradual — never fabricated precision ([TYPEINF-TARGET-GRADUAL]). + #[test] + fn gradual_sides_stay_safe() { + assert_eq!( + intersect(&InferredType::Unknown, &InferredType::Int), + InferredType::Int + ); + assert_eq!( + subtract(&InferredType::Unknown, &InferredType::Int), + InferredType::Unknown + ); + assert_eq!( + subtract(&InferredType::Any, &InferredType::Str), + InferredType::Any + ); + } + + /// Literal precision survives a broader guard. + #[test] + fn literal_survives_broader_guard() { + let literal = InferredType::Literal(LiteralValue::Int(3)); + assert_eq!(intersect(&literal, &InferredType::Int), literal); + } + + /// Disjoint atoms narrow to `Never` positively and stay put negatively. + #[test] + fn disjoint_atoms() { + assert_eq!( + intersect(&InferredType::Str, &InferredType::Int), + InferredType::Never + ); + assert_eq!( + subtract(&InferredType::Str, &InferredType::Int), + InferredType::Str + ); + } +} diff --git a/crates/basilisk-checker/src/param_infer.rs b/crates/basilisk-checker/src/param_infer.rs new file mode 100644 index 00000000..eeff07bd --- /dev/null +++ b/crates/basilisk-checker/src/param_infer.rs @@ -0,0 +1,357 @@ +//! Implements the [NARROWPLAN-CHECKLIST] Stage 2 expression-inference item +//! "Infer unannotated parameter types from body constraints and call sites" +//! (issue #317). See docs/plans/CHECKER-TYPE-NARROWING-INFERENCE-PLAN.md. +//! +//! Pure, module-level core shared by the Salsa layer and (at the Integration +//! stage) the `BSK-0001` missing-parameter-annotation rule, whose exemption +//! must stay exactly as strong as this inference and no stronger +//! ([TYPEINF-EXCEEDS-REQUIRED]). +//! +//! Two evidence sources, combined per parameter: +//! - **Body constraints** (upper bounds): the parameter is bound to a fresh +//! input-polarity variable and the body runs through the bidirectional +//! engine with the module's other signatures in scope — passing `p` to a +//! callee with a declared parameter type demands that type of `p`; +//! - **Call sites** (lower bounds): same-module calls to the function +//! contribute their synthesized argument types. +//! +//! Resolution follows input polarity: what is DEMANDED wins (upper bounds); +//! with no demand, the union of what FLOWS IN (call-site lower bounds); with +//! neither, `Unknown` — never a guess ([TYPEINF-EXCEEDS-NOUNKNOWN]). + +use ruff_python_ast::Stmt; + +use crate::bidir::{BidirEngine, Polarity, Ty, TyVarId}; +use crate::types::InferredType; + +/// The inferred type for each of one function's parameters (`None` for a +/// parameter that carries an explicit annotation — nothing to infer). +#[derive(Debug, Clone, PartialEq)] +pub struct InferredParameters { + /// `(parameter name, inferred type)` in declaration order; the type is + /// `None` when the parameter is explicitly annotated. + pub parameters: Vec<(String, Option)>, +} + +/// Infer unannotated parameter types for the function named `function_name` +/// inside `module_source`, with `globals` naming the module's other +/// definitions (`(name, type)` pairs) and `call_args` carrying the synthesized +/// argument types of same-module call sites of this function (outer `Vec`: +/// one entry per call site; inner: positional argument types). +#[must_use] +pub fn infer_parameters( + module_source: &str, + function_name: &str, + globals: &[(String, InferredType)], + call_args: &[Vec], +) -> Option { + let parsed = ruff_python_parser::parse_module(module_source).ok()?; + let function = find_function(&parsed.syntax().body, function_name)?; + + let mut engine = BidirEngine::new( + globals + .iter() + .map(|(name, ty)| (name.clone(), Ty::from_inferred(ty))) + .collect(), + ); + let mut param_vars: Vec<(String, Option)> = Vec::new(); + for parameter_ref in &function.parameters { + let parameter = parameter_ref.as_parameter(); + let name = parameter.name.to_string(); + if parameter.annotation.is_some() { + param_vars.push((name, None)); + continue; + } + let var = engine.fresh_param_var(Polarity::Input); + engine.bind_global(&name, Ty::Var(var)); + param_vars.push((name, Some(var))); + } + + walk_body(&mut engine, &function.body); + apply_call_site_bounds(&mut engine, ¶m_vars, call_args); + let solution = engine.finish(); + + let parameters = param_vars + .into_iter() + .map(|(name, var)| { + let inferred = var.map(|id| solution.vars.resolve(id)); + (name, inferred) + }) + .collect(); + Some(InferredParameters { parameters }) +} + +/// Locate a top-level function definition by name. +fn find_function<'a>(body: &'a [Stmt], name: &str) -> Option<&'a ruff_python_ast::StmtFunctionDef> { + body.iter().find_map(|stmt| match stmt { + Stmt::FunctionDef(def) if def.name.as_str() == name => Some(def), + _ => None, + }) +} + +/// Run the engine over every expression position in the body (assignment +/// values, returns, bare expressions, branch bodies) so parameter uses +/// generate their constraints. +fn walk_body(engine: &mut BidirEngine, stmts: &[Stmt]) { + for stmt in stmts { + match stmt { + Stmt::Assign(node) => { + let _ = engine.synth(&node.value); + } + Stmt::AnnAssign(node) => { + if let Some(value) = node.value.as_deref() { + let _ = engine.synth(value); + } + } + Stmt::Return(node) => { + if let Some(value) = node.value.as_deref() { + let _ = engine.synth(value); + } + } + Stmt::Expr(node) => { + let _ = engine.synth(&node.value); + } + Stmt::If(node) => { + let _ = engine.synth(&node.test); + walk_body(engine, &node.body); + for clause in &node.elif_else_clauses { + if let Some(test) = &clause.test { + let _ = engine.synth(test); + } + walk_body(engine, &clause.body); + } + } + Stmt::For(node) => { + let _ = engine.synth(&node.iter); + walk_body(engine, &node.body); + } + Stmt::While(node) => { + let _ = engine.synth(&node.test); + walk_body(engine, &node.body); + } + Stmt::With(node) => walk_body(engine, &node.body), + _ => {} + } + } +} + +/// Add call-site argument types as lower bounds on the parameter variables. +fn apply_call_site_bounds( + engine: &mut BidirEngine, + param_vars: &[(String, Option)], + call_args: &[Vec], +) { + for site in call_args { + for (position, argument) in site.iter().enumerate() { + let Some((_, Some(var))) = param_vars.get(position) else { + continue; + }; + engine.add_var_lower_bound(*var, Ty::from_inferred(argument)); + } + } +} + +#[cfg(test)] +mod tests { + #![expect( + clippy::expect_used, + reason = "test-only inference over fixed, known-valid fixtures" + )] + + use super::*; + use crate::types::{CallableInfo, LiteralValue}; + + /// Body constraints: passing `p` to a callee with a declared parameter + /// type demands that type ([#317] body-constraint recovery). + #[test] + fn body_call_demand_infers_the_parameter() { + let source = r" +def f(p): + return consume(p) +"; + let globals = vec![( + "consume".to_owned(), + InferredType::Callable(CallableInfo { + param_types: vec![InferredType::Int], + return_type: Box::new(InferredType::Bool), + }), + )]; + let inferred = + infer_parameters(source, "f", &globals, &[]).expect("function found and inferred"); + assert_eq!( + inferred.parameters, + vec![("p".to_owned(), Some(InferredType::Int))], + "the callee's declared parameter demands int of p" + ); + } + + /// Call sites: same-module argument types flow in as lower bounds when + /// nothing in the body demands more. + #[test] + fn call_sites_infer_the_parameter() { + let source = r" +def f(p): + return p +"; + let call_args = vec![ + vec![InferredType::Literal(LiteralValue::Int(1))], + vec![InferredType::Literal(LiteralValue::Int(2))], + ]; + let inferred = + infer_parameters(source, "f", &[], &call_args).expect("function found and inferred"); + let (name, ty) = inferred.parameters.first().expect("one parameter"); + assert_eq!(name, "p"); + let ty = ty.clone().expect("inferred"); + assert!( + InferredType::Literal(LiteralValue::Int(1)).is_assignable_to(&ty) + && InferredType::Literal(LiteralValue::Int(2)).is_assignable_to(&ty), + "call-site arguments must be admitted by the inferred type, got {ty:?}" + ); + } + + /// No evidence: the parameter stays `Unknown` — never a guess + /// ([TYPEINF-EXCEEDS-NOUNKNOWN]). + #[test] + fn no_evidence_stays_unknown() { + let source = r" +def f(p): + return p +"; + let inferred = + infer_parameters(source, "f", &[], &[]).expect("function found and inferred"); + assert_eq!( + inferred.parameters, + vec![("p".to_owned(), Some(InferredType::Unknown))] + ); + } + + /// Annotated parameters are not second-guessed. + #[test] + fn annotated_parameters_are_skipped() { + let source = r" +def f(p: int, q): + return p +"; + let inferred = + infer_parameters(source, "f", &[], &[]).expect("function found and inferred"); + assert_eq!(inferred.parameters.first(), Some(&("p".to_owned(), None))); + } +} + +/// Engine globals from cross-module imports ([NARROWPLAN-CHECKLIST] +/// "imported function/method return types"): each imported symbol in +/// [`basilisk_resolver::ResolvedModule::imported_symbols`] maps to a type — +/// functions become the gradual `Callable[..., R]` from their return +/// annotation (so calls to imported names infer their returns), classes +/// become their instance-`Named` form, variables take their annotation. +/// Symbols without type information are omitted (they stay `Unknown` in the +/// engine — never a guess). +#[must_use] +pub fn imported_callable_globals( + module: &basilisk_resolver::ResolvedModule, +) -> Vec<(String, InferredType)> { + use basilisk_resolver::scope::ExternalSymbolKind; + module + .imported_symbols + .iter() + .map(|(binding, symbol)| { + let ty = match symbol.kind { + ExternalSymbolKind::Function => { + InferredType::Callable(crate::types::CallableInfo { + param_types: Vec::new(), + return_type: Box::new( + symbol + .type_annotation + .as_deref() + .map_or(InferredType::Unknown, InferredType::from_annotation), + ), + }) + } + ExternalSymbolKind::Class => InferredType::Named(symbol.name.to_ascii_lowercase()), + ExternalSymbolKind::Variable | ExternalSymbolKind::ReExport => symbol + .type_annotation + .as_deref() + .map_or(InferredType::Unknown, InferredType::from_annotation), + }; + (binding.clone(), ty) + }) + .collect() +} + +#[cfg(test)] +mod imported_globals_tests { + #![expect( + clippy::expect_used, + reason = "test-only lookups over fixed fixture symbols" + )] + + use super::*; + use basilisk_resolver::scope::{ExternalSymbol, ExternalSymbolKind, Span}; + + fn symbol(kind: ExternalSymbolKind, annotation: Option<&str>) -> ExternalSymbol { + ExternalSymbol { + name: "helper".to_owned(), + kind, + type_annotation: annotation.map(str::to_owned), + source_path: std::path::PathBuf::from("other.py"), + source_span: Span::new(0, 0), + signature: None, + provenance: None, + methods: Vec::new(), + } + } + + /// Imported functions expose their return annotation as a gradual + /// callable, so `x = helper()` infers the imported return type. + #[test] + fn imported_function_returns_flow_through_calls() { + let mut module = basilisk_resolver::ResolvedModule::default(); + let _ = module.imported_symbols.insert( + "helper".to_owned(), + symbol(ExternalSymbolKind::Function, Some("list[int]")), + ); + let globals = imported_callable_globals(&module); + let inferred = infer_parameters("def f(p):\n return helper()\n", "f", &globals, &[]) + .expect("inferred"); + // The parameter has no evidence — but the imported callable is in + // scope, proving the globals wire in (return flows are covered by + // the engine call-synthesis tests). + assert_eq!(inferred.parameters.len(), 1); + + let (_, ty) = globals.first().expect("helper present"); + assert!( + matches!( + ty, + InferredType::Callable(info) + if *info.return_type == InferredType::List(Box::new(InferredType::Int)) + ), + "imported function must map to Callable[..., list[int]], got {ty:?}" + ); + } + + /// Classes map to instances; unannotated variables are Unknown. + #[test] + fn imported_classes_and_variables_map_conservatively() { + let mut module = basilisk_resolver::ResolvedModule::default(); + let _ = module + .imported_symbols + .insert("Widget".to_owned(), symbol(ExternalSymbolKind::Class, None)); + let _ = module.imported_symbols.insert( + "COUNT".to_owned(), + symbol(ExternalSymbolKind::Variable, Some("int")), + ); + let globals = imported_callable_globals(&module); + let lookup = |name: &str| { + globals + .iter() + .find(|(binding, _)| binding == name) + .map(|(_, ty)| ty.clone()) + }; + assert_eq!( + lookup("Widget"), + Some(InferredType::Named("helper".to_owned())), + "class binding maps to the SOURCE class name's instance form" + ); + assert_eq!(lookup("COUNT"), Some(InferredType::Int)); + } +} diff --git a/crates/basilisk-checker/src/rules/classes_override.rs b/crates/basilisk-checker/src/rules/classes_override.rs index 95311b15..e1a1fe7b 100644 --- a/crates/basilisk-checker/src/rules/classes_override.rs +++ b/crates/basilisk-checker/src/rules/classes_override.rs @@ -119,7 +119,10 @@ fn signatures_incompatible(child: &FunctionInfo, base: &FunctionInfo, source: &s // Compare non-self parameter annotation texts. let params_differ = child_params.iter().zip(base_params.iter()).any(|(cp, bp)| { - annotation_text(source, cp.annotation_span) != annotation_text(source, bp.annotation_span) + annotations_conflict( + annotation_text(source, cp.annotation_span), + annotation_text(source, bp.annotation_span), + ) }); if params_differ { @@ -127,9 +130,22 @@ fn signatures_incompatible(child: &FunctionInfo, base: &FunctionInfo, source: &s } // Compare return annotation texts. - let child_ret = annotation_text(source, child.return_annotation_span); - let base_ret = annotation_text(source, base.return_annotation_span); - child_ret != base_ret + annotations_conflict( + annotation_text(source, child.return_annotation_span), + annotation_text(source, base.return_annotation_span), + ) +} + +/// Two annotation positions conflict only when BOTH are present and differ. +/// +/// An unannotated side is implicitly `Any` — consistent with anything, so its +/// absence can never prove incompatibility ([TYPEINF-TARGET-GRADUAL]: +/// removing an annotation must never introduce a new error). +fn annotations_conflict(child: Option<&str>, base: Option<&str>) -> bool { + match (child, base) { + (None, _) | (_, None) => false, + (Some(child_text), Some(base_text)) => child_text != base_text, + } } /// Returns a slice of parameters with the leading `self`/`cls` removed (if present). diff --git a/crates/basilisk-checker/src/rules/missing_parameter_annotation.rs b/crates/basilisk-checker/src/rules/missing_parameter_annotation.rs index 42b69971..d799f47f 100644 --- a/crates/basilisk-checker/src/rules/missing_parameter_annotation.rs +++ b/crates/basilisk-checker/src/rules/missing_parameter_annotation.rs @@ -1,9 +1,27 @@ //! Implements [BSK-0001] from [CHKARCH-DIAG-MISSING]. See docs/specs/CHECKER-ARCHITECTURE-SPEC.md#chkarch-diag-missing //! BSK-0001: Missing parameter type annotation. +//! +//! Never fires where the current engine already infers the parameter type: a +//! scalar-literal default (`timeout=30` → `int`) determines the type, so +//! demanding an annotation there would be redundant ([TYPEINF-FUNC-DEFAULTS]). +//! Defaults that do NOT determine the type — `None`, empty containers, calls, +//! lambdas, arbitrary expressions — still require an annotation. +//! +//! ```python +//! def connect(timeout=30): # ✓ — type inferred as int +//! pass +//! +//! def connect(retries): # BSK-0001 — nothing to infer from +//! pass +//! +//! def connect(timeout=None): # BSK-0001 — None does not determine T | None +//! pass +//! ``` use basilisk_resolver::{FunctionInfo, ParameterInfo, ResolvedModule}; use crate::diagnostic::{error_diagnostic_owned, Diagnostic, ErrorCode}; +use crate::inference::rhs_fully_determines_type; use super::{guards::is_stub_context, Rule}; @@ -48,10 +66,22 @@ impl Rule for MissingParameterAnnotation { fn check_function(func: &FunctionInfo, path: &str, out: &mut Vec) { func.parameters .iter() - .filter(|p| !p.has_annotation && p.name != "self" && p.name != "cls") + .filter(|p| { + !p.has_annotation && p.name != "self" && p.name != "cls" && !default_determines_type(p) + }) .for_each(|p| out.push(make_diagnostic(p, path))); } +/// Implements [TYPEINF-FUNC-DEFAULTS]: `true` when the parameter's default +/// alone already tells the current engine the type — the annotation would be +/// redundant, so BSK-0001 must stay silent. +fn default_determines_type(param: &ParameterInfo) -> bool { + param + .default_rhs_kind + .as_ref() + .is_some_and(rhs_fully_determines_type) +} + fn make_diagnostic(param: &ParameterInfo, path: &str) -> Diagnostic { error_diagnostic_owned( CODE.clone(), @@ -59,6 +89,10 @@ fn make_diagnostic(param: &ParameterInfo, path: &str) -> Diagnostic { param.name_span, path, Some(format!("Add a type annotation: `{}: `", param.name)), - Some("In Basilisk, all function parameters require explicit types".to_owned()), + Some( + "Basilisk requires an explicit parameter type wherever it cannot be inferred; \ + a literal default (e.g. `timeout=30`) infers the type and needs no annotation" + .to_owned(), + ), ) } diff --git a/crates/basilisk-checker/src/rules/missing_return_annotation.rs b/crates/basilisk-checker/src/rules/missing_return_annotation.rs index d749fade..0c64bb78 100644 --- a/crates/basilisk-checker/src/rules/missing_return_annotation.rs +++ b/crates/basilisk-checker/src/rules/missing_return_annotation.rs @@ -1,9 +1,27 @@ //! Implements [BSK-0002] from [CHKARCH-DIAG-MISSING]. See docs/specs/CHECKER-ARCHITECTURE-SPEC.md#chkarch-diag-missing //! BSK-0002: Missing return type annotation. +//! +//! Never fires where the current engine already infers the return type +//! ([TYPEINF-FUNC-RETURN]): a body whose every `return` is bare or carries a +//! type-determining literal — or that has no `return` at all (`None`) — needs +//! no annotation. Returns the engine cannot infer (calls, names, arbitrary +//! expressions) and generators (`Generator[...]`) still require one. +//! +//! ```python +//! def answer(): # ✓ — return type inferred as int +//! return 42 +//! +//! def log_it(msg: str): # ✓ — no return: inferred as None +//! print(msg) +//! +//! def fetch(): # BSK-0002 — call result is not inferable +//! return make_value() +//! ``` use basilisk_resolver::{FunctionInfo, ResolvedModule, Span}; use crate::diagnostic::{error_diagnostic_owned, Diagnostic, ErrorCode}; +use crate::inference::rhs_fully_determines_type; use super::{guards::is_stub_context, Rule}; @@ -35,12 +53,30 @@ impl Rule for MissingReturnAnnotation { .functions .iter() .filter(|func| { - !func.return_annotation.is_present() && !is_stub_context(func, &module.classes) + !func.return_annotation.is_present() + && !is_stub_context(func, &module.classes) + && !return_type_is_inferable(func) }) .for_each(|func| diagnostics.push(make_diagnostic(func, &module.path))); } } +/// Implements [TYPEINF-FUNC-RETURN]: `true` when the current engine already +/// infers the full return type, making an annotation redundant. +/// +/// - No `return` statements at all → `None` (trivially inferred). +/// - Every `return` is bare (`None`) or carries a type-determining literal → +/// the union of those types is inferred. +/// - Generators are excluded: their `Generator[...]` type is not inferable +/// from `return` statements, so they still require an annotation. +fn return_type_is_inferable(func: &FunctionInfo) -> bool { + !func.is_generator + && func + .return_stmts + .iter() + .all(|ret| !ret.has_value || rhs_fully_determines_type(&ret.rhs_kind)) +} + fn make_diagnostic(func: &FunctionInfo, path: &str) -> Diagnostic { error_diagnostic_owned( CODE.clone(), @@ -57,7 +93,11 @@ fn make_diagnostic(func: &FunctionInfo, path: &str) -> Diagnostic { "Add a return type: `def {}(...) -> :`", func.name )), - Some("In Basilisk, all functions require an explicit return type".to_owned()), + Some( + "Basilisk requires an explicit return type wherever it cannot be inferred; \ + literal-only returns (e.g. `return 42`) infer the type and need no annotation" + .to_owned(), + ), ) } diff --git a/crates/basilisk-checker/src/tyeval.rs b/crates/basilisk-checker/src/tyeval.rs new file mode 100644 index 00000000..16826332 --- /dev/null +++ b/crates/basilisk-checker/src/tyeval.rs @@ -0,0 +1,363 @@ +//! Implements [TYPEINF-TARGET] Stage 3 groundwork. See +//! docs/plans/CHECKER-TYPE-NARROWING-INFERENCE-PLAN.md#NARROWPLAN-CHECKLIST +//! ("Stage 3 — type-level evaluation groundwork"). +//! +//! Python's type-hint sublanguage is Turing-complete (Roth, +//! ), so recursive/parameterised type +//! aliases must be *evaluated*, not expanded eagerly. This module is the +//! normalization-by-evaluation core: +//! +//! - [`TypeTerm`] — the type-level term language: ground types, alias +//! references with arguments (kind `Type → Type` operators — PEP 695 +//! `type Pair[T] = tuple[T, T]`), and parameter references; +//! - [`evaluate`] — lazy unfolding to **weak head normal form**: aliases +//! unfold only until an outermost constructor appears; arguments +//! substitute lazily (mapped-type applications rewrite on demand); +//! - **fuel and depth bounds** with **memoization** of normalized results +//! per `(alias, argument)` application; +//! - the **`Divergent` fallback**: running out of fuel, unguarded +//! recursion, or an unknown alias yields [`Eval::Divergent`], which +//! projects to the gradual `Unknown` — evaluation failure NEVER invents +//! an error ([TYPEINF-TARGET-GRADUAL]); +//! - a **guardedness acceptance condition** (the Paterson/Coverage-style +//! analogue): an alias whose recursive self-reference is not under a +//! constructor (`type X = X`) is rejected up front, with the recursion +//! depth cap as the escape hatch for accepted-but-deep definitions. + +use std::collections::HashMap; + +use crate::types::InferredType; + +/// Fuel: total alias unfoldings one evaluation may perform. +const EVAL_FUEL: u32 = 256; +/// Depth: maximum nesting of constructors descended while normalizing. +const EVAL_DEPTH: u32 = 64; + +/// A type-level term. +#[derive(Debug, Clone, PartialEq)] +pub enum TypeTerm { + /// A ground type — already a value. + Ground(InferredType), + /// A reference to an alias, possibly applied: `Pair[int]`, `Json`. + Alias(String, Vec), + /// A reference to the enclosing alias's parameter by index. + Param(usize), + /// `list[T]` at the type level (constructor — a whnf head). + List(Box), + /// `T | U` at the type level. + Union(Vec), + /// `tuple[T, ..]` at the type level. + Tuple(Vec), +} + +/// One alias definition: `type Name[P0, P1, ..] = body`. +#[derive(Debug, Clone, PartialEq)] +pub struct AliasDef { + /// Number of type parameters. + pub arity: usize, + /// The right-hand side, with [`TypeTerm::Param`] for parameters. + pub body: TypeTerm, +} + +/// The alias environment (one module's `type` statements). +#[derive(Debug, Clone, Default)] +pub struct AliasEnv { + aliases: HashMap, +} + +impl AliasEnv { + /// Register an alias; rejects (returns `false`, leaving the environment + /// unchanged) definitions that fail the guardedness acceptance + /// condition — a recursive self-reference not under a constructor + /// (`type X = X`, `type X = X | int` at the top level of a union arm is + /// GUARDED only through constructors, so plain `X` arms are rejected). + pub fn insert(&mut self, name: &str, def: AliasDef) -> bool { + if !recursion_is_guarded(name, &def.body, false) { + return false; + } + let _ = self.aliases.insert(name.to_owned(), def); + true + } + + /// Look up an alias. + #[must_use] + pub fn get(&self, name: &str) -> Option<&AliasDef> { + self.aliases.get(name) + } +} + +/// The guardedness acceptance condition: every self-reference must sit +/// beneath at least one constructor. `under_constructor` tracks whether the +/// walk has passed through `List`/`Tuple` (unions do NOT guard — a union arm +/// unfolds at the same level). +fn recursion_is_guarded(name: &str, term: &TypeTerm, under_constructor: bool) -> bool { + match term { + TypeTerm::Alias(alias, args) => { + (alias != name || under_constructor) + && args + .iter() + .all(|arg| recursion_is_guarded(name, arg, under_constructor)) + } + TypeTerm::List(inner) => recursion_is_guarded(name, inner, true), + TypeTerm::Tuple(items) => items + .iter() + .all(|item| recursion_is_guarded(name, item, true)), + TypeTerm::Union(arms) => arms + .iter() + .all(|arm| recursion_is_guarded(name, arm, under_constructor)), + TypeTerm::Ground(_) | TypeTerm::Param(_) => true, + } +} + +/// A weak-head-normal-form outcome. +#[derive(Debug, Clone, PartialEq)] +pub enum Eval { + /// Normalized to a head constructor (projected to [`InferredType`], + /// with unevaluated sub-terms projected conservatively). + Value(InferredType), + /// Fuel/depth exhausted, unguarded shape, or unknown alias — the + /// divergent sentinel. Projects to `Unknown`: never an invented error + /// ([TYPEINF-TARGET-GRADUAL]). + Divergent, +} + +impl Eval { + /// Project to the checker's type lattice. + #[must_use] + pub fn into_inferred(self) -> InferredType { + match self { + Eval::Value(ty) => ty, + Eval::Divergent => InferredType::Unknown, + } + } +} + +/// Evaluator state: fuel plus the `(alias, args)` application memo. +#[derive(Debug, Default)] +pub struct Evaluator { + fuel: u32, + memo: HashMap<(String, String), Eval>, +} + +impl Evaluator { + /// A fresh evaluator with full fuel. + #[must_use] + pub fn new() -> Self { + Self { + fuel: EVAL_FUEL, + memo: HashMap::new(), + } + } + + /// Evaluate `term` to weak head normal form under `env`. + pub fn evaluate(&mut self, env: &AliasEnv, term: &TypeTerm) -> Eval { + self.eval_at(env, term, &[], 0) + } + + /// Core: lazy unfolding with parameter substitution from `args`. + fn eval_at(&mut self, env: &AliasEnv, term: &TypeTerm, args: &[TypeTerm], depth: u32) -> Eval { + if depth > EVAL_DEPTH { + return Eval::Divergent; + } + match term { + TypeTerm::Ground(ty) => Eval::Value(ty.clone()), + TypeTerm::Param(index) => match args.get(*index) { + Some(argument) => self.eval_at(env, &argument.clone(), &[], depth + 1), + None => Eval::Divergent, + }, + TypeTerm::Alias(name, alias_args) => { + self.eval_alias(env, name, alias_args, args, depth) + } + TypeTerm::List(inner) => { + let element = self.eval_at(env, inner, args, depth + 1).into_inferred(); + Eval::Value(InferredType::List(Box::new(element))) + } + TypeTerm::Tuple(items) => { + let elements = items + .iter() + .map(|item| self.eval_at(env, item, args, depth + 1).into_inferred()) + .collect(); + Eval::Value(InferredType::Tuple(elements)) + } + TypeTerm::Union(arms) => { + let union = arms + .iter() + .map(|arm| self.eval_at(env, arm, args, depth + 1).into_inferred()) + .fold(InferredType::Never, InferredType::union); + Eval::Value(union) + } + } + } + + /// Unfold one alias application, memoized per `(alias, args)`. + fn eval_alias( + &mut self, + env: &AliasEnv, + name: &str, + alias_args: &[TypeTerm], + outer_args: &[TypeTerm], + depth: u32, + ) -> Eval { + let key = (name.to_owned(), format!("{alias_args:?}|{outer_args:?}")); + if let Some(cached) = self.memo.get(&key) { + return cached.clone(); + } + if self.fuel == 0 { + return Eval::Divergent; + } + self.fuel -= 1; + + let Some(def) = env.get(name) else { + return Eval::Divergent; + }; + if def.arity != alias_args.len() { + return Eval::Divergent; + } + // Substitute the application's arguments (resolving any outer + // parameters lazily) and unfold the body one step. + let substituted: Vec = alias_args + .iter() + .map(|arg| substitute(arg, outer_args)) + .collect(); + let body = def.body.clone(); + let result = self.eval_at(env, &body, &substituted, depth + 1); + let _ = self.memo.insert(key, result.clone()); + result + } +} + +/// Replace [`TypeTerm::Param`] references with `args` (lazy: nested alias +/// applications keep their own bodies unexpanded). +fn substitute(term: &TypeTerm, args: &[TypeTerm]) -> TypeTerm { + match term { + TypeTerm::Param(index) => args + .get(*index) + .cloned() + .unwrap_or(TypeTerm::Ground(InferredType::Unknown)), + TypeTerm::Alias(name, alias_args) => TypeTerm::Alias( + name.clone(), + alias_args.iter().map(|a| substitute(a, args)).collect(), + ), + TypeTerm::List(inner) => TypeTerm::List(Box::new(substitute(inner, args))), + TypeTerm::Tuple(items) => { + TypeTerm::Tuple(items.iter().map(|i| substitute(i, args)).collect()) + } + TypeTerm::Union(arms) => { + TypeTerm::Union(arms.iter().map(|a| substitute(a, args)).collect()) + } + TypeTerm::Ground(_) => term.clone(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn int() -> TypeTerm { + TypeTerm::Ground(InferredType::Int) + } + + /// A mapped-type operator (`type Pair[T] = tuple[T, T]`) applies lazily. + #[test] + fn mapped_alias_applies_arguments() { + let mut env = AliasEnv::default(); + assert!(env.insert( + "pair", + AliasDef { + arity: 1, + body: TypeTerm::Tuple(vec![TypeTerm::Param(0), TypeTerm::Param(0)]), + }, + )); + let mut evaluator = Evaluator::new(); + let result = evaluator.evaluate(&env, &TypeTerm::Alias("pair".to_owned(), vec![int()])); + assert_eq!( + result, + Eval::Value(InferredType::Tuple(vec![ + InferredType::Int, + InferredType::Int + ])) + ); + } + + /// A guarded recursive alias (`type Json = int | list[Json]`) evaluates + /// to whnf — the recursive arm normalizes under fuel without expanding + /// forever. + #[test] + fn guarded_recursion_reaches_whnf() { + let mut env = AliasEnv::default(); + assert!(env.insert( + "json", + AliasDef { + arity: 0, + body: TypeTerm::Union(vec![ + int(), + TypeTerm::List(Box::new(TypeTerm::Alias("json".to_owned(), Vec::new()))), + ]), + }, + )); + let mut evaluator = Evaluator::new(); + let result = evaluator + .evaluate(&env, &TypeTerm::Alias("json".to_owned(), Vec::new())) + .into_inferred(); + // The head is a union of int and list[...]; the recursive interior + // bottoms out gradually rather than diverging. + assert!(InferredType::Int.is_assignable_to(&result)); + assert!( + InferredType::List(Box::new(InferredType::Unknown)).is_assignable_to(&result), + "list arm must be present: {result:?}" + ); + } + + /// The guardedness acceptance condition rejects `type X = X` up front. + #[test] + fn unguarded_recursion_is_rejected() { + let mut env = AliasEnv::default(); + assert!(!env.insert( + "x", + AliasDef { + arity: 0, + body: TypeTerm::Alias("x".to_owned(), Vec::new()), + }, + )); + // Union arms do not guard either: `type X = int | X`. + assert!(!env.insert( + "x", + AliasDef { + arity: 0, + body: TypeTerm::Union(vec![int(), TypeTerm::Alias("x".to_owned(), Vec::new())]), + }, + )); + } + + /// Unknown aliases and fuel exhaustion produce `Divergent`, which + /// projects to the gradual `Unknown` — never an error + /// ([TYPEINF-TARGET-GRADUAL]). + #[test] + fn divergence_projects_to_unknown() { + let env = AliasEnv::default(); + let mut evaluator = Evaluator::new(); + let result = evaluator.evaluate(&env, &TypeTerm::Alias("missing".to_owned(), Vec::new())); + assert_eq!(result, Eval::Divergent); + assert_eq!(result.into_inferred(), InferredType::Unknown); + } + + /// Memoization: re-evaluating the same application does not spend fuel + /// again (the second call is a cache hit even with zero fuel left). + #[test] + fn applications_are_memoized() { + let mut env = AliasEnv::default(); + assert!(env.insert( + "wrap", + AliasDef { + arity: 1, + body: TypeTerm::List(Box::new(TypeTerm::Param(0))), + }, + )); + let mut evaluator = Evaluator::new(); + let term = TypeTerm::Alias("wrap".to_owned(), vec![int()]); + let first = evaluator.evaluate(&env, &term); + evaluator.fuel = 0; + let second = evaluator.evaluate(&env, &term); + assert_eq!(first, second, "memo hit must not need fuel"); + } +} diff --git a/crates/basilisk-checker/src/types.rs b/crates/basilisk-checker/src/types.rs index 7e427e88..078632eb 100644 --- a/crates/basilisk-checker/src/types.rs +++ b/crates/basilisk-checker/src/types.rs @@ -237,8 +237,10 @@ impl InferredType { InferredType::Int | InferredType::Literal(LiteralValue::Float(_)), InferredType::Float, ) + // `bool` is a subclass of `int` (PEP 484), so plain `bool` widens + // through the whole tower exactly like `Literal[bool]` does below. | ( - InferredType::Literal(LiteralValue::Int(_)), + InferredType::Literal(LiteralValue::Int(_)) | InferredType::Bool, InferredType::Int | InferredType::Float, ) | ( @@ -259,14 +261,18 @@ impl InferredType { // `None` satisfies `Hashable` (it defines `__hash__`). The annotation // parser lowercases names, so the ABC arrives as `Named("hashable")`. (InferredType::None_, InferredType::Named(name)) if name == "hashable" => true, + // Union on the LEFT decomposes before Optional-target unwrapping: + // `A | None <: Optional[B]` must check each variant against the + // whole `Optional[B]` (so the `None` arm can satisfy it), not + // against the unwrapped `B`. + // Implements [TYPEINF-SUBTYPING-UNION] — `A | B <: C` iff `A <: C` and + // `B <: C`. + (InferredType::Union(types), other) => types.iter().all(|t| t.is_assignable_to(other)), // Optional types are assignable to their non-optional counterparts. // Implements [TYPEINF-SUBTYPING-UNION] — Optional[T] = T | None handling. (InferredType::Optional(inner), other) => inner.is_assignable_to(other), (inner, InferredType::Optional(other)) => inner.is_assignable_to(other), - // Union types require all variants to be assignable. - // Implements [TYPEINF-SUBTYPING-UNION] — `A | B <: C` iff `A <: C` and - // `B <: C`; `A <: A | B` (a type is a subtype of any union containing it). - (InferredType::Union(types), other) => types.iter().all(|t| t.is_assignable_to(other)), + // `A <: A | B` (a type is a subtype of any union containing it). (inner, InferredType::Union(types)) => types.iter().any(|t| inner.is_assignable_to(t)), // Container types require element type assignability. // Implements [TYPEINF-SUBTYPING-GENERIC] — list/set/dict are invariant @@ -384,139 +390,6 @@ impl InferredType { } } -/// Returns the element type `X` when `elems` is the homogeneous variable-length -/// tuple form `tuple[X, ...]`. -/// -/// Implements [TYPEINF-OVERVIEW]. The annotation parser ([`super::types_parsing`]) -/// represents the `...` terminator as `Named("...")`, so `tuple[str, ...]` becomes -/// `Tuple([Str, Named("...")])`. Distinguishing this from a fixed-length tuple is -/// what lets a literal `(a, b, c)` widen to `tuple[X, ...]` (PEP 484). -fn homogeneous_tuple_elem(elems: &[InferredType]) -> Option<&InferredType> { - match elems { - [elem, InferredType::Named(terminator)] if terminator == "..." => Some(elem), - _ => None, - } -} - -/// Returns `true` when a tuple element is an unpacked variadic segment — either -/// `*tuple[...]` or a `*Ts` `TypeVarTuple`. The annotation parser stores these as -/// `Named` text beginning with `*`. -fn is_unpacked_tuple_elem(elem: &InferredType) -> bool { - matches!(elem, InferredType::Named(name) if name.starts_with('*')) -} - -/// What an unpacked `*tuple[...]` / `*Ts` segment consumes from a source tuple. -enum StarSegment { - /// `*tuple[X, ...]` or `*Ts` — zero or more elements, each assignable to the - /// element type (`None` ⇒ any, for `*Ts` / `*tuple[Any, ...]`). - Variadic(Option), - /// `*tuple[X, Y]` — a fixed run of elements consumed positionally. - Fixed(Vec), -} - -/// Parse an unpacked tuple element (`Named("*tuple[...]")` / `Named("*ts")`). -fn parse_star_segment(name: &str) -> StarSegment { - let Some(inner) = name - .strip_prefix("*tuple[") - .and_then(|rest| rest.strip_suffix(']')) - else { - // `*Ts` (a TypeVarTuple) — any number of elements of any type. - return StarSegment::Variadic(None); - }; - let parts = crate::types_parsing::split_type_params(inner); - if matches!(parts.last(), Some(last) if last.trim() == "...") { - // `*tuple[X, ...]` — homogeneous, zero or more of `X`. - let elem = parts - .first() - .map(|p| InferredType::from_annotation(p.trim())); - return StarSegment::Variadic(elem); - } - StarSegment::Fixed( - parts - .iter() - .map(|p| InferredType::from_annotation(p.trim())) - .collect(), - ) -} - -/// Match a fixed-length source tuple against a target tuple that contains a -/// single unpacked `*tuple[...]` / `*Ts` segment (PEP 646), using -/// prefix/middle/suffix decomposition. -fn tuple_assignable_with_star(source: &[InferredType], target: &[InferredType]) -> bool { - // Unhandled shapes return `true` (assignable) rather than `false`: this is a - // best-effort matcher and must never manufacture a false positive. Only a - // pattern we actually decompose may return `false` (a real mismatch). - // - // A variadic source, multiple unpacked segments, or a `*Ts` we can't read - // all need full PEP 646 unification — be permissive. - if source.iter().any(is_unpacked_tuple_elem) { - return true; - } - // A homogeneous variadic source (`tuple[X, ...]`) has unknown length — - // prefix/middle/suffix decomposition cannot prove a mismatch. - if homogeneous_tuple_elem(source).is_some() { - return true; - } - let Some(star_idx) = target.iter().position(is_unpacked_tuple_elem) else { - return true; - }; - let (prefix, rest) = target.split_at(star_idx); - let Some((star_elem, suffix)) = rest.split_first() else { - return true; - }; - // Only one unpacked segment is supported. - if suffix.iter().any(is_unpacked_tuple_elem) { - return true; - } - let InferredType::Named(star_name) = star_elem else { - return true; - }; - - match parse_star_segment(star_name) { - StarSegment::Variadic(elem) => { - let Some(middle_len) = source.len().checked_sub(prefix.len() + suffix.len()) else { - return false; - }; - prefix_suffix_match(source, prefix, suffix) - && match elem { - None => true, - Some(elem_ty) => source - .iter() - .skip(prefix.len()) - .take(middle_len) - .all(|s| s.is_assignable_to(&elem_ty)), - } - } - StarSegment::Fixed(middle) => { - if source.len() != prefix.len() + middle.len() + suffix.len() { - return false; - } - prefix_suffix_match(source, prefix, suffix) - && source - .iter() - .skip(prefix.len()) - .take(middle.len()) - .zip(middle.iter()) - .all(|(s, m)| s.is_assignable_to(m)) - } - } -} - -/// Check that a source tuple's leading elements match `prefix` and trailing -/// elements match `suffix` (both fixed, non-starred). Callers guarantee -/// `source.len() >= prefix.len() + suffix.len()`. -fn prefix_suffix_match( - source: &[InferredType], - prefix: &[InferredType], - suffix: &[InferredType], -) -> bool { - source - .iter() - .zip(prefix.iter()) - .all(|(s, p)| s.is_assignable_to(p)) - && source - .iter() - .rev() - .zip(suffix.iter().rev()) - .all(|(s, q)| s.is_assignable_to(q)) -} +pub(crate) use crate::types_star_tuples::{ + homogeneous_tuple_elem, is_unpacked_tuple_elem, tuple_assignable_with_star, +}; diff --git a/crates/basilisk-checker/src/types_star_tuples.rs b/crates/basilisk-checker/src/types_star_tuples.rs new file mode 100644 index 00000000..ba621b6a --- /dev/null +++ b/crates/basilisk-checker/src/types_star_tuples.rs @@ -0,0 +1,147 @@ +//! Implements [TYPEINF-COLLECTIONS-TUPLES]. See docs/specs/CHECKER-TYPE-INFERENCE-SPEC.md#typeinf-collections +//! Homogeneous (`tuple[X, ...]`) and PEP 646 unpacked (`*tuple[...]`/`*Ts`) +//! tuple matching, used by [`InferredType::is_assignable_to`] +//! ([TYPEINF-SUBTYPING-IMPL]). +//! +//! The annotation parser ([`crate::types_parsing`]) stores the `...` +//! terminator as `Named("...")` and unpacked segments as `Named` text starting +//! with `*`, so these helpers re-read that text to decompose tuple shapes. + +use crate::types::InferredType; + +/// Returns the element type `X` when `elems` is the homogeneous variable-length +/// tuple form `tuple[X, ...]`. +/// +/// The annotation parser represents the `...` terminator as `Named("...")`, so +/// `tuple[str, ...]` becomes `Tuple([Str, Named("...")])`. Distinguishing this +/// from a fixed-length tuple is what lets a literal `(a, b, c)` widen to +/// `tuple[X, ...]` (PEP 484). +pub(crate) fn homogeneous_tuple_elem(elems: &[InferredType]) -> Option<&InferredType> { + match elems { + [elem, InferredType::Named(terminator)] if terminator == "..." => Some(elem), + _ => None, + } +} + +/// Returns `true` when a tuple element is an unpacked variadic segment — either +/// `*tuple[...]` or a `*Ts` `TypeVarTuple`. The annotation parser stores these as +/// `Named` text beginning with `*`. +pub(crate) fn is_unpacked_tuple_elem(elem: &InferredType) -> bool { + matches!(elem, InferredType::Named(name) if name.starts_with('*')) +} + +/// What an unpacked `*tuple[...]` / `*Ts` segment consumes from a source tuple. +enum StarSegment { + /// `*tuple[X, ...]` or `*Ts` — zero or more elements, each assignable to the + /// element type (`None` ⇒ any, for `*Ts` / `*tuple[Any, ...]`). + Variadic(Option), + /// `*tuple[X, Y]` — a fixed run of elements consumed positionally. + Fixed(Vec), +} + +/// Parse an unpacked tuple element (`Named("*tuple[...]")` / `Named("*ts")`). +fn parse_star_segment(name: &str) -> StarSegment { + let Some(inner) = name + .strip_prefix("*tuple[") + .and_then(|rest| rest.strip_suffix(']')) + else { + // `*Ts` (a TypeVarTuple) — any number of elements of any type. + return StarSegment::Variadic(None); + }; + let parts = crate::types_parsing::split_type_params(inner); + if matches!(parts.last(), Some(last) if last.trim() == "...") { + // `*tuple[X, ...]` — homogeneous, zero or more of `X`. + let elem = parts + .first() + .map(|p| InferredType::from_annotation(p.trim())); + return StarSegment::Variadic(elem); + } + StarSegment::Fixed( + parts + .iter() + .map(|p| InferredType::from_annotation(p.trim())) + .collect(), + ) +} + +/// Match a fixed-length source tuple against a target tuple that contains a +/// single unpacked `*tuple[...]` / `*Ts` segment (PEP 646), using +/// prefix/middle/suffix decomposition. +pub(crate) fn tuple_assignable_with_star(source: &[InferredType], target: &[InferredType]) -> bool { + // Unhandled shapes return `true` (assignable) rather than `false`: this is a + // best-effort matcher and must never manufacture a false positive. Only a + // pattern we actually decompose may return `false` (a real mismatch). + // + // A variadic source, multiple unpacked segments, or a `*Ts` we can't read + // all need full PEP 646 unification — be permissive. + if source.iter().any(is_unpacked_tuple_elem) { + return true; + } + // A homogeneous variadic source (`tuple[X, ...]`) has unknown length — + // prefix/middle/suffix decomposition cannot prove a mismatch. + if homogeneous_tuple_elem(source).is_some() { + return true; + } + let Some(star_idx) = target.iter().position(is_unpacked_tuple_elem) else { + return true; + }; + let (prefix, rest) = target.split_at(star_idx); + let Some((star_elem, suffix)) = rest.split_first() else { + return true; + }; + // Only one unpacked segment is supported. + if suffix.iter().any(is_unpacked_tuple_elem) { + return true; + } + let InferredType::Named(star_name) = star_elem else { + return true; + }; + + match parse_star_segment(star_name) { + StarSegment::Variadic(elem) => { + let Some(middle_len) = source.len().checked_sub(prefix.len() + suffix.len()) else { + return false; + }; + prefix_suffix_match(source, prefix, suffix) + && match elem { + None => true, + Some(elem_ty) => source + .iter() + .skip(prefix.len()) + .take(middle_len) + .all(|s| s.is_assignable_to(&elem_ty)), + } + } + StarSegment::Fixed(middle) => { + if source.len() != prefix.len() + middle.len() + suffix.len() { + return false; + } + prefix_suffix_match(source, prefix, suffix) + && source + .iter() + .skip(prefix.len()) + .take(middle.len()) + .zip(middle.iter()) + .all(|(s, m)| s.is_assignable_to(m)) + } + } +} + +/// Check that a source tuple's leading elements match `prefix` and trailing +/// elements match `suffix` (both fixed, non-starred). Callers guarantee +/// `source.len() >= prefix.len() + suffix.len()`. +fn prefix_suffix_match( + source: &[InferredType], + prefix: &[InferredType], + suffix: &[InferredType], +) -> bool { + source + .iter() + .zip(prefix.iter()) + .all(|(s, p)| s.is_assignable_to(p)) + && source + .iter() + .rev() + .zip(suffix.iter().rev()) + .all(|(s, q)| s.is_assignable_to(q)) +} diff --git a/crates/basilisk-checker/tests/bidir_typeshed_validation_tests.rs b/crates/basilisk-checker/tests/bidir_typeshed_validation_tests.rs new file mode 100644 index 00000000..b6736844 --- /dev/null +++ b/crates/basilisk-checker/tests/bidir_typeshed_validation_tests.rs @@ -0,0 +1,165 @@ +//! Tests for [TYPEINF-TARGET-CONSTRAINTS]. See +//! docs/specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-TARGET-CONSTRAINTS and +//! the Stage 0 checklist in +//! docs/plans/CHECKER-TYPE-NARROWING-INFERENCE-PLAN.md#NARROWPLAN-CHECKLIST. +//! +//! Prototype validation of the borrowed algebraic-subtyping ideas — polar +//! type variables and constraint simplification — against **real typeshed +//! stubs** before the engine relies on them. The fixtures under +//! `tests/fixtures/typeshed/` are verbatim files from +//! `python/typeshed@main` (commit pinned in `TYPESHED_COMMIT.txt`), chosen +//! for generic/`Callable`/overload density: `functools`, `itertools`, +//! `contextlib`, `collections`, `heapq`. +//! +//! For every parameter and return annotation in those stubs the validation +//! asserts, through the public `bidir` API: +//! 1. **Reflexivity through the solver** — `T <: T` decomposes and +//! discharges with zero errors (catches asymmetries in leaf delegation +//! over real-world type shapes); +//! 2. **Projection stability** — lifting to the solver language and +//! projecting back is idempotent (normalization reaches a fixpoint, +//! no oscillation on real shapes); +//! 3. **Polar-variable resolution** — an output variable lower-bounded by +//! the type (and an input variable upper-bounded by it) resolves to a +//! projection compatible with the original (deferred generalization +//! holds on real shapes, not just synthetic ones). +#![expect( + clippy::expect_used, + reason = "test-only parsing of pinned typeshed fixture files" +)] + +use basilisk_checker::bidir::{solve, ConstraintReason, ConstraintSet, Polarity, Ty, TyVarStore}; +use basilisk_checker::types::InferredType; + +/// Extract every parameter/return annotation text from one stub. +fn annotation_texts(source: &str, path: &str) -> Vec { + let parsed = basilisk_parser::parse_source(source.to_owned(), path.to_owned()) + .expect("typeshed fixture must parse"); + let resolved = basilisk_resolver::resolve(&parsed).expect("typeshed fixture must resolve"); + let slice = |span: basilisk_resolver::Span| { + let start = usize::try_from(span.start).ok()?; + let end = usize::try_from(span.end).ok()?; + source.get(start..end).map(str::to_owned) + }; + let mut texts = Vec::new(); + for function in &resolved.functions { + for parameter in &function.parameters { + if let Some(text) = parameter.annotation_span.and_then(slice) { + texts.push(text); + } + } + if let Some(text) = function.return_annotation_span.and_then(slice) { + texts.push(text); + } + } + texts +} + +/// Load all fixture stubs as `(name, source)` pairs. +fn fixture_stubs() -> Vec<(String, String)> { + let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/typeshed"); + let entries = std::fs::read_dir(&dir).expect("typeshed fixtures directory must exist"); + let mut stubs = Vec::new(); + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().is_some_and(|ext| ext == "pyi") { + let source = std::fs::read_to_string(&path).expect("fixture must be readable"); + stubs.push((path.display().to_string(), source)); + } + } + stubs.sort(); + stubs +} + +/// Reflexivity: `T <: T` must solve with zero errors for every annotation. +#[test] +fn typeshed_annotations_are_reflexive_through_the_solver() { + let mut validated = 0_u32; + for (name, source) in fixture_stubs() { + for text in annotation_texts(&source, &name) { + let lifted = Ty::from_inferred(&InferredType::from_annotation(&text)); + let mut constraints = ConstraintSet::default(); + constraints.push( + lifted.clone(), + lifted, + ruff_text_size::TextRange::default(), + ConstraintReason::ExpectedType, + ); + let solution = solve(TyVarStore::default(), constraints.into_vec()); + assert!( + solution.errors.is_empty(), + "{name}: `{text}` <: `{text}` must hold, got {:?}", + solution.errors + ); + validated += 1; + } + } + assert!( + validated >= 300, + "expected 300+ real annotations validated, got {validated} — fixture rot?" + ); +} + +/// Projection stability: lift → project reaches a fixpoint in one step. +#[test] +fn typeshed_annotations_project_stably() { + for (name, source) in fixture_stubs() { + let vars = TyVarStore::default(); + for text in annotation_texts(&source, &name) { + let once = Ty::from_inferred(&InferredType::from_annotation(&text)).to_inferred(&vars); + let twice = Ty::from_inferred(&once).to_inferred(&vars); + assert_eq!( + once, twice, + "{name}: projection of `{text}` must be idempotent" + ); + } + } +} + +/// Polar variables: bounds from real annotations resolve compatibly. +#[test] +fn typeshed_annotations_flow_through_polar_variables() { + for (name, source) in fixture_stubs() { + for text in annotation_texts(&source, &name) { + let inferred = InferredType::from_annotation(&text); + let lifted = Ty::from_inferred(&inferred); + + // Output polarity: what flowed in is what comes out. + let mut store = TyVarStore::default(); + let out = store.fresh(Polarity::Output); + store.add_lower(out, lifted.clone()); + let resolved_out = store.resolve(out); + assert!( + resolved_out.is_assignable_to(&inferred) + || inferred.is_assignable_to(&resolved_out), + "{name}: output var seeded with `{text}` resolved to incompatible {resolved_out:?}" + ); + + // Input polarity: what is demanded is what must be accepted. + let input = store.fresh(Polarity::Input); + store.add_upper(input, lifted); + let resolved_in = store.resolve(input); + assert!( + resolved_in.is_assignable_to(&inferred) || inferred.is_assignable_to(&resolved_in), + "{name}: input var bounded by `{text}` resolved to incompatible {resolved_in:?}" + ); + } + } +} + +/// The fixtures really are the pinned typeshed files (guard against silent +/// truncation of the corpus). +#[test] +fn typeshed_fixture_corpus_is_present() { + let stubs = fixture_stubs(); + assert!( + stubs.len() >= 5, + "expected the 5 pinned typeshed stubs, found {}", + stubs.len() + ); + let total_lines: usize = stubs.iter().map(|(_, s)| s.lines().count()).sum(); + assert!( + total_lines >= 1000, + "expected 1000+ lines of real typeshed, got {total_lines}" + ); +} diff --git a/crates/basilisk-checker/tests/checker/inference_tests.rs b/crates/basilisk-checker/tests/checker/inference_tests.rs index 9eb0ca76..04e2f215 100644 --- a/crates/basilisk-checker/tests/checker/inference_tests.rs +++ b/crates/basilisk-checker/tests/checker/inference_tests.rs @@ -101,7 +101,13 @@ fn test_unannotated_param_fires_e0001() -> Result<(), Box #[test] fn test_missing_return_fires_e0002() -> Result<(), Box> { - let diags = run_with_config("def process(data: str): pass\n", &annotation_rules_config())?; + // The returned method-call result is not inferable, so the annotation is + // required. A `pass` body would infer `-> None` and stay silent + // ([TYPEINF-FUNC-RETURN]). + let diags = run_with_config( + "def process(data: str): return data.upper()\n", + &annotation_rules_config(), + )?; let e0002: Vec<_> = diags.iter().filter(|d| d.code.code == "BSK-0002").collect(); assert!( !e0002.is_empty(), diff --git a/crates/basilisk-checker/tests/checker/missing_parameter_annotation_tests.rs b/crates/basilisk-checker/tests/checker/missing_parameter_annotation_tests.rs index a0aac8fc..3f309673 100644 --- a/crates/basilisk-checker/tests/checker/missing_parameter_annotation_tests.rs +++ b/crates/basilisk-checker/tests/checker/missing_parameter_annotation_tests.rs @@ -63,6 +63,77 @@ class Foo: Ok(()) } +// ── Inference-aware exemption [TYPEINF-FUNC-DEFAULTS]: a missing-annotation +// rule must never fire where the current engine already infers the type. ───── + +/// `timeout=30` makes the parameter type trivially inferable (`int`), so +/// demanding an annotation is redundant — BSK-0001 must stay silent. +#[test] +fn param_with_inferable_literal_default_no_fire() -> Result<(), Box> { + let source = r" +def connect(timeout=30) -> None: + pass +"; + let diags = run_with_config(source, &annotation_rules_config())?; + assert!( + !codes(&diags).contains(&"BSK-0001"), + "parameter type inferable from literal default should not fire BSK-0001, got: {:?}", + codes(&diags) + ); + Ok(()) +} + +/// String and bool literal defaults are equally inferable — BSK-0001 must stay +/// silent for every parameter here. +#[test] +fn params_with_scalar_literal_defaults_no_fire() -> Result<(), Box> { + let source = r#" +def configure(name="basilisk", strict=True, ratio=1.5) -> None: + pass +"#; + let diags = run_with_config(source, &annotation_rules_config())?; + assert!( + !codes(&diags).contains(&"BSK-0001"), + "scalar literal defaults are inferable and should not fire BSK-0001, got: {:?}", + codes(&diags) + ); + Ok(()) +} + +/// A default the current engine cannot infer (call expression) still requires +/// an annotation. +#[test] +fn param_with_uninferable_default_still_fires() -> Result<(), Box> { + let source = r" +def connect(timeout=default_timeout()) -> None: + pass +"; + let diags = run_with_config(source, &annotation_rules_config())?; + assert!( + codes(&diags).contains(&"BSK-0001"), + "uninferable default must still fire BSK-0001, got: {:?}", + codes(&diags) + ); + Ok(()) +} + +/// A `None` default alone does not determine the parameter type (`T | None` +/// for unknown `T`) — the annotation is still required. +#[test] +fn param_with_none_default_still_fires() -> Result<(), Box> { + let source = r" +def connect(timeout=None) -> None: + pass +"; + let diags = run_with_config(source, &annotation_rules_config())?; + assert!( + codes(&diags).contains(&"BSK-0001"), + "a bare None default cannot determine the type and must still fire BSK-0001, got: {:?}", + codes(&diags) + ); + Ok(()) +} + #[test] fn multiple_unannotated_params() -> Result<(), Box> { let source = r" diff --git a/crates/basilisk-checker/tests/checker/missing_return_annotation_tests.rs b/crates/basilisk-checker/tests/checker/missing_return_annotation_tests.rs index e46aefd9..4a641bb3 100644 --- a/crates/basilisk-checker/tests/checker/missing_return_annotation_tests.rs +++ b/crates/basilisk-checker/tests/checker/missing_return_annotation_tests.rs @@ -45,3 +45,93 @@ def do_nothing() -> None: ); Ok(()) } + +// ── Inference-aware exemption [TYPEINF-FUNC-RETURN]: a missing-annotation rule +// must never fire where the current engine already infers the type. ────────── + +/// The return type of `return 42` is trivially inferable (`int`), so demanding +/// an annotation is redundant — BSK-0002 must stay silent. +#[test] +fn return_type_inferable_from_literal_no_fire() -> Result<(), Box> { + let source = r" +def answer(): + return 42 +"; + let diags = run_with_config(source, &annotation_rules_config())?; + assert!( + !codes(&diags).contains(&"BSK-0002"), + "return type inferable from literal return should not fire BSK-0002, got: {:?}", + codes(&diags) + ); + Ok(()) +} + +/// A function whose body never returns a value has the inferable return type +/// `None` — BSK-0002 must stay silent. +#[test] +fn no_return_statements_infers_none_no_fire() -> Result<(), Box> { + let source = r" +def log_it(msg: str): + print(msg) +"; + let diags = run_with_config(source, &annotation_rules_config())?; + assert!( + !codes(&diags).contains(&"BSK-0002"), + "no-return function infers -> None and should not fire BSK-0002, got: {:?}", + codes(&diags) + ); + Ok(()) +} + +/// Mixed inferable literals (`int` / `str`) still form an inferable union — +/// BSK-0002 must stay silent. +#[test] +fn return_type_inferable_from_literal_union_no_fire() -> Result<(), Box> { + let source = r#" +def pick(flag: bool): + if flag: + return 1 + return "no" +"#; + let diags = run_with_config(source, &annotation_rules_config())?; + assert!( + !codes(&diags).contains(&"BSK-0002"), + "union of inferable literal returns should not fire BSK-0002, got: {:?}", + codes(&diags) + ); + Ok(()) +} + +/// A returned call expression is NOT inferable by the current engine, so the +/// annotation is still required. +#[test] +fn uninferable_call_return_still_fires() -> Result<(), Box> { + let source = r" +def fetch(): + return make_value() +"; + let diags = run_with_config(source, &annotation_rules_config())?; + assert!( + codes(&diags).contains(&"BSK-0002"), + "uninferable returned call must still fire BSK-0002, got: {:?}", + codes(&diags) + ); + Ok(()) +} + +/// Generators produce `Generator[...]`, which the current engine cannot infer +/// from return statements — the annotation is still required. +#[test] +fn generator_still_fires() -> Result<(), Box> { + let source = r" +def numbers(): + yield 1 +"; + let diags = run_with_config(source, &annotation_rules_config())?; + assert!( + codes(&diags).contains(&"BSK-0002"), + "generator return type is not inferable and must still fire BSK-0002, got: {:?}", + codes(&diags) + ); + Ok(()) +} diff --git a/crates/basilisk-checker/tests/checker/types_tests.rs b/crates/basilisk-checker/tests/checker/types_tests.rs index 28c83aac..dba43469 100644 --- a/crates/basilisk-checker/tests/checker/types_tests.rs +++ b/crates/basilisk-checker/tests/checker/types_tests.rs @@ -267,3 +267,30 @@ def apply(f: Callable[[int], str]) -> str: let _diags = run(source)?; Ok(()) } + +// Exercises [TYPEINF-SUBTYPING-NOMINAL] — plain `bool` widens through the +// builtin tower (`bool <: int <: float`, PEP 484), and never the reverse. +// Guards the `(Bool, Int | Float)` arm in `is_assignable_to`. +#[test] +fn bool_widens_through_numeric_tower() { + use basilisk_checker::types::InferredType; + assert!(InferredType::Bool.is_assignable_to(&InferredType::Int)); + assert!(InferredType::Bool.is_assignable_to(&InferredType::Float)); + assert!(!InferredType::Int.is_assignable_to(&InferredType::Bool)); + assert!(!InferredType::Float.is_assignable_to(&InferredType::Bool)); +} + +// Exercises [TYPEINF-SUBTYPING-UNION] — a union containing `None` satisfies an +// `Optional` target: union-LEFT decomposition must run before Optional-target +// unwrapping, else the `None` variant is checked against the unwrapped inner +// type and wrongly fails. Guards the match-arm ordering in `is_assignable_to`. +#[test] +fn union_with_none_satisfies_optional_target() { + use basilisk_checker::types::InferredType; + let int_or_none = InferredType::Union(vec![InferredType::Int, InferredType::None_]); + let optional_int = InferredType::Optional(Box::new(InferredType::Int)); + assert!(int_or_none.is_assignable_to(&optional_int)); + + let str_or_none = InferredType::Union(vec![InferredType::Str, InferredType::None_]); + assert!(!str_or_none.is_assignable_to(&optional_int)); +} diff --git a/crates/basilisk-checker/tests/checker_tests.rs b/crates/basilisk-checker/tests/checker_tests.rs index 23dad735..af4b6741 100644 --- a/crates/basilisk-checker/tests/checker_tests.rs +++ b/crates/basilisk-checker/tests/checker_tests.rs @@ -134,8 +134,11 @@ fn emits_e0001_for_missing_parameter_annotation() -> Result<(), Box Result<(), Box> { + // The returned method-call result is NOT inferable by the current engine, + // so the annotation is required ([TYPEINF-FUNC-RETURN]). A body that never + // returns a value would infer `-> None` and stay silent. let diags = run_with_config( - "def process(data: str):\n pass\n", + "def process(data: str):\n return data.upper()\n", &annotation_rules_config(), )?; assert_eq!(diags.len(), 1); @@ -146,7 +149,12 @@ fn emits_e0002_for_missing_return_annotation() -> Result<(), Box Result<(), Box> { - let diags = run_with_config("def process(data):\n pass\n", &annotation_rules_config())?; + // `data` has no default to infer from (BSK-0001) and `return data` is a + // name reference the current engine cannot infer (BSK-0002) — both fire. + let diags = run_with_config( + "def process(data):\n return data\n", + &annotation_rules_config(), + )?; assert_eq!(diags.len(), 2, "should emit BSK-0001 and BSK-0002"); let codes: Vec<&str> = diags.iter().map(|d| d.code.code).collect(); @@ -1166,12 +1174,14 @@ fn guards_abstractmethod_is_stub_context() -> Result<(), Box Result<(), Box> { // Two classes: one Protocol, one not. Methods in Protocol must be exempt; // methods in the non-Protocol class must NOT be exempt. + // NotProto.unannotated returns an attribute access — not inferable, so + // BSK-0002 must fire there (a `pass` body would infer `-> None`). let src = concat!( "from typing import Protocol\n", "class MyProto(Protocol):\n", " def required(self) -> None: ...\n", "class NotProto:\n", - " def unannotated(self): pass\n", + " def unannotated(self): return self.value\n", ); let diags = run_with_config(src, &annotation_rules_config())?; let e2: Vec<_> = diags.iter().filter(|d| d.code.code == "BSK-0002").collect(); diff --git a/crates/basilisk-checker/tests/fixtures/typeshed/TYPESHED_COMMIT.txt b/crates/basilisk-checker/tests/fixtures/typeshed/TYPESHED_COMMIT.txt new file mode 100644 index 00000000..6b9a4006 --- /dev/null +++ b/crates/basilisk-checker/tests/fixtures/typeshed/TYPESHED_COMMIT.txt @@ -0,0 +1 @@ +83c2518a9e6abbda0c44592c3483de459198f887 diff --git a/crates/basilisk-checker/tests/fixtures/typeshed/collections.pyi b/crates/basilisk-checker/tests/fixtures/typeshed/collections.pyi new file mode 100644 index 00000000..39b8a3c9 --- /dev/null +++ b/crates/basilisk-checker/tests/fixtures/typeshed/collections.pyi @@ -0,0 +1,564 @@ +import sys +from _collections_abc import dict_items, dict_keys, dict_values +from _typeshed import SupportsItems, SupportsKeysAndGetItem, SupportsRichComparison, SupportsRichComparisonT +from collections.abc import ( + Callable, + ItemsView, + Iterable, + Iterator, + KeysView, + Mapping, + MutableMapping, + MutableSequence, + Sequence, + ValuesView, +) +from types import GenericAlias +from typing import Any, ClassVar, Generic, NoReturn, SupportsIndex, TypeVar, final, overload, type_check_only +from typing_extensions import Self, disjoint_base + +if sys.version_info >= (3, 15): + from builtins import frozendict + +__all__ = ["ChainMap", "Counter", "OrderedDict", "UserDict", "UserList", "UserString", "defaultdict", "deque", "namedtuple"] + +_S = TypeVar("_S") +_T = TypeVar("_T") +_T1 = TypeVar("_T1") +_T2 = TypeVar("_T2") +_KT = TypeVar("_KT") +_VT = TypeVar("_VT") +_KT_co = TypeVar("_KT_co", covariant=True) +_VT_co = TypeVar("_VT_co", covariant=True) + +# namedtuple is special-cased in the type checker; the initializer is ignored. +def namedtuple( + typename: str, + field_names: str | Iterable[str], + *, + rename: bool = False, + module: str | None = None, + defaults: Iterable[Any] | None = None, +) -> type[tuple[Any, ...]]: ... + +class UserDict(MutableMapping[_KT, _VT]): + data: dict[_KT, _VT] + + # __init__ should be kept roughly in line with `dict.__init__`, which has the same semantics + @overload + def __init__(self, dict: None = None, /) -> None: ... + @overload + def __init__( + self: UserDict[str, _VT], dict: None = None, /, **kwargs: _VT # pyright: ignore[reportInvalidTypeVarUse] #11780 + ) -> None: ... + @overload + def __init__(self, dict: SupportsKeysAndGetItem[_KT, _VT], /) -> None: ... + @overload + def __init__( + self: UserDict[str, _VT], # pyright: ignore[reportInvalidTypeVarUse] #11780 + dict: SupportsKeysAndGetItem[str, _VT], + /, + **kwargs: _VT, + ) -> None: ... + @overload + def __init__(self, iterable: Iterable[tuple[_KT, _VT]], /) -> None: ... + @overload + def __init__( + self: UserDict[str, _VT], # pyright: ignore[reportInvalidTypeVarUse] #11780 + iterable: Iterable[tuple[str, _VT]], + /, + **kwargs: _VT, + ) -> None: ... + @overload + def __init__(self: UserDict[str, str], iterable: Iterable[list[str]], /) -> None: ... + @overload + def __init__(self: UserDict[bytes, bytes], iterable: Iterable[list[bytes]], /) -> None: ... + + def __len__(self) -> int: ... + def __getitem__(self, key: _KT) -> _VT: ... + def __setitem__(self, key: _KT, item: _VT) -> None: ... + def __delitem__(self, key: _KT) -> None: ... + def __iter__(self) -> Iterator[_KT]: ... + def __contains__(self, key: object) -> bool: ... + def copy(self) -> Self: ... + def __copy__(self) -> Self: ... + + # `UserDict.fromkeys` has the same semantics as `dict.fromkeys`, so should be kept in line with `dict.fromkeys`. + # TODO: Much like `dict.fromkeys`, the true signature of `UserDict.fromkeys` is inexpressible in the current type system. + # See #3800 & https://github.com/python/typing/issues/548#issuecomment-683336963. + @classmethod + @overload + def fromkeys(cls, iterable: Iterable[_T], value: None = None) -> UserDict[_T, Any | None]: ... + @classmethod + @overload + def fromkeys(cls, iterable: Iterable[_T], value: _S) -> UserDict[_T, _S]: ... + + @overload + def __or__(self, other: UserDict[_KT, _VT] | dict[_KT, _VT]) -> Self: ... + @overload + def __or__(self, other: UserDict[_T1, _T2] | dict[_T1, _T2]) -> UserDict[_KT | _T1, _VT | _T2]: ... + + @overload + def __ror__(self, other: UserDict[_KT, _VT] | dict[_KT, _VT]) -> Self: ... + @overload + def __ror__(self, other: UserDict[_T1, _T2] | dict[_T1, _T2]) -> UserDict[_KT | _T1, _VT | _T2]: ... + + # UserDict.__ior__ should be kept roughly in line with MutableMapping.update() + @overload # type: ignore[misc] + def __ior__(self, other: SupportsKeysAndGetItem[_KT, _VT]) -> Self: ... + @overload + def __ior__(self, other: Iterable[tuple[_KT, _VT]]) -> Self: ... + + if sys.version_info >= (3, 12): + @overload + def get(self, key: _KT, default: None = None) -> _VT | None: ... + @overload + def get(self, key: _KT, default: _VT) -> _VT: ... + @overload + def get(self, key: _KT, default: _T) -> _VT | _T: ... + +class UserList(MutableSequence[_T]): + data: list[_T] + + @overload + def __init__(self, initlist: None = None) -> None: ... + @overload + def __init__(self, initlist: Iterable[_T]) -> None: ... + + __hash__: ClassVar[None] # type: ignore[assignment] + def __lt__(self, other: list[_T] | UserList[_T]) -> bool: ... + def __le__(self, other: list[_T] | UserList[_T]) -> bool: ... + def __gt__(self, other: list[_T] | UserList[_T]) -> bool: ... + def __ge__(self, other: list[_T] | UserList[_T]) -> bool: ... + def __eq__(self, other: object) -> bool: ... + def __contains__(self, item: object) -> bool: ... + def __len__(self) -> int: ... + + @overload + def __getitem__(self, i: SupportsIndex) -> _T: ... + @overload + def __getitem__(self, i: slice[SupportsIndex | None]) -> Self: ... + + @overload + def __setitem__(self, i: SupportsIndex, item: _T) -> None: ... + @overload + def __setitem__(self, i: slice[SupportsIndex | None], item: Iterable[_T]) -> None: ... + + def __delitem__(self, i: SupportsIndex | slice[SupportsIndex | None]) -> None: ... + def __add__(self, other: Iterable[_T]) -> Self: ... + def __radd__(self, other: Iterable[_T]) -> Self: ... + def __iadd__(self, other: Iterable[_T]) -> Self: ... + def __mul__(self, n: int) -> Self: ... + def __rmul__(self, n: int) -> Self: ... + def __imul__(self, n: int) -> Self: ... + def append(self, item: _T) -> None: ... + def insert(self, i: int, item: _T) -> None: ... + def pop(self, i: int = -1) -> _T: ... + def remove(self, item: _T) -> None: ... + def copy(self) -> Self: ... + def __copy__(self) -> Self: ... + def count(self, item: _T) -> int: ... + # The runtime signature is "item, *args", and the arguments are then passed + # to `list.index`. In order to give more precise types, we pretend that the + # `item` argument is positional-only. + def index(self, item: _T, start: SupportsIndex = 0, stop: SupportsIndex = sys.maxsize, /) -> int: ... + + # All arguments are passed to `list.sort` at runtime, so the signature should be kept in line with `list.sort`. + @overload + def sort(self: UserList[SupportsRichComparisonT], *, key: None = None, reverse: bool = False) -> None: ... + @overload + def sort(self, *, key: Callable[[_T], SupportsRichComparison], reverse: bool = False) -> None: ... + + def extend(self, other: Iterable[_T]) -> None: ... + +class UserString(Sequence[UserString]): + data: str + def __init__(self, seq: object) -> None: ... + def __int__(self) -> int: ... + def __float__(self) -> float: ... + def __complex__(self) -> complex: ... + def __getnewargs__(self) -> tuple[str]: ... + def __lt__(self, string: str | UserString) -> bool: ... + def __le__(self, string: str | UserString) -> bool: ... + def __gt__(self, string: str | UserString) -> bool: ... + def __ge__(self, string: str | UserString) -> bool: ... + def __eq__(self, string: object) -> bool: ... + def __hash__(self) -> int: ... + def __contains__(self, char: object) -> bool: ... + def __len__(self) -> int: ... + def __getitem__(self, index: SupportsIndex | slice[SupportsIndex | None]) -> Self: ... + def __iter__(self) -> Iterator[Self]: ... + def __reversed__(self) -> Iterator[Self]: ... + def __add__(self, other: object) -> Self: ... + def __radd__(self, other: object) -> Self: ... + def __mul__(self, n: int) -> Self: ... + def __rmul__(self, n: int) -> Self: ... + def __mod__(self, args: Any) -> Self: ... + def __rmod__(self, template: object) -> Self: ... + def capitalize(self) -> Self: ... + def casefold(self) -> Self: ... + def center(self, width: int, *args: Any) -> Self: ... + def count(self, sub: str | UserString, start: int = 0, end: int = sys.maxsize) -> int: ... + def encode(self: UserString, encoding: str | None = "utf-8", errors: str | None = "strict") -> bytes: ... + def endswith(self, suffix: str | tuple[str, ...], start: int | None = 0, end: int | None = sys.maxsize) -> bool: ... + def expandtabs(self, tabsize: int = 8) -> Self: ... + def find(self, sub: str | UserString, start: int = 0, end: int = sys.maxsize) -> int: ... + def format(self, *args: Any, **kwds: Any) -> str: ... + def format_map(self, mapping: Mapping[str, Any]) -> str: ... + def index(self, sub: str, start: int = 0, end: int = sys.maxsize) -> int: ... + def isalpha(self) -> bool: ... + def isalnum(self) -> bool: ... + def isdecimal(self) -> bool: ... + def isdigit(self) -> bool: ... + def isidentifier(self) -> bool: ... + def islower(self) -> bool: ... + def isnumeric(self) -> bool: ... + def isprintable(self) -> bool: ... + def isspace(self) -> bool: ... + def istitle(self) -> bool: ... + def isupper(self) -> bool: ... + def isascii(self) -> bool: ... + def join(self, seq: Iterable[str]) -> str: ... + def ljust(self, width: int, *args: Any) -> Self: ... + def lower(self) -> Self: ... + def lstrip(self, chars: str | None = None) -> Self: ... + maketrans = str.maketrans + def partition(self, sep: str) -> tuple[str, str, str]: ... + def removeprefix(self, prefix: str | UserString, /) -> Self: ... + def removesuffix(self, suffix: str | UserString, /) -> Self: ... + def replace(self, old: str | UserString, new: str | UserString, maxsplit: int = -1) -> Self: ... + def rfind(self, sub: str | UserString, start: int = 0, end: int = sys.maxsize) -> int: ... + def rindex(self, sub: str | UserString, start: int = 0, end: int = sys.maxsize) -> int: ... + def rjust(self, width: int, *args: Any) -> Self: ... + def rpartition(self, sep: str) -> tuple[str, str, str]: ... + def rstrip(self, chars: str | None = None) -> Self: ... + def split(self, sep: str | None = None, maxsplit: int = -1) -> list[str]: ... + def rsplit(self, sep: str | None = None, maxsplit: int = -1) -> list[str]: ... + def splitlines(self, keepends: bool = False) -> list[str]: ... + def startswith(self, prefix: str | tuple[str, ...], start: int | None = 0, end: int | None = sys.maxsize) -> bool: ... + def strip(self, chars: str | None = None) -> Self: ... + def swapcase(self) -> Self: ... + def title(self) -> Self: ... + def translate(self, *args: Any) -> Self: ... + def upper(self) -> Self: ... + def zfill(self, width: int) -> Self: ... + +@disjoint_base +class deque(MutableSequence[_T]): + @property + def maxlen(self) -> int | None: ... + + @overload + def __init__(self, *, maxlen: int | None = None) -> None: ... + @overload + def __init__(self, iterable: Iterable[_T], maxlen: int | None = None) -> None: ... + + def append(self, x: _T, /) -> None: ... + def appendleft(self, x: _T, /) -> None: ... + def copy(self) -> Self: ... + def count(self, x: _T, /) -> int: ... + def extend(self, iterable: Iterable[_T], /) -> None: ... + def extendleft(self, iterable: Iterable[_T], /) -> None: ... + def insert(self, i: int, x: _T, /) -> None: ... + def index(self, x: _T, start: int = 0, stop: int = ..., /) -> int: ... + def pop(self) -> _T: ... # type: ignore[override] + def popleft(self) -> _T: ... + def remove(self, value: _T, /) -> None: ... + def rotate(self, n: int = 1, /) -> None: ... + def __copy__(self) -> Self: ... + def __len__(self) -> int: ... + __hash__: ClassVar[None] # type: ignore[assignment] + # These methods of deque don't take slices, unlike MutableSequence, hence the type: ignores + def __getitem__(self, key: SupportsIndex, /) -> _T: ... # type: ignore[override] + def __setitem__(self, key: SupportsIndex, value: _T, /) -> None: ... # type: ignore[override] + def __delitem__(self, key: SupportsIndex, /) -> None: ... # type: ignore[override] + def __contains__(self, key: object, /) -> bool: ... + def __reduce__(self) -> tuple[type[Self], tuple[()], None, Iterator[_T]]: ... + def __iadd__(self, value: Iterable[_T], /) -> Self: ... + def __add__(self, value: Self, /) -> Self: ... + def __mul__(self, value: int, /) -> Self: ... + def __imul__(self, value: int, /) -> Self: ... + def __lt__(self, value: deque[_T], /) -> bool: ... + def __le__(self, value: deque[_T], /) -> bool: ... + def __gt__(self, value: deque[_T], /) -> bool: ... + def __ge__(self, value: deque[_T], /) -> bool: ... + def __eq__(self, value: object, /) -> bool: ... + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + +class Counter(dict[_T, int], Generic[_T]): + @overload + def __init__(self, iterable: None = None, /) -> None: ... + @overload + def __init__(self: Counter[str], iterable: None = None, /, **kwargs: int) -> None: ... + @overload + def __init__(self, mapping: SupportsKeysAndGetItem[_T, int], /) -> None: ... + @overload + def __init__(self, iterable: Iterable[_T], /) -> None: ... + + def copy(self) -> Self: ... + def elements(self) -> Iterator[_T]: ... + def most_common(self, n: int | None = None) -> list[tuple[_T, int]]: ... + @classmethod + def fromkeys(cls, iterable: Any, v: int | None = None) -> NoReturn: ... # type: ignore[override] + + @overload + def subtract(self, iterable: None = None, /) -> None: ... + @overload + def subtract(self, mapping: Mapping[_T, int], /) -> None: ... + @overload + def subtract(self, iterable: Iterable[_T], /) -> None: ... + + # Unlike dict.update(), use Mapping instead of SupportsKeysAndGetItem for the first overload + # (source code does an `isinstance(other, Mapping)` check) + # + # The second overload is also deliberately different to dict.update() + # (if it were `Iterable[_T] | Iterable[tuple[_T, int]]`, + # the tuples would be added as keys, breaking type safety) + @overload # type: ignore[override] + def update(self, m: Mapping[_T, int], /, **kwargs: int) -> None: ... + @overload + def update(self, iterable: Iterable[_T], /, **kwargs: int) -> None: ... + @overload + def update(self, iterable: None = None, /, **kwargs: int) -> None: ... + + def total(self) -> int: ... + def __missing__(self, key: _T) -> int: ... + def __delitem__(self, elem: object) -> None: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + def __le__(self, other: Counter[Any]) -> bool: ... + def __lt__(self, other: Counter[Any]) -> bool: ... + def __ge__(self, other: Counter[Any]) -> bool: ... + def __gt__(self, other: Counter[Any]) -> bool: ... + def __add__(self, other: Counter[_S]) -> Counter[_T | _S]: ... + def __sub__(self, other: Counter[_T]) -> Counter[_T]: ... + def __and__(self, other: Counter[_T]) -> Counter[_T]: ... + def __or__(self, other: Counter[_S]) -> Counter[_T | _S]: ... # type: ignore[override] + if sys.version_info >= (3, 15): + def __xor__(self, other: Counter[_S]) -> Counter[_T | _S]: ... # type: ignore[override] + + def __pos__(self) -> Counter[_T]: ... + def __neg__(self) -> Counter[_T]: ... + # several type: ignores because __iadd__ is supposedly incompatible with __add__, etc. + def __iadd__(self, other: SupportsItems[_T, int]) -> Self: ... # type: ignore[misc] + def __isub__(self, other: SupportsItems[_T, int]) -> Self: ... + def __iand__(self, other: SupportsItems[_T, int]) -> Self: ... + def __ior__(self, other: SupportsItems[_T, int]) -> Self: ... # type: ignore[override,misc] + if sys.version_info >= (3, 15): + def __ixor__(self, other: Counter[_T]) -> Self: ... # type: ignore[misc] + +# The pure-Python implementations of the "views" classes +# These are exposed at runtime in `collections/__init__.py` +class _OrderedDictKeysView(KeysView[_KT_co]): + def __reversed__(self) -> Iterator[_KT_co]: ... + +class _OrderedDictItemsView(ItemsView[_KT_co, _VT_co]): + def __reversed__(self) -> Iterator[tuple[_KT_co, _VT_co]]: ... + +class _OrderedDictValuesView(ValuesView[_VT_co]): + def __reversed__(self) -> Iterator[_VT_co]: ... + +# The C implementations of the "views" classes +# (At runtime, these are called `odict_keys`, `odict_items` and `odict_values`, +# but they are not exposed anywhere) +# pyright doesn't have a specific error code for subclassing error! +@final +@type_check_only +class _odict_keys(dict_keys[_KT_co, _VT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # ty:ignore[subclass-of-final-class] + def __reversed__(self) -> Iterator[_KT_co]: ... + +@final +@type_check_only +class _odict_items(dict_items[_KT_co, _VT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # ty:ignore[subclass-of-final-class] + def __reversed__(self) -> Iterator[tuple[_KT_co, _VT_co]]: ... + +@final +@type_check_only +class _odict_values(dict_values[_KT_co, _VT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # ty:ignore[subclass-of-final-class] + def __reversed__(self) -> Iterator[_VT_co]: ... + +@disjoint_base +class OrderedDict(dict[_KT, _VT]): + def popitem(self, last: bool = True) -> tuple[_KT, _VT]: ... + def move_to_end(self, key: _KT, last: bool = True) -> None: ... + def copy(self) -> Self: ... + def __reversed__(self) -> Iterator[_KT]: ... + def keys(self) -> _odict_keys[_KT, _VT]: ... + def items(self) -> _odict_items[_KT, _VT]: ... + def values(self) -> _odict_values[_KT, _VT]: ... + + # The signature of OrderedDict.fromkeys should be kept in line with `dict.fromkeys`, modulo positional-only differences. + # Like dict.fromkeys, its true signature is not expressible in the current type system. + # See #3800 & https://github.com/python/typing/issues/548#issuecomment-683336963. + @classmethod + @overload + def fromkeys(cls, iterable: Iterable[_T], value: None = None) -> OrderedDict[_T, Any | None]: ... + @classmethod + @overload + def fromkeys(cls, iterable: Iterable[_T], value: _S) -> OrderedDict[_T, _S]: ... + + # Keep OrderedDict.setdefault in line with MutableMapping.setdefault, modulo positional-only differences. + @overload + def setdefault(self: OrderedDict[_KT, _T | None], key: _KT, default: None = None) -> _T | None: ... + @overload + def setdefault(self, key: _KT, default: _VT) -> _VT: ... + + # Same as dict.pop, but accepts keyword arguments + @overload + def pop(self, key: _KT) -> _VT: ... + @overload + def pop(self, key: _KT, default: _VT) -> _VT: ... + @overload + def pop(self, key: _KT, default: _T) -> _VT | _T: ... + + def __eq__(self, value: object, /) -> bool: ... + + if sys.version_info >= (3, 15): + @overload + def __or__(self, value: dict[_KT, _VT] | frozendict[_KT, _VT], /) -> Self: ... + @overload + def __or__(self, value: dict[_T1, _T2] | frozendict[_T1, _T2], /) -> OrderedDict[_KT | _T1, _VT | _T2]: ... + + @overload # type: ignore[override] + def __ror__(self, value: dict[_KT, _VT] | frozendict[_KT, _VT], /) -> Self: ... # type: ignore[override,misc] + @overload + def __ror__( # type: ignore[misc] + self, value: dict[_T1, _T2] | frozendict[_T1, _T2], / + ) -> OrderedDict[_KT | _T1, _VT | _T2]: ... + else: + @overload + def __or__(self, value: dict[_KT, _VT], /) -> Self: ... + @overload + def __or__(self, value: dict[_T1, _T2], /) -> OrderedDict[_KT | _T1, _VT | _T2]: ... + + @overload + def __ror__(self, value: dict[_KT, _VT], /) -> Self: ... + @overload + def __ror__(self, value: dict[_T1, _T2], /) -> OrderedDict[_KT | _T1, _VT | _T2]: ... # type: ignore[misc] + +@disjoint_base +class defaultdict(dict[_KT, _VT]): + default_factory: Callable[[], _VT] | None + + @overload + def __init__(self) -> None: ... + @overload + def __init__(self: defaultdict[str, _VT], **kwargs: _VT) -> None: ... # pyright: ignore[reportInvalidTypeVarUse] #11780 + @overload + def __init__(self, default_factory: Callable[[], _VT] | None, /) -> None: ... + @overload + def __init__( + self: defaultdict[str, _VT], # pyright: ignore[reportInvalidTypeVarUse] #11780 + default_factory: Callable[[], _VT] | None, + /, + **kwargs: _VT, + ) -> None: ... + @overload + def __init__(self, default_factory: Callable[[], _VT] | None, map: SupportsKeysAndGetItem[_KT, _VT], /) -> None: ... + @overload + def __init__( + self: defaultdict[str, _VT], # pyright: ignore[reportInvalidTypeVarUse] #11780 + default_factory: Callable[[], _VT] | None, + map: SupportsKeysAndGetItem[str, _VT], + /, + **kwargs: _VT, + ) -> None: ... + @overload + def __init__(self, default_factory: Callable[[], _VT] | None, iterable: Iterable[tuple[_KT, _VT]], /) -> None: ... + @overload + def __init__( + self: defaultdict[str, _VT], # pyright: ignore[reportInvalidTypeVarUse] #11780 + default_factory: Callable[[], _VT] | None, + iterable: Iterable[tuple[str, _VT]], + /, + **kwargs: _VT, + ) -> None: ... + + def __missing__(self, key: _KT, /) -> _VT: ... + def __copy__(self) -> Self: ... + def copy(self) -> Self: ... + + # defaultdict rejects frozendict in its direct __or__/__ror__ methods, even though dict accepts it. + # See https://github.com/python/cpython/issues/149534. + @overload # type: ignore[override] + def __or__(self, value: dict[_KT, _VT], /) -> Self: ... + @overload + def __or__(self, value: dict[_T1, _T2], /) -> defaultdict[_KT | _T1, _VT | _T2]: ... + + @overload # type: ignore[override] + def __ror__(self, value: dict[_KT, _VT], /) -> Self: ... + @overload + def __ror__(self, value: dict[_T1, _T2], /) -> defaultdict[_KT | _T1, _VT | _T2]: ... # type: ignore[misc] + +class ChainMap(MutableMapping[_KT, _VT]): + maps: list[MutableMapping[_KT, _VT]] + def __init__(self, *maps: MutableMapping[_KT, _VT]) -> None: ... + def new_child(self, m: MutableMapping[_KT, _VT] | None = None) -> Self: ... + @property + def parents(self) -> Self: ... + def __setitem__(self, key: _KT, value: _VT) -> None: ... + def __delitem__(self, key: _KT) -> None: ... + def __getitem__(self, key: _KT) -> _VT: ... + def __iter__(self) -> Iterator[_KT]: ... + def __len__(self) -> int: ... + def __contains__(self, key: object) -> bool: ... + + @overload + def get(self, key: _KT, default: None = None) -> _VT | None: ... + @overload + def get(self, key: _KT, default: _VT) -> _VT: ... + @overload + def get(self, key: _KT, default: _T) -> _VT | _T: ... + + def __missing__(self, key: _KT) -> _VT: ... # undocumented + def __bool__(self) -> bool: ... + + # Keep ChainMap.setdefault in line with MutableMapping.setdefault, modulo positional-only differences. + @overload + def setdefault(self: ChainMap[_KT, _T | None], key: _KT, default: None = None) -> _T | None: ... + @overload + def setdefault(self, key: _KT, default: _VT) -> _VT: ... + + @overload + def pop(self, key: _KT) -> _VT: ... + @overload + def pop(self, key: _KT, default: _VT) -> _VT: ... + @overload + def pop(self, key: _KT, default: _T) -> _VT | _T: ... + + def copy(self) -> Self: ... + __copy__ = copy + # All arguments to `fromkeys` are passed to `dict.fromkeys` at runtime, + # so the signature should be kept in line with `dict.fromkeys`. + if sys.version_info >= (3, 13): + @classmethod + @overload + def fromkeys(cls, iterable: Iterable[_T], /) -> ChainMap[_T, Any | None]: ... + else: + @classmethod + @overload + def fromkeys(cls, iterable: Iterable[_T]) -> ChainMap[_T, Any | None]: ... + + @classmethod + @overload + # Special-case None: the user probably wants to add non-None values later. + def fromkeys(cls, iterable: Iterable[_T], value: None, /) -> ChainMap[_T, Any | None]: ... + @classmethod + @overload + def fromkeys(cls, iterable: Iterable[_T], value: _S, /) -> ChainMap[_T, _S]: ... + + @overload + def __or__(self, other: Mapping[_KT, _VT]) -> Self: ... + @overload + def __or__(self, other: Mapping[_T1, _T2]) -> ChainMap[_KT | _T1, _VT | _T2]: ... + + @overload + def __ror__(self, other: Mapping[_KT, _VT]) -> Self: ... + @overload + def __ror__(self, other: Mapping[_T1, _T2]) -> ChainMap[_KT | _T1, _VT | _T2]: ... + + # ChainMap.__ior__ should be kept roughly in line with MutableMapping.update() + @overload # type: ignore[misc] + def __ior__(self, other: SupportsKeysAndGetItem[_KT, _VT]) -> Self: ... + @overload + def __ior__(self, other: Iterable[tuple[_KT, _VT]]) -> Self: ... diff --git a/crates/basilisk-checker/tests/fixtures/typeshed/contextlib.pyi b/crates/basilisk-checker/tests/fixtures/typeshed/contextlib.pyi new file mode 100644 index 00000000..dd601675 --- /dev/null +++ b/crates/basilisk-checker/tests/fixtures/typeshed/contextlib.pyi @@ -0,0 +1,224 @@ +import abc +import sys +from _typeshed import FileDescriptorOrPath, Unused +from abc import ABC, abstractmethod +from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable, Generator, Iterator +from types import TracebackType +from typing import Any, Generic, ParamSpec, Protocol, TypeAlias, TypeVar, overload, runtime_checkable, type_check_only +from typing_extensions import Self, deprecated + +__all__ = [ + "aclosing", + "contextmanager", + "closing", + "AbstractContextManager", + "ContextDecorator", + "ExitStack", + "redirect_stdout", + "redirect_stderr", + "suppress", + "AbstractAsyncContextManager", + "AsyncExitStack", + "asynccontextmanager", + "nullcontext", +] + +if sys.version_info >= (3, 11): + __all__ += ["chdir"] + +_T = TypeVar("_T") +_T_co = TypeVar("_T_co", covariant=True) +_ExitT_co = TypeVar("_ExitT_co", covariant=True, bound=bool | None, default=bool | None) +_F = TypeVar("_F", bound=Callable[..., Any]) +_G_co = TypeVar("_G_co", bound=Generator[Any, Any, Any] | AsyncGenerator[Any, Any], covariant=True) +_P = ParamSpec("_P") + +_SendT_contra = TypeVar("_SendT_contra", contravariant=True, default=None) +_ReturnT_co = TypeVar("_ReturnT_co", covariant=True, default=None) + +_ExitFunc: TypeAlias = Callable[[type[BaseException] | None, BaseException | None, TracebackType | None], bool | None] +_CM_EF = TypeVar("_CM_EF", bound=AbstractContextManager[Any, Any] | _ExitFunc) + +# mypy and pyright object to this being both ABC and Protocol. +# At runtime it inherits from ABC and is not a Protocol, but it is on the +# allowlist for use as a Protocol. +@runtime_checkable +class AbstractContextManager(ABC, Protocol[_T_co, _ExitT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # ty:ignore[invalid-protocol] + __slots__ = () + def __enter__(self) -> _T_co: ... + @abstractmethod + def __exit__( + self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / + ) -> _ExitT_co: ... + +# mypy and pyright object to this being both ABC and Protocol. +# At runtime it inherits from ABC and is not a Protocol, but it is on the +# allowlist for use as a Protocol. +@runtime_checkable +class AbstractAsyncContextManager(ABC, Protocol[_T_co, _ExitT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # ty:ignore[invalid-protocol] + __slots__ = () + async def __aenter__(self) -> _T_co: ... + @abstractmethod + async def __aexit__( + self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / + ) -> _ExitT_co: ... + +class ContextDecorator: + def _recreate_cm(self) -> Self: ... + def __call__(self, func: _F) -> _F: ... + +class _GeneratorContextManagerBase(Generic[_G_co]): + # Ideally this would use ParamSpec, but that requires (*args, **kwargs), which this isn't. see #6676 + def __init__(self, func: Callable[..., _G_co], args: tuple[Any, ...], kwds: dict[str, Any]) -> None: ... + gen: _G_co + func: Callable[..., _G_co] + args: tuple[Any, ...] + kwds: dict[str, Any] + +class _GeneratorContextManager( + _GeneratorContextManagerBase[Generator[_T_co, _SendT_contra, _ReturnT_co]], + AbstractContextManager[_T_co, bool | None], + ContextDecorator, +): + def __exit__( + self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None + ) -> bool | None: ... + +@overload +def contextmanager(func: Callable[_P, Generator[_T_co, None, object]]) -> Callable[_P, _GeneratorContextManager[_T_co]]: ... +@overload +@deprecated( + "Annotating the return type as `-> Iterator[Foo]` with `@contextmanager` is deprecated. Use `-> Generator[Foo]` instead." +) +def contextmanager(func: Callable[_P, Iterator[_T_co]]) -> Callable[_P, _GeneratorContextManager[_T_co]]: ... + +_AF = TypeVar("_AF", bound=Callable[..., Awaitable[Any]]) + +class AsyncContextDecorator: + def _recreate_cm(self) -> Self: ... + def __call__(self, func: _AF) -> _AF: ... + +class _AsyncGeneratorContextManager( + _GeneratorContextManagerBase[AsyncGenerator[_T_co, _SendT_contra]], + AbstractAsyncContextManager[_T_co, bool | None], + AsyncContextDecorator, +): + async def __aexit__( + self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None + ) -> bool | None: ... + +@overload +def asynccontextmanager(func: Callable[_P, AsyncGenerator[_T_co]]) -> Callable[_P, _AsyncGeneratorContextManager[_T_co]]: ... +@overload +@deprecated( + "Annotating the return type as `-> AsyncIterator[Foo]` with `@asynccontextmanager` is deprecated. " + "Use `-> AsyncGenerator[Foo]` instead." +) +def asynccontextmanager(func: Callable[_P, AsyncIterator[_T_co]]) -> Callable[_P, _AsyncGeneratorContextManager[_T_co]]: ... + +@type_check_only +class _SupportsClose(Protocol): + def close(self) -> object: ... + +_SupportsCloseT = TypeVar("_SupportsCloseT", bound=_SupportsClose) + +class closing(AbstractContextManager[_SupportsCloseT, None]): + def __init__(self, thing: _SupportsCloseT) -> None: ... + def __exit__(self, *exc_info: Unused) -> None: ... + +@type_check_only +class _SupportsAclose(Protocol): + def aclose(self) -> Awaitable[object]: ... + +_SupportsAcloseT = TypeVar("_SupportsAcloseT", bound=_SupportsAclose) + +class aclosing(AbstractAsyncContextManager[_SupportsAcloseT, None]): + def __init__(self, thing: _SupportsAcloseT) -> None: ... + async def __aexit__(self, *exc_info: Unused) -> None: ... + +class suppress(AbstractContextManager[None, bool]): + def __init__(self, *exceptions: type[BaseException]) -> None: ... + def __exit__( + self, exctype: type[BaseException] | None, excinst: BaseException | None, exctb: TracebackType | None + ) -> bool: ... + +# This is trying to describe what is needed for (most?) uses +# of `redirect_stdout` and `redirect_stderr`. +# https://github.com/python/typeshed/issues/14903 +@type_check_only +class _SupportsRedirect(Protocol): + def write(self, s: str, /) -> int: ... + def flush(self) -> None: ... + +_SupportsRedirectT = TypeVar("_SupportsRedirectT", bound=_SupportsRedirect | None) + +class _RedirectStream(AbstractContextManager[_SupportsRedirectT, None]): + def __init__(self, new_target: _SupportsRedirectT) -> None: ... + def __exit__( + self, exctype: type[BaseException] | None, excinst: BaseException | None, exctb: TracebackType | None + ) -> None: ... + +class redirect_stdout(_RedirectStream[_SupportsRedirectT]): ... +class redirect_stderr(_RedirectStream[_SupportsRedirectT]): ... + +class _BaseExitStack(Generic[_ExitT_co]): + def enter_context(self, cm: AbstractContextManager[_T, _ExitT_co]) -> _T: ... + def push(self, exit: _CM_EF) -> _CM_EF: ... + def callback(self, callback: Callable[_P, _T], /, *args: _P.args, **kwds: _P.kwargs) -> Callable[_P, _T]: ... + def pop_all(self) -> Self: ... + +# this class is to avoid putting `metaclass=abc.ABCMeta` on the implementations directly, as this would make them +# appear explicitly abstract to some tools. this is due to the implementations not subclassing `AbstractContextManager` +# see note on the subclasses +@type_check_only +class _BaseExitStackAbstract(_BaseExitStack[_ExitT_co], metaclass=abc.ABCMeta): ... + +# In reality this is a subclass of `AbstractContextManager`, but we can't provide `Self` as the argument for `__enter__` +# https://discuss.python.org/t/self-as-typevar-default/90939 +class ExitStack(_BaseExitStackAbstract[_ExitT_co]): + def close(self) -> None: ... + def __enter__(self) -> Self: ... + def __exit__( + self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / + ) -> _ExitT_co: ... + +_ExitCoroFunc: TypeAlias = Callable[ + [type[BaseException] | None, BaseException | None, TracebackType | None], Awaitable[bool | None] +] +_ACM_EF = TypeVar("_ACM_EF", bound=AbstractAsyncContextManager[Any, Any] | _ExitCoroFunc) + +# In reality this is a subclass of `AbstractContextManager`, but we can't provide `Self` as the argument for `__enter__` +# https://discuss.python.org/t/self-as-typevar-default/90939 +class AsyncExitStack(_BaseExitStackAbstract[_ExitT_co]): + async def enter_async_context(self, cm: AbstractAsyncContextManager[_T, _ExitT_co]) -> _T: ... + def push_async_exit(self, exit: _ACM_EF) -> _ACM_EF: ... + def push_async_callback( + self, callback: Callable[_P, Awaitable[_T]], /, *args: _P.args, **kwds: _P.kwargs + ) -> Callable[_P, Awaitable[_T]]: ... + async def aclose(self) -> None: ... + async def __aenter__(self) -> Self: ... + async def __aexit__( + self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / + ) -> _ExitT_co: ... + +class nullcontext(AbstractContextManager[_T, None], AbstractAsyncContextManager[_T, None]): + enter_result: _T + + @overload + def __init__(self: nullcontext[None]) -> None: ... + @overload + def __init__(self: nullcontext[_T], enter_result: _T) -> None: ... # pyright: ignore[reportInvalidTypeVarUse] #11780 + + def __enter__(self) -> _T: ... + def __exit__(self, *exctype: Unused) -> None: ... + async def __aenter__(self) -> _T: ... + async def __aexit__(self, *exctype: Unused) -> None: ... + +if sys.version_info >= (3, 11): + _T_fd_or_any_path = TypeVar("_T_fd_or_any_path", bound=FileDescriptorOrPath) + + class chdir(AbstractContextManager[None, None], Generic[_T_fd_or_any_path]): + path: _T_fd_or_any_path + def __init__(self, path: _T_fd_or_any_path) -> None: ... + def __enter__(self) -> None: ... + def __exit__(self, *excinfo: Unused) -> None: ... diff --git a/crates/basilisk-checker/tests/fixtures/typeshed/functools.pyi b/crates/basilisk-checker/tests/fixtures/typeshed/functools.pyi new file mode 100644 index 00000000..5619a64f --- /dev/null +++ b/crates/basilisk-checker/tests/fixtures/typeshed/functools.pyi @@ -0,0 +1,281 @@ +import sys +import types +from _typeshed import SupportsAllComparisons, SupportsItems +from collections.abc import Callable, Hashable, Iterable, Sized +from types import GenericAlias +from typing import ( + Any, + Final, + Generic, + Literal, + NamedTuple, + ParamSpec, + TypeAlias, + TypedDict, + TypeVar, + final, + overload, + type_check_only, +) +from typing_extensions import Self, disjoint_base + +__all__ = [ + "update_wrapper", + "wraps", + "WRAPPER_ASSIGNMENTS", + "WRAPPER_UPDATES", + "total_ordering", + "cmp_to_key", + "lru_cache", + "reduce", + "partial", + "partialmethod", + "singledispatch", + "cached_property", + "singledispatchmethod", + "cache", +] + +_T = TypeVar("_T") +_T_co = TypeVar("_T_co", covariant=True) +_S = TypeVar("_S") +_PWrapped = ParamSpec("_PWrapped") +_RWrapped = TypeVar("_RWrapped") +_PWrapper = ParamSpec("_PWrapper") +_RWrapper = TypeVar("_RWrapper") + +if sys.version_info >= (3, 14): + @overload + def reduce(function: Callable[[_T, _S], _T], iterable: Iterable[_S], /, initial: _T) -> _T: ... +else: + @overload + def reduce(function: Callable[[_T, _S], _T], iterable: Iterable[_S], initial: _T, /) -> _T: ... + +@overload +def reduce(function: Callable[[_T, _T], _T], iterable: Iterable[_T], /) -> _T: ... + +class _CacheInfo(NamedTuple): + hits: int + misses: int + maxsize: int | None + currsize: int + +@type_check_only +class _CacheParameters(TypedDict): + maxsize: int + typed: bool + +@final +class _lru_cache_wrapper(Generic[_T_co]): + __wrapped__: Callable[..., _T_co] + def __call__(self, *args: Hashable, **kwargs: Hashable) -> _T_co: ... + def cache_info(self) -> _CacheInfo: ... + def cache_clear(self) -> None: ... + def cache_parameters(self) -> _CacheParameters: ... + def __copy__(self) -> _lru_cache_wrapper[_T_co]: ... + def __deepcopy__(self, memo: Any, /) -> _lru_cache_wrapper[_T_co]: ... + + # as with ``Callable``, we'll assume that these attributes exist + __name__: str + __qualname__: str + +@overload +def lru_cache(maxsize: int | None = 128, typed: bool = False) -> Callable[[Callable[..., _T]], _lru_cache_wrapper[_T]]: ... +@overload +def lru_cache(maxsize: Callable[..., _T], typed: bool = False) -> _lru_cache_wrapper[_T]: ... + +if sys.version_info >= (3, 14): + WRAPPER_ASSIGNMENTS: Final[ + tuple[ + Literal["__module__"], + Literal["__name__"], + Literal["__qualname__"], + Literal["__doc__"], + Literal["__annotate__"], + Literal["__type_params__"], + ] + ] +elif sys.version_info >= (3, 12): + WRAPPER_ASSIGNMENTS: Final[ + tuple[ + Literal["__module__"], + Literal["__name__"], + Literal["__qualname__"], + Literal["__doc__"], + Literal["__annotations__"], + Literal["__type_params__"], + ] + ] +else: + WRAPPER_ASSIGNMENTS: Final[ + tuple[Literal["__module__"], Literal["__name__"], Literal["__qualname__"], Literal["__doc__"], Literal["__annotations__"]] + ] + +WRAPPER_UPDATES: Final[tuple[Literal["__dict__"]]] + +@type_check_only +class _Wrapped(Generic[_PWrapped, _RWrapped, _PWrapper, _RWrapper]): + __wrapped__: Callable[_PWrapped, _RWrapped] + def __call__(self, *args: _PWrapper.args, **kwargs: _PWrapper.kwargs) -> _RWrapper: ... + # as with ``Callable``, we'll assume that these attributes exist + __name__: str + __qualname__: str + +@type_check_only +class _Wrapper(Generic[_PWrapped, _RWrapped]): + def __call__(self, f: Callable[_PWrapper, _RWrapper]) -> _Wrapped[_PWrapped, _RWrapped, _PWrapper, _RWrapper]: ... + +if sys.version_info >= (3, 14): + def update_wrapper( + wrapper: Callable[_PWrapper, _RWrapper], + wrapped: Callable[_PWrapped, _RWrapped], + assigned: Iterable[str] = ("__module__", "__name__", "__qualname__", "__doc__", "__annotate__", "__type_params__"), + updated: Iterable[str] = ("__dict__",), + ) -> _Wrapped[_PWrapped, _RWrapped, _PWrapper, _RWrapper]: ... + def wraps( + wrapped: Callable[_PWrapped, _RWrapped], + assigned: Iterable[str] = ("__module__", "__name__", "__qualname__", "__doc__", "__annotate__", "__type_params__"), + updated: Iterable[str] = ("__dict__",), + ) -> _Wrapper[_PWrapped, _RWrapped]: ... + +elif sys.version_info >= (3, 12): + def update_wrapper( + wrapper: Callable[_PWrapper, _RWrapper], + wrapped: Callable[_PWrapped, _RWrapped], + assigned: Iterable[str] = ("__module__", "__name__", "__qualname__", "__doc__", "__annotations__", "__type_params__"), + updated: Iterable[str] = ("__dict__",), + ) -> _Wrapped[_PWrapped, _RWrapped, _PWrapper, _RWrapper]: ... + def wraps( + wrapped: Callable[_PWrapped, _RWrapped], + assigned: Iterable[str] = ("__module__", "__name__", "__qualname__", "__doc__", "__annotations__", "__type_params__"), + updated: Iterable[str] = ("__dict__",), + ) -> _Wrapper[_PWrapped, _RWrapped]: ... + +else: + def update_wrapper( + wrapper: Callable[_PWrapper, _RWrapper], + wrapped: Callable[_PWrapped, _RWrapped], + assigned: Iterable[str] = ("__module__", "__name__", "__qualname__", "__doc__", "__annotations__"), + updated: Iterable[str] = ("__dict__",), + ) -> _Wrapped[_PWrapped, _RWrapped, _PWrapper, _RWrapper]: ... + def wraps( + wrapped: Callable[_PWrapped, _RWrapped], + assigned: Iterable[str] = ("__module__", "__name__", "__qualname__", "__doc__", "__annotations__"), + updated: Iterable[str] = ("__dict__",), + ) -> _Wrapper[_PWrapped, _RWrapped]: ... + +def total_ordering(cls: type[_T]) -> type[_T]: ... +def cmp_to_key(mycmp: Callable[[_T, _T], int]) -> Callable[[_T], SupportsAllComparisons]: ... + +@disjoint_base +class partial(Generic[_T]): + @property + def func(self) -> Callable[..., _T]: ... + @property + def args(self) -> tuple[Any, ...]: ... + @property + def keywords(self) -> dict[str, Any]: ... + def __new__(cls, func: Callable[..., _T], /, *args: Any, **kwargs: Any) -> Self: ... + def __call__(self, /, *args: Any, **kwargs: Any) -> _T: ... + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + +# With protocols, this could change into a generic protocol that defines __get__ and returns _T +_Descriptor: TypeAlias = Any + +class partialmethod(Generic[_T]): + func: Callable[..., _T] | _Descriptor + args: tuple[Any, ...] + keywords: dict[str, Any] + if sys.version_info >= (3, 14): + @overload + def __new__(self, func: Callable[..., _T], /, *args: Any, **keywords: Any) -> Self: ... + @overload + def __new__(self, func: _Descriptor, /, *args: Any, **keywords: Any) -> Self: ... + else: + @overload + def __init__(self, func: Callable[..., _T], /, *args: Any, **keywords: Any) -> None: ... + @overload + def __init__(self, func: _Descriptor, /, *args: Any, **keywords: Any) -> None: ... + + def __get__(self, obj: Any, cls: type[Any] | None = None) -> Callable[..., _T]: ... + @property + def __isabstractmethod__(self) -> bool: ... + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + +if sys.version_info >= (3, 11): + _RegType: TypeAlias = type[Any] | types.UnionType +else: + _RegType: TypeAlias = type[Any] + +@type_check_only +class _SingleDispatchCallable(Generic[_T]): + registry: types.MappingProxyType[Any, Callable[..., _T]] + def dispatch(self, cls: Any) -> Callable[..., _T]: ... + + # @fun.register(complex) + # def _(arg, verbose=False): ... + @overload + def register(self, cls: _RegType, func: None = None) -> Callable[[Callable[..., _T]], Callable[..., _T]]: ... + # @fun.register + # def _(arg: int, verbose=False): + @overload + def register(self, cls: Callable[..., _T], func: None = None) -> Callable[..., _T]: ... + # fun.register(int, lambda x: x) + @overload + def register(self, cls: _RegType, func: Callable[..., _T]) -> Callable[..., _T]: ... + + def _clear_cache(self) -> None: ... + def __call__(self, /, *args: Any, **kwargs: Any) -> _T: ... + +def singledispatch(func: Callable[..., _T]) -> _SingleDispatchCallable[_T]: ... + +class singledispatchmethod(Generic[_T]): + dispatcher: _SingleDispatchCallable[_T] + func: Callable[..., _T] + def __init__(self, func: Callable[..., _T]) -> None: ... + @property + def __isabstractmethod__(self) -> bool: ... + + @overload + def register(self, cls: _RegType, method: None = None) -> Callable[[Callable[..., _T]], Callable[..., _T]]: ... + @overload + def register(self, cls: Callable[..., _T], method: None = None) -> Callable[..., _T]: ... + @overload + def register(self, cls: _RegType, method: Callable[..., _T]) -> Callable[..., _T]: ... + + def __get__(self, obj: _S, cls: type[_S] | None = None) -> Callable[..., _T]: ... + +class cached_property(Generic[_T_co]): + func: Callable[[Any], _T_co] + attrname: str | None + def __init__(self, func: Callable[[Any], _T_co]) -> None: ... + + @overload + def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: ... + @overload + def __get__(self, instance: object, owner: type[Any] | None = None) -> _T_co: ... + + def __set_name__(self, owner: type[Any], name: str) -> None: ... + # __set__ is not defined at runtime, but @cached_property is designed to be settable + def __set__(self, instance: object, value: _T_co) -> None: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + +def cache(user_function: Callable[..., _T], /) -> _lru_cache_wrapper[_T]: ... +def _make_key( + args: tuple[Hashable, ...], + kwds: SupportsItems[Any, Any], + typed: bool, + kwd_mark: tuple[object, ...] = ..., + fasttypes: set[type] = ..., + tuple: type = ..., + type: Any = ..., + len: Callable[[Sized], int] = ..., +) -> Hashable: ... + +if sys.version_info >= (3, 14): + @final + class _PlaceholderType: ... + + Placeholder: Final[_PlaceholderType] + + __all__ += ["Placeholder"] diff --git a/crates/basilisk-checker/tests/fixtures/typeshed/heapq.pyi b/crates/basilisk-checker/tests/fixtures/typeshed/heapq.pyi new file mode 100644 index 00000000..7969aa4e --- /dev/null +++ b/crates/basilisk-checker/tests/fixtures/typeshed/heapq.pyi @@ -0,0 +1,32 @@ +import sys +from _heapq import * +from _typeshed import SupportsRichComparison, SupportsRichComparisonT as _T +from collections.abc import Callable, Generator, Iterable +from typing import Final, TypeVar, overload + +__all__ = ["heappush", "heappop", "heapify", "heapreplace", "merge", "nlargest", "nsmallest", "heappushpop"] + +if sys.version_info >= (3, 14): + # Added to __all__ in 3.14.1 + __all__ += ["heapify_max", "heappop_max", "heappush_max", "heappushpop_max", "heapreplace_max"] + +_S = TypeVar("_S") + +__about__: Final[str] + +@overload +def merge(*iterables: Iterable[_S], key: Callable[[_S], SupportsRichComparison], reverse: bool = False) -> Generator[_S]: ... +@overload +def merge(*iterables: Iterable[_T], key: None = None, reverse: bool = False) -> Generator[_T]: ... + +@overload +def nlargest(n: int, iterable: Iterable[_S], key: Callable[[_S], SupportsRichComparison]) -> list[_S]: ... +@overload +def nlargest(n: int, iterable: Iterable[_T], key: None = None) -> list[_T]: ... + +@overload +def nsmallest(n: int, iterable: Iterable[_S], key: Callable[[_S], SupportsRichComparison]) -> list[_S]: ... +@overload +def nsmallest(n: int, iterable: Iterable[_T], key: None = None) -> list[_T]: ... + +def _heapify_max(heap: list[SupportsRichComparison], /) -> None: ... # undocumented diff --git a/crates/basilisk-checker/tests/fixtures/typeshed/itertools.pyi b/crates/basilisk-checker/tests/fixtures/typeshed/itertools.pyi new file mode 100644 index 00000000..d26a4e1d --- /dev/null +++ b/crates/basilisk-checker/tests/fixtures/typeshed/itertools.pyi @@ -0,0 +1,376 @@ +import sys +from _typeshed import MaybeNone +from collections.abc import Callable, Iterable, Iterator +from types import GenericAlias +from typing import Any, Generic, Literal, SupportsComplex, SupportsFloat, SupportsIndex, SupportsInt, TypeAlias, TypeVar, overload +from typing_extensions import Self, disjoint_base + +_T = TypeVar("_T") +_S = TypeVar("_S") +_N = TypeVar("_N", int, float, SupportsFloat, SupportsInt, SupportsIndex, SupportsComplex) +_T_co = TypeVar("_T_co", covariant=True) +_S_co = TypeVar("_S_co", covariant=True) +_T1 = TypeVar("_T1") +_T2 = TypeVar("_T2") +_T3 = TypeVar("_T3") +_T4 = TypeVar("_T4") +_T5 = TypeVar("_T5") +_T6 = TypeVar("_T6") +_T7 = TypeVar("_T7") +_T8 = TypeVar("_T8") +_T9 = TypeVar("_T9") +_T10 = TypeVar("_T10") + +_Step: TypeAlias = SupportsFloat | SupportsInt | SupportsIndex | SupportsComplex + +_Predicate: TypeAlias = Callable[[_T], object] + +# Technically count can take anything that implements a number protocol and has an add method +# but we can't enforce the add method +@disjoint_base +class count(Generic[_N]): + @overload + def __new__(cls) -> count[int]: ... + @overload + def __new__(cls, start: _N, step: _Step = 1) -> count[_N]: ... + @overload + def __new__(cls, *, step: _N) -> count[_N]: ... + + def __next__(self) -> _N: ... + def __iter__(self) -> Self: ... + +@disjoint_base +class cycle(Generic[_T]): + def __new__(cls, iterable: Iterable[_T], /) -> Self: ... + def __next__(self) -> _T: ... + def __iter__(self) -> Self: ... + +@disjoint_base +class repeat(Generic[_T]): + @overload + def __new__(cls, object: _T) -> Self: ... + @overload + def __new__(cls, object: _T, times: int) -> Self: ... + + def __next__(self) -> _T: ... + def __iter__(self) -> Self: ... + def __length_hint__(self) -> int: ... + +@disjoint_base +class accumulate(Generic[_T]): + @overload + def __new__(cls, iterable: Iterable[_T], func: None = None, *, initial: _T | None = None) -> Self: ... + @overload + def __new__(cls, iterable: Iterable[_S], func: Callable[[_T, _S], _T], *, initial: _T | None = None) -> Self: ... + + def __iter__(self) -> Self: ... + def __next__(self) -> _T: ... + +@disjoint_base +class chain(Generic[_T]): + def __new__(cls, *iterables: Iterable[_T]) -> Self: ... + def __next__(self) -> _T: ... + def __iter__(self) -> Self: ... + @classmethod + # We use type[Any] and not type[_S] to not lose the type inference from __iterable + def from_iterable(cls: type[Any], iterable: Iterable[Iterable[_S]], /) -> chain[_S]: ... + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + +@disjoint_base +class compress(Generic[_T]): + def __new__(cls, data: Iterable[_T], selectors: Iterable[Any]) -> Self: ... + def __iter__(self) -> Self: ... + def __next__(self) -> _T: ... + +@disjoint_base +class dropwhile(Generic[_T]): + def __new__(cls, predicate: _Predicate[_T], iterable: Iterable[_T], /) -> Self: ... + def __iter__(self) -> Self: ... + def __next__(self) -> _T: ... + +@disjoint_base +class filterfalse(Generic[_T]): + def __new__(cls, function: _Predicate[_T] | None, iterable: Iterable[_T], /) -> Self: ... + def __iter__(self) -> Self: ... + def __next__(self) -> _T: ... + +@disjoint_base +class groupby(Generic[_T_co, _S_co]): + @overload + def __new__(cls, iterable: Iterable[_T1], key: None = None) -> groupby[_T1, _T1]: ... + @overload + def __new__(cls, iterable: Iterable[_T1], key: Callable[[_T1], _T2]) -> groupby[_T2, _T1]: ... + + def __iter__(self) -> Self: ... + def __next__(self) -> tuple[_T_co, Iterator[_S_co]]: ... + +@disjoint_base +class islice(Generic[_T]): + @overload + def __new__(cls, iterable: Iterable[_T], stop: int | None, /) -> Self: ... + @overload + def __new__(cls, iterable: Iterable[_T], start: int | None, stop: int | None, step: int | None = 1, /) -> Self: ... + + def __iter__(self) -> Self: ... + def __next__(self) -> _T: ... + +@disjoint_base +class starmap(Generic[_T_co]): + def __new__(cls, function: Callable[..., _T], iterable: Iterable[Iterable[Any]], /) -> starmap[_T]: ... + def __iter__(self) -> Self: ... + def __next__(self) -> _T_co: ... + +@disjoint_base +class takewhile(Generic[_T]): + def __new__(cls, predicate: _Predicate[_T], iterable: Iterable[_T], /) -> Self: ... + def __iter__(self) -> Self: ... + def __next__(self) -> _T: ... + +def tee(iterable: Iterable[_T], n: int = 2, /) -> tuple[Iterator[_T], ...]: ... + +@disjoint_base +class zip_longest(Generic[_T_co]): + # one iterable (fillvalue doesn't matter) + @overload + def __new__(cls, iter1: Iterable[_T1], /, *, fillvalue: object = None) -> zip_longest[tuple[_T1]]: ... + # two iterables + @overload + # In the overloads without fillvalue, all of the tuple members could theoretically be None, + # but we return Any instead to avoid false positives for code where we know one of the iterables + # is longer. + def __new__(cls, iter1: Iterable[_T1], iter2: Iterable[_T2], /) -> zip_longest[tuple[_T1 | MaybeNone, _T2 | MaybeNone]]: ... + @overload + def __new__( + cls, iter1: Iterable[_T1], iter2: Iterable[_T2], /, *, fillvalue: _T + ) -> zip_longest[tuple[_T1 | _T, _T2 | _T]]: ... + # three iterables + @overload + def __new__( + cls, iter1: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], / + ) -> zip_longest[tuple[_T1 | MaybeNone, _T2 | MaybeNone, _T3 | MaybeNone]]: ... + @overload + def __new__( + cls, iter1: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], /, *, fillvalue: _T + ) -> zip_longest[tuple[_T1 | _T, _T2 | _T, _T3 | _T]]: ... + # four iterables + @overload + def __new__( + cls, iter1: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], iter4: Iterable[_T4], / + ) -> zip_longest[tuple[_T1 | MaybeNone, _T2 | MaybeNone, _T3 | MaybeNone, _T4 | MaybeNone]]: ... + @overload + def __new__( + cls, iter1: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], iter4: Iterable[_T4], /, *, fillvalue: _T + ) -> zip_longest[tuple[_T1 | _T, _T2 | _T, _T3 | _T, _T4 | _T]]: ... + # five iterables + @overload + def __new__( + cls, iter1: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], iter4: Iterable[_T4], iter5: Iterable[_T5], / + ) -> zip_longest[tuple[_T1 | MaybeNone, _T2 | MaybeNone, _T3 | MaybeNone, _T4 | MaybeNone, _T5 | MaybeNone]]: ... + @overload + def __new__( + cls, + iter1: Iterable[_T1], + iter2: Iterable[_T2], + iter3: Iterable[_T3], + iter4: Iterable[_T4], + iter5: Iterable[_T5], + /, + *, + fillvalue: _T, + ) -> zip_longest[tuple[_T1 | _T, _T2 | _T, _T3 | _T, _T4 | _T, _T5 | _T]]: ... + # six or more iterables + @overload + def __new__( + cls, + iter1: Iterable[_T], + iter2: Iterable[_T], + iter3: Iterable[_T], + iter4: Iterable[_T], + iter5: Iterable[_T], + iter6: Iterable[_T], + /, + *iterables: Iterable[_T], + ) -> zip_longest[tuple[_T | MaybeNone, ...]]: ... + @overload + def __new__( + cls, + iter1: Iterable[_T], + iter2: Iterable[_T], + iter3: Iterable[_T], + iter4: Iterable[_T], + iter5: Iterable[_T], + iter6: Iterable[_T], + /, + *iterables: Iterable[_T], + fillvalue: _T, + ) -> zip_longest[tuple[_T, ...]]: ... + + def __iter__(self) -> Self: ... + def __next__(self) -> _T_co: ... + +@disjoint_base +class product(Generic[_T_co]): + @overload + def __new__(cls, iter1: Iterable[_T1], /) -> product[tuple[_T1]]: ... + @overload + def __new__(cls, iter1: Iterable[_T1], iter2: Iterable[_T2], /) -> product[tuple[_T1, _T2]]: ... + @overload + def __new__(cls, iter1: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], /) -> product[tuple[_T1, _T2, _T3]]: ... + @overload + def __new__( + cls, iter1: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], iter4: Iterable[_T4], / + ) -> product[tuple[_T1, _T2, _T3, _T4]]: ... + @overload + def __new__( + cls, iter1: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], iter4: Iterable[_T4], iter5: Iterable[_T5], / + ) -> product[tuple[_T1, _T2, _T3, _T4, _T5]]: ... + @overload + def __new__( + cls, + iter1: Iterable[_T1], + iter2: Iterable[_T2], + iter3: Iterable[_T3], + iter4: Iterable[_T4], + iter5: Iterable[_T5], + iter6: Iterable[_T6], + /, + ) -> product[tuple[_T1, _T2, _T3, _T4, _T5, _T6]]: ... + @overload + def __new__( + cls, + iter1: Iterable[_T1], + iter2: Iterable[_T2], + iter3: Iterable[_T3], + iter4: Iterable[_T4], + iter5: Iterable[_T5], + iter6: Iterable[_T6], + iter7: Iterable[_T7], + /, + ) -> product[tuple[_T1, _T2, _T3, _T4, _T5, _T6, _T7]]: ... + @overload + def __new__( + cls, + iter1: Iterable[_T1], + iter2: Iterable[_T2], + iter3: Iterable[_T3], + iter4: Iterable[_T4], + iter5: Iterable[_T5], + iter6: Iterable[_T6], + iter7: Iterable[_T7], + iter8: Iterable[_T8], + /, + ) -> product[tuple[_T1, _T2, _T3, _T4, _T5, _T6, _T7, _T8]]: ... + @overload + def __new__( + cls, + iter1: Iterable[_T1], + iter2: Iterable[_T2], + iter3: Iterable[_T3], + iter4: Iterable[_T4], + iter5: Iterable[_T5], + iter6: Iterable[_T6], + iter7: Iterable[_T7], + iter8: Iterable[_T8], + iter9: Iterable[_T9], + /, + ) -> product[tuple[_T1, _T2, _T3, _T4, _T5, _T6, _T7, _T8, _T9]]: ... + @overload + def __new__( + cls, + iter1: Iterable[_T1], + iter2: Iterable[_T2], + iter3: Iterable[_T3], + iter4: Iterable[_T4], + iter5: Iterable[_T5], + iter6: Iterable[_T6], + iter7: Iterable[_T7], + iter8: Iterable[_T8], + iter9: Iterable[_T9], + iter10: Iterable[_T10], + /, + ) -> product[tuple[_T1, _T2, _T3, _T4, _T5, _T6, _T7, _T8, _T9, _T10]]: ... + @overload + def __new__(cls, *iterables: Iterable[_T1], repeat: int = 1) -> product[tuple[_T1, ...]]: ... + + def __iter__(self) -> Self: ... + def __next__(self) -> _T_co: ... + +@disjoint_base +class permutations(Generic[_T_co]): + @overload + def __new__(cls, iterable: Iterable[_T], r: Literal[2]) -> permutations[tuple[_T, _T]]: ... + @overload + def __new__(cls, iterable: Iterable[_T], r: Literal[3]) -> permutations[tuple[_T, _T, _T]]: ... + @overload + def __new__(cls, iterable: Iterable[_T], r: Literal[4]) -> permutations[tuple[_T, _T, _T, _T]]: ... + @overload + def __new__(cls, iterable: Iterable[_T], r: Literal[5]) -> permutations[tuple[_T, _T, _T, _T, _T]]: ... + @overload + def __new__(cls, iterable: Iterable[_T], r: int | None = None) -> permutations[tuple[_T, ...]]: ... + + def __iter__(self) -> Self: ... + def __next__(self) -> _T_co: ... + +@disjoint_base +class combinations(Generic[_T_co]): + @overload + def __new__(cls, iterable: Iterable[_T], r: Literal[2]) -> combinations[tuple[_T, _T]]: ... + @overload + def __new__(cls, iterable: Iterable[_T], r: Literal[3]) -> combinations[tuple[_T, _T, _T]]: ... + @overload + def __new__(cls, iterable: Iterable[_T], r: Literal[4]) -> combinations[tuple[_T, _T, _T, _T]]: ... + @overload + def __new__(cls, iterable: Iterable[_T], r: Literal[5]) -> combinations[tuple[_T, _T, _T, _T, _T]]: ... + @overload + def __new__(cls, iterable: Iterable[_T], r: int) -> combinations[tuple[_T, ...]]: ... + + def __iter__(self) -> Self: ... + def __next__(self) -> _T_co: ... + +@disjoint_base +class combinations_with_replacement(Generic[_T_co]): + @overload + def __new__(cls, iterable: Iterable[_T], r: Literal[2]) -> combinations_with_replacement[tuple[_T, _T]]: ... + @overload + def __new__(cls, iterable: Iterable[_T], r: Literal[3]) -> combinations_with_replacement[tuple[_T, _T, _T]]: ... + @overload + def __new__(cls, iterable: Iterable[_T], r: Literal[4]) -> combinations_with_replacement[tuple[_T, _T, _T, _T]]: ... + @overload + def __new__(cls, iterable: Iterable[_T], r: Literal[5]) -> combinations_with_replacement[tuple[_T, _T, _T, _T, _T]]: ... + @overload + def __new__(cls, iterable: Iterable[_T], r: int) -> combinations_with_replacement[tuple[_T, ...]]: ... + + def __iter__(self) -> Self: ... + def __next__(self) -> _T_co: ... + +@disjoint_base +class pairwise(Generic[_T_co]): + def __new__(cls, iterable: Iterable[_T], /) -> pairwise[tuple[_T, _T]]: ... + def __iter__(self) -> Self: ... + def __next__(self) -> _T_co: ... + +if sys.version_info >= (3, 12): + @disjoint_base + class batched(Generic[_T_co]): + if sys.version_info >= (3, 13): + @overload + def __new__(cls, iterable: Iterable[_T], n: Literal[1], *, strict: Literal[True]) -> batched[tuple[_T]]: ... + @overload + def __new__(cls, iterable: Iterable[_T], n: Literal[2], *, strict: Literal[True]) -> batched[tuple[_T, _T]]: ... + @overload + def __new__(cls, iterable: Iterable[_T], n: Literal[3], *, strict: Literal[True]) -> batched[tuple[_T, _T, _T]]: ... + @overload + def __new__( + cls, iterable: Iterable[_T], n: Literal[4], *, strict: Literal[True] + ) -> batched[tuple[_T, _T, _T, _T]]: ... + @overload + def __new__( + cls, iterable: Iterable[_T], n: Literal[5], *, strict: Literal[True] + ) -> batched[tuple[_T, _T, _T, _T, _T]]: ... + @overload + def __new__(cls, iterable: Iterable[_T], n: int, *, strict: bool = False) -> batched[tuple[_T, ...]]: ... + else: + def __new__(cls, iterable: Iterable[_T], n: int) -> batched[tuple[_T, ...]]: ... + + def __iter__(self) -> Self: ... + def __next__(self) -> _T_co: ... diff --git a/crates/basilisk-checker/tests/gradual_guarantee_tests.rs b/crates/basilisk-checker/tests/gradual_guarantee_tests.rs new file mode 100644 index 00000000..60b49be9 --- /dev/null +++ b/crates/basilisk-checker/tests/gradual_guarantee_tests.rs @@ -0,0 +1,369 @@ +//! Tests for [TYPEINF-TARGET-GRADUAL]. See +//! docs/specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-TARGET-GRADUAL and the +//! Stage 0 checklist in +//! docs/plans/CHECKER-TYPE-NARROWING-INFERENCE-PLAN.md#NARROWPLAN-CHECKLIST. +//! +//! The gradual guarantee is a differential invariant: **removing an +//! annotation must never introduce a new static error**. The harness strips +//! annotations from a corpus (byte-preserving — every removed span is +//! overwritten with spaces, so all other diagnostic spans stay comparable), +//! re-checks with the default config, and asserts the stripped file's error +//! set is a subset of the annotated file's. +#![expect( + clippy::expect_used, + reason = "test-only parsing of fixed, known-valid corpus fixtures" +)] + +use std::collections::BTreeSet; + +use ruff_python_ast::visitor::{walk_stmt, Visitor}; +use ruff_python_ast::{Parameter, Stmt}; +use ruff_text_size::{Ranged, TextRange}; + +/// Collect the byte ranges of removable annotations in one module. +#[derive(Default)] +struct AnnotationRanges<'src> { + ranges: Vec, + source: &'src str, +} + +impl AnnotationRanges<'_> { + /// Record the `: T` span of one parameter (name end → annotation end, + /// which covers the colon). + fn record_parameter(&mut self, parameter: &Parameter) { + if let Some(annotation) = ¶meter.annotation { + self.ranges.push(TextRange::new( + parameter.name.range().end(), + annotation.range().end(), + )); + } + } + + /// Whether a class body is a **declaration interface** — a `Protocol`, + /// `TypedDict`, or `NamedTuple`. Annotations there define the structural + /// interface itself (member types, protocol variance inference per PEP + /// 544), so removing them yields a *different* interface, not the same + /// program with fewer hints. The gradual guarantee + /// ([TYPEINF-TARGET-GRADUAL]) is about inferable positions; these bodies + /// are excluded from stripping. + fn is_declaration_interface(&self, def: &ruff_python_ast::StmtClassDef) -> bool { + let base_is_interface = def.bases().iter().any(|base| { + let range = base.range(); + self.source + .get(usize::from(range.start())..usize::from(range.end())) + .is_some_and(|text| { + text.contains("Protocol") + || text.contains("TypedDict") + || text.contains("NamedTuple") + }) + }); + // A dataclass body is declaration syntax at RUNTIME (PEP 557): only + // annotated assignments become fields, so `x: int = 1` and `x = 1` + // are different programs, not the same program with fewer hints. + let is_dataclass = def.decorator_list.iter().any(|decorator| { + let range = decorator.range(); + self.source + .get(usize::from(range.start())..usize::from(range.end())) + .is_some_and(|text| text.contains("dataclass")) + }); + base_is_interface || is_dataclass + } +} + +impl Visitor<'_> for AnnotationRanges<'_> { + fn visit_stmt(&mut self, stmt: &Stmt) { + match stmt { + Stmt::ClassDef(def) if self.is_declaration_interface(def) => { + // Do not recurse: every annotation inside is load-bearing. + return; + } + // `@overload` signatures are declaration syntax too — the + // annotations ARE the overload (PEP 484 §overloading). Stripping + // them produces a different (and typically inconsistent) overload + // set, not the same program with fewer hints. + Stmt::FunctionDef(def) + if def.decorator_list.iter().any(|decorator| { + let range = decorator.range(); + self.source + .get(usize::from(range.start())..usize::from(range.end())) + .is_some_and(|text| text.contains("overload")) + }) => + { + return; + } + Stmt::FunctionDef(def) => { + for parameter in &def.parameters { + self.record_parameter(parameter.as_parameter()); + } + if let Some(returns) = &def.returns { + // The `->` arrow before the annotation is blanked by the + // splicer's back-scan; only the annotation span is recorded. + self.ranges.push(returns.range()); + } + } + // `x: T = value` → `x = value`. Bare `x: T` (no value) IS the + // declaration — nothing would remain after removal, so keep it. + // `Final`/`ClassVar` are semantic QUALIFIERS (PEP 591/526): they + // change what the program may legally do (reassignment, instance + // vs class storage), not just the inferable type — removing them + // is a semantic edit, so they stay. + Stmt::AnnAssign(ann) if ann.value.is_some() => { + let annotation_range = ann.annotation.range(); + let is_qualifier = self + .source + .get(usize::from(annotation_range.start())..usize::from(annotation_range.end())) + .is_some_and(|text| text.contains("Final") || text.contains("ClassVar")); + if !is_qualifier { + self.ranges.push(TextRange::new( + ann.target.range().end(), + annotation_range.end(), + )); + } + } + _ => {} + } + walk_stmt(self, stmt); + } +} + +/// Blank `range` (with a back-scan over any `->` arrow and whitespace before +/// it) in `bytes`, preserving newlines so line structure never shifts. +fn blank_range(bytes: &mut [u8], range: TextRange) { + let start = usize::from(range.start()); + let end = usize::from(range.end()).min(bytes.len()); + // Back-scan: blank a preceding `->` (and the whitespace around it) so a + // return annotation removal leaves `def f(a) :` rather than `-> :`. + let mut arrow = start; + while arrow > 0 && bytes.get(arrow - 1).is_some_and(|b| *b == b' ') { + arrow -= 1; + } + let arrow_start = arrow + .checked_sub(2) + .filter(|candidate| bytes.get(*candidate..arrow).is_some_and(|s| s == b"->")); + for byte in bytes + .get_mut(arrow_start.unwrap_or(start)..end) + .unwrap_or_default() + { + if *byte != b'\n' && *byte != b'\r' { + *byte = b' '; + } + } +} + +/// Strip every removable annotation from `source`, byte-preserving. +/// +/// Annotations whose span contains a newline are kept (blanking them would +/// merge physical lines and could leave dangling continuation syntax); the +/// guarantee is still exercised by every single-line annotation around them. +fn strip_annotations(source: &str) -> String { + let parsed = ruff_python_parser::parse_module(source).expect("corpus fixture must parse"); + let mut collector = AnnotationRanges { + ranges: Vec::new(), + source, + }; + for stmt in &parsed.syntax().body { + collector.visit_stmt(stmt); + } + let mut bytes = source.as_bytes().to_vec(); + for range in collector.ranges { + let start = usize::from(range.start()); + let end = usize::from(range.end()).min(bytes.len()); + let spans_newline = bytes + .get(start..end) + .unwrap_or_default() + .iter() + .any(|b| *b == b'\n' || *b == b'\r' || *b == b'\\'); + if !spans_newline { + blank_range(&mut bytes, range); + } + } + String::from_utf8(bytes).expect("blanking preserves UTF-8") +} + +/// Rules that judge the coherence of the annotation web ITSELF (`TypeVar` +/// scoping, variance inference over declared signatures) rather than values +/// against types. Removing a subset of annotations can leave the remaining +/// web ill-formed — e.g. a body annotation referencing a `TypeVarTuple` whose +/// only binding site was a stripped parameter — so these are outside the +/// gradual guarantee's scope (which is about inferable value positions) and +/// excluded from the differential. +const ANNOTATION_WEB_RULES: &[&str] = &["generics_scoping", "generics_variance_inference"]; + +/// Check `source` with the default config; return `(code, span-start)` pairs. +/// +/// Byte-preserving stripping keeps all spans stable, so this pair identifies +/// "the same error at the same place" across the annotated/stripped variants. +fn error_set(source: &str, path: &str) -> BTreeSet<(String, u32)> { + let parsed = basilisk_parser::parse_source(source.to_owned(), path.to_owned()) + .expect("corpus variant must parse"); + let resolved = basilisk_resolver::resolve(&parsed).expect("corpus variant must resolve"); + basilisk_checker::check(&resolved) + .into_iter() + .filter(|diagnostic| matches!(diagnostic.severity, basilisk_checker::Severity::Error)) + .filter(|diagnostic| !ANNOTATION_WEB_RULES.contains(&diagnostic.code.code)) + .map(|diagnostic| (diagnostic.code.code.to_owned(), diagnostic.span.start)) + .collect() +} + +/// The differential assertion: stripping annotations must not add errors. +fn assert_gradual_guarantee(source: &str, name: &str) { + let annotated = error_set(source, name); + let stripped_source = strip_annotations(source); + let stripped = error_set(&stripped_source, name); + let new_errors: Vec<_> = stripped.difference(&annotated).collect(); + assert!( + new_errors.is_empty(), + "[TYPEINF-TARGET-GRADUAL] violated in {name}: stripping annotations \ + introduced new errors {new_errors:?}\n--- stripped source ---\n{stripped_source}" + ); +} + +/// Curated corpus: annotated programs exercising containers, unions, +/// optionals, callables, comprehensions, and class bodies. +const CURATED_CORPUS: &[(&str, &str)] = &[ + ( + "containers", + r#" +from typing import Optional + +def totals(prices: list[float], names: list[str]) -> dict[str, float]: + table: dict[str, float] = {} + for name, price in zip(names, prices): + table[name] = price + return table + +counts: list[int] = [1, 2, 3] +maybe: Optional[int] = None +pairs: dict[str, int] = {"a": 1} +"#, + ), + ( + "callables_and_lambdas", + r" +from typing import Callable + +def apply(f: Callable[[int], int], x: int) -> int: + return f(x) + +double: Callable[[int], int] = lambda n: n * 2 +result: int = apply(double, 21) +", + ), + ( + "unions_and_narrowing", + r" +def describe(value: int | str | None) -> str: + if value is None: + return 'nothing' + if isinstance(value, int): + return str(value + 1) + return value +", + ), + ( + "comprehensions", + r" +def squares(limit: int) -> list[int]: + values: list[int] = [n * n for n in range(limit)] + return values + +index: dict[str, int] = {str(n): n for n in range(3)} +", + ), + ( + "class_bodies", + r" +class Point: + x: float + y: float + + def __init__(self, x: float, y: float) -> None: + self.x = x + self.y = y + + def magnitude_squared(self) -> float: + total: float = self.x * self.x + self.y * self.y + return total +", + ), + ( + "intentional_errors_stay_bounded", + r#" +def bad() -> int: + value: int = "not an int" + return value +"#, + ), +]; + +/// [TYPEINF-TARGET-GRADUAL]: the differential harness over the curated corpus. +#[test] +fn stripping_annotations_never_adds_errors_curated() { + for (name, source) in CURATED_CORPUS { + assert_gradual_guarantee(source, name); + } +} + +/// The splicer itself must produce parseable Python for every corpus entry — +/// a splice bug would silently shrink the differential to nothing. +#[test] +fn stripped_corpus_still_parses_and_actually_stripped() { + for (name, source) in CURATED_CORPUS { + let stripped = strip_annotations(source); + assert!( + ruff_python_parser::parse_module(&stripped).is_ok(), + "{name}: stripped variant must parse\n{stripped}" + ); + assert_eq!( + stripped.len(), + source.len(), + "{name}: stripping must be byte-preserving" + ); + if source.contains("->") { + assert!( + !stripped.contains("->"), + "{name}: single-line return annotations must be stripped\n{stripped}" + ); + } + } +} + +/// Sweep the mirrored conformance fixtures when present (they are synced by +/// `scripts/test-rust.sh` and git-ignored). Every fixture that parses both +/// annotated and stripped must satisfy the guarantee — these files are dense +/// with intentional errors, making them a strong differential corpus. +#[test] +fn stripping_annotations_never_adds_errors_conformance_fixtures() { + let fixtures_dir = + std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../conformance/tests"); + let Ok(entries) = std::fs::read_dir(&fixtures_dir) else { + // Fixtures not synced in this environment — the curated corpus above + // still enforces the invariant. + return; + }; + let mut checked = 0_u32; + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().is_none_or(|ext| ext != "py") { + continue; + } + let Ok(source) = std::fs::read_to_string(&path) else { + continue; + }; + // Skip fixtures the annotated pipeline cannot process (syntax-error + // fixtures); the guarantee is about programs that check today. + if basilisk_parser::parse_source(source.clone(), path.display().to_string()).is_err() { + continue; + } + let stripped_source = strip_annotations(&source); + if ruff_python_parser::parse_module(&stripped_source).is_err() { + continue; + } + assert_gradual_guarantee(&source, &path.display().to_string()); + checked += 1; + } + assert!( + checked > 0, + "conformance fixtures were present but none were checkable" + ); +} diff --git a/crates/basilisk-checker/tests/incremental_defs_tests.rs b/crates/basilisk-checker/tests/incremental_defs_tests.rs new file mode 100644 index 00000000..96d6774a --- /dev/null +++ b/crates/basilisk-checker/tests/incremental_defs_tests.rs @@ -0,0 +1,313 @@ +//! Tests for [TYPEINF-TARGET-INCREMENTAL] Stage 1. See +//! docs/specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-TARGET-INCREMENTAL and +//! docs/plans/CHECKER-TYPE-NARROWING-INFERENCE-PLAN.md#NARROWPLAN-CHECKLIST. +//! +//! Exercises the definition-level and expression-level Salsa queries in +//! `crates/basilisk-checker/src/incremental_defs.rs`: definition extraction, +//! per-definition inference, the fixpoint cycle sentinel, the interface +//! boundary, and — via [`basilisk_test_utils::EventDb`]'s `WillExecute` log — +//! the definition-level **early cutoff** itself. +#![expect( + clippy::expect_used, + reason = "test-only lookups over fixed fixture definitions" +)] + +use basilisk_checker::incremental_defs::{ + definition_type, definitions, expression_types, module_interface, DefKind, +}; +use basilisk_checker::types::{CallableInfo, InferredType, LiteralValue}; +use basilisk_db::SourceFile; +use basilisk_test_utils::EventDb; +use salsa::Setter as _; + +const MODULE: &str = r#"def alpha(a: int) -> str: + return "x" + +BETA = [1] + +class Gamma: + pass +"#; + +/// Top-level definitions are extracted with their kinds and names. +#[test] +fn definitions_are_extracted_with_kinds() { + let db = EventDb::default(); + let file = SourceFile::new(&db, "m.py".to_owned(), MODULE.to_owned()); + let defs = definitions(&db, file); + let summary: Vec<(&str, DefKind)> = defs + .iter() + .map(|def| (def.name(&db).as_str(), def.kind(&db))) + .collect(); + assert_eq!( + summary, + vec![ + ("alpha", DefKind::Function), + ("BETA", DefKind::Variable), + ("Gamma", DefKind::Class), + ] + ); +} + +/// A function definition's type is its declared `Callable` surface. +#[test] +fn function_definition_type_reads_annotations() { + let db = EventDb::default(); + let file = SourceFile::new(&db, "m.py".to_owned(), MODULE.to_owned()); + let defs = definitions(&db, file); + let alpha = defs.first().copied().expect("alpha exists"); + assert_eq!( + definition_type(&db, alpha), + InferredType::Callable(CallableInfo { + param_types: vec![InferredType::Int], + return_type: Box::new(InferredType::Str), + }) + ); +} + +/// A variable definition synthesizes through the bidirectional engine — +/// deferred generalization keeps `[1]` as `list[Literal[1]]` +/// ([TYPEINF-TARGET-CONSTRAINTS]). +#[test] +fn variable_definition_type_keeps_literal_precision() { + let db = EventDb::default(); + let file = SourceFile::new(&db, "m.py".to_owned(), MODULE.to_owned()); + let defs = definitions(&db, file); + let beta = defs.get(1).copied().expect("BETA exists"); + assert_eq!( + definition_type(&db, beta), + InferredType::List(Box::new(InferredType::Literal(LiteralValue::Int(1)))) + ); +} + +/// A bare-name alias resolves through its sibling definition. +#[test] +fn alias_chain_resolves_through_siblings() { + let db = EventDb::default(); + let source = "x = 1\ny = x\n"; + let file = SourceFile::new(&db, "m.py".to_owned(), source.to_owned()); + let defs = definitions(&db, file); + let y = defs.get(1).copied().expect("y exists"); + assert_eq!( + definition_type(&db, y), + InferredType::Literal(LiteralValue::Int(1)) + ); +} + +/// Mutually-referential definitions terminate via fixpoint iteration and +/// settle on the divergent/bottom sentinel (`Unknown`) — never a hang, never +/// an invented type ([TYPEINF-TARGET-INCREMENTAL] cycle handling). +#[test] +fn definition_cycles_settle_on_the_divergent_sentinel() { + let db = EventDb::default(); + let source = "a = b\nb = a\n"; + let file = SourceFile::new(&db, "m.py".to_owned(), source.to_owned()); + let defs = definitions(&db, file); + for def in defs { + assert_eq!(definition_type(&db, *def), InferredType::Unknown); + } +} + +/// Expression-level query: assignment RHS and return values get types, +/// slice-relative. +#[test] +fn expression_types_cover_assignments_and_returns() { + let db = EventDb::default(); + let file = SourceFile::new(&db, "m.py".to_owned(), MODULE.to_owned()); + let defs = definitions(&db, file); + let alpha = defs.first().copied().expect("alpha exists"); + let types = expression_types(&db, alpha); + assert_eq!(types.len(), 1, "one return expression in alpha"); + let ret = types.first().expect("return expr typed"); + assert_eq!( + ret.inferred, + InferredType::Literal(LiteralValue::Str("x".to_owned())) + ); +} + +/// The module interface is the compact `(name, type)` boundary. +#[test] +fn module_interface_lists_definition_types() { + let db = EventDb::default(); + let file = SourceFile::new(&db, "m.py".to_owned(), MODULE.to_owned()); + let interface = module_interface(&db, file); + let names: Vec<&str> = interface.0.iter().map(|(name, _)| name.as_str()).collect(); + assert_eq!(names, vec!["alpha", "BETA", "Gamma"]); +} + +/// **Definition-level early cutoff**: editing one definition's body re-runs +/// `definition_type` ONLY for that definition — the untouched sibling's memo +/// survives, proven by salsa's `WillExecute` log. +#[test] +fn editing_one_definition_recomputes_only_that_definition() { + let mut db = EventDb::default(); + let file = SourceFile::new(&db, "m.py".to_owned(), MODULE.to_owned()); + for def in definitions(&db, file) { + let _ = definition_type(&db, *def); + } + // Drain setup events. + let _ = db.executions_of("definition_type"); + + // Edit ONLY alpha's body (BETA's and Gamma's slices are unchanged). + let edited = MODULE.replace("return \"x\"", "return \"xy\""); + assert_ne!(edited, MODULE); + let _ = file.set_text(&mut db).to(edited); + for def in definitions(&db, file) { + let _ = definition_type(&db, *def); + } + assert_eq!( + db.executions_of("definition_type"), + 1, + "only the edited definition's type may recompute" + ); +} + +/// **Interface backdating**: a body-only edit leaves the module interface +/// value unchanged, so a consumer query reading it is never re-executed — +/// the cross-file early-cutoff boundary. +#[test] +fn body_only_edit_backdates_the_module_interface() { + let mut db = EventDb::default(); + let file = SourceFile::new(&db, "m.py".to_owned(), MODULE.to_owned()); + let before = module_interface(&db, file).clone(); + + let edited = MODULE.replace("return \"x\"", "return \"different body\""); + let _ = file.set_text(&mut db).to(edited); + let after = module_interface(&db, file).clone(); + assert_eq!( + before, after, + "a body-only edit must not change the interface value" + ); +} + +/// The Salsa-backed use-def map ([TYPEINF-TARGET-NARROWING]): `narrowed_uses` +/// reports flow-narrowed reads per definition, and editing one function +/// re-executes only that function's query — narrowing is incremental at +/// definition granularity. +#[test] +fn narrowed_uses_is_definition_incremental() { + use basilisk_checker::incremental_defs::narrowed_uses; + + const TWO_FUNCTIONS: &str = r"def first(x: int | None) -> int: + assert x is not None + return x + +def second(y: int | str) -> None: + if isinstance(y, int): + a = y +"; + let mut db = EventDb::default(); + let file = SourceFile::new(&db, "m.py".to_owned(), TWO_FUNCTIONS.to_owned()); + for def in definitions(&db, file) { + let _ = narrowed_uses(&db, *def); + } + let first_uses = definitions(&db, file) + .first() + .map(|def| narrowed_uses(&db, *def).clone()) + .unwrap_or_default(); + assert!( + first_uses + .iter() + .any(|use_site| use_site.name == "x" && use_site.narrowed == InferredType::Int), + "assert narrowing must be visible through the tracked query: {first_uses:?}" + ); + let _ = db.executions_of("narrowed_uses"); + + // Edit ONLY `second` — `first`'s narrowing memo must survive. + let edited = TWO_FUNCTIONS.replace("a = y", "b = y"); + let _ = file.set_text(&mut db).to(edited); + for def in definitions(&db, file) { + let _ = narrowed_uses(&db, *def); + } + assert_eq!( + db.executions_of("narrowed_uses"), + 1, + "only the edited definition's narrowing may recompute" + ); +} + +/// Same-module return inference: an unannotated function's return type is +/// synthesized from its body ([NARROWPLAN-CHECKLIST] expression inference). +#[test] +fn unannotated_return_is_synthesized_from_the_body() { + let db = EventDb::default(); + let source = r#"def no_return(x: int): + y = x + +def returns_literal(): + return "done" + +def diverging_return(): + return 1 +"#; + let file = SourceFile::new(&db, "m.py".to_owned(), source.to_owned()); + let defs = definitions(&db, file); + + let return_of = |index: usize| -> InferredType { + let def = defs.get(index).copied().expect("definition exists"); + match definition_type(&db, def) { + InferredType::Callable(info) => *info.return_type, + other => other, + } + }; + + // No return statement at all → None. + assert_eq!(return_of(0), InferredType::None_); + + // A single trailing return synthesizes its literal type precisely. + assert_eq!( + return_of(1), + InferredType::Literal(LiteralValue::Str("done".to_owned())) + ); + + // Trailing return diverges: no implicit None union. + let diverging = return_of(2); + assert!( + !InferredType::None_.is_assignable_to(&diverging), + "a body ending in return must not union None: {diverging:?}" + ); +} + +/// Same-module call-return inference: `x = f()` resolves through the +/// sibling's declared or synthesized return type, including chains. +#[test] +fn variable_from_sibling_call_infers_the_return() { + let db = EventDb::default(); + let source = r#"def make(a: int) -> str: + return "s" + +def synth_only(): + return 42 + +made = make(1) +synthesized = synth_only() +"#; + let file = SourceFile::new(&db, "m.py".to_owned(), source.to_owned()); + let defs = definitions(&db, file); + let made = defs.get(2).copied().expect("made"); + assert_eq!(definition_type(&db, made), InferredType::Str); + + let synthesized = defs.get(3).copied().expect("synthesized"); + assert_eq!( + definition_type(&db, synthesized), + InferredType::Literal(LiteralValue::Int(42)), + "the callee's SYNTHESIZED return flows into the variable" + ); +} + +/// Plain attribute loads on user-class instances answer from the class's +/// annotated attribute schema (constructor + attribute inference composed). +#[test] +fn attribute_loads_answer_from_class_schemas() { + let db = EventDb::default(); + let source = r"class Point: + x: float + y: float + +origin_x = Point().x +"; + let file = SourceFile::new(&db, "m.py".to_owned(), source.to_owned()); + let defs = definitions(&db, file); + let origin_x = defs.get(1).copied().expect("origin_x"); + assert_eq!(definition_type(&db, origin_x), InferredType::Float); +} diff --git a/crates/basilisk-checker/tests/inference_corpus_tests.rs b/crates/basilisk-checker/tests/inference_corpus_tests.rs new file mode 100644 index 00000000..99d80652 --- /dev/null +++ b/crates/basilisk-checker/tests/inference_corpus_tests.rs @@ -0,0 +1,181 @@ +//! Tests for the [NARROWPLAN-CHECKLIST] Stage 2 item "Build the curated +//! container/comprehension/lambda benchmark and the targeted higher-order +//! (`map`/`filter`/decorators/`ParamSpec`) benchmark". See +//! docs/plans/CHECKER-TYPE-NARROWING-INFERENCE-PLAN.md. +//! +//! Each case is a tiny module whose LAST definition's inferred type +//! (through the definition-level Salsa queries — the same engine everything +//! else consumes) must be EQUIVALENT to the expected annotation (mutual +//! assignability, so `list[Literal[1]]` vs `list[int]` counts as a miss — +//! precision is the whole game). +//! +//! The suite is a RATCHET, not an all-pass gate: `PRECISION_FLOOR` is the +//! self-measured number of currently-passing cases; it may only go UP. +//! Higher-order cases the engine cannot answer yet (generic `map`/`filter`, +//! decorators, `ParamSpec`) are in the corpus on purpose — they document the +//! gap the generic-constraints work closes, and the floor rises as it does. +#![expect( + clippy::expect_used, + reason = "test-only lookups over fixed corpus fixtures" +)] + +use basilisk_checker::incremental_defs::{definition_type, definitions}; +use basilisk_checker::types::InferredType; +use basilisk_db::SourceFile; +use basilisk_test_utils::EventDb; + +/// Cases that currently pass may never regress; new engine strength moves +/// this floor UP (self-measured over `CORPUS`). +const PRECISION_FLOOR: usize = 18; + +/// `(case name, module source, expected annotation for the LAST definition)`. +const CORPUS: &[(&str, &str, &str)] = &[ + // ── containers ──────────────────────────────────────────────────── + ( + "list_literal", + "x = [1, 2]\n", + "list[Literal[1] | Literal[2]]", + ), + ( + "nested_list", + "x = [[1], [2]]\n", + "list[list[Literal[1] | Literal[2]]]", + ), + ( + "dict_literal", + "x = {\"a\": 1}\n", + "dict[Literal[\"a\"], Literal[1]]", + ), + ( + "set_literal", + "x = {1, 2}\n", + "set[Literal[1] | Literal[2]]", + ), + ( + "tuple_literal", + "x = (1, \"s\")\n", + "tuple[Literal[1], Literal[\"s\"]]", + ), + ("empty_list", "x = []\n", "list[Never]"), + ( + "mixed_list", + "x = [1, \"s\"]\n", + "list[Literal[1] | Literal[\"s\"]]", + ), + // ── comprehensions ──────────────────────────────────────────────── + ( + "list_comp", + "x = [n for n in [1, 2]]\n", + "list[Literal[1] | Literal[2]]", + ), + ("set_comp", "x = {n for n in [1]}\n", "set[Literal[1]]"), + ( + "dict_comp", + "x = {k: 1 for k in [\"a\"]}\n", + "dict[Literal[\"a\"], Literal[1]]", + ), + ("str_call_comp", "x = [str(n) for n in [1]]\n", "list[str]"), + // ── calls, constructors, attributes, subscripts ─────────────────── + ("builtin_len", "x = len([1])\n", "int"), + ( + "sibling_return", + "def f() -> str:\n return \"s\"\n\nx = f()\n", + "str", + ), + ( + "synthesized_return", + "def f():\n return 42\n\nx = f()\n", + "Literal[42]", + ), + ( + "constructor_attr", + "class P:\n y: float\n\nx = P().y\n", + "float", + ), + ( + "subscript_list", + "xs = [1, 2]\nx = xs[0]\n", + "Literal[1] | Literal[2]", + ), + ("method_split", "x = \"a b\".split()\n", "list[str]"), + ( + "ternary", + "x = 1 if c else \"s\"\n", + "Literal[1] | Literal[\"s\"]", + ), + ("binop_int", "x = 1 + 2\n", "int"), + ("division", "x = 1 / 2\n", "float"), + // ── lambdas ─────────────────────────────────────────────────────── + // Unconstrained lambda: parameters and body stay unknown-gradual. + ("lambda_id", "x = lambda v: v\n", "Callable[..., Any]"), + // ── higher-order gap (documents what generics work must close) ──── + ("map_builtin", "x = list(map(str, [1]))\n", "list[str]"), + ( + "filter_builtin", + "x = list(filter(None, [1, 2]))\n", + "list[int]", + ), + ( + "decorator_identity", + "def dec(f):\n return f\n\n@dec\ndef g() -> int:\n return 1\n\nx = g()\n", + "int", + ), +]; + +/// Equivalence up to the lattice: mutual assignability — PLUS a gradual +/// honesty check. `Unknown` is bidirectionally assignable, so without this a +/// `list[Unknown]` answer would "match" `list[str]` and inflate the score; +/// the actual type may carry no more gradual holes than the expectation +/// licenses (`Any`/`Unknown` in the expected text). +fn equivalent(actual: &InferredType, expected: &InferredType) -> bool { + actual.is_assignable_to(expected) + && expected.is_assignable_to(actual) + && gradual_holes(actual) <= gradual_holes(expected) +} + +/// Count `Any`/`Unknown` leaves, recursively. +fn gradual_holes(ty: &InferredType) -> usize { + match ty { + InferredType::Any | InferredType::Unknown => 1, + InferredType::List(inner) + | InferredType::Set(inner) + | InferredType::Optional(inner) + | InferredType::TypeForm(inner) => gradual_holes(inner), + InferredType::Dict(key, value) => gradual_holes(key) + gradual_holes(value), + InferredType::Tuple(items) | InferredType::Union(items) => { + items.iter().map(gradual_holes).sum() + } + InferredType::Callable(info) => { + info.param_types.iter().map(gradual_holes).sum::() + + gradual_holes(&info.return_type) + } + _ => 0, + } +} + +/// [NARROWPLAN-CHECKLIST]: the curated inference-precision corpus ratchet. +#[test] +fn inference_precision_corpus_holds_the_floor() { + let db = EventDb::default(); + let mut passed = Vec::new(); + let mut missed = Vec::new(); + for (name, source, expected_text) in CORPUS { + let file = SourceFile::new(&db, format!("{name}.py"), (*source).to_owned()); + let defs = definitions(&db, file); + let last = defs.last().copied().expect("corpus case defines a name"); + let actual = definition_type(&db, last); + let expected = InferredType::from_annotation(expected_text); + if equivalent(&actual, &expected) { + passed.push(*name); + } else { + missed.push(format!("{name}: expected {expected:?}, got {actual:?}")); + } + } + println!("inference corpus: {} / {} pass", passed.len(), CORPUS.len()); + println!("missed: {missed:#?}"); + assert!( + passed.len() >= PRECISION_FLOOR, + "precision ratchet violated: {} passing < floor {PRECISION_FLOOR}; missed: {missed:#?}", + passed.len() + ); +} diff --git a/crates/basilisk-checker/tests/mutation_kill_tests.rs b/crates/basilisk-checker/tests/mutation_kill_tests.rs index 5636471c..8dea0e63 100644 --- a/crates/basilisk-checker/tests/mutation_kill_tests.rs +++ b/crates/basilisk-checker/tests/mutation_kill_tests.rs @@ -1666,3 +1666,94 @@ fn mutant_context_line_index_places_inline_suppression() -> Result<(), Box usize { + diags + .iter() + .filter(|d| d.code.code == "classes_override") + .count() +} + +/// `annotations_conflict`: an `@override` signature conflicts only when BOTH +/// annotation sides are present and differ. Absent annotations are implicitly +/// `Any` and can never prove incompatibility ([TYPEINF-TARGET-GRADUAL]) — +/// stripping a child annotation must not create a new error. +#[mutation_safe(rule = "classes_override", fns = "annotations_conflict")] +#[test] +fn mutant_classes_override_annotations_conflict() -> Result<(), Box> { + // Differing parameter annotations must fire. + let param_mismatch = r" +from typing import override + +class Base: + def process(self, value: int) -> int: + return value + +class Child(Base): + @override + def process(self, value: str) -> int: + return 0 +"; + assert!( + classes_override_count(&run(param_mismatch)?) >= 1, + "str-vs-int parameter override must fire classes_override" + ); + + // Differing return annotations must fire. + let return_mismatch = r" +from typing import override + +class Base: + def process(self, value: int) -> int: + return value + +class Child(Base): + @override + def process(self, value: int) -> str: + return '' +"; + assert!( + classes_override_count(&run(return_mismatch)?) >= 1, + "str-vs-int return override must fire classes_override" + ); + + // Identical signatures never fire. + let identical = r" +from typing import override + +class Base: + def process(self, value: int) -> int: + return value + +class Child(Base): + @override + def process(self, value: int) -> int: + return value +"; + assert_eq!( + classes_override_count(&run(identical)?), + 0, + "identical override signature must not fire" + ); + + // An UNANNOTATED override side is implicitly Any — never a conflict. + let unannotated_child = r" +from typing import override + +class Base: + def process(self, value: int) -> int: + return value + +class Child(Base): + @override + def process(self, value): + return value +"; + assert_eq!( + classes_override_count(&run(unannotated_child)?), + 0, + "absent child annotations are implicitly Any and must not fire" + ); + Ok(()) +} diff --git a/crates/basilisk-checker/tests/narrow_flow_tests.rs b/crates/basilisk-checker/tests/narrow_flow_tests.rs new file mode 100644 index 00000000..af2f9af7 --- /dev/null +++ b/crates/basilisk-checker/tests/narrow_flow_tests.rs @@ -0,0 +1,513 @@ +//! Tests for [TYPEINF-TARGET-NARROWING] Stage 2 flow analysis. See +//! docs/specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-TARGET-NARROWING and +//! docs/plans/CHECKER-TYPE-NARROWING-INFERENCE-PLAN.md#NARROWPLAN-CHECKLIST. +//! +//! End-to-end over the real pipeline: parse → resolve (the resolver collects +//! the guards) → [`basilisk_checker::narrow::analyse_function`]. Covers the +//! checklist's positive/complement branches, early exits, loops, closures, +//! and merge behaviour. +#![expect( + clippy::expect_used, + reason = "test-only parsing/resolution of fixed fixtures" +)] + +use std::collections::HashMap; + +use basilisk_checker::narrow::{analyse_function, FlowResult, NarrowEnv}; +use basilisk_checker::types::{InferredType, LiteralValue}; +use ruff_python_ast::Stmt; + +/// Run the flow walker over `source`'s FIRST top-level function, with +/// declared types taken from its parameter annotations. +fn analyse(source: &str) -> FlowResult { + let parsed = basilisk_parser::parse_source(source.to_owned(), "flow.py".to_owned()) + .expect("fixture parses"); + let resolved = basilisk_resolver::resolve(&parsed).expect("fixture resolves"); + let function = resolved.functions.first().expect("fixture has a function"); + let declared: HashMap = function + .parameters + .iter() + .filter_map(|param| { + let span = param.annotation_span?; + let start = usize::try_from(span.start).ok()?; + let end = usize::try_from(span.end).ok()?; + let text = source.get(start..end)?; + Some((param.name.clone(), InferredType::from_annotation(text))) + }) + .collect(); + + let reparsed = ruff_python_parser::parse_module(source).expect("fixture reparses"); + let body = reparsed + .syntax() + .body + .iter() + .find_map(|stmt| match stmt { + Stmt::FunctionDef(def) => Some(def.body.clone()), + _ => None, + }) + .expect("function body found"); + + analyse_function(&body, NarrowEnv::new(declared), &function.narrowing_guards) +} + +/// The narrowed type observed for `name` at its LAST recorded use. +fn last_use(result: &FlowResult, name: &str) -> Option { + result + .narrowed_uses + .iter() + .rfind(|use_site| use_site.name == name) + .map(|use_site| use_site.narrowed.clone()) +} + +/// Positive branch narrows, complement branch subtracts +/// ([TYPEINF-NARROWING-ISINSTANCE]). +#[test] +fn isinstance_positive_and_complement_branches() { + let result = analyse( + r" +def f(x: int | str) -> None: + if isinstance(x, int): + a = x + else: + b = x +", + ); + let uses: Vec<(&str, &InferredType)> = result + .narrowed_uses + .iter() + .map(|u| (u.name.as_str(), &u.narrowed)) + .collect(); + assert!( + uses.contains(&("x", &InferredType::Int)), + "positive branch must see int: {uses:?}" + ); + assert!( + uses.contains(&("x", &InferredType::Str)), + "complement branch must see str: {uses:?}" + ); +} + +/// Early exit: `if x is None: return` leaves the complement narrowing for +/// all subsequent statements. +#[test] +fn early_exit_persists_the_complement() { + let result = analyse( + r" +def f(x: int | None) -> int: + if x is None: + return 0 + return x +", + ); + assert_eq!( + last_use(&result, "x"), + Some(InferredType::Int), + "after the early return, x must be int: {:?}", + result.narrowed_uses + ); +} + +/// `assert x is not None` narrows the remainder of the scope +/// ([TYPEINF-NARROWING-ASSERT]). +#[test] +fn assert_narrows_the_rest_of_the_scope() { + let result = analyse( + r" +def f(x: int | None) -> int: + assert x is not None + return x +", + ); + assert_eq!(last_use(&result, "x"), Some(InferredType::Int)); +} + +/// Narrowing does NOT leak past the merge when both branches complete. +#[test] +fn merge_reverts_one_sided_narrowing() { + let result = analyse( + r" +def f(x: int | None) -> None: + if x is None: + y = 1 + z = x +", + ); + assert_eq!( + last_use(&result, "x"), + None, + "after a non-diverging if, x must revert to its declared type: {:?}", + result.narrowed_uses + ); +} + +/// Guards inside loops do not narrow ([TYPEINF-NARROWING-SCOPE]). +#[test] +fn loop_guards_do_not_leak() { + let result = analyse( + r" +def f(x: int | None) -> None: + for i in [1, 2]: + if x is None: + continue + y = x +", + ); + assert_eq!( + last_use(&result, "x"), + None, + "loop-internal narrowing must not persist: {:?}", + result.narrowed_uses + ); +} + +/// Nested functions are a narrowing boundary: enclosing narrows do not +/// flow into the closure body. +#[test] +fn closures_do_not_inherit_narrowing() { + let result = analyse( + r" +def f(x: int | None) -> None: + assert x is not None + def inner() -> None: + y = x + z = x +", + ); + // The closure's `x` read is not walked (fresh boundary), so the only + // recorded narrowed uses of x are in the outer scope. + for use_site in &result.narrowed_uses { + assert_eq!(use_site.name, "x"); + assert_eq!(use_site.narrowed, InferredType::Int); + } + assert_eq!(last_use(&result, "x"), Some(InferredType::Int)); +} + +/// Assignment narrowing: `x = 1` narrows the flow type at later uses. +#[test] +fn assignment_narrows_flow_type() { + let result = analyse( + r" +def f(x: int | str) -> None: + x = 1 + y = x +", + ); + let narrowed = last_use(&result, "x").expect("x use recorded after assignment"); + assert!( + narrowed.is_assignable_to(&InferredType::Int), + "after x = 1 the flow type must be int-compatible, got {narrowed:?}" + ); +} + +/// `match` cases narrow the subject per case ([TYPEINF-NARROWING-MATCH]). +#[test] +fn match_cases_narrow_the_subject() { + let result = analyse( + r" +def f(x: int | str) -> None: + match x: + case int(): + a = x + case str(): + b = x +", + ); + let uses: Vec<(&str, &InferredType)> = result + .narrowed_uses + .iter() + .map(|u| (u.name.as_str(), &u.narrowed)) + .collect(); + assert!( + uses.contains(&("x", &InferredType::Int)), + "int() case must narrow to int: {uses:?}" + ); + assert!( + uses.contains(&("x", &InferredType::Str)), + "str() case must narrow to str: {uses:?}" + ); +} + +/// Value patterns (`case 1:`) never fabricate `Never` narrowing. +#[test] +fn match_value_patterns_stay_conservative() { + let result = analyse( + r" +def f(x: int | str) -> None: + match x: + case 1: + a = x +", + ); + for use_site in &result.narrowed_uses { + assert_ne!( + use_site.narrowed, + InferredType::Never, + "value pattern must not narrow to Never" + ); + } +} + +/// Unreachable branch: a guard impossible for the declared type narrows the +/// variable to `Never` inside that branch — the inference-driven +/// reachability signal. +#[test] +fn impossible_guard_narrows_to_never_in_branch() { + let result = analyse( + r" +def f(x: int) -> None: + if isinstance(x, str): + y = x + z = x +", + ); + assert_eq!( + last_use(&result, "x"), + Some(InferredType::Never), + "inside `isinstance(x, str)` with x: int the branch is unreachable: {:?}", + result.narrowed_uses + ); +} + +/// `x == ` narrows to the literal; the complement removes exactly +/// that literal member ([TYPEINF-NARROWING-EQ-LITERAL]). +#[test] +fn equality_literal_narrows_both_branches() { + let result = analyse( + r" +def f(x: Literal[1] | Literal[2]) -> None: + if x == 1: + a = x + else: + b = x +", + ); + let uses: Vec<&InferredType> = result + .narrowed_uses + .iter() + .filter(|u| u.name == "x") + .map(|u| &u.narrowed) + .collect(); + assert!( + uses.contains(&&InferredType::Literal(LiteralValue::Int(1))), + "positive branch must see Literal[1]: {uses:?}" + ); + assert!( + uses.contains(&&InferredType::Literal(LiteralValue::Int(2))), + "complement must see Literal[2]: {uses:?}" + ); +} + +/// `x in (1, 2)` narrows to the union of members; `not in`/else removes them +/// ([TYPEINF-NARROWING-IN-LITERAL]). +#[test] +fn membership_literals_narrow_both_branches() { + let result = analyse( + r" +def f(x: Literal[1] | Literal[2] | Literal[3]) -> None: + if x in (1, 2): + a = x + else: + b = x +", + ); + let else_use = last_use(&result, "x").expect("else-branch use recorded"); + assert_eq!( + else_use, + InferredType::Literal(LiteralValue::Int(3)), + "the complement of `in (1, 2)` must be Literal[3]: {:?}", + result.narrowed_uses + ); +} + +/// `"key" in td` keeps union members whose `TypedDict` schema declares the +/// key; the complement drops members where it is required +/// ([TYPEINF-NARROWING-TYPEDDICT-KEY]). +#[test] +fn typeddict_key_membership_narrows_union() { + use basilisk_checker::narrow::{analyse_function_in, NarrowContext, TypedDictKeys}; + let source = r#" +def f(x: WithKey | WithoutKey) -> None: + if "k" in x: + a = x + else: + b = x +"#; + let parsed = basilisk_parser::parse_source(source.to_owned(), "td.py".to_owned()) + .expect("fixture parses"); + let resolved = basilisk_resolver::resolve(&parsed).expect("fixture resolves"); + let function = resolved.functions.first().expect("function"); + let declared: HashMap = [( + "x".to_owned(), + InferredType::Union(vec![ + InferredType::Named("withkey".to_owned()), + InferredType::Named("withoutkey".to_owned()), + ]), + )] + .into_iter() + .collect(); + let mut ctx = NarrowContext::default(); + let _ = ctx.typeddict_keys.insert( + "withkey".to_owned(), + TypedDictKeys { + all: ["k".to_owned()].into_iter().collect(), + required: ["k".to_owned()].into_iter().collect(), + }, + ); + let _ = ctx + .typeddict_keys + .insert("withoutkey".to_owned(), TypedDictKeys::default()); + + let reparsed = ruff_python_parser::parse_module(source).expect("reparses"); + let body = reparsed + .syntax() + .body + .iter() + .find_map(|stmt| match stmt { + Stmt::FunctionDef(def) => Some(def.body.clone()), + _ => None, + }) + .expect("body"); + let result = analyse_function_in( + &body, + NarrowEnv::new(declared), + &function.narrowing_guards, + &ctx, + ); + let uses: Vec<&InferredType> = result + .narrowed_uses + .iter() + .filter(|u| u.name == "x") + .map(|u| &u.narrowed) + .collect(); + assert!( + uses.contains(&&InferredType::Named("withkey".to_owned())), + "positive branch keeps the schema with the key: {uses:?}" + ); + assert!( + uses.contains(&&InferredType::Named("withoutkey".to_owned())), + "complement keeps the schema lacking the required key: {uses:?}" + ); +} + +/// Implied else: when every match case diverges, code after the match sees +/// the subject minus all covered pattern types. +#[test] +fn exhaustive_diverging_match_narrows_after() { + let result = analyse( + r" +def f(x: int | str | None) -> None: + match x: + case int(): + return + case str(): + return + y = x +", + ); + assert_eq!( + last_use(&result, "x"), + Some(InferredType::None_), + "after int/str cases both return, only None remains: {:?}", + result.narrowed_uses + ); +} + +/// Inference-driven reachability: a branch whose guard narrows the variable +/// to `Never` is reported unreachable — derived from the type lattice, not +/// a pattern-matched idiom. +#[test] +fn impossible_branch_is_reported_unreachable() { + let result = analyse( + r" +def f(x: int) -> None: + if isinstance(x, str): + y = x +", + ); + assert_eq!( + result.unreachable_ranges.len(), + 1, + "the isinstance(x, str) body must be unreachable for x: int: {:?}", + result.unreachable_ranges + ); +} + +/// `type(x) is C` implies `isinstance(x, C)` positively; the negative branch +/// excludes `C` only when `C` is `@final` ([TYPEINF-NARROWING-TYPEOF]). +#[test] +fn type_of_is_narrows_with_final_awareness() { + use basilisk_checker::narrow::{analyse_function_in, NarrowContext}; + let source = r" +def f(x: A | B) -> None: + if type(x) is A: + p = x + else: + q = x +"; + let parsed = basilisk_parser::parse_source(source.to_owned(), "t.py".to_owned()) + .expect("fixture parses"); + let resolved = basilisk_resolver::resolve(&parsed).expect("fixture resolves"); + let function = resolved.functions.first().expect("function"); + let declared: HashMap = [( + "x".to_owned(), + InferredType::Union(vec![ + InferredType::Named("a".to_owned()), + InferredType::Named("b".to_owned()), + ]), + )] + .into_iter() + .collect(); + let reparsed = ruff_python_parser::parse_module(source).expect("reparses"); + let body = reparsed + .syntax() + .body + .iter() + .find_map(|stmt| match stmt { + Stmt::FunctionDef(def) => Some(def.body.clone()), + _ => None, + }) + .expect("body"); + + // Without @final knowledge, the negative branch stays unchanged. + let plain = analyse_function_in( + &body, + NarrowEnv::new(declared.clone()), + &function.narrowing_guards, + &NarrowContext::default(), + ); + let plain_uses: Vec<&InferredType> = plain + .narrowed_uses + .iter() + .filter(|u| u.name == "x") + .map(|u| &u.narrowed) + .collect(); + assert!( + plain_uses.contains(&&InferredType::Named("a".to_owned())), + "positive branch narrows to A: {plain_uses:?}" + ); + assert_eq!( + plain_uses.len(), + 1, + "non-final A must not be excluded in the negative branch: {plain_uses:?}" + ); + + // With A known @final, the negative branch excludes it. + let mut ctx = NarrowContext::default(); + let _ = ctx.final_classes.insert("a".to_owned()); + let with_final = analyse_function_in( + &body, + NarrowEnv::new(declared), + &function.narrowing_guards, + &ctx, + ); + let final_uses: Vec<&InferredType> = with_final + .narrowed_uses + .iter() + .filter(|u| u.name == "x") + .map(|u| &u.narrowed) + .collect(); + assert!( + final_uses.contains(&&InferredType::Named("b".to_owned())), + "with @final A, the complement must be B: {final_uses:?}" + ); +} diff --git a/crates/basilisk-cli/src/adopt.rs b/crates/basilisk-cli/src/adopt.rs index ccd6f7c2..a7cbcdfa 100644 --- a/crates/basilisk-cli/src/adopt.rs +++ b/crates/basilisk-cli/src/adopt.rs @@ -294,7 +294,10 @@ mod tests { /// Python code with a missing parameter annotation (triggers BSK-0001) /// and a missing return type annotation (triggers BSK-0002). - const BAD_PYTHON: &str = "def foo(x):\n pass\n"; + // `x` has no default to infer from (BSK-0001) and `return x` is not + // inferable (BSK-0002) — a `pass` body would infer `-> None` and only + // fire BSK-0001 ([TYPEINF-FUNC-RETURN]). + const BAD_PYTHON: &str = "def foo(x):\n return x\n"; /// Fully typed Python code that should produce zero errors. const CLEAN_PYTHON: &str = "def greet(name: str) -> str:\n return name\n"; @@ -308,7 +311,11 @@ mod tests { /// and those analyze-scope rules are off by default — so the test project /// turns them on exactly as a real adopter would ([CHKARCH-COMMANDS]). fn temp_dir(name: &str) -> PathBuf { - let dir = std::env::temp_dir().join(format!("bsk_adopt_test_{name}")); + // Per-process dir name (same pattern as `stage_project` in + // cli_binary_tests): a stray watcher or leftover harness process from + // a previous run must never touch this run's fixture files. + let dir = + std::env::temp_dir().join(format!("bsk_adopt_test_{name}.{}", std::process::id())); let _ = fs::remove_dir_all(&dir); fs::create_dir_all(&dir).unwrap(); fs::write( @@ -373,7 +380,9 @@ mod tests { /// afterwards. #[test] fn run_adopt_records_pep_debt_as_warning_entry() { - let dir = std::env::temp_dir().join("bsk_adopt_test_pep_debt"); + // Per-process dir name — see `temp_dir` for the rationale. + let dir = + std::env::temp_dir().join(format!("bsk_adopt_test_pep_debt.{}", std::process::id())); let _ = fs::remove_dir_all(&dir); fs::create_dir_all(&dir).unwrap(); fs::write( diff --git a/crates/basilisk-cli/src/fix.rs b/crates/basilisk-cli/src/fix.rs index f4365b52..75ce8d83 100644 --- a/crates/basilisk-cli/src/fix.rs +++ b/crates/basilisk-cli/src/fix.rs @@ -203,7 +203,10 @@ mod tests { /// that config from disk exactly as it would in production. No modes; this /// is configuration. See [CHKARCH-CONFIGURATION-ONLY]. fn write_temp(name: &str, source: &str) -> (std::path::PathBuf, String) { - let dir = std::env::temp_dir().join(format!("{name}.proj")); + // Per-process dir name (same pattern as `stage_project` in + // cli_binary_tests): a stray watcher or leftover harness process from + // a previous run must never touch this run's fixture files. + let dir = std::env::temp_dir().join(format!("{name}.{}.proj", std::process::id())); std::fs::create_dir_all(&dir).expect("create temp project dir"); std::fs::write( dir.join("pyproject.toml"), @@ -401,10 +404,19 @@ mod tests { #[test] fn run_fix_applies_e0002_missing_return_annotation() { - let (py, path) = write_temp("basilisk_test_fix_e0002.py", "def foo(x: int):\n pass\n"); + // The returned method call is not inferable, so BSK-0002 fires and the + // fix inserts the honest `-> Any` placeholder ([TYPEINF-FUNC-RETURN]). + // A `pass` body would infer `-> None` and leave nothing to fix. + let (py, path) = write_temp( + "basilisk_test_fix_e0002.py", + "def foo(x: int):\n return x.bit_length()\n", + ); let (code, fixed) = fix_and_read(&path, &py, false, &["BSK-0002".to_owned()]); assert_eq!(code, 0, "BSK-0002 fix must return 0"); - assert_eq!(fixed, "def foo(x: int) -> None:\n pass\n"); + assert_eq!( + fixed, + "def foo(x: int) -> Any:\n return x.bit_length()\n" + ); } #[test] @@ -417,9 +429,11 @@ mod tests { #[test] fn run_fix_applies_multiple_rules_in_one_file() { + // `x` has no default to infer from (BSK-0001) and `return x` is not + // inferable (BSK-0002); `y: int = 42` is redundant (BSK-0050). let (py, path) = write_temp( "basilisk_test_fix_multi_rules.py", - "def foo(x):\n pass\n\ny: int = 42\n", + "def foo(x):\n return x\n\ny: int = 42\n", ); let (code, fixed) = fix_and_read(&path, &py, false, &[]); assert_eq!(code, 0); @@ -428,7 +442,7 @@ mod tests { "BSK-0001 not applied, got: {fixed}" ); assert!( - fixed.contains("-> None"), + fixed.contains("-> Any"), "BSK-0002 not applied, got: {fixed}" ); assert!( @@ -439,7 +453,11 @@ mod tests { #[test] fn run_fix_directory_traversal() { - let dir = std::env::temp_dir().join("basilisk_test_fix_dir_traversal"); + // Per-process dir name — see `write_temp` for the rationale. + let dir = std::env::temp_dir().join(format!( + "basilisk_test_fix_dir_traversal.{}", + std::process::id() + )); let _ = std::fs::create_dir_all(&dir); std::fs::write( dir.join("pyproject.toml"), diff --git a/crates/basilisk-cli/src/pipeline/tests.rs b/crates/basilisk-cli/src/pipeline/tests.rs index 041aa516..91ca515f 100644 --- a/crates/basilisk-cli/src/pipeline/tests.rs +++ b/crates/basilisk-cli/src/pipeline/tests.rs @@ -457,7 +457,10 @@ fn pep_grade_config_is_valid() -> Result<(), Box> { /// Source that violates the annotation house rules (BSK-0001 on the /// parameter, BSK-0002 on the return) once those opt-in rules are enabled. -const UNANNOTATED_FN: &[u8] = b"def foo(x):\n pass\n"; +// `x` has no default to infer from (BSK-0001) and `return x` is not inferable +// (BSK-0002) — a `pass` body would infer `-> None` and only fire BSK-0001 +// ([TYPEINF-FUNC-RETURN]). +const UNANNOTATED_FN: &[u8] = b"def foo(x):\n return x\n"; /// A `[tool.basilisk.rules]` table enabling the opt-in annotation rules. const ANNOTATION_RULES_TOML: &[u8] = diff --git a/crates/basilisk-cli/tests/e2e_rules_a.rs b/crates/basilisk-cli/tests/e2e_rules_a.rs index 0fa7343c..7488ab55 100644 --- a/crates/basilisk-cli/tests/e2e_rules_a.rs +++ b/crates/basilisk-cli/tests/e2e_rules_a.rs @@ -190,19 +190,18 @@ fn and_e0002_class_methods() -> Result<(), Box> { // dunder methods without return annotations // --------------------------------------------------------------------------- +/// [TYPEINF-FUNC-RETURN]: BSK-0002 fires only where the return type is not +/// inferable. `__init__` (no return → `None`), `__repr__` (f-string → `str`), +/// and `__len__` (`return 2` → `int`) are all inferable and must stay silent; +/// only `__add__` (returns a constructor call) still needs an annotation. #[test] -fn dunder_methods_all_missing_return() -> Result<(), Box> { +fn dunder_methods_only_uninferable_return_fires() -> Result<(), Box> { let diags = run("errors/e0002_dunder_methods.py")?; let src = std::fs::read_to_string(fixture("errors/e0002_dunder_methods.py"))?; assert_diagnostics( &src, &diags, - &[ - Expected::error("BSK-0002", "`__init__`", 2, 9), - Expected::error("BSK-0002", "`__repr__`", 6, 9), - Expected::error("BSK-0002", "`__add__`", 9, 9), - Expected::error("BSK-0002", "`__len__`", 12, 9), - ], + &[Expected::error("BSK-0002", "`__add__`", 9, 9)], ); Ok(()) } @@ -294,7 +293,8 @@ fn and_e0002_four_completely_untyped_functions() -> Result<(), Box None: pass def send(self, payload: str): - pass + return object() diff --git a/crates/basilisk-cli/tests/fixtures/errors/e0002_multiple_funcs.py b/crates/basilisk-cli/tests/fixtures/errors/e0002_multiple_funcs.py index 195f8fa6..160142f8 100644 --- a/crates/basilisk-cli/tests/fixtures/errors/e0002_multiple_funcs.py +++ b/crates/basilisk-cli/tests/fixtures/errors/e0002_multiple_funcs.py @@ -1,5 +1,5 @@ def fetch(url: str): - pass + return url.encode() def compute(x: int, y: int): @@ -7,4 +7,4 @@ def compute(x: int, y: int): def noop(): - pass + return print() diff --git a/crates/basilisk-cli/tests/fixtures/errors/e0002_no_params.py b/crates/basilisk-cli/tests/fixtures/errors/e0002_no_params.py index 4962ac0a..432e3baf 100644 --- a/crates/basilisk-cli/tests/fixtures/errors/e0002_no_params.py +++ b/crates/basilisk-cli/tests/fixtures/errors/e0002_no_params.py @@ -1,10 +1,10 @@ def get_version(): - return "1.0.0" + return "1.0.0".strip() def get_timestamp(): - return 0 + return abs(0) def noop(): - pass + return print() diff --git a/crates/basilisk-cli/tests/fixtures/errors/e0002_single_func.py b/crates/basilisk-cli/tests/fixtures/errors/e0002_single_func.py index 2b113fcc..6e3b8dcf 100644 --- a/crates/basilisk-cli/tests/fixtures/errors/e0002_single_func.py +++ b/crates/basilisk-cli/tests/fixtures/errors/e0002_single_func.py @@ -1,2 +1,2 @@ def fetch(url: str): - pass + return url.encode() diff --git a/crates/basilisk-cli/tests/fixtures/missing_both.py b/crates/basilisk-cli/tests/fixtures/missing_both.py index 3dab51b2..f0bb37f8 100644 --- a/crates/basilisk-cli/tests/fixtures/missing_both.py +++ b/crates/basilisk-cli/tests/fixtures/missing_both.py @@ -3,4 +3,4 @@ def broken(x, y): def also_broken(name): - pass + return name diff --git a/crates/basilisk-cli/tests/fixtures/missing_return_annotation.py b/crates/basilisk-cli/tests/fixtures/missing_return_annotation.py index cdffe501..5cae30fe 100644 --- a/crates/basilisk-cli/tests/fixtures/missing_return_annotation.py +++ b/crates/basilisk-cli/tests/fixtures/missing_return_annotation.py @@ -1,5 +1,5 @@ def fetch(url: str): - pass + return url.encode() def compute(x: int, y: int): diff --git a/crates/basilisk-common/src/lib.rs b/crates/basilisk-common/src/lib.rs index e44acd4e..472dcc30 100644 --- a/crates/basilisk-common/src/lib.rs +++ b/crates/basilisk-common/src/lib.rs @@ -175,6 +175,16 @@ pub mod commands { /// dependency-free common crate lets every editor use the exact server contract /// without copying method names. pub mod configuration_editor { + /// Client-registered command that opens the configuration editor UI. + /// + /// Unlike [`crate::commands::ALL`] this is NOT a server-executed command: + /// the editor host (VS Code) registers it, capability-gated on the + /// server's `configurationEditor` capability. The server references it in + /// hover markdown command links (`command:` URIs) so a diagnostic can + /// deep-link into the editor ([CONFIGEDITOR-VSIX-EXPERIENCE]). An optional + /// `{ "rule": "BSK-XXXX" }` argument focuses that rule on open. + pub const OPEN_EDITOR_COMMAND: &str = "basilisk.openConfigurationEditor"; + /// Read the resolved configuration, catalog, and workspace impact. pub const SNAPSHOT: &str = "basilisk/configurationSnapshot"; /// Validate and analyse a configuration mutation without writing it. diff --git a/crates/basilisk-compiler/tests/e2e/arithmetic.py b/crates/basilisk-compiler/tests/e2e/arithmetic.py index 71b9b488..89573d2f 100644 --- a/crates/basilisk-compiler/tests/e2e/arithmetic.py +++ b/crates/basilisk-compiler/tests/e2e/arithmetic.py @@ -2,7 +2,7 @@ def add(a: int, b: int) -> int: return a + b -def multiply(x: int, y: int) -> int: +def multiply(x, y) -> int: return x * y diff --git a/crates/basilisk-config/src/parse.rs b/crates/basilisk-config/src/parse.rs index 223c25b8..44b03d5f 100644 --- a/crates/basilisk-config/src/parse.rs +++ b/crates/basilisk-config/src/parse.rs @@ -88,6 +88,16 @@ pub struct BasiliskConfig { /// Target platform for platform-aware rules ([CHKARCH-VERSION-TARGET]). pub python_platform: Option, + + /// Whether attribute narrowing (`x.attr` guards) survives intervening + /// calls — the explicit soundness tradeoff of + /// [TYPEINF-NARROWING-ATTR-CALLS]. `None` means the default `true`: the + /// USABLE behavior (a call *could* invalidate the attribute, but + /// treating every call as an invalidation makes attribute narrowing + /// useless in practice — Pyrefly's documented lesson). Projects that + /// prefer the sound-but-strict behavior set + /// `narrow-attributes-across-calls = false` in `[tool.basilisk]`. + pub narrow_attributes_across_calls: Option, } impl Default for BasiliskConfig { @@ -106,6 +116,7 @@ impl Default for BasiliskConfig { auto_stub_path: PathBuf::from(".basilisk/stubs"), python_version: None, python_platform: None, + narrow_attributes_across_calls: None, } } } @@ -192,6 +203,9 @@ impl BasiliskConfig { self.typeshed_path = child.typeshed_path.or(self.typeshed_path); self.python_version = child.python_version.or(self.python_version); self.python_platform = child.python_platform.or(self.python_platform); + self.narrow_attributes_across_calls = child + .narrow_attributes_across_calls + .or(self.narrow_attributes_across_calls); self } } @@ -273,6 +287,14 @@ pub(crate) fn parse_pyproject_content(content: &str) -> Option { cfg.python_platform = Some(val.to_owned()); } + // [TYPEINF-NARROWING-ATTR-CALLS]: the attribute-narrowing soundness knob. + if let Some(val) = basilisk + .get("narrow-attributes-across-calls") + .and_then(toml::Value::as_bool) + { + cfg.narrow_attributes_across_calls = Some(val); + } + Some(cfg) } diff --git a/crates/basilisk-lsp/src/code_actions/fixes.rs b/crates/basilisk-lsp/src/code_actions/fixes.rs index 73f7377a..a9fe64ef 100644 --- a/crates/basilisk-lsp/src/code_actions/fixes.rs +++ b/crates/basilisk-lsp/src/code_actions/fixes.rs @@ -27,14 +27,19 @@ pub(super) fn fix_missing_param_annotation(uri: &Url, diag: &Diagnostic) -> Code ) } -/// Insert ` -> None` after the closing `)` of the parameter list. +/// Insert ` -> Any` after the closing `)` of the parameter list. +/// +/// BSK-0002 only fires when the return type is NOT inferable (a valued, +/// uninferable return or a generator — [TYPEINF-FUNC-RETURN]), so `Any` is the +/// only honest placeholder; a hardcoded `-> None` would be false on every +/// firing function. Mirrors the BSK-0001 fix (`: Any`). pub(super) fn fix_missing_return_annotation(uri: &Url, diag: &Diagnostic) -> CodeAction { single_insert( uri, diag, diag.range.end, - " -> None", - "Add `-> None` return type (basilisk)", + " -> Any", + "Add `-> Any` return type (basilisk)", ) } diff --git a/crates/basilisk-lsp/src/code_actions/mass_fix.rs b/crates/basilisk-lsp/src/code_actions/mass_fix.rs index 71ae09e4..80970254 100644 --- a/crates/basilisk-lsp/src/code_actions/mass_fix.rs +++ b/crates/basilisk-lsp/src/code_actions/mass_fix.rs @@ -17,7 +17,7 @@ use super::fixes; /// All diagnostic codes that have an autofix implementation. pub const ALL_FIXABLE_RULES: &[&str] = &[ "BSK-0001", // Missing parameter type → `: Any` - "BSK-0002", // Missing return type → `-> None` + "BSK-0002", // Missing return type → `-> Any` "BSK-0003", // Missing variable type → `: ` "BSK-0005", // Missing class attribute type → `: Any` "BSK-0050", // Redundant annotation → remove diff --git a/crates/basilisk-lsp/src/hover/mod.rs b/crates/basilisk-lsp/src/hover/mod.rs index 23852490..a03d97db 100644 --- a/crates/basilisk-lsp/src/hover/mod.rs +++ b/crates/basilisk-lsp/src/hover/mod.rs @@ -114,6 +114,9 @@ pub fn hover_at( if let Some(ref help) = d.help { let _ = write!(diag_md, "\n\n_{help}_"); } + if let Some(link) = configure_severity_link(d.code.code) { + let _ = write!(diag_md, "\n\n{link}"); + } sections.push(diag_md); } } @@ -243,6 +246,27 @@ fn format_import_hover(import_info: &ImportInfo) -> String { parts.join("\n\n") } +/// A "Configure Severity" command link for a non-PEP diagnostic `code`. +/// +/// Opt-in Basilisk house rules can be graded or disabled per project, so +/// their hover deep-links into the configuration editor focused on the rule +/// ([CONFIGEDITOR-VSIX-EXPERIENCE]). PEP rules are graded by the typing spec +/// and never disabled ([CHKARCH-CONFIG-MODEL]) — they get no link. +/// +/// The link is a `command:` URI whose argument list `[{"rule":""}]` is +/// percent-encoded per the VS Code command-URI contract. Rule codes are +/// static ASCII identifiers, so only the fixed JSON punctuation needs +/// encoding. +fn configure_severity_link(code: &str) -> Option { + if basilisk_checker::is_pep_rule(code) { + return None; + } + let command = basilisk_common::configuration_editor::OPEN_EDITOR_COMMAND; + Some(format!( + "[Configure Severity](command:{command}?%5B%7B%22rule%22%3A%22{code}%22%7D%5D)" + )) +} + mod members; use members::{builtin_member_hover, external_member_hover, find_class_init}; @@ -254,3 +278,11 @@ use members::{builtin_member_hover, external_member_hover, find_class_init}; reason = "test-only code: expect/panic acceptable in unit tests" )] mod tests; + +#[cfg(test)] +#[expect( + clippy::expect_used, + clippy::panic, + reason = "test-only code: expect/panic acceptable in unit tests" +)] +mod tests_diagnostics; diff --git a/crates/basilisk-lsp/src/hover/tests.rs b/crates/basilisk-lsp/src/hover/tests.rs index fd1f6d57..198c0107 100644 --- a/crates/basilisk-lsp/src/hover/tests.rs +++ b/crates/basilisk-lsp/src/hover/tests.rs @@ -1,7 +1,7 @@ use super::*; /// Parse and resolve `source` as `test.py` for hover tests. -fn parse_and_resolve(source: &str) -> ResolvedModule { +pub(super) fn parse_and_resolve(source: &str) -> ResolvedModule { let parsed = basilisk_parser::parse_source(source.to_owned(), "test.py".to_owned()) .expect("test source should parse"); basilisk_resolver::resolve(&parsed).expect("resolution should not fail") diff --git a/crates/basilisk-lsp/src/hover/tests_diagnostics.rs b/crates/basilisk-lsp/src/hover/tests_diagnostics.rs new file mode 100644 index 00000000..b1e8355e --- /dev/null +++ b/crates/basilisk-lsp/src/hover/tests_diagnostics.rs @@ -0,0 +1,90 @@ +//! Hover tests for the diagnostic sections ([LSPARCH-FEATURES-HOVER]), +//! including the Configure Severity link for non-PEP rules +//! ([CONFIGEDITOR-VSIX-EXPERIENCE]). + +use super::tests::parse_and_resolve; +use super::*; +use basilisk_checker::{ErrorCode, Severity}; +use basilisk_resolver::Span; + +/// Build a checker diagnostic with `code` covering the first occurrence of +/// `needle` in `source`. +fn diagnostic_over(source: &str, needle: &str, code: ErrorCode) -> basilisk_checker::Diagnostic { + let start = source.find(needle).expect("needle present in source"); + let end = start + needle.len(); + basilisk_checker::Diagnostic { + code, + severity: Severity::Error, + message: format!("Missing parameter type annotation for `{needle}`"), + span: Span::new( + u32::try_from(start).expect("test offsets fit in u32"), + u32::try_from(end).expect("test offsets fit in u32"), + ), + path: "test.py".to_owned(), + help: Some(std::borrow::Cow::Borrowed( + "Add a type annotation: `raw: `", + )), + note: None, + provenance: None, + } +} + +/// Hover markdown for `source` with `diag` at the diagnostic's start offset. +fn hover_markdown(source: &str, diag: basilisk_checker::Diagnostic) -> String { + let resolved = parse_and_resolve(source); + let offset = diag.span.start_usize(); + let hover = hover_at(&resolved, source, offset, &[diag]) + .expect("hover should be Some over a diagnostic"); + let HoverContents::Markup(markup) = hover.contents else { + panic!("expected Markup hover contents"); + }; + markup.value +} + +/// A non-PEP diagnostic (opt-in Basilisk house rule, e.g. `BSK-0001`) must +/// offer a "Configure Severity" command link that opens the configuration +/// editor focused on the rule, so users can grade or disable the rule from +/// the hover ([CONFIGEDITOR-VSIX-EXPERIENCE]). +#[test] +fn test_hover_on_non_pep_diagnostic_offers_configure_severity_link() { + let source = "def normalize_reading(raw) -> None:\n return None\n"; + let diag = diagnostic_over( + source, + "raw", + ErrorCode { + code: "BSK-0001", + docs_url: "https://www.basilisk-python.dev/errors/BSK-0001", + }, + ); + + let markdown = hover_markdown(source, diag); + assert!( + markdown.contains( + "[Configure Severity](command:basilisk.openConfigurationEditor?\ + %5B%7B%22rule%22%3A%22BSK-0001%22%7D%5D)" + ), + "non-PEP diagnostic hover must link to the configuration editor \ + focused on the rule: {markdown}" + ); +} + +/// A PEP rule is graded by the typing spec and can never be disabled +/// ([CHKARCH-CONFIG-MODEL]) — its hover must NOT offer the link. +#[test] +fn test_hover_on_pep_diagnostic_has_no_configure_severity_link() { + let source = "def normalize_reading(raw) -> None:\n return None\n"; + let diag = diagnostic_over( + source, + "raw", + ErrorCode { + code: "returns_compatibility", + docs_url: "https://www.basilisk-python.dev/errors/returns_compatibility", + }, + ); + + let markdown = hover_markdown(source, diag); + assert!( + !markdown.contains("Configure Severity"), + "PEP diagnostic hover must not offer a Configure Severity link: {markdown}" + ); +} diff --git a/crates/basilisk-lsp/src/server/activity_panel/module_tree.rs b/crates/basilisk-lsp/src/server/activity_panel/module_tree.rs index 9666ea3d..ad718239 100644 --- a/crates/basilisk-lsp/src/server/activity_panel/module_tree.rs +++ b/crates/basilisk-lsp/src/server/activity_panel/module_tree.rs @@ -174,9 +174,14 @@ fn module_kind(path: &Path) -> &'static str { /// Fold the per-file grading rollup into a module node — enabled path only /// ([ANALYSIS-ENABLED]): while disabled these fields are absent by construction. +/// The raw symbol counts ride along so clients can roll folder/package coverage +/// up symbol-weighted — matching the workspace header — instead of averaging +/// pre-divided percentages ([EXTACT-MODULES-TREE-STRUCTURE]). fn attach_grading(node: &mut serde_json::Value, health: &super::type_health::FileHealth) { if let Some(obj) = node.as_object_mut() { let _ = obj.insert("coveragePercent".into(), health.coverage_percent.into()); + let _ = obj.insert("totalSymbols".into(), health.total_symbols.into()); + let _ = obj.insert("annotatedSymbols".into(), health.annotated_symbols.into()); let _ = obj.insert("errors".into(), health.errors.into()); let _ = obj.insert("warnings".into(), health.warnings.into()); } @@ -507,6 +512,48 @@ mod tests { ); } + // Server side of [EXTACT-MODULES-TREE-STRUCTURE] coverage rollup: each + // module node must carry its symbol counts (`totalSymbols` / + // `annotatedSymbols`) so the client can roll folder/package coverage up + // symbol-weighted — matching the workspace header — instead of having only + // the pre-divided per-file percentage with no weights. + #[test] + fn test_module_nodes_carry_symbol_counts_for_folder_rollup() { + let root = PathBuf::from("/workspace"); + let idx = make_index_with_roots(vec![root.clone()]); + // Half-annotated file: 1 of 2 symbols annotated. + let uri = make_uri("/workspace/pkg/partial.py"); + let _ = idx.set_open(&uri, "a: int = 1\nb = 2\n", 1); + + let tree = build_module_tree(&idx, "", true, true); + assert_eq!(tree.modules.len(), 1); + let module = &tree.modules[0]; + assert_eq!( + module + .get("totalSymbols") + .and_then(serde_json::Value::as_u64), + Some(2), + "module node must carry totalSymbols as the client's rollup weight, got {module}" + ); + assert_eq!( + module + .get("annotatedSymbols") + .and_then(serde_json::Value::as_u64), + Some(1), + "module node must carry annotatedSymbols for the client's rollup, got {module}" + ); + + // Disabled toggle omits the counts like every other grading field + // ([ANALYSIS-ENABLED], #119). + let disabled = build_module_tree(&idx, "", false, true); + for field in ["totalSymbols", "annotatedSymbols"] { + assert!( + disabled.modules[0].get(field).is_none(), + "disabled toggle must omit '{field}' from module nodes" + ); + } + } + // Tests [EXTACT-MODULES-DIAGNOSTICS] (GitHub #235): every module node must // carry its diagnostics as a navigable list so the `errors`/`warnings` // tallies rendered on the row are reachable, not dead. The wire shape is the diff --git a/crates/basilisk-lsp/tests/lsp/lsp_e2e_basics.rs b/crates/basilisk-lsp/tests/lsp/lsp_e2e_basics.rs index 4061def0..911cc3f3 100644 --- a/crates/basilisk-lsp/tests/lsp/lsp_e2e_basics.rs +++ b/crates/basilisk-lsp/tests/lsp/lsp_e2e_basics.rs @@ -30,7 +30,10 @@ fn test_lsp_did_open_with_type_errors() -> TestResult<()> { let mut fixture = LspTestFixture::new()?; let _ = fixture.initialize()?; - let python_code = "def greet(name):\n return f\"Hello, {name}!\""; + // `name` has no default to infer from (BSK-0001) and the returned method + // call is not inferable (BSK-0002) — an f-string return would infer + // `-> str` and silence BSK-0002 ([TYPEINF-FUNC-RETURN]). + let python_code = "def greet(name):\n return name.upper()"; fixture.did_open("file:///test.py", python_code)?; let diag = fixture diff --git a/crates/basilisk-lsp/tests/lsp/lsp_e2e_code_actions.rs b/crates/basilisk-lsp/tests/lsp/lsp_e2e_code_actions.rs index c935a9a8..12613da7 100644 --- a/crates/basilisk-lsp/tests/lsp/lsp_e2e_code_actions.rs +++ b/crates/basilisk-lsp/tests/lsp/lsp_e2e_code_actions.rs @@ -383,7 +383,10 @@ fn test_lsp_code_action_missing_return_annotation() -> TestResult<()> { let mut fixture = LspTestFixture::new()?; let _ = fixture.initialize()?; - let code = "def greet(name: str):\n return f\"Hello, {name}!\""; + // The returned method call is not inferable, so BSK-0002 fires — an + // f-string return would infer `-> str` and stay silent + // ([TYPEINF-FUNC-RETURN]). + let code = "def greet(name: str):\n return name.upper()"; fixture.did_open("file:///retact.py", code)?; let diag_msg = fixture @@ -415,8 +418,8 @@ fn test_lsp_code_action_missing_return_annotation() -> TestResult<()> { .ok_or("no code action response")?; assert!( - resp.contains("-> None"), - "code action should insert '-> None': {resp}" + resp.contains("-> Any"), + "code action should insert '-> Any': {resp}" ); assert!( resp.contains("quickfix"), @@ -431,7 +434,7 @@ fn test_lsp_code_action_missing_return_annotation() -> TestResult<()> { .ok_or("expected result array")?; let return_fix = actions .iter() - .find(|a| a["title"].as_str().is_some_and(|t| t.contains("-> None"))) + .find(|a| a["title"].as_str().is_some_and(|t| t.contains("-> Any"))) .ok_or("no return type fix action")?; let edit = &return_fix["edit"]["changes"]["file:///retact.py"][0]; let start_line = edit["range"]["start"]["line"].as_u64().unwrap_or(u64::MAX); @@ -445,8 +448,8 @@ fn test_lsp_code_action_missing_return_annotation() -> TestResult<()> { "edit must insert at column 20 (after closing paren), not at function name" ); assert_eq!( - new_text, " -> None", - "inserted text must be ' -> None' (space before arrow)" + new_text, " -> Any", + "inserted text must be ' -> Any' (space before arrow)" ); Ok(()) } diff --git a/crates/basilisk-lsp/tests/lsp/ws_test_basics.rs b/crates/basilisk-lsp/tests/lsp/ws_test_basics.rs index b3a4b8e9..2cff037b 100644 --- a/crates/basilisk-lsp/tests/lsp/ws_test_basics.rs +++ b/crates/basilisk-lsp/tests/lsp/ws_test_basics.rs @@ -31,9 +31,12 @@ async fn test_ws_initialize() -> TestResult<()> { #[tokio::test] async fn test_ws_did_open_with_type_errors() -> TestResult<()> { + // `name` has no default to infer from (BSK-0001) and the returned method + // call is not inferable (BSK-0002) — an f-string return would infer + // `-> str` and silence BSK-0002 ([TYPEINF-FUNC-RETURN]). let (_fixture, diag) = open_and_diagnose( "file:///test.py", - "def greet(name):\n return f\"Hello, {name}!\"", + "def greet(name):\n return name.upper()", ) .await?; diff --git a/crates/basilisk-lsp/tests/lsp/ws_test_code_actions.rs b/crates/basilisk-lsp/tests/lsp/ws_test_code_actions.rs index 632dccaf..b3c2398a 100644 --- a/crates/basilisk-lsp/tests/lsp/ws_test_code_actions.rs +++ b/crates/basilisk-lsp/tests/lsp/ws_test_code_actions.rs @@ -83,14 +83,17 @@ async fn test_ws_code_action_missing_return_annotation() -> TestResult<()> { let mut fixture = WsTestFixture::new().await?; let _ = fixture.initialize().await?; - let code = "def greet(name: str):\n return f\"Hello, {name}!\""; + // The returned method call is not inferable, so BSK-0002 fires — an + // f-string return would infer `-> str` and stay silent + // ([TYPEINF-FUNC-RETURN]). + let code = "def greet(name: str):\n return name.upper()"; fixture.did_open("file:///ca_e0002.py", code).await?; let resp = code_action_for(&mut fixture, "file:///ca_e0002.py", 201, "BSK-0002").await?; assert!( - resp.contains("-> None"), - "BSK-0002 action should insert '-> None': {resp}" + resp.contains("-> Any"), + "BSK-0002 action should insert '-> Any': {resp}" ); assert!( resp.contains("quickfix"), diff --git a/crates/basilisk-lsp/tests/lsp/zed_extension_e2e_tests.rs b/crates/basilisk-lsp/tests/lsp/zed_extension_e2e_tests.rs index 064f99c7..a2971f8a 100644 --- a/crates/basilisk-lsp/tests/lsp/zed_extension_e2e_tests.rs +++ b/crates/basilisk-lsp/tests/lsp/zed_extension_e2e_tests.rs @@ -192,7 +192,10 @@ fn test_zed_diagnostics_on_open() -> TestResult<()> { let mut fixture = ZedLspFixture::new()?; let _ = fixture.initialize_zed_style()?; - let code = "def greet(name):\n return f\"Hello, {name}!\"\n"; + // `name` has no default to infer from (BSK-0001) and the returned method + // call is not inferable (BSK-0002) — an f-string return would infer + // `-> str` and silence BSK-0002 ([TYPEINF-FUNC-RETURN]). + let code = "def greet(name):\n return name.upper()\n"; fixture.did_open("file:///greet.py", code)?; let diag = fixture diff --git a/crates/basilisk-resolver/src/scope/function_types.rs b/crates/basilisk-resolver/src/scope/function_types.rs index d6facf0e..ec39bd19 100644 --- a/crates/basilisk-resolver/src/scope/function_types.rs +++ b/crates/basilisk-resolver/src/scope/function_types.rs @@ -22,6 +22,12 @@ pub struct ParameterInfo { pub annotation_is_numeric_literal: bool, /// `true` when the parameter has a default value (`param = default`). pub has_default: bool, + /// Classified kind of the default expression, if present. + /// + /// Lets BSK-0001 skip parameters whose type the current engine already + /// infers from a literal default (`timeout=30` → `int`) — see + /// [TYPEINF-FUNC-DEFAULTS]. `None` when there is no default. + pub default_rhs_kind: Option, /// The source span of the parameter name token. pub name_span: Span, /// The source span of the annotation expression, if present. diff --git a/crates/basilisk-resolver/src/scope/narrowing_types.rs b/crates/basilisk-resolver/src/scope/narrowing_types.rs index beebdad4..7fc68e17 100644 --- a/crates/basilisk-resolver/src/scope/narrowing_types.rs +++ b/crates/basilisk-resolver/src/scope/narrowing_types.rs @@ -105,6 +105,84 @@ pub enum NarrowingGuardKind { /// Whether a wildcard `case _:` is present (exhaustive). has_wildcard: bool, }, + /// `issubclass(x, T)` — narrows a class-object variable (§7.9). + IsSubclass { + /// The variable being narrowed. + variable: String, + /// The class name(s) checked against. + type_names: Vec, + /// Span of the `if` block. + if_body_span: Span, + /// Span of the `else` block (if present). + else_body_span: Option, + }, + /// `x == ` / `x != ` — literal equality narrowing. + EqualsLiteral { + /// The variable being narrowed. + variable: String, + /// The literal's source-faithful text (case preserved; strings keep + /// their quotes, e.g. `"Red"`, `1`, `True`). + literal_text: String, + /// `true` for `==`, `false` for `!=`. + is_positive: bool, + /// Span of the `if` block. + if_body_span: Span, + /// Span of the `else` block (if present). + else_body_span: Option, + }, + /// `x in ()` / `x not in ()` — membership narrowing. + InLiterals { + /// The variable being narrowed. + variable: String, + /// The literal texts of the container elements (case preserved). + literal_texts: Vec, + /// `true` for `in`, `false` for `not in`. + is_positive: bool, + /// Span of the `if` block. + if_body_span: Span, + /// Span of the `else` block (if present). + else_body_span: Option, + }, + /// `hasattr(x, "name")` — synthetic-protocol intersection groundwork. + HasAttr { + /// The variable being narrowed. + variable: String, + /// The attribute name checked for. + attribute: String, + /// Span of the `if` block. + if_body_span: Span, + /// Span of the `else` block (if present). + else_body_span: Option, + }, + /// `type(x) is C` / `type(x) is not C` — exact-class narrowing. The + /// positive branch implies `isinstance(x, C)`; the negative branch can + /// exclude `C` only when `C` is `@final` (a subclass instance may still + /// answer `type(x) is not C`). + TypeOfIs { + /// The variable whose `type(...)` is compared. + variable: String, + /// The class name compared against. + type_name: String, + /// `true` for `is`, `false` for `is not`. + is_positive: bool, + /// Span of the `if` block. + if_body_span: Span, + /// Span of the `else` block (if present). + else_body_span: Option, + }, + /// `"key" in td` / `"key" not in td` — `TypedDict` key-presence narrowing. + KeyInDict { + /// The variable (a `TypedDict`-typed name) being narrowed. + variable: String, + /// The key literal's value (unquoted). + key: String, + /// `true` for `in`, `false` for `not in`. + is_positive: bool, + /// Span of the `if` block. + if_body_span: Span, + /// Span of the `else` block (if present). + else_body_span: Option, + }, } /// A single `case` branch in a match statement narrowing. diff --git a/crates/basilisk-resolver/src/visitor/function_info.rs b/crates/basilisk-resolver/src/visitor/function_info.rs index a984adac..913fd40b 100644 --- a/crates/basilisk-resolver/src/visitor/function_info.rs +++ b/crates/basilisk-resolver/src/visitor/function_info.rs @@ -161,6 +161,9 @@ pub(super) fn collect_local_unannotated_vars(stmts: &[Stmt]) -> Vec ParameterInfo { let mut info = parameter_to_info(&p.parameter); info.has_default = p.default.is_some(); + // Classified default kind feeds the BSK-0001 inference exemption + // ([TYPEINF-FUNC-DEFAULTS]): a literal default already determines the type. + info.default_rhs_kind = p.default.as_deref().map(classify_rhs); info } @@ -177,6 +180,7 @@ pub(super) fn parameter_to_info(p: &Parameter) -> ParameterInfo { annotation_is_any, annotation_is_numeric_literal, has_default: false, + default_rhs_kind: None, name_span: text_range_to_span(p.name.range), annotation_span: p .annotation diff --git a/crates/basilisk-resolver/src/visitor/narrowing.rs b/crates/basilisk-resolver/src/visitor/narrowing.rs index 30a1700e..c4e0b20d 100644 --- a/crates/basilisk-resolver/src/visitor/narrowing.rs +++ b/crates/basilisk-resolver/src/visitor/narrowing.rs @@ -150,54 +150,11 @@ fn extract_guard_from_test( else_body_span: Option, ) -> Option { match test { - // isinstance(x, T) or isinstance(x, (T1, T2)) - Expr::Call(call) => { - let func_name = expr_simple_name(&call.func)?; - if func_name == "isinstance" && call.arguments.args.len() == 2 { - let variable = expr_simple_name(call.arguments.args.first()?)?; - let type_names = extract_type_names(call.arguments.args.get(1)?); - if !type_names.is_empty() { - return Some(NarrowingGuardKind::IsInstance { - variable, - type_names, - if_body_span, - else_body_span, - }); - } - } - None - } - // x is None / x is not None + // isinstance(x, T) / issubclass(x, T) / hasattr(x, "name") + Expr::Call(call) => extract_call_guard(call, if_body_span, else_body_span), + // x is None / x == lit / x in (lits) / "key" in td Expr::Compare(cmp) if cmp.comparators.len() == 1 => { - let is_none_check = matches!(cmp.comparators.first(), Some(Expr::NoneLiteral(_))); - let left_is_name = expr_simple_name(&cmp.left); - - // Also handle `None is x` (reversed) - let (variable, is_none) = if is_none_check { - (left_is_name?, true) - } else if matches!(cmp.left.as_ref(), Expr::NoneLiteral(_)) { - let right_name = cmp.comparators.first().and_then(expr_simple_name)?; - (right_name, true) - } else { - return None; - }; - - if !is_none { - return None; - } - - let is_positive = match cmp.ops.first() { - Some(CmpOp::Is) => true, - Some(CmpOp::IsNot) => false, - _ => return None, - }; - - Some(NarrowingGuardKind::IsNone { - variable, - is_positive, - if_body_span, - else_body_span, - }) + extract_compare_guard(cmp, if_body_span, else_body_span) } // `not x` — inverted truthiness Expr::UnaryOp(unary) if matches!(unary.op, ruff_python_ast::UnaryOp::Not) => { @@ -223,6 +180,207 @@ fn extract_guard_from_test( } } +/// Extract a guard from a call test: `isinstance`, `issubclass`, `hasattr`. +fn extract_call_guard( + call: &ruff_python_ast::ExprCall, + if_body_span: Span, + else_body_span: Option, +) -> Option { + let func_name = expr_simple_name(&call.func)?; + if call.arguments.args.len() != 2 { + return None; + } + let variable = expr_simple_name(call.arguments.args.first()?)?; + let second = call.arguments.args.get(1)?; + match func_name.as_str() { + "isinstance" => { + let type_names = extract_type_names(second); + (!type_names.is_empty()).then_some(NarrowingGuardKind::IsInstance { + variable, + type_names, + if_body_span, + else_body_span, + }) + } + // Implements [TYPEINF-NARROWING-ISSUBCLASS]. + "issubclass" => { + let type_names = extract_type_names(second); + (!type_names.is_empty()).then_some(NarrowingGuardKind::IsSubclass { + variable, + type_names, + if_body_span, + else_body_span, + }) + } + // Implements [TYPEINF-NARROWING-HASATTR] (groundwork). + "hasattr" => match second { + Expr::StringLiteral(lit) => Some(NarrowingGuardKind::HasAttr { + variable, + attribute: lit.value.to_str().to_owned(), + if_body_span, + else_body_span, + }), + _ => None, + }, + _ => None, + } +} + +/// Extract a guard from a single comparison: `is None`, `== `, +/// `in ()`, or `"key" in td`. +fn extract_compare_guard( + cmp: &ruff_python_ast::ExprCompare, + if_body_span: Span, + else_body_span: Option, +) -> Option { + let op = *cmp.ops.first()?; + let right = cmp.comparators.first()?; + match op { + CmpOp::Is | CmpOp::IsNot => { + // Implements [TYPEINF-NARROWING-TYPEOF]: `type(x) is C`. + if let Some(guard) = extract_type_of_guard(cmp, op, right, if_body_span, else_body_span) + { + return Some(guard); + } + extract_none_guard(cmp, op, right, if_body_span, else_body_span) + } + // Implements [TYPEINF-NARROWING-EQ-LITERAL]. + CmpOp::Eq | CmpOp::NotEq => { + let variable = expr_simple_name(&cmp.left)?; + let literal_text = literal_source_text(right)?; + Some(NarrowingGuardKind::EqualsLiteral { + variable, + literal_text, + is_positive: matches!(op, CmpOp::Eq), + if_body_span, + else_body_span, + }) + } + CmpOp::In | CmpOp::NotIn => { + extract_membership_guard(cmp, op, right, if_body_span, else_body_span) + } + _ => None, + } +} + +/// `type(x) is C` / `type(x) is not C` — exact-class comparison. +fn extract_type_of_guard( + cmp: &ruff_python_ast::ExprCompare, + op: CmpOp, + right: &Expr, + if_body_span: Span, + else_body_span: Option, +) -> Option { + let Expr::Call(call) = cmp.left.as_ref() else { + return None; + }; + if expr_simple_name(&call.func)? != "type" || call.arguments.args.len() != 1 { + return None; + } + let variable = expr_simple_name(call.arguments.args.first()?)?; + let type_name = expr_simple_name(right)?; + Some(NarrowingGuardKind::TypeOfIs { + variable, + type_name, + is_positive: matches!(op, CmpOp::Is), + if_body_span, + else_body_span, + }) +} + +/// The original `is None` / `is not None` extraction (both operand orders). +fn extract_none_guard( + cmp: &ruff_python_ast::ExprCompare, + op: CmpOp, + right: &Expr, + if_body_span: Span, + else_body_span: Option, +) -> Option { + let variable = if matches!(right, Expr::NoneLiteral(_)) { + expr_simple_name(&cmp.left)? + } else if matches!(cmp.left.as_ref(), Expr::NoneLiteral(_)) { + expr_simple_name(right)? + } else { + return None; + }; + Some(NarrowingGuardKind::IsNone { + variable, + is_positive: matches!(op, CmpOp::Is), + if_body_span, + else_body_span, + }) +} + +/// `x in (lits)` → [`NarrowingGuardKind::InLiterals`]; +/// `"key" in td` → [`NarrowingGuardKind::KeyInDict`]. +fn extract_membership_guard( + cmp: &ruff_python_ast::ExprCompare, + op: CmpOp, + right: &Expr, + if_body_span: Span, + else_body_span: Option, +) -> Option { + let is_positive = matches!(op, CmpOp::In); + // Implements [TYPEINF-NARROWING-TYPEDDICT-KEY]: `"key" in td`. + if let (Expr::StringLiteral(key), Some(variable)) = (cmp.left.as_ref(), expr_simple_name(right)) + { + return Some(NarrowingGuardKind::KeyInDict { + variable, + key: key.value.to_str().to_owned(), + is_positive, + if_body_span, + else_body_span, + }); + } + // Implements [TYPEINF-NARROWING-IN-LITERAL]: `x in ("a", "b")`. + let variable = expr_simple_name(&cmp.left)?; + let elements: &[Expr] = match right { + Expr::Tuple(tuple) => &tuple.elts, + Expr::List(list) => &list.elts, + Expr::Set(set) => &set.elts, + _ => return None, + }; + let literal_texts: Vec = elements.iter().filter_map(literal_source_text).collect(); + if literal_texts.len() != elements.len() || literal_texts.is_empty() { + return None; + } + Some(NarrowingGuardKind::InLiterals { + variable, + literal_texts, + is_positive, + if_body_span, + else_body_span, + }) +} + +/// Case-preserving literal text for equality/membership guards: quoted +/// strings, decimal ints (including negated), and booleans. Anything else — +/// including strings containing a double quote — is skipped rather than +/// approximated. +fn literal_source_text(expr: &Expr) -> Option { + match expr { + Expr::StringLiteral(lit) => { + let value = lit.value.to_str(); + (!value.contains('"')).then(|| format!("\"{value}\"")) + } + Expr::NumberLiteral(num) => match &num.value { + ruff_python_ast::Number::Int(int) => int.as_i64().map(|n| n.to_string()), + _ => None, + }, + Expr::BooleanLiteral(lit) => Some(if lit.value { "True" } else { "False" }.to_owned()), + Expr::UnaryOp(unary) if matches!(unary.op, ruff_python_ast::UnaryOp::USub) => { + match unary.operand.as_ref() { + Expr::NumberLiteral(num) => match &num.value { + ruff_python_ast::Number::Int(int) => int.as_i64().map(|n| (-n).to_string()), + _ => None, + }, + _ => None, + } + } + _ => None, + } +} + /// Extract a narrowing guard from an `assert` statement's test expression. // Implements [TYPEINF-NARROWING-ASSERT] — `assert isinstance(x, T)` / // `assert x is not None` narrows for all subsequent code in the flow path. diff --git a/docs/INDEX.md b/docs/INDEX.md index d2b93b17..ef128560 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -14,7 +14,7 @@ linked to an active plan. | File | Purpose | |---|---| | [Checker architecture](specs/CHECKER-ARCHITECTURE-SPEC.md) | Configuration, rules, diagnostics, analysis pipeline, CLI, and quality gates. | -| [Type inference](specs/CHECKER-TYPE-INFERENCE-SPEC.md) | Conservative expression/type inference and narrowing contracts. | +| [Type inference](specs/CHECKER-TYPE-INFERENCE-SPEC.md) | Expression/type inference and narrowing contracts, plus the target bidirectional/constraint architecture and its research grounding. | | [Stub resolution](specs/CHECKER-STUB-RESOLUTION-SPEC.md) | Static import/stub order, custom typeshed, provenance, and generation. | | [Checker cache](specs/CHECKER-CACHE-SPEC.md) | Opt-in content-addressed CLI result cache. | | [Rule tagging](specs/CHECKER-RULE-TAGGING-SPEC.md) | Rule provenance/category/free-form tags and conflict rules. | @@ -49,10 +49,10 @@ Plans contain only unfinished work. Delete a plan when its acceptance gate passe | [Roadmap](plans/ROADMAP-NEXT-STEPS-PLAN.md) | Distribution follow-ups, scale, ecosystem, and links to focused plans. | | [Specification conformance audit](plans/SPEC-CONFORMANCE-AUDIT-PLAN.md) | Confirmed implementation/spec deviations. | | [Configuration editor](plans/LSP-CONFIGURATION-EDITOR-PLAN.md) | Buffer safety, protocol E2E coverage, provenance/metadata, cross-editor clients, and release evidence. | -| [Formatting](plans/LSP-FORMATTING-PLAN.md) | Release provenance, remaining clients/CLI/docs, and acceptance. | +| [Formatting](plans/LSP-FORMATTING-PLAN.md) | VS Code default-formatter opt-in and published-artifact verification. | | [AI-assisted LSP](plans/LSP-AI-PLAN.md) | First opt-in provider slice and privacy/safety gate. | | [Activity panel](plans/EXTENSION-ACTIVITY-PANEL-PLAN.md) | Settings wiring and remaining cross-editor/test quality. | -| [Type narrowing](plans/CHECKER-TYPE-NARROWING-INFERENCE-PLAN.md) | Flow, expression inference, constraints, and shared subtyping. | +| [Type narrowing and inference](plans/CHECKER-TYPE-NARROWING-INFERENCE-PLAN.md) | Bidirectional/constraint-based inference engine, flow analysis, shared subtyping, and PEP 827 readiness. | | [Eliminate line scanning](plans/CHECKER-ELIMINATE-LINE-SCANNING-PLAN.md) | Replace remaining raw-source rule scans with AST data. | | [Advanced checker features](plans/CHECKER-ADVANCED-FEATURES-PLAN.md) | Mojo checks, plugin host, migration, and CI helpers. | | [Native compiler](plans/COMPILER-ARCHITECTURE-PLAN.md) | HIR, backend, runtime, interop, CLI, and native acceptance. | diff --git a/docs/plans/CHECKER-TYPE-NARROWING-INFERENCE-PLAN.md b/docs/plans/CHECKER-TYPE-NARROWING-INFERENCE-PLAN.md index 9e6a25fb..3149d6d5 100644 --- a/docs/plans/CHECKER-TYPE-NARROWING-INFERENCE-PLAN.md +++ b/docs/plans/CHECKER-TYPE-NARROWING-INFERENCE-PLAN.md @@ -1,39 +1,523 @@ # Type Narrowing and Inference Plan {#NARROWPLAN-INFERENCE} -Specs: [TYPEINF-OVERVIEW](../specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-OVERVIEW) -and [CHKARCH-INFERENCE](../specs/CHECKER-ARCHITECTURE-SPEC.md#CHKARCH-INFERENCE). +Specs: [TYPEINF-OVERVIEW](../specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-OVERVIEW), +[TYPEINF-TARGET](../specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-TARGET), and +[CHKARCH-INFERENCE](../specs/CHECKER-ARCHITECTURE-SPEC.md#CHKARCH-INFERENCE). -Basilisk already passes the typing conformance suite. This plan is therefore -about consolidating duplicated rule-local inference and improving editor/user -behavior without weakening the zero-false-positive gate. +Basilisk already passes the typing conformance suite. This plan therefore has two +tracks that share one implementation: + +1. **Consolidation** — merge duplicated rule-local inference into shared + components and improve editor/user behavior without weakening the + zero-false-positive gate. +2. **A substantially more powerful inference engine** — bidirectional + (synthesis + checking) typing over a subtype-constraint solver, per the + target architecture in + [TYPEINF-TARGET](../specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-TARGET) + and the research grounding in + [TYPEINF-RESEARCH](../specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-RESEARCH). + This closes inference-gap issues like + [#317](https://github.com/MelbourneDeveloper/Basilisk/issues/317) and lays + the groundwork for PEP 827-style type manipulation. Current foundations include `InferredType`, annotation parsing, literal RHS inference, resolver-collected narrowing guards, and several rule-local Protocol/TypedDict/Callable algorithms. There is no shared checker-side flow environment, expression inferrer, constraint solver, or subtype context. +## Goals and non-goals {#NARROWPLAN-GOALS} + +**Goals** + +- Inference powerful enough that annotations become recoverable from usage. + Issue [#317](https://github.com/MelbourneDeveloper/Basilisk/issues/317) is + the canonical example: in `def multiply(x, y) -> int: return x * y` called as + `multiply(4, 5)`, both parameters are recoverable from the body constraint + (`x * y` must produce `int` given `-> int`, via constraint-based inference + over the overloaded `__mul__`/`__rmul__` operator) and from the call site. + A constraint-based and/or call-site-driven engine types this with no + annotations, making a `BSK-0001` diagnostic unnecessary in such cases. +- Everything is oriented toward + [PEP 827 – Type Manipulation](https://peps.python.org/pep-0827/): the engine + must become powerful enough (bidirectional context, constraint solving, + bounded type-level evaluation) that PEP 827's conditional/mapped types have a + sound home if adopted later. +- Preserve the gradual guarantee as a testable invariant, keep the + zero-false-positive conformance gate, and hold both benchmark ratchets + ([CHKARCH-TESTING-BENCH-RATCHET](../specs/CHECKER-ARCHITECTURE-SPEC.md#CHKARCH-TESTING-BENCH-RATCHET)). +- **Superiority is the exit criterion, not an aspiration.** Basilisk MUST end + this plan with measurably better type inference than pyright, mypy, ty, + pyrefly, and zuban. The plan is not complete while any competitor leads any + axis in [NARROWPLAN-TARGETS](#NARROWPLAN-TARGETS); the mechanism that makes + the claim honest, enforceable, and permanent is the superiority gate in + [NARROWPLAN-SUPERIORITY](#NARROWPLAN-SUPERIORITY). + +**Non-goals** + +- **Not implementing PEP 827.** No `typing.IsAssignable`, Type Booleans, + conditional type expressions, or `RaiseError` surface syntax ships from this + plan. The type-level evaluation work in + [NARROWPLAN-TYPELEVEL](#NARROWPLAN-TYPELEVEL) is engine groundwork + (bounded normalization, memoization, divergence fallbacks) so the inference + engine is PEP 827-ready — nothing more. +- No alternate checking mode + ([CHKARCH-CONFIGURATION-ONLY](../specs/CHECKER-ARCHITECTURE-SPEC.md#CHKARCH-CONFIGURATION-ONLY)); + every shared component lands behind existing checker APIs. +- No global Hindley–Milner/Algorithm W core — see + [TYPEINF-RESEARCH-THEORY](../specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-RESEARCH-THEORY) + for why unification-based HM does not fit Python's subtyping, mutation, and + gradual `Any`. + +## Staged delivery {#NARROWPLAN-STAGES} + +Delivery is staged so each stage is independently shippable and each carries an +explicit decision threshold for changing course. + +- **Stage 0 — foundational commitments** (before writing the solver): the + two-mode bidirectional core and the two-stage constraint architecture with + deferred generalization, per + [TYPEINF-TARGET-BIDIRECTIONAL](../specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-TARGET-BIDIRECTIONAL) + and + [TYPEINF-TARGET-CONSTRAINTS](../specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-TARGET-CONSTRAINTS). + *Threshold:* if bidirectional `check` mode cannot be threaded cleanly through + Salsa queries without ballooning dependencies, fall back to Pyrefly-style + "peek-ahead" only for the specific constructs that fail. +- **Stage 1 — incrementality**: match ty on granularity, exceed Pyrefly, per + [TYPEINF-TARGET-INCREMENTAL](../specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-TARGET-INCREMENTAL). + *Threshold:* if fine-grained queries cause memory blowup on a 1M+ LOC + target, adopt Pyrefly-style AST/binding eviction (drop intermediate state, + keep only interfaces) behind the query layer. +- **Stage 2 — flow-sensitive narrowing that beats everyone**, per + [TYPEINF-TARGET-NARROWING](../specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-TARGET-NARROWING), + together with the consolidation sections below. +- **Stage 3 — type-level computation groundwork for PEP 827 readiness**, per + [TYPEINF-TARGET-TYPELEVEL](../specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-TARGET-TYPELEVEL). + ## Flow analysis {#NARROWPLAN-FLOW} -- [ ] Add a scoped narrowing environment with branch push, complement, join, and - nested-function boundaries. -- [ ] Consume resolver guards for `isinstance`, `is None`, truthiness, +Add a scoped narrowing environment with branch push, complement, join, and +nested-function boundaries, consuming resolver guards (`isinstance`, `is None`, +truthiness, `TypeGuard`, `TypeIs`, `assert`, pattern matching). Model +assignment narrowing without changing the declared type used for assignment +validation. + +The target formulation is occurrence typing +(Tobin-Hochstadt–Felleisen; Castagna et al. — see +[TYPEINF-RESEARCH-GRADUAL](../specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-RESEARCH-GRADUAL)) +as **intersection-and-negation-based narrowing** (ty's model) over a +Salsa-backed use-def map, with `phi`/join at control-flow merges (Pyrefly's +binding DSL). Supported guards grow to include `issubclass`, `==`/`in` against +literals, `hasattr` (synthetic protocol intersection), and +exhaustiveness/implied-else. The **attribute-narrowing-across-calls** soundness +tradeoff is decided explicitly per Pyrefly's lesson — default to Pyrefly's +usable behavior but make it configurable for security-sensitive users. +Reachability becomes **inference-driven** (ty's model) rather than +pattern-matched idioms. + +## Expression inference {#NARROWPLAN-EXPRESSIONS} + +Infer same-module and imported function/method return types; constructor, +attribute, subscript, binary/unary, conditional, and walrus expressions from +structured AST data. Centralize builtin constructor/method signatures instead +of adding rule-local string tables, and reuse the same inference results for +diagnostics, hover, completions, and inlay hints. + +With the bidirectional core in place, `check` mode propagates expected types +into container literals, comprehensions, lambda parameters, and call +arguments; parameter types become recoverable from body constraints and call +sites (issue #317). + +## Generic constraints {#NARROWPLAN-CONSTRAINTS} + +Collect lower, upper, constrained, default, and expected-return bounds for +TypeVars; solve bounds deterministically and report ambiguity without +guessing. Cover constrained/bound TypeVars, PEP 696 defaults, ParamSpec, and +TypeVarTuple interactions before wiring the solver into rule decisions. + +Type variables carry explicit lower/upper bounds (like Pyright's type +intervals and Pyrefly's `Var`) with the input/output polarity discipline +borrowed from Dolan/Parreaux — **without** committing to full biunification +(see the risk in [NARROWPLAN-RISKS](#NARROWPLAN-RISKS)). Generalization is +deferred: infer `list[Var{lower=Literal[1]}]` and settle `Var` only at first +constraining use, preserving `list[int]` vs `list[Literal[1]]` precision +instead of eagerly widening `Literal[1] → int`. The Jane Street Q&A confirms +Pyrefly's own team conceded the deferred "most general type" is "the correct +type" and "might be more enjoyable" — Basilisk should ship it. + +## Shared subtyping {#NARROWPLAN-SUBTYPING} + +Build a context for nominal class relationships, Protocol members, TypedDict +schemas, generic variance, and Callable parameter kinds. Replace duplicated +rule-local subtype helpers only after parity tests pin their current +accepted/rejected cases. Keep `Any`/`Unknown` gradual behavior and the numeric +tower consistent across annotation parsing and inferred types. + +## Incrementality {#NARROWPLAN-INCREMENTAL} + +Use Salsa with **definition-level and expression-level tracked queries** (ty's +model), not file-level (Pyrefly's). Compute a compact per-module +**interface/signature** query as the cross-file dependency boundary (Pyrefly's +"Interface" idea) to get early cutoff and prevent whole-program invalidation. +Model cycles with **fixpoint iteration seeded by a divergent/bottom sentinel** +and a hard iteration cap (ty's `Divergent`; Pyrefly's thunks). The Stage 1 +memory threshold in [NARROWPLAN-STAGES](#NARROWPLAN-STAGES) governs the +eviction fallback. + +## Type-level evaluation groundwork (PEP 827 readiness) {#NARROWPLAN-TYPELEVEL} + +Build a **normalization-by-evaluation engine** for type-level functions +(conditional/mapped types) as a set of memoized Salsa queries returning types +in weak-head normal form (whnf). Enforce: + +- (a) **fuel/depth bounds** (TypeScript's instantiation-depth model); +- (b) **memoization** of normalized results; +- (c) a **`Divergent`/`@Todo` fallback** that preserves the gradual guarantee + when evaluation is truncated; +- (d) **GHC-style acceptance conditions** (Paterson/Coverage analogues) that + statically reject obviously-nonterminating type-level definitions, with an + opt-in "undecidable" escape hatch. + +Represent mapped types as **kind `Type → Type` operators** and conditional +types as guarded rewrites keyed on a consistency/assignability check +(`IsAssignable` in PEP 827); evaluate lazily (call-by-need) so unused branches +never diverge. Bounded evaluation is mandatory, not optional: type-level +computation in this space is provably Turing-complete (see +[TYPEINF-RESEARCH-TYPELEVEL](../specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-RESEARCH-TYPELEVEL)). + +Scope reminder: this is **engine groundwork only** — no PEP 827 surface +feature ships from this plan (see [NARROWPLAN-GOALS](#NARROWPLAN-GOALS)). + +## Integration {#NARROWPLAN-INTEGRATION} + +Introduce each shared component behind existing checker APIs; do not create an +alternate checking mode. Migrate assignment, return, call, and `assert_type` +rules incrementally, deleting the replaced local logic in the same change. Add +spec-ID-linked mutation-resistant tests for each migrated behavior. + +## Measurable targets {#NARROWPLAN-TARGETS} + +The axes on which inference superiority is defined and measured. Each axis has +a concrete metric so the lead is provable, not asserted: + +- **Bidirectional literal/generic inference:** deferred bounded type variables + preserve `list[int]` vs `list[Literal[1]]` precision *and* accept more + programs (measure against the typing-spec conformance suite + a curated + container/comprehension/lambda benchmark). +- **Narrowing richness:** intersection+negation narrowing and + `hasattr`/pattern narrowing (measure against the utahplt/ifT-benchmark, + ). +- **Higher-order inference:** propagate expected types through + `map`/`filter`/decorators/`ParamSpec` (build a targeted higher-order + benchmark; Pyrefly is strong here, so this is the hardest win). +- **Incremental latency:** aim for single-digit-millisecond keystroke updates + via definition-level Salsa queries (ty reports 4.7ms on a load-bearing + PyTorch edit) vs Pyrefly's file-level invalidation (measure p50/p99 re-check + latency on a 1M-LOC corpus). +- **Gradual-guarantee conformance:** a differential test that strips + annotations and asserts no new errors — Pyrefly fails this by design; + Basilisk should pass. + +## Superiority gate {#NARROWPLAN-SUPERIORITY} + +Basilisk MUST have better type inference than every officially-recognized +competitor. "Better" is defined operationally and enforced exactly the way +this repo already enforces conformance and speed — self-measured, +reproducible, write-always, ratcheted: + +- **Definition.** Basilisk is superior on an axis when it scores strictly + better than the LATEST official release of every officially-recognized + competitor (pyright, mypy, ty, pyrefly, zuban — the same set as the speed + benchmarks) on that axis's metric, measured by Basilisk's own harness with + the methodology stated in the results. Consistent with the documentation + honesty rules, we never claim a lead by comparing our numbers against + vendor-published figures — only same-harness, same-corpus, same-machine + measurements count. +- **Inference scoreboard harness.** Mirror the `benchmarks/` design: every run + pulls the latest official release of each competitor and runs the full + corpus set against all checkers — the reveal_type-precision corpus + (containers/comprehensions/lambdas/literal-generic precision), the + utahplt/ifT narrowing benchmark, the higher-order corpus, the + gradual-guarantee differential suite, and the incremental-latency + measurement. Scores are written to a status file **immediately and + unconditionally** (WRITE-ALWAYS); a separate read-only gate compares against + the committed baseline (GATE-SEPARATELY). A run that measured a score but + didn't record it is a lie. +- **Ratchet.** Once Basilisk takes the lead on an axis, the lead becomes a CI + gate: falling behind any competitor on a led axis is a build failure. Leads + only accumulate. The plan exits only when Basilisk leads **all five axes + simultaneously** while the 100%/0-FP conformance gate and the speed + benchmark gate stay green — inference superiority must never be bought by + regressing conformance or performance, and vice versa. +- **Moving targets.** Because the harness pulls latest competitor releases, + superiority is continuously re-proven against competitors as they improve — + never against frozen versions. If a competitor release takes back an axis, + CI goes red and reclaiming that axis becomes the top-priority work item on + this plan. +- **Claims discipline.** No "better inference than X" statement ships in + docs, website, or marketing unless the current committed scoreboard run + shows the lead, and the claim links to how it is measured. + +## Risks and decision thresholds {#NARROWPLAN-RISKS} + +- **Decidability wall.** With PEP 827 conditional/mapped types, type-level + evaluation is Turing-complete (proven for both TypeScript and Python type + hints — see + [TYPEINF-RESEARCH-TYPELEVEL](../specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-RESEARCH-TYPELEVEL)). + Basilisk cannot be simultaneously complete and terminating; bounded + evaluation with explicit `Divergent`/`@Todo` fallbacks is mandatory, and + some legitimate type-level programs will hit the bound. This is an inherent + limitation, not an implementation gap. +- **Algebraic subtyping may not transfer.** MLsub/Simple-sub assume + structural, fully-inferred types; Python's nominal classes, invariant + generics, overloads, and `Any` do not fit cleanly. Committing to full + biunification is a research risk; the safe path is to borrow polar types and + constraint simplification only. Flag this as unvalidated until a prototype + confirms it on real typeshed stubs. +- **Soundness vs. practicality is unavoidable.** Pyrefly explicitly chose + usability over soundness (attribute narrowing across calls). Every such + choice trades false negatives for ergonomics. Basilisk must make these + choices *explicitly and configurably*, and document them, rather than + inheriting them implicitly. +- **Gradual guarantee vs. aggressive inference are in direct tension.** You + cannot both (a) infer concrete types in unannotated code to catch bugs + (Pyrefly) and (b) guarantee that removing annotations never adds errors + (ty). Basilisk must pick a default and likely offer a strictness dial; + claiming to "beat Pyrefly on inference" while "preserving the gradual + guarantee" requires being precise about *which* mode is being compared. +- **Incrementality vs. global inference.** The more Basilisk infers across + function/call boundaries (to beat Pyrefly), the larger its Salsa dependency + graph and the more a single edit invalidates. The interface/signature-boundary + technique mitigates but does not eliminate this; expect to tune the + granularity empirically. +- **Stage thresholds.** The per-stage fallback triggers live in + [NARROWPLAN-STAGES](#NARROWPLAN-STAGES): peek-ahead fallback if `check` mode + balloons Salsa dependencies; eviction fallback if fine-grained queries blow + memory on a 1M+ LOC target. +- **Source quality of competitor numbers.** Comparative figures quoted in + [TYPEINF-RESEARCH-COMPETITORS](../specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-RESEARCH-COMPETITORS) + are vendor/benchmark claims, not independently audited; treat them as + directional. The only numbers Basilisk acts on are the ones its own + scoreboard harness produces ([NARROWPLAN-SUPERIORITY](#NARROWPLAN-SUPERIORITY)). +- **Competitors are moving targets.** Pyrefly and ty ship fast and are well + funded; ty is actively closing its bidirectional gap. The superiority gate + is designed for this: leads are re-proven against latest releases on every + run, and a lost axis turns CI red rather than silently eroding the claim. + +## Acceptance {#NARROWPLAN-ACCEPTANCE} + +- Inference and narrowing have one shared implementation rather than diverging + rule-local approximations. +- Hover/inlay results and checker diagnostics agree for the same expression. +- The gradual-guarantee differential suite (strip annotations → assert no new + errors) passes. +- The inference scoreboard ([NARROWPLAN-SUPERIORITY](#NARROWPLAN-SUPERIORITY)) + shows Basilisk strictly ahead of the latest official releases of pyright, + mypy, ty, pyrefly, and zuban on **every** axis in + [NARROWPLAN-TARGETS](#NARROWPLAN-TARGETS), and the per-axis ratchet is wired + into CI so the lead cannot silently erode. +- `make test`, mutation/coverage ratchets, benchmarks for touched hot paths, and + the live 141/141 conformance gate all pass with zero false positives. + +## Checklist {#NARROWPLAN-CHECKLIST} + +### Stage 0 — bidirectional + constraint foundations + +- [x] Add the two-mode bidirectional core: every AST expression node supports + `synth(e) → τ` and `check(e, τ)`, with `check` as the primary driver + ([TYPEINF-TARGET-BIDIRECTIONAL](../specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-TARGET-BIDIRECTIONAL)). + — `crates/basilisk-checker/src/bidir/{engine,check}.rs`. +- [x] Thread expected types through container literals, comprehensions, lambda + parameters, and call arguments; verify Salsa dependency growth stays + acceptable, else fall back to peek-ahead for the failing constructs only. + — Verified by architecture: the engine is a pure function of one module's + AST inside the existing file-level tracked queries, so it adds zero Salsa + edges (see the Salsa note in `crates/basilisk-checker/src/bidir/mod.rs`); + re-evaluate per construct when Stage 1 moves to finer-grained queries. +- [x] Add the two-stage constraint architecture: a constraint-generation pass + producing subtype constraints (`τ₁ <: τ₂`) and a separate solver + ([TYPEINF-TARGET-CONSTRAINTS](../specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-TARGET-CONSTRAINTS)). + — `crates/basilisk-checker/src/bidir/{constraints,solve}.rs`; every ground + leaf delegates to `InferredType::is_assignable_to`. +- [x] Represent type variables with explicit lower/upper bounds and + input/output polarity; defer generalization to first constraining use + (`list[Var{lower=Literal[1]}]`, not eager `Literal[1] → int`). + — `crates/basilisk-checker/src/bidir/tyvar.rs`; the exact + `list[Var{lower=Literal[1]}]` case is a unit test. +- [x] Build the gradual-guarantee differential test harness: strip annotations + from a corpus and assert no new errors appear. + — `crates/basilisk-checker/tests/gradual_guarantee_tests.rs` (curated + corpus + a sweep of all synced conformance fixtures); it immediately caught + and drove the fix of a real rule defect (`classes_override` treated an + absent annotation as a signature mismatch). +- [x] Prototype-validate the borrowed algebraic-subtyping ideas (polar types, + constraint simplification) against real typeshed stubs before relying on + them. + — `crates/basilisk-checker/tests/bidir_typeshed_validation_tests.rs` over + five verbatim `python/typeshed` stubs (commit pinned in + `tests/fixtures/typeshed/TYPESHED_COMMIT.txt`): solver reflexivity, + projection idempotence, and polar-variable resolution over 300+ real + annotations. + +### Stage 1 — incrementality + +- [x] Move inference onto definition-level and expression-level Salsa tracked + queries (not file-level). + — `crates/basilisk-checker/src/incremental_defs.rs`: a tracked + `Definition` struct per top-level definition (keyed on the definition's own + source *slice*, so edits elsewhere leave its memos untouched), with + `definition_type` (per-definition) and `expression_types` + (per-expression) queries. Early cutoff is proven by salsa's `WillExecute` + log in `tests/incremental_defs_tests.rs`: editing one definition + re-executes exactly one `definition_type`. +- [x] Compute a compact per-module interface/signature query as the cross-file + dependency boundary for early cutoff. + — `module_interface` returns a `PartialEq` `(name, type)` list; a + body-only edit backdates to "unchanged" (test: + `body_only_edit_backdates_the_module_interface`). +- [x] Model inference cycles with fixpoint iteration seeded by a + divergent/bottom sentinel and a hard iteration cap. + — `definition_type` opts into salsa fixpoint iteration + (`cycle_initial` = `Unknown`, the divergent/bottom sentinel; + `cycle_fn` caps at `CYCLE_ITERATION_CAP = 16` and falls back to the + sentinel). `a = b; b = a` terminates and settles on `Unknown` (test). +- [x] Measure memory on a 1M+ LOC target; if it blows up, add AST/binding + eviction behind the query layer (keep only interfaces). + — Self-measured via `examples/incremental_measure.rs` over the seeded + synthetic corpus from `scripts/gen_incremental_corpus.py` (2,100 files, + 1,117,199 LOC, 268,800 definitions): RSS ≈ 312 MB after the cold pass and + a 200-edit keystroke loop — no blow-up, so eviction is not needed at this + stage (re-measure when the per-definition queries carry richer state). +- [x] Measure p50/p99 keystroke re-check latency on a 1M-LOC corpus; target + single-digit milliseconds. + — Same harness, same corpus: **p50 0.16 ms, p99 0.22 ms** per keystroke + re-check of the edited file's definition-level queries (cold pass 0.99 s). + Scope caveat, stated plainly: this measures the Stage 1 definition-level + query layer, not the full 165-rule diagnostics pipeline, which remains + file-level until the Integration stage migrates it. + +### Stage 2 — flow analysis and narrowing + +- [x] Add a scoped narrowing environment with branch push, complement, join, + and nested-function boundaries. + — `crates/basilisk-checker/src/narrow/env.rs` (`NarrowEnv`: branch frames, + a whole-scope layer for `assert`/early-exit facts, `phi`-join at merges, + fresh-environment nested-function boundary) plus the statement-level + walker in `narrow/flow.rs` (early-exit complement persistence, loop and + try/except frames discarded). +- [x] Consume resolver guards for `isinstance`, `is None`, truthiness, `TypeGuard`, `TypeIs`, `assert`, and pattern matching. -- [ ] Model assignment narrowing without changing the declared type used for + — `narrow/guards.rs` (two-branch outcomes with PEP 647/742 asymmetry, + loop-guard suppression) + `narrow/flow.rs` (span-matched application, + `match` per-case subject narrowing with value-pattern conservatism). +- [x] Model assignment narrowing without changing the declared type used for assignment validation. -- [ ] Test positive/complement branches, loops, early exits, closures, and + — `NarrowEnv` keeps the declared layer immutable (`declared()` is the + validation anchor); `x = expr` narrows only the flow layer via the + bidirectional engine's synthesized type (tests in + `tests/narrow_flow_tests.rs` and `narrow/env.rs`). +- [x] Test positive/complement branches, loops, early exits, closures, and unreachable branches through public checker behavior. + — `tests/narrow_flow_tests.rs` runs the real parse → resolve → + `analyse_function` pipeline over all six cases (unreachable branches via + the `Never` narrowing signal). Diagnostic-level surfacing of these + results lands with the Integration-stage rule migration. +- [x] Reformulate narrowing as intersection-and-negation types over a + Salsa-backed use-def map with `phi`/join at control-flow merges. + — Intersection/negation: `narrow/set_ops.rs` (`intersect`/`subtract` + delegating atoms to `is_assignable_to`); `phi`/join: `NarrowEnv::join`; + Salsa backing: the tracked `narrowed_uses` query in + `incremental_defs.rs` keys the whole flow analysis on one definition's + source slice — editing one function re-executes only its own narrowing + (proven by `WillExecute` log in `tests/incremental_defs_tests.rs`). +- [x] Extend guard support: `issubclass`, `==`/`in` against literals, + `hasattr` (synthetic protocol intersection), exhaustiveness/implied-else, + and TypedDict `"key" in td` narrowing. + — Resolver extraction for all five (`visitor/narrowing.rs`, new + `NarrowingGuardKind` variants with case-preserving literal capture) and + checker interpretation (`narrow/guards.rs`): `==`/`in` literal + narrowing with exact-literal complements, `"key" in td` union filtering + over a `NarrowContext` of `TypedDict` key sets (required vs optional), + and implied-else after fully-diverging `match` statements. `issubclass` + and `hasattr` interpretation is deliberately identity until `type[...]` + object modelling and synthetic-protocol intersections land with the + shared-subtyping work — extraction is live, so flipping them on is a + local change. +- [x] Decide and document the attribute-narrowing-across-calls tradeoff; + default to the usable behavior, make it configurable. + — Decided and recorded in + [TYPEINF-NARROWING-ATTR-CALLS](../specs/CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-NARROWING-ATTR-CALLS): + narrows survive calls by default (usable), and + `narrow-attributes-across-calls = false` in `[tool.basilisk]` opts into + the sound-but-strict behavior + (`BasiliskConfig::narrow_attributes_across_calls`, parsed + merged in + `crates/basilisk-config/src/parse.rs`). +- [ ] Replace pattern-matched reachability idioms with inference-driven + reachability. +- [x] Measure narrowing richness against the utahplt/ifT-benchmark + (). + — Harness: `crates/basilisk-checker/examples/ift_measure.rs` over a fresh + clone's `Pyright/main.py` (benchmark commit `cfb31ef` at measurement + time). Self-measured baseline, methodology stated in the harness header: + **11/37 functions (29%) produce a flow-narrowing signal** (the + `type(x) is C` positive/negative/`@final` families). The silent families + each map to a known pending guard form — boolean connectives in tests, + nested conditions, cross-function `TypeGuard` calls, attribute + narrowing ([TYPEINF-NARROWING-ATTR-CALLS]), and tuple element/length + narrowing — so this number is the ratchet floor, not a claim of + benchmark conformance (that needs the full diagnostics pipeline at the + Integration stage). -## Expression inference {#NARROWPLAN-EXPRESSIONS} +### Stage 2 — expression inference -- [ ] Infer same-module and imported function/method return types. -- [ ] Infer constructor, attribute, subscript, binary/unary, conditional, and +- [x] Infer same-module and imported function/method return types. + — Same-module: unannotated returns synthesize from the body + (`incremental_defs::function_type`) and `x = f()` resolves through the + sibling's declared-or-synthesized return via the backdated + `callable_interface` query. Imported: `param_infer::imported_callable_globals` + maps the cross-module layer's `imported_symbols` (functions → + `Callable[..., R]` from their return annotation, classes → instance form, + variables → their annotation) into engine scope. Builtin METHOD returns + come from the centralized table. +- [x] Infer constructor, attribute, subscript, binary/unary, conditional, and walrus expressions from structured AST data. -- [ ] Centralize builtin constructor/method signatures instead of adding + — All in the bidirectional engine over the ruff AST: constructors + (`Named` callee → instance), plain attribute loads via per-module class + schemas (`class_attribute_interface`, backdated; `Point().x` → `float`), + subscripts (list/dict/tuple-position/str with literal-index precision), + the binary/unary tables, conditional unions, and walrus binding. + `self.attr` assignments in `__init__` extend the schema as a follow-up. +- [x] Centralize builtin constructor/method signatures instead of adding rule-local string tables. + — `crates/basilisk-checker/src/bidir/builtins.rs`: one table for builtin + call returns and `str`/`list`/`dict`/`set` method returns, consumed by + the engine's call synthesis; argument-dependent builtins deliberately + stay `Unknown` rather than guessed. Existing rule-local tables migrate + onto it at the Integration stage. - [ ] Reuse the same inference results for diagnostics, hover, completions, and inlay hints. +- [x] Infer unannotated parameter types from body constraints and call sites + so `BSK-0001` becomes unnecessary where types are recoverable (issue + [#317](https://github.com/MelbourneDeveloper/Basilisk/issues/317)). + — Pure core in `crates/basilisk-checker/src/param_infer.rs`: parameters + bind to input-polarity variables; BODY constraints (passing `p` to a + callee with a declared parameter type) accumulate demands, same-module + CALL SITES accumulate lower bounds, and resolution follows input + polarity (demand wins, else union of flows, else `Unknown` — never a + guess). Wiring into the `BSK-0001` exemption happens at the Integration + stage, where the [TYPEINF-EXCEEDS-REQUIRED] predicate widens in + lockstep with this inference. +- [x] Build the curated container/comprehension/lambda benchmark and the + targeted higher-order (`map`/`filter`/decorators/`ParamSpec`) benchmark. + — `crates/basilisk-checker/tests/inference_corpus_tests.rs`: a ratcheted + precision corpus over the definition-level queries. Equivalence is + mutual assignability PLUS a gradual-honesty check (an `Unknown` answer + never silently matches a concrete expectation — the naive scorer + inflated 23/24; the honest score is **18/24**, `PRECISION_FLOOR = 18`, + up-only). The six misses are the documented gaps: nested-union + normalization, lambda parameter display, and the higher-order family + (`map`/`filter`/decorators) that the generic-constraints stage closes. -## Generic constraints {#NARROWPLAN-CONSTRAINTS} +### Stage 2 — generic constraints - [ ] Collect lower, upper, constrained, default, and expected-return bounds for TypeVars. @@ -41,7 +525,7 @@ environment, expression inferrer, constraint solver, or subtype context. - [ ] Cover constrained/bound TypeVars, PEP 696 defaults, ParamSpec, and TypeVarTuple interactions before wiring the solver into rule decisions. -## Shared subtyping {#NARROWPLAN-SUBTYPING} +### Stage 2 — shared subtyping - [ ] Build a context for nominal class relationships, Protocol members, TypedDict schemas, generic variance, and Callable parameter kinds. @@ -50,18 +534,49 @@ environment, expression inferrer, constraint solver, or subtype context. - [ ] Keep `Any`/`Unknown` gradual behavior and the numeric tower consistent across annotation parsing and inferred types. -## Integration {#NARROWPLAN-INTEGRATION} +### Stage 3 — type-level evaluation groundwork + +- [ ] Build the normalization-by-evaluation engine for type-level functions as + memoized Salsa queries returning whnf types. +- [ ] Enforce fuel/depth bounds and memoization of normalized results. +- [ ] Add the `Divergent`/`@Todo` fallback preserving the gradual guarantee on + truncated evaluation. +- [ ] Add GHC-style (Paterson/Coverage-analogue) acceptance conditions with an + opt-in "undecidable" escape hatch. +- [ ] Represent mapped types as kind `Type → Type` operators and conditional + types as guarded rewrites on assignability, evaluated lazily + (call-by-need). + +### Superiority gate + +- [ ] Build the inference scoreboard harness mirroring `benchmarks/`: pull the + latest official release of each competitor (pyright, mypy, ty, pyrefly, + zuban) every run; write scores to a status file immediately and + unconditionally; gate read-only against the committed baseline. +- [ ] Build the reveal_type-precision corpus + (containers/comprehensions/lambdas/literal-generic precision) and score all + checkers on it. +- [ ] Wire the utahplt/ifT narrowing benchmark, the higher-order corpus, the + gradual-guarantee differential suite, and the incremental-latency + measurement into the scoreboard. +- [ ] Add per-axis ratchet entries: once Basilisk leads an axis, falling + behind any competitor on that axis fails CI; leads only accumulate. +- [ ] Take and hold the lead on **all five axes simultaneously**, with the + 100%/0-FP conformance gate and the speed benchmark gate green in the same + run. +- [ ] Enforce claims discipline: every superiority statement in docs, website, + or marketing traces to the current committed scoreboard run and states the + methodology. + +### Integration and acceptance -- [ ] Introduce each shared component behind existing checker APIs; do not create - an alternate checking mode. +- [ ] Introduce each shared component behind existing checker APIs; do not + create an alternate checking mode. - [ ] Migrate assignment, return, call, and `assert_type` rules incrementally, deleting the replaced local logic in the same change. - [ ] Add spec-ID-linked mutation-resistant tests for each migrated behavior. - -## Acceptance {#NARROWPLAN-ACCEPTANCE} - -- Inference and narrowing have one shared implementation rather than diverging - rule-local approximations. -- Hover/inlay results and checker diagnostics agree for the same expression. -- `make test`, mutation/coverage ratchets, benchmarks for touched hot paths, and - the live 141/141 conformance gate all pass with zero false positives. +- [ ] Verify hover/inlay results and checker diagnostics agree for the same + expression. +- [ ] `make test`, mutation/coverage ratchets, benchmarks for touched hot + paths, and the live 141/141 conformance gate all pass with zero false + positives. diff --git a/docs/plans/NVIM-UPGRADE-PLAN.md b/docs/plans/NVIM-UPGRADE-PLAN.md deleted file mode 100644 index 86550813..00000000 --- a/docs/plans/NVIM-UPGRADE-PLAN.md +++ /dev/null @@ -1,94 +0,0 @@ -# basilisk.nvim — Binary Upgrade Story Plan `[NVIM-UPGRADE]` - -> Spec: [NVIM-BINARY-UPGRADE] in [NEOVIM-SPEC.md](../specs/NEOVIM-SPEC.md). -> Status: core deliverables (1, 2, 3, docs half of 4, 5) **shipped**; the -> Mason registry submission and release-asset follow-ups remain open. - -## Problem - -The `basilisk.nvim` plugin detected a newer release and printed -`[basilisk] update available: → v0.33.0. Run :checkhealth basilisk for details.` -([`binary.lua` `check_for_updates`](../../basilisk.nvim/lua/basilisk/binary.lua)). - -That notice was a **dead end**: `:checkhealth basilisk` only shows a status list, and -there was **no in-editor action to actually install the new version**. Users had no -"normal way" to upgrade the binary from nvim. - -Worse, the download engine itself was broken on macOS: `platform_asset_name()` -asked for `basilisk-aarch64-apple-darwin.tar.gz` but `release.yml` publishes a -`.zip` whose binaries are nested under a `basilisk-darwin/` staging dir — the -downloader silently found no asset ([NVIM-BINARY-UPGRADE-ASSETS]). - -## What already existed (built on it, not duplicated) — Rule: reduce duplication - -- `M.download()` — full working downloader: `platform_asset_name()` → - `fetch_latest_release()` → curl → tar/unzip → chmod. Downloads - `releases/latest`, previously called only as fallback step 7 of `resolve()`. -- `is_newer_version()` / `parse_semver()` — version comparison, unit-tested. -- `check_for_updates()` — the notifier that produces the message on screen. -- The `:Basilisk*` user-command surface in `commands.lua`. - -**The engine was done. The gap was a user-facing action + distribution surfaces + docs.** - -## Deliverables — "all the things people normally do" - -### 1. In-editor self-update — `:BasiliskUpdate` ✅ SHIPPED -- `:BasiliskUpdate` in `commands.lua`, delegating to - [`update.lua`](../../basilisk.nvim/lua/basilisk/update.lua), which reuses - `download()` (no copy-paste of curl/extract logic). -- Downloads into the versioned cache dir (`stdpath("data")/basilisk//`), - sets `binary_path`, force-restarts the LSP client. -- Confirmation UX: `vim.ui.select({"Update now","Later"})` — the real "accept" - step in the TUI ([NVIM-BINARY-UPGRADE-CONFIRM]). -- Refuses gracefully when the resolved binary is a **local dev build** - (`0.0.0-PLACEHOLDER`) or a package-manager install (Homebrew/Scoop/cargo): - tells the user the owning upgrade command instead of clobbering it - ([NVIM-BINARY-UPGRADE-SOURCES]). -- The notification is actionable: `check_for_updates` names the owning - upgrade action (`:BasiliskUpdate`, `brew upgrade basilisk`, …) — never - `:checkhealth` ([NVIM-BINARY-UPGRADE-NOTICE]). - -### 2. `:BasiliskInstall` / bootstrap ✅ SHIPPED -- Installs the binary on first use when nothing is resolvable, surfacing the - auto-download that `resolve()` step 7 already performed but never announced - ([NVIM-BINARY-UPGRADE-INSTALL]). `:checkhealth basilisk` advice now names it. - -### 3. Plugin-manager guidance (how the *Lua* half updates) ✅ SHIPPED -- README (en + zh): copy-paste install specs for lazy.nvim, packer, vim-plug, - vim.pack, plus an Updating section (`:Lazy update` / `:PackerSync` / - `:PlugUpdate` for the plugin; `:BasiliskUpdate` for the binary). -- Same story in `doc/basilisk.txt` (`:h basilisk-binary`) and the website - guide [`/docs/install-neovim/`](../../website/src/docs/install-neovim.md). - -### 4. Distribution surfaces for the *binary* (what CI must publish) -- ✅ **Asset naming verified & fixed client-side**: `platform_asset_name()` now - byte-matches the five archives `release.yml` publishes (Linux `.tar.gz`, - macOS/Windows `.zip`); zip extraction flattens the macOS staging dir and - chmods `basilisk-profiler-helper`. A binary_spec contract test pins the - exact published names ([NVIM-BINARY-UPGRADE-ASSETS]). -- ✅ **Homebrew / Scoop**: `publish-homebrew` / `publish-scoop` jobs in - `release.yml` push the tap/bucket on every tag; the update notice shows - `brew upgrade basilisk` / `scoop update basilisk` for those installs. -- ⬜ **Mason**: submit `basilisk` to the upstream - [mason-registry](https://github.com/mason-org/mason-registry) so - `:MasonInstall basilisk` / `:MasonUpdate` work. External PR — the release - assets it needs already exist. Do not document Mason support until the - registry entry is merged. -- ⬜ **macOS x86_64**: no `x86_64-apple-darwin` release asset is built; - `platform_asset_name()` deliberately returns `nil` there and the flows - advise `cargo install basilisk-cli`. Add the build to `release.yml` if - Intel-mac demand appears. - -### 5. Tests (never fewer failing tests; ratchet up) ✅ SHIPPED -- `binary_spec.lua`: published-asset-name contract, per-OS asset assertions, - `install_source` classification, `upgrade_hint` mapping, actionable-notice - regression (must name `:BasiliskUpdate`, must not name `:checkhealth`), - dev-build silence. -- `update_spec.lua` (new): happy path, decline no-op, already-latest no-op, - Homebrew/cargo/dev refusals, unreachable-GitHub error, install bootstrap, - install-when-present redirect, download-failure report. -- `commands_spec.lua`: `:BasiliskUpdate` / `:BasiliskInstall` registration. - -## Explicitly NOT touched -- The Rust checker / conformance harness — this is plugin + distribution only. -- No conformance ratchet, rule, or `basilisk.json` change (forbidden per CLAUDE.md). diff --git a/docs/plans/ROADMAP-NEXT-STEPS-PLAN.md b/docs/plans/ROADMAP-NEXT-STEPS-PLAN.md index 9198d6ff..83478e0e 100644 --- a/docs/plans/ROADMAP-NEXT-STEPS-PLAN.md +++ b/docs/plans/ROADMAP-NEXT-STEPS-PLAN.md @@ -24,6 +24,15 @@ Current baseline: `zed-industries/extensions`; automation already renders and tests that mirror. - [ ] **`[HUMAN]`** Submit the prepared `basilisk.nvim/lspconfig/basilisk.lua` definition upstream. +- [ ] **`[HYBRID]`** Submit `basilisk` to the upstream + [mason-registry](https://github.com/mason-org/mason-registry) so + `:MasonInstall basilisk` / `:MasonUpdate` work; the release assets it needs + already exist ([NVIM-BINARY-UPGRADE]). Do not document Mason support until + the registry entry is merged. +- [ ] **`[HUMAN]`** Decide whether `release.yml` should add an + `x86_64-apple-darwin` build if Intel-mac demand appears; until then + `platform_asset_name()` deliberately returns `nil` there and the flows + advise `cargo install basilisk-cli` ([NVIM-BINARY-UPGRADE-ASSETS]). - [ ] **`[HYBRID]`** Confirm the Neovim mirror tag flow on a release and decide whether the mirror also needs GitHub Release objects; plugin managers consume tags directly. @@ -37,14 +46,16 @@ complete: - [LSP-CONFIGURATION-EDITOR-PLAN.md](LSP-CONFIGURATION-EDITOR-PLAN.md) — typed, transactional rule configuration and the thin VS Code editor. -- [LSP-FORMATTING-PLAN.md](LSP-FORMATTING-PLAN.md) — release provenance, client - opt-in UX, and the `basilisk format` CLI. +- [LSP-FORMATTING-PLAN.md](LSP-FORMATTING-PLAN.md) — the VS Code + default-formatter opt-in and published-artifact verification. - [EXTENSION-ACTIVITY-PANEL-PLAN.md](EXTENSION-ACTIVITY-PANEL-PLAN.md) — real server-side feature toggles plus remaining accessibility and performance work. - [CHECKER-ELIMINATE-LINE-SCANNING-PLAN.md](CHECKER-ELIMINATE-LINE-SCANNING-PLAN.md) — replace Python-structure reconstruction from raw lines with Ruff AST data. - [CHECKER-TYPE-NARROWING-INFERENCE-PLAN.md](CHECKER-TYPE-NARROWING-INFERENCE-PLAN.md) - — non-conformance inference and flow-analysis improvements. + — the bidirectional/constraint-based inference-engine upgrade (fixes + inference-gap issues like #317, PEP 827 readiness), flow analysis, and + shared subtyping. - [LSP-AI-PLAN.md](LSP-AI-PLAN.md), [COMPILER-ARCHITECTURE-PLAN.md](COMPILER-ARCHITECTURE-PLAN.md), and [CHECKER-ADVANCED-FEATURES-PLAN.md](CHECKER-ADVANCED-FEATURES-PLAN.md) — larger, diff --git a/docs/specs/CHECKER-ARCHITECTURE-SPEC.md b/docs/specs/CHECKER-ARCHITECTURE-SPEC.md index 79cf7765..8503042f 100644 --- a/docs/specs/CHECKER-ARCHITECTURE-SPEC.md +++ b/docs/specs/CHECKER-ARCHITECTURE-SPEC.md @@ -60,14 +60,20 @@ def greet(name): return f"Hello, {name}" # Passes check always; fires BSK-0002 under analyze once configured +# (the returned call is not inferable — a literal/f-string return would +# infer the type and stay silent, [TYPEINF-EXCEEDS-REQUIRED]) def greet(name: str): - return f"Hello, {name}" + return build_greeting(name) # OK under any configuration def greet(name: str) -> str: return f"Hello, {name}" ``` +Even when enabled, the require-annotation rules fire only where the type cannot +be inferred — see +[TYPEINF-EXCEEDS-REQUIRED](CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-EXCEEDS-REQUIRED). + #### `Any` Is Explicit, Never Implicit {#CHKARCH-STRICTNESS-ANY} ```python @@ -294,7 +300,7 @@ Basilisk's **target** is 100% conformance with the Python typing specification. ### Type Inference Engine {#CHKARCH-INFERENCE} -When the require-annotation rules (`BSK-0001`/`BSK-0002`/`BSK-0004`) have config entries, explicit annotations are required on public APIs; local variable types are always inferred: +When the require-annotation rules (`BSK-0001`/`BSK-0002`/`BSK-0004`) have config entries, explicit annotations are required on public APIs **only where the type cannot be inferred** ([TYPEINF-EXCEEDS-REQUIRED](CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-EXCEEDS-REQUIRED)); local variable types are always inferred: ```python def process(items: list[str]) -> int: @@ -329,24 +335,24 @@ Full support for: ### Target Version and Platform {#CHKARCH-VERSION-TARGET} -Every rule runs against the **configured** target Python version — never a -hardcoded constant (issue #93). +A rule consults the project's Python version only when the maintained typing +specification, an accepted PEP, or Python language semantics makes its result +version-dependent. A version-independent rule must not branch on a Python +release. Basilisk has no canonical Python version. - `BasiliskConfig.python_version` / `python_platform` (from `pyproject.toml` `[tool.basilisk]` `python-version`/`python-platform`) parse into a typed `CheckContext { target_version: (major, minor), target_platform }` (`crates/basilisk-checker/src/context.rs`). -- The centralized default is `DEFAULT_TARGET_VERSION = (3, 12)` — the **only** - place the default version constant lives. A malformed version string falls - back to the default rather than panicking or disabling gating. - When the checker config does not pin a version, the CLI and LSP detect it from project files per [`[LSPUV-PYTHON-VERSION-RESOLUTION-ORDER]`](LSP-UV-INTEGRATION-SPEC.md): `.python-version` → `[project].requires-python` lower bound → `uv.lock` `requires-python` lower bound (`basilisk_uv::python_version::resolve_target_python_version`). - `rules::run_all(module, ctx)` threads the context into every - `Rule::check(module, ctx, diagnostics)` — no rule may reference a literal - target version. + `Rule::check(module, ctx, diagnostics)`. Feature-version boundaries come from + their governing PEP or Python language rule, never from a book-wide or + product-wide support target. #### Version/Platform Narrowing {#CHKARCH-VERSION-NARROWING} @@ -994,11 +1000,10 @@ is **never read or written** How the file is found and how multiple ancestor tables combine is specified in [Configuration Discovery](#CHKARCH-CONFIG-DISCOVERY). -Canonical TOML example: +Project TOML example: ```toml [tool.basilisk] -python-version = "3.12" python-platform = "All" # Default: check for all platforms stub-paths = ["stubs/"] # resolution step 1: prepend extra .pyi stub dirs # typeshed-path = "typeshed-x" # resolution step 3: provide canonical stdlib stubs @@ -1135,8 +1140,8 @@ Every error has at least one associated code action: | Error | Quick Fix | |---|---| -| BSK-0001 (missing param type) | Insert `: ` | -| BSK-0002 (missing return type) | Insert `-> ` | +| BSK-0001 (missing param type) | Insert `: Any` (the rule only fires where no type is inferable, [TYPEINF-EXCEEDS-REQUIRED](CHECKER-TYPE-INFERENCE-SPEC.md#TYPEINF-EXCEEDS-REQUIRED)) | +| BSK-0002 (missing return type) | Insert `-> Any` (same rationale) | | enums_behaviors (mutation of immutable param) | Add `InOut` annotation | | dataclasses_order (implicit coercion) | Wrap in explicit conversion | diff --git a/docs/specs/CHECKER-TYPE-INFERENCE-SPEC.md b/docs/specs/CHECKER-TYPE-INFERENCE-SPEC.md index 876efa58..87c591dc 100644 --- a/docs/specs/CHECKER-TYPE-INFERENCE-SPEC.md +++ b/docs/specs/CHECKER-TYPE-INFERENCE-SPEC.md @@ -2,8 +2,6 @@ Basilisk combines conservative shared inference with focused typing-rule algorithms. The default configuration follows the typing specification; optional house rules can require or discourage annotations without changing PEP behavior (see [TYPEINF-REDUNDANT]). -> **Canonical Python version**: 3.12 -> > **Authoritative references**: [PEP 484](https://peps.python.org/pep-0484/), [PEP 526](https://peps.python.org/pep-0526/), [Python Typing Spec](https://typing.python.org/en/latest/spec/), [Python Typing Conformance Suite](https://github.com/python/typing/tree/main/conformance) > > **Implementation**: Core inference engine (`inference.rs`, `collection_inference.rs`, `types.rs`, `types_parsing.rs`) is wired into rules E0011, E0013, E0014, E0120, and W0050. @@ -83,6 +81,9 @@ collection syntax produces an `InferredType`; unsupported expressions produce focused rule/resolver paths, not in a general `infer_type(expr, expected)` engine. Consolidating those paths is tracked in [NARROWPLAN-INFERENCE](../plans/CHECKER-TYPE-NARROWING-INFERENCE-PLAN.md#NARROWPLAN-INFERENCE). +The target architecture that supersedes this conservative core — bidirectional +checking over a subtype-constraint solver — is specified in +[TYPEINF-TARGET](#TYPEINF-TARGET). ## Variable Type Inference {#TYPEINF-VARS} @@ -168,42 +169,61 @@ The walrus operator `:=` assigns the value and the **expression type equals the ### Parameters {#TYPEINF-FUNC-PARAMS} When the opt-in annotation policy is enabled, an unannotated non-receiver parameter fires -`BSK-0001`. The unconfigured PEP default does not require annotations merely for style. +`BSK-0001` **only when the current engine cannot infer its type** — see +[TYPEINF-EXCEEDS-REQUIRED](#TYPEINF-EXCEEDS-REQUIRED) for the governing +principle. The unconfigured PEP default does not require annotations merely for style. ```python -def process(data): # BSK-0001: parameter 'data' has no type annotation +def process(data): # BSK-0001: nothing to infer 'data' from pass def process(data: bytes): # ✓ pass ``` -The only parameters inferred rather than annotated are: +Parameters inferred rather than annotated: - `self` in instance methods → inferred as `Self` (the containing class bound to `Self`) - `cls` in class methods → inferred as `type[Self]` - `__` (positional-only placeholder) → accepted as `Any` for compatibility +- parameters whose literal default determines the type — + [TYPEINF-FUNC-DEFAULTS](#TYPEINF-FUNC-DEFAULTS) > **Authority**: [PEP 673 (Self type)](https://peps.python.org/pep-0673/) for `Self` semantics. ### Default Parameters {#TYPEINF-FUNC-DEFAULTS} -A default expression does not become a declared parameter type. Under the opt-in annotation -policy the parameter still needs an annotation. +A type-determining literal default infers the parameter type, so `BSK-0001` +MUST NOT fire there — demanding an annotation the engine already knows is +redundant. A default that does **not** determine the type (`None`, empty +containers, calls, lambdas, arbitrary expressions) still requires one. ```python -def connect(timeout=30): # BSK-0001 — annotation required even with default +def connect(timeout=30): # ✓ — inferred as int from the default + pass + +def connect(timeout: int = 30): # ✓ — explicit annotation always accepted pass -def connect(timeout: int = 30): # ✓ +def connect(timeout=None): # BSK-0001 — None does not determine T | None + pass + +def connect(timeout=make_default()): # BSK-0001 — call results are not inferable pass ``` +The exemption is exactly as strong as the current engine +(`rhs_fully_determines_type` in `crates/basilisk-checker/src/inference.rs`): +scalar literals and non-empty containers of type-determining elements qualify; +nothing else does. + ### Return Types {#TYPEINF-FUNC-RETURN} Focused resolver/checker paths infer simple return expressions and validate them against a -declared return type. The opt-in annotation policy can separately require a public return -annotation; there is no universal PEP-default requirement. +declared return type. The opt-in annotation policy can separately require a return +annotation, but `BSK-0002` fires **only when the current engine cannot infer the +return type** ([TYPEINF-EXCEEDS-REQUIRED](#TYPEINF-EXCEEDS-REQUIRED)); there is +no universal PEP-default requirement. ```python def f(x: int) -> int | str: # ✓ — annotation matches inference @@ -212,6 +232,23 @@ def f(x: int) -> int | str: # ✓ — annotation matches inference return "negative" # str ``` +A function is exempt from `BSK-0002` when every `return` is bare or carries a +type-determining literal, or the body has no `return` at all (inferred `None`): + +```python +def answer(): # ✓ — inferred as int + return 42 + +def log_it(msg: str): # ✓ — no return: inferred as None + print(msg) + +def fetch(url: str): # BSK-0002 — call result is not inferable + return download(url) + +def numbers(): # BSK-0002 — Generator[...] is not inferable from returns + yield 1 +``` + If the annotated return type is **narrower** than the inferred union, Basilisk emits `returns_compatibility` (return type mismatch). If the function body has **no reachable `return` statement** and does not raise, the inferred return type is `None`. If the function always raises, the inferred return type is `Never`. @@ -840,9 +877,26 @@ With the `strictness` tag enabled, a module/class variable assigned a lambda wit annotation emits `BSK-0040`. The diagnostic is an annotation nudge, not evidence that lambda parameters were otherwise contextually inferred. -### Annotation Required, Not Optional {#TYPEINF-EXCEEDS-REQUIRED} +### Annotation Required Only Where Inference Fails {#TYPEINF-EXCEEDS-REQUIRED} -When the require-annotation house rules are enabled, missing public-API annotations are diagnostics. They are not part of the unconfigured PEP default. +When the require-annotation house rules are enabled, missing public-API annotations are +diagnostics. They are not part of the unconfigured PEP default. + +**Inference-first principle:** a missing-annotation rule MUST NOT fire where the +current engine already infers the type — demanding a type the checker knows is +redundant noise (and contradicts `BSK-0050`, which flags exactly such +annotations as redundant). Each rule fires only where inference fails: + +| Rule | Exempt (inferable today) | Still fires (not inferable today) | +|---|---|---| +| `BSK-0001` (parameter) | `self`/`cls`; type-determining literal default ([TYPEINF-FUNC-DEFAULTS](#TYPEINF-FUNC-DEFAULTS)) | no default; `None`/empty-container/call/lambda default | +| `BSK-0002` (return) | all returns bare/literal, or no returns → `None` ([TYPEINF-FUNC-RETURN](#TYPEINF-FUNC-RETURN)) | any uninferable return; generators | +| `BSK-0003` (module var) | any RHS except empty containers / `None` | `[]`, `{}`, `None` | +| `BSK-0005` (class attr) | scalar/tuple literal RHS | everything else | +| `BSK-0004` (`*args`/`**kwargs`), `BSK-0040` (lambda) | — (nothing inferable today) | always | + +The exemptions are exactly as strong as today's inference and MUST widen as the +engine grows ([TYPEINF-TARGET](#TYPEINF-TARGET)) — never the reverse. --- @@ -860,3 +914,560 @@ Shared inference lives in `basilisk-checker`: The LSP analysis path is memoized by the Salsa database described in [CHKARCH-INCREMENTAL-SALSA](CHECKER-ARCHITECTURE-SPEC.md#CHKARCH-INCREMENTAL-SALSA). A separate content-addressed cache serves opt-in cross-session CLI reuse. + +--- + +## Target inference architecture {#TYPEINF-TARGET} + +This section specifies the design of the next-generation inference engine. +The current conservative core ([TYPEINF-ALGO](#TYPEINF-ALGO)) is superseded by +this design; delivery is staged in +[NARROWPLAN-INFERENCE](../plans/CHECKER-TYPE-NARROWING-INFERENCE-PLAN.md#NARROWPLAN-INFERENCE). +The design is oriented toward +[PEP 827 – Type Manipulation](https://peps.python.org/pep-0827/) — the engine +must be powerful enough to host PEP 827-style conditional/mapped types — but +implementing PEP 827 itself is out of scope; only the inference-engine +groundwork is specified here. Every claim below is grounded in the research +survey in [TYPEINF-RESEARCH](#TYPEINF-RESEARCH). The outcome requirement — +inference measurably superior to every officially-recognized competitor, +proven and held by a self-measured ratcheted scoreboard — is defined in +[NARROWPLAN-SUPERIORITY](../plans/CHECKER-TYPE-NARROWING-INFERENCE-PLAN.md#NARROWPLAN-SUPERIORITY). + +### Bidirectional core {#TYPEINF-TARGET-BIDIRECTIONAL} + +Bidirectional typing (Pierce–Turner *Local Type Inference*; +Dunfield–Krishnaswami survey — see +[TYPEINF-RESEARCH-THEORY](#TYPEINF-RESEARCH-THEORY)) is the backbone, layered +over a subtyping-constraint solver rather than global Hindley–Milner. Every +AST expression node supports two modes: + +- `synth(e) → τ` — *synthesis*: infer a type bottom-up; +- `check(e, τ)` — *checking*: verify against an expected type propagated + top-down. + +`check` is the primary driver, so expected-type context flows into container +literals, comprehensions, lambda parameters, and higher-order call arguments. +This is deliberately where Basilisk aims to beat Pyrefly, whose context +propagation is heuristic ("take one peek ahead"), and ty, which only recently +added outside-in inference (see +[TYPEINF-RESEARCH-COMPETITORS](#TYPEINF-RESEARCH-COMPETITORS)). + +### Constraint architecture {#TYPEINF-TARGET-CONSTRAINTS} + +A **two-stage constraint architecture** (Pottier–Rémy): a +constraint-generation pass over the AST produces subtype constraints +(`τ₁ <: τ₂`), and a separate solver resolves them. + +- Type variables are **bounded and polar**: each carries explicit lower/upper + bounds (like Pyright's type intervals and Pyrefly's `Var`), with the + input/output polarity discipline borrowed from Dolan's algebraic subtyping + and Parreaux's Simple-sub — **without** committing to full biunification + (see [TYPEINF-RESEARCH-THEORY](#TYPEINF-RESEARCH-THEORY) and the transfer + risk noted there). +- **Generalization is deferred**: infer `list[Var{lower=Literal[1]}]` and + settle `Var` only at first constraining use. This preserves + literal/generic precision (`list[int]` vs `list[Literal[1]]`) instead of + Pyrefly's eager `Literal[1] → int` widening, while staying + gradual-guarantee-safe. + +### Gradual guarantee {#TYPEINF-TARGET-GRADUAL} + +The gradual guarantee (Siek, Vitousek, Cimini, Boyland, *Refined Criteria for +Gradual Typing* — see [TYPEINF-RESEARCH-GRADUAL](#TYPEINF-RESEARCH-GRADUAL)) +is an explicit, testable invariant: consistency (`~`) replaces subtyping where +`Any` is involved, and **removing an annotation must never introduce a new +static error**. This forbids inferring over-precise types from unannotated +code (Pyrefly's known risk) and is enforced by a differential test suite that +strips annotations and asserts no new errors. This matches ty's design +position and composes with the conservative-`Unknown` behavior in +[TYPEINF-EXCEEDS-NOUNKNOWN](#TYPEINF-EXCEEDS-NOUNKNOWN). + +### Flow-sensitive narrowing {#TYPEINF-TARGET-NARROWING} + +Occurrence typing (Tobin-Hochstadt–Felleisen; Castagna et al.) is implemented +as **intersection-and-negation-based narrowing** (ty's model) over a +Salsa-backed use-def map, with `phi`/join operators at control-flow merges +(Pyrefly's binding-DSL idea). Guards: `isinstance`, `issubclass`, `is None`, +truthiness, `==`/`in` against literals, `hasattr` (intersection with a +synthetic protocol), `TypeGuard`/`TypeIs` (PEP 647/742), and +exhaustiveness/implied-else. The **attribute-narrowing-across-calls** +soundness tradeoff is decided explicitly (per Pyrefly's lesson): default to +the usable behavior, configurable for security-sensitive users. Reachability +analysis is **inference-driven** (ty's model), not pattern-matched idioms. + +#### Attribute narrowing across calls {#TYPEINF-NARROWING-ATTR-CALLS} + +**Decision (Stage 2):** attribute narrowing (`if x.attr is not None:`) +**survives intervening calls by default.** Any call *could* re-enter and +mutate the attribute, so this default is deliberately unsound — but treating +every call as an invalidation discards nearly every attribute narrow in real +code, which is why the usable behavior is the ecosystem norm. Projects that +want the sound-but-strict behavior set +`narrow-attributes-across-calls = false` under `[tool.basilisk]` +(`BasiliskConfig::narrow_attributes_across_calls`; `None`/unset means the +usable default `true`). The knob is wired through configuration now so the +semantics are user-visible from the first release that narrows attributes; +the checker consults it wherever attribute narrowing is applied. + +### Incrementality {#TYPEINF-TARGET-INCREMENTAL} + +Salsa with **definition-level and expression-level tracked queries** (ty's +model), not file-level invalidation (Pyrefly's). A compact per-module +**interface/signature query** is the cross-file dependency boundary +(Pyrefly's "Interface" idea), giving early cutoff and preventing +whole-program invalidation. Cycles are modeled with **fixpoint iteration +seeded by a divergent/bottom sentinel** and a hard iteration cap (ty's +`Divergent`; Pyrefly's thunks). If fine-grained queries cause memory blowup +on large targets, AST/binding eviction (keep only interfaces) sits behind the +query layer — see the threshold in +[NARROWPLAN-STAGES](../plans/CHECKER-TYPE-NARROWING-INFERENCE-PLAN.md#NARROWPLAN-STAGES). + +### Type-level evaluation (PEP 827 readiness) {#TYPEINF-TARGET-TYPELEVEL} + +Type-level computation with conditional/mapped types is Turing-complete +territory (proven for both TypeScript and Python type hints — see +[TYPEINF-RESEARCH-TYPELEVEL](#TYPEINF-RESEARCH-TYPELEVEL)), so the only safe +engineering path is **bounded evaluation**: a call-by-need +normalization-by-evaluation engine over type-level functions, built as +memoized Salsa queries returning types in weak-head normal form (whnf), with: + +- **fuel/depth bounds** (TypeScript's instantiation-depth model); +- **memoization** of normalized results; +- a **`Divergent`/`@Todo`-style fallback** that preserves the gradual + guarantee when evaluation is truncated; +- **GHC-style acceptance conditions** (Paterson/Coverage analogues) that + statically reject obviously-nonterminating type-level definitions, with an + opt-in "undecidable" escape hatch. + +Mapped types are **kind `Type → Type` operators**; conditional types are +guarded rewrites keyed on a consistency/assignability check (`IsAssignable` +in PEP 827), evaluated lazily so unused branches never diverge. Because +bounded evaluation cannot be complete, some legitimate type-level programs +will hit the bound — an inherent limitation, not an implementation gap. + +--- + +## Research grounding {#TYPEINF-RESEARCH} + +The literature survey and competitor analysis behind +[TYPEINF-TARGET](#TYPEINF-TARGET). + +### Key findings {#TYPEINF-RESEARCH-FINDINGS} + +1. **Global HM/Algorithm W is the wrong foundation for Python.** Damas–Milner + inference relies on unification (equational constraints) and + let-generalization, and it does not natively accommodate subtyping, + mutation (the value restriction problem), or gradual `Any`. Python has all + three. Every serious Python checker (Pyright, mypy, Pyrefly, ty) instead + uses **bidirectional checking plus local subtype-constraint solving**. Neil + Mitchell (Pyrefly) explicitly contrasts Python's origin with "let's pick a + really nice structure like Hindley-Milner"; Python "grafts types back onto" + existing programs, so subtype constraints, not unification, are the core. + +2. **Bidirectionality is the consensus architecture, and it is where inference + quality is won or lost.** Pierce & Turner introduced local type inference + with two modes — *synthesis* (infer a type bottom-up) and *checking* + (verify against an expected type propagated top-down). Pyright, Pyrefly, + and ty all use it. Crucially, ty only recently added bidirectional + ("outside-in") inference — for most of its life it did pure "inside-out" + inference — and Pyrefly's context propagation is present but heuristic + ("take one peek ahead"). This is Basilisk's single biggest opportunity. + +3. **Algebraic subtyping (MLsub / Dolan–Mycroft; Parreaux's Simple-sub) is the + state of the art for inference *with* subtyping** and is directly relevant, + but adopting it wholesale for Python is risky. It gives principal types and + compact type simplification via biunification and polar (input/output) + types, and Parreaux's Simple-sub "can be implemented efficiently in under + 500 lines of code (including parsing, simplification, and pretty-printing)" + (Parreaux, ICFP 2020, ). However, Python's + nominal classes, invariant generics, overloads, and gradual `Any` do not + map cleanly onto MLsub's structural, fully-inferred model. The + recommendation is to **borrow its ideas (polar types, bounded type + variables, constraint simplification) selectively** rather than commit to + full biunification. + +4. **Pyrefly's concrete design is now well-documented and beatable on specific + axes.** Pyrefly compiles each file through six phases (Code → AST → Exports + → Bindings → Answers → Interface), works at **file-level** incremental + granularity (not expression/definition level), uses subtype constraints + with unification variables ("Vars") and cycle-breaking thunks, and + deliberately chooses **usability over soundness** (e.g., it allows + attribute narrowing across function calls that Pyre rejected). Its + inference is "aggressive": it infers concrete unions in unannotated code + and reports errors there, at the cost of false positives. Pyrefly's + official site advertises type checking "over 1.85 million lines of code per + second" (tested on Meta infrastructure, 166 cores / 228 GB RAM), and Meta + reports re-checking Instagram's ~20M-line codebase in ~13.4 seconds — in + Mitchell's words, "projects that used to take 14 minutes to type check that + are now down to more like five seconds." + +5. **ty (Astral) is the philosophical opposite and the more sophisticated + incremental engine.** ty is built on **Salsa with fine-grained, + multi-granularity queries** (scope-, definition-, expression-, and + statement-level), uses **fixpoint iteration with a `Divergent` type** for + recursive cycles, models types as a single `Type` enum with a nested + `DynamicType` (Any/Unknown/Todo/Divergent), has **first-class intersection + types** driving narrowing, does **reachability analysis based on type + inference** (not pattern matching), and enforces the **gradual guarantee**. + Its generics use an explicit constraint solver + (`SpecializationBuilder`/`ConstraintSetBuilder`). Astral reports that + "after editing a load-bearing file in the PyTorch repository, ty recomputes + diagnostics in 4.7ms: 80x faster than Pyright (386ms) and 500x faster than + Pyrefly (2.38 seconds)." + +6. **Type-level computation is provably unbounded.** TypeScript's type system + is Turing-complete (Microsoft/TypeScript issue #14833), and Python's type + hints are Turing-complete: Ori Roth (arXiv:2208.14755, submitted 31 Aug + 2022; published ECOOP 2023, LIPIcs vol. 263, pp. 44:1–44:15) states, + "Grigore showed that Java generics are Turing complete by describing a + reduction from Turing machines to Java subtyping. We apply Grigore's + algorithm to Python type hints and deduce that they are Turing complete." + PEP 827 explicitly models conditional/mapped types on TypeScript. Therefore + Basilisk's type-level evaluator must be a bounded normalizer. The proven + engineering strategies come from Haskell type classes (GHC's Paterson + Conditions and Coverage Condition ensuring instance-resolution termination, + relaxed by `UndecidableInstances`) and from checkers' recursion/fuel + limits. + +7. **The gradual guarantee is a precise, mechanizable specification** (Siek, + Vitousek, Cimini, Boyland, SNAPL 2015): consistency (`~`) replaces + subtyping where `Any` is involved, and adding precision to annotations must + never turn a well-typed program ill-typed (static guarantee) nor change its + behavior except to surface errors (dynamic guarantee). ty treats this as a + design invariant; Basilisk should encode it as a differential test suite. + +### Foundational type-inference theory {#TYPEINF-RESEARCH-THEORY} + +**Hindley–Milner / Algorithm W.** + +- Robin Milner, "A Theory of Type Polymorphism in Programming," *JCSS* 17(3), + 1978. Introduces Algorithm W and unification-based let-polymorphism. +- Luis Damas & Robin Milner, "Principal Type-Schemes for Functional Programs," + POPL 1982, pp. 207–212. — + establishes principal types for the HM system. + +Why HM is a poor global fit for Python: (a) **Subtyping.** HM unification +solves *equality* constraints (`τ₁ = τ₂`); subtyping requires *inequality* +constraints (`τ₁ <: τ₂`) with upper/lower bounds, which unification cannot +express without extension. Adding subtyping to HM is a decades-long research +problem (Aiken & Wimmers 1993; Fuh & Mishra 1988; Mitchell 1991; Pottier 2001; +Dolan & Mycroft 2017). (b) **Mutation.** Let-generalization is unsound with +mutable references (the value restriction); Python is pervasively mutable, and +invariant containers (`list[T]`) make naive generalization wrong — exactly the +`list[Literal[1]]` vs `list[int]` problem Pyrefly demonstrates. (c) **Gradual +types.** `Any` is neither a top nor bottom type under subtyping; it +participates in *consistency*, not subtyping, so it cannot be modeled as an HM +type variable. + +**Bidirectional typing.** + +- Benjamin C. Pierce & David N. Turner, "Local Type Inference," POPL 1998, + pp. 252–265 (); full version *ACM + TOPLAS* 22(1):1–44, Jan 2000 (). + PDF: . Recovers + annotations using only *locally* adjacent syntax-tree information; solves + type arguments in applications via a local constraint solver over + upper/lower bounds, and propagates expected types downward into + abstractions. +- Jana Dunfield & Neel Krishnaswami, "Bidirectional Typing," *ACM Computing + Surveys* 54(5), Article 98, May 2021, 38 pp. + . The canonical survey: *checking* mode + supports features whose full inference is undecidable; *synthesis* reduces + annotation burden; the split improves **error locality**. Documents the + "Pfenning recipe" for deriving bidirectional rules. +- Jana Dunfield & Neelakantan R. Krishnaswami, "Complete and Easy + Bidirectional Typechecking for Higher-Rank Polymorphism," ICFP 2013, + pp. 429–442. . Shows how a small, + implementable bidirectional algorithm with ordered existential-variable + contexts scales to higher-rank (rank-N) polymorphism — directly relevant to + Python's `Callable`/`ParamSpec` higher-order types. + +**Constraint-based inference.** + +- Martin Odersky, Martin Sulzmann & Martin Wehr, "Type Inference with + Constrained Types" (HM(X)), *Theory and Practice of Object Systems* + 5(1):35–55, 1999. Parameterizes HM over a constraint domain X (subtyping, + records, type classes). +- François Pottier & Didier Rémy, "The Essence of ML Type Inference," + Chapter 10 of *Advanced Topics in Types and Programming Languages* + (B. Pierce, ed.), MIT Press, 2005, pp. 389–489. + PDF: . The + definitive treatment of **constraint generation + constraint solving as a + two-stage architecture**, with a built-in binary subtyping predicate `≤`. + This two-stage separation (generate constraints in one AST pass, solve + separately) is the architecture Basilisk adopts + ([TYPEINF-TARGET-CONSTRAINTS](#TYPEINF-TARGET-CONSTRAINTS)). +- Stephen Dolan, "Algebraic Subtyping," PhD dissertation, University of + Cambridge, 2017. And Stephen Dolan & Alan Mycroft, "Polymorphism, Subtyping, + and Type Inference in MLsub," POPL 2017, pp. 60–72. + . Combines subtyping with ML + polymorphism while keeping **principal types** and compact types, via + **biunification** over **polar types** (strict separation of input/output + types) and type simplification exploiting connections to regular-language + algebra. +- Lionel Parreaux, "The Simple Essence of Algebraic Subtyping: Principal Type + Inference with Subtyping Made Easy (Functional Pearl)," *PACMPL* 4(ICFP), + Article 124, Aug 2020, 28 pp. . + PDF: . Reformulates MLsub as + **Simple-sub**, "implemented efficiently in under 500 lines of code + (including parsing, simplification, and pretty-printing)," without + bisubstitution/abstract algebra — the practical entry point if Basilisk + adopts algebraic-subtyping ideas. Follow-on work extends it (MLstruct, + OOPSLA 2022, for a Boolean algebra of structural types; "When Subtyping + Constraints Liberate," POPL 2024, for first-class polymorphism). + +### Inference for dynamic and gradual languages {#TYPEINF-RESEARCH-GRADUAL} + +- Jeremy G. Siek & Walid Taha, "Gradual Typing for Functional Languages," + Scheme and Functional Programming Workshop, 2006, pp. 81–92. Introduces the + **consistency relation** (`~`) and the gradually typed lambda calculus + (GTLC): `Any` (written `?`/`⋆`) is consistent with every type but subtyping + is not transitive through it. +- Jeremy G. Siek, Michael M. Vitousek, Matteo Cimini & John Tang Boyland, + "Refined Criteria for Gradual Typing," SNAPL 2015, LIPIcs vol. 32, + pp. 274–293. . Defines the + **gradual guarantee** (static + dynamic). Consistency-vs-subtyping + distinction: a gradual type system replaces `<:` with consistency `~` (or + "consistent subtyping"), so `int ~ Any` and `Any ~ str` but `int` is not + `~` `str`. For an inference engine, the guarantee demands: **removing an + annotation must never introduce a new static error**, which forbids + inferring over-precise types from unannotated code (exactly Pyrefly's + risk). +- Sam Tobin-Hochstadt & Matthias Felleisen, "Logical Types for Untyped + Languages," ICFP 2010, pp. 117–128. + . + PDF: . Formalizes + **occurrence typing**: predicates in test positions (`isinstance`, + `is None`) produce *propositions* about variables that refine types in each + control-flow branch. Foundational for Typed Racket and for all Python + narrowing. Newer line: Giuseppe Castagna et al., "On type-cases, union + elimination, and occurrence typing," *PACMPL* 2022, + , and "Revisiting occurrence typing," + *Sci. Comput. Program.* 2022, recast narrowing via set-theoretic + union/intersection/negation types — the same intersection-based approach ty + uses. +- Tobias Lindahl & Konstantinos Sagonas, "Practical Type Inference Based on + Success Typings," PPDP 2006, pp. 167–178. + . The **"never wrong" philosophy**: + success typings over-approximate the set of terms that *can* succeed, so the + tool (Dialyzer) reports only *definite* errors — no false positives. This is + the correct model for the *optional/gradual* subset of Basilisk's + diagnostics: report an error only when *no* instantiation could succeed. +- Python-specific / dynamic-language inference context: Giuseppe Castagna, + Mickaël Laurent & Kim Nguyễn, "Polymorphic Type Inference for Dynamic + Languages," *PACMPL* 8(POPL):1179–1210, 2024, + + (arXiv: ). ML-based inference (Typilus, + TypeWriter) is tangential and not recommended as an architecture. + +### Type-level computation, conditional/mapped types, decidability {#TYPEINF-RESEARCH-TYPELEVEL} + +- **PEP 827 – Type Manipulation** () + proposes type-level introspection and construction "inspired largely by + TypeScript's conditional and mapped types," using `typing.IsAssignable`, + "Type Booleans," conditional type expressions, and a `RaiseError` primitive + for custom compile-time errors. It is intended to be **fully statically + checkable** (a mypy proof-of-concept is referenced). The Vercel write-up + "Advancing Python typing" + () frames it as giving + Python "a programmable core." +- **Turing-completeness / undecidability.** "TypeScript's Type System is + Turing Complete," microsoft/TypeScript issue #14833 + (). Ori Roth, "Python + Type Hints are Turing Complete," arXiv:2208.14755, 2022 + (; ECOOP 2023, LIPIcs vol. 263, + pp. 44:1–44:15) — proves it via nominal subtyping with variance (building on + Grigore, "Java Generics are Turing Complete," POPL 2017). Consequence: + **type-level evaluation cannot be both complete and guaranteed-terminating**; + Basilisk must bound it. +- **Termination strategies from Haskell type classes.** GHC User's Guide + §6.8.8, "Instance declarations and resolution" + (): + the **Paterson Conditions** (each constraint must have no type variable + occurring more often than in the head, must have strictly fewer + constructors+variables than the head, and must mention no type functions) + plus the **Coverage Condition** ensure instance resolution terminates; + `UndecidableInstances` lifts these and can loop. This is the model for + bounding conditional-type recursion. +- **Higher-kinded types / type operators.** Mapped types are type-level + functions (kind `Type → Type`), i.e., System Fω operators. Evaluating them + requires **normalization to weak-head normal form (whnf)**; decidability of + Fω type equality rests on strong normalization of the type-operator + calculus, which the general recursive PEP 827 setting does *not* enjoy — + hence bounded evaluation. +- **Practical fast+terminating strategies:** memoization of normalized types + (Salsa queries are ideal), depth/fuel limits (TypeScript caps instantiation + depth and type-instantiation count; ty falls back to a widened type "after a + certain number of iterations"), and normalization to whnf with structural + caching. ty's `Divergent` type is exactly this fallback made explicit. + +### How existing Python checkers do inference {#TYPEINF-RESEARCH-COMPETITORS} + +**Pyrefly (Meta, Rust, MIT).** + +- Announcement: "Introducing Pyrefly," Engineering at Meta, 15 May 2025 + (). + Clean-slate successor to OCaml Pyre; aggressive inference of returns and + local variables. +- Design detail (authoritative): Neil Mitchell, "Pyrefly: Type Checking 1.8 + Million Lines of Python Per Second," Jane Street Tech Talk + (). Six-phase per-file + pipeline: **Code → AST → Exports → Bindings → Answers → Interface**. The + **Bindings** phase desugars all control flow into a key→value map (with + `phi` join operators for control-flow merges and narrowing operators), + effectively a DSL for type checking. The **Answers** phase solves + per-binding, special-casing every Python type; recursion and generics are + handled with **`Var`** unification variables and **thunks** for cycles. + Incrementality is **file-level** ("if you press a keystroke, we invalidate + that entire file"); cycles up to depth 5 are re-checked incrementally before + falling back to whole-cycle invalidation. Constraint solving is "almost + always this type must be a subtype of this type," with unions making it "a + bit tricky"; Mitchell notes intersection types would push toward "SAT + solving" for subset checks. On the `x=[]; x.append(1)` example, Pyrefly + peeks one use ahead and infers `list[int]`, generalizing `Literal[1]` to + `int` before it enters an invariant container. +- "Lessons from Pyre that Shaped Pyrefly," 18 Mar 2026 + (): Pyrefly's engine "embraces + cycles" with cycle detection + fixpoint resolution (Pyre's acyclic-phase + rule forced MRO recomputation); Pyrefly "chose usability over absolute + soundness," notably allowing attribute narrowing (`isinstance(c.x, int)`) + to survive an intervening function call that Pyre rejected as potentially + mutating. +- Pyrefly team notes module-level (file) incrementality was chosen because it + "is already fast enough in Rust" and fine-grained is "much more complex" for + "minimal performance improvements" (Edward Li, PyCon 2025 Typing Summit + notes: ). Pyrefly + does **not** use Salsa; it keeps manual control over memory eviction + (). +- **Inference strengths:** aggressive whole-program-ish inference of + unannotated returns/locals (catches `None * 2` in untyped code), strong + generics/overloads/ParamSpec support (they "focused on hard problems + first"), higher typing-spec conformance than ty. **Weaknesses:** file-level + invalidation limits IDE incrementality granularity; heuristic ("peek one + ahead") context propagation rather than principled bidirectional checking; + usability-over-soundness introduces false negatives; aggressive inference + produces false positives that violate the gradual guarantee. + +**ty / red-knot (Astral, Rust, MIT).** + +- Built on **Salsa with fine-grained multi-granularity queries**: + `infer_scope_types`, `infer_definition_types`, `infer_expression_types`, and + statement-level inference are each memoized tracked queries (DeepWiki, + , citing + `crates/ty_python_semantic/src/types/infer.rs`). Carl Meyer: "if you just + change one part of one file, Salsa can just flow that change through the + graph of queries and figure out exactly which queries…need to + re-execute…very fine-grained incrementality" (Talk Python #506, + ). Astral reports 4.7ms recompute + after editing a load-bearing PyTorch file (astral.sh/blog/ty). +- **Cycles:** fixpoint iteration seeded with a `Divergent` type until + convergence; falls back to a widened type after a bounded number of + iterations (ty docs: ). +- **Type representation:** single `Type<'db>` enum; dynamic types are a nested + `DynamicType` enum — `Any` (explicit), `Unknown` (implicit/inferred gap), + `Todo` (unimplemented feature), `Divergent` (non-converging recursion) + (ty Typing FAQ: ). + **First-class intersection types** with negation drive narrowing + (`isinstance` → `A & B`; `hasattr` → intersection with a synthetic + protocol). +- **Reachability analysis is based on type inference** (Kleene/ternary + logic), not pattern-matching known idioms, letting ty prune unreachable + branches far more generally (ty docs). +- **Bidirectional inference** was added via `TypeContext` after living for a + long time as pure "inside-out" inference; ty issue #168 + () records the motivation and + Carl Meyer's judgment that type context is needed "to apply contextual + constraints that will help the generic solver find better solutions." + **Generics** use an explicit constraint solver (`SpecializationBuilder` + + `ConstraintSetBuilder`), with PEP 695 variance inference. +- **Gradual guarantee** is a core design invariant ("adding type annotations + to working code never introduces new errors"); ty infers `Unknown` (not a + concrete type) for unannotated symbols and does not error on them. This is + the deliberate opposite of Pyrefly. +- Astral reports ty is "consistently between 10x and 60x faster than mypy and + Pyright" without caching (astral.sh/blog/ty). +- **Strengths:** best-in-class incrementality, principled gradual guarantee, + intersection types, inference-driven reachability. **Weaknesses:** lower + spec conformance than Pyrefly — roughly 15% for ty vs ~58% for Pyrefly on + the typing-spec conformance suite as of March 2026 (Pyrefly + typing-conformance comparison, via byteiota); some permissive behaviors + (e.g., `list.append` after inferred empty-container) are acknowledged + inference *limitations*, not intended design (per the ty team, via Edward + Li). + +**Pyright (Microsoft, TypeScript).** + +- Uses bidirectional inference ("bidirectional type inference is used to + determine the types of the argument expressions") and code-flow-based + **type narrowing / type guards**, documented in + `pyright/docs/type-concepts-advanced.md` + (): + narrowing on `isinstance`, `is None`, truthiness, discriminated unions, and + "narrowing for implied else" (limited to declared-type names for + performance). User-defined type guards: PEP 647 (`TypeGuard`, Eric Traut, + ), PEP 724 (stricter guards, withdrawn), + PEP 742 (`TypeIs`). Eric Traut's constraint solver for generics uses **type + intervals** (lower/upper bounds) with join at downward and meet at upward + inference directions (US patent filings describe the algorithm). + **Strength:** mature, high-conformance bidirectional inference and the + richest narrowing. **Weakness:** TypeScript implementation is not the + fastest; not Salsa-incremental. + +**mypy.** + +- Uses bidirectional checking with **join/meet** operations on the type + lattice and constraint solving for generics. Known limitation: join-based + inference "discards valuable type information and leads to many false + positives" (basedpyright's mypy comparison, + ); e.g., + `isinstance` chains widen to `object`. Constraint inference for unions of + nested generics is incomplete (python/mypy#9435). Narrowing documented at + . **Strength:** + reference semantics, mature. **Weakness:** slow, join-induced imprecision, + weaker bidirectional context propagation. + +**Inference-quality scorecard (where each is strong/weak):** generic +inference — Pyright/Pyrefly strong, mypy weak (join loss), ty improving via +constraint solver; literal inference — Pyright/Pyrefly strong; narrowing — +Pyright richest, ty most principled (intersections), Pyrefly good, mypy +join-limited; comprehensions/lambdas — all weak without bidirectional context +(ty's `TypeContext` targets lambdas first); higher-order/`ParamSpec` — Pyrefly +strongest; bidirectional context propagation — Pyright most complete, Pyrefly +heuristic, ty catching up. + +**Source-quality note.** Competitor performance numbers (Pyrefly's "1.85M +LOC/s" on 166-core Meta infrastructure; ty's "4.7ms" recompute and "10–60x +faster than mypy/Pyright"; the ~58% vs ~15% conformance gap) are +vendor/benchmark claims, not independently audited, and several figures come +from 2026 vendor blogs and a community handbook (pydevtools); treat +comparative percentages as directional. The DeepWiki ty internals are +AI-generated but cite exact source files in astral-sh/ruff and should be +verified against a pinned commit before being quoted further. + +### Incremental / demand-driven architecture {#TYPEINF-RESEARCH-INCREMENTAL} + +- **Salsa** (): "define your program as a + set of queries," inputs + memoized pure functions; recomputes on demand with + **early cutoff** when a recomputed query's result is unchanged. Used by + rust-analyzer and chalk. Architecture: rust-analyzer dev docs + () + and "Durable Incrementality" + () + — a global revision counter, forward flooding to check dependencies, + backward flooding on change, with a **durability** system so + standard-library queries aren't rechecked on every keystroke. +- Niko Matsakis, "Responsive Compilers," PLISS 2019 — the motivating talk for + demand-driven, incremental, query-based compiler architecture (referenced + widely, e.g., ). +- **The central tension:** whole-program/global inference conflicts with + incrementality. Every inferred type that depends on a distant call site + becomes a cross-file Salsa dependency, so a change invalidates a large query + subgraph. Pyrefly resolves this by (a) file-level granularity and (b) + computing a compact **Interface** (exported types only) that "shields" + downstream files. ty resolves it by fine-grained queries + Salsa early + cutoff + durability. Both compute a stable per-module *signature* boundary + so that intra-file changes don't cascade. Research: incremental type + checking via Salsa-style memoization is the de facto standard; Roslyn's + **red-green trees** (immutable "green" nodes + positioned "red" facades) are + the parallel technique in the C# compiler for incremental, persistent + syntax. diff --git a/docs/specs/EXTENSION-ACTIVITY-PANEL-SPEC.md b/docs/specs/EXTENSION-ACTIVITY-PANEL-SPEC.md index e4f307bf..2689d4f8 100644 --- a/docs/specs/EXTENSION-ACTIVITY-PANEL-SPEC.md +++ b/docs/specs/EXTENSION-ACTIVITY-PANEL-SPEC.md @@ -44,6 +44,8 @@ interface ModuleNode { kind: "package" | "module"; symbols: SymbolNode[]; coveragePercent?: number; + totalSymbols?: number; + annotatedSymbols?: number; errors?: number; warnings?: number; adopted?: boolean; @@ -116,8 +118,12 @@ state even when the workspace is empty. Clients split dotted names into segments. Real package modules attach to their segment; missing intermediate packages become structural folders. Containers precede leaf modules, -then sort alphabetically. Container issue counts roll up from descendants. Flat view removes -folders but keeps symbols under their owning module. +then sort alphabetically. Container issue counts roll up from descendants, and so does type +coverage: each module node's `totalSymbols`/`annotatedSymbols` are summed across the subtree +and the folder row shows the symbol-weighted percentage — the same aggregation as the +workspace header, never an average of child percentages. Ungraded subtrees (type checking +disabled) show no percentage. Flat view removes folders but keeps symbols under their owning +module. ### Symbol items {#EXTACT-MODULES-ITEM-PROPERTIES} diff --git a/docs/specs/LSP-ARCHITECTURE-SPEC.md b/docs/specs/LSP-ARCHITECTURE-SPEC.md index cf9e7340..2d49d489 100644 --- a/docs/specs/LSP-ARCHITECTURE-SPEC.md +++ b/docs/specs/LSP-ARCHITECTURE-SPEC.md @@ -274,7 +274,12 @@ navigation and display features. ### Hover {#LSPARCH-FEATURES-HOVER} Hover presents the resolved symbol signature and relevant Basilisk diagnostics; unknown -inferred pieces are represented explicitly rather than fabricated. +inferred pieces are represented explicitly rather than fabricated. A non-PEP diagnostic's +hover section additionally carries a **Configure Severity** command link +(`command:basilisk.openConfigurationEditor` with a `{ "rule": }` argument) that +deep-links into the configuration editor focused on the rule +([CONFIGEDITOR-VSIX-EXPERIENCE](LSP-CONFIGURATION-EDITOR-SPEC.md#CONFIGEDITOR-VSIX-EXPERIENCE)); +PEP rules are graded by the typing spec and never disabled, so they get no link. ### Go to definition {#LSPARCH-FEATURES-DEFINITION} diff --git a/docs/specs/LSP-CONFIGURATION-EDITOR-SPEC.md b/docs/specs/LSP-CONFIGURATION-EDITOR-SPEC.md index e5c582cc..3630081d 100644 --- a/docs/specs/LSP-CONFIGURATION-EDITOR-SPEC.md +++ b/docs/specs/LSP-CONFIGURATION-EDITOR-SPEC.md @@ -70,10 +70,12 @@ The four suppression-audit rules (`BSK-0060`–`BSK-0063`) are ordinary analyze- The capability-gated **Basilisk: Open Configuration Editor** command opens one full-width singleton webview using the shared Signals store and typed LSP transport. -The Rules view is tag-first, grouping source, PEP-category, and policy facets. Tag groups expose the tag's entry control (set/remove a `rule-tags` severity); rows expose per-rule entry controls. `pep`-tagged rows offer `error` / `warning` / `info` / remove entry — no disable control, because no disable exists for them; analyze rows additionally offer `disabled`. The view supports search, virtualized rows, exact impact preview/apply, paged occurrence navigation, and conflict refresh. +The Rules view is tag-first, grouping source, PEP-category, and policy facets. Tag groups expose the tag's entry control; rows expose per-rule entry controls. Every control lists concrete severities only — there is no separate "no entry" choice, because an analyze rule or tag with no entry already resolves to disabled ([CHKARCH-CONFIG-MODEL](CHECKER-ARCHITECTURE-SPEC.md#CHKARCH-CONFIG-MODEL) resolution step 3), so the two choices were one. A control with no underlying entry displays what no entry resolves to (the rule's effective severity; `error` for pep-affecting tags, `disabled` otherwise), and choosing a value always writes an explicit entry (`SetRule` / `SetTag` — an explicit `disabled` beats any tag entry, so Disabled always disables; `RemoveRule` / `RemoveTag` stay wire-only). `pep`-affecting controls — pep rows, the `pep` source tag, and PEP-category tags — offer `error` / `warning` / `info` with no disable control, because no disable exists for pep rules; analyze rows and non-pep tags additionally offer `disabled`. The view supports search, virtualized rows, exact impact preview/apply, paged occurrence navigation, and conflict refresh. Multi-root selection is explicit: the active editor's root wins, otherwise the user chooses a workspace. Responses and navigation are checked against that root. The extension does not read or write configuration files itself. The analyze opt-out is an ordinary editor setting relayed as an initialization option ([LSPARCH-DIAGNOSTIC-SCOPE](LSP-ARCHITECTURE-SPEC.md#LSPARCH-DIAGNOSTIC-SCOPE)), not part of this editor. +A non-PEP diagnostic's hover carries a **Configure Severity** deep link ([LSPARCH-FEATURES-HOVER](LSP-ARCHITECTURE-SPEC.md#LSPARCH-FEATURES-HOVER)): the LSP embeds a `command:basilisk.openConfigurationEditor` link with a `{ "rule": }` argument, the client trusts hover markdown for exactly that one command, and the opened editor focuses the rule once per webview lifetime — the search filter is prefilled with the code and the rule's detail opens. The argument is untrusted input: anything but a bounded, non-empty string is ignored, and an unknown code opens the editor unfocused. PEP rules get no link — no disable exists for them. + ## Accessibility and security {#CONFIGEDITOR-ACCESSIBILITY-SECURITY} The webview uses theme tokens, text-labelled severities, keyboard controls, high-contrast/responsive styles, reduced-motion handling, a default-deny CSP, nonce-gated local scripts, no remote resources, and runtime-decoded intents. Workspace data arrives only after the ready handshake and is never interpolated into executable HTML. diff --git a/docs/specs/LSP-DEBUG-INTEGRATION-SPEC.md b/docs/specs/LSP-DEBUG-INTEGRATION-SPEC.md index bed2eb62..2221b3d8 100644 --- a/docs/specs/LSP-DEBUG-INTEGRATION-SPEC.md +++ b/docs/specs/LSP-DEBUG-INTEGRATION-SPEC.md @@ -137,4 +137,6 @@ When the Python interpreter can't be found, the error lists what was tried: ## Python Version Targeting {#LSPDEBUG-PYTHON} -Primary target Python 3.12 (canonical Basilisk version), where debugpy's `sys.settrace`/pydevd path is fully supported. +The session uses the interpreter resolved by [LSPDEBUG-PYRES](#LSPDEBUG-PYRES). +Interpreter compatibility belongs to that runtime and debugpy; Basilisk does +not declare one canonical Python release for debugging. diff --git a/docs/specs/LSP-UV-INTEGRATION-SPEC.md b/docs/specs/LSP-UV-INTEGRATION-SPEC.md index 159a9ba2..9eb3e9dc 100644 --- a/docs/specs/LSP-UV-INTEGRATION-SPEC.md +++ b/docs/specs/LSP-UV-INTEGRATION-SPEC.md @@ -71,7 +71,8 @@ so one change rebuilds exactly once. An explicit Basilisk config value is applied by the consumer first. Otherwise `basilisk-uv` checks `.python-version`, then the lower bound of `[project].requires-python`, then the lock file's `requires-python`. If none exists, the -checker uses its centralized default. No interpreter subprocess is probed. +resolver returns no project-version evidence. No interpreter subprocess is probed by this +resolution path. ## Diagnostics {#LSPUV-DIAGNOSTICS} diff --git a/docs/specs/VSIX-SPEC.md b/docs/specs/VSIX-SPEC.md index 53f8019e..0118e5a4 100644 --- a/docs/specs/VSIX-SPEC.md +++ b/docs/specs/VSIX-SPEC.md @@ -95,10 +95,13 @@ client.start(); The VSIX is the first visual client for [CONFIGEDITOR](LSP-CONFIGURATION-EDITOR-SPEC.md#CONFIGEDITOR). It contributes the client-only command **Basilisk: Open Configuration Editor** -(`basilisk.openConfigurationEditor`) and a settings-gear action in the Basilisk -activity view. The command opens one full-width editor-tab webview; it does not -take over `pyproject.toml` as a custom editor and does not put the full rule -catalog into the narrow sidebar. +(`basilisk.openConfigurationEditor`), a settings-gear action in the Basilisk +activity view, and an **Edit Config** item (`basilisk.editConfig`, hidden from +the palette) at the top of the file-explorer context menu on `pyproject.toml` +(`explorer/context`, group `navigation@1`), which opens the editor for the +workspace folder that owns the clicked file. The command opens one full-width +editor-tab webview; it does not take over `pyproject.toml` as a custom editor +and does not put the full rule catalog into the narrow sidebar. The command is exposed only when the server advertises `capabilities.experimental.basilisk.configurationEditor` — pure presence, no diff --git a/mutation_testing/mutants_report.html b/mutation_testing/mutants_report.html index 96aa5ddb..98ebb8a7 100644 --- a/mutation_testing/mutants_report.html +++ b/mutation_testing/mutants_report.html @@ -62,7 +62,7 @@

Basilisk Mutation Report cargo-mutants v27.1.0

-
2026-07-16T18:58:34.332973Z → 2026-07-16T19:44:12.754661Z
+
2026-07-17T00:48:17.972205Z →
@@ -71,7 +71,7 @@

Basilisk Mutation Report cargo-mutants v27.1.0

Mutation Score
-
132
+
141
Total Mutants
@@ -79,11 +79,11 @@

Basilisk Mutation Report cargo-mutants v27.1.0

Missed
-
93
+
105
Caught
-
32
+
29
Timeout (= kill)
@@ -94,8 +94,8 @@

Basilisk Mutation Report cargo-mutants v27.1.0

Missed (0)
-
Caught (93)
-
Other (39)
+
Caught (105)
+
Other (36)
@@ -116,7 +116,7 @@

Basilisk Mutation Report cargo-mutants v27.1.0

crates/basilisk-checker/src/context.rs:82:13 CheckContext::from_config_with_source -> Self StructField - 153.5s + 76.7s
▶ show diff
@@ -146,13 +146,13 @@

Basilisk Mutation Report cargo-mutants v27.1.0

CAUGHT crates/basilisk-checker/src/rules/aliases_implicit.rs:61:5 collect_type_alias_names -> Vec FnValue - vec!["xyzzy".into()] - 207.3s + vec![] + 86.3s
▶ show diff
- - CAUGHT - crates/basilisk-checker/src/rules/aliases_implicit.rs:63:24 - collect_type_alias_names -> Vec BinaryOperator - == - 62.6s - - -
▶ show diff
- CAUGHT crates/basilisk-checker/src/rules/aliases_implicit.rs:61:5 collect_type_alias_names -> Vec FnValue vec![String::new()] - 243.3s + 28.1s -
▶ show diff
-
@@ -3522,11 +3930,11 @@

Basilisk Mutation Report cargo-mutants v27.1.0

crates/basilisk-checker/src/rules/match_exhaustiveness.rs:48:5 make_diagnostic -> Diagnostic FnValue Default::default() - 5.4s + 1.5s -
▶ show diff
-
diff --git a/mutation_testing/mutation_scores.json b/mutation_testing/mutation_scores.json index 7643f397..6bed8c34 100644 --- a/mutation_testing/mutation_scores.json +++ b/mutation_testing/mutation_scores.json @@ -1,12 +1,12 @@ { "scores": { "working": { - "caught": 93, + "caught": 105, "date": "2026-07-17", "kill_rate": 100.0, "missed": 0, - "timeout": 32, - "total": 132, + "timeout": 29, + "total": 141, "unviable": 7 } }, diff --git a/scripts/gen_incremental_corpus.py b/scripts/gen_incremental_corpus.py new file mode 100644 index 00000000..f87f55eb --- /dev/null +++ b/scripts/gen_incremental_corpus.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +"""Generate a synthetic 1M+ LOC Python workspace for the Stage 1 incremental +measurements ([TYPEINF-TARGET-INCREMENTAL], NARROWPLAN-CHECKLIST Stage 1: +"Measure memory on a 1M+ LOC target" / "Measure p50/p99 keystroke re-check +latency on a 1M-LOC corpus"). + +The corpus is deterministic (seeded) so measurements are reproducible: modules +with annotated functions, classes, module-level constants, container literals, +comprehensions, and cross-module imports — the shapes the definition-level +Salsa queries (crates/basilisk-checker/src/incremental_defs.rs) exercise. + +Usage: + python3 scripts/gen_incremental_corpus.py OUT_DIR [--files N] [--loc-per-file N] + +Defaults produce ~1.05M LOC (2100 files x ~500 LOC). +""" + +from __future__ import annotations + +import argparse +import random +from pathlib import Path + +MODULE_TEMPLATE_HEADER = '"""Synthetic module {index} for incremental measurement."""\n' + + +def gen_function(rng: random.Random, index: int) -> list[str]: + """One annotated function with a small typed body (~14 lines).""" + a, b = rng.randint(1, 99), rng.randint(1, 99) + return [ + f"def compute_{index}(base: int, scale: float, label: str) -> float:", + f' """Deterministic arithmetic body {index}."""', + f" offset: int = {a}", + f" factor: float = {b}.5", + " total = base * scale + offset * factor", + f' tag: str = label + "_{index}"', + " if total > 0:", + " return total", + " return float(len(tag))", + "", + "", + ] + + +def gen_class(rng: random.Random, index: int) -> list[str]: + """One class with annotated attributes and two methods (~16 lines).""" + default = rng.randint(1, 9) + return [ + f"class Record{index}:", + f' """Synthetic record {index}."""', + "", + f" def __init__(self, key: str, count: int = {default}) -> None:", + " self.key = key", + " self.count = count", + " self.values: list[int] = []", + "", + " def add(self, value: int) -> None:", + " self.values.append(value + self.count)", + "", + " def summary(self) -> dict[str, int]:", + ' return {"count": self.count, "total": sum(self.values)}', + "", + "", + ] + + +def gen_constants(rng: random.Random, index: int) -> list[str]: + """Module-level definitions: literals, containers, comprehensions.""" + n = rng.randint(3, 9) + return [ + f"LIMIT_{index}: int = {rng.randint(10, 500)}", + f'NAME_{index} = "record_{index}"', + f"WEIGHTS_{index}: list[float] = [{rng.randint(1, 9)}.0, {rng.randint(1, 9)}.5]", + f'TABLE_{index}: dict[str, int] = {{"a": {n}, "b": {n + 1}}}', + f"SQUARES_{index} = [i * i for i in range({n})]", + f"ALIAS_{index} = LIMIT_{index}", + "", + ] + + +def gen_module(rng: random.Random, index: int, files: int, loc_target: int) -> str: + """One module of roughly `loc_target` lines.""" + lines: list[str] = [MODULE_TEMPLATE_HEADER.format(index=index)] + if index > 0: + lines.append( + f"from mod_{(index - 1) % files:05d} import compute_0 # noqa: F401" + ) + lines.append("") + block = 0 + while len(lines) < loc_target: + lines.extend(gen_constants(rng, block)) + lines.extend(gen_function(rng, block)) + lines.extend(gen_class(rng, block)) + block += 1 + return "\n".join(lines) + "\n" + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("out_dir", type=Path) + parser.add_argument("--files", type=int, default=2100) + parser.add_argument("--loc-per-file", type=int, default=500) + parser.add_argument("--seed", type=int, default=20260718) + args = parser.parse_args() + + args.out_dir.mkdir(parents=True, exist_ok=True) + rng = random.Random(args.seed) + total_loc = 0 + for index in range(args.files): + source = gen_module(rng, index, args.files, args.loc_per_file) + total_loc += source.count("\n") + (args.out_dir / f"mod_{index:05d}.py").write_text(source, encoding="utf-8") + print(f"wrote {args.files} files, {total_loc} LOC to {args.out_dir}") + + +if __name__ == "__main__": + main() diff --git a/vscode-extension/README.md b/vscode-extension/README.md index aa7198a4..b6742f91 100644 --- a/vscode-extension/README.md +++ b/vscode-extension/README.md @@ -40,12 +40,12 @@ And it is the **fastest checker we’ve measured** — median cold full- | Type checker | Median cold check | | --- | --- | -| ⚡ **Basilisk** | **11 ms** | -| zuban | 27 ms | -| ty | 39 ms | -| Pyrefly | 148 ms | -| Pyright | 582 ms | -| mypy | 610 ms | +| ⚡ **Basilisk** | **10 ms** | +| zuban | 28 ms | +| ty | 40 ms | +| Pyrefly | 110 ms | +| Pyright | 573 ms | +| mypy | 574 ms | Median cold full-file check across 26 single-construct typing-spec stress fixtures on an Apple M4 Max — lower is better; inside the editor a warm re-check is faster again. Every figure is produced by [`hyperfine`](https://github.com/sharkdp/hyperfine) and committed per machine, so nothing here is hand-typed. **Clone the repo, run `make bench` on your own hardware, and send us the CSV — independent audits are welcome.** [Full benchmarks & methodology →](https://www.basilisk-python.dev/docs/benchmarks/) diff --git a/vscode-extension/README.zh.md b/vscode-extension/README.zh.md index 6da21dfa..99ae4c28 100644 --- a/vscode-extension/README.zh.md +++ b/vscode-extension/README.zh.md @@ -41,12 +41,12 @@ Basilisk 是**唯一**在官方 | 类型检查器 | 冷启动检查中位数 | | --- | --- | -| ⚡ **Basilisk** | **11 ms** | -| zuban | 27 ms | -| ty | 39 ms | -| Pyrefly | 148 ms | -| Pyright | 582 ms | -| mypy | 610 ms | +| ⚡ **Basilisk** | **10 ms** | +| zuban | 28 ms | +| ty | 40 ms | +| Pyrefly | 110 ms | +| Pyright | 573 ms | +| mypy | 574 ms | 在 Apple M4 Max 上,跨 26 个单一类型构造压力测试样本的冷启动全文件检查中位数 —— 数值越低越好;在编辑器内热重检查更快。每个数字均由 [`hyperfine`](https://github.com/sharkdp/hyperfine) 生成并按机器提交,没有一个是手写的。**克隆仓库,在你自己的硬件上运行 `make bench`,并把 CSV 发给我们 —— 欢迎社区独立复核。** [完整基准测试与方法论(英文)→](https://www.basilisk-python.dev/docs/benchmarks/) diff --git a/vscode-extension/package.json b/vscode-extension/package.json index 0ad7236b..6228ef90 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -133,6 +133,13 @@ "icon": "$(settings-gear)", "enablement": "basilisk.configurationEditorSupported" }, + { + "command": "basilisk.editConfig", + "title": "Edit Config", + "category": "Basilisk", + "icon": "$(settings-gear)", + "enablement": "basilisk.configurationEditorSupported" + }, { "command": "basilisk.organizeImports", "title": "Basilisk: Organize Imports", @@ -388,6 +395,10 @@ "command": "basilisk.openConfigurationEditor", "when": "basilisk.configurationEditorSupported" }, + { + "command": "basilisk.editConfig", + "when": "false" + }, { "command": "basilisk.info.runAction", "when": "false" @@ -437,6 +448,13 @@ "when": "basilisk.debugging" } ], + "explorer/context": [ + { + "command": "basilisk.editConfig", + "when": "resourceFilename == pyproject.toml && basilisk.configurationEditorSupported", + "group": "navigation@1" + } + ], "debug/toolBar": [ { "command": "basilisk.memorySnapshot", diff --git a/vscode-extension/src/configuration-editor-script-core.ts b/vscode-extension/src/configuration-editor-script-core.ts index 30d90604..9c1c4347 100644 --- a/vscode-extension/src/configuration-editor-script-core.ts +++ b/vscode-extension/src/configuration-editor-script-core.ts @@ -20,6 +20,10 @@ export const CONFIGURATION_EDITOR_SCRIPT_CORE = String.raw` let selectedRuleCode; let lastFocusedRule; let overlayWasBlocking = false; + // One-shot per webview lifetime: the Configure Severity deep link's + // focus target is applied on the first snapshot render only, so later + // state posts never stomp the user's own search/selection. + let focusRuleConsumed = false; function byId(id) { return document.getElementById(id); } function clear(node) { node.replaceChildren(); } @@ -49,6 +53,12 @@ export const CONFIGURATION_EDITOR_SCRIPT_CORE = String.raw` // THE partition ([CHKARCH-COMMANDS]): pep-tagged rules always run and can // never be disabled; only analyze rules get a Disabled control. function isPepRule(rule) { return rule.descriptor.tags.indexOf(PEP_TAG) !== -1; } + // A pep-affecting tag entry can never be disabled either: the pep source + // tag and every PEP category grade only pep rules ([CHKARCH-CONFIG-MODEL]). + function isPepTag(tag) { return tag.name === PEP_TAG || kind(tag.kind, 'Descriptive') === 'PepCategory'; } + // Dropdowns list concrete severities only — there is no separate no-entry + // choice; an analyze rule with no entry is disabled, so the two were one. + function severityOptions(pep) { return pep ? SEVERITIES.filter((value) => value !== 'Disabled') : SEVERITIES; } function ruleSearchText(rule) { const descriptor = rule.descriptor; return [descriptor.code, descriptor.title, descriptor.summary].concat(descriptor.tags).join(' ').toLowerCase(); @@ -81,17 +91,13 @@ export const CONFIGURATION_EDITOR_SCRIPT_CORE = String.raw` function postPreview(mutations) { vscode.postMessage({ type: 'preview', mutations }); } - // A rule row can request exactly SetRule or RemoveRule ([CONFIGEDITOR-MODEL]). + // A dropdown change always writes an explicit entry ([CONFIGEDITOR-MODEL]): + // an explicit 'disabled' beats any tag entry, so Disabled always disables. function ruleMutation(code, value) { - return value === NO_ENTRY - ? { kind: 'RemoveRule', code } - : { kind: 'SetRule', code, severity: { kind: value } }; + return { kind: 'SetRule', code, severity: { kind: value } }; } - // A tag group can request exactly SetTag or RemoveTag ([CONFIGEDITOR-MODEL]). function tagMutation(tag, value) { - return value === NO_ENTRY - ? { kind: 'RemoveTag', tag } - : { kind: 'SetTag', tag, severity: { kind: value } }; + return { kind: 'SetTag', tag, severity: { kind: value } }; } function selectedRule() { return snapshot && snapshot.rules.find((rule) => rule.descriptor.code === selectedRuleCode); @@ -106,10 +112,16 @@ export const CONFIGURATION_EDITOR_SCRIPT_CORE = String.raw` } function restoreFocus() { if (!lastFocusedRule) return; + // Only re-attach focus the row rebuild destroyed (focus fell back to + // body) — never steal live focus. preventScroll keeps the restored + // focus from scrolling the stale row back into view, which pinned the + // viewport and made rules below the fold unreachable. + const active = document.activeElement; + if (active && active !== document.body) return; const row = document.querySelector('[data-rule-code="' + CSS.escape(lastFocusedRule.code) + '"]'); if (!row) return; const selector = lastFocusedRule.control === 'select' ? 'select' : '.rule-copy button'; const control = row.querySelector(selector); - if (control) control.focus(); + if (control) control.focus({ preventScroll: true }); } `; diff --git a/vscode-extension/src/configuration-editor-script-render.ts b/vscode-extension/src/configuration-editor-script-render.ts index 77818d4d..4cddb4ec 100644 --- a/vscode-extension/src/configuration-editor-script-render.ts +++ b/vscode-extension/src/configuration-editor-script-render.ts @@ -5,7 +5,7 @@ export const CONFIGURATION_EDITOR_SCRIPT_RENDER = String.raw` function entryOption(value, selected) { const option = document.createElement('option'); option.value = value; - option.textContent = value === 'None' ? 'No entry' : value; + option.textContent = value; option.selected = value === selected; return option; } @@ -15,11 +15,15 @@ export const CONFIGURATION_EDITOR_SCRIPT_RENDER = String.raw` const select = document.createElement('select'); select.className = 'severity-select'; select.dataset.tagEntry = tag.name; - const current = entryValue(tag.entry); + // No entry shows what no entry resolves to ([CHKARCH-CONFIG-MODEL]): + // pep-affecting tags run at error, everything else does not run. + const current = entryValue(tag.entry) === NO_ENTRY + ? (isPepTag(tag) ? 'Error' : 'Disabled') + : entryValue(tag.entry); select.dataset.severity = current; - // Tag-entry control: error/warning/info/disabled/remove entry. The - // server rejects a tag entry that would resolve a pep rule to disabled. - [NO_ENTRY].concat(SEVERITIES).forEach((value) => select.append(entryOption(value, current))); + // Tag-entry control: error/warning/info — plus disabled only where the + // server would accept it (never on a tag that grades pep rules). + severityOptions(isPepTag(tag)).forEach((value) => select.append(entryOption(value, current))); label.append(select); return label; } @@ -62,13 +66,12 @@ export const CONFIGURATION_EDITOR_SCRIPT_RENDER = String.raw` select.className = 'severity-select'; select.dataset.ruleEntry = rule.descriptor.code; select.dataset.severity = effectiveValue(rule); - const current = entryValue(rule.entry); - // pep rows: error/warning/info/remove entry — no Disabled control exists - // for them ([CHKARCH-CONFIG-MODEL]); analyze rows also offer Disabled. - const options = isPepRule(rule) - ? [NO_ENTRY].concat(SEVERITIES.filter((value) => value !== 'Disabled')) - : [NO_ENTRY].concat(SEVERITIES); - options.forEach((value) => select.append(entryOption(value, current))); + // No entry shows the resolved severity — for an untouched analyze rule + // that IS Disabled ([CHKARCH-CONFIG-MODEL] resolution step 3). + const current = entryValue(rule.entry) === NO_ENTRY ? effectiveValue(rule) : entryValue(rule.entry); + // pep rows: error/warning/info — no Disabled control exists for them + // ([CHKARCH-CONFIG-MODEL]); analyze rows also offer Disabled. + severityOptions(isPepRule(rule)).forEach((value) => select.append(entryOption(value, current))); label.append(select); return label; } @@ -167,12 +170,29 @@ export const CONFIGURATION_EDITOR_SCRIPT_RENDER = String.raw` byId('root-label').textContent = compactUri(snapshot.rootUri); byId('source-label').textContent = compactUri(snapshot.configUri) + ' · revision ' + snapshot.revision; } + // Configure Severity deep link ([CONFIGEDITOR-VSIX-EXPERIENCE]): focus + // the requested rule once — prefill the search filter with its code, + // scroll its row into the virtual window, and open its detail panel. + function consumeFocusRule() { + const code = editorState.focusRule; + if (focusRuleConsumed || !code || !snapshot) return; + if (!snapshot.rules.some((rule) => rule.descriptor.code === code)) return; + focusRuleConsumed = true; + byId('rule-search').value = code; + applyFilter(); + const index = filteredRules.findIndex((rule) => rule.descriptor.code === code); + if (index >= 0) byId('rule-viewport').scrollTop = index * ROW_HEIGHT; + renderRuleWindow(); + showRule(code); + announce('Focused rule ' + code); + } function renderSnapshot() { if (!snapshot) return; saveFocus(); renderSource(); renderTags(); applyFilter(); + consumeFocusRule(); renderRuleDetail(); window.requestAnimationFrame(restoreFocus); } diff --git a/vscode-extension/src/configuration-editor-state.ts b/vscode-extension/src/configuration-editor-state.ts index c439d594..844727be 100644 --- a/vscode-extension/src/configuration-editor-state.ts +++ b/vscode-extension/src/configuration-editor-state.ts @@ -30,6 +30,12 @@ export interface ConfigurationEditorState { readonly repairUri: string | undefined; readonly message: string; readonly refreshRequested: boolean; + /** + * Rule code the webview should focus once per webview lifetime — set when + * the editor is opened from a diagnostic's Configure Severity hover link + * ([CONFIGEDITOR-VSIX-EXPERIENCE]). + */ + readonly focusRule: string | undefined; } export const IDLE_CONFIGURATION_EDITOR: ConfigurationEditorState = { @@ -42,10 +48,16 @@ export const IDLE_CONFIGURATION_EDITOR: ConfigurationEditorState = { repairUri: undefined, message: "", refreshRequested: false, + focusRule: undefined, }; export interface ConfigurationEditorActions { - beginConfigurationLoad(rootUri: string): void; + /** + * `focusRule` semantics: a string sets the pending focus target, `null` + * clears it (a plain open with no target), and `undefined` (internal + * refreshes) preserves any pending same-root focus. + */ + beginConfigurationLoad(rootUri: string, focusRule?: string | null): void; acceptConfigurationSnapshot(snapshot: ConfigurationSnapshot): void; beginConfigurationPreview(): void; acceptConfigurationPreview(preview: ConfigurationPreview): void; @@ -86,8 +98,17 @@ export function requestConfigurationRefresh( }; } -function beginLoad(state: Signal, rootUri: string): void { +function beginLoad( + state: Signal, + rootUri: string, + focusRule?: string | null, +): void { const sameRoot = state.value.rootUri === rootUri; + // string = set, null = clear (plain open), undefined = internal refresh — + // keep any pending same-root focus so it survives open()'s load chain. + const nextFocus = focusRule === undefined + ? (sameRoot ? state.value.focusRule : undefined) + : (focusRule ?? undefined); state.value = { ...state.value, phase: "loading", @@ -99,6 +120,7 @@ function beginLoad(state: Signal, rootUri: string): vo repairUri: undefined, message: "Reading the active project policy…", refreshRequested: false, + focusRule: nextFocus, }; } @@ -153,7 +175,7 @@ export function createConfigurationEditorActions( state: Signal, ): ConfigurationEditorActions { return { - beginConfigurationLoad(rootUri): void { beginLoad(state, rootUri); }, + beginConfigurationLoad(rootUri, focusRule): void { beginLoad(state, rootUri, focusRule); }, acceptConfigurationSnapshot(snapshot): void { acceptSnapshot(state, snapshot); }, beginConfigurationPreview(): void { beginPreview(state); }, acceptConfigurationPreview(preview): void { acceptPreview(state, preview); }, diff --git a/vscode-extension/src/configuration-editor.ts b/vscode-extension/src/configuration-editor.ts index 59062b47..e28c56b5 100644 --- a/vscode-extension/src/configuration-editor.ts +++ b/vscode-extension/src/configuration-editor.ts @@ -23,6 +23,8 @@ import { SingletonWebviewPanel, type WebviewMessage } from "./profiler-webview"; import type { Store } from "./store"; export const CONFIGURATION_EDITOR_COMMAND = "basilisk.openConfigurationEditor"; +/** Explorer context-menu entry on pyproject.toml ("Edit Config"). */ +export const EDIT_CONFIG_COMMAND = "basilisk.editConfig"; export const CONFIGURATION_EDITOR_CONTEXT = "basilisk.configurationEditorSupported"; const SNAPSHOT_METHOD = "basilisk/configurationSnapshot"; const PREVIEW_METHOD = "basilisk/previewConfigurationChange"; @@ -186,8 +188,12 @@ export class ConfigurationEditorController implements vscode.Disposable { }); } - /** Open/re-render the editor for one explicit workspace root. */ - public open(rootUri: string): void { + /** + * Open/re-render the editor for one explicit workspace root, optionally + * focused on one rule (the diagnostic hover's Configure Severity link, + * [CONFIGEDITOR-VSIX-EXPERIENCE]). + */ + public open(rootUri: string, focusRule?: string): void { if (this.disposed) { return; } const wasOpen = this.panel.isOpen(); const wasVisible = this.panel.isVisible(); @@ -195,7 +201,8 @@ export class ConfigurationEditorController implements vscode.Disposable { if (this.store.configurationEditor.value.rootUri !== rootUri) { this.pendingRefreshRoot = undefined; } - this.store.beginConfigurationLoad(rootUri); + // A plain open must clear any stale focus target — `null`, not undefined. + this.store.beginConfigurationLoad(rootUri, focusRule ?? null); this.panel.show("Basilisk Configuration", buildConfigurationEditorDocument()); // A hidden live panel refreshes from the real hidden→visible callback. // New/already-visible panels do not produce that transition, so load here. @@ -450,30 +457,65 @@ function fileIsWithinRoot(target: vscode.Uri, rootUri: string | undefined): bool } } -/** Register the capability-gated command and context used by the view-title gear. */ +const MAX_RULE_CODE_LENGTH = 64; + +/** + * Decode the optional `{ rule }` command argument carried by a diagnostic's + * Configure Severity hover link ([CONFIGEDITOR-VSIX-EXPERIENCE]). Untrusted: + * anything that is not a bounded, non-empty string yields no focus target. + */ +export function configurationEditorFocusRule(value: unknown): string | undefined { + if (typeof value !== "object" || value === null || Array.isArray(value)) { return undefined; } + const rule = (value as { readonly rule?: unknown }).rule; + return typeof rule === "string" && rule.length > 0 && rule.length <= MAX_RULE_CODE_LENGTH + ? rule + : undefined; +} + +/** + * Open the editor for the workspace folder that owns `resource` (the + * explorer context-menu target), falling back to the usual root selection. + * `focusRule` deep-links the opened editor to one rule's row. + */ +async function openConfigurationFor( + controller: ConfigurationEditorController, + resource?: vscode.Uri, + focusRule?: string, +): Promise { + const folder = resource === undefined ? undefined : vscode.workspace.getWorkspaceFolder(resource); + const rootUri = folder?.uri.toString() ?? await selectConfigurationRoot(); + if (rootUri === undefined) { + void vscode.window.showInformationMessage("Open a workspace folder to configure Basilisk."); + return; + } + controller.open(rootUri, focusRule); +} + +/** Register the capability-gated commands and context used by the view-title gear and explorer menu. */ export function registerConfigurationEditor( store: Store, transport?: ConfigurationEditorTransport, ): { readonly controller: ConfigurationEditorController; readonly disposables: vscode.Disposable[] } { const controller = new ConfigurationEditorController(store, transport); - let command: vscode.Disposable | undefined; + let commands: vscode.Disposable[] | undefined; + function disposeCommands(): void { + commands?.forEach((command) => { command.dispose(); }); + commands = undefined; + } let previouslySupported = false; const disposeCapabilityEffect = effect(() => { const supported = transport !== undefined || (store.lspState.value === "running" && supportsConfigurationEditor(store.client.value)); void vscode.commands.executeCommand("setContext", CONFIGURATION_EDITOR_CONTEXT, supported); - if (supported && command === undefined) { - command = vscode.commands.registerCommand(CONFIGURATION_EDITOR_COMMAND, async () => { - const rootUri = await selectConfigurationRoot(); - if (rootUri === undefined) { - void vscode.window.showInformationMessage("Open a workspace folder to configure Basilisk."); - return; - } - controller.open(rootUri); - }); + if (supported && commands === undefined) { + commands = [ + vscode.commands.registerCommand(CONFIGURATION_EDITOR_COMMAND, async (argument?: unknown) => + openConfigurationFor(controller, undefined, configurationEditorFocusRule(argument))), + vscode.commands.registerCommand(EDIT_CONFIG_COMMAND, async (resource?: vscode.Uri) => + openConfigurationFor(controller, resource instanceof vscode.Uri ? resource : undefined)), + ]; } else if (!supported) { - command?.dispose(); - command = undefined; + disposeCommands(); if (controller.isOpen() && previouslySupported) { controller.capabilityLost("The language server no longer advertises the configuration editor. Reconnect or update Basilisk."); } @@ -484,8 +526,7 @@ export function registerConfigurationEditor( const capabilityLifecycle: vscode.Disposable = { dispose(): void { disposeCapabilityEffect(); - command?.dispose(); - command = undefined; + disposeCommands(); void vscode.commands.executeCommand("setContext", CONFIGURATION_EDITOR_CONTEXT, false); }, }; diff --git a/vscode-extension/src/lsp-client.ts b/vscode-extension/src/lsp-client.ts index dcb47c4d..d9ac0c9b 100644 --- a/vscode-extension/src/lsp-client.ts +++ b/vscode-extension/src/lsp-client.ts @@ -20,6 +20,7 @@ import { effect } from "@preact/signals-core"; import { Logger } from "./logger"; import { createLspTraceChannel } from "./lsp-trace"; import { BASILISK_DOCUMENT_SELECTOR } from "./lsp-document-selector"; +import { CONFIGURATION_EDITOR_COMMAND } from "./configuration-editor"; import { type Store, type LspState } from "./store"; /** Maximum LSP errors before shutting down the server. */ @@ -200,7 +201,12 @@ function bindLspStateEffects(store: Store, updateStatusBar: StatusBarUpdater): v }); } -function buildClientOptions( +/** + * Build the LanguageClient options — documentSelector, synchronize, + * middleware (hover trust, executeCommand UI, configuration merge), and + * error recovery. Exported so tests can exercise the middleware wiring. + */ +export function buildClientOptions( outputCh: vscode.LogOutputChannel | undefined, traceCh: vscode.LogOutputChannel, updateStatusBar: StatusBarUpdater @@ -236,6 +242,9 @@ function buildClientOptions( }, middleware: { executeCommand: executeCommandMiddleware, + // eslint-disable-next-line max-params -- vscode-languageclient fixes the 4-arg middleware signature. + provideHover: async (document, position, token, next) => + trustConfigureSeverityLinks(await next(document, position, token)), workspace: { configuration: async (params, token, next) => { const results = await next(params, token); @@ -290,6 +299,25 @@ const TOAST_MESSAGES: Record = { type NextFn = (command: string, args: unknown[]) => Thenable; +/** + * The LSP embeds `command:basilisk.openConfigurationEditor` links in hover + * markdown for non-PEP diagnostics (the Configure Severity deep link, + * [CONFIGEDITOR-VSIX-EXPERIENCE]). VS Code renders LSP hover markdown + * untrusted by default, which strips command links — so trust hover content + * for exactly that one command and nothing else. + */ +export function trustConfigureSeverityLinks( + hover: T, +): T { + if (hover === null || hover === undefined) { return hover; } + for (const content of hover.contents) { + if (content instanceof vscode.MarkdownString) { + content.isTrusted = { enabledCommands: [CONFIGURATION_EDITOR_COMMAND] }; + } + } + return hover; +} + function activeOrVisibleFileEditor(): vscode.TextEditor | undefined { const active = vscode.window.activeTextEditor; if (active?.document.uri.scheme === "file") { diff --git a/vscode-extension/src/module-explorer-render.ts b/vscode-extension/src/module-explorer-render.ts new file mode 100644 index 00000000..e8a60e12 --- /dev/null +++ b/vscode-extension/src/module-explorer-render.ts @@ -0,0 +1,215 @@ +// Implements [EXTACT-MODULES]. See docs/specs/EXTENSION-ACTIVITY-PANEL-SPEC.md#EXTACT-MODULES +/** + * Module Explorer wire types and row rendering. + * + * The [EXTACT-DATA-MODEL] client mirrors plus the pure description / tooltip / + * icon-colour helpers for module and folder/package rows, split out of + * `module-explorer.ts` so the provider file stays focused on tree mechanics. + */ + +import * as vscode from "vscode"; +import { type DiagnosticNode } from "./module-explorer-diagnostics"; + +// ── LSP response types ─────────────────────────────────────────────────── +// +// Implements the client mirror of [EXTACT-DATA-MODEL] — the shared +// WorkspaceModulesResponse / ModuleNode / SymbolNode / DiagnosticNode / +// HealthStats wire shapes returned by basilisk.workspaceModules. + +export interface SymbolNode { + readonly name: string; + readonly kind: "class" | "function" | "variable" | "constant" | "typeAlias"; + readonly line: number; + readonly annotated: boolean; + readonly exported: boolean; + readonly children?: readonly SymbolNode[]; +} + +export interface ModuleNode { + readonly name: string; + readonly path: string; + readonly kind: "package" | "module"; + readonly symbols: readonly SymbolNode[]; + // Every diagnostic for this module, rendered as the first drill-down rows so + // the errors/warnings tallies below are navigable, never dead + // ([EXTACT-MODULES-DIAGNOSTICS], #235). Empty while Type Checking is + // disabled; optional only to stay crash-safe against a pre-#235 server + // binary supplied via basilisk.executablePath. + readonly diagnostics?: readonly DiagnosticNode[]; + // Health rollup folded into each module by basilisk.workspaceModules + // [EXTACT-MODULES] — coverage %, diagnostic counts, and adoption state, so the + // merged panel needs no separate basilisk.typeHealth round-trip. ABSENT while + // Type Checking is disabled ([ANALYSIS-ENABLED], #119): the server omits all + // grading, so there is nothing to render as "% typed" or a red tint. + readonly coveragePercent?: number; + // Raw symbol counts behind coveragePercent — the weights for the client-side + // folder/package coverage rollup ([EXTACT-MODULES-TREE-STRUCTURE]), so folder + // percentages aggregate exactly like the workspace header's "% typed". + readonly totalSymbols?: number; + readonly annotatedSymbols?: number; + readonly errors?: number; + readonly warnings?: number; + readonly adopted?: boolean; +} + +/** Workspace-wide health rollup carried alongside the module list. */ +export interface HealthStats { + // The Type Checking toggle state stamped by the server ([ANALYSIS-ENABLED], + // #119). `false` means the grading fields below are absent by construction. + readonly typeCheckingEnabled?: boolean; + readonly totalSymbols?: number; + readonly annotatedSymbols?: number; + readonly coveragePercent?: number; + readonly errors?: number; + readonly warnings?: number; + readonly adoptedFiles?: number; + readonly totalFiles: number; + // Whether the server's initial workspace scan has finished. A zero-file + // rollup only means "empty workspace" when this is true; before that it + // means "not scanned yet" ([EXTACT-MODULES-HEADER-LOADING], #144). + readonly scanComplete?: boolean; +} + +export interface WorkspaceModulesResponse { + readonly modules: readonly ModuleNode[]; + readonly workspace: HealthStats; +} + +/** + * A node in the client-reconstructed package/folder tree + * [EXTACT-MODULES-TREE-STRUCTURE] (#149). The LSP returns a *flat* list of + * modules keyed by dotted name (e.g. `pkg.sub.mod`); the nested tree is rebuilt + * here by splitting each name into path segments. Intermediate folders that are + * not themselves Python packages are synthesised as container nodes with no + * `module`, so the panel renders `pkg/ → sub/ → mod` instead of a flat list. + */ +export interface PackageTreeNode { + /** Last path segment — the row's display label (e.g. `auth`). */ + readonly segment: string; + /** Fully-qualified dotted prefix up to and including this node. */ + readonly fullName: string; + /** The module/package file mapping exactly here, if one exists. */ + module?: ModuleNode; + /** Child packages and modules, keyed by their segment. */ + readonly children: Map; + // Diagnostics rolled up across this node's whole subtree (self module + + // every descendant). Surfaced on the folder/package row so errors are + // visible without drilling into the hierarchy (#149). Set by `rollup`. + errors: number; + warnings: number; + // Symbol counts rolled up across the subtree — the weights for the folder's + // coverage %, aggregated exactly like the workspace header. Set by `rollup`. + totalSymbols: number; + annotatedSymbols: number; + /** Whether any module in the subtree carried grading (Type Checking on, #119). */ + graded: boolean; +} + +// ── Coverage rendering [EXTACT-MODULES] ────────────────────────────────── + +/** Width of the Unicode coverage bar in characters. */ +const COVERAGE_BAR_WIDTH = 10; +/** Coverage threshold for "good" (green). */ +const COVERAGE_GOOD_THRESHOLD = 90; +/** Coverage threshold for "warning" (yellow); below it is red. */ +const COVERAGE_WARN_THRESHOLD = 50; +/** Neutral coverage for ungraded rows (Type Checking disabled, #119). */ +export const FULL_COVERAGE_PERCENT = 100; +/** Scale factor from a 0–1 ratio to a percentage. */ +const PERCENT_SCALE = 100; + +/** Render a coverage progress bar using Unicode block characters. */ +function coverageBar(percent: number): string { + const filled = Math.round(percent / COVERAGE_BAR_WIDTH); + return "█".repeat(filled) + "░".repeat(COVERAGE_BAR_WIDTH - filled); +} + +/** Theme color for a coverage percentage: green >=90%, yellow >=50%, else red. */ +export function coverageColor(percent: number): vscode.ThemeColor { + if (percent >= COVERAGE_GOOD_THRESHOLD) { return new vscode.ThemeColor("testing.iconPassed"); } + if (percent >= COVERAGE_WARN_THRESHOLD) { return new vscode.ThemeColor("list.warningForeground"); } + return new vscode.ThemeColor("list.errorForeground"); +} + +// [EXTACT-MODULES-COUNT-STYLE] is the diagnostic-tally surface for module rows. +/** Module row description: coverage bar + % + error/warning counts + adopted badge. */ +export function moduleDescription(module: ModuleNode): string { + // Type Checking disabled (#119): the server serves no grading, so the row is + // a plain navigation entry — no bar, no percentage, no tallies. + if (module.coveragePercent === undefined) { return ""; } + const issueTally = diagnosticTally(module.errors ?? 0, module.warnings ?? 0); + const issueStr = issueTally === "" ? "" : ` — ${issueTally}`; + const badge = module.adopted === true ? " [adopted]" : ""; + return `${coverageBar(module.coveragePercent)} ${module.coveragePercent}%${issueStr}${badge}`; +} + +/** Module row tooltip: name + path + coverage + diagnostics + adoption. */ +export function moduleTooltip(module: ModuleNode): string { + return [ + module.name, + module.path, + module.coveragePercent !== undefined ? `Coverage: ${module.coveragePercent}%` : "", + module.errors !== undefined ? `Errors: ${module.errors}` : "", + module.warnings !== undefined ? `Warnings: ${module.warnings}` : "", + module.adopted === true ? "Status: Adopted (errors demoted to warnings)" : "", + ].filter(Boolean).join("\n"); +} + +/** Implements [EXTACT-MODULES-COUNT-STYLE]: coloured glyphs `🔴 n` (errors) / + * `🟠 n` (warnings) — never `nE nW`; a zero severity is omitted, or "" when clean. */ +export function diagnosticTally(errors: number, warnings: number): string { + const issues: string[] = []; + if (errors > 0) { issues.push(`🔴 ${errors}`); } + if (warnings > 0) { issues.push(`🟠 ${warnings}`); } + return issues.join(" "); +} + +/** + * Subtree type coverage for a folder/package row: symbol-weighted across every + * module beneath the node, exactly like the workspace header's "% typed" + * ([EXTACT-MODULES-TREE-STRUCTURE]) — never an average of pre-divided child + * percentages. `undefined` while ungraded (Type Checking disabled, #119) so no + * folder renders a vacuous 100% conjured from zero data. + */ +function subtreeCoverage(node: PackageTreeNode): number | undefined { + if (!node.graded) { return undefined; } + if (node.totalSymbols === 0) { return FULL_COVERAGE_PERCENT; } + return Math.round((node.annotatedSymbols / node.totalSymbols) * PERCENT_SCALE); +} + +/** + * Folder/package icon tint: red if the subtree holds any error, else yellow if + * any warning, else the subtree's rolled-up coverage colour (an ungraded folder + * stays untinted). Lets a folder with hidden errors read red without expanding (#149). + */ +export function packageIconColor(node: PackageTreeNode): vscode.ThemeColor | undefined { + if (node.errors > 0) { return new vscode.ThemeColor("list.errorForeground"); } + if (node.warnings > 0) { return new vscode.ThemeColor("list.warningForeground"); } + // No coverage served (Type Checking disabled, #119) → untinted, like a folder. + const coverage = subtreeCoverage(node); + return coverage !== undefined ? coverageColor(coverage) : undefined; +} + +/** + * Folder/package row description: the subtree's rolled-up coverage bar + % and + * count-style tally ([EXTACT-MODULES-COUNT-STYLE]) so type health and problems + * are both visible without drilling in (#149). + */ +export function packageDescription(node: PackageTreeNode): string { + const coverage = subtreeCoverage(node); + const bar = coverage !== undefined ? `${coverageBar(coverage)} ${coverage}%` : ""; + return [bar, diagnosticTally(node.errors, node.warnings)].filter(Boolean).join(" — "); +} + +/** Folder/package row tooltip: name + (package path) + subtree coverage/diagnostics. */ +export function packageTooltip(node: PackageTreeNode): string { + const errs = `${node.errors} error${node.errors === 1 ? "" : "s"}`; + const warns = `${node.warnings} warning${node.warnings === 1 ? "" : "s"}`; + const coverage = subtreeCoverage(node); + return [ + node.fullName, + node.module?.path, + coverage !== undefined ? `Coverage: ${coverage}% (subtree)` : "", + `Subtree: ${errs}, ${warns}`, + ].filter(Boolean).join("\n"); +} diff --git a/vscode-extension/src/module-explorer.ts b/vscode-extension/src/module-explorer.ts index c2f34b04..bb4d4fd7 100644 --- a/vscode-extension/src/module-explorer.ts +++ b/vscode-extension/src/module-explorer.ts @@ -12,96 +12,26 @@ import * as vscode from "vscode"; import { type Store } from "./store"; import { subscribeRevision } from "./reactive-refresh"; import { Logger } from "./logger"; +import { + FULL_COVERAGE_PERCENT, + coverageColor, + diagnosticTally, + moduleDescription, + moduleTooltip, + packageDescription, + packageIconColor, + packageTooltip, + type HealthStats, + type ModuleNode, + type PackageTreeNode, + type SymbolNode, + type WorkspaceModulesResponse, +} from "./module-explorer-render"; import { DiagnosticTreeItem, diagnosticItems, - type DiagnosticNode, } from "./module-explorer-diagnostics"; -// ── LSP response types ─────────────────────────────────────────────────── -// -// Implements the client mirror of [EXTACT-DATA-MODEL] — the shared -// WorkspaceModulesResponse / ModuleNode / SymbolNode / DiagnosticNode / -// HealthStats wire shapes returned by basilisk.workspaceModules. - -interface SymbolNode { - readonly name: string; - readonly kind: "class" | "function" | "variable" | "constant" | "typeAlias"; - readonly line: number; - readonly annotated: boolean; - readonly exported: boolean; - readonly children?: readonly SymbolNode[]; -} - -interface ModuleNode { - readonly name: string; - readonly path: string; - readonly kind: "package" | "module"; - readonly symbols: readonly SymbolNode[]; - // Every diagnostic for this module, rendered as the first drill-down rows so - // the errors/warnings tallies below are navigable, never dead - // ([EXTACT-MODULES-DIAGNOSTICS], #235). Empty while Type Checking is - // disabled; optional only to stay crash-safe against a pre-#235 server - // binary supplied via basilisk.executablePath. - readonly diagnostics?: readonly DiagnosticNode[]; - // Health rollup folded into each module by basilisk.workspaceModules - // [EXTACT-MODULES] — coverage %, diagnostic counts, and adoption state, so the - // merged panel needs no separate basilisk.typeHealth round-trip. ABSENT while - // Type Checking is disabled ([ANALYSIS-ENABLED], #119): the server omits all - // grading, so there is nothing to render as "% typed" or a red tint. - readonly coveragePercent?: number; - readonly errors?: number; - readonly warnings?: number; - readonly adopted?: boolean; -} - -/** Workspace-wide health rollup carried alongside the module list. */ -interface HealthStats { - // The Type Checking toggle state stamped by the server ([ANALYSIS-ENABLED], - // #119). `false` means the grading fields below are absent by construction. - readonly typeCheckingEnabled?: boolean; - readonly totalSymbols?: number; - readonly annotatedSymbols?: number; - readonly coveragePercent?: number; - readonly errors?: number; - readonly warnings?: number; - readonly adoptedFiles?: number; - readonly totalFiles: number; - // Whether the server's initial workspace scan has finished. A zero-file - // rollup only means "empty workspace" when this is true; before that it - // means "not scanned yet" ([EXTACT-MODULES-HEADER-LOADING], #144). - readonly scanComplete?: boolean; -} - -interface WorkspaceModulesResponse { - readonly modules: readonly ModuleNode[]; - readonly workspace: HealthStats; -} - -/** - * A node in the client-reconstructed package/folder tree - * [EXTACT-MODULES-TREE-STRUCTURE] (#149). The LSP returns a *flat* list of - * modules keyed by dotted name (e.g. `pkg.sub.mod`); the nested tree is rebuilt - * here by splitting each name into path segments. Intermediate folders that are - * not themselves Python packages are synthesised as container nodes with no - * `module`, so the panel renders `pkg/ → sub/ → mod` instead of a flat list. - */ -interface PackageTreeNode { - /** Last path segment — the row's display label (e.g. `auth`). */ - readonly segment: string; - /** Fully-qualified dotted prefix up to and including this node. */ - readonly fullName: string; - /** The module/package file mapping exactly here, if one exists. */ - module?: ModuleNode; - /** Child packages and modules, keyed by their segment. */ - readonly children: Map; - // Diagnostics rolled up across this node's whole subtree (self module + - // every descendant). Surfaced on the folder/package row so errors are - // visible without drilling into the hierarchy (#149). Set by `rollup`. - errors: number; - warnings: number; -} - // ── Tree items ─────────────────────────────────────────────────────────── type TreeItem = ModuleTreeItem | SymbolTreeItem | PackageTreeItem | DiagnosticTreeItem; @@ -229,101 +159,6 @@ function symbolIcon(symbol: SymbolNode): vscode.ThemeIcon { } } -// ── Coverage rendering [EXTACT-MODULES] ────────────────────────────────── - -/** Width of the Unicode coverage bar in characters. */ -const COVERAGE_BAR_WIDTH = 10; -/** Coverage threshold for "good" (green). */ -const COVERAGE_GOOD_THRESHOLD = 90; -/** Coverage threshold for "warning" (yellow); below it is red. */ -const COVERAGE_WARN_THRESHOLD = 50; -/** Neutral coverage for ungraded rows (Type Checking disabled, #119). */ -const FULL_COVERAGE_PERCENT = 100; - -/** Render a coverage progress bar using Unicode block characters. */ -function coverageBar(percent: number): string { - const filled = Math.round(percent / COVERAGE_BAR_WIDTH); - return "█".repeat(filled) + "░".repeat(COVERAGE_BAR_WIDTH - filled); -} - -/** Theme color for a coverage percentage: green >=90%, yellow >=50%, else red. */ -function coverageColor(percent: number): vscode.ThemeColor { - if (percent >= COVERAGE_GOOD_THRESHOLD) { return new vscode.ThemeColor("testing.iconPassed"); } - if (percent >= COVERAGE_WARN_THRESHOLD) { return new vscode.ThemeColor("list.warningForeground"); } - return new vscode.ThemeColor("list.errorForeground"); -} - -// [EXTACT-MODULES-COUNT-STYLE] is the diagnostic-tally surface for module rows. -/** Module row description: coverage bar + % + error/warning counts + adopted badge. */ -function moduleDescription(module: ModuleNode): string { - // Type Checking disabled (#119): the server serves no grading, so the row is - // a plain navigation entry — no bar, no percentage, no tallies. - if (module.coveragePercent === undefined) { return ""; } - const issueTally = diagnosticTally(module.errors ?? 0, module.warnings ?? 0); - const issueStr = issueTally === "" ? "" : ` — ${issueTally}`; - const badge = module.adopted === true ? " [adopted]" : ""; - return `${coverageBar(module.coveragePercent)} ${module.coveragePercent}%${issueStr}${badge}`; -} - -/** Module row tooltip: name + path + coverage + diagnostics + adoption. */ -function moduleTooltip(module: ModuleNode): string { - return [ - module.name, - module.path, - module.coveragePercent !== undefined ? `Coverage: ${module.coveragePercent}%` : "", - module.errors !== undefined ? `Errors: ${module.errors}` : "", - module.warnings !== undefined ? `Warnings: ${module.warnings}` : "", - module.adopted === true ? "Status: Adopted (errors demoted to warnings)" : "", - ].filter(Boolean).join("\n"); -} - -/** Implements [EXTACT-MODULES-COUNT-STYLE]: coloured glyphs `🔴 n` (errors) / - * `🟠 n` (warnings) — never `nE nW`; a zero severity is omitted, or "" when clean. */ -function diagnosticTally(errors: number, warnings: number): string { - const issues: string[] = []; - if (errors > 0) { issues.push(`🔴 ${errors}`); } - if (warnings > 0) { issues.push(`🟠 ${warnings}`); } - return issues.join(" "); -} - -/** - * Folder/package icon tint: red if the subtree holds any error, else yellow if - * any warning, else a package's own coverage colour (a pure folder stays - * untinted). Lets a folder with hidden errors read red without expanding (#149). - */ -function packageIconColor(node: PackageTreeNode): vscode.ThemeColor | undefined { - if (node.errors > 0) { return new vscode.ThemeColor("list.errorForeground"); } - if (node.warnings > 0) { return new vscode.ThemeColor("list.warningForeground"); } - // No coverage served (Type Checking disabled, #119) → untinted, like a folder. - return node.module?.coveragePercent !== undefined - ? coverageColor(node.module.coveragePercent) - : undefined; -} - -/** - * Folder/package row description: the subtree's rolled-up count-style tally - * ([EXTACT-MODULES-COUNT-STYLE]) so problems are visible without drilling in - * (#149). A package (`__init__.py`) also keeps its own coverage bar. - */ -function packageDescription(node: PackageTreeNode): string { - const coverage = node.module?.coveragePercent; - const own = coverage !== undefined ? `${coverageBar(coverage)} ${coverage}%` : ""; - return [own, diagnosticTally(node.errors, node.warnings)].filter(Boolean).join(" — "); -} - -/** Folder/package row tooltip: name + (package path/coverage) + subtree diagnostics. */ -function packageTooltip(node: PackageTreeNode): string { - const errs = `${node.errors} error${node.errors === 1 ? "" : "s"}`; - const warns = `${node.warnings} warning${node.warnings === 1 ? "" : "s"}`; - const coverage = node.module?.coveragePercent; - return [ - node.fullName, - node.module?.path, - coverage !== undefined ? `Coverage: ${coverage}%` : "", - `Subtree: ${errs}, ${warns}`, - ].filter(Boolean).join("\n"); -} - // ── Workspace health chrome [EXTACT-MODULES-HEADER] ────────────────────── /** Loading affordance while the analyzer starts up or its initial workspace @@ -537,14 +372,14 @@ export class ModuleExplorerProvider implements vscode.TreeDataProvider * (`pkg/__init__.py`, dotted name `pkg`) shares its node with the `pkg/` folder. */ private static buildPackageTree(modules: readonly ModuleNode[]): PackageTreeNode { - const root: PackageTreeNode = { segment: "", fullName: "", children: new Map(), errors: 0, warnings: 0 }; + const root = ModuleExplorerProvider.emptyNode("", ""); for (const module of modules) { const segments = module.name.split(".").filter((seg) => seg !== ""); let node = root; for (const segment of segments) { const fullName = node.fullName === "" ? segment : `${node.fullName}.${segment}`; const existing = node.children.get(segment); - const child = existing ?? { segment, fullName, children: new Map(), errors: 0, warnings: 0 }; + const child = existing ?? ModuleExplorerProvider.emptyNode(segment, fullName); if (existing === undefined) { node.children.set(segment, child); } node = child; } @@ -553,6 +388,20 @@ export class ModuleExplorerProvider implements vscode.TreeDataProvider return root; } + /** A fresh container node with zeroed rollup accumulators (filled by `rollup`). */ + private static emptyNode(segment: string, fullName: string): PackageTreeNode { + return { + segment, + fullName, + children: new Map(), + errors: 0, + warnings: 0, + totalSymbols: 0, + annotatedSymbols: 0, + graded: false, + }; + } + /** * Roll each subtree's diagnostics up onto its container node (post-order), so a * folder/package row can show the total errors/warnings hiding beneath it @@ -561,13 +410,22 @@ export class ModuleExplorerProvider implements vscode.TreeDataProvider private static rollup(node: PackageTreeNode): void { let errors = node.module?.errors ?? 0; let warnings = node.module?.warnings ?? 0; + let totalSymbols = node.module?.totalSymbols ?? 0; + let annotatedSymbols = node.module?.annotatedSymbols ?? 0; + let graded = node.module?.coveragePercent !== undefined; for (const child of node.children.values()) { ModuleExplorerProvider.rollup(child); errors += child.errors; warnings += child.warnings; + totalSymbols += child.totalSymbols; + annotatedSymbols += child.annotatedSymbols; + graded ||= child.graded; } node.errors = errors; node.warnings = warnings; + node.totalSymbols = totalSymbols; + node.annotatedSymbols = annotatedSymbols; + node.graded = graded; } /** Render a node: a container becomes a package row, a bare leaf a module row. */ diff --git a/vscode-extension/src/test/suite/configuration-editor-focus.test.ts b/vscode-extension/src/test/suite/configuration-editor-focus.test.ts new file mode 100644 index 00000000..27c54e8f --- /dev/null +++ b/vscode-extension/src/test/suite/configuration-editor-focus.test.ts @@ -0,0 +1,215 @@ +// Implements [CONFIGEDITOR-VSIX-EXPERIENCE]: the Configure Severity hover +// deep link — `basilisk.openConfigurationEditor` with a `{ rule }` argument +// opens the editor focused on that rule, and the LSP hover markdown is +// trusted for exactly that one command. + +import * as assert from "assert"; +import * as vscode from "vscode"; +import type { + ApplyConfigurationRequest, + ConfigurationPreview, + ConfigurationSnapshot, + PreviewConfigurationRequest, + RuleOccurrencesRequest, + RuleOccurrencesResponse, +} from "../../configuration-editor-model"; +import { + ConfigurationEditorController, + CONFIGURATION_EDITOR_COMMAND, + configurationEditorFocusRule, + EDIT_CONFIG_COMMAND, + type ConfigurationEditorTransport, +} from "../../configuration-editor"; +import { buildClientOptions, trustConfigureSeverityLinks } from "../../lsp-client"; +import { createStore } from "../../store"; + +const ROOT_URI = "file:///workspace"; +const RULE_CODE = "BSK-0001"; + +function snapshotWithRule(): ConfigurationSnapshot { + return { + rootUri: ROOT_URI, + configUri: `${ROOT_URI}/pyproject.toml`, + revision: "revision-1", + rules: [{ + descriptor: { + code: RULE_CODE, + title: "Missing parameter type annotation", + summary: "All function parameters require explicit types.", + docsUrl: `https://example.test/errors/${RULE_CODE}`, + tags: ["basilisk", "annotations"], + }, + entry: undefined, + effectiveSeverity: { kind: "Error" }, + diagnosticCount: 1, + }], + tags: [], + }; +} + +/** Snapshot-only transport; preview/apply/occurrences are unreachable here. */ +function snapshotTransport(): ConfigurationEditorTransport { + return { + async snapshot(_rootUri: string): Promise { + return snapshotWithRule(); + }, + async preview(_request: PreviewConfigurationRequest): Promise { + throw new Error("preview is not under test"); + }, + async apply(_request: ApplyConfigurationRequest): Promise { + throw new Error("apply is not under test"); + }, + async occurrences(_request: RuleOccurrencesRequest): Promise { + throw new Error("occurrences is not under test"); + }, + }; +} + +async function pollUntil(predicate: () => boolean, timeoutMs = 5_000): Promise { + const deadline = Date.now() + timeoutMs; + while (!predicate() && Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, 25)); + } + assert.ok(predicate(), "condition did not become true before timeout"); +} + +suite("Configuration editor — Configure Severity deep link", () => { + // The command argument is untrusted webview/markdown input: only a bounded, + // non-empty `{ rule: string }` yields a focus target. + test("decodes only a bounded { rule } command argument", () => { + assert.strictEqual(configurationEditorFocusRule({ rule: RULE_CODE }), RULE_CODE); + assert.strictEqual(configurationEditorFocusRule(undefined), undefined); + assert.strictEqual(configurationEditorFocusRule(null), undefined); + assert.strictEqual(configurationEditorFocusRule(RULE_CODE), undefined); + assert.strictEqual(configurationEditorFocusRule({ rule: 7 }), undefined); + assert.strictEqual(configurationEditorFocusRule({ rule: "" }), undefined); + assert.strictEqual(configurationEditorFocusRule({ rule: "x".repeat(65) }), undefined); + assert.strictEqual(configurationEditorFocusRule([{ rule: RULE_CODE }]), undefined); + }); + + // Store semantics: string sets, undefined (internal refresh) preserves for + // the same root, null (plain open) clears, and a root change clears. + test("focusRule is set, survives same-root refreshes, and clears on plain open", () => { + const store = createStore(); + store.beginConfigurationLoad(ROOT_URI, RULE_CODE); + assert.strictEqual(store.configurationEditor.value.focusRule, RULE_CODE); + + store.beginConfigurationLoad(ROOT_URI); + assert.strictEqual(store.configurationEditor.value.focusRule, RULE_CODE); + + store.acceptConfigurationSnapshot(snapshotWithRule()); + assert.strictEqual(store.configurationEditor.value.focusRule, RULE_CODE); + + store.beginConfigurationLoad(ROOT_URI, null); + assert.strictEqual(store.configurationEditor.value.focusRule, undefined); + + store.beginConfigurationLoad(ROOT_URI, RULE_CODE); + store.beginConfigurationLoad("file:///elsewhere"); + assert.strictEqual(store.configurationEditor.value.focusRule, undefined); + }); + + // The controller's open() carries the focus target through its load chain + // into the state the webview renders; a later plain open clears it. + test("open(rootUri, rule) keeps the focus target through load; plain open clears it", async () => { + const store = createStore(); + const controller = new ConfigurationEditorController(store, snapshotTransport()); + try { + controller.open(ROOT_URI, RULE_CODE); + await pollUntil(() => store.configurationEditor.value.phase === "ready"); + assert.strictEqual(store.configurationEditor.value.focusRule, RULE_CODE); + + controller.open(ROOT_URI); + await pollUntil(() => store.configurationEditor.value.phase === "ready"); + assert.strictEqual(store.configurationEditor.value.focusRule, undefined); + } finally { + controller.dispose(); + } + }); + + // Hover markdown from the LSP must become clickable for exactly the one + // configuration-editor command — nothing else gets trusted. + test("hover middleware trusts exactly the openConfigurationEditor command", () => { + const markdown = new vscode.MarkdownString( + `[Configure Severity](command:${CONFIGURATION_EDITOR_COMMAND}?%5B%7B%22rule%22%3A%22${RULE_CODE}%22%7D%5D)`, + ); + const hover = trustConfigureSeverityLinks(new vscode.Hover([markdown])); + assert.ok(hover); + const [content] = hover.contents; + assert.ok(content instanceof vscode.MarkdownString); + assert.deepStrictEqual(content.isTrusted, { enabledCommands: [CONFIGURATION_EDITOR_COMMAND] }); + + assert.strictEqual(trustConfigureSeverityLinks(null), null); + assert.strictEqual(trustConfigureSeverityLinks(undefined), undefined); + }); + + // The wiring, not just the helper: buildClientOptions must actually route + // hovers through trustConfigureSeverityLinks — deleting the provideHover + // middleware line would pass the helper test above but fail this one. + test("buildClientOptions pipes hovers through the trust middleware", async () => { + const trace = vscode.window.createOutputChannel("bsk-focus-test-trace", { log: true }); + const options = buildClientOptions(undefined, trace, () => undefined); + try { + const provideHover = options.middleware?.provideHover; + assert.ok(provideHover, "client options must register the hover middleware"); + const markdown = new vscode.MarkdownString( + `[Configure Severity](command:${CONFIGURATION_EDITOR_COMMAND})`, + ); + const hover = await provideHover( + {} as vscode.TextDocument, + new vscode.Position(0, 0), + new vscode.CancellationTokenSource().token, + async () => new vscode.Hover([markdown]), + ); + assert.ok(hover, "middleware must return the server's hover"); + const [content] = hover.contents; + assert.ok(content instanceof vscode.MarkdownString); + assert.deepStrictEqual( + content.isTrusted, + { enabledCommands: [CONFIGURATION_EDITOR_COMMAND] }, + "hover leaving the middleware must trust exactly the one command", + ); + } finally { + const watcher = options.synchronize?.fileEvents; + if (watcher !== undefined && !Array.isArray(watcher)) { watcher.dispose(); } + trace.dispose(); + } + }); + + // The registerCommand glue [CONFIGEDITOR-VSIX-EXPERIENCE]: executing the + // real basilisk.editConfig with an explorer resource must open the + // configuration editor panel for that resource's workspace folder. + test("basilisk.editConfig opens the configuration editor for the resource's folder", async function () { + this.timeout(60_000); + const folder = vscode.workspace.workspaceFolders?.[0]; + assert.ok(folder, "the e2e suite always opens a workspace folder"); + const resource = vscode.Uri.joinPath(folder.uri, "pyproject.toml"); + + // The command registers once the live server advertises the editor + // capability — poll execution until the capability effect has fired. + const deadline = Date.now() + 45_000; + let lastError: unknown; + let executed = false; + while (!executed && Date.now() < deadline) { + try { + await vscode.commands.executeCommand(EDIT_CONFIG_COMMAND, resource); + executed = true; + } catch (error) { + lastError = error; + await new Promise((resolve) => setTimeout(resolve, 250)); + } + } + assert.ok(executed, `basilisk.editConfig never became executable: ${String(lastError)}`); + + function isConfigTab(tab: vscode.Tab): boolean { + return tab.input instanceof vscode.TabInputWebview + && tab.input.viewType.includes("basilisk.configurationEditor"); + } + await pollUntil(() => + vscode.window.tabGroups.all.some((group) => group.tabs.some(isConfigTab))); + const tab = vscode.window.tabGroups.all + .flatMap((group) => group.tabs) + .find(isConfigTab); + assert.ok(tab, "the configuration editor webview tab must open"); + await vscode.window.tabGroups.close(tab); + }); +}); diff --git a/vscode-extension/src/test/suite/configuration-editor-menu.test.ts b/vscode-extension/src/test/suite/configuration-editor-menu.test.ts new file mode 100644 index 00000000..37d6a73a --- /dev/null +++ b/vscode-extension/src/test/suite/configuration-editor-menu.test.ts @@ -0,0 +1,53 @@ +// Implements [VSIX-CONFIGURATION-EDITOR]: file-explorer context-menu entry point. +/** + * Right-clicking pyproject.toml in the Explorer must offer an "Edit Config" + * item at the very top of the context menu, opening the configuration editor. + * Manifest-level contract, same convention as activity-panel.test.ts. + */ + +import * as assert from "assert"; +import { + getPackageJsonCommands, + getPackageJsonMenu, +} from "./profiler-test-constants"; + +suite("Configuration editor — pyproject.toml explorer context menu", () => { + test("pyproject.toml context menu has an Edit Config item at the top", function () { + const commands = getPackageJsonCommands(); + const editConfig = commands.find((entry) => entry.title === "Edit Config"); + assert.ok( + editConfig, + 'package.json must declare a command titled "Edit Config" for the explorer context menu', + ); + + const explorerMenu = getPackageJsonMenu("explorer/context"); + const menuEntry = explorerMenu.find((entry) => entry.command === editConfig.command); + assert.ok( + menuEntry, + `"${editConfig.command}" must be contributed to the explorer/context menu; got: ${ + explorerMenu.map((entry) => entry.command).join(", ") || "(no explorer/context menu)" + }`, + ); + + // Scoped to pyproject.toml only — never on unrelated files. + assert.ok( + menuEntry.when?.includes("resourceFilename == pyproject.toml"), + `Edit Config must target pyproject.toml via resourceFilename; got when: "${menuEntry.when}"`, + ); + + // Gated on editor support so it never renders a dead item when the + // server lacks the configuration editor (same rule as the view-title gears). + assert.ok( + menuEntry.when?.includes("basilisk.configurationEditorSupported"), + `Edit Config must be gated on basilisk.configurationEditorSupported; got when: "${menuEntry.when}"`, + ); + + // "Right at the top": the navigation group always renders first in + // explorer/context, ahead of every numbered group. + assert.match( + menuEntry.group ?? "", + /^navigation(@\d+)?$/, + `Edit Config must sit in the top-most (navigation) group; got group: "${menuEntry.group}"`, + ); + }); +}); diff --git a/vscode-extension/src/test/suite/configuration-editor-webview-dom.test.ts b/vscode-extension/src/test/suite/configuration-editor-webview-dom.test.ts new file mode 100644 index 00000000..e1cf9e5a --- /dev/null +++ b/vscode-extension/src/test/suite/configuration-editor-webview-dom.test.ts @@ -0,0 +1,487 @@ +// Tests [CONFIGEDITOR-VSIX-EXPERIENCE] webview runtime behaviour in a REAL +// webview DOM. See docs/specs/LSP-CONFIGURATION-EDITOR-SPEC.md#CONFIGEDITOR-VSIX-EXPERIENCE. +// +// Regression test for the stale RULE DETAIL panel: after selecting a rule, +// every virtualized re-render called restoreFocus() without preventScroll, +// which yanked the viewport back to the previously selected rule on every +// scroll frame. Basilisk (BSK-*) rules sit below the pep rules in the catalog, +// so they could never be scrolled to or clicked — the detail panel displayed +// stale data from the previously selected rule forever. +// +// String-containment tests (configuration-editor-webview.test.ts) cannot catch +// this: the bug is an interaction between focus(), scroll anchoring, and the +// row rebuild — so this suite runs the real CSP-locked document inside a real +// VS Code webview (Chromium) and drives it like a user. + +import * as assert from "assert"; +import * as vscode from "vscode"; +import { buildConfigurationEditorDocument } from "../../configuration-editor-document"; +import { embedJson } from "../../profiler-webview"; + +/** Rows are 112px tall (ROW_HEIGHT in the webview script); 3 rows visible. */ +const VIEWPORT_HEIGHT_PX = 336; +const PEP_RULE_COUNT = 40; +const BASILISK_RULE_COUNT = 5; +const RESULT_TIMEOUT_MS = 30_000; + +interface DomTestResult { + readonly ok: boolean; + readonly reason?: string; + readonly headingAfterPep?: string; + readonly scrollTopAfterScroll?: number; + readonly maxScrollTop?: number; + readonly bskRowRendered?: boolean; + readonly detailHeading?: string; + readonly searchValue?: string; + readonly filteredCount?: string; + readonly searchAfterPush?: string; + readonly detailAfterPush?: string; + readonly pepRuleSelect?: string; + readonly pepRuleHasDisabledOption?: boolean; + readonly disabledRuleSelect?: string; + readonly disabledRuleHasDisabledOption?: boolean; + readonly pepTagSelect?: string; + readonly basiliskTagSelect?: string; +} + +/** A realistic snapshot: pep rules first, basilisk rules at the bottom. */ +function fixtureSnapshot(): unknown { + const pepRules = Array.from({ length: PEP_RULE_COUNT }, (_ignored, index) => ({ + descriptor: { + code: `pep_rule_${String(index).padStart(3, "0")}`, + title: `PEP rule ${index}`, + summary: `Summary for pep rule ${index}`, + tags: ["pep", "generics"], + docsUrl: `https://www.basilisk-python.dev/errors/pep-${index}`, + }, + entry: undefined, + effectiveSeverity: { kind: "Error" }, + diagnosticCount: index, + })); + const basiliskRules = Array.from({ length: BASILISK_RULE_COUNT }, (_ignored, index) => ({ + descriptor: { + code: `BSK-${String(index + 1).padStart(4, "0")}`, + title: `Basilisk rule ${index + 1}`, + summary: `Summary for basilisk rule ${index + 1}`, + tags: ["basilisk", "strictness"], + docsUrl: `https://www.basilisk-python.dev/errors/BSK-${String(index + 1).padStart(4, "0")}`, + }, + entry: undefined, + // The last basilisk rule resolves to Disabled ([CHKARCH-CONFIG-MODEL] + // step 3: an untouched analyze rule does not run) — the no-entry select + // display-value test reads it. + effectiveSeverity: index === BASILISK_RULE_COUNT - 1 ? { kind: "Disabled" } : { kind: "Error" }, + diagnosticCount: index + 1, + })); + return { + rootUri: "file:///workspace/project", + configUri: "file:///workspace/project/pyproject.toml", + revision: "fnv1a64:test", + rules: [...pepRules, ...basiliskRules], + tags: [ + { name: "basilisk", kind: { kind: "Provenance" }, entry: undefined, ruleCount: BASILISK_RULE_COUNT, diagnosticCount: 15 }, + { name: "pep", kind: { kind: "Provenance" }, entry: undefined, ruleCount: PEP_RULE_COUNT, diagnosticCount: 780 }, + ], + }; +} + +/** + * In-page fake of the ConfigurationEditorController/store loop: answers the + * runtime's `ready` and `occurrences` intents with the same state pushes the + * real host produces, and keeps the REAL acquireVsCodeApi handle for the + * driver to report results back to the extension host. + */ +function hostShimScript(focusRule: string | null = null): string { + return ` + const __realApi = acquireVsCodeApi(); + window.__realApi = __realApi; + // Boot beacon + page-error reporting: lets the extension-side test tell + // "webview never loaded" apart from "driver hung" and surfaces script + // errors that would otherwise silently eat the result. + __realApi.postMessage({ type: 'domTestBoot' }); + window.addEventListener('error', (event) => { + __realApi.postMessage({ type: 'domTestResult', ok: false, reason: 'page error: ' + event.message }); + }); + const __snapshot = ${embedJson(fixtureSnapshot())}; + let __state = { + phase: 'ready', rootUri: __snapshot.rootUri, snapshot: __snapshot, + preview: undefined, occurrences: undefined, occurrencesLoading: false, + repairUri: undefined, message: 'Configuration is up to date', refreshRequested: false, + focusRule: ${embedJson(focusRule)}, + }; + const __push = (partial) => { + __state = Object.assign({}, __state, partial); + window.dispatchEvent(new MessageEvent('message', { data: { type: 'state', state: __state } })); + }; + // Drivers re-push state to simulate later host updates (one-shot focus). + window.__push = __push; + window.acquireVsCodeApi = () => ({ + postMessage(message) { + if (message.type === 'ready') { + setTimeout(() => __push({}), 0); + } else if (message.type === 'occurrences') { + const code = message.selector && message.selector.codes ? message.selector.codes[0] : ''; + setTimeout(() => __push({ occurrences: undefined, occurrencesLoading: true }), 0); + setTimeout(() => __push({ + occurrences: { + items: [{ + code, + uri: 'file:///workspace/project/app.py', + range: { start: { line: 1, character: 0 }, end: { line: 1, character: 4 } }, + severity: { kind: 'Error' }, + }], + nextCursor: undefined, + }, + occurrencesLoading: false, + }), 30); + } + }, + getState() { return undefined; }, + setState() {}, + }); + `; +} + +/** + * Drives the runtime like a user: select the first (pep) rule, scroll down in + * wheel-sized increments toward the basilisk rules, click one, and report what + * the RULE DETAIL panel shows. + */ +function driverScript(): string { + return ` + (async () => { + const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + const heading = () => { + const node = document.querySelector('#detail-content h3'); + return node && node.textContent ? node.textContent : ''; + }; + const report = (result) => window.__realApi.postMessage(Object.assign({ type: 'domTestResult' }, result)); + try { + const viewport = document.getElementById('rule-viewport'); + viewport.style.height = '${VIEWPORT_HEIGHT_PX}px'; + viewport.style.minHeight = '${VIEWPORT_HEIGHT_PX}px'; + viewport.style.maxHeight = '${VIEWPORT_HEIGHT_PX}px'; + let waited = 0; + while (!document.querySelector('[data-show-rule]') && waited < 200) { await sleep(25); waited += 1; } + if (!document.querySelector('[data-show-rule]')) { + report({ ok: false, reason: 'snapshot never rendered' }); + return; + } + // 1. The user selects a pep rule; the occurrences round trip re-renders. + const pepButton = document.querySelector('[data-show-rule="pep_rule_000"]'); + pepButton.focus(); + pepButton.click(); + await sleep(250); + const headingAfterPep = heading(); + // 2. The user scrolls toward the basilisk rules in wheel-sized steps. + for (let step = 0; step < 30; step += 1) { + viewport.scrollTop = viewport.scrollTop + 300; + await sleep(40); + } + await sleep(150); + const scrollTopAfterScroll = viewport.scrollTop; + const maxScrollTop = viewport.scrollHeight - viewport.clientHeight; + // 3. The user clicks the last basilisk rule. + const bskButton = document.querySelector('[data-show-rule="BSK-0005"]'); + if (bskButton) { + bskButton.focus(); + bskButton.click(); + await sleep(250); + } + report({ + ok: true, + headingAfterPep, + scrollTopAfterScroll, + maxScrollTop, + bskRowRendered: bskButton !== null, + detailHeading: heading(), + }); + } catch (error) { + report({ ok: false, reason: String(error) }); + } + })(); + `; +} + +/** + * The Configure Severity deep-link scenario ([CONFIGEDITOR-VSIX-EXPERIENCE]): + * the state arrives with a focusRule; the runtime must prefill the search + * filter with the code and open that rule's detail panel — no user input. + */ +function focusDriverScript(): string { + return ` + (async () => { + const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + const heading = () => { + const node = document.querySelector('#detail-content h3'); + return node && node.textContent ? node.textContent : ''; + }; + const report = (result) => window.__realApi.postMessage(Object.assign({ type: 'domTestResult' }, result)); + try { + let waited = 0; + while (!document.querySelector('[data-rule-code]') && waited < 200) { await sleep(25); waited += 1; } + // Allow the occurrences round trip triggered by showRule to settle. + await sleep(300); + report({ + ok: true, + searchValue: document.getElementById('rule-search').value, + filteredCount: document.getElementById('filter-result').textContent, + detailHeading: heading(), + }); + } catch (error) { + report({ ok: false, reason: String(error) }); + } + })(); + `; +} + +/** + * One-shot semantics: focus is applied on the first snapshot render ONLY — + * once the user edits the search, later host state pushes (still carrying the + * same focusRule) must never stomp their filter or selection. + */ +function oneShotDriverScript(): string { + return ` + (async () => { + const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + const heading = () => { + const node = document.querySelector('#detail-content h3'); + return node && node.textContent ? node.textContent : ''; + }; + const report = (result) => window.__realApi.postMessage(Object.assign({ type: 'domTestResult' }, result)); + try { + let waited = 0; + while (!document.querySelector('[data-rule-code]') && waited < 200) { await sleep(25); waited += 1; } + await sleep(300); + const searchValue = document.getElementById('rule-search').value; + // The user retargets the filter to a pep rule... + const search = document.getElementById('rule-search'); + search.value = 'pep_rule_001'; + search.dispatchEvent(new Event('input', { bubbles: true })); + await sleep(100); + // ...then the host pushes a later state still carrying the focusRule. + window.__push({}); + await sleep(250); + report({ + ok: true, + searchValue, + searchAfterPush: document.getElementById('rule-search').value, + filteredCount: document.getElementById('filter-result').textContent, + detailAfterPush: heading(), + }); + } catch (error) { + report({ ok: false, reason: String(error) }); + } + })(); + `; +} + +/** + * Chrome scenario: a no-entry select must DISPLAY the resolved severity — + * an untouched pep rule/tag reads Error, an untouched analyze rule its + * effective value (Disabled for one that does not run), an untouched + * non-pep tag Disabled — and only non-pep controls offer Disabled at all. + */ +function selectValueDriverScript(): string { + return ` + (async () => { + const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + const report = (result) => window.__realApi.postMessage(Object.assign({ type: 'domTestResult' }, result)); + const optionValues = (select) => Array.from(select.options).map((option) => option.value); + try { + let waited = 0; + while (!document.querySelector('[data-rule-code]') && waited < 200) { await sleep(25); waited += 1; } + const pepRule = document.querySelector('select[data-rule-entry="pep_rule_000"]'); + const pepTag = document.querySelector('select[data-tag-entry="pep"]'); + const basiliskTag = document.querySelector('select[data-tag-entry="basilisk"]'); + // The Disabled analyze rule sits below the virtual window — filter to it. + const search = document.getElementById('rule-search'); + search.value = 'BSK-0005'; + search.dispatchEvent(new Event('input', { bubbles: true })); + await sleep(150); + const disabledRule = document.querySelector('select[data-rule-entry="BSK-0005"]'); + if (!pepRule || !pepTag || !basiliskTag || !disabledRule) { + report({ ok: false, reason: 'expected entry selects did not render' }); + return; + } + report({ + ok: true, + pepRuleSelect: pepRule.value, + pepRuleHasDisabledOption: optionValues(pepRule).includes('Disabled'), + disabledRuleSelect: disabledRule.value, + disabledRuleHasDisabledOption: optionValues(disabledRule).includes('Disabled'), + pepTagSelect: pepTag.value, + basiliskTagSelect: basiliskTag.value, + }); + } catch (error) { + report({ ok: false, reason: String(error) }); + } + })(); + `; +} + +/** Inject the shim before and the driver after the real runtime, same nonce. */ +function harnessDocument(driver: string, focusRule: string | null = null): string { + const html = buildConfigurationEditorDocument(); + const openTag = /\n", `;\n${driver}\n`); +} + +async function runWebviewScenario(document: string): Promise { + // A hidden webview gets its timers throttled and requestAnimationFrame + // paused, which starves both the driver and the virtualized rule window — + // start from a clean editor area so the panel is frontmost and stays so. + await vscode.commands.executeCommand("workbench.action.closeAllEditors"); + const panel = vscode.window.createWebviewPanel( + "basilisk.configurationEditorDomTest", + "Configuration Editor DOM Test", + vscode.ViewColumn.One, + { enableScripts: true, retainContextWhenHidden: true, localResourceRoots: [] }, + ); + try { + return await new Promise((resolve, reject) => { + let booted = false; + const timer = setTimeout(() => { + reject(new Error( + "the webview driver never reported a result " + + `(boot beacon ${booted ? "received" : "missing"}; panel visible=${panel.visible}, active=${panel.active})`, + )); + }, RESULT_TIMEOUT_MS); + panel.webview.onDidReceiveMessage((message: DomTestResult & { type?: string }) => { + if (message.type === "domTestBoot") { + booted = true; + } else if (message.type === "domTestResult") { + clearTimeout(timer); + resolve(message); + } + }); + panel.webview.html = document; + }); + } finally { + panel.dispose(); + } +} + +suite("Configuration editor — rule detail panel in a real webview DOM", () => { + // The reported bug: basilisk rules show no details — clicking one leaves the + // detail panel on stale data from the previously selected (pep) rule, + // because restoreFocus() yanks every scroll back to that rule's row. + test("scrolling to and clicking a basilisk rule updates the rule detail panel", async function () { + this.timeout(RESULT_TIMEOUT_MS + 15_000); + const result = await runWebviewScenario(harnessDocument(driverScript())); + assert.strictEqual(result.ok, true, `webview driver failed: ${result.reason ?? "unknown"}`); + assert.ok( + result.headingAfterPep?.includes("pep_rule_000"), + `selecting a pep rule must populate the detail panel (got "${result.headingAfterPep}")`, + ); + assert.ok( + result.bskRowRendered, + "scrolling toward the basilisk rules must reach them — the viewport was yanked back to the " + + `previously selected rule (scrollTop ${result.scrollTopAfterScroll} of ${result.maxScrollTop})`, + ); + assert.ok( + result.detailHeading?.includes("BSK-0005"), + `clicking a basilisk rule must show ITS detail, not stale data (panel shows "${result.detailHeading}")`, + ); + }); + + // [CONFIGEDITOR-VSIX-EXPERIENCE]: the Configure Severity hover deep link — + // a state carrying focusRule must open the editor "to the right place": + // search prefilled with the code, the list filtered to it, and the rule's + // detail panel open, all without any user interaction. + test("a focusRule state opens the editor focused on that rule", async function () { + this.timeout(RESULT_TIMEOUT_MS + 15_000); + const result = await runWebviewScenario(harnessDocument(focusDriverScript(), "BSK-0003")); + assert.strictEqual(result.ok, true, `webview driver failed: ${result.reason ?? "unknown"}`); + assert.strictEqual( + result.searchValue, + "BSK-0003", + `the search filter must be prefilled with the focused rule code (got "${result.searchValue}")`, + ); + assert.ok( + result.filteredCount?.startsWith("1 "), + `the rule list must be filtered to the focused rule (got "${result.filteredCount}")`, + ); + assert.ok( + result.detailHeading?.includes("BSK-0003"), + `the focused rule's detail panel must open (panel shows "${result.detailHeading}")`, + ); + }); + + // One-shot: the focus target is applied on the FIRST snapshot render only. + // Later state pushes (occurrences round trips, refreshes) still carry the + // focusRule — they must never stomp the user's own search or selection. + test("a later state push never re-applies the consumed focusRule over the user's search", async function () { + this.timeout(RESULT_TIMEOUT_MS + 15_000); + const result = await runWebviewScenario(harnessDocument(oneShotDriverScript(), "BSK-0003")); + assert.strictEqual(result.ok, true, `webview driver failed: ${result.reason ?? "unknown"}`); + assert.strictEqual(result.searchValue, "BSK-0003", "the deep link must focus first"); + assert.strictEqual( + result.searchAfterPush, + "pep_rule_001", + `a later state push must keep the user's own filter (got "${result.searchAfterPush}")`, + ); + assert.ok( + result.filteredCount?.startsWith("1 "), + `the list must stay filtered to the USER's query, not the focus target (got "${result.filteredCount}")`, + ); + }); + + // A focus target the snapshot does not contain (stale hover, wrong server) + // must be ignored gracefully: no crash, no vacuous filter, no detail panel. + test("an unknown focusRule is ignored without filtering or crashing", async function () { + this.timeout(RESULT_TIMEOUT_MS + 15_000); + const result = await runWebviewScenario(harnessDocument(focusDriverScript(), "BSK-9999")); + assert.strictEqual(result.ok, true, `webview driver failed: ${result.reason ?? "unknown"}`); + assert.strictEqual( + result.searchValue, + "", + `an unknown focus target must not prefill the search (got "${result.searchValue}")`, + ); + assert.ok( + !(result.detailHeading ?? "").includes("BSK-9999"), + `an unknown focus target must not open a detail panel (panel shows "${result.detailHeading}")`, + ); + }); + + // [CHKARCH-CONFIG-MODEL] resolution shown honestly: a no-entry select must + // DISPLAY what no entry resolves to — never a blank or a lying default. + test("no-entry selects display the resolved severity (pep→Error, analyze→effective, non-pep tag→Disabled)", async function () { + this.timeout(RESULT_TIMEOUT_MS + 15_000); + const result = await runWebviewScenario(harnessDocument(selectValueDriverScript())); + assert.strictEqual(result.ok, true, `webview driver failed: ${result.reason ?? "unknown"}`); + assert.strictEqual( + result.pepRuleSelect, + "Error", + `an untouched pep rule runs at error and its select must say so (got "${result.pepRuleSelect}")`, + ); + assert.strictEqual( + result.pepRuleHasDisabledOption, + false, + "pep rule selects must not offer Disabled ([CHKARCH-CONFIG-MODEL])", + ); + assert.strictEqual( + result.disabledRuleSelect, + "Disabled", + `an untouched analyze rule that does not run must display Disabled (got "${result.disabledRuleSelect}")`, + ); + assert.strictEqual( + result.disabledRuleHasDisabledOption, + true, + "analyze rule selects must offer Disabled", + ); + assert.strictEqual( + result.pepTagSelect, + "Error", + `an untouched pep tag grades at error and its select must say so (got "${result.pepTagSelect}")`, + ); + assert.strictEqual( + result.basiliskTagSelect, + "Disabled", + `an untouched non-pep tag does not run and its select must say so (got "${result.basiliskTagSelect}")`, + ); + }); +}); diff --git a/vscode-extension/src/test/suite/configuration-editor-webview.test.ts b/vscode-extension/src/test/suite/configuration-editor-webview.test.ts index 7d6c2e43..e6e400e0 100644 --- a/vscode-extension/src/test/suite/configuration-editor-webview.test.ts +++ b/vscode-extension/src/test/suite/configuration-editor-webview.test.ts @@ -104,7 +104,7 @@ suite("Configuration editor — hardened, accessible document", () => { // [CONFIGEDITOR-VSIX-EXPERIENCE]: the tag-first Rules view is the whole // editor. Tag groups get the tag-entry control; rows get per-rule entry - // controls; pep rows have no Disabled option ([CHKARCH-CONFIG-MODEL]). + // controls; pep controls have no Disabled option ([CHKARCH-CONFIG-MODEL]). test("renders the tag-first Rules view with pep-gated entry controls", () => { const html = buildConfigurationEditorDocument(); assert.ok(html.includes("select.dataset.tagEntry = tag.name"), "tag groups expose the tag-entry control"); @@ -112,11 +112,9 @@ suite("Configuration editor — hardened, accessible document", () => { assert.ok(html.includes("const PEP_TAG = 'pep'")); assert.ok( html.includes("SEVERITIES.filter((value) => value !== 'Disabled')"), - "pep rows must offer error/warning/info/remove-entry and never Disabled", + "pep controls must offer error/warning/info and never Disabled", ); assert.ok(html.includes("isPepRule(rule)")); - assert.ok(html.includes("{ kind: 'RemoveRule', code }"), "remove-entry emits RemoveRule"); - assert.ok(html.includes("{ kind: 'RemoveTag', tag }"), "remove-entry emits RemoveTag"); assert.ok(html.includes("{ kind: 'SetRule', code, severity: { kind: value } }")); assert.ok(html.includes("{ kind: 'SetTag', tag, severity: { kind: value } }")); assert.ok(html.includes("Load more occurrences")); @@ -126,6 +124,36 @@ suite("Configuration editor — hardened, accessible document", () => { assert.ok(html.includes("Open raw")); }); + // [CONFIGEDITOR-VSIX-EXPERIENCE] / [CHKARCH-CONFIG-MODEL]: an entry dropdown + // lists concrete severities only. "No entry" duplicated Disabled — an analyze + // rule or tag with no entry does not run (resolution step 3) — so the + // redundant choice is gone from every select. Disabled is gone from every + // pep-affecting control (pep rows, the pep source tag, PEP-category tags) + // because no disable exists for pep rules. + test("entry dropdowns never offer No entry and pep-affecting controls omit Disabled", () => { + const html = buildConfigurationEditorDocument(); + assert.ok( + !html.includes("[NO_ENTRY].concat"), + "no dropdown may offer a No-entry option", + ); + assert.ok( + html.includes("severityOptions(isPepRule(rule))"), + "rule rows must gate Disabled on pep provenance", + ); + assert.ok( + html.includes("severityOptions(isPepTag(tag))"), + "tag controls must gate Disabled on pep-affecting tags", + ); + assert.ok( + !html.includes("'RemoveRule'"), + "a dropdown change always writes a rule entry — never removes one", + ); + assert.ok( + !html.includes("'RemoveTag'"), + "a dropdown change always writes a tag entry — never removes one", + ); + }); + // [CONFIGEDITOR-ACCEPTANCE]: adoption, path overrides, presets, and // Inherit/Native controls are removed outright — no legacy shims. test("carries no adoption, path-override, preset, or Inherit/Native UI", () => { diff --git a/vscode-extension/src/test/suite/module-explorer-tree.test.ts b/vscode-extension/src/test/suite/module-explorer-tree.test.ts index 2d094ecd..0f7c458b 100644 --- a/vscode-extension/src/test/suite/module-explorer-tree.test.ts +++ b/vscode-extension/src/test/suite/module-explorer-tree.test.ts @@ -35,6 +35,8 @@ interface TestModule { readonly kind: "package" | "module"; readonly symbols: readonly TestSymbol[]; readonly coveragePercent: number; + readonly totalSymbols?: number; + readonly annotatedSymbols?: number; readonly errors: number; readonly warnings: number; readonly adopted: boolean; @@ -47,13 +49,23 @@ function sym(name: string): TestSymbol { function mod( name: string, kind: "package" | "module", - opts: { coverage: number; symbols?: readonly TestSymbol[]; errors?: number; warnings?: number; path?: string }, + opts: { + coverage: number; + symbols?: readonly TestSymbol[]; + totalSymbols?: number; + annotatedSymbols?: number; + errors?: number; + warnings?: number; + path?: string; + }, ): TestModule { return { name, kind, symbols: opts.symbols ?? [], coveragePercent: opts.coverage, + totalSymbols: opts.totalSymbols, + annotatedSymbols: opts.annotatedSymbols, path: opts.path ?? `/ws/${name.split(".").join("/")}.py`, errors: opts.errors ?? 0, warnings: opts.warnings ?? 0, @@ -112,6 +124,12 @@ function labelsOf(items: readonly vscode.TreeItem[]): string[] { return items.map(labelOf); } +/** Theme-colour id of a row's icon tint, or undefined when untinted. */ +function iconColorId(item: vscode.TreeItem): string | undefined { + const icon = item.iconPath as vscode.ThemeIcon; + return (icon.color as { id?: string } | undefined)?.id; +} + // ── Tests ───────────────────────────────────────────────────────────────── // eslint-disable-next-line max-lines-per-function @@ -297,6 +315,215 @@ suite("Module Explorer tree structure [EXTACT-MODULES-TREE-STRUCTURE]", () => { } }); + // Tests [EXTACT-MODULES-TREE-STRUCTURE] coverage rollup: folder/package rows + // must show the subtree's symbol-weighted type-coverage % — not just error + // tallies, and not only the package's own __init__.py coverage. + test("folder/package rows roll up subtree type coverage, symbol-weighted like the workspace header", async () => { + // Weights are chosen so the honest symbol-weighted rollup for `app` + // ((2+1+0) annotated / (2+2+6) total = 30%) differs from a naive average of + // child percentages ((100+50+0)/3 = 50%) — only a weighted rollup passes. + const modules = [ + mod("app", "package", { coverage: 100, totalSymbols: 2, annotatedSymbols: 2 }), + mod("app.api.auth", "module", { coverage: 50, totalSymbols: 2, annotatedSymbols: 1 }), + mod("app.models.user", "module", { coverage: 0, totalSymbols: 6, annotatedSymbols: 0 }), + mod("util", "module", { coverage: 100, totalSymbols: 1, annotatedSymbols: 1 }), + ]; + const provider = new ModuleExplorerProvider(storeWith(modules)); + try { + const roots = await provider.getChildren(); + + const app = roots.find((row) => labelOf(row) === "app"); + assert.ok(app instanceof PackageTreeItem, "'app' is a package container"); + const appDesc = String(app.description); + assert.ok( + appDesc.includes("30%"), + `'app' must show the subtree's symbol-weighted coverage (3/10 = 30%), got: ${appDesc}`, + ); + assert.ok( + appDesc.includes("█") || appDesc.includes("░"), + `'app' must render the coverage bar like module rows do, got: ${appDesc}`, + ); + + // A synthesised pure folder (models/ has no __init__.py, so no module of + // its own) must still show its subtree's coverage — this is the exact + // "folders show no percentage" bug. + const appChildren = await provider.getChildren(app); + const models = appChildren.find((row) => labelOf(row) === "models"); + assert.ok(models instanceof PackageTreeItem, "'models' is a synthesised folder"); + const modelsDesc = String(models.description); + assert.ok( + modelsDesc.includes("0%"), + `pure folder must show its subtree coverage (0/6 = 0%), got: "${modelsDesc}"`, + ); + + const api = appChildren.find((row) => labelOf(row) === "api"); + assert.ok(api instanceof PackageTreeItem, "'api' is a synthesised folder"); + assert.ok( + String(api.description).includes("50%"), + `'api' folder must show its subtree coverage (1/2 = 50%), got: "${String(api.description)}"`, + ); + } finally { + provider.dispose(); + } + }); + + // Tests [EXTACT-MODULES-TREE-STRUCTURE] + [ANALYSIS-ENABLED] (#119): with type + // checking disabled the server omits all grading, so folder rows must render + // NO percentage — never a vacuous 100% conjured from zero data. + test("folder rows show no coverage percentage while type checking is disabled (#119)", async () => { + const ungraded = [ + { name: "app", kind: "package", symbols: [], path: "/ws/app/__init__.py" }, + { name: "app.mod", kind: "module", symbols: [], path: "/ws/app/mod.py" }, + ]; + const provider = new ModuleExplorerProvider(storeWith(ungraded as unknown as readonly TestModule[])); + try { + const roots = await provider.getChildren(); + const app = roots.find((row) => labelOf(row) === "app"); + assert.ok(app instanceof PackageTreeItem, "'app' is a package container"); + assert.ok( + !String(app.description ?? "").includes("%"), + `ungraded folder must show no percentage, got: "${String(app.description)}"`, + ); + } finally { + provider.dispose(); + } + }); + + // Tests [EXTACT-MODULES-TREE-STRUCTURE] icon tint: the folder/package icon + // colour must follow the SUBTREE rollup, never the package's own + // __init__.py coverage — a green __init__.py over a red subtree reads red. + test("package icon tint follows the subtree coverage rollup, not the package's own coverage", async () => { + // `app`'s own module is fully typed (green on its own: 100% ≥ 90), but the + // subtree rolls up to 2/12 ≈ 17% (< 50) — only the rolled-up tint is red. + const modules = [ + mod("app", "package", { coverage: 100, totalSymbols: 2, annotatedSymbols: 2 }), + mod("app.core", "module", { coverage: 0, totalSymbols: 10, annotatedSymbols: 0 }), + ]; + const provider = new ModuleExplorerProvider(storeWith(modules)); + try { + const app = (await provider.getChildren()).find((row) => labelOf(row) === "app"); + assert.ok(app instanceof PackageTreeItem, "'app' is a package container"); + assert.strictEqual( + iconColorId(app), + "list.errorForeground", + "tint must come from the subtree rollup (17% → red), not the package's own 100% (green)", + ); + } finally { + provider.dispose(); + } + }); + + test("package icon tint bands: subtree errors win, then warnings, then coverage colour, untinted when ungraded", async () => { + const cases: readonly { readonly modules: readonly TestModule[]; readonly expected: string | undefined; readonly why: string }[] = [ + { + modules: [ + mod("app", "package", { coverage: 100, totalSymbols: 2, annotatedSymbols: 2 }), + mod("app.core", "module", { coverage: 100, totalSymbols: 2, annotatedSymbols: 2, errors: 1 }), + ], + expected: "list.errorForeground", + why: "a subtree error tints red even when fully typed", + }, + { + modules: [ + mod("app", "package", { coverage: 100, totalSymbols: 2, annotatedSymbols: 2 }), + mod("app.core", "module", { coverage: 100, totalSymbols: 2, annotatedSymbols: 2, warnings: 3 }), + ], + expected: "list.warningForeground", + why: "a warning-only subtree tints yellow", + }, + { + modules: [ + mod("app", "package", { coverage: 100, totalSymbols: 9, annotatedSymbols: 9 }), + mod("app.core", "module", { coverage: 90, totalSymbols: 1, annotatedSymbols: 1 }), + ], + expected: "testing.iconPassed", + why: "a clean ≥90% subtree tints green", + }, + { + modules: [ + mod("app", "package", { coverage: 100, totalSymbols: 1, annotatedSymbols: 1 }), + mod("app.core", "module", { coverage: 0, totalSymbols: 1, annotatedSymbols: 0 }), + ], + expected: "list.warningForeground", + why: "a clean 50–89% subtree tints yellow", + }, + { + modules: [ + { name: "app", kind: "package", symbols: [], path: "/ws/app/__init__.py" }, + { name: "app.core", kind: "module", symbols: [], path: "/ws/app/core.py" }, + ] as unknown as readonly TestModule[], + expected: undefined, + why: "an ungraded subtree (Type Checking disabled, #119) stays untinted", + }, + ]; + for (const { modules, expected, why } of cases) { + const provider = new ModuleExplorerProvider(storeWith(modules)); + try { + const app = (await provider.getChildren()).find((row) => labelOf(row) === "app"); + assert.ok(app instanceof PackageTreeItem, `'app' is a package container (${why})`); + assert.strictEqual(iconColorId(app), expected, why); + } finally { + provider.dispose(); + } + } + }); + + // Tests [EXTACT-MODULES-TREE-STRUCTURE] tooltips: folder tooltips must quote + // the SUBTREE rollup (labelled as such) and module tooltips the row's stats. + test("package tooltip quotes the subtree coverage rollup and subtree tallies; module tooltip its own stats", async () => { + const modules = [ + mod("app", "package", { coverage: 100, totalSymbols: 2, annotatedSymbols: 2 }), + mod("app.core", "module", { + coverage: 0, totalSymbols: 10, annotatedSymbols: 0, errors: 1, warnings: 2, + }), + ]; + const provider = new ModuleExplorerProvider(storeWith(modules)); + try { + const roots = await provider.getChildren(); + const app = roots.find((row) => labelOf(row) === "app"); + assert.ok(app instanceof PackageTreeItem, "'app' is a package container"); + assert.strictEqual(typeof app.tooltip, "string", "package tooltip is plain text"); + const packageTip = app.tooltip as string; + assert.ok( + packageTip.includes("Coverage: 17% (subtree)"), + `package tooltip must quote the rolled-up subtree coverage (2/12 = 17%), not its own 100%, got: ${packageTip}`, + ); + assert.ok( + packageTip.includes("Subtree: 1 error, 2 warnings"), + `package tooltip must tally subtree diagnostics with correct pluralisation, got: ${packageTip}`, + ); + + const core = (await provider.getChildren(app)).find((row) => labelOf(row) === "core"); + assert.ok(core instanceof ModuleTreeItem, "'core' is a leaf module"); + assert.strictEqual(typeof core.tooltip, "string", "module tooltip is plain text"); + const moduleTip = core.tooltip as string; + for (const line of ["app.core", "/ws/app/core.py", "Coverage: 0%", "Errors: 1", "Warnings: 2"]) { + assert.ok(moduleTip.includes(line), `module tooltip must include "${line}", got: ${moduleTip}`); + } + } finally { + provider.dispose(); + } + }); + + // Tests the graded-but-empty branch: a graded subtree with zero symbols is + // vacuously fully typed — it must render 100%, never NaN or a blank. + test("a graded folder with zero symbols renders 100%, never NaN", async () => { + const modules = [ + mod("app", "package", { coverage: 100, totalSymbols: 0, annotatedSymbols: 0 }), + mod("app.core", "module", { coverage: 100, totalSymbols: 0, annotatedSymbols: 0 }), + ]; + const provider = new ModuleExplorerProvider(storeWith(modules)); + try { + const app = (await provider.getChildren()).find((row) => labelOf(row) === "app"); + assert.ok(app instanceof PackageTreeItem, "'app' is a package container"); + const desc = String(app.description); + assert.ok(desc.includes("100%"), `zero-symbol graded folder shows 100%, got: "${desc}"`); + assert.ok(!desc.includes("NaN"), `must never render NaN, got: "${desc}"`); + } finally { + provider.dispose(); + } + }); + test("folder/package rows roll up subtree errors/warnings so problems show without drilling in (#149)", async () => { const modules = [ mod("app", "package", { coverage: 90 }), diff --git a/website/src/_data/rules.json b/website/src/_data/rules.json index 11f6ced3..7e6c6f01 100644 --- a/website/src/_data/rules.json +++ b/website/src/_data/rules.json @@ -9,7 +9,17 @@ ], "summary": "Missing parameter type annotation", "summaryHtml": "Missing parameter type annotation", - "body": [], + "body": [ + { + "type": "text", + "html": "Never fires where the current engine already infers the parameter type: a scalar-literal default (timeout=30 \u2192 int) determines the type, so demanding an annotation there would be redundant (TYPEINF-FUNC-DEFAULTS). Defaults that do NOT determine the type \u2014 None, empty containers, calls, lambdas, arbitrary expressions \u2014 still require an annotation." + }, + { + "type": "code", + "lang": "python", + "code": "def connect(timeout=30): # \u2713 \u2014 type inferred as int\n pass\n\ndef connect(retries): # BSK-0001 \u2014 nothing to infer from\n pass\n\ndef connect(timeout=None): # BSK-0001 \u2014 None does not determine T | None\n pass" + } + ], "group": "Missing Annotations", "docsUrl": "https://www.basilisk-python.dev/errors/BSK-0001" }, @@ -23,7 +33,17 @@ ], "summary": "Missing return type annotation", "summaryHtml": "Missing return type annotation", - "body": [], + "body": [ + { + "type": "text", + "html": "Never fires where the current engine already infers the return type (TYPEINF-FUNC-RETURN): a body whose every return is bare or carries a type-determining literal \u2014 or that has no return at all (None) \u2014 needs no annotation. Returns the engine cannot infer (calls, names, arbitrary expressions) and generators (Generator...) still require one." + }, + { + "type": "code", + "lang": "python", + "code": "def answer(): # \u2713 \u2014 return type inferred as int\n return 42\n\ndef log_it(msg: str): # \u2713 \u2014 no return: inferred as None\n print(msg)\n\ndef fetch(): # BSK-0002 \u2014 call result is not inferable\n return make_value()" + } + ], "group": "Missing Annotations", "docsUrl": "https://www.basilisk-python.dev/errors/BSK-0002" }, diff --git a/website/src/docs/quick-start.md b/website/src/docs/quick-start.md index c64fcb98..baec27db 100644 --- a/website/src/docs/quick-start.md +++ b/website/src/docs/quick-start.md @@ -122,8 +122,8 @@ warnings tell you strictness isn't at full yet: ```toml # pyproject.toml [tool.basilisk.rules] -"BSK-0001" = "warning" # every parameter needs a type annotation -"BSK-0002" = "warning" # every function needs a return type +"BSK-0001" = "warning" # parameters need a type where none is inferable +"BSK-0002" = "warning" # functions need a return type where none is inferable ``` Add an unannotated function to `good.py`: @@ -141,7 +141,7 @@ warning[BSK-0001]: Missing parameter type annotation for `data` | ^^^^ | = help: Add a type annotation: `data: ` - = note: In Basilisk, all function parameters require explicit types + = note: Basilisk requires an explicit parameter type wherever it cannot be inferred; a literal default (e.g. `timeout=30`) infers the type and needs no annotation = see: https://www.basilisk-python.dev/errors/BSK-0001 warning[BSK-0002]: Missing return type annotation for function `process` @@ -151,7 +151,7 @@ warning[BSK-0002]: Missing return type annotation for function `process` | ^^^^^^^^^^^^^ | = help: Add a return type: `def process(...) -> :` - = note: In Basilisk, all functions require an explicit return type + = note: Basilisk requires an explicit return type wherever it cannot be inferred; literal-only returns (e.g. `return 42`) infer the type and need no annotation = see: https://www.basilisk-python.dev/errors/BSK-0002 Found 2 diagnostics (0 errors). diff --git a/website/src/zh/docs/quick-start.md b/website/src/zh/docs/quick-start.md index f7af934e..102d45bf 100644 --- a/website/src/zh/docs/quick-start.md +++ b/website/src/zh/docs/quick-start.md @@ -115,8 +115,8 @@ Basilisk 自己的严格性规则(每个参数、返回类型、属性等都 ```toml # pyproject.toml [tool.basilisk.rules] -"BSK-0001" = "warning" # 每个参数都需要类型注解 -"BSK-0002" = "warning" # 每个函数都需要返回类型 +"BSK-0001" = "warning" # 无法推断类型的参数需要类型注解 +"BSK-0002" = "warning" # 无法推断返回类型的函数需要返回类型注解 ``` 在 `good.py` 中添加一个没有注解的函数: @@ -134,7 +134,7 @@ warning[BSK-0001]: Missing parameter type annotation for `data` | ^^^^ | = help: Add a type annotation: `data: ` - = note: In Basilisk, all function parameters require explicit types + = note: Basilisk requires an explicit parameter type wherever it cannot be inferred; a literal default (e.g. `timeout=30`) infers the type and needs no annotation = see: https://www.basilisk-python.dev/errors/BSK-0001 warning[BSK-0002]: Missing return type annotation for function `process` @@ -144,7 +144,7 @@ warning[BSK-0002]: Missing return type annotation for function `process` | ^^^^^^^^^^^^^ | = help: Add a return type: `def process(...) -> :` - = note: In Basilisk, all functions require an explicit return type + = note: Basilisk requires an explicit return type wherever it cannot be inferred; literal-only returns (e.g. `return 42`) infer the type and need no annotation = see: https://www.basilisk-python.dev/errors/BSK-0002 Found 2 diagnostics (0 errors).