mt-lang 0.4.25 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f4c65f190209c5486b52d39b2ac868b2e2c076ad3ec01206151df16675319cf9
4
- data.tar.gz: 28bb0127501319fb64f0da085409bb8658b6fd9cd41d8c18c0ac2175608f93dd
3
+ metadata.gz: 8f75e56a0679c161c076efb3dd694afe256154c512e7ffae171e13c2d98d181d
4
+ data.tar.gz: ff00b0459c88d4fd31d12b0fc25e953d339fe7d8f7ed10964b5d8e2b70065a6f
5
5
  SHA512:
6
- metadata.gz: '098dc1e2c0303bbf39dcd4402d7f21482c682f83b904799c1c85a9a73207b4c8443c0a98bdee192db9d17a0bf33ec4a96ad31c03a733f28c57880b7601c91d42'
7
- data.tar.gz: ce0f5ba1ae8e8c6f958ab4cd5e2d0e89c522a9f814d8e5e31a1e8b8b849faafe437caaab098bd3a4d59755cdbf4f12c23d521810e9892c6e039acf27176b7d10
6
+ metadata.gz: 72e4cfb94f691ace787f420cbd2b6481c0b189282d21dbe42b47ebd795844f5e42a59a95fc64f1e75f56c6ec7119723231fef4601c8d856f6d7ada5ed107d26f
7
+ data.tar.gz: 01ba76cc9a0aeb3ab65fbd5dea21a9995ace49a3328003352fa13b0c54c9cc42e8b3e1bb6423d703913439ca4de91b4f70348d128a6c813a835c28e9345ff88b
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.25"
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
@@ -127,6 +127,13 @@ module MilkTea
127
127
 
128
128
  def infer_value_type(handle_expression, env:)
129
129
  handle_type = infer_expression_type(handle_expression, env:)
130
+ # BUG FIX: nullable pointer handles (ptr[T]?) read through as their
131
+ # base pointer type; std/net relies on this after flow-unguarded
132
+ # null checks on storage fields.
133
+ if handle_type.is_a?(Types::Nullable) &&
134
+ (ref_type?(handle_type.base) || pointer_type?(handle_type.base))
135
+ handle_type = handle_type.base
136
+ end
130
137
  return referenced_type(handle_type) if ref_type?(handle_type)
131
138
  return pointee_type(handle_type) if pointer_type?(handle_type)
132
139
 
@@ -371,7 +378,7 @@ module MilkTea
371
378
  elements = value.map { |element| lower_compile_time_literal(element, element_type) }
372
379
  return nil if elements.any?(&:nil?)
373
380
 
374
- IR::ArrayLiteral.new(type:, elements:)
381
+ return IR::ArrayLiteral.new(type:, elements:)
375
382
  when Hash
376
383
  return nil unless type.is_a?(Types::Struct)
377
384
  fields = value.map do |name, field_value|
@@ -721,8 +721,10 @@ module MilkTea
721
721
  raise_sema_error("operator #{expression.operator} is not supported for type #{bad_type}")
722
722
  end
723
723
  end
724
- unless common_numeric_type(left_type, right_type) || types_compatible?(left_type, right_type) || types_compatible?(right_type, left_type)
725
- raise_sema_error("operator #{expression.operator} requires comparable types, got #{left_type} and #{right_type}")
724
+ unless common_numeric_type(left_type, right_type) || types_compatible?(left_type, right_type) || types_compatible?(right_type, left_type) || refined_null_comparison?(expression, scopes)
725
+ message = "operator #{expression.operator} requires comparable types, got #{left_type} and #{right_type}"
726
+ hint = null_comparison_hint(left_type, right_type)
727
+ raise_sema_error(hint ? "#{message}; #{hint}" : message)
726
728
  end
727
729
 
728
730
  @ctx.types.fetch("bool")
@@ -731,6 +733,45 @@ module MilkTea
731
733
  end
732
734
  end
733
735
 
736
+ # A null comparison on a nullable binding stays legal after flow
737
+ # refinement has narrowed the binding to non-null: the declared storage
738
+ # is still nullable, and the check is a redundant-but-valid test (the
739
+ # linter reports it as redundant-null-check). Only bare `null` qualifies;
740
+ # a typed `null[...]` with a mismatched target stays a type error.
741
+ def refined_null_comparison?(expression, scopes)
742
+ if expression.left.is_a?(AST::NullLiteral) && expression.left.type.nil? && expression.right.is_a?(AST::Identifier)
743
+ identifier_expression = expression.right
744
+ elsif expression.right.is_a?(AST::NullLiteral) && expression.right.type.nil? && expression.left.is_a?(AST::Identifier)
745
+ identifier_expression = expression.left
746
+ else
747
+ return false
748
+ end
749
+
750
+ binding = lookup_value(identifier_expression.name, scopes)
751
+ binding&.storage_type.is_a?(Types::Nullable)
752
+ end
753
+
754
+ # A null comparison against a non-nullable operand is a contract mismatch,
755
+ # not just incompatible operand types: the message should say which fix
756
+ # applies (nullable declaration, raw zero comparison, or removal).
757
+ def null_comparison_hint(left_type, right_type)
758
+ other = if left_type.is_a?(Types::Null) && !right_type.is_a?(Types::Null)
759
+ right_type
760
+ elsif right_type.is_a?(Types::Null) && !left_type.is_a?(Types::Null)
761
+ left_type
762
+ end
763
+ return nil unless other
764
+ return nil if other.is_a?(Types::Nullable) || other.is_a?(Types::Null)
765
+
766
+ if ref_type?(other)
767
+ "#{other} is never null by design; refs cannot be compared against null"
768
+ elsif pointer_type?(other) || opaque_type?(other) || other == Types::Registry.primitive("cstr")
769
+ hint = "#{other} is declared non-null; declare it #{other}? if null is possible"
770
+ hint << ", or compare against zero[#{other}] for raw zero-pointer storage" if zero_supported_type?(other)
771
+ hint
772
+ end
773
+ end
774
+
734
775
  def infer_if_expression(expression, scopes:, expected_type: nil)
735
776
  condition_type = infer_expression(expression.condition, scopes:, expected_type: @ctx.types.fetch("bool"))
736
777
  ensure_assignable!(condition_type, @ctx.types.fetch("bool"), "if expression condition must be bool, got #{condition_type}")
@@ -740,6 +740,12 @@ module MilkTea
740
740
  end
741
741
 
742
742
  def zero_initializable_type?(type, operation: "zero")
743
+ return true if zero_supported_type?(type)
744
+
745
+ raise_sema_error("#{operation} does not support type #{type}")
746
+ end
747
+
748
+ def zero_supported_type?(type)
743
749
  return true if type.is_a?(Types::Primitive) && !type.void?
744
750
  return true if type.is_a?(Types::Nullable)
745
751
  return true if type.is_a?(Types::EnumBase)
@@ -761,7 +767,7 @@ module MilkTea
761
767
  return true if simd_type?(type)
762
768
  return true if atomic_type?(type)
763
769
 
764
- raise_sema_error("#{operation} does not support type #{type}")
770
+ false
765
771
  end
766
772
 
767
773
  def layout_aggregate_type?(type)
@@ -101,6 +101,11 @@ module MilkTea
101
101
  return missing_option_value(option) unless value
102
102
 
103
103
  options[:nullable_report_path] = value
104
+ when "--nullable-policy"
105
+ value = @argv.shift
106
+ return missing_option_value(option) unless value
107
+
108
+ options[:nullable_policy_path] = value
104
109
  else
105
110
  @err.puts("unknown bindgen option #{option}")
106
111
  print_help
@@ -109,9 +114,60 @@ module MilkTea
109
114
  end
110
115
 
111
116
  options[:include_directives] = nil if options[:include_directives].empty?
117
+ if options[:nullable_policy_path]
118
+ policy = load_nullable_policy(options.delete(:nullable_policy_path))
119
+ return nil unless policy
120
+
121
+ options[:function_return_type_overrides] = policy[:return_types]
122
+ options[:function_param_type_overrides] = policy[:parameters]
123
+ end
112
124
  options
113
125
  end
114
126
 
127
+ # A nullable policy lists the symbols a header documents as returning NULL
128
+ # (or taking NULL-able out-pointers) so generated raw bindings expose them
129
+ # as nullable `T?` without hand-annotating the C header. Entries feed the
130
+ # same function_return_type_overrides / function_param_type_overrides
131
+ # machinery as the raw binding registry and appear in the nullable report.
132
+ def load_nullable_policy(path)
133
+ require "json"
134
+ policy_path = File.expand_path(path)
135
+ unless File.file?(policy_path)
136
+ @err.puts("nullable policy file not found: #{path}")
137
+ return nil
138
+ end
139
+
140
+ document = begin
141
+ JSON.parse(File.read(policy_path))
142
+ rescue JSON::ParserError => e
143
+ @err.puts("nullable policy is not valid JSON: #{e.message}")
144
+ return nil
145
+ end
146
+
147
+ unless document.is_a?(Hash)
148
+ @err.puts("nullable policy must be a JSON object")
149
+ return nil
150
+ end
151
+
152
+ unknown_keys = document.keys - ["return_types", "parameters"]
153
+ unless unknown_keys.empty?
154
+ @err.puts("nullable policy has unknown keys: #{unknown_keys.join(', ')} (expected return_types, parameters)")
155
+ return nil
156
+ end
157
+
158
+ return_types = document["return_types"] || {}
159
+ parameters = document["parameters"] || {}
160
+ policy_shape_valid = return_types.is_a?(Hash) && return_types.keys.all?(String) && return_types.values.all?(String) &&
161
+ parameters.is_a?(Hash) && parameters.keys.all?(String) &&
162
+ parameters.values.all? { |value| value.is_a?(Hash) && value.keys.all?(String) && value.values.all?(String) }
163
+ unless policy_shape_valid
164
+ @err.puts('nullable policy must map {"return_types": {symbol: "type?"}, "parameters": {symbol: {param: "type?"}}}')
165
+ return nil
166
+ end
167
+
168
+ { return_types:, parameters: }
169
+ end
170
+
115
171
  def missing_option_value(option)
116
172
  @err.puts("missing value for #{option}")
117
173
  print_help
@@ -776,6 +776,7 @@ module MilkTea
776
776
 
777
777
  Options:
778
778
  -o, --output OUTPUT Write the generated module to this file.
779
+ --nullable-policy PATH Apply a JSON nullable policy of symbols documented as returning NULL.
779
780
  --nullable-report PATH Write the remaining manual nullable policy report to this file.
780
781
  --link LIB Link against this library (repeatable).
781
782
  --include HEADER Extra #include directive (repeatable).
@@ -911,7 +912,7 @@ module MilkTea
911
912
  io.puts(" mtc deps lock [PATH_OR_PACKAGE] [--check]")
912
913
  io.puts(" mtc deps publish [PATH_OR_PACKAGE] [--upstream]")
913
914
  io.puts(" mtc deps fetch [PATH_OR_PACKAGE]")
914
- io.puts(" mtc bindgen MODULE HEADER [-o OUTPUT] [--nullable-report PATH] [--link LIB] [--include HEADER] [--clang PATH] [--clang-arg ARG]")
915
+ io.puts(" mtc bindgen MODULE HEADER [-o OUTPUT] [--nullable-policy PATH] [--nullable-report PATH] [--link LIB] [--include HEADER] [--clang PATH] [--clang-arg ARG]")
915
916
  io.puts(" mtc cache purge|status")
916
917
  io.puts(" mtc docs [--open] [--port PORT]")
917
918
  io.puts(" mtc std list [--json]")
data/std/c/stb_image.mt CHANGED
@@ -11,16 +11,16 @@ struct stbi_io_callbacks:
11
11
  skip: fn(arg0: ptr[void], arg1: int) -> void
12
12
  eof: fn(arg0: ptr[void]) -> int
13
13
 
14
- external function stbi_load_from_memory(buffer: const_ptr[stbi_uc], len: int, x: ptr[int], y: ptr[int], channels_in_file: ptr[int], desired_channels: int) -> ptr[stbi_uc]
15
- external function stbi_load_from_callbacks(clbk: const_ptr[stbi_io_callbacks], user: ptr[void], x: ptr[int], y: ptr[int], channels_in_file: ptr[int], desired_channels: int) -> ptr[stbi_uc]
16
- external function stbi_load(filename: cstr, x: ptr[int], y: ptr[int], channels_in_file: ptr[int], desired_channels: int) -> ptr[stbi_uc]
17
- external function stbi_load_gif_from_memory(buffer: const_ptr[stbi_uc], len: int, delays: ptr[ptr[int]], x: ptr[int], y: ptr[int], z: ptr[int], comp: ptr[int], req_comp: int) -> ptr[stbi_uc]
18
- external function stbi_load_16_from_memory(buffer: const_ptr[stbi_uc], len: int, x: ptr[int], y: ptr[int], channels_in_file: ptr[int], desired_channels: int) -> ptr[stbi_us]
19
- external function stbi_load_16_from_callbacks(clbk: const_ptr[stbi_io_callbacks], user: ptr[void], x: ptr[int], y: ptr[int], channels_in_file: ptr[int], desired_channels: int) -> ptr[stbi_us]
20
- external function stbi_load_16(filename: cstr, x: ptr[int], y: ptr[int], channels_in_file: ptr[int], desired_channels: int) -> ptr[stbi_us]
21
- external function stbi_loadf_from_memory(buffer: const_ptr[stbi_uc], len: int, x: ptr[int], y: ptr[int], channels_in_file: ptr[int], desired_channels: int) -> ptr[float]
22
- external function stbi_loadf_from_callbacks(clbk: const_ptr[stbi_io_callbacks], user: ptr[void], x: ptr[int], y: ptr[int], channels_in_file: ptr[int], desired_channels: int) -> ptr[float]
23
- external function stbi_loadf(filename: cstr, x: ptr[int], y: ptr[int], channels_in_file: ptr[int], desired_channels: int) -> ptr[float]
14
+ external function stbi_load_from_memory(buffer: const_ptr[stbi_uc], len: int, x: ptr[int], y: ptr[int], channels_in_file: ptr[int], desired_channels: int) -> ptr[stbi_uc]?
15
+ external function stbi_load_from_callbacks(clbk: const_ptr[stbi_io_callbacks], user: ptr[void], x: ptr[int], y: ptr[int], channels_in_file: ptr[int], desired_channels: int) -> ptr[stbi_uc]?
16
+ external function stbi_load(filename: cstr, x: ptr[int], y: ptr[int], channels_in_file: ptr[int], desired_channels: int) -> ptr[stbi_uc]?
17
+ external function stbi_load_gif_from_memory(buffer: const_ptr[stbi_uc], len: int, delays: ptr[ptr[int]], x: ptr[int], y: ptr[int], z: ptr[int], comp: ptr[int], req_comp: int) -> ptr[stbi_uc]?
18
+ external function stbi_load_16_from_memory(buffer: const_ptr[stbi_uc], len: int, x: ptr[int], y: ptr[int], channels_in_file: ptr[int], desired_channels: int) -> ptr[stbi_us]?
19
+ external function stbi_load_16_from_callbacks(clbk: const_ptr[stbi_io_callbacks], user: ptr[void], x: ptr[int], y: ptr[int], channels_in_file: ptr[int], desired_channels: int) -> ptr[stbi_us]?
20
+ external function stbi_load_16(filename: cstr, x: ptr[int], y: ptr[int], channels_in_file: ptr[int], desired_channels: int) -> ptr[stbi_us]?
21
+ external function stbi_loadf_from_memory(buffer: const_ptr[stbi_uc], len: int, x: ptr[int], y: ptr[int], channels_in_file: ptr[int], desired_channels: int) -> ptr[float]?
22
+ external function stbi_loadf_from_callbacks(clbk: const_ptr[stbi_io_callbacks], user: ptr[void], x: ptr[int], y: ptr[int], channels_in_file: ptr[int], desired_channels: int) -> ptr[float]?
23
+ external function stbi_loadf(filename: cstr, x: ptr[int], y: ptr[int], channels_in_file: ptr[int], desired_channels: int) -> ptr[float]?
24
24
  external function stbi_hdr_to_ldr_gamma(gamma: float) -> void
25
25
  external function stbi_hdr_to_ldr_scale(scale: float) -> void
26
26
  external function stbi_ldr_to_hdr_gamma(gamma: float) -> void
@@ -28,7 +28,7 @@ external function stbi_ldr_to_hdr_scale(scale: float) -> void
28
28
  external function stbi_is_hdr_from_callbacks(clbk: const_ptr[stbi_io_callbacks], user: ptr[void]) -> int
29
29
  external function stbi_is_hdr_from_memory(buffer: const_ptr[stbi_uc], len: int) -> int
30
30
  external function stbi_is_hdr(filename: cstr) -> int
31
- external function stbi_failure_reason() -> cstr
31
+ external function stbi_failure_reason() -> cstr?
32
32
  external function stbi_image_free(retval_from_stbi_load: ptr[void]) -> void
33
33
  external function stbi_info_from_memory(buffer: const_ptr[stbi_uc], len: int, x: ptr[int], y: ptr[int], comp: ptr[int]) -> int
34
34
  external function stbi_info_from_callbacks(clbk: const_ptr[stbi_io_callbacks], user: ptr[void], x: ptr[int], y: ptr[int], comp: ptr[int]) -> int
@@ -42,11 +42,11 @@ external function stbi_set_flip_vertically_on_load(flag_true_if_should_flip: int
42
42
  external function stbi_set_unpremultiply_on_load_thread(flag_true_if_should_unpremultiply: int) -> void
43
43
  external function stbi_convert_iphone_png_to_rgb_thread(flag_true_if_should_convert: int) -> void
44
44
  external function stbi_set_flip_vertically_on_load_thread(flag_true_if_should_flip: int) -> void
45
- external function stbi_zlib_decode_malloc_guesssize(buffer: cstr, len: int, initial_size: int, outlen: ptr[int]) -> ptr[char]
46
- external function stbi_zlib_decode_malloc_guesssize_headerflag(buffer: cstr, len: int, initial_size: int, outlen: ptr[int], parse_header: int) -> ptr[char]
47
- external function stbi_zlib_decode_malloc(buffer: cstr, len: int, outlen: ptr[int]) -> ptr[char]
45
+ external function stbi_zlib_decode_malloc_guesssize(buffer: cstr, len: int, initial_size: int, outlen: ptr[int]) -> ptr[char]?
46
+ external function stbi_zlib_decode_malloc_guesssize_headerflag(buffer: cstr, len: int, initial_size: int, outlen: ptr[int], parse_header: int) -> ptr[char]?
47
+ external function stbi_zlib_decode_malloc(buffer: cstr, len: int, outlen: ptr[int]) -> ptr[char]?
48
48
  external function stbi_zlib_decode_buffer(obuffer: ptr[char], olen: int, ibuffer: cstr, ilen: int) -> int
49
- external function stbi_zlib_decode_noheader_malloc(buffer: cstr, len: int, outlen: ptr[int]) -> ptr[char]
49
+ external function stbi_zlib_decode_noheader_malloc(buffer: cstr, len: int, outlen: ptr[int]) -> ptr[char]?
50
50
  external function stbi_zlib_decode_noheader_buffer(obuffer: ptr[char], olen: int, ibuffer: cstr, ilen: int) -> int
51
51
 
52
52
  const STBI_VERSION: int = 1
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mt-lang
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.25
4
+ version: 0.4.26
5
5
  platform: ruby
6
6
  authors:
7
7
  - Long (Teefan) Tran
@@ -632,7 +632,7 @@ metadata:
632
632
  homepage_uri: https://teefan.github.io/mt-lang/
633
633
  source_code_uri: https://github.com/teefan/mt-lang
634
634
  post_install_message: |
635
- Milk Tea 0.4.25 installed!
635
+ Milk Tea 0.4.26 installed!
636
636
 
637
637
  System requirements:
638
638
  - A C compiler (gcc or clang) must be available on PATH