mt-lang 0.3.13 → 0.3.16

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/docs/index.html +31 -23
  4. data/docs/language-design.md +13 -17
  5. data/docs/language-manual.md +19 -7
  6. data/lib/milk_tea/base.rb +1 -1
  7. data/lib/milk_tea/core/ast.rb +2 -2
  8. data/lib/milk_tea/core/control_flow/builder.rb +62 -2
  9. data/lib/milk_tea/core/lowering/async/analysis.rb +3 -9
  10. data/lib/milk_tea/core/lowering/async/lowering.rb +2 -10
  11. data/lib/milk_tea/core/lowering/async/normalization.rb +2 -7
  12. data/lib/milk_tea/core/lowering/block.rb +1 -5
  13. data/lib/milk_tea/core/lowering/expressions.rb +47 -3
  14. data/lib/milk_tea/core/lowering/proc.rb +1 -5
  15. data/lib/milk_tea/core/lowering/resolve.rb +26 -2
  16. data/lib/milk_tea/core/lowering/scans.rb +3 -1
  17. data/lib/milk_tea/core/lowering/utils.rb +7 -28
  18. data/lib/milk_tea/core/parser/statements.rb +18 -12
  19. data/lib/milk_tea/core/pretty_printer/ast_formatter.rb +3 -11
  20. data/lib/milk_tea/core/semantic_analyzer/analysis_context.rb +2 -3
  21. data/lib/milk_tea/core/semantic_analyzer/flow_refinement.rb +1 -1
  22. data/lib/milk_tea/core/semantic_analyzer/function_binding.rb +1 -2
  23. data/lib/milk_tea/core/semantic_analyzer/nullability.rb +2 -4
  24. data/lib/milk_tea/core/semantic_analyzer/statements.rb +2 -8
  25. data/lib/milk_tea/core/semantic_analyzer/type_compatibility.rb +29 -2
  26. data/lib/milk_tea/core/types.rb +0 -6
  27. data/lib/milk_tea/tooling/linter/imports_platform.rb +2 -4
  28. data/lib/milk_tea/tooling/linter/release_rules.rb +1 -6
  29. data/lib/milk_tea/tooling/linter/source_helpers.rb +0 -6
  30. data/lib/milk_tea/tooling/linter/visitors.rb +4 -8
  31. data/lib/milk_tea/tooling/sexpr_dumper.rb +2 -0
  32. data/std/asset_pack.mt +1 -1
  33. data/std/async/libuv_runtime.mt +6 -6
  34. data/std/async/mailbox.mt +4 -4
  35. data/std/{cell.mt → box.mt} +9 -9
  36. data/std/cookie.mt +2 -2
  37. data/std/crypto.mt +1 -1
  38. data/std/curl/runtime.mt +10 -10
  39. data/std/fs.linux.mt +26 -26
  40. data/std/fs.windows.mt +22 -22
  41. data/std/goap.mt +2 -2
  42. data/std/http/server.mt +21 -21
  43. data/std/http.mt +14 -14
  44. data/std/jobs.mt +4 -4
  45. data/std/json.mt +5 -5
  46. data/std/log.mt +1 -1
  47. data/std/mem/tracking.mt +1 -1
  48. data/std/net/channel.mt +4 -4
  49. data/std/net/clock.mt +6 -6
  50. data/std/net/discovery.mt +7 -7
  51. data/std/net/lobby.mt +10 -10
  52. data/std/net/manager.mt +1 -1
  53. data/std/net/mux.mt +4 -4
  54. data/std/net/nat.mt +4 -4
  55. data/std/net/packet.mt +3 -3
  56. data/std/net/punch.mt +3 -3
  57. data/std/net/rpc.mt +3 -3
  58. data/std/net/session.mt +15 -15
  59. data/std/net/stun.mt +4 -4
  60. data/std/net/turn.mt +4 -4
  61. data/std/oauth2.mt +12 -12
  62. data/std/path.mt +8 -8
  63. data/std/process.mt +4 -4
  64. data/std/raylib/packed_assets.mt +5 -5
  65. data/std/serialize.mt +1 -1
  66. data/std/tar.mt +10 -10
  67. data/std/terminal.mt +18 -18
  68. data/std/tls.mt +8 -8
  69. data/std/uri.mt +1 -1
  70. data/std/utility.mt +1 -1
  71. metadata +3 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ccbf498d3496c9dac904d8f2b43442e971507874b856149171b78f7eed90e949
4
- data.tar.gz: '090885ac2d6181ba4cabb1ef6ed5c5a6e79cb3e60fe4fa61315c6f2404d5e00f'
3
+ metadata.gz: b5cd3c1ef5e380b8d0c5d4e20f829400d01c6fdfd3e495e8c98050f4af8b59ba
4
+ data.tar.gz: 0e083d1780c9d591b996604a8b489726fa0f0c6fb85b8dc03da87b681383d5ca
5
5
  SHA512:
6
- metadata.gz: '08b68224eb3b428b8fab0845a98f79605c06f95ffc66a6a653586da99f230720f177781ac481debee8aede3cda843f34f12fb953f62846fc8d7048d3b97726c1'
7
- data.tar.gz: cd69cddcf5b0cf2bb86fe4443c502c214fc73dda6318532f14e9548b8bbc7b84186956b9bf4a975148ad0f45d8bb9b86f1093956c089af293f1755ca2c8b6589
6
+ metadata.gz: 44f866e7d6bf6fc4998e0a9054bf898b27ed47ef38149de1881baf1d810e845b94e3d1f3ff304486654ea339329d726a10d969bcff4021943cc98b03bfe3fe01
7
+ data.tar.gz: 0fa7237c14aa9e038b4d51ae08a5971430d0e47b97fb19caf619aeb80a5e562de99aae1cf342a11ffea41e3b2268a128454bbf022b3e9182ddd94ff2ff0aff45
data/README.md CHANGED
@@ -236,7 +236,7 @@ Callable and `ref[...]` rules:
236
236
  - External functions still cannot take `ref[...]` parameters, and ordinary functions still cannot return `ref[...]`.
237
237
  - `proc` captures are value captures. A captured local is not a mutable alias back to the outer binding. Any storable type may be captured, including scalars, arrays, structs, and other `proc` values. Captured `proc` values participate in the ref-counted lifecycle: the capturing proc retains the captured proc on creation and releases it when the env is freed.
238
238
  - `ref[T]` values are not capturable by design since they are non-owning.
239
- - Shared mutable proc state should use explicit storage such as `std.cell.alloc[T](...)` or other explicit pointer-backed state, not implicit mutable capture.
239
+ - Shared mutable proc state should use explicit storage such as `std.box.alloc[T](...)` or other explicit pointer-backed state, not implicit mutable capture.
240
240
 
241
241
  ## 5. Data Declarations
242
242
 
@@ -477,7 +477,7 @@ Rules:
477
477
  - Conditions must be `bool`.
478
478
  - There is no truthy or falsy coercion from integers or pointers.
479
479
  - `pass` is an explicit no-op statement for intentionally empty block bodies.
480
- - `if` supports an inline single-statement form: `if cond: stmt else: stmt`. The body is a single statement on the same line instead of an indented block.
480
+ - Every block-taking statement accepts a `:`-newline-indented block, or a single statement on the same line after `:`. This applies to `if`/`else` (e.g. `if cond: stmt else: stmt`), `while`, `for`, `defer` (e.g. `defer: cleanup()`), and `unsafe`. `match` arms are the exception: their inline form is a single value expression.
481
481
 
482
482
  `match` supports the following scrutinee types:
483
483
 
@@ -615,7 +615,7 @@ Rules:
615
615
 
616
616
  `defer`:
617
617
 
618
- - `defer expr`
618
+ - `defer: expr` — single-statement inline form
619
619
  - `defer:` block form
620
620
  - `return` is not allowed inside defer blocks.
621
621
 
@@ -902,7 +902,7 @@ Core modules in `std/`:
902
902
 
903
903
  **Compression**: `std.gzip`, `std.tar`
904
904
 
905
- **Other**: `std.bytes`, `std.ctype`, `std.asset_pack`, `std.cell`
905
+ **Other**: `std.bytes`, `std.ctype`, `std.asset_pack`, `std.box`
906
906
 
907
907
  See module source for full method surface. Iterator forms:
908
908
  - Pointer-returning (`next() -> nullable ptr[T]`): `Vec`, `Deque`, `BinaryHeap`/`PriorityQueue`/`OrderedSet` (read-only), `OrderedMap.keys`/`Map.keys`/`Set`/`LinkedMap.keys`/`LinkedSet`/`Counter.keys`/`MultiSet.values`, `Queue`/`Stack` (mutable), `RingBuffer`, `SparseSet`
@@ -955,7 +955,7 @@ Heredoc notes:
955
955
  - Conditions must be `bool`.
956
956
  - No truthy or falsy coercion.
957
957
  - Outside external-call boundaries, enum and flags values do not implicitly coerce to their backing integer types.
958
- - Mixed signed and unsigned integer arithmetic requires an explicit cast.
958
+ - Mixed signed and unsigned integer arithmetic promotes to the narrowest signed type that holds both ranges (e.g. `ubyte + int` is `int`, `int + uint` is `long`); mixing with a 64-bit unsigned type such as `ulong` or `ptr_uint` requires an explicit cast.
959
959
  - Non-widening integer conversions (narrowing, signed → unsigned) require an explicit cast. Lossless widening conversions (same signedness to wider or equal width, unsigned → wider signed) are implicit.
960
960
  - `%` requires integer-compatible operands.
961
961
  - Bitwise operators require matching integer or flags types.
data/docs/index.html CHANGED
@@ -311,7 +311,7 @@ function main() -> int:
311
311
  <div class="table-wrap">
312
312
  <table class="attr-table">
313
313
  <tr><th>Principle</th><th>Description</th></tr>
314
- <tr><td><strong>What you see is what runs</strong></td><td>No hidden allocation, dispatch, or heap traffic. Every cost is spelled in source.</td></tr>
314
+ <tr><td><strong>What you see is what runs</strong></td><td>Ordinary values have no hidden allocation or dispatch. Runtime interface dispatch and closure environments are explicit language features with their own costs.</td></tr>
315
315
  <tr><td><strong>C is the ABI ground truth</strong></td><td>Structs, unions, pointers, and calling conventions map directly without lossy translation.</td></tr>
316
316
  <tr><td><strong>Safe by default, unsafe by choice</strong></td><td>Pointer operations require <code>unsafe</code>. Indexing is bounds-checked. Conversions are explicit.</td></tr>
317
317
  <tr><td><strong>Data-oriented first</strong></td><td>Plain structs, arrays, spans, pools, and arenas matter more than elaborate object systems.</td></tr>
@@ -355,7 +355,7 @@ function main() -> int:
355
355
  <!-- ────────────────────────────────────────────────────────────────────────── -->
356
356
  <section id="generated-c">
357
357
  <h2>Generated C</h2>
358
- <p>Milk Tea compiles to readable C that mirrors the source structure. No hidden runtime, no hidden heap traffic, no hidden dispatch. The output is designed for a human to debug, diff, and ship.</p>
358
+ <p>Milk Tea compiles to readable C that mirrors the source structure. Ordinary value operations do not introduce hidden heap traffic or dispatch; runtime interface values and closure environments remain explicit language features. The output is designed for a human to debug, diff, and ship.</p>
359
359
 
360
360
  <div class="code-wrap">
361
361
  <button class="copy-btn" onclick="copyCode(this)">Copy</button>
@@ -466,7 +466,7 @@ variant Action:
466
466
  heal(amount: int)
467
467
  idle
468
468
 
469
- ## ── Interfaces (nominal contracts, no hidden dispatch) ──────────────
469
+ ## ── Interfaces (nominal contracts; dyn provides explicit dispatch) ───
470
470
  public interface Damageable:
471
471
  editable function take_damage(amount: int) -&gt; void
472
472
  function is_alive() -&gt; bool
@@ -658,12 +658,12 @@ function main() -&gt; int:
658
658
 
659
659
  ## Collections: Vec and Map (import std.hash for primitive keys)
660
660
  var entities = vec.Vec[Entity].create()
661
- defer entities.release()
661
+ defer: entities.release()
662
662
  entities.push(Entity.create(1, EntityKind.player, "Hero"))
663
663
  entities.push(Entity.create(2, EntityKind.enemy, "Goblin"))
664
664
 
665
665
  var name_counts = map.Map[str, int].create()
666
- defer name_counts.release()
666
+ defer: name_counts.release()
667
667
  name_counts.set("player", 1)
668
668
  name_counts.set("enemy", 5)
669
669
 
@@ -680,7 +680,7 @@ function main() -&gt; int:
680
680
  break
681
681
  io.print_line(f"value #{v}")
682
682
 
683
- ## Parallel for (arrays/spans only, lengths must match)
683
+ ## Lockstep for over arrays/spans (lengths must match)
684
684
  let xs = array[float, 3](1.0, 2.0, 3.0)
685
685
  let ys = array[float, 3](4.0, 5.0, 6.0)
686
686
  for x, y in xs, ys:
@@ -742,7 +742,7 @@ function main() -&gt; int:
742
742
  io.print_line(f"Position: #{position.x:.2}, #{position.y:.2}")
743
743
  io.print_line(f"ID hex: #{player.entity.id:x}")
744
744
  var msg = fmt.format(f"Score: #{score}")
745
- defer msg.release()
745
+ defer: msg.release()
746
746
 
747
747
  ## Heredoc string
748
748
  let help = &lt;&lt;-HELP
@@ -753,7 +753,7 @@ function main() -&gt; int:
753
753
 
754
754
  ## Defer: cleanup at scope exit (lowers to C cleanup labels)
755
755
  let data: own[int] = heap.must_alloc[int](1)
756
- defer heap.release[int](data)
756
+ defer: heap.release[int](data)
757
757
 
758
758
  ## Proc / closure
759
759
  let scale = make_scaler(2.5)
@@ -840,7 +840,7 @@ function main() -&gt; int:
840
840
  <p>Milk Tea source files use the <code class="code-inline">.mt</code> extension. A file is either an <strong>ordinary</strong> source file or an <strong>external</strong> file for raw ABI bindings. There are no wildcard imports — every module must be imported by name.</p>
841
841
 
842
842
  <h3>Ordinary Files</h3>
843
- <p>The normal source form. No module header; module identity is inferred from the file path relative to <code class="code-inline">package.source_root</code>. <code class="code-inline">import</code> statements appear only at the top.</p>
843
+ <p>The normal source form. No module header; module identity is inferred from the file path relative to <code class="code-inline">package.source_root</code>. <code class="code-inline">import</code> statements appear only at the top. Circular module imports are supported through forward-declaration bindings and two-pass checking.</p>
844
844
  <div class="code-wrap">
845
845
  <button class="copy-btn" onclick="copyCode(this)">Copy</button>
846
846
  <pre><code>import std.math as math
@@ -1184,7 +1184,7 @@ static_assert(MAX_ENTITIES &gt; 0, "need at least one entity")</code></pre>
1184
1184
  <h2>Interfaces &amp; Methods</h2>
1185
1185
 
1186
1186
  <h3>Interfaces</h3>
1187
- <p>Explicit nominal method-set contracts for static polymorphism. No inheritance, no hidden dispatch.</p>
1187
+ <p>Explicit nominal method-set contracts for static polymorphism. No inheritance or hidden dispatch; runtime polymorphism is explicit through <code>dyn[I]</code>.</p>
1188
1188
  <div class="code-wrap">
1189
1189
  <button class="copy-btn" onclick="copyCode(this)">Copy</button>
1190
1190
  <pre><code>public interface Damageable:
@@ -1304,7 +1304,7 @@ let handle = null[ptr[char]]</code></pre>
1304
1304
  </div>
1305
1305
 
1306
1306
  <h3>Memory Model</h3>
1307
- <p>Milk Tea follows <strong>value semantics by default</strong>. Scalars, structs, and fixed arrays copy by value. There is no hidden heap boxing, no garbage collector, and no hidden reference counting. Heap allocation is always explicit and allocator-driven:</p>
1307
+ <p>Milk Tea follows <strong>value semantics by default</strong>. Scalars, structs, and fixed arrays copy by value. Ordinary values do not carry hidden heap boxes or reference counts; captured <code>proc</code> values may allocate ref-counted closure environments as part of their lifecycle. User-owned heap allocation remains explicit and allocator-driven:</p>
1308
1308
  <div class="table-wrap">
1309
1309
  <table class="attr-table">
1310
1310
  <tr><th>Module</th><th>Use Case</th></tr>
@@ -1450,8 +1450,8 @@ for item in items:
1450
1450
  process(item)</code></pre>
1451
1451
  </div>
1452
1452
 
1453
- <h3>for (Parallel)</h3>
1454
- <p>Arrays and spans only. Lengths must match.</p>
1453
+ <h3>for (Lockstep)</h3>
1454
+ <p>This is the lockstep multi-iterable form, distinct from <code>parallel for</code>. Arrays and spans only; lengths must match.</p>
1455
1455
  <div class="code-wrap">
1456
1456
  <button class="copy-btn" onclick="copyCode(this)">Copy</button>
1457
1457
  <pre><code>for entity, position, velocity in entities, positions, velocities:
@@ -1598,11 +1598,11 @@ match record:
1598
1598
  <button class="copy-btn" onclick="copyCode(this)">Copy</button>
1599
1599
  <pre><code>function main() -> int:
1600
1600
  rl.init_window(800, 600, "Game")
1601
- defer rl.close_window()
1601
+ defer: rl.close_window()
1602
1602
 
1603
1603
  while not rl.window_should_close():
1604
1604
  rl.begin_drawing()
1605
- defer rl.end_drawing()
1605
+ defer: rl.end_drawing()
1606
1606
  render_frame()
1607
1607
 
1608
1608
  return 0</code></pre>
@@ -1628,7 +1628,7 @@ unsafe:
1628
1628
  <h2>Compile-Time Control Flow</h2>
1629
1629
 
1630
1630
  <h3>when</h3>
1631
- <p>Evaluates discriminant at compile time; only the chosen branch is type-checked and emitted.</p>
1631
+ <p>Evaluates discriminant at compile time; only the chosen branch is type-checked and emitted. <code>when</code> may also appear at module level to conditionally include declarations, imports, or type definitions.</p>
1632
1632
  <div class="code-wrap">
1633
1633
  <button class="copy-btn" onclick="copyCode(this)">Copy</button>
1634
1634
  <pre><code>when TARGET_OS:
@@ -2012,7 +2012,7 @@ let r = aio.result(task)</code></pre>
2012
2012
  <!-- ────────────────────────────────────────────────────────────────────────── -->
2013
2013
  <section id="concurrency">
2014
2014
  <h2>Concurrency</h2>
2015
- <p>Milk Tea has first-class compiler support for multithreading using real OS threads (libuv). All concurrency is structured: spawned work completes before the parent scope continues. No hidden thread pool, no GC interaction, no fire-and-forget.</p>
2015
+ <p>Milk Tea has first-class compiler support for multithreading using real OS threads (libuv). <code>parallel for</code> and <code>parallel:</code> are structured barriers; <code>detach</code> continues asynchronously until an explicit <code>gather</code>. There is no GC interaction or implicit fire-and-forget cleanup.</p>
2016
2016
 
2017
2017
  <h3>Parallel For (data-parallel)</h3>
2018
2018
  <p>Dispatches loop iterations across CPU cores. Each iteration writes to its own index range &mdash; the bread and butter of data-oriented game programming.</p>
@@ -2149,8 +2149,8 @@ function attach(window: ref[Window]) -> Result[void, EventError]:
2149
2149
  let closed_sub = window.closed.subscribe(on_close)?
2150
2150
  let resized_sub = window.resized.subscribe(on_resize)?
2151
2151
 
2152
- defer window.closed.unsubscribe(closed_sub)
2153
- defer window.resized.unsubscribe(resized_sub)
2152
+ defer: window.closed.unsubscribe(closed_sub)
2153
+ defer: window.resized.unsubscribe(resized_sub)
2154
2154
 
2155
2155
  return Result[void, EventError].success()</code></pre>
2156
2156
  </div>
@@ -2245,7 +2245,7 @@ function attach(window: ref[Window]) -> Result[void, EventError]:
2245
2245
  <tr><td><code>mtc lower &lt;path&gt;</code></td><td>Print lowered IR</td></tr>
2246
2246
  <tr><td><code>mtc debug &lt;file.mt&gt;</code></td><td>Print debug info (tokens, AST, facts, bindings, diagnostics)</td></tr>
2247
2247
  <tr><td><code>mtc emit-c &lt;path&gt;</code></td><td>Emit generated C to stdout</td></tr>
2248
- <tr><td><code>mtc format &lt;path&gt;</code></td><td>Format sources in place (<code>--check</code> for dry-run)</td></tr>
2248
+ <tr><td><code>mtc format &lt;path&gt;</code></td><td>Format source to stdout (<code>--write</code> rewrites in place, <code>--check</code> verifies)</td></tr>
2249
2249
  <tr><td><code>mtc lint &lt;path&gt;</code></td><td>Run linter (<code>--fix</code>, <code>--select</code>, <code>--ignore</code>)</td></tr>
2250
2250
  <tr><td><code>mtc test &lt;path&gt;</code></td><td>Discover and run <code>@[test]</code> functions</td></tr>
2251
2251
  <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>
@@ -2416,9 +2416,17 @@ import std.math # intentionally available for downstream</code></pre>
2416
2416
 
2417
2417
  <h3>Event Restrictions</h3>
2418
2418
  <ul style="margin-left:1.25rem;margin-bottom:1rem">
2419
- <li>Event payload cannot be <code>ref[T]</code></li>
2420
- <li><code>emit</code> is only callable from within the declaring module</li>
2421
- <li>Events not allowed in external files</li>
2419
+ <li>Event payload types must be storable and cannot be event storage types</li>
2420
+ <li>Event payload cannot be <code>ref[T]</code></li>
2421
+ <li>Event storage types cannot be returned, passed through ordinary value parameters, or copied into locals</li>
2422
+ <li><code>emit</code> is only callable from within the declaring module</li>
2423
+ <li>Events not allowed in external files</li>
2424
+ </ul>
2425
+
2426
+ <h3>Interface Restrictions</h3>
2427
+ <ul style="margin-left:1.25rem;margin-bottom:1rem">
2428
+ <li>Interface methods cannot be <code>async</code> or declare their own type parameters</li>
2429
+ <li>Interface conformance must be declared on the nominal type, not an <code>extending</code> block</li>
2422
2430
  </ul>
2423
2431
  </section>
2424
2432
 
@@ -16,7 +16,7 @@ The output target is beautiful C. The generated C should be readable enough that
16
16
  2. Stay statically typed. Public APIs must be explicit and unsurprising.
17
17
  3. Feel good for game programming. Tight loops, structs, arrays, arenas, handles, and FFI should be first-class.
18
18
  4. Interoperate with C as a native citizen. C libraries are not a side feature; they are part of the core story.
19
- 5. Generate C that mirrors the source closely. No hidden runtime, hidden heap traffic, or hidden dispatch.
19
+ 5. Generate C that mirrors the source closely. Ordinary value operations do not add hidden heap traffic or dispatch; explicit runtime surfaces such as `dyn` and captured `proc` environments remain visible in the language model.
20
20
 
21
21
  ## Non-goals
22
22
 
@@ -28,7 +28,7 @@ The output target is beautiful C. The generated C should be readable enough that
28
28
  - No macro system that rewrites arbitrary ASTs
29
29
  - No garbage collector
30
30
  - No implicit conversions between unrelated primitive types
31
- - No user-invisible allocation for strings, collections, closures, or method calls. If a surface allocates owned text or storage, the allocating surface must be spelled in source.
31
+ - No user-invisible allocation for strings, collections, ordinary values, or method calls. Capturing a `proc` may allocate a ref-counted closure environment as part of proc value semantics; owned text and other storage use explicit allocating surfaces.
32
32
 
33
33
  ## Design rules
34
34
 
@@ -119,7 +119,7 @@ extending Player:
119
119
 
120
120
  function main() -> int:
121
121
  rl.init_window(screen_width, screen_height, "Milk Tea")
122
- defer rl.close_window()
122
+ defer: rl.close_window()
123
123
 
124
124
  var player = Player(
125
125
  position = rl.Vector2(x = 400.0, y = 300.0),
@@ -132,7 +132,7 @@ function main() -> int:
132
132
  player.update(dt)
133
133
 
134
134
  rl.begin_drawing()
135
- defer rl.end_drawing()
135
+ defer: rl.end_drawing()
136
136
 
137
137
  rl.clear_background(rl.BLACK)
138
138
  rl.draw_circle_v(player.position, player.radius, rl.GOLD)
@@ -291,7 +291,7 @@ The rule is fixed and inspectable. There is no method lookup at runtime.
291
291
  ### Interfaces
292
292
 
293
293
  Interfaces are explicit nominal method-set contracts for static polymorphism.
294
- They do not introduce inheritance, hidden dispatch, or a second object model.
294
+ They do not introduce inheritance, hidden dispatch, or a second object model. Runtime polymorphism is an explicit `dyn[Interface]` surface.
295
295
 
296
296
  ```mt
297
297
  interface Damageable:
@@ -363,7 +363,7 @@ Because fixed arrays copy by value, mutating interface-constrained collection co
363
363
 
364
364
  Iteration stays structural in v1 rather than going through a nominal `Iterator[T]` or `Iterable[T]` interface. Arrays, spans, and ranges keep their built-in behavior, and custom iterables participate by exposing the same method shape the compiler already recognizes: a non-editable `iter()` method on the iterable, then either `next() ->` nullable pointer-like item or `next() -> bool` together with `current()` on the iterator.
365
365
 
366
- This is deliberate. Interfaces are compile-time-only nominal contracts and are not generic today, so introducing a central iterator interface hierarchy now would either erase the item type or duplicate type-specific interfaces. The standard library should instead standardize on one iterator convention:
366
+ This is deliberate. Interfaces are nominal contracts, and generic interfaces are supported, but introducing a central iterator interface hierarchy would still either erase the item type or duplicate type-specific interfaces. The standard library should instead standardize on one iterator convention:
367
367
 
368
368
  - `collection.iter()` is the canonical traversal surface.
369
369
  - Alternate traversals use explicit view methods such as `map.keys()`, `map.values()`, and `map.entries()`.
@@ -423,16 +423,12 @@ Milk Tea should include a small number of control-flow features that materially
423
423
 
424
424
  #### `defer`
425
425
 
426
- `defer` registers cleanup code at scope exit and lowers to obvious cleanup labels in C.
426
+ `defer` registers cleanup code at scope exit and lowers to obvious cleanup labels in C. Both forms use the block-header `:`:
427
427
 
428
428
  ```mt
429
-
430
-
431
429
  function load_texture(path: str) -> Result[Texture, LoadError]:
432
430
  let texture = rl.load_texture(path)
433
-
434
- if texture.id == 0:
435
- return Result[Texture, LoadError].failure(error= LoadError.file_not_found)
431
+ defer: rl.unload_texture(texture)
436
432
 
437
433
  return Result[Texture, LoadError].success(value= texture)
438
434
  ```
@@ -484,7 +480,7 @@ The default async model is a language-integrated entry boundary. `std.async` rem
484
480
 
485
481
  #### Concurrency: `parallel for` and `parallel:` blocks
486
482
 
487
- Milk Tea has first-class compiler support for multithreading. The threading model is structured: all spawned work completes before the parent scope continues. There is no hidden thread pool, no garbage collector interaction, and no fire-and-forget. Every thread boundary is visible in source.
483
+ Milk Tea has first-class compiler support for multithreading. The compiler-integrated `parallel for` and `parallel:` forms use structured barriers; explicit `detach` work continues until a matching `gather`. There is no hidden thread pool, no garbage collector interaction, and no implicit fire-and-forget cleanup. Every thread boundary is visible in source.
488
484
 
489
485
  ```mt
490
486
  # Data-parallel: each iteration runs on a separate CPU core
@@ -794,7 +790,7 @@ unsafe:
794
790
  Binary arithmetic and numeric comparison operators may promote primitive operands to a common type locally.
795
791
  This is limited to `+ - * / % == != < <= > >=` and does not change assignment, return, or aggregate-field typing rules.
796
792
  Non-external call boundaries remain strict, but external calls may pass enum or flags values to same-width fixed-width integer parameters without an explicit cast for C ABI interop.
797
- Mixed signed and unsigned integers still require an explicit cast.
793
+ Mixed signed and unsigned integer operands use the same lossless rule as assignment: they promote to the narrowest signed type that holds both full ranges — a strictly-wider signed type covers an unsigned operand (`ubyte + int` is `int`, `uint + long` is `long`), and equal-width or narrower signed operands widen to the next signed width (`int + uint` is `long`, `byte + ubyte` is `short`). Mixing with a 64-bit unsigned type (`ulong`, `ptr_uint`) has no safe signed common type and still requires an explicit cast.
798
794
 
799
795
  `char` stays outside the general numeric-promotion rules. If code wants arithmetic on a character value, cast it to an integer type first. If code wants to write bytes back into a `char` buffer, either use `char<-...` explicitly or rely on the expected `char` boundary where a known `char` target is being initialized or assigned.
800
796
 
@@ -876,7 +872,7 @@ The memory model must feel like C with better defaults and cleaner surfaces.
876
872
  - Fixed arrays copy by value.
877
873
  - Returning a struct is allowed and lowers to normal C return or out-parameter lowering as needed.
878
874
 
879
- There is no hidden reference counting and no hidden heap boxing.
875
+ Ordinary values do not carry hidden reference counts or heap boxes. Captured `proc` values are the explicit callable-model exception: their environments retain and release nested captured procs so stored closures have a defined lifecycle.
880
876
 
881
877
  ### Explicit allocation
882
878
 
@@ -1100,7 +1096,7 @@ Rules:
1100
1096
  - one module identity per file, derived from its path
1101
1097
  - explicit imports only
1102
1098
  - no wildcard imports in v1
1103
- - no cyclic imports
1099
+ - cyclic imports are supported through forward-declaration bindings and two-pass checking
1104
1100
  - package naming stays filesystem-friendly
1105
1101
 
1106
1102
  Recommended layout:
@@ -1428,7 +1424,7 @@ Callbacks must map directly to C function pointers.
1428
1424
  type LogCallback = fn(level: int, message: cstr, user_data: ptr[void]) -> void
1429
1425
  ```
1430
1426
 
1431
- Capturing closures should not be lowered to hidden heap objects. If user state is needed, pass it explicitly as `user_data` at ABI boundaries or allocate explicit local storage such as `std.cell.alloc[T](...)` in ordinary Milk Tea code.
1427
+ Capturing closures should not be lowered to hidden heap objects. If user state is needed, pass it explicitly as `user_data` at ABI boundaries or allocate explicit local storage such as `std.box.alloc[T](...)` in ordinary Milk Tea code.
1432
1428
 
1433
1429
  ## Data layout and ABI controls
1434
1430
 
@@ -292,7 +292,7 @@ Callable and `ref[...]` rules:
292
292
  - External functions still cannot take `ref[...]` parameters, and ordinary functions still cannot return `ref[...]`.
293
293
  - `proc` captures are value captures. A captured local is not a mutable alias back to the outer binding. Any storable type may be captured, including scalars, arrays, structs, and other `proc` values. Captured `proc` values participate in the ref-counted lifecycle: the capturing proc retains the captured proc on creation and releases it when the env is freed.
294
294
  - `ref[T]` values are not capturable by design since they are non-owning.
295
- - Shared mutable proc state should use explicit storage such as `std.cell.alloc[T](...)` or other explicit pointer-backed state, not implicit mutable capture.
295
+ - Shared mutable proc state should use explicit storage such as `std.box.alloc[T](...)` or other explicit pointer-backed state, not implicit mutable capture.
296
296
 
297
297
  ### 3.4 Struct, union, enum, flags, opaque
298
298
 
@@ -596,6 +596,8 @@ if x > 10: return "big" else if x > 5: return "med" else: return "small"
596
596
 
597
597
  The inline form supports all statement kinds in the body (`return`, `let`, `var`, `defer`, assignment, expression statement, etc.). Blocks and inline form may be mixed: any branch may use either form independently.
598
598
 
599
+ Every block-taking statement follows the same rule: a `:` followed by a newline starts an indented block, while a `:` followed by anything else on the same line introduces exactly one inline statement. This applies uniformly to `if`/`else`, `while`, `for`, `defer`, and `unsafe` (§4.3, §4.4, §4.5). `match` arms are the exception: their inline form is a single value expression (§4.2).
600
+
599
601
  ### 4.2 Match
600
602
 
601
603
  Scrutinee types supported:
@@ -717,6 +719,13 @@ Single-form `for` supports:
717
719
  - `span[T]`
718
720
  - custom structural iterables with a non-editable zero-argument `iter()` method
719
721
 
722
+ Every loop body accepts either a `:`-newline-indented block or a single statement on the same line as the header:
723
+
724
+ ```mt
725
+ while ready: poll()
726
+ for value in values: accumulate(value)
727
+ ```
728
+
720
729
  Iterator protocol for custom structural iterables:
721
730
 
722
731
  - the iterable value must expose `iter()` with no parameters
@@ -795,10 +804,10 @@ Rules:
795
804
 
796
805
  ### 4.4 Defer
797
806
 
798
- Forms:
807
+ Both forms use the block-header `:` introduced in §4:
799
808
 
800
809
  ```mt
801
- defer cleanup()
810
+ defer: cleanup()
802
811
 
803
812
  # or
804
813
 
@@ -807,7 +816,10 @@ defer:
807
816
  release_b()
808
817
  ```
809
818
 
810
- `return` is not allowed inside defer blocks.
819
+ Rules:
820
+
821
+ - `defer` always requires `:`. A single statement may follow on the same line (`defer: release_a()`), or a `:`-newline-indented block may hold several statements.
822
+ - `return` is not allowed inside defer blocks.
811
823
 
812
824
  ### 4.5 Unsafe
813
825
 
@@ -1363,8 +1375,8 @@ function attach(window: ref[Window]) -> Result[void, EventError]:
1363
1375
  let closed_sub = window.closed.subscribe(on_close)?
1364
1376
  let resized_sub = window.resized.subscribe(on_resize)?
1365
1377
 
1366
- defer window.closed.unsubscribe(closed_sub)
1367
- defer window.resized.unsubscribe(resized_sub)
1378
+ defer: window.closed.unsubscribe(closed_sub)
1379
+ defer: window.resized.unsubscribe(resized_sub)
1368
1380
 
1369
1381
  return Result[void, EventError].success()
1370
1382
  ```
@@ -1508,7 +1520,7 @@ The compiler intentionally rejects the following patterns. These are design cons
1508
1520
  ### 13.6 Type system restrictions
1509
1521
 
1510
1522
  - conditions must be `bool`; integers and pointers have no implicit truthy or falsy coercion
1511
- - mixed signed and unsigned integer arithmetic requires an explicit cast
1523
+ - mixed signed and unsigned integer arithmetic promotes to the narrowest signed type that holds both ranges (`ubyte + int` is `int`, `int + uint` is `long`, `byte + ubyte` is `short`); mixing with a 64-bit unsigned type (`ulong`, `ptr_uint`) still requires an explicit cast
1512
1524
  - enum and flags values do not implicitly coerce to their backing integer types outside external-call boundaries
1513
1525
  - `enum` backing types must be integer primitives; flags members must be compile-time integer constants
1514
1526
  - variant arm payloads cannot use `ref[T]` in v1
data/lib/milk_tea/base.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  require "pathname"
4
4
 
5
5
  module MilkTea
6
- VERSION = "0.3.13"
6
+ VERSION = "0.3.16"
7
7
 
8
8
  def self.root
9
9
  @root ||= Pathname.new(File.expand_path("../..", __dir__))
@@ -212,8 +212,8 @@ module MilkTea
212
212
  ReturnStmt = Data.define(:value, :line, :column, :length) do
213
213
  def initialize(value:, line: nil, column: nil, length: nil) = super
214
214
  end
215
- DeferStmt = Data.define(:expression, :body, :line, :column, :length) do
216
- def initialize(expression:, body:, line: nil, column: nil, length: nil) = super
215
+ DeferStmt = Data.define(:body, :line, :column, :length) do
216
+ def initialize(body:, line: nil, column: nil, length: nil) = super
217
217
  end
218
218
  ErrorBlockStmt = Data.define(:body, :line, :column, :length, :message, :header_type, :header_expression, :header_bindings, :header_iterables) do
219
219
  def initialize(body:, line: nil, column: nil, length: nil, message: nil, header_type: nil, header_expression: nil, header_bindings: nil, header_iterables: nil) = super
@@ -212,8 +212,8 @@ module MilkTea
212
212
  when AST::UnsafeStmt
213
213
  build_block(stmt.body, next_id, break_target:, continue_target:)
214
214
  when AST::DeferStmt
215
- body_entry = stmt.body ? build_block(stmt.body, next_id, break_target:, continue_target:) : next_id
216
- expression_reads, expression_reads_info = stmt.expression ? read_identifiers_with_sites(stmt.expression) : [Set.new, []]
215
+ body_entry = build_block(stmt.body, next_id, break_target:, continue_target:)
216
+ expression_reads, expression_reads_info = statement_list_reads_with_sites(stmt.body)
217
217
  defer_id = @graph.add_node(
218
218
  kind: :defer,
219
219
  statement: stmt,
@@ -315,6 +315,66 @@ module MilkTea
315
315
  [reads, reads_info]
316
316
  end
317
317
 
318
+ def statement_list_reads_with_sites(statements)
319
+ reads = Set.new
320
+ reads_info = []
321
+ statement_list_read_identifiers(statements, reads, reads_info)
322
+ [reads, reads_info]
323
+ end
324
+
325
+ def statement_list_read_identifiers(statements, reads, reads_info)
326
+ Array(statements).each do |statement|
327
+ statement_read_identifiers(statement, reads, reads_info)
328
+ end
329
+ end
330
+
331
+ def statement_read_identifiers(statement, reads, reads_info)
332
+ case statement
333
+ when AST::ExpressionStmt
334
+ read_identifiers(statement.expression, reads, reads_info)
335
+ when AST::DeferStmt
336
+ statement_list_read_identifiers(statement.body, reads, reads_info)
337
+ when AST::LocalDecl
338
+ read_identifiers(statement.value, reads, reads_info)
339
+ statement_list_read_identifiers(statement.else_body, reads, reads_info)
340
+ when AST::Assignment
341
+ read_identifiers(statement.target, reads, reads_info)
342
+ read_identifiers(statement.value, reads, reads_info)
343
+ when AST::ReturnStmt
344
+ read_identifiers(statement.value, reads, reads_info)
345
+ when AST::IfStmt
346
+ statement.branches.each do |branch|
347
+ read_identifiers(branch.condition, reads, reads_info)
348
+ statement_list_read_identifiers(branch.body, reads, reads_info)
349
+ end
350
+ statement_list_read_identifiers(statement.else_body, reads, reads_info)
351
+ when AST::WhileStmt
352
+ read_identifiers(statement.condition, reads, reads_info)
353
+ statement_list_read_identifiers(statement.body, reads, reads_info)
354
+ when AST::ForStmt
355
+ statement.iterables.each { |iterable| read_identifiers(iterable, reads, reads_info) }
356
+ statement_list_read_identifiers(statement.body, reads, reads_info)
357
+ when AST::UnsafeStmt
358
+ statement_list_read_identifiers(statement.body, reads, reads_info)
359
+ when AST::MatchStmt
360
+ read_identifiers(statement.expression, reads, reads_info)
361
+ statement.arms.each do |arm|
362
+ read_identifiers(arm.pattern, reads, reads_info)
363
+ statement_list_read_identifiers(arm.body, reads, reads_info)
364
+ end
365
+ when AST::StaticAssert
366
+ read_identifiers(statement.condition, reads, reads_info)
367
+ read_identifiers(statement.message, reads, reads_info)
368
+ when AST::WhenStmt
369
+ read_identifiers(statement.discriminant, reads, reads_info)
370
+ statement.branches.each do |branch|
371
+ read_identifiers(branch.pattern, reads, reads_info)
372
+ statement_list_read_identifiers(branch.body, reads, reads_info)
373
+ end
374
+ statement_list_read_identifiers(statement.else_body, reads, reads_info)
375
+ end
376
+ end
377
+
318
378
  def read_identifiers(expression, names = Set.new, reads_info = [])
319
379
  case expression
320
380
  when nil
@@ -145,15 +145,9 @@ module MilkTea
145
145
  when AST::UnsafeStmt
146
146
  await_counter = analyze_async_statements!(statement.body, await_counter, env, param_fields, local_fields, await_fields)
147
147
  when AST::DeferStmt
148
- if statement.body
149
- cleanup_env = duplicate_env(env)
150
- cleanup_env[:return_context] = cleanup_env[:return_context]&.merge(allow_return: false)
151
- await_counter = analyze_async_statements!(statement.body, await_counter, cleanup_env, param_fields, local_fields, await_fields)
152
- end
153
- if statement.expression.is_a?(AST::AwaitExpr)
154
- await_fields[statement.expression.object_id] = build_async_await_field_info(statement.expression, await_counter, env:, param_fields:, local_fields:)
155
- await_counter += 1
156
- end
148
+ cleanup_env = duplicate_env(env)
149
+ cleanup_env[:return_context] = cleanup_env[:return_context]&.merge(allow_return: false)
150
+ await_counter = analyze_async_statements!(statement.body, await_counter, cleanup_env, param_fields, local_fields, await_fields)
157
151
  else
158
152
  nil
159
153
  end
@@ -194,7 +194,7 @@ module MilkTea
194
194
  when AST::UnsafeStmt
195
195
  statements_contain_await?(s.body, async_info)
196
196
  when AST::DeferStmt
197
- (s.body && statements_contain_await?(s.body, async_info)) || (s.expression && async_expression_contains_await?(s.expression))
197
+ statements_contain_await?(s.body, async_info)
198
198
  else
199
199
  false
200
200
  end
@@ -1237,15 +1237,7 @@ module MilkTea
1237
1237
  end
1238
1238
 
1239
1239
  def lower_async_defer_cleanup(statement, env:, async_info:)
1240
- body = if statement.body
1241
- statement.body
1242
- elsif statement.expression
1243
- [AST::ExpressionStmt.new(expression: statement.expression, line: statement.line)]
1244
- else
1245
- []
1246
- end
1247
-
1248
- { body:, env: snapshot_env(env) }
1240
+ { body: statement.body, env: snapshot_env(env) }
1249
1241
  end
1250
1242
 
1251
1243
  def lower_async_cleanup_entries(local_defers, outer_defers, frame_expr:, raw_frame_expr:, async_info:)
@@ -154,13 +154,8 @@ module MilkTea
154
154
  when AST::DeferStmt
155
155
  cleanup_env = duplicate_env(env)
156
156
  cleanup_env[:return_context] = cleanup_env[:return_context]&.merge(allow_return: false)
157
- cleanup_body = if statement.body
158
- normalize_async_statements(statement.body, counter, cleanup_env, return_type:)
159
- else
160
- expression_setup, expression = normalize_async_expression(statement.expression, counter, env: cleanup_env)
161
- expression_setup + [AST::ExpressionStmt.new(expression:, line: statement.line)]
162
- end
163
- [AST::DeferStmt.new(expression: nil, body: cleanup_body, line: statement.line, column: statement.column, length: statement.length)]
157
+ cleanup_body = normalize_async_statements(statement.body, counter, cleanup_env, return_type:)
158
+ [AST::DeferStmt.new(body: cleanup_body, line: statement.line, column: statement.column, length: statement.length)]
164
159
  when AST::BreakStmt, AST::ContinueStmt, AST::StaticAssert, AST::PassStmt
165
160
  [statement]
166
161
  else
@@ -16,11 +16,7 @@ module MilkTea
16
16
  statements.each do |statement|
17
17
  case statement
18
18
  when AST::DeferStmt
19
- local_defers << if statement.body
20
- lower_defer_cleanup_body(statement.body, env: local_env, return_type:)
21
- else
22
- lower_defer_cleanup_expression(statement.expression, env: local_env)
23
- end
19
+ local_defers << lower_defer_cleanup_body(statement.body, env: local_env, return_type:)
24
20
  when AST::UnsafeStmt
25
21
  lower_block_unsafe_stmt(statement, lowered:, local_defers:, local_env:, active_defers:, return_type:, loop_flow:, allow_return:)
26
22
  when AST::LocalDecl