mt-lang 0.3.12 → 0.3.14

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: bc80c3a3240cbcf4770dd067aa33559e74e3f139322b8fdb757316f3c7faf8af
4
- data.tar.gz: a623e5ecaa9617aa161a83e96c4cb0e7a75b08855edae27fb56c8ddc47c7cd75
3
+ metadata.gz: 12f7a371c9cb4162851cefb9277292455904b6e0bedcba05312770f00d724481
4
+ data.tar.gz: 4d73b7c4ef5c45abc731edbfd6e465067230b6772669ad8952a75247cd10e2ad
5
5
  SHA512:
6
- metadata.gz: c90aa51a21411fdcfe612f2ded69afdde6e055a012a4e886f5aa9ac69110d39a8bf93a36dde0543d1375a8bfa75ffd2f287eb082833a28b42efe19ab45c62ae6
7
- data.tar.gz: a87ac20f1228f059e3508818be58e11e5491a90299e56d5ab569ab3ebf1ae3dc1f7ddff8761321754bd2bfc8c520dc9ac3876360714ced2baf6e06a82bbf635d
6
+ metadata.gz: d434a523bf7e5678512af400a5054765d62976778d27c1d647203fa03a39145509648d82d32d56d02218da1324d66717b1eb1da75543ea513e49fcc2e9fc5410
7
+ data.tar.gz: d2fa42eb284fd01df9492fa772d69c46853712862dd6f8d4f1f0f20aa8c40fc996abf821763175419cb58abb2ffe2da94a750e30b8c47d7b0c9ae9c31ec75f75
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
 
@@ -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
@@ -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:
@@ -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:
@@ -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>
@@ -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
 
@@ -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()`.
@@ -484,7 +484,7 @@ The default async model is a language-integrated entry boundary. `std.async` rem
484
484
 
485
485
  #### Concurrency: `parallel for` and `parallel:` blocks
486
486
 
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.
487
+ 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
488
 
489
489
  ```mt
490
490
  # Data-parallel: each iteration runs on a separate CPU core
@@ -794,7 +794,7 @@ unsafe:
794
794
  Binary arithmetic and numeric comparison operators may promote primitive operands to a common type locally.
795
795
  This is limited to `+ - * / % == != < <= > >=` and does not change assignment, return, or aggregate-field typing rules.
796
796
  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.
797
+ 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
798
 
799
799
  `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
800
 
@@ -876,7 +876,7 @@ The memory model must feel like C with better defaults and cleaner surfaces.
876
876
  - Fixed arrays copy by value.
877
877
  - Returning a struct is allowed and lowers to normal C return or out-parameter lowering as needed.
878
878
 
879
- There is no hidden reference counting and no hidden heap boxing.
879
+ 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
880
 
881
881
  ### Explicit allocation
882
882
 
@@ -1100,7 +1100,7 @@ Rules:
1100
1100
  - one module identity per file, derived from its path
1101
1101
  - explicit imports only
1102
1102
  - no wildcard imports in v1
1103
- - no cyclic imports
1103
+ - cyclic imports are supported through forward-declaration bindings and two-pass checking
1104
1104
  - package naming stays filesystem-friendly
1105
1105
 
1106
1106
  Recommended layout:
@@ -1428,7 +1428,7 @@ Callbacks must map directly to C function pointers.
1428
1428
  type LogCallback = fn(level: int, message: cstr, user_data: ptr[void]) -> void
1429
1429
  ```
1430
1430
 
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.
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.box.alloc[T](...)` in ordinary Milk Tea code.
1432
1432
 
1433
1433
  ## Data layout and ABI controls
1434
1434
 
@@ -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
 
@@ -1508,7 +1508,7 @@ The compiler intentionally rejects the following patterns. These are design cons
1508
1508
  ### 13.6 Type system restrictions
1509
1509
 
1510
1510
  - 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
1511
+ - 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
1512
  - enum and flags values do not implicitly coerce to their backing integer types outside external-call boundaries
1513
1513
  - `enum` backing types must be integer primitives; flags members must be compile-time integer constants
1514
1514
  - 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.12"
6
+ VERSION = "0.3.14"
7
7
 
8
8
  def self.root
9
9
  @root ||= Pathname.new(File.expand_path("../..", __dir__))
@@ -5,7 +5,7 @@ require "open3"
5
5
  require "tempfile"
6
6
  require_relative "../tooling/formatter"
7
7
  require_relative "../core/token"
8
- require_relative "../core/types/types"
8
+ require_relative "../core/types"
9
9
  require_relative "bindgen/ast_parser"
10
10
  require_relative "bindgen/declaration"
11
11
  require_relative "bindgen/type_mapper"
@@ -3,7 +3,7 @@
3
3
  require "json"
4
4
  require_relative "../tooling/formatter"
5
5
  require_relative "../core/token"
6
- require_relative "../core/types/types"
6
+ require_relative "../core/types"
7
7
 
8
8
  require_relative "imported_bindings/generator"
9
9
  require_relative "imported_bindings/method_source"
@@ -1506,10 +1506,54 @@ module MilkTea
1506
1506
  return false unless receiver_type.is_a?(Types::VariantArmPayload)
1507
1507
 
1508
1508
  outer = receiver_type.variant_type
1509
- return field_type == outer if outer.is_a?(Types::Variant)
1510
- return field_type == outer if outer.is_a?(Types::VariantInstance)
1509
+ return false unless outer.is_a?(Types::Variant) || outer.is_a?(Types::VariantInstance)
1511
1510
 
1512
- false
1511
+ # The C backend pointer-breaks every cyclic field, including cycles that
1512
+ # run through generic instantiations such as `Option[Atom]` inside `Atom`.
1513
+ # Deref the access whenever the field type can reach the outer variant.
1514
+ type_reaches_target?(field_type, outer)
1515
+ end
1516
+
1517
+ def type_reaches_target?(type, target)
1518
+ type_reaches_target_with_visited?(type, target, {})
1519
+ end
1520
+
1521
+ def type_reaches_target_with_visited?(type, target, visited)
1522
+ return true if type == target
1523
+ return false if visited[type.object_id]
1524
+
1525
+ visited[type.object_id] = true
1526
+ reachability_children(type).any? do |child|
1527
+ type_reaches_target_with_visited?(child, target, visited)
1528
+ end
1529
+ end
1530
+
1531
+ # Structural expansion used by cyclic-field reachability. Mirrors the
1532
+ # CBackend's `aggregate_type_dependencies` so the deref injected for a
1533
+ # pointer-broken cyclic field agrees with the emitted C struct layout.
1534
+ def reachability_children(type)
1535
+ case type
1536
+ when Types::Nullable
1537
+ [type.base]
1538
+ when Types::GenericInstance
1539
+ if pointer_type?(type)
1540
+ []
1541
+ elsif array_type?(type)
1542
+ [array_element_type(type)]
1543
+ else
1544
+ []
1545
+ end
1546
+ when Types::Span
1547
+ [type.element_type]
1548
+ when Types::Struct, Types::StructInstance, Types::Union
1549
+ type.fields.values
1550
+ when Types::Variant, Types::VariantInstance
1551
+ type.arm_names.flat_map { |name| type.arm(name).values }
1552
+ when Types::VariantArmPayload
1553
+ type.fields.values
1554
+ else
1555
+ []
1556
+ end
1513
1557
  end
1514
1558
 
1515
1559
  def lower_compile_time_handle_member(handle, member, type)
@@ -1137,9 +1137,33 @@ module MilkTea
1137
1137
  return unless left_type.is_a?(Types::Primitive) && right_type.is_a?(Types::Primitive)
1138
1138
  return unless left_type.integer? && right_type.integer?
1139
1139
  return unless left_type.fixed_width_integer? && right_type.fixed_width_integer?
1140
- return unless left_type.signed_integer? == right_type.signed_integer?
1141
1140
 
1142
- left_type.integer_width >= right_type.integer_width ? left_type : right_type
1141
+ # Mirrors the semantic analyzer's rule: same signedness picks the wider
1142
+ # type; mixed signed/unsigned promotes to the narrowest signed type that
1143
+ # holds both operands' full ranges (a strictly-wider signed type covers
1144
+ # an unsigned operand, otherwise widen to the next signed width). Mixing
1145
+ # with a 64-bit unsigned type has no safe signed common type.
1146
+ if left_type.signed_integer? == right_type.signed_integer?
1147
+ return left_type.integer_width >= right_type.integer_width ? left_type : right_type
1148
+ end
1149
+
1150
+ signed_type, unsigned_type = if left_type.signed_integer?
1151
+ [left_type, right_type]
1152
+ else
1153
+ [right_type, left_type]
1154
+ end
1155
+
1156
+ return signed_type if signed_type.integer_width > unsigned_type.integer_width
1157
+
1158
+ signed_type_above_width(unsigned_type.integer_width)
1159
+ end
1160
+
1161
+ def signed_type_above_width(width)
1162
+ case width
1163
+ when 8 then @ctx.types.fetch("short")
1164
+ when 16 then @ctx.types.fetch("int")
1165
+ when 32 then @ctx.types.fetch("long")
1166
+ end
1143
1167
  end
1144
1168
 
1145
1169
  def wider_float_type(left_type, right_type)
@@ -943,7 +943,12 @@ module MilkTea
943
943
  payload_type = Types::VariantArmPayload.new(storage_type, arm_name, storage_type.arm(arm_name))
944
944
  data_expr = IR::Member.new(receiver: storage_expr, member: "data", type: nil)
945
945
  arm_expr = IR::Member.new(receiver: data_expr, member: arm_name, type: payload_type)
946
- IR::Member.new(receiver: arm_expr, member: field_name, type: field_type)
946
+ member_expr = IR::Member.new(receiver: arm_expr, member: field_name, type: field_type)
947
+ if type_reaches_target?(field_type, payload_type.variant_type)
948
+ return IR::Unary.new(operator: "*", operand: member_expr, type: field_type)
949
+ end
950
+
951
+ member_expr
947
952
  end
948
953
 
949
954
  def infer_result_propagation_type(expression, env:)
@@ -187,9 +187,36 @@ module MilkTea
187
187
  return unless left_type.integer? && right_type.integer?
188
188
  return left_type if left_type == right_type
189
189
  return unless left_type.fixed_width_integer? && right_type.fixed_width_integer?
190
- return unless left_type.signed_integer? == right_type.signed_integer?
191
190
 
192
- left_type.integer_width >= right_type.integer_width ? left_type : right_type
191
+ # Same signedness: the wider type wins (both operands are losslessly
192
+ # assignable to it).
193
+ if left_type.signed_integer? == right_type.signed_integer?
194
+ return left_type.integer_width >= right_type.integer_width ? left_type : right_type
195
+ end
196
+
197
+ # Mixed signed/unsigned: promote to the narrowest signed type that holds
198
+ # both operands' full ranges, mirroring the lossless assignment rule. A
199
+ # strictly-wider signed type covers an unsigned operand; equal-width or
200
+ # wider unsigned operands widen to the next signed width. Mixing with a
201
+ # 64-bit unsigned type has no safe signed common type, so callers fall
202
+ # back to requiring an explicit cast.
203
+ signed_type, unsigned_type = if left_type.signed_integer?
204
+ [left_type, right_type]
205
+ else
206
+ [right_type, left_type]
207
+ end
208
+
209
+ return signed_type if signed_type.integer_width > unsigned_type.integer_width
210
+
211
+ signed_type_above_width(unsigned_type.integer_width)
212
+ end
213
+
214
+ def signed_type_above_width(width)
215
+ case width
216
+ when 8 then @ctx.types.fetch("short")
217
+ when 16 then @ctx.types.fetch("int")
218
+ when 32 then @ctx.types.fetch("long")
219
+ end
193
220
  end
194
221
 
195
222
  def wider_float_type(left_type, right_type)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fiddle"
4
- require_relative "types"
4
+ require_relative "../types"
5
5
 
6
6
  module MilkTea
7
7
  module Layout
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fiddle"
4
- require_relative "registry"
5
- require_relative "visitor"
4
+ require_relative "types/registry"
5
+ require_relative "types/visitor"
6
6
 
7
7
  module MilkTea
8
8
  module Types
@@ -850,9 +850,6 @@ module MilkTea
850
850
  end
851
851
 
852
852
  def field_c_name(name)
853
- stripped = name.delete_suffix("_")
854
- return stripped if stripped != name && MilkTea::KEYWORDS.key?(stripped)
855
-
856
853
  name
857
854
  end
858
855
 
@@ -961,9 +958,6 @@ module MilkTea
961
958
  end
962
959
 
963
960
  def field_c_name(name)
964
- stripped = name.delete_suffix("_")
965
- return stripped if stripped != name && MilkTea::KEYWORDS.key?(stripped)
966
-
967
961
  name
968
962
  end
969
963
 
data/lib/milk_tea/core.rb CHANGED
@@ -9,7 +9,7 @@ require_relative "core/lexer"
9
9
  require_relative "core/cst"
10
10
  require_relative "core/cst_builder"
11
11
  require_relative "core/ast"
12
- require_relative "core/types/types"
12
+ require_relative "core/types"
13
13
  require_relative "core/binding_types"
14
14
  require_relative "core/compile_time"
15
15
  require_relative "core/compatibility_helpers"
@@ -229,6 +229,7 @@ module MilkTea
229
229
 
230
230
  def lex_command
231
231
  path = nil
232
+ sexpr = false
232
233
 
233
234
  args = @argv.dup
234
235
  @argv = []
@@ -236,6 +237,11 @@ module MilkTea
236
237
  arg = args.shift
237
238
  next if arg == "--"
238
239
 
240
+ if arg == "--sexpr"
241
+ sexpr = true
242
+ next
243
+ end
244
+
239
245
  if path.nil?
240
246
  path = arg
241
247
  else
@@ -252,15 +258,24 @@ module MilkTea
252
258
  end
253
259
 
254
260
  tokens = Lexer.lex(read_source_file(path), path: path)
255
- @out.write(PP.pp(tokens, +""))
261
+ if sexpr
262
+ @out.puts(SexprDumper.dump_tokens(tokens))
263
+ else
264
+ @out.write(PP.pp(tokens, +""))
265
+ end
256
266
  0
257
267
  end
258
268
 
259
269
  def parse_command
270
+ sexpr = false
260
271
  args = @argv.dup
261
272
  @argv = []
262
273
  until args.empty?
263
274
  arg = args.shift
275
+ if arg == "--sexpr"
276
+ sexpr = true
277
+ next
278
+ end
264
279
  @argv << arg
265
280
  end
266
281
 
@@ -283,9 +298,13 @@ module MilkTea
283
298
  paths.each_with_index do |path, index|
284
299
  ast = make_module_loader(path, locked: resolution[:locked], platform: ModuleLoader.default_host_platform).load_file(path)
285
300
  if multiple
286
- @out.puts("# --- #{path} ---")
301
+ @out.puts("# --- #{path} ---") unless sexpr
302
+ end
303
+ if sexpr
304
+ @out.puts(SexprDumper.dump_ast(ast))
305
+ else
306
+ @out.write(PrettyPrinter.format_ast(ast))
287
307
  end
288
- @out.write(PrettyPrinter.format_ast(ast))
289
308
  @out.puts if multiple && index < paths.length - 1
290
309
  end
291
310
  0
@@ -764,10 +783,15 @@ module MilkTea
764
783
  end
765
784
 
766
785
  def lower_command
786
+ sexpr = false
767
787
  args = @argv.dup
768
788
  @argv = []
769
789
  until args.empty?
770
790
  arg = args.shift
791
+ if arg == "--sexpr"
792
+ sexpr = true
793
+ next
794
+ end
771
795
  @argv << arg
772
796
  end
773
797
 
@@ -790,10 +814,13 @@ module MilkTea
790
814
  paths.each_with_index do |path, index|
791
815
  program = make_module_loader(path, locked: resolution[:locked], platform: ModuleLoader.default_host_platform).check_program(path)
792
816
  if multiple
793
- @out.puts("# --- #{path} ---")
817
+ @out.puts("# --- #{path} ---") unless sexpr
818
+ end
819
+ if sexpr
820
+ @out.puts(SexprDumper.dump_ir(Lowering.lower(program)))
821
+ else
822
+ @out.write(PrettyPrinter.format_ir(Lowering.lower(program)))
794
823
  end
795
- @out.write(PrettyPrinter.format_ir(Lowering.lower(program)))
796
- @out.puts if multiple && index < paths.length - 1
797
824
  end
798
825
  0
799
826
  end