mt-lang 0.4.24 → 0.4.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/Gemfile +1 -1
  4. data/Gemfile.lock +8 -8
  5. data/README.md +39 -7
  6. data/docs/build-guide.md +5 -0
  7. data/docs/index.html +61 -29
  8. data/docs/language-design.md +2 -2
  9. data/docs/language-manual.md +15 -3
  10. data/lib/milk_tea/base.rb +1 -1
  11. data/lib/milk_tea/bindings/raw_bindings/defaults.rb +22 -0
  12. data/lib/milk_tea/core/c_backend/statements.rb +118 -1
  13. data/lib/milk_tea/core/control_flow/builder.rb +12 -0
  14. data/lib/milk_tea/core/lowering/utils.rb +8 -1
  15. data/lib/milk_tea/core/semantic_analyzer/expressions.rb +43 -2
  16. data/lib/milk_tea/core/semantic_analyzer/name_resolution.rb +7 -1
  17. data/lib/milk_tea/tooling/bindgen_cli.rb +56 -0
  18. data/lib/milk_tea/tooling/cli/commands/std.rb +84 -0
  19. data/lib/milk_tea/tooling/cli.rb +27 -1
  20. data/lib/milk_tea/tooling/linter/fix_engine.rb +100 -0
  21. data/lib/milk_tea/tooling/linter/release_rules.rb +69 -11
  22. data/lib/milk_tea/tooling/linter/visitors.rb +58 -31
  23. data/lib/milk_tea/tooling/linter.rb +8 -1
  24. data/lib/milk_tea/tooling/std_catalog.rb +277 -0
  25. data/lib/milk_tea/tooling.rb +1 -0
  26. data/std/async/libuv_runtime.mt +20 -23
  27. data/std/async/mailbox.mt +2 -2
  28. data/std/base64.mt +3 -6
  29. data/std/behavior_tree.mt +1 -6
  30. data/std/binary.mt +2 -5
  31. data/std/c/stb_image.mt +15 -15
  32. data/std/color.mt +20 -20
  33. data/std/cookie.mt +1 -4
  34. data/std/deque.mt +2 -8
  35. data/std/encoding.mt +1 -4
  36. data/std/fmt.mt +1 -4
  37. data/std/graph.mt +5 -1
  38. data/std/htn.mt +9 -1
  39. data/std/http/server.mt +1 -4
  40. data/std/http.mt +14 -17
  41. data/std/jobs.mt +9 -10
  42. data/std/json.mt +17 -28
  43. data/std/linked_map.mt +3 -9
  44. data/std/map.mt +1 -4
  45. data/std/net/channel.mt +6 -6
  46. data/std/net/lobby.mt +3 -3
  47. data/std/net/manager.mt +0 -1
  48. data/std/net/mux.mt +1 -1
  49. data/std/net/packet.mt +4 -1
  50. data/std/net/punch.mt +3 -2
  51. data/std/net/rpc.mt +1 -3
  52. data/std/net/session.mt +12 -25
  53. data/std/net/sync.mt +3 -3
  54. data/std/net/turn.mt +7 -3
  55. data/std/net.mt +145 -159
  56. data/std/noise.mt +11 -7
  57. data/std/ordered_map.mt +2 -8
  58. data/std/ordered_set.mt +3 -5
  59. data/std/path.mt +1 -4
  60. data/std/process.mt +7 -18
  61. data/std/random.mt +2 -2
  62. data/std/sdl3/runtime.mt +1 -1
  63. data/std/steering.mt +27 -4
  64. data/std/string.mt +1 -4
  65. data/std/tar.mt +8 -10
  66. data/std/terminal.mt +21 -40
  67. data/std/thread.mt +3 -4
  68. data/std/tls.mt +22 -29
  69. data/std/toml.mt +23 -31
  70. data/std/uri.mt +2 -1
  71. data/std/utility.mt +2 -2
  72. data/std/vec.mt +7 -17
  73. metadata +5 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 95423784419c8060f10277cb7abbb1fe1cd955fe648008781d1184976968e2d8
4
- data.tar.gz: 6e1b33f4c2a17226dc34ba00618b89be6f329841c5badc7a53ccd022daa3c54d
3
+ metadata.gz: 8f75e56a0679c161c076efb3dd694afe256154c512e7ffae171e13c2d98d181d
4
+ data.tar.gz: ff00b0459c88d4fd31d12b0fc25e953d339fe7d8f7ed10964b5d8e2b70065a6f
5
5
  SHA512:
6
- metadata.gz: aec1e36ac602ad755b89252058932df48abd25c0e645e212fa404afd25b23679e86d6cf45a970a36d566b08c9be774231496cf59766f64efa366da382e559963
7
- data.tar.gz: 85af5d83647fd5cb635bec77a3ae8846be1b6f8f62638b5c82a2f99b9f76f2de6fb08455b6660e0cace75669e9281456a3d7336043ef3009988e50fc3efb4c0c
6
+ metadata.gz: 72e4cfb94f691ace787f420cbd2b6481c0b189282d21dbe42b47ebd795844f5e42a59a95fc64f1e75f56c6ec7119723231fef4601c8d856f6d7ada5ed107d26f
7
+ data.tar.gz: 01ba76cc9a0aeb3ab65fbd5dea21a9995ace49a3328003352fa13b0c54c9cc42e8b3e1bb6423d703913439ca4de91b4f70348d128a6c813a835c28e9345ff88b
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 4.0.3
1
+ 4.0.6
data/Gemfile CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- ruby "4.0.3"
5
+ ruby "4.0.6"
6
6
 
7
7
  gem "rake", "~> 13.4"
8
8
  gem "rexml", "~> 3.4"
data/Gemfile.lock CHANGED
@@ -5,7 +5,7 @@ GEM
5
5
  drb (2.2.3)
6
6
  fiddle (1.1.8)
7
7
  logger (1.7.0)
8
- minitest (6.0.4)
8
+ minitest (6.0.6)
9
9
  drb (~> 2.0)
10
10
  prism (~> 1.5)
11
11
  minitest-coverage (1.0.0.b3)
@@ -15,7 +15,7 @@ GEM
15
15
  puma (6.6.1)
16
16
  nio4r (~> 2.0)
17
17
  racc (1.8.1)
18
- rack (3.2.6)
18
+ rack (3.2.7)
19
19
  rack-protection (4.2.1)
20
20
  base64 (>= 0.1.0)
21
21
  logger (>= 1.6.0)
@@ -34,7 +34,7 @@ GEM
34
34
  rack-protection (= 4.2.1)
35
35
  rack-session (>= 2.0.0, < 3)
36
36
  tilt (~> 2.0)
37
- tilt (2.7.0)
37
+ tilt (2.9.0)
38
38
  tomlrb (2.0.4)
39
39
 
40
40
  PLATFORMS
@@ -58,25 +58,25 @@ CHECKSUMS
58
58
  drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
59
59
  fiddle (1.1.8) sha256=7fa8ee3627271497f3add5503acdbc3f40b32f610fc1cf49634f083ef3f32eee
60
60
  logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
61
- minitest (6.0.4) sha256=df1304664589d40f46089247fdc451f866b0ce0d7cae1457a15fc1eb7d48dca1
61
+ minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
62
62
  minitest-coverage (1.0.0.b3) sha256=27c8b6b8df1259325dd206baa2b143c594f8a88b7dfd97fd8d3389a0e8a6ad26
63
63
  mustermann (3.1.1) sha256=4c6170c7234d5499c345562ba7c7dfe73e1754286dcc1abb053064d66a127198
64
64
  nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1
65
65
  prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
66
66
  puma (6.6.1) sha256=b9b56e4a4ea75d1bfa6d9e1972ee2c9f43d0883f011826d914e8e37b3694ea1e
67
67
  racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
68
- rack (3.2.6) sha256=5ed78e1f73b2e25679bec7d45ee2d4483cc4146eb1be0264fc4d94cb5ef212c2
68
+ rack (3.2.7) sha256=93e13e1c24f93556671d85d2d79fa228c3485815c50d7e2f265b5330c6528fb7
69
69
  rack-protection (4.2.1) sha256=cf6e2842df8c55f5e4d1a4be015e603e19e9bc3a7178bae58949ccbb58558bac
70
70
  rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8
71
71
  rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
72
72
  rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
73
73
  rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
74
74
  sinatra (4.2.1) sha256=b7aeb9b11d046b552972ade834f1f9be98b185fa8444480688e3627625377080
75
- tilt (2.7.0) sha256=0d5b9ba69f6a36490c64b0eee9f6e9aad517e20dcc848800a06eb116f08c6ab3
75
+ tilt (2.9.0) sha256=da5735d0280bba96e9a91041bb14aee435ccad5c17b0fa519249ae543d9aa3a5
76
76
  tomlrb (2.0.4) sha256=262f77947ac3ac9b3366a0a5940ecd238300c553e2e14f22009e2afcd2181b99
77
77
 
78
78
  RUBY VERSION
79
- ruby 4.0.3
79
+ ruby 4.0.6
80
80
 
81
81
  BUNDLED WITH
82
- 4.0.3
82
+ 4.0.16
data/README.md CHANGED
@@ -840,6 +840,8 @@ Nullability:
840
840
  - For non-pointer value bases (`int`, `bool`, `float`, structs, ...), `T?` is stored inline by value as a tagged optional (a presence flag plus the value). It copies by value with no hidden heap allocation or pointer aliasing.
841
841
  - Use `null` for absence in any nullable context.
842
842
  - In nullable pointer-like contexts, prefer `null` over `zero[ptr[T]]`.
843
+ - `null` is only a value of nullable types. Comparing a non-nullable pointer against `null` is a type error: either declare the value `T?` when null is possible, or compare against `zero[ptr[T]]` (also `zero[cstr]`, `zero[own[T]]`) when testing the raw C zero pointer at the raw ABI level. Nullable types must use `null`, not `zero[...]`.
844
+ - Null checks narrow flow: inside the `!= null` branch (and after an `== null` branch exits), the binding is treated as non-null. A further null check on a narrowed binding is still legal; the linter reports it as `redundant-null-check`.
843
845
  - `ref[T]` is non-null and cannot be nullable.
844
846
  - At an FFI boundary (`external` / `foreign function` parameters and returns), only pointer-like `T?` is allowed. A non-pointer value nullable such as `int?` is rejected — use `ptr[T]?` or pass an explicit struct.
845
847
 
@@ -911,6 +913,8 @@ A `field_handle`'s `.type` is usable directly **in type position** within a comp
911
913
 
912
914
  ### Standard library
913
915
 
916
+ The authoritative module catalog is `mtc std list [--json]`; it discovers every hand-written module shipped in `std/` with its category, description, and path. Generated binding modules (`std/c/*` and imported-binding wrappers such as `raylib` or `zstd`) are excluded from `list` but stay viewable with `mtc std show`.
917
+
914
918
  Core modules in `std/`:
915
919
 
916
920
  - `std.linear_algebra` — extends native vector/matrix/quaternion types with `dot`, `cross`, `length`, `normalized`, `lerp`, `identity`, `transpose`, `conjugate` (pure Mt, no C dependency beyond `std.math` for `sqrt`)
@@ -922,7 +926,7 @@ Core modules in `std/`:
922
926
  - `std.encoding` — UTF-8 validation (`is_valid_utf8`, `utf8_codepoint_count`, `decode_utf8_codepoint`, `utf8_overlong_check`)
923
927
  - `std.string.String` — growable owned UTF-8 text
924
928
  - `std.mem.heap`, `std.mem.arena`, `std.mem.pool`, `std.mem.stack`, `std.mem.tracking`, `std.mem.endian` — allocators and memory utilities
925
- - `std.async` — task runtime (`sleep`, `work`, `completed`, `result`, `wait`, `run`)
929
+ - `std.async` — task runtime (`sleep`, `work`, `completed`, `result`, `wait`, `run`, plus explicit-runtime variants `wait_on`, `work_on`, `sleep_on`, `run_on`, `pump`, `with_runtime`)
926
930
  - `std.option.Option[T]` — optional value with `is_some`, `is_none`, `unwrap`, `expect`, `unwrap_or`, `unwrap_or_else` (auto-imported via prelude)
927
931
  - `std.result.Result[T, E]` — fallible computation with `is_success`, `is_failure`, `unwrap`, `unwrap_error`, `unwrap_or`, `unwrap_or_else`, `ok`, `error`, `map_error` (auto-imported via prelude)
928
932
 
@@ -1241,6 +1245,13 @@ Resolve a module by name, build it, and run it:
1241
1245
  mtc run-module <module> # Resolve, build, and run a module by name (e.g. std.http.server)
1242
1246
  ```
1243
1247
 
1248
+ Standard library inspection:
1249
+
1250
+ ```
1251
+ mtc std list [--json] # List hand-written std modules grouped by category
1252
+ mtc std show MODULE # Print a module's source (dotted or slashed names)
1253
+ ```
1254
+
1244
1255
  Toolchain maintenance:
1245
1256
 
1246
1257
  ```
@@ -1259,6 +1270,28 @@ mtc dap # Start the Debug Adapter Protocol server
1259
1270
  mtc docs [--open] [--port P] # Serve the local documentation site
1260
1271
  ```
1261
1272
 
1273
+ `mtc bindgen` nullable policy:
1274
+
1275
+ C headers usually do not state which functions can return NULL. A nullable policy file lists those symbols so generated raw bindings expose them as `T?` and null checks work downstream. The policy feeds the same override machinery as the checked-in `std.c.*` registries and shows up in `--nullable-report`.
1276
+
1277
+ ```sh
1278
+ mtc bindgen mylib.mt mylib.h --nullable-policy nullable_policy.json --nullable-report report.json
1279
+ ```
1280
+
1281
+ ```json
1282
+ {
1283
+ "return_types": {
1284
+ "mylib_load": "ptr[ubyte]?",
1285
+ "mylib_get_error": "cstr?"
1286
+ },
1287
+ "parameters": {
1288
+ "mylib_read": { "out_size": "ptr[uint]?" }
1289
+ }
1290
+ }
1291
+ ```
1292
+
1293
+ Types must match what bindgen would otherwise emit for the symbol (minus the `?`). Bindgen rejects policy entries for unknown symbols, so a stale policy fails loudly instead of silently drifting.
1294
+
1262
1295
  Build and run commands support `--profile`, `--platform`, `--cc`, `--keep-c`, `--locked`, `--frozen`, and `-I` include paths. Dependency-locked flows support `--locked` (use package.lock) and `--frozen` (require current package.lock).
1263
1296
 
1264
1297
  Global options work with any command (before or after the subcommand, up to a `--` separator): `-h`/`--help` (also `mtc help <command>` for command-specific help), `-V`/`--version`, `-q`/`--quiet` (suppress informational output), `-v`/`--verbose` (per-file progress), and `--color auto|always|never`. Generate shell completions with `mtc completions bash|zsh|fish`. Timing breakdowns use `--timings` (on `format` and `lint`); `lint` reports warnings with per-file progress.
@@ -1266,14 +1299,13 @@ Global options work with any command (before or after the subcommand, up to a `-
1266
1299
  Diagnostic output uses standard compiler format (file:line:column with source context, error codes, and caret highlighting):
1267
1300
 
1268
1301
  ```
1269
- [E0001] error: unknown type floa
1270
- --> file.mt:1:16
1302
+ error[sema/error]: unknown type floa -- /tmp/diag_probe.mt:2:12
1271
1303
  |
1272
- 1 | type Seconds = floa
1273
- | ^~~~
1274
- note: did you mean 'float'?
1304
+ 2 | let x: floa = 1
1305
+ | ^~~~
1306
+ = help: did you mean 'float'?
1275
1307
 
1276
- error: could not check due to 1 previous error
1308
+ 1 error found
1277
1309
  ```
1278
1310
 
1279
1311
  `mtc check` surfaces both errors and linter warnings:
data/docs/build-guide.md CHANGED
@@ -351,6 +351,11 @@ Common options:
351
351
  - `--bundle` for native package builds when you want a distributable app directory instead of a bare executable
352
352
  - `--archive` for native package builds when you also want a `.tar.gz` archive of the bundle; this implies `--bundle`
353
353
 
354
+ Debug profile builds enable runtime debug guards:
355
+
356
+ - Loops get an iteration-limit guard that calls `mt_fatal` when a synchronous loop exceeds 50,000,000 iterations (a hang detector).
357
+ - Every unconditionally-evaluated raw pointer dereference — pointer field access, pointer indexing, and `read(ptr)` in `unsafe` — is preceded by a null trap that calls `mt_fatal("null pointer dereferenced in <function>")` instead of segfaulting. This catches a NULL that crossed a non-nullable FFI declaration at the source line. Derefs guarded only by flow (inside `and`/`or` right sides or `?:` branches) and `ref[T]` receivers are not trapped: conditional derefs may never execute, and refs are non-null by the type system.
358
+
354
359
  The wasm platform also accepts the aliases `web`, `html5`, and `browser`.
355
360
 
356
361
  For editor tooling, the Milk Tea VS Code extension also accepts `milkTea.lsp.platform = auto|linux|windows|wasm`.
data/docs/index.html CHANGED
@@ -254,7 +254,7 @@ p + p{margin-top:-.25rem}
254
254
  </button>
255
255
  <span style="font-size:.75rem">
256
256
  <a href="https://github.com/teefan/mt-lang" style="color:inherit;text-decoration:none" title="Source code on GitHub">&#9757; GitHub</a>
257
- &nbsp;&middot;&nbsp; v1
257
+ &nbsp;&middot;&nbsp; v0.4
258
258
  </span>
259
259
  </div>
260
260
  </aside>
@@ -2101,7 +2101,7 @@ async function main() -> int:
2101
2101
  </div>
2102
2102
 
2103
2103
  <h3>Task Helpers</h3>
2104
- <p>Import <code>std.async</code> for runtime control:</p>
2104
+ <p>Import <code>std.async</code> for runtime control (<code>sleep</code>, <code>work</code>, <code>completed</code>, <code>result</code>, <code>wait</code>, <code>run</code>, plus explicit-runtime variants such as <code>wait_on</code>, <code>work_on</code>, and <code>with_runtime</code>):</p>
2105
2105
  <div class="code-wrap">
2106
2106
  <button class="copy-btn" onclick="copyCode(this)">Copy</button>
2107
2107
  <pre><code>import std.async as aio
@@ -2306,7 +2306,7 @@ function attach(window: ref[Window]) -> Result[void, EventError]:
2306
2306
  <tr><td><code>std.mem.arena</code></td><td>Frame/level/scratch arena allocator</td></tr>
2307
2307
  <tr><td><code>std.mem.pool</code></td><td>Fixed-size object pool</td></tr>
2308
2308
  <tr><td><code>std.mem.stack</code></td><td>Explicit temporary allocator</td></tr>
2309
- <tr><td><code>std.async</code></td><td>Async runtime: <code>sleep</code>, <code>work</code>, <code>completed</code>, <code>result</code>, <code>wait</code>, <code>run</code></td></tr>
2309
+ <tr><td><code>std.async</code></td><td>Async runtime: <code>sleep</code>, <code>work</code>, <code>completed</code>, <code>result</code>, <code>wait</code>, <code>run</code>, plus explicit-runtime variants (<code>wait_on</code>, <code>work_on</code>, <code>with_runtime</code>)</td></tr>
2310
2310
  <tr><td><code>std.option</code></td><td><code>Option[T]</code> — optional value (prelude type; methods always available without import). Methods: <code>is_some</code>, <code>is_none</code>, <code>unwrap</code>, <code>expect</code>, <code>unwrap_or</code>, <code>unwrap_or_else</code></td></tr>
2311
2311
  <tr><td><code>std.result</code></td><td><code>Result[T, E]</code> — fallible computation (prelude type; methods always available without import). Methods: <code>is_success</code>, <code>is_failure</code>, <code>unwrap</code>, <code>unwrap_error</code>, <code>unwrap_or</code>, <code>unwrap_or_else</code>, <code>ok</code>, <code>error</code>, <code>map_error</code></td></tr>
2312
2312
  </table>
@@ -2356,6 +2356,8 @@ function attach(window: ref[Window]) -> Result[void, EventError]:
2356
2356
  <tr><td><code>mtc test &lt;path&gt;</code></td><td>Discover and run <code>@[test]</code> functions</td></tr>
2357
2357
  <tr><td><code>mtc new &lt;name&gt;</code></td><td>Scaffold a new package (<code>package.toml</code> + <code>src/main.mt</code>)</td></tr>
2358
2358
  <tr><td><code>mtc run-module &lt;module&gt;</code></td><td>Resolve a module by name, build it, and run it</td></tr>
2359
+ <tr><td><code>mtc std list [--json]</code></td><td>List hand-written standard library modules grouped by category (<code>--json</code> for machine-readable output)</td></tr>
2360
+ <tr><td><code>mtc std show &lt;module&gt;</code></td><td>Print a standard library module's source (dotted or slashed names; platform variants resolve to the active platform)</td></tr>
2359
2361
  <tr><td><code>mtc completions &lt;shell&gt;</code></td><td>Print a bash/zsh/fish completion script</td></tr>
2360
2362
  </table>
2361
2363
  </div>
@@ -2430,46 +2432,76 @@ mtc lint path/to/file.mt --ignore line-too-long # skip selected rules</code></p
2430
2432
  </div>
2431
2433
 
2432
2434
  <h3>Inline Suppression</h3>
2433
- <p>Suppress a rule for the next line with <code># lint: ignore &lt;rule-code&gt;</code>:</p>
2435
+ <p>Suppress all rules on a line with <code># lint: ignore</code>, or specific rules with <code># lint: ignore(rule1, rule2)</code>:</p>
2434
2436
  <div class="code-wrap">
2435
2437
  <button class="copy-btn" onclick="copyCode(this)">Copy</button>
2436
- <pre><code># lint: ignore unused-import
2437
- import std.math # intentionally available for downstream</code></pre>
2438
+ <pre><code>var count = 0 # lint: ignore
2439
+ var total = 0 # lint: ignore(prefer-let, dead-assignment)</code></pre>
2438
2440
  </div>
2439
2441
 
2440
2442
  <h3>Configuration</h3>
2441
- <p>Place a <code>.mt-lint.yml</code> file in your package root to customize rule severity:</p>
2443
+ <p>Create a default config with <code>mtc lint --init</code>, or place a <code>.mt-lint.yml</code> file in the project root (or any ancestor directory):</p>
2442
2444
  <div class="code-wrap">
2443
2445
  <button class="copy-btn" onclick="copyCode(this)">Copy</button>
2444
- <pre><code>rules:
2445
- prefer-let-else: warn
2446
- unused-import: error
2447
- line-too-long: off</code></pre>
2446
+ <pre><code>max_line_length: 120
2447
+ select:
2448
+ - line-too-long
2449
+ - prefer-let
2450
+ - missing-return
2451
+ ignore:
2452
+ - shadow
2453
+ - useless-expression</code></pre>
2448
2454
  </div>
2455
+ <p><code>max_line_length</code> defaults to <code>120</code> when omitted. When both <code>select</code> and <code>ignore</code> are present, <code>select</code> takes precedence.</p>
2449
2456
 
2450
2457
  <h3>Rule Reference</h3>
2451
2458
  <div class="table-wrap">
2452
2459
  <table class="attr-table">
2453
- <tr><th>Category</th><th>Code</th><th>Description</th></tr>
2454
- <tr><td>Correctness</td><td><code>constant-condition</code></td><td>Always-true or always-false conditions</td></tr>
2455
- <tr><td>Correctness</td><td><code>dead-assignment</code></td><td>Variable assigned but never read</td></tr>
2456
- <tr><td>Correctness</td><td><code>unreachable-code</code></td><td>Code after a terminating statement</td></tr>
2457
- <tr><td>Correctness</td><td><code>redundant-null-check</code></td><td>Null check on already-proven non-null</td></tr>
2458
- <tr><td>Correctness</td><td><code>loop-single-iteration</code></td><td>Loop with known single iteration</td></tr>
2459
- <tr><td>Style</td><td><code>prefer-let-else</code></td><td>Var followed by null check; suggest let...else</td></tr>
2460
- <tr><td>Style</td><td><code>prefer-var-else</code></td><td>Same as above for mutable bindings</td></tr>
2461
- <tr><td>Style</td><td><code>redundant-cast</code></td><td>Explicit cast where implicit widening already works</td></tr>
2462
- <tr><td>Style</td><td><code>redundant-return</code></td><td>Unnecessary return at end of void function</td></tr>
2463
- <tr><td>Style</td><td><code>trailing-list-comma</code></td><td>Multiline lists should end with trailing comma</td></tr>
2464
- <tr><td>Style</td><td><code>line-too-long</code></td><td>Line exceeds recommended width</td></tr>
2465
- <tr><td>Style</td><td><code>doc-tag</code></td><td>Documentation comment formatting</td></tr>
2466
- <tr><td>Convention</td><td><code>unused-import</code></td><td>Imported module never referenced</td></tr>
2467
- <tr><td>Convention</td><td><code>unused-var</code></td><td>Variable declared but never read</td></tr>
2468
- <tr><td>Convention</td><td><code>reserved-names</code></td><td>Binding shadows a primitive type or built-in name</td></tr>
2469
- <tr><td>Convention</td><td><code>event-capacity</code></td><td>Event capacity too large or unbounded</td></tr>
2460
+ <tr><th>Code</th><th>Severity</th><th>Auto-fix</th><th>Description</th></tr>
2461
+ <tr><td><code>borrow-and-mutate</code></td><td>warning</td><td>&mdash;</td><td>Local is borrowed with <code>ref_of</code>/<code>ptr_of</code> and also mutated in the same scope</td></tr>
2462
+ <tr><td><code>constant-condition</code></td><td>warning</td><td>&mdash;</td><td>Branch or loop condition is provably always true or false</td></tr>
2463
+ <tr><td><code>dead-assignment</code></td><td>warning</td><td>&mdash;</td><td>Assigned value is overwritten before any read</td></tr>
2464
+ <tr><td><code>duplicate-if-condition</code></td><td>warning</td><td>&mdash;</td><td><code>if</code>/<code>else if</code> branch repeats a previous condition and is unreachable</td></tr>
2465
+ <tr><td><code>directional-ffi-arg</code></td><td>hint</td><td>&mdash;</td><td>Legacy <code>ptr_of</code>/<code>ref_of</code>/<code>out</code> call-site wrapper is redundant for directional FFI parameters</td></tr>
2466
+ <tr><td><code>doc-tag</code></td><td>hint</td><td>&mdash;</td><td><code>##</code> doc comment tag (<code>@param</code>, <code>@return</code>, <code>@throws</code>, <code>@see</code>) is invalid or inconsistent</td></tr>
2467
+ <tr><td><code>event-capacity</code></td><td>warning</td><td>&mdash;</td><td>Event capacity may copy too many listeners to stack on emit</td></tr>
2468
+ <tr><td><code>line-too-long</code></td><td>warning</td><td>&mdash;</td><td>Source line exceeds configured maximum length</td></tr>
2469
+ <tr><td><code>loop-single-iteration</code></td><td>warning</td><td>&mdash;</td><td>Loop body always exits on the first iteration</td></tr>
2470
+ <tr><td><code>missing-return</code></td><td>error</td><td>&mdash;</td><td>Non-void function lacks a guaranteed return on all paths</td></tr>
2471
+ <tr><td><code>noop-compound-assignment</code></td><td>hint</td><td>&mdash;</td><td>Compound assignment uses an identity value and has no effect</td></tr>
2472
+ <tr><td><code>owning-release-double</code></td><td>warning</td><td>&mdash;</td><td>Owning binding may be released more than once</td></tr>
2473
+ <tr><td><code>owning-release-leak</code></td><td>warning</td><td>&mdash;</td><td>Owning binding is never released</td></tr>
2474
+ <tr><td><code>platform-api-drift</code></td><td>warning</td><td>&mdash;</td><td>Public API differs across platform-specific variants of the same module</td></tr>
2475
+ <tr><td><code>prefer-let</code></td><td>hint</td><td>yes</td><td><code>var</code> binding is never mutated; use <code>let</code></td></tr>
2476
+ <tr><td><code>prefer-let-else</code></td><td>hint</td><td>yes</td><td>Nullable guard can be rewritten as <code>let ... else:</code></td></tr>
2477
+ <tr><td><code>prefer-inline-methods</code></td><td>hint</td><td>yes</td><td>Methods on a struct can be written inline inside the struct declaration</td></tr>
2478
+ <tr><td><code>prefer-is-variant</code></td><td>hint</td><td>&mdash;</td><td>A match that maps one variant arm to a boolean can be <code>expr is Arm</code></td></tr>
2479
+ <tr><td><code>prefer-own-ptr</code></td><td>hint</td><td>&mdash;</td><td>Pointer binding used only inside <code>unsafe</code> could be <code>own[T]</code> for auto-deref</td></tr>
2480
+ <tr><td><code>prefer-or-pattern</code></td><td>hint</td><td>&mdash;</td><td>Adjacent match arms with identical bodies can merge with <code>|</code></td></tr>
2481
+ <tr><td><code>prefer-struct-with</code></td><td>hint</td><td>&mdash;</td><td>Field copies from another value can use <code>.with(...)</code></td></tr>
2482
+ <tr><td><code>prefer-try</code></td><td>hint</td><td>&mdash;</td><td>A match that only propagates the failure branch can use <code>expr?</code></td></tr>
2483
+ <tr><td><code>prefer-var-else</code></td><td>hint</td><td>yes</td><td>Nullable guard can be rewritten as <code>var ... else:</code></td></tr>
2484
+ <tr><td><code>redundant-bool-compare</code></td><td>hint</td><td>yes</td><td>Comparing a boolean expression to <code>true</code>/<code>false</code> is redundant</td></tr>
2485
+ <tr><td><code>redundant-cast</code></td><td>hint</td><td>yes</td><td>Explicit cast where implicit widening already works</td></tr>
2486
+ <tr><td><code>redundant-else</code></td><td>warning</td><td>yes</td><td><code>else</code> block is unnecessary because all prior branches return</td></tr>
2487
+ <tr><td><code>redundant-ignored-match-binding</code></td><td>hint</td><td>yes</td><td>Ignored <code>as _</code> match binding is redundant</td></tr>
2488
+ <tr><td><code>redundant-null-check</code></td><td>hint</td><td>&mdash;</td><td>Null check on a value already known non-null by flow analysis</td></tr>
2489
+ <tr><td><code>redundant-return</code></td><td>hint</td><td>yes</td><td>Final bare <code>return</code> in a void function is unnecessary</td></tr>
2490
+ <tr><td><code>redundant-type-annotation</code></td><td>hint</td><td>yes</td><td>Type annotation is redundant; the type is inferred from the initializer</td></tr>
2491
+ <tr><td><code>redundant-unsafe</code></td><td>hint</td><td>yes</td><td><code>unsafe</code> block contains no unsafe operations and can be removed</td></tr>
2492
+ <tr><td><code>reserved-primitive-name</code></td><td>warning</td><td>yes</td><td>Binding uses a reserved built-in type name in its active namespace</td></tr>
2493
+ <tr><td><code>self-assignment</code></td><td>warning</td><td>&mdash;</td><td>Variable is assigned to itself</td></tr>
2494
+ <tr><td><code>self-comparison</code></td><td>warning</td><td>&mdash;</td><td>Value is compared to itself, making the condition constant</td></tr>
2495
+ <tr><td><code>shadow</code></td><td>warning</td><td>&mdash;</td><td>Local binding shadows an outer binding with the same name</td></tr>
2496
+ <tr><td><code>trailing-list-comma</code></td><td>hint</td><td>yes</td><td>Trailing comma in call argument list is redundant</td></tr>
2497
+ <tr><td><code>unreachable-code</code></td><td>warning</td><td>&mdash;</td><td>Code after a guaranteed terminator cannot execute</td></tr>
2498
+ <tr><td><code>unused-import</code></td><td>warning</td><td>&mdash;</td><td>Import alias is never referenced (intentionally not auto-fixable: removals can drop extension methods or canonical hooks)</td></tr>
2499
+ <tr><td><code>unused-local</code></td><td>warning</td><td>&mdash;</td><td>Local binding is never referenced</td></tr>
2500
+ <tr><td><code>unused-param</code></td><td>warning</td><td>&mdash;</td><td>Parameter is never referenced</td></tr>
2501
+ <tr><td><code>useless-expression</code></td><td>warning</td><td>&mdash;</td><td>Expression statement has no side effects and its result is unused</td></tr>
2470
2502
  </table>
2471
2503
  </div>
2472
- <p>Severity levels: <code>error</code> (fail build), <code>warn</code> (report only), <code>off</code> (disabled). Rules default to <code>warn</code> unless noted in the manual.</p>
2504
+ <p>Severity levels: <code>error</code> (fail build), <code>warning</code>, and <code>hint</code>.</p>
2473
2505
  </section>
2474
2506
 
2475
2507
  <!-- ────────────────────────────────────────────────────────────────────────── -->
@@ -473,12 +473,12 @@ Current implemented shape:
473
473
 
474
474
  - `async function` lifts its return type to `Task[T]`
475
475
  - `await` is only valid inside async functions
476
- - async entrypoint bootstrapping is compiler-owned, but async helpers stay explicit library surface; import `std.async as aio` when user code needs `sleep`, `work`, `completed`, `result`, `wait`, `run`, or runtime control
476
+ - async entrypoint bootstrapping is compiler-owned, but async helpers stay explicit library surface; import `std.async as aio` when user code needs `sleep`, `work`, `completed`, `result`, `wait`, `run`, `wait_on`, `with_runtime`, or runtime control
477
477
  - `aio.wait(...)` and `aio.run(...)` accept direct task expressions as well as zero-arg task roots; the compiler rewrites the direct-task form into the deferred root shape automatically
478
478
  - async bodies support ordinary local declarations, including `let ... else:`, assignments, returns, `if`, `while`, single-form and parallel `for`, `match`, `defer`, `unsafe`, and deferred cleanup bodies that `await`
479
479
  - await placement is handled by the normalization pass which hoists nested awaits into `let` bindings, so `await` is supported in all expression contexts (call arguments, binary operations, if-expr, match-expr, member access, index access, format strings)
480
480
 
481
- The default async model is a language-integrated entry boundary. `std.async` remains the explicit high-level helper surface for operations such as `sleep`, `work`, `completed`, `result`, `wait`, `wait_on`, and `with_runtime`, while the normal runtime model stays the single integrated libuv-backed runtime.
481
+ The default async model is a language-integrated entry boundary. `std.async` remains the explicit high-level helper surface for operations such as `sleep`, `work`, `completed`, `result`, `wait`, `run`, `wait_on`, and `with_runtime`, while the normal runtime model stays the single integrated libuv-backed runtime.
482
482
 
483
483
  #### Concurrency: `parallel for` and `parallel:` blocks
484
484
 
@@ -1172,7 +1172,9 @@ let q = quat(x = 0.0, y = 0.0, z = 0.0, w = 1.0)
1172
1172
  - for pointer-like bases (`ptr[T]`, `const_ptr[T]`, `own[T]`, `cstr`, `fn(...)`, `proc(...)`, opaque), `T?` is a nullable pointer with `null` as the absent value
1173
1173
  - for non-pointer value bases (`int`, `bool`, `float`, structs, ...), `T?` is stored inline by value as a tagged optional (a presence flag plus the value); it copies by value with no hidden heap allocation or pointer aliasing
1174
1174
  - `null` expresses absence in any nullable context; the explicit typed `null[...]` form's target must be pointer-like
1175
+ - `null` is only a value of nullable types. Comparing a non-nullable pointer, `cstr`, or opaque handle against `null` is a type error: declare the value `T?` when null is possible, or compare against `zero[ptr[T]]` (also `zero[cstr]`, `zero[own[T]]`) to test the raw C zero pointer at the raw ABI level. Nullable types must use `null`, not `zero[...]`
1175
1176
  - in nullable pointer-like contexts, `zero[ptr[T]]` is rejected; use `null` instead
1177
+ - null checks narrow flow: inside the `!= null` branch (and after an `== null` branch exits), the binding is treated as non-null. A further null check on a narrowed binding is still legal; the linter reports it as `redundant-null-check`
1176
1178
  - at an FFI boundary (`external` / `foreign function` parameters and returns), only pointer-like `T?` is allowed; a non-pointer value nullable such as `int?` is rejected — use `ptr[T]?` or pass an explicit struct
1177
1179
 
1178
1180
  ### 6.4 Generics
@@ -1523,7 +1525,7 @@ mtc lint --ignore shadow file.mt
1523
1525
 
1524
1526
  ### 12.2 Rules
1525
1527
 
1526
- The auto-fix column corresponds to `mtc lint --fix`.
1528
+ The auto-fix column corresponds to `mtc lint --fix`. `unused-import` is intentionally not auto-fixable: removing an import has non-local effects (extension methods and canonical hooks) the per-file linter cannot see.
1527
1529
 
1528
1530
  | Code | Severity | Auto-fix | Description |
1529
1531
  |---|---|---|---|
@@ -1538,23 +1540,33 @@ The auto-fix column corresponds to `mtc lint --fix`.
1538
1540
  | `loop-single-iteration` | warning | — | Loop body always exits on the first iteration |
1539
1541
  | `missing-return` | error | — | Function with a non-void return type lacks a guaranteed return on all paths |
1540
1542
  | `noop-compound-assignment` | hint | — | Compound assignment uses an identity value and has no effect |
1543
+ | `owning-release-double` | warning | — | Owning binding may be released more than once |
1544
+ | `owning-release-leak` | warning | — | Owning binding is never released |
1541
1545
  | `platform-api-drift` | warning | — | Public API differs across sibling platform-specific variants of the same module |
1542
1546
  | `prefer-let` | hint | yes | `var` binding is never mutated; use `let` instead |
1543
1547
  | `prefer-let-else` | hint | yes | Nullable guard can be rewritten as `let ... else:` |
1548
+ | `prefer-inline-methods` | hint | yes | Methods on a struct can be written inline inside the struct declaration |
1549
+ | `prefer-is-variant` | hint | — | A match that maps one variant arm to a boolean can be `expr is Arm` |
1550
+ | `prefer-own-ptr` | hint | — | Pointer binding used only inside `unsafe` could be `own[T]` for auto-deref |
1551
+ | `prefer-or-pattern` | hint | — | Adjacent match arms with identical bodies can merge with `\|` |
1552
+ | `prefer-struct-with` | hint | — | Field copies from another value can use `.with(...)` |
1553
+ | `prefer-try` | hint | — | A match that only propagates the failure branch can use `expr?` |
1544
1554
  | `prefer-var-else` | hint | yes | Nullable guard can be rewritten as `var ... else:` |
1545
1555
  | `redundant-bool-compare` | hint | yes | Comparing a boolean expression to `true`/`false` is redundant |
1556
+ | `redundant-cast` | hint | yes | Explicit cast where implicit widening already works |
1546
1557
  | `redundant-else` | warning | yes | `else` block is unnecessary because all prior branches return |
1547
1558
  | `redundant-ignored-match-binding` | hint | yes | Ignored `as _` match binding is redundant |
1548
1559
  | `redundant-null-check` | hint | — | Null check on a value already known to be non-null by flow analysis |
1549
1560
  | `redundant-return` | hint | yes | Final bare `return` in a `void` function is unnecessary |
1561
+ | `redundant-type-annotation` | hint | yes | Type annotation is redundant; the type is inferred from the initializer |
1562
+ | `redundant-unsafe` | hint | yes | `unsafe` block contains no unsafe operations and can be removed |
1550
1563
  | `reserved-primitive-name` | warning | yes | Binding uses a reserved built-in type name in its active namespace |
1551
1564
  | `self-assignment` | warning | — | Variable is assigned to itself |
1552
1565
  | `self-comparison` | warning | — | Value is compared to itself, making the condition constant |
1553
1566
  | `shadow` | warning | — | Local binding shadows an outer binding with the same name |
1554
1567
  | `trailing-list-comma` | hint | yes | Trailing comma in call argument list is redundant |
1555
1568
  | `unreachable-code` | warning | — | Code after a guaranteed terminator cannot execute |
1556
- | `redundant-unsafe` | hint | — | `unsafe` block contains no unsafe operations and can be removed |
1557
- | `unused-import` | warning | yes | Import alias is never referenced |
1569
+ | `unused-import` | warning | — | Import alias is never referenced |
1558
1570
  | `unused-local` | warning | — | Local binding is never referenced |
1559
1571
  | `unused-param` | warning | — | Parameter is never referenced |
1560
1572
  | `useless-expression` | warning | — | Expression statement has no side effects and its result is unused |
data/lib/milk_tea/base.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  require "pathname"
4
4
 
5
5
  module MilkTea
6
- VERSION = "0.4.24"
6
+ VERSION = "0.4.26"
7
7
 
8
8
  def self.root
9
9
  @root ||= Pathname.new(File.expand_path("../..", __dir__))
@@ -141,6 +141,27 @@ module MilkTea
141
141
  "GuiLoadIcons" => "ptr[ptr[char]]?",
142
142
  ).freeze
143
143
 
144
+ # stb_image.h documents these as returning NULL on failure (and
145
+ # stbi_failure_reason as returning NULL before the first failure), so
146
+ # the raw binding must expose them as nullable for null checks to work.
147
+ stb_image_function_return_overrides = {
148
+ "stbi_failure_reason" => "cstr?",
149
+ "stbi_load" => "ptr[stbi_uc]?",
150
+ "stbi_load_16" => "ptr[stbi_us]?",
151
+ "stbi_load_16_from_callbacks" => "ptr[stbi_us]?",
152
+ "stbi_load_16_from_memory" => "ptr[stbi_us]?",
153
+ "stbi_load_from_callbacks" => "ptr[stbi_uc]?",
154
+ "stbi_load_from_memory" => "ptr[stbi_uc]?",
155
+ "stbi_load_gif_from_memory" => "ptr[stbi_uc]?",
156
+ "stbi_loadf" => "ptr[float]?",
157
+ "stbi_loadf_from_callbacks" => "ptr[float]?",
158
+ "stbi_loadf_from_memory" => "ptr[float]?",
159
+ "stbi_zlib_decode_malloc" => "ptr[char]?",
160
+ "stbi_zlib_decode_malloc_guesssize" => "ptr[char]?",
161
+ "stbi_zlib_decode_malloc_guesssize_headerflag" => "ptr[char]?",
162
+ "stbi_zlib_decode_noheader_malloc" => "ptr[char]?",
163
+ }.freeze
164
+
144
165
  sdl3_documented_function_param_overrides = {
145
166
  "SDL_AcquireGPUSwapchainTexture" => {
146
167
  "swapchain_texture_width" => "ptr[uint]?",
@@ -1269,6 +1290,7 @@ module MilkTea
1269
1290
  binding_path: root.join("std/c/stb_image.mt"),
1270
1291
  include_directives: ["stb_image.h"],
1271
1292
  declaration_name_prefixes: ["stbi_", "STBI_"],
1293
+ function_return_type_overrides: stb_image_function_return_overrides,
1272
1294
  header_candidates: [
1273
1295
  MilkTea.data_root.join("third_party/stb-upstream/stb_image.h").to_s,
1274
1296
  ],
@@ -31,8 +31,10 @@ module MilkTea
31
31
  else
32
32
  []
33
33
  end
34
+ debug_guard_receivers = collect_debug_null_guards_for_statement(statement)
34
35
  statement_lines = with_checked_index_aliases(aliases) do
35
- case statement
36
+ debug_guard_lines = emit_debug_null_guard_lines(debug_guard_receivers, indent, function)
37
+ debug_guard_lines + case statement
36
38
  when IR::LocalDecl
37
39
  if array_type?(statement.type) && statement.value.is_a?(IR::Call)
38
40
  lines = ["#{indent}#{c_declaration(statement.type, statement.linkage_name)};"]
@@ -524,6 +526,121 @@ module MilkTea
524
526
  nil
525
527
  end
526
528
 
529
+ # ── debug null traps ────────────────────────────────────────────────────
530
+ # Debug builds insert `if (ptr == NULL) mt_fatal(...)` before every
531
+ # unconditionally-evaluated raw pointer dereference (pointer field
532
+ # access, pointer indexing, read(ptr)) so a NULL that crosses the type
533
+ # system's non-null contract — typically from an FFI boundary — fatales
534
+ # with a function name instead of segfaulting. Receivers are duplicated
535
+ # into the guard, so only side-effect-free receivers qualify, and
536
+ # conditional subtrees (and/or right sides, ?: branches) are skipped so
537
+ # derefs that would never execute are not reported. ref[T] receivers are
538
+ # excluded: refs are non-null by the type system, so a trap there would
539
+ # be pure noise.
540
+
541
+ def collect_debug_null_guards_for_statement(statement)
542
+ return [] unless @debug_guards
543
+
544
+ expressions = case statement
545
+ when IR::LocalDecl
546
+ [statement.value]
547
+ when IR::Assignment
548
+ [statement.target, statement.value]
549
+ when IR::ExpressionStmt
550
+ [statement.expression]
551
+ when IR::ReturnStmt
552
+ statement.value ? [statement.value] : []
553
+ when IR::IfStmt
554
+ [statement.condition]
555
+ when IR::WhileStmt
556
+ [statement.condition]
557
+ when IR::ForStmt
558
+ [statement.init, statement.condition, statement.post].compact
559
+ when IR::SwitchStmt
560
+ [statement.expression]
561
+ else
562
+ []
563
+ end
564
+
565
+ receivers = []
566
+ expressions.compact.each do |expression|
567
+ collect_debug_null_guard_receivers(expression, receivers)
568
+ end
569
+ receivers
570
+ end
571
+
572
+ def collect_debug_null_guard_receivers(expression, receivers)
573
+ case expression
574
+ when IR::Member
575
+ collect_debug_null_guard_receivers(expression.receiver, receivers)
576
+ add_debug_null_guard_receiver(receivers, expression.receiver)
577
+ when IR::Index
578
+ collect_debug_null_guard_receivers(expression.receiver, receivers)
579
+ collect_debug_null_guard_receivers(expression.index, receivers)
580
+ add_debug_null_guard_receiver(receivers, expression.receiver)
581
+ when IR::Unary
582
+ collect_debug_null_guard_receivers(expression.operand, receivers)
583
+ add_debug_null_guard_receiver(receivers, expression.operand) if expression.operator == "*"
584
+ when IR::Call
585
+ collect_debug_null_guard_receivers(expression.callee, receivers) unless expression.callee.is_a?(String)
586
+ expression.arguments.each { |argument| collect_debug_null_guard_receivers(argument, receivers) }
587
+ when IR::Binary
588
+ collect_debug_null_guard_receivers(expression.left, receivers)
589
+ return if expression.operator == "and" || expression.operator == "or"
590
+
591
+ collect_debug_null_guard_receivers(expression.right, receivers)
592
+ when IR::Conditional
593
+ collect_debug_null_guard_receivers(expression.condition, receivers)
594
+ when IR::AddressOf, IR::Cast, IR::ReinterpretExpr
595
+ collect_debug_null_guard_receivers(expression.expression, receivers)
596
+ when IR::CheckedIndex, IR::CheckedSpanIndex, IR::NullableIndex, IR::NullableSpanIndex
597
+ collect_debug_null_guard_receivers(expression.receiver, receivers)
598
+ collect_debug_null_guard_receivers(expression.index, receivers)
599
+ when IR::AggregateLiteral
600
+ expression.fields.each { |field| collect_debug_null_guard_receivers(field.value, receivers) }
601
+ when IR::ArrayLiteral
602
+ expression.elements.each { |element| collect_debug_null_guard_receivers(element, receivers) }
603
+ when IR::VariantLiteral
604
+ expression.fields.each { |field| collect_debug_null_guard_receivers(field.value, receivers) }
605
+ when IR::SimdLaneWith
606
+ collect_debug_null_guard_receivers(expression.src, receivers)
607
+ collect_debug_null_guard_receivers(expression.value, receivers)
608
+ when IR::Assignment, IR::LocalDecl
609
+ collect_debug_null_guard_receivers(expression.target, receivers) if expression.is_a?(IR::Assignment)
610
+ collect_debug_null_guard_receivers(expression.value, receivers) if expression.value
611
+ end
612
+ end
613
+
614
+ def add_debug_null_guard_receiver(receivers, receiver_expression)
615
+ return unless debug_null_guardable_receiver?(receiver_expression)
616
+ return if receivers.any? { |existing| existing.equal?(receiver_expression) }
617
+
618
+ receivers << receiver_expression
619
+ end
620
+
621
+ def debug_null_guardable_receiver?(expression)
622
+ type = expression.respond_to?(:type) ? expression.type : nil
623
+ return false if ref_type?(type)
624
+
625
+ raw_pointer_type?(type) || nullable_pointer_like_type?(type)
626
+ end
627
+
628
+ def emit_debug_null_guard_lines(receivers, indent, function)
629
+ return [] if receivers.empty?
630
+
631
+ message = "null pointer dereferenced in #{function.linkage_name}"
632
+ lines = []
633
+ emitted = Set.new
634
+ receivers.each do |receiver|
635
+ receiver_text = emit_expression(receiver)
636
+ next if emitted.include?(receiver_text)
637
+
638
+ emitted << receiver_text
639
+ lines << "#{indent}if (#{receiver_text} == NULL) mt_fatal(\"#{message}\");"
640
+ end
641
+ lines
642
+ end
643
+
527
644
  def statements_require_scope?(statements)
528
645
  statements.any? { |statement| statement.is_a?(IR::LocalDecl) }
529
646
  end