mt-lang 0.4.1 → 0.4.21

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: 7e72d5a921ff9b83cef5fb60e89672e913daa367f7e64b7fd000664664c4bb22
4
- data.tar.gz: 7876a95d45c0a11a801ea6ec3bef10adc448e69eacaccd5bc65df5c22e01786b
3
+ metadata.gz: 78cefa537b068c8eb6d28d4b9111448c60e837bcaaddc82400b3b7f00ca6bbb8
4
+ data.tar.gz: 3ceff08e45e74f22ef4fe2f3a7eee31dd11a7943e5f8596766e792741c366cdf
5
5
  SHA512:
6
- metadata.gz: 641eb7f5a45dfd4b737342ccb89ef2e58036ff6e3a889032664b0136ba6a8c632d45180f9a18ce56061c9278e2bd7e68ad37374cf40dc6da2d626940f8457d95
7
- data.tar.gz: 399e6943e1207afa3a14201e8a3f1984405810da46d245685b9b2a9b5fd74b92486f982377452f416caf5aa9d6878d6094ffb11c7382d35b37d11820c3df6a89
6
+ metadata.gz: 4b7fc7883e1fd86961012b35f7a91da1f996eaade75d2799863991d37acae57ab41360cc783ecbca5afc792b7a6455a25b638003ea0ffa9b456e7ece1a3395da
7
+ data.tar.gz: 259f4a718acace0f4c3ce07fcb01f458fc7bef4ea23798ac16458eb2ffc7b63688fa05fea53a553260d33bc9e031f768ada4b7f98effc803e5099f9c976a7d03
data/README.md CHANGED
@@ -20,7 +20,7 @@ It is the preferred first entry point for getting to know the language.
20
20
  If this file conflicts with `docs/language-manual.md`, the manual wins.
21
21
  `docs/language-design.md` is for design direction and rationale, not the authoritative implementation spec.
22
22
 
23
- Package manifests, build workflow, and run workflow are documented separately in `docs/build-guide.md`.
23
+ Package manifests, build workflow, and run workflow are documented separately in `docs/build-guide.md`. The `@[test]` attribute, assertion intrinsics, and `mtc test` runner are documented in `docs/testing.md`.
24
24
 
25
25
  ## 1. File Kinds And Layout
26
26
 
@@ -364,6 +364,8 @@ Method kinds:
364
364
  - `function` -> value receiver
365
365
  - `editable function` -> editable receiver
366
366
  - `static function` -> no receiver
367
+ - `const function` -> value receiver, compile-time-evaluable
368
+ - `static const function` -> no receiver, compile-time-evaluable
367
369
 
368
370
  Methods may appear inside a struct body (desugared to an `extending` block) or in a separate
369
371
  `extending` declaration:
@@ -424,6 +426,8 @@ const RESULT: int = square(5) # folded to 25 at compile time
424
426
 
425
427
  `const function` also generates a normal runtime function, callable from ordinary runtime code.
426
428
 
429
+ `const` applies to methods as well: `const function` (value receiver) and `static const function` (no receiver) methods fold when called from a compile-time context and also generate normal runtime functions. `editable const function`, `async const function`, and `const` on interface methods are rejected.
430
+
427
431
  External functions:
428
432
 
429
433
  ```mt
@@ -856,6 +860,10 @@ Generics:
856
860
  Special recognized callables:
857
861
 
858
862
  - `fatal(message)`
863
+ - `assert(condition, message?)` — runtime check; aborts when the `bool` condition is false
864
+ - `expect(condition, message?)` — same as `assert`, always-on test assertion
865
+ - `expect_eq(actual, expected, message?)` — compares with the language `==` operator (primitives, `str`, structs, variants, arrays) and aborts when the values differ
866
+ - `expect_ne(actual, expected, message?)` — compares with `!=` and aborts when the values are equal
859
867
  - `ref_of(x)`
860
868
  - `const_ptr_of(x)`
861
869
  - `read(r)`
@@ -873,6 +881,8 @@ Special recognized callables:
873
881
  - `get(coll, index)` — recoverable array/span indexing returning `ptr[T]?`; null on out‑of‑bounds instead of aborting
874
882
  - `adapt[I](value)` — constructs a `dyn[I]` runtime interface value; verifies `value`'s type implements interface `I` at compile time
875
883
 
884
+ `assert`, `expect`, `expect_eq`, and `expect_ne` are recognized in bare call position (not reserved words) and take an optional `str`/`cstr` message. When omitted, the compiler synthesizes a message carrying the source location; the message is only evaluated on the failing path. A literal-false `assert(false, ...)` / `expect(false, ...)` is treated as terminating control flow, so it satisfies the `else:` block of a `let ... else:` guard.
885
+
876
886
  Reference and pointer notes:
877
887
 
878
888
  - `read(ref_value)` explicitly projects the referent value. Use `read(handle) = value` to write through a bare `ref[T]` value.
data/docs/index.html CHANGED
@@ -556,6 +556,14 @@ function make_buffer[N: int]() -> str_buffer[N]:
556
556
  var buf: str_buffer[N]
557
557
  return buf
558
558
 
559
+ ## ── inline if with a compile-time type comparison ───────────────────
560
+ function size_label[T]() -> str:
561
+ inline if T == int:
562
+ return "32-bit"
563
+ inline if T == float:
564
+ return "float"
565
+ return "other"
566
+
559
567
  ## ── Error handling with Result[T, E] ────────────────────────────────
560
568
  enum LoadError: ubyte
561
569
  not_found = 1
@@ -575,6 +583,12 @@ function load_pair() -> Result[bool, LoadError]:
575
583
  io.print_line(f"Loaded #{first.name} and #{second.name}")
576
584
  return Result[bool, LoadError].success(value = true)
577
585
 
586
+ ## ── Foreign FFI: raw external + foreign projection ──────────────────
587
+ external function atoi(input: cstr) -> int
588
+
589
+ ## foreign function projects a raw ABI call into ordinary Milk Tea types
590
+ foreign function parse_int_foreign(input: str as cstr) -> int = atoi
591
+
578
592
  ## ── Events (fixed-capacity pub/sub, zero heap during dispatch) ──────
579
593
  struct DamageEvent:
580
594
  target_id: EntityId
@@ -601,6 +615,19 @@ async function load_scores() -> int:
601
615
  ## ── Compile-time constants for inline if ─────────────────────────────
602
616
  const DEBUG: bool = false
603
617
 
618
+ ## ── when: compile-time conditional (only the chosen branch is emitted) ──
619
+ enum Backend: ubyte
620
+ gl = 1
621
+ vulkan = 2
622
+ const TARGET_BACKEND: Backend = Backend.gl
623
+
624
+ function backend_label() -> str:
625
+ when TARGET_BACKEND:
626
+ Backend.gl:
627
+ return "OpenGL"
628
+ Backend.vulkan:
629
+ return "Vulkan"
630
+
604
631
  ## ── Main entry point ────────────────────────────────────────────────
605
632
  function main() -> int:
606
633
  ## Local declarations: let (immutable), var (mutable)
@@ -727,12 +754,22 @@ function main() -> int:
727
754
  Action.idle:
728
755
  pass
729
756
 
757
+ ## is: variant arm membership test
758
+ if action is Action.attack:
759
+ io.print_line("action is an attack")
760
+
730
761
  ## match expression
731
762
  let label = match player.level:
732
763
  1: "beginner"
733
764
  2: "novice"
734
765
  _: "advanced"
735
766
 
767
+ ## Struct equality: field-wise ==/!=
768
+ let stats_a = Entity.Stats(attack = 10, defense = 5, speed = 1.0)
769
+ let stats_b = Entity.Stats(attack = 10, defense = 5, speed = 1.0)
770
+ if stats_a == stats_b:
771
+ io.print_line("stats equal")
772
+
736
773
  ## Explicit cast: T<-value
737
774
  let raw_kind = ubyte<-player.entity.kind
738
775
  let ratio = float<-score
@@ -795,6 +832,12 @@ function main() -> int:
795
832
  inline if DEBUG:
796
833
  io.print_line("debug mode")
797
834
 
835
+ ## Foreign FFI, when, and inline-if type dispatch
836
+ let parsed_int = parse_int_foreign("42")
837
+ io.print_line(f"FFI parsed: #{parsed_int}")
838
+ io.print_line(f"Backend: #{backend_label()}")
839
+ io.print_line(f"int label: #{size_label[int]()}")
840
+
798
841
  ## ── Concurrency ─────────────────────────────────────────────────────
799
842
  ## parallel for: data-parallel loop dispatched across CPU cores
800
843
  var positions = array[float, 4](0.0, 1.0, 2.0, 3.0)
@@ -829,7 +872,7 @@ function main() -> int:
829
872
  </div>
830
873
 
831
874
  <div class="callout tip">
832
- <strong>Tip:</strong> This example covers imports, constants, <code>const function</code>, type aliases, enums, flags, structs (nested, attributed), variants, interfaces, <code>extending</code> methods, generics with constraints, value parameter generics, <code>Result</code> error handling with <code>?</code> and guard binding, events, closures (<code>proc</code>), <code>async</code>/<code>await</code>, concurrency (<code>parallel for</code>, <code>parallel:</code>, <code>detach</code> + <code>gather</code>, <code>atomic[T]</code>), native vector types, arrays, spans, collections, <code>for</code>/<code>while</code>/<code>match</code>/<code>if</code>, variant membership test (<code>is</code>), parallel <code>for</code>, <code>defer</code>, <code>unsafe</code>, <code>ref[T]</code>, <code>own[T]</code>, <code>dyn[Interface]</code>, format strings, heredocs, <code>str_buffer</code>, compile-time reflection, <code>inline for</code>/<code>inline if</code>, <code>static_assert</code>, and explicit casts. See the sections below for full details on each feature.
875
+ <strong>Tip:</strong> This example covers imports, constants, <code>const function</code>, type aliases, enums, flags, structs (nested, attributed), variants, interfaces, foreign functions (<code>external</code> / <code>foreign function</code>), <code>extending</code> methods, generics with constraints, value parameter generics, <code>Result</code> error handling with <code>?</code> and guard binding, events, closures (<code>proc</code>), <code>async</code>/<code>await</code>, concurrency (<code>parallel for</code>, <code>parallel:</code>, <code>detach</code> + <code>gather</code>, <code>atomic[T]</code>), native vector types, arrays, spans, collections, <code>for</code>/<code>while</code>/<code>match</code>/<code>if</code>, variant membership test (<code>is</code>), parallel <code>for</code>, <code>defer</code>, <code>unsafe</code>, <code>ref[T]</code>, <code>own[T]</code>, <code>dyn[Interface]</code>, format strings, heredocs, <code>str_buffer</code>, compile-time reflection, <code>when</code>, <code>inline for</code>/<code>inline if</code> (including type comparison), struct <code>==</code>/<code>!=</code>, <code>static_assert</code>, and explicit casts. See the sections below for full details on each feature.
833
876
  </div>
834
877
  </section>
835
878
 
@@ -1221,7 +1264,7 @@ struct NPC implements Damageable:
1221
1264
  </div>
1222
1265
 
1223
1266
  <h3>Methods (extending)</h3>
1224
- <p>Three kinds: <code>function</code> (value receiver), <code>editable function</code> (editable receiver), <code>static function</code> (no receiver).</p>
1267
+ <p>Method kinds: <code>function</code> (value receiver), <code>editable function</code> (editable receiver), <code>static function</code> (no receiver), plus compile-time-evaluable <code>const function</code> and <code>static const function</code>. <code>editable const function</code>, <code>async const function</code>, and <code>const</code> on interface methods are rejected.</p>
1225
1268
  <div class="code-wrap">
1226
1269
  <button class="copy-btn" onclick="copyCode(this)">Copy</button>
1227
1270
  <pre><code>extending Counter:
@@ -1394,6 +1437,7 @@ function say_hello(name: str):
1394
1437
 
1395
1438
  <h3>const function</h3>
1396
1439
  <p>Evaluable at compile time. Generates both a compile-time constant-folding path and a normal runtime function. Recursive calls between <code>const</code> functions are supported. Use <code>const function</code> for reusable compile-time logic; use a block-bodied <code>const X -&gt; T: ...</code> for one-shot computed constants.</p>
1440
+ <p><code>const</code> also applies to methods: <code>const function</code> (value receiver) and <code>static const function</code> (no receiver) fold when called from a compile-time context. <code>editable const function</code>, <code>async const function</code>, and <code>const</code> on interface methods are rejected.</p>
1397
1441
  <div class="code-wrap">
1398
1442
  <button class="copy-btn" onclick="copyCode(this)">Copy</button>
1399
1443
  <pre><code>const function square(x: int) -> int:
@@ -1656,6 +1700,31 @@ unsafe:
1656
1700
  <section id="compile-time">
1657
1701
  <h2>Compile-Time Control Flow</h2>
1658
1702
 
1703
+ <h3>Block-bodied <code>const</code> and <code>const function</code></h3>
1704
+ <p>Block-bodied <code>const</code> initializers and <code>const function</code> bodies are evaluated at compile time, and the result is folded into the emitted C. Supported surface: literals (character literals fold to their byte value), other <code>const</code> values, arithmetic, <code>str</code> concatenation, <code>if</code>/<code>while</code>/<code>for</code> with <code>break</code> and <code>continue</code>, <code>match</code> expressions, index and range access, struct member access, <code>let</code>/<code>var</code> with destructuring and <code>else:</code> / <code>else as error:</code> guards, assignment (including compound), numeric prefix casts, calls to other const functions and const methods, and the reflection builtins.</p>
1705
+ <div class="code-wrap">
1706
+ <button class="copy-btn" onclick="copyCode(this)">Copy</button>
1707
+ <pre><code>## str concatenation folds to a literal
1708
+ const GREET: str = "hello" + " " + "world"
1709
+
1710
+ ## match expressions fold
1711
+ const LABEL -&gt; str:
1712
+ return match 2:
1713
+ 1: "one"
1714
+ 2: "two"
1715
+ _: "other"
1716
+
1717
+ ## const methods fold too
1718
+ struct Rect:
1719
+ w: int
1720
+ h: int
1721
+ const function area() -&gt; int:
1722
+ return this.w * this.h
1723
+
1724
+ const R: Rect = Rect(w = 10, h = 20)
1725
+ const AREA: int = R.area() ## folded to 200 at compile time</code></pre>
1726
+ </div>
1727
+
1659
1728
  <h3>when</h3>
1660
1729
  <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>
1661
1730
  <div class="code-wrap">
@@ -1810,6 +1879,10 @@ let sized = name[32]</code></pre>
1810
1879
  <table class="attr-table">
1811
1880
  <tr><th>Callable</th><th>Description</th></tr>
1812
1881
  <tr><td><code>fatal(message)</code></td><td>Terminate with message</td></tr>
1882
+ <tr><td><code>assert(condition, message?)</code></td><td>Runtime check; aborts when the <code>bool</code> condition is false</td></tr>
1883
+ <tr><td><code>expect(condition, message?)</code></td><td>Always-on test assertion (same as <code>assert</code>)</td></tr>
1884
+ <tr><td><code>expect_eq(actual, expected, message?)</code></td><td>Compares with the language <code>==</code> (primitives, <code>str</code>, structs, variants, arrays); aborts when the values differ</td></tr>
1885
+ <tr><td><code>expect_ne(actual, expected, message?)</code></td><td>Compares with <code>!=</code>; aborts when the values are equal</td></tr>
1813
1886
  <tr><td><code>ref_of(x)</code></td><td>Writable safe reference to an addressable lvalue</td></tr>
1814
1887
  <tr><td><code>const_ptr_of(x)</code></td><td>Read-only raw pointer to an addressable lvalue</td></tr>
1815
1888
  <tr><td><code>ptr_of(x)</code></td><td>Writable raw pointer from a mutable addressable lvalue</td></tr>
@@ -2591,6 +2664,7 @@ const ATOMS = new Set(['true','false','null'])
2591
2664
  const BUILTINS = new Set([
2592
2665
  'ref_of','ptr_of','const_ptr_of','read','reinterpret','zero','default',
2593
2666
  'hash','equal','order','size_of','align_of','offset_of','get','adapt','fatal',
2667
+ 'assert','expect','expect_eq','expect_ne',
2594
2668
  'field_of','callable_of','attribute_of','has_attribute','attribute_arg',
2595
2669
  'fields_of','members_of','attributes_of',
2596
2670
  ])
@@ -79,7 +79,7 @@ The intended reductions are deliberate:
79
79
 
80
80
  If a new feature introduces a second ordinary way to express the same concept, the language should delete one of them instead of documenting both.
81
81
 
82
- The compile-time evaluation surface is described in [Compile-Time Evaluation](compile-time.md).
82
+ The compile-time evaluation surface is described in the [language manual](language-manual.md): block-bodied `const` and `const function` limits in §3.2 and §3.7a, and `when` plus the `inline` statements in §4.7–§4.11.
83
83
 
84
84
  ## Overall shape
85
85
 
@@ -716,7 +716,7 @@ type FileHandle = ptr[libc.FILE]
716
716
 
717
717
  ### Generics
718
718
 
719
- Generics are useful, but they must stay boring. The compile-time surface is documented in [Compile-Time Evaluation](compile-time.md); generic bodies participate in that surface by using `when`, `inline for`, `inline while`, `inline match`, `inline if`, `type`-returning functions, `const function`, and block-bodied `const` initializers (`const X -> T: ...`) at the lexical positions where the compile-time rules allow them.
719
+ Generics are useful, but they must stay boring. The compile-time surface is documented in the [language manual](language-manual.md); generic bodies participate in that surface by using `when`, `inline for`, `inline while`, `inline match`, `inline if`, `type`-returning functions, `const function`, and block-bodied `const` initializers (`const X -> T: ...`) at the lexical positions where the compile-time rules allow them.
720
720
 
721
721
  Allowed in v1:
722
722
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  This manual documents the Milk Tea language as implemented today in the lexer, parser, semantic checker, and compiler tests.
4
4
 
5
- Package manifests and build or run workflow are documented separately in `docs/build-guide.md`.
5
+ Package manifests and build or run workflow are documented separately in `docs/build-guide.md`. The `@[test]` attribute, assertion intrinsics, and `mtc test` runner are documented in `docs/testing.md`.
6
6
 
7
7
  ## 1. Source Files And Modules
8
8
 
@@ -208,7 +208,7 @@ const NEXT_POW2 -> int:
208
208
  return n
209
209
  ```
210
210
 
211
- The block body is evaluated at compile time. Allowed inside the block: literals, names of other `const` values, arithmetic, control flow (`if`/`else if`/`else`, `while`, `for`), `let` and `var` declarations, calls to other compile-time functions, and calls to whitelisted builtins (`size_of`, `align_of`, `offset_of`, `fields_of`, `members_of`, `attributes_of`).
211
+ The block body is evaluated at compile time. Allowed inside the block: literals (character literals fold to their byte value), names of other `const` values, arithmetic, `str` concatenation (`+`), control flow (`if`/`else if`/`else`, `while`, `for`, with `break` and `continue`), `match` expressions, index and range access (`arr[i]`, `s[i]`, `arr[start..stop]`, `s[start..stop]`), struct member access, `let` and `var` declarations (including tuple/struct destructuring and `else:` / `else as error:` guard forms), assignment (including compound assignment to block-local struct fields and array elements), numeric prefix casts (`T<-value`), calls to other compile-time functions and const methods, and calls to whitelisted builtins (`size_of`, `align_of`, `offset_of`, `fields_of`, `members_of`, `attributes_of`).
212
212
 
213
213
  Rules:
214
214
 
@@ -524,6 +524,8 @@ Kinds:
524
524
  - `function` (value receiver)
525
525
  - `editable function` (editable receiver)
526
526
  - `static function` (no receiver)
527
+ - `const function` (value receiver, compile-time-evaluable)
528
+ - `static const function` (no receiver, compile-time-evaluable)
527
529
 
528
530
  Names such as `init` and `default` are ordinary static functions. There is no constructor keyword or hidden initializer syntax.
529
531
 
@@ -531,6 +533,9 @@ Method capabilities:
531
533
 
532
534
  - async methods are supported
533
535
  - generic methods are supported
536
+ - `const function` and `static const function` methods follow the same compile-time body rules as a block-bodied `const` (§3.2). Called from a compile-time context, the call is constant-folded; they also generate normal runtime functions.
537
+ - `editable const function` and `async const function` are rejected.
538
+ - `const` is not allowed on interface methods.
534
539
 
535
540
  ### 3.7 Functions
536
541
 
@@ -591,10 +596,11 @@ const SQUARE_5: int = square(5) # folded to 25 at compile time
591
596
 
592
597
  Rules:
593
598
 
594
- - The body must be evaluable at compile time (literals, `const` values, arithmetic, `if`/`else`, `while`, `for`, `let`/`var`, calls to other `const` functions, and whitelisted builtins).
599
+ - The body follows the same compile-time rules as a block-bodied `const` (§3.2).
595
600
  - Generates a normal runtime function as well — callable from ordinary runtime code.
596
601
  - Called from `const` initializers, `when` discriminants, `inline for` bodies, and other compile-time contexts.
597
602
  - Recursive calls between `const` functions are supported.
603
+ - `const` applies to methods as well: `const function` (value receiver) and `static const function` (no receiver) methods fold when called from compile-time contexts (§3.6).
598
604
 
599
605
  ### 3.8 External functions
600
606
 
@@ -1219,6 +1225,10 @@ The call site specializes with a literal: `int_with_bits[64]`.
1219
1225
  Special recognized callables:
1220
1226
 
1221
1227
  - `fatal(message)`
1228
+ - `assert(condition, message?)` — runtime check; aborts with `message` when the `bool` condition is false
1229
+ - `expect(condition, message?)` — same as `assert`, always-on test assertion
1230
+ - `expect_eq(actual, expected, message?)` — compares with the language `==` operator (works for primitives, `str`, structs, variants, arrays) and aborts when the values differ
1231
+ - `expect_ne(actual, expected, message?)` — compares with `!=` and aborts when the values are equal
1222
1232
  - `ref_of(x)`
1223
1233
  - `const_ptr_of(x)`
1224
1234
  - `read(r)`
@@ -1236,6 +1246,8 @@ Special recognized callables:
1236
1246
  - `get(coll, index)` — recoverable array/span indexing returning `ptr[T]?`; null on out‑of‑bounds instead of aborting
1237
1247
  - `adapt[I](value)` — constructs a `dyn[I]` runtime interface value; verifies `value`'s type implements `I` at compile time
1238
1248
 
1249
+ `assert`, `expect`, `expect_eq`, and `expect_ne` are special recognized callables, not reserved words: they are recognized in bare call position and may be shadowed by user declarations like any ordinary name. Their message argument (when given) must be `str` or `cstr`; when omitted, the compiler synthesizes a message carrying the source location. The message is only evaluated on the failing path. A literal-false `assert(false, ...)` or `expect(false, ...)` is treated as terminating control flow (like `static_assert(false, ...)`), so it may be used where the checker requires guaranteed exit, such as the `else:` block of a `let ... else:` guard.
1250
+
1239
1251
  `default[T]` requires an accessible zero-argument associated function `T.default()` that returns `T`.
1240
1252
 
1241
1253
  `hash[T](value)` lowers to `T.hash(value: const_ptr[T]) -> uint`, `equal[T](left, right)` lowers to `T.equal(left: const_ptr[T], right: const_ptr[T]) -> bool`, and `order[T](left, right)` lowers to `T.order(left: const_ptr[T], right: const_ptr[T]) -> int`. Each argument must already be a `ref[T]`, `ptr[T]`, or `const_ptr[T]`, or be a safe stored `T` lvalue that can be borrowed implicitly.
data/docs/testing.md ADDED
@@ -0,0 +1,91 @@
1
+ # Milk Tea Testing Guide
2
+
3
+ This guide documents the in-language testing surface: the `assert`/`expect`/`expect_eq`/`expect_ne` intrinsics, the `@[test]` attribute, and the `mtc test` runner. The assertion intrinsics are part of the built-in callable surface (see `language-manual.md` §7); there is no standard-library testing module to import.
4
+
5
+ ## 1. Assertions
6
+
7
+ Four abort-based assertion intrinsics are available in every module, in both tests and ordinary code:
8
+
9
+ ```mt
10
+ assert(condition, message?) # runtime check
11
+ expect(condition, message?) # always-on test assertion
12
+ expect_eq(actual, expected, message?) # compares with the language `==`
13
+ expect_ne(actual, expected, message?) # compares with `!=`
14
+ ```
15
+
16
+ Rules:
17
+
18
+ - `condition` must be `bool`. `expect_eq`/`expect_ne` compare with the language `==`/`!=` operators, so they work for primitives, `str`, structs, variants, and arrays.
19
+ - `message` (when given) must be `str` or `cstr`; a `f"..."` format string flows straight in. When omitted, the compiler synthesizes a message carrying the source location.
20
+ - On failure the program aborts through the same path as `fatal`. The message is only evaluated on the failing path, so a `f"..."` message costs nothing while the assertion holds.
21
+ - `assert` defaults to always-on. `expect` is the test-flavored form and is always on.
22
+ - A literal-false `assert(false, ...)` or `expect(false, ...)` is treated as terminating control flow (like `static_assert(false, ...)`), so it satisfies contexts that require guaranteed exit, such as the `else:` block of a `let ... else:` guard.
23
+
24
+ ## 2. Tests
25
+
26
+ Tests are ordinary functions annotated with `@[test]`. They take no parameters and return `void`; a failing assertion aborts the test.
27
+
28
+ ```mt
29
+ function square(x: int) -> int:
30
+ return x * x
31
+
32
+ @[test]
33
+ function test_square() -> void:
34
+ expect_eq(square(3), 9)
35
+ expect(square(4) > 0, "square must be positive")
36
+ ```
37
+
38
+ Rules:
39
+
40
+ - A test file must not define `main`; `mtc test` synthesizes the entry point.
41
+ - A function annotated with `@[test] @[expect_fatal]` is a death test: it must abort (via `fatal`, a failed assertion, or a failed safety check) to pass.
42
+
43
+ ## 3. Running Tests
44
+
45
+ ```sh
46
+ mtc test PATH # run @[test] functions in one file
47
+ mtc test DIR # recursively run every test file under a directory
48
+ ```
49
+
50
+ `mtc test` builds one runner binary per test file and runs each test in its own process, so an aborting assertion is isolated to its own test and cannot suppress the results of its siblings. Passing tests print `ok - name`; failures print `FAIL - name: message`. The run is sandboxed with a wall-clock timeout (default 30s) and an address-space memory cap (default 1024 MB).
51
+
52
+ Options:
53
+
54
+ - `--timeout SECONDS` — per-test wall-clock timeout
55
+ - `--mem MB` — per-test address-space memory cap
56
+ - `--jobs N` — build and run N files in parallel (output stays in file order)
57
+ - `-n SUBSTRING` — run only tests whose name contains SUBSTRING
58
+ - `--format tap|junit` — machine-readable results for CI
59
+ - `--sanitize` — build with AddressSanitizer/UBSan; any sanitizer error fails the run
60
+ - `--profile debug|release`, `--platform linux|windows|wasm`, `--cc COMPILER` — build controls, same as `mtc build`
61
+
62
+ A file containing `# expect-error: <text>` is a compile-fail fixture: `mtc test` requires the compiler to reject it with a diagnostic containing that text, instead of running it.
63
+
64
+ ## 4. Example
65
+
66
+ ```mt
67
+ # tests/vec_test.mt
68
+ import std.vec as vec
69
+
70
+ @[test]
71
+ function test_vec_push_and_len() -> void:
72
+ var values = vec.Vec[int].create()
73
+ defer: values.release()
74
+ values.push(1)
75
+ values.push(2)
76
+ expect_eq(values.len(), 2)
77
+
78
+ @[test]
79
+ @[expect_fatal]
80
+ function test_vec_oob_aborts() -> void:
81
+ var values = vec.Vec[int].create()
82
+ defer: values.release()
83
+ values.push(1)
84
+ values.at(5)
85
+ ```
86
+
87
+ ```sh
88
+ mtc test tests
89
+ ```
90
+
91
+ See the `mtc test --help` text for the current full option list.
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.1"
6
+ VERSION = "0.4.21"
7
7
 
8
8
  def self.root
9
9
  @root ||= Pathname.new(File.expand_path("../..", __dir__))
@@ -254,7 +254,16 @@ module MilkTea
254
254
  end
255
255
 
256
256
  def fatal_expression?(expression)
257
- expression.is_a?(AST::Call) && expression.callee.is_a?(AST::Identifier) && expression.callee.name == "fatal"
257
+ return true if expression.is_a?(AST::Call) && expression.callee.is_a?(AST::Identifier) && expression.callee.name == "fatal"
258
+
259
+ # A literal-false `assert`/`expect` always aborts, so it terminates the
260
+ # current control flow like a `fatal` call (matching static_assert(false)).
261
+ expression.is_a?(AST::Call) &&
262
+ expression.callee.is_a?(AST::Identifier) &&
263
+ %w[assert expect].include?(expression.callee.name) &&
264
+ (first_arg = expression.arguments.first) &&
265
+ first_arg.value.is_a?(AST::BooleanLiteral) &&
266
+ first_arg.value.value == false
258
267
  end
259
268
 
260
269
  def assignment_target_reads(target, operator)
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ module Lowering
5
+ module Assertions
6
+ ASSERT_CALL_NAMES = %w[assert expect expect_eq expect_ne].freeze
7
+
8
+ # Lowers an `assert`/`expect`/`expect_eq`/`expect_ne` expression statement
9
+ # into an `IR::IfStmt` whose then-branch aborts via `fatal`. The message is
10
+ # only evaluated on the failing path so unused messages cost nothing.
11
+ # Returns `nil` when the statement is not one of the assertion calls so the
12
+ # caller can fall through to ordinary expression-statement lowering.
13
+ def lower_assert_like_statement(statement, env:)
14
+ kind = assert_like_call_kind(statement.expression)
15
+ return nil unless kind
16
+
17
+ arguments = statement.expression.arguments
18
+ line = statement.expression.line
19
+ column = statement.expression.column
20
+ default_message = default_assert_message(kind, statement.line)
21
+
22
+ condition_ast = nil
23
+ message = nil
24
+ case kind
25
+ when "assert", "expect"
26
+ condition_ast = unary_expression("not", arguments.fetch(0).value, line:, column:)
27
+ message = arguments.length > 1 ? arguments.fetch(1).value : string_literal(default_message, line:, column:)
28
+ when "expect_eq"
29
+ condition_ast = binary_expression("!=", arguments.fetch(0).value, arguments.fetch(1).value, line:, column:)
30
+ message = arguments.length > 2 ? arguments.fetch(2).value : string_literal(default_message, line:, column:)
31
+ when "expect_ne"
32
+ condition_ast = binary_expression("==", arguments.fetch(0).value, arguments.fetch(1).value, line:, column:)
33
+ message = arguments.length > 2 ? arguments.fetch(2).value : string_literal(default_message, line:, column:)
34
+ end
35
+
36
+ # Hoist inline-proc/foreign-temporary setup out of the condition so the
37
+ # failure check itself stays a plain `if (!cond)` in C.
38
+ setup, prepared_condition, cleanups = prepare_expression_with_cleanups(
39
+ condition_ast,
40
+ env:,
41
+ expected_type: @ctx.types.fetch("bool"),
42
+ )
43
+
44
+ [
45
+ *setup,
46
+ IR::IfStmt.new(
47
+ condition: lower_expression(prepared_condition, env:, expected_type: @ctx.types.fetch("bool")),
48
+ then_body: [lower_fatal_expression_statement(message, line:, column:, env:)],
49
+ else_body: [],
50
+ ),
51
+ *cleanups.flat_map(&:itself),
52
+ ]
53
+ end
54
+
55
+ def assert_like_call_kind(expression)
56
+ return nil unless expression.is_a?(AST::Call)
57
+ return nil unless expression.callee.is_a?(AST::Identifier)
58
+ return nil unless ASSERT_CALL_NAMES.include?(expression.callee.name)
59
+
60
+ expression.callee.name
61
+ end
62
+
63
+ def default_assert_message(kind, line)
64
+ path = @ctx.current_analysis_path.to_s
65
+ case kind
66
+ when "assert" then "assertion failed at #{path}:#{line}"
67
+ when "expect" then "expectation failed at #{path}:#{line}"
68
+ when "expect_eq" then "expect_eq failed: values are not equal at #{path}:#{line}"
69
+ when "expect_ne" then "expect_ne failed: values are equal at #{path}:#{line}"
70
+ end
71
+ end
72
+
73
+ def lower_fatal_expression_statement(message, line:, column:, env:)
74
+ fatal_call = AST::Call.new(
75
+ callee: AST::Identifier.new(name: "fatal", line:, column:),
76
+ arguments: [AST::Argument.new(name: nil, value: message, line:, column:)],
77
+ line:,
78
+ column:,
79
+ )
80
+ IR::ExpressionStmt.new(
81
+ expression: lower_expression(fatal_call, env:, expected_type: @ctx.types.fetch("void")),
82
+ line:,
83
+ path: @ctx.current_analysis_path,
84
+ )
85
+ end
86
+
87
+ def unary_expression(operator, operand, line:, column:)
88
+ AST::UnaryOp.new(operator:, operand:, line:, column:)
89
+ end
90
+
91
+ def binary_expression(operator, left, right, line:, column:)
92
+ AST::BinaryOp.new(operator:, left:, right:, line:, column:)
93
+ end
94
+
95
+ def string_literal(value, line:, column:)
96
+ AST::StringLiteral.new(lexeme: value.inspect, value:, cstring: false, line:, column:)
97
+ end
98
+ end
99
+ end
100
+ end
@@ -422,6 +422,11 @@ module MilkTea
422
422
  end
423
423
 
424
424
  def lower_expression_stmt(statement, lowered:, local_defers:, local_env:)
425
+ if (assert_statements = lower_assert_like_statement(statement, env: local_env))
426
+ lowered.concat(assert_statements)
427
+ return
428
+ end
429
+
425
430
  if (format_sink_statements = lower_explicit_format_sink_expression_statement(statement.expression, env: local_env, line: statement.line))
426
431
  lowered.concat(format_sink_statements)
427
432
  return
@@ -271,6 +271,8 @@ module MilkTea
271
271
  )
272
272
  when :zero
273
273
  IR::ZeroInit.new(type:)
274
+ when :assert, :expect, :expect_eq, :expect_ne
275
+ raise LoweringError.new("#{kind} must be used as a statement", line: 0, column: 0, path: @ctx.current_analysis_path)
274
276
  when :fatal
275
277
  argument = expression.arguments.fetch(0)
276
278
  message_type = infer_expression_type(argument.value, env:)
@@ -6,6 +6,10 @@ module MilkTea
6
6
  include CompileTime::MethodFolding
7
7
  PASS_THROUGH_BUILTINS = {
8
8
  "fatal" => :fatal,
9
+ "assert" => :assert,
10
+ "expect" => :expect,
11
+ "expect_eq" => :expect_eq,
12
+ "expect_ne" => :expect_ne,
9
13
  "ref_of" => :ref_of,
10
14
  "const_ptr_of" => :const_ptr_of,
11
15
  "read" => :read,
@@ -26,6 +26,7 @@
26
26
 
27
27
  require_relative "lowering/scans"
28
28
  require_relative "lowering/declarations"
29
+ require_relative "lowering/assertions"
29
30
  require_relative "lowering/events"
30
31
  require_relative "lowering/functions"
31
32
  require_relative "lowering/async/analysis"
@@ -107,6 +108,7 @@ module MilkTea
107
108
  include Lowering::Functions
108
109
  include Lowering::Async
109
110
  include Lowering::Block
111
+ include Lowering::Assertions
110
112
  include Lowering::Proc
111
113
  include Lowering::Loops
112
114
  include Lowering::Expressions
@@ -834,6 +834,37 @@ module MilkTea
834
834
 
835
835
  raise_sema_error("fatal expects str or cstr, got #{message_type}")
836
836
  end
837
+ def check_assert_call(kind, arguments, scopes:)
838
+ name = kind.to_s
839
+ raise_sema_error("#{name} does not support named arguments") if arguments.any?(&:name)
840
+ raise_sema_error("#{name} expects 1 or 2 arguments, got #{arguments.length}") unless arguments.length == 1 || arguments.length == 2
841
+
842
+ condition_type = infer_expression(arguments.fetch(0).value, scopes:, expected_type: @ctx.types.fetch("bool"))
843
+ ensure_assignable!(condition_type, @ctx.types.fetch("bool"), "#{name} condition must be bool, got #{condition_type}", expression: arguments.fetch(0).value)
844
+ check_assert_message(name, arguments[1]&.value, scopes:) if arguments.length == 2
845
+
846
+ @ctx.types.fetch("void")
847
+ end
848
+ def check_expect_eq_call(kind, arguments, scopes:)
849
+ name = kind.to_s
850
+ raise_sema_error("#{name} does not support named arguments") if arguments.any?(&:name)
851
+ raise_sema_error("#{name} expects 2 or 3 arguments, got #{arguments.length}") unless arguments.length == 2 || arguments.length == 3
852
+
853
+ left = arguments.fetch(0).value
854
+ right = arguments.fetch(1).value
855
+ operator = kind == :expect_ne ? "!=" : "=="
856
+ comparison = AST::BinaryOp.new(operator:, left:, right:, line: nil, column: nil)
857
+ infer_binary(comparison, scopes:, expected_type: @ctx.types.fetch("bool"))
858
+ check_assert_message(name, arguments[2]&.value, scopes:) if arguments.length == 3
859
+
860
+ @ctx.types.fetch("void")
861
+ end
862
+ def check_assert_message(name, message, scopes:)
863
+ return unless message
864
+
865
+ message_type = infer_expression(message, scopes:, expected_type: @ctx.types.fetch("str"))
866
+ raise_sema_error("#{name} message must be str or cstr, got #{message_type}") unless string_like_type?(message_type)
867
+ end
837
868
  def check_get_call(arguments, scopes:)
838
869
  raise_sema_error("get does not support named arguments") if arguments.any?(&:name)
839
870
  raise_sema_error("get expects 2 arguments, got #{arguments.length}") unless arguments.length == 2
@@ -1043,6 +1043,10 @@ module MilkTea
1043
1043
  check_order_call(callable, expression.arguments, scopes:)
1044
1044
  when :fatal
1045
1045
  check_fatal_call(expression.arguments, scopes:)
1046
+ when :assert, :expect
1047
+ check_assert_call(callable_kind, expression.arguments, scopes:)
1048
+ when :expect_eq, :expect_ne
1049
+ check_expect_eq_call(callable_kind, expression.arguments, scopes:)
1046
1050
  when :ref_of
1047
1051
  check_ref_of_call(expression.arguments, scopes:)
1048
1052
  when :const_ptr_of
@@ -1318,6 +1322,10 @@ module MilkTea
1318
1322
 
1319
1323
  return [:function, @ctx.top_level_functions.fetch(callee.name), nil] if @ctx.top_level_functions.key?(callee.name)
1320
1324
  return [:fatal, nil, nil] if callee.name == "fatal"
1325
+ return [:assert, nil, nil] if callee.name == "assert"
1326
+ return [:expect, nil, nil] if callee.name == "expect"
1327
+ return [:expect_eq, nil, nil] if callee.name == "expect_eq"
1328
+ return [:expect_ne, nil, nil] if callee.name == "expect_ne"
1321
1329
  return [:ref_of, nil, nil] if callee.name == "ref_of"
1322
1330
  return [:const_ptr_of, nil, nil] if callee.name == "const_ptr_of"
1323
1331
  return [:read, nil, nil] if callee.name == "read"