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 +4 -4
- data/README.md +11 -1
- data/docs/index.html +76 -2
- data/docs/language-design.md +2 -2
- data/docs/language-manual.md +15 -3
- data/docs/testing.md +91 -0
- data/lib/milk_tea/base.rb +1 -1
- data/lib/milk_tea/core/control_flow/builder.rb +10 -1
- data/lib/milk_tea/core/lowering/assertions.rb +100 -0
- data/lib/milk_tea/core/lowering/block.rb +5 -0
- data/lib/milk_tea/core/lowering/calls.rb +2 -0
- data/lib/milk_tea/core/lowering/resolve.rb +4 -0
- data/lib/milk_tea/core/lowering.rb +2 -0
- data/lib/milk_tea/core/semantic_analyzer/calls.rb +31 -0
- data/lib/milk_tea/core/semantic_analyzer/expressions.rb +8 -0
- data/lib/milk_tea/core/semantic_analyzer/statements.rb +2 -1
- data/lib/milk_tea/lsp/server/completion.rb +9 -18
- data/lib/milk_tea/lsp/server/utilities.rb +2 -3
- data/lib/milk_tea/lsp/workspace/caches.rb +2 -0
- data/lib/milk_tea/lsp/workspace/definition_index.rb +12 -13
- data/lib/milk_tea/lsp/workspace/store.rb +6 -1
- data/lib/milk_tea/lsp/workspace/utilities.rb +50 -30
- data/lib/milk_tea/lsp/workspace.rb +5 -1
- data/lib/milk_tea/tooling/cli/commands/run.rb +3 -2
- data/lib/milk_tea/tooling/cli/commands/test.rb +39 -40
- data/lib/milk_tea/tooling/cli.rb +5 -4
- data/lib/milk_tea/tooling/linter/rules.rb +13 -2
- metadata +4 -3
- data/std/testing.mt +0 -266
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 78cefa537b068c8eb6d28d4b9111448c60e837bcaaddc82400b3b7f00ca6bbb8
|
|
4
|
+
data.tar.gz: 3ceff08e45e74f22ef4fe2f3a7eee31dd11a7943e5f8596766e792741c366cdf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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>
|
|
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 -> 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 -> 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() -> 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
|
])
|
data/docs/language-design.md
CHANGED
|
@@ -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 [
|
|
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 [
|
|
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
|
|
data/docs/language-manual.md
CHANGED
|
@@ -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
|
|
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
|
@@ -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"
|