mt-lang 0.3.28 → 0.3.30
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 +3 -3
- data/docs/index.html +2 -2
- data/docs/language-design.md +2 -2
- data/docs/language-manual.md +2 -2
- data/lib/milk_tea/base.rb +1 -1
- data/lib/milk_tea/core/c_backend/feature_detection.rb +8 -0
- data/lib/milk_tea/core/c_backend/runtime_helpers.rb +21 -0
- data/lib/milk_tea/core/c_backend.rb +4 -0
- data/lib/milk_tea/core/lowering/expressions.rb +4 -0
- data/lib/milk_tea/core/semantic_analyzer/expressions.rb +2 -2
- metadata +2 -3
- data/docs/self-hosted-compiler-plan.md +0 -737
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d6d13cde9fca875b324ff3e0cb3b892c371e2445ac5e061d138bd0dc8b284b88
|
|
4
|
+
data.tar.gz: 2a91eedc02120e0d042bc6bafbfc783a4eefb5bbc44c34940e1ee9be6917405e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 478dc59b852c703b5cf63ce3cd77a92d6de305424c96cfa7dbeda97ed1882f2d8c52f5770893dc99a3077cb1599cbf0602c6e54b773f219ce859b8b9275c7258
|
|
7
|
+
data.tar.gz: d221d15fe3c3192ee0b10b61101f5234e8b92830f878051c6e2641aca334b7d165c9668bf410a727b4964f02d45805322044470a9e406b971ab7508650d60a00
|
data/README.md
CHANGED
|
@@ -94,7 +94,7 @@ Supported literals:
|
|
|
94
94
|
- floats: `3.14`, `1.2e-3`, `1.0f` (float suffix), `1.0d` (double suffix)
|
|
95
95
|
- character: `'a'`, `'\n'`, `'\t'`, `'\\'`, `'\''`, `'\0'`, `'\x41'`. Type is `ubyte`. Escape sequences: `\n`, `\r`, `\t`, `\\`, `\'`, `\"`, `\0` (null byte), `\xNN` (hex byte).
|
|
96
96
|
- booleans: `true`, `false`
|
|
97
|
-
- string: `"hello"` -> `str`
|
|
97
|
+
- string: `"hello"` -> `str`. The `+` operator concatenates `str` values: `"hello" + " " + "world"` produces `"hello world"`. For loops or repeated concatenation, prefer `string.String`.
|
|
98
98
|
- cstring: `c"hello"` -> `cstr`
|
|
99
99
|
- heredoc string: `<<-TAG ... TAG`
|
|
100
100
|
- heredoc cstring: `c<<-TAG ... TAG`
|
|
@@ -106,7 +106,7 @@ Common punctuation and operators:
|
|
|
106
106
|
- delimiters: `(` `)` `[` `]`
|
|
107
107
|
- access and separators: `:` `,` `.`
|
|
108
108
|
- type markers: `->` `?`
|
|
109
|
-
- arithmetic: `+ - * / %`
|
|
109
|
+
- arithmetic: `+ - * / %` (additionally, `+` on `str` concatenates)
|
|
110
110
|
- bitwise: `~ & | ^ << >>`
|
|
111
111
|
- comparison: `== != < <= > >=`
|
|
112
112
|
- assignment: `= += -= *= /= %= &= |= ^= <<= >>=`
|
|
@@ -937,7 +937,7 @@ See module source for full method surface. Iterator forms:
|
|
|
937
937
|
|
|
938
938
|
Text categories:
|
|
939
939
|
|
|
940
|
-
- `str` -> string view
|
|
940
|
+
- `str` -> string view. The `+` operator concatenates two `str` values, allocating a new heap-backed string. For loops or repeated concatenation, prefer `string.String` for amortized performance.
|
|
941
941
|
- `cstr` -> C ABI string
|
|
942
942
|
- `str_buffer[N]` -> fixed-capacity mutable UTF-8 text buffer
|
|
943
943
|
|
data/docs/index.html
CHANGED
|
@@ -961,7 +961,7 @@ let total = (
|
|
|
961
961
|
<tr><td>Delimiters</td><td><code>( ) [ ]</code></td></tr>
|
|
962
962
|
<tr><td>Separators / Access</td><td><code>: , .</code></td></tr>
|
|
963
963
|
<tr><td>Type markers</td><td><code>-></code> <code>?</code></td></tr>
|
|
964
|
-
<tr><td>Arithmetic</td><td><code>+ - * / %</code
|
|
964
|
+
<tr><td>Arithmetic</td><td><code>+ - * / %</code> (the <code>+</code> operator on <code>str</code> concatenates, allocating a new heap-backed string)</td></tr>
|
|
965
965
|
<tr><td>Bitwise</td><td><code>~ & | ^ << >></code></td></tr>
|
|
966
966
|
<tr><td>Comparison</td><td><code>== != < <= > >=</code></td></tr>
|
|
967
967
|
<tr><td>Assignment</td><td><code>= += -= *= /= %= &= |= ^= <<= >>=</code></td></tr>
|
|
@@ -1243,7 +1243,7 @@ let d: dyn[Drawable] = adapt[Drawable](ref_of(entity))</code></pre>
|
|
|
1243
1243
|
<tr><td><code>ptr_int</code> <code>ptr_uint</code></td><td>Pointer-sized integers</td></tr>
|
|
1244
1244
|
<tr><td><code>float</code> <code>double</code></td><td>Floating-point</td></tr>
|
|
1245
1245
|
<tr><td><code>void</code></td><td>No value</td></tr>
|
|
1246
|
-
<tr><td><code>str</code></td><td>UTF-8 string view (borrowed)</td></tr>
|
|
1246
|
+
<tr><td><code>str</code></td><td>UTF-8 string view (borrowed). The <code>+</code> operator concatenates two <code>str</code> values, allocating a new heap-backed string.</td></tr>
|
|
1247
1247
|
<tr><td><code>cstr</code></td><td>NUL-terminated C string</td></tr>
|
|
1248
1248
|
<tr><td><code>vec2</code> <code>vec3</code> <code>vec4</code></td><td>Float vectors with <code>.x .y .z .w</code></td></tr>
|
|
1249
1249
|
<tr><td><code>ivec2</code> <code>ivec3</code> <code>ivec4</code></td><td>Integer vectors</td></tr>
|
data/docs/language-design.md
CHANGED
|
@@ -38,7 +38,7 @@ If code allocates, takes an address, dereferences a raw pointer, performs an FFI
|
|
|
38
38
|
|
|
39
39
|
FFI visibility belongs at the declaration site. Raw `external` files expose exact ABI types. Imported foreign declarations may project those raw types into ordinary Milk Tea types, but the projection rule, temporary-storage rule, and ownership rule must be declared there instead of repeated at every call site.
|
|
40
40
|
|
|
41
|
-
The same rule applies to text construction. Plain string literals and format string literals are borrowed `str` values. Any surface that builds owned text must say so explicitly, for example `std.fmt.format(f"...")` when ownership must escape.
|
|
41
|
+
The same rule applies to text construction. Plain string literals and format string literals are borrowed `str` values. The `+` operator on `str` allocates a new heap-backed `str` — the one everyday convenience that does allocate. For loops or amortized building, `string.String` and `str_buffer[N]` remain the explicit surfaces with visible cost. Any other surface that builds owned text must say so explicitly, for example `std.fmt.format(f"...")` when ownership must escape.
|
|
42
42
|
|
|
43
43
|
### 2. C is the ABI ground truth
|
|
44
44
|
|
|
@@ -755,7 +755,7 @@ Explicit specialization arguments may be type references like `bytes_for[int](4)
|
|
|
755
755
|
|
|
756
756
|
Built-in operators should match familiar C behavior where possible:
|
|
757
757
|
|
|
758
|
-
- arithmetic: `+ - * / %`
|
|
758
|
+
- arithmetic: `+ - * / %` (the `+` operator on `str` concatenates, allocating a new heap-backed string)
|
|
759
759
|
- comparison: `== != < <= > >=`
|
|
760
760
|
- boolean: `and or not`
|
|
761
761
|
- bitwise: `& | ^ ~ << >>`
|
data/docs/language-manual.md
CHANGED
|
@@ -124,7 +124,7 @@ Supported literals:
|
|
|
124
124
|
- integer: `42`, `0xff`, `0b1010`, with `_` separators. Integer type suffixes: `42u` (`uint`), `0xFFub` (`ubyte`), `100z` (`ptr_uint`), `7i` (`int`), `-1l` (`long`), etc.
|
|
125
125
|
- float: `3.14`, `1.2e-3`, `1.1920929E-7`, `1.0f` (float suffix), `1.0d` (double suffix)
|
|
126
126
|
- character: `'a'`, `'\n'`, `'\t'`, `'\\'`, `'\''`, `'\0'`, `'\x41'`. Type is `ubyte`. Escape sequences: `\n`, `\r`, `\t`, `\\`, `\'`, `\"`, `\0` (null), `\xNN` (hex byte).
|
|
127
|
-
- string: `"hello"` (`str`). Supported string escapes are `\n`, `\r`, `\t`, `\0` (null), `\"`, `\'`, and `\\`; any other `\x` sequence is taken literally. Hex byte escapes (`\xNN`) are character-literal only, not string-literal.
|
|
127
|
+
- string: `"hello"` (`str`). Supported string escapes are `\n`, `\r`, `\t`, `\0` (null), `\"`, `\'`, and `\\`; any other `\x` sequence is taken literally. Hex byte escapes (`\xNN`) are character-literal only, not string-literal. The `+` operator concatenates `str` values: `"hello" + " " + "world"` produces `"hello world"`. Each `+` allocates a new heap-backed `str`; for loops or repeated concatenation, prefer `string.String` for amortized performance.
|
|
128
128
|
- cstring: `c"hello"` (`cstr`)
|
|
129
129
|
- heredoc string: `<<-TAG ... TAG` (`str`)
|
|
130
130
|
- heredoc cstring: `c<<-TAG ... TAG` (`cstr`)
|
|
@@ -1015,7 +1015,7 @@ Rules:
|
|
|
1015
1015
|
8. `==`, `!=`
|
|
1016
1016
|
9. `<`, `<=`, `>`, `>=`
|
|
1017
1017
|
10. `<<`, `>>`
|
|
1018
|
-
11. `+`, `-`
|
|
1018
|
+
11. `+`, `-` (additionally, `+` on `str` concatenates; each `+` allocates a new heap-backed `str`)
|
|
1019
1019
|
12. `*`, `/`, `%`
|
|
1020
1020
|
|
|
1021
1021
|
### 5.4 Assignment operators
|
data/lib/milk_tea/base.rb
CHANGED
|
@@ -289,6 +289,14 @@ module MilkTea
|
|
|
289
289
|
emitted_functions.any? { |function| function_uses_str_equality?(function) }
|
|
290
290
|
end
|
|
291
291
|
|
|
292
|
+
def uses_str_concat_helper?
|
|
293
|
+
return @uses_str_concat if defined?(@uses_str_concat)
|
|
294
|
+
|
|
295
|
+
@uses_str_concat = emitted_functions.any? do |function|
|
|
296
|
+
function_uses_named_call?(function, %w[mt_str_concat])
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
292
300
|
def uses_variant_equality_helper?
|
|
293
301
|
emitted_functions.any? { |function| function_uses_variant_equality?(function) }
|
|
294
302
|
end
|
|
@@ -42,6 +42,27 @@ module MilkTea
|
|
|
42
42
|
]
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
def emit_str_concat_helper
|
|
46
|
+
[
|
|
47
|
+
"#define MT_STR_CONCAT_BUF_SIZE 65536",
|
|
48
|
+
"",
|
|
49
|
+
"static _Thread_local char mt_str_concat_buf[MT_STR_CONCAT_BUF_SIZE];",
|
|
50
|
+
"static _Thread_local uintptr_t mt_str_concat_offset = 0;",
|
|
51
|
+
"",
|
|
52
|
+
"static mt_str mt_str_concat(mt_str a, mt_str b) {",
|
|
53
|
+
"#{INDENT}uintptr_t total = a.len + b.len;",
|
|
54
|
+
"#{INDENT}if (mt_str_concat_offset + total > MT_STR_CONCAT_BUF_SIZE) {",
|
|
55
|
+
"#{INDENT * 2}mt_str_concat_offset = 0;",
|
|
56
|
+
"#{INDENT}}",
|
|
57
|
+
"#{INDENT}char* buf = mt_str_concat_buf + mt_str_concat_offset;",
|
|
58
|
+
"#{INDENT}if (a.len > 0) memcpy(buf, a.data, a.len);",
|
|
59
|
+
"#{INDENT}if (b.len > 0) memcpy(buf + a.len, b.data, b.len);",
|
|
60
|
+
"#{INDENT}mt_str_concat_offset += total;",
|
|
61
|
+
"#{INDENT}return (mt_str){ .data = buf, .len = total };",
|
|
62
|
+
"}",
|
|
63
|
+
]
|
|
64
|
+
end
|
|
65
|
+
|
|
45
66
|
def emit_variant_equality_helpers
|
|
46
67
|
emitted_aggregate_variants.flat_map { |variant_decl| emit_variant_equality_helper(variant_decl) }
|
|
47
68
|
end
|
|
@@ -146,6 +146,10 @@ module MilkTea
|
|
|
146
146
|
lines.concat(emit_str_equality_helper)
|
|
147
147
|
lines << ""
|
|
148
148
|
end
|
|
149
|
+
if uses_str_concat_helper?
|
|
150
|
+
lines.concat(emit_str_concat_helper)
|
|
151
|
+
lines << ""
|
|
152
|
+
end
|
|
149
153
|
if uses_str_buffer_helpers?
|
|
150
154
|
lines.concat(emit_utf8_validation_helpers)
|
|
151
155
|
lines << ""
|
|
@@ -1387,6 +1387,10 @@ module MilkTea
|
|
|
1387
1387
|
left = cast_expression(left, operand_type) if operand_type
|
|
1388
1388
|
right = cast_expression(right, operand_type) if operand_type
|
|
1389
1389
|
|
|
1390
|
+
if expression.operator == "+" && left_type == @ctx.types.fetch("str") && right_type == @ctx.types.fetch("str")
|
|
1391
|
+
return IR::Call.new(callee: "mt_str_concat", arguments: [left, right], type:)
|
|
1392
|
+
end
|
|
1393
|
+
|
|
1390
1394
|
expanded = lower_vector_binary_operation(expression.operator, left, left_type, right, right_type, type)
|
|
1391
1395
|
return expanded if expanded
|
|
1392
1396
|
|
|
@@ -612,8 +612,8 @@ module MilkTea
|
|
|
612
612
|
|
|
613
613
|
left_type
|
|
614
614
|
when "+", "-", "*", "/"
|
|
615
|
-
if expression.operator == "+" && (
|
|
616
|
-
|
|
615
|
+
if expression.operator == "+" && left_type == @ctx.types.fetch("str") && right_type == @ctx.types.fetch("str")
|
|
616
|
+
return left_type
|
|
617
617
|
end
|
|
618
618
|
|
|
619
619
|
pointer_result = pointer_arithmetic_result(expression.operator, left_type, right_type)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mt-lang
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.30
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Long (Teefan) Tran
|
|
@@ -149,7 +149,6 @@ files:
|
|
|
149
149
|
- docs/index.html
|
|
150
150
|
- docs/language-design.md
|
|
151
151
|
- docs/language-manual.md
|
|
152
|
-
- docs/self-hosted-compiler-plan.md
|
|
153
152
|
- lib/milk_tea.rb
|
|
154
153
|
- lib/milk_tea/base.rb
|
|
155
154
|
- lib/milk_tea/bindings.rb
|
|
@@ -625,7 +624,7 @@ metadata:
|
|
|
625
624
|
homepage_uri: https://teefan.github.io/mt-lang/
|
|
626
625
|
source_code_uri: https://github.com/teefan/mt-lang
|
|
627
626
|
post_install_message: |
|
|
628
|
-
Milk Tea 0.3.
|
|
627
|
+
Milk Tea 0.3.30 installed!
|
|
629
628
|
|
|
630
629
|
System requirements:
|
|
631
630
|
- A C compiler (gcc or clang) must be available on PATH
|
|
@@ -1,737 +0,0 @@
|
|
|
1
|
-
# Self-Hosted Milk Tea Compiler — Architecture Plan
|
|
2
|
-
|
|
3
|
-
## `projects/mtc`
|
|
4
|
-
|
|
5
|
-
This document describes the architecture and implementation plan for a self-hosted
|
|
6
|
-
Milk Tea compiler, written in Milk Tea, that compiles Milk Tea source to C.
|
|
7
|
-
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
## 0. Core Design Principles
|
|
11
|
-
|
|
12
|
-
Three principles separate a modern self-hosted compiler from the Ruby prototype:
|
|
13
|
-
|
|
14
|
-
1. **IR-stage separation**. The Ruby compiler's `Lowering` does too much in one pass — CPS
|
|
15
|
-
transform, monomorphization, desugaring, and C-biased lowering are all intertwined.
|
|
16
|
-
`mtc` uses two distinct IRs between the surface AST and the C backend: **HIR** (typed,
|
|
17
|
-
monomorphized, name-resolved) and **LIR** (flat, C-shaped). Generics are
|
|
18
|
-
monomorphized during the AST → HIR transition rather than deferred to lowering.
|
|
19
|
-
|
|
20
|
-
2. **Modular passes**. Each pass is a standalone module with a single `run(ctx) ->
|
|
21
|
-
Result[ctx, Error]` signature. Passes compose via a pipeline struct. No global
|
|
22
|
-
mutable state.
|
|
23
|
-
|
|
24
|
-
3. **Diagnostics-first**. Every pass accumulates `Diagnostic` values into a shared
|
|
25
|
-
`DiagnosticEngine`. Errors never abort the pipeline; they are collected, sorted by
|
|
26
|
-
location, and emitted as a batch.
|
|
27
|
-
|
|
28
|
-
---
|
|
29
|
-
|
|
30
|
-
## 1. Project Layout
|
|
31
|
-
|
|
32
|
-
```
|
|
33
|
-
projects/mtc/
|
|
34
|
-
├── package.toml # kind=application, source_root=src
|
|
35
|
-
├── src/
|
|
36
|
-
│ ├── main.mt # Entry: parse CLI, dispatch compile/run/check/test
|
|
37
|
-
│ ├── mtc/
|
|
38
|
-
│ │ ├── diagnostics.mt # Diagnostic, DiagnosticKind, DiagnosticEngine, SourceSpan
|
|
39
|
-
│ │ ├── token.mt # TokenKind enum, Token struct
|
|
40
|
-
│ │ ├── lexer/
|
|
41
|
-
│ │ │ ├── lexer.mt # Lexer.lex(source, path) -> Result[Vec[Token], Error]
|
|
42
|
-
│ │ │ ├── trivia.mt # Whitespace/comment handling (CST reconstruction)
|
|
43
|
-
│ │ │ └── char_tables.mt # Static lookup tables (is_alpha, is_digit, etc.)
|
|
44
|
-
│ │ ├── parser/
|
|
45
|
-
│ │ │ ├── parser.mt # Recursive-descent parser entry
|
|
46
|
-
│ │ │ ├── expressions.mt # Pratt parser for operator expressions
|
|
47
|
-
│ │ │ ├── declarations.mt # Top-level declarations
|
|
48
|
-
│ │ │ ├── statements.mt # Statement parsing (if, for, match, while, etc.)
|
|
49
|
-
│ │ │ └── recovery.mt # Error recovery synchronizer sets
|
|
50
|
-
│ │ ├── ast.mt # AST node definitions and arena
|
|
51
|
-
│ │ ├── hir.mt # HIR types (typed, monomorphized, name-resolved)
|
|
52
|
-
│ │ ├── lir.mt # LIR types (flat, C-shaped)
|
|
53
|
-
│ │ ├── symbol_table.mt # Scoped symbol table (stacked maps)
|
|
54
|
-
│ │ ├── type_system/
|
|
55
|
-
│ │ │ ├── types.mt # Type variant and registry
|
|
56
|
-
│ │ │ └── layout.mt # size_of / align_of / offset_of (const functions)
|
|
57
|
-
│ │ ├── semantic/
|
|
58
|
-
│ │ │ ├── checker.mt # Phase: AST → HIR (name resolution, type checking)
|
|
59
|
-
│ │ │ ├── infer.mt # Bidirectional type inference
|
|
60
|
-
│ │ │ ├── conform.mt # Interface conformance checking
|
|
61
|
-
│ │ │ ├── monomorphize.mt # Generic instantiation (HIR construction time)
|
|
62
|
-
│ │ │ └── const_eval.mt # Compile-time expression evaluator
|
|
63
|
-
│ │ ├── lowering/
|
|
64
|
-
│ │ │ ├── lower.mt # Phase: HIR → LIR (desugaring, flattening)
|
|
65
|
-
│ │ │ ├── desugar.mt # async→CPS, for→while, match→switch, ?→if-return
|
|
66
|
-
│ │ │ └── cps.mt # Continuation-passing-style transform for async
|
|
67
|
-
│ │ ├── module_loader.mt # Load and resolve transitive imports
|
|
68
|
-
│ │ ├── module_path_resolver.mt # Resolve import paths to filesystem paths
|
|
69
|
-
│ │ ├── module_binder.mt # Public/private visibility split per module
|
|
70
|
-
│ │ ├── backend/
|
|
71
|
-
│ │ │ ├── c_backend.mt # Phase: LIR → C source text
|
|
72
|
-
│ │ │ ├── llvm.mt # Future: LIR → LLVM IR (via external function bindings)
|
|
73
|
-
│ │ │ └── runtime.mt # C runtime helper emission (fatal, format, refcount, etc.)
|
|
74
|
-
│ │ └── pipeline.mt # CompilerPipeline orchestrator struct
|
|
75
|
-
│ └── tests/ # In-language tests using @[test] and std.testing
|
|
76
|
-
│ ├── lexer_test.mt
|
|
77
|
-
│ ├── parser_test.mt
|
|
78
|
-
│ ├── semantic_test.mt
|
|
79
|
-
│ ├── type_test.mt
|
|
80
|
-
│ └── roundtrip_test.mt
|
|
81
|
-
└── tests/ # Canary .mt programs as test corpus (run via mtc test)
|
|
82
|
-
├── lex/
|
|
83
|
-
├── parse/
|
|
84
|
-
├── type/
|
|
85
|
-
├── lower/
|
|
86
|
-
└── roundtrip/ # Self-compilation verification
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
Note: LSP support (`lsp/server.mt`, `hover.mt`, `completion.mt`, `goto_def.mt`) is
|
|
90
|
-
deferred to v2. The v1 compiler is CLI-only. The `DiagnosticEngine` is designed to
|
|
91
|
-
accommodate LSP consumers in the future via byte-offset source spans.
|
|
92
|
-
|
|
93
|
-
---
|
|
94
|
-
|
|
95
|
-
## 2. Pipeline Architecture
|
|
96
|
-
|
|
97
|
-
```
|
|
98
|
-
┌─────────┐ ┌─────────┐ ┌───────────┐ ┌──────────┐ ┌─────────┐
|
|
99
|
-
│ Source │───▶│ Lexer │───▶│ Parser │───▶│ Semantic │───▶│ Lowerer │
|
|
100
|
-
│ Text │ │ │ │ │ │ (AST→HIR)│ │(HIR→LIR)│
|
|
101
|
-
└─────────┘ └─────────┘ └───────────┘ └──────────┘ └─────────┘
|
|
102
|
-
│ │ │ │
|
|
103
|
-
▼ ▼ ▼ ▼
|
|
104
|
-
┌─────────────────────────────────────────────────────────┐
|
|
105
|
-
│ DiagnosticEngine │
|
|
106
|
-
└─────────────────────────────────────────────────────────┘
|
|
107
|
-
│
|
|
108
|
-
▼
|
|
109
|
-
┌──────────┐
|
|
110
|
-
│ CBackend │──▶ C source
|
|
111
|
-
└──────────┘
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
`CompilerPipeline` is the orchestrator:
|
|
115
|
-
|
|
116
|
-
```mt
|
|
117
|
-
struct CompilerPipeline:
|
|
118
|
-
options: CompileOptions
|
|
119
|
-
diagnostics: DiagnosticEngine
|
|
120
|
-
|
|
121
|
-
function run(source: str, path: str) -> Result[vec.Vec[ubyte], Diagnostic]:
|
|
122
|
-
let tokens = this.lex(source, path)?
|
|
123
|
-
let ast = this.parse(tokens, path)?
|
|
124
|
-
let hir = this.semantic(ast, path)?
|
|
125
|
-
let lir = this.lower(hir)?
|
|
126
|
-
return this.backend(lir)?
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
Each pass returns `Result[Output, Diagnostic]`. Critical errors short-circuit via `?`.
|
|
130
|
-
Non-critical diagnostics accumulate in the engine for batch emission.
|
|
131
|
-
|
|
132
|
-
---
|
|
133
|
-
|
|
134
|
-
## 3. Memory Model: Arena + Handle Pattern
|
|
135
|
-
|
|
136
|
-
Milk Tea prohibits storing `ref[T]` inside generic containers such as `Vec`.
|
|
137
|
-
Tree-shaped data (AST, HIR, LIR) must therefore avoid `Vec[ref[Expr]]` and similar
|
|
138
|
-
patterns. The standard solution, used by industrial compilers including `rustc`, is
|
|
139
|
-
arena-allocated value storage with opaque index handles.
|
|
140
|
-
|
|
141
|
-
### The pattern
|
|
142
|
-
|
|
143
|
-
Every IR node type has a corresponding `Id` — a plain `ptr_uint` index into a
|
|
144
|
-
`Vec[Node]` owned by an arena struct:
|
|
145
|
-
|
|
146
|
-
```mt
|
|
147
|
-
type ExprId = ptr_uint
|
|
148
|
-
type StmtId = ptr_uint
|
|
149
|
-
type DeclId = ptr_uint
|
|
150
|
-
type TypeId = ptr_uint
|
|
151
|
-
|
|
152
|
-
struct AstArena:
|
|
153
|
-
exprs: Vec[Expr]
|
|
154
|
-
stmts: Vec[Stmt]
|
|
155
|
-
decls: Vec[Decl]
|
|
156
|
-
|
|
157
|
-
editable function alloc_expr(node: Expr) -> ExprId:
|
|
158
|
-
let index: ptr_uint = this.exprs.len()
|
|
159
|
-
this.exprs.push(node)
|
|
160
|
-
return index
|
|
161
|
-
|
|
162
|
-
editable function alloc_stmt(node: Stmt) -> StmtId:
|
|
163
|
-
let index: ptr_uint = this.stmts.len()
|
|
164
|
-
this.stmts.push(node)
|
|
165
|
-
return index
|
|
166
|
-
|
|
167
|
-
editable function alloc_decl(node: Decl) -> DeclId:
|
|
168
|
-
let index: ptr_uint = this.decls.len()
|
|
169
|
-
this.decls.push(node)
|
|
170
|
-
return index
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
All child references in AST/HIR/LIR nodes use `Id` types, never `ref[T]`. Ids are
|
|
174
|
-
plain integers — legal in any container, trivially copyable, and zero-cost to
|
|
175
|
-
resolve through the arena. Bulk deallocation happens when the arena itself is released.
|
|
176
|
-
|
|
177
|
-
The same pattern applies to the type system: types live in a `TypeRegistry` arena
|
|
178
|
-
and are referenced by `TypeId`:
|
|
179
|
-
|
|
180
|
-
```mt
|
|
181
|
-
struct TypeRegistry:
|
|
182
|
-
types: Vec[Type]
|
|
183
|
-
|
|
184
|
-
edible function intern(typ: Type) -> TypeId:
|
|
185
|
-
let index: ptr_uint = this.types.len()
|
|
186
|
-
this.types.push(typ)
|
|
187
|
-
return index
|
|
188
|
-
|
|
189
|
-
function resolve(id: TypeId) -> ptr[Type]?
|
|
190
|
-
```
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
### Why not `own[T]` or `ref[T]`
|
|
194
|
-
|
|
195
|
-
`ref[T]` is rejected in every relevant tree-storage position:
|
|
196
|
-
|
|
197
|
-
- `Vec[ref[T]]` — rejected at compile time: *"ref types cannot be nested inside Vec"*
|
|
198
|
-
- `ref[T]` as a variant arm payload field — rejected: *"cannot store ref types; declare
|
|
199
|
-
a lifetime on the struct"*
|
|
200
|
-
- `ref[T]` in a struct field auto-generates an implicit lifetime, making the struct
|
|
201
|
-
non-owning and rejected from `Vec`, returns, and module storage
|
|
202
|
-
|
|
203
|
-
`own[T]` (owning heap pointers) is storable in containers and variant fields, but
|
|
204
|
-
using it for every tree child produces millions of separate heap allocations for a
|
|
205
|
-
50K-LOC compiler. The arena pattern stores all nodes contiguously in a single
|
|
206
|
-
allocation per IR stage, yielding far better cache locality and eliminating per-node
|
|
207
|
-
allocation overhead.
|
|
208
|
-
|
|
209
|
-
### Arena implementation
|
|
210
|
-
|
|
211
|
-
The arena pattern above is built on plain `Vec` storage: each arena field (`exprs`,
|
|
212
|
-
`stmts`, `decls`) is a `Vec[Node]`, and `alloc_*` methods simply push the node and
|
|
213
|
-
return `this.vec.len()` as the index. No unsafe code, no pointer arithmetic — just
|
|
214
|
-
contiguous expandable storage with O(1) append and O(1) index resolution via `Vec.get`.
|
|
215
|
-
For release, the arena's `release()` iterates and calls `release()` on each contained
|
|
216
|
-
`Vec` and `Map`, then bulk-deallocates.
|
|
217
|
-
|
|
218
|
-
For lower-level bump-pointer allocation (e.g., in the C backend's output buffer),
|
|
219
|
-
`std.mem.arena` is available but not needed for IR tree storage.
|
|
220
|
-
|
|
221
|
-
---
|
|
222
|
-
|
|
223
|
-
## 4. Key Data Structures
|
|
224
|
-
|
|
225
|
-
*Import preamble: `Vec`, `Map`, `Set` are not prelude types (only `Option` and `Result`
|
|
226
|
-
are). Real source files use `import std.vec as vec`, `import std.map as m`, etc.
|
|
227
|
-
and reference them as `vec.Vec[T]`, `m.Map[K,V]`. Code examples below use bare
|
|
228
|
-
`Vec[T]`, `Map[K,V]` for readability.*
|
|
229
|
-
|
|
230
|
-
### AST Nodes (the surface tree)
|
|
231
|
-
|
|
232
|
-
```mt
|
|
233
|
-
variant Expr:
|
|
234
|
-
literal(value: Literal)
|
|
235
|
-
identifier(name: str)
|
|
236
|
-
binary(left: ExprId, op: BinOp, right: ExprId)
|
|
237
|
-
unary(op: UnaryOp, operand: ExprId)
|
|
238
|
-
call(callee: ExprId, args: Vec[ExprId])
|
|
239
|
-
member(obj: ExprId, field: str)
|
|
240
|
-
index(obj: ExprId, idx: ExprId)
|
|
241
|
-
if_expr(cond: ExprId, then_branch: ExprId, else_branch: Option[ExprId])
|
|
242
|
-
match_expr(scrutinee: ExprId, arms: Vec[MatchArm])
|
|
243
|
-
proc_expr(params: Vec[Param], body: StmtId)
|
|
244
|
-
tuple_expr(elements: Vec[ExprId])
|
|
245
|
-
|
|
246
|
-
variant Stmt:
|
|
247
|
-
expr(expr: ExprId)
|
|
248
|
-
let_decl(name: str, typ: Option[TypeId], init: Option[ExprId], guard: Option[Guard])
|
|
249
|
-
assign(target: ExprId, op: AssignOp, value: ExprId)
|
|
250
|
-
if_stmt(cond: ExprId, then_block: Vec[StmtId], else_ifs: Vec[ElseIf],
|
|
251
|
-
else_block: Option[Vec[StmtId]])
|
|
252
|
-
while_stmt(cond: ExprId, body: Vec[StmtId])
|
|
253
|
-
for_stmt(bindings: Vec[ForBinding], iterable: ExprId, body: Vec[StmtId])
|
|
254
|
-
match_stmt(scrutinee: ExprId, arms: Vec[MatchArm])
|
|
255
|
-
return_stmt(value: Option[ExprId])
|
|
256
|
-
defer_stmt(body: Vec[StmtId])
|
|
257
|
-
|
|
258
|
-
variant Decl:
|
|
259
|
-
function_def(name: str, params: Vec[Param], return_type: Option[TypeId],
|
|
260
|
-
body: Option[Vec[StmtId]], visibility: Visibility, is_async: bool)
|
|
261
|
-
struct_def(name: str, fields: Vec[Field], type_params: Vec[TypeParam],
|
|
262
|
-
implements: Vec[QualName], visibility: Visibility)
|
|
263
|
-
variant_def(name: str, arms: Vec[VariantArm], type_params: Vec[TypeParam],
|
|
264
|
-
visibility: Visibility)
|
|
265
|
-
const_decl(name: str, typ: TypeId, init: ExprId, visibility: Visibility)
|
|
266
|
-
import_decl(module_path: Vec[str], alias: Option[str])
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
Note: variant arm fields of types like `Vec[ExprId]`, `Option[ExprId]`, and direct
|
|
270
|
-
`ExprId` values are all storable because `ExprId = ptr_uint` is a plain integer.
|
|
271
|
-
|
|
272
|
-
### HIR (typed, monomorphized, name-resolved)
|
|
273
|
-
|
|
274
|
-
HIR is the typed intermediate representation. Generic types are fully monomorphized,
|
|
275
|
-
names are resolved to bindings, and types are attached to every expression node.
|
|
276
|
-
|
|
277
|
-
```mt
|
|
278
|
-
struct HirProgram:
|
|
279
|
-
modules: Vec[HirModule]
|
|
280
|
-
|
|
281
|
-
struct HirModule:
|
|
282
|
-
name: str
|
|
283
|
-
path: str
|
|
284
|
-
declarations: Vec[HirDecl]
|
|
285
|
-
type_registry: TypeRegistry
|
|
286
|
-
symbol_table: SymbolTable
|
|
287
|
-
|
|
288
|
-
variant HirExpr:
|
|
289
|
-
literal(value: Literal, typ: TypeId)
|
|
290
|
-
var_ref(binding: BindingId, typ: TypeId)
|
|
291
|
-
call(callee: HirCallable, args: Vec[HirExprId], typ: TypeId)
|
|
292
|
-
member(obj: HirExprId, field: str, typ: TypeId)
|
|
293
|
-
intrinsic(kind: IntrinsicKind, args: Vec[HirExprId], typ: TypeId)
|
|
294
|
-
|
|
295
|
-
type HirExprId = ptr_uint
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
### LIR (flat, C-shaped)
|
|
299
|
-
|
|
300
|
-
LIR matches the Ruby compiler's `IR::Program` in spirit — flat statement lists, no
|
|
301
|
-
type parameters, C-compatible expression forms:
|
|
302
|
-
|
|
303
|
-
```mt
|
|
304
|
-
struct LirProgram:
|
|
305
|
-
modules: Vec[LirModule]
|
|
306
|
-
|
|
307
|
-
struct LirFunction:
|
|
308
|
-
name: str
|
|
309
|
-
linkage_name: str
|
|
310
|
-
params: Vec[LirParam]
|
|
311
|
-
return_type: TypeId
|
|
312
|
-
body: Vec[LirStmt]
|
|
313
|
-
locals: Vec[LirLocal]
|
|
314
|
-
|
|
315
|
-
variant LirExpr:
|
|
316
|
-
name(id: LocalId)
|
|
317
|
-
integer(value: int, width: IntWidth)
|
|
318
|
-
call(target: str, args: Vec[LirExpr])
|
|
319
|
-
member(obj: LirExpr, field: str)
|
|
320
|
-
binop(left: LirExpr, op: BinOp, right: LirExpr)
|
|
321
|
-
address_of(expr: LirExpr)
|
|
322
|
-
load(expr: LirExpr)
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
---
|
|
326
|
-
|
|
327
|
-
## 5. Type System Design
|
|
328
|
-
|
|
329
|
-
The type representation is the compiler's internal model — distinct from the language's
|
|
330
|
-
user-facing types:
|
|
331
|
-
|
|
332
|
-
```mt
|
|
333
|
-
# (All arm names use _type suffix uniformly. Of the 24 reserved primitive type
|
|
334
|
-
# names, only 6 appear here — bool/int/uint/float/char/void — and they require
|
|
335
|
-
# the suffix. Every other name works bare, but we suffix all for consistency.)
|
|
336
|
-
|
|
337
|
-
variant Type:
|
|
338
|
-
# Primitives
|
|
339
|
-
bool_type
|
|
340
|
-
int_type(width: IntWidth)
|
|
341
|
-
uint_type(width: UintWidth)
|
|
342
|
-
float_type(width: FloatWidth)
|
|
343
|
-
char_type
|
|
344
|
-
void_type
|
|
345
|
-
|
|
346
|
-
# Constructors
|
|
347
|
-
ptr_type(inner: TypeId)
|
|
348
|
-
const_ptr_type(inner: TypeId)
|
|
349
|
-
own_type(inner: TypeId)
|
|
350
|
-
ref_type(inner: TypeId)
|
|
351
|
-
span_type(inner: TypeId)
|
|
352
|
-
array_type(inner: TypeId, len: ptr_uint)
|
|
353
|
-
tuple_type(elements: Vec[TypeId])
|
|
354
|
-
func_type(params: Vec[TypeId], ret: TypeId)
|
|
355
|
-
proc_type(params: Vec[TypeId], ret: TypeId)
|
|
356
|
-
|
|
357
|
-
# User-defined
|
|
358
|
-
struct_type(name: QualName, args: Vec[TypeId])
|
|
359
|
-
variant_type(name: QualName, args: Vec[TypeId])
|
|
360
|
-
enum_type(name: QualName)
|
|
361
|
-
dyn_type(interface: QualName, args: Vec[TypeId])
|
|
362
|
-
|
|
363
|
-
# Special
|
|
364
|
-
type_var_type(index: int) # For generic resolution (replaced during monomorphization)
|
|
365
|
-
error_type # Sentinel for recovery
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
Types live in the `TypeRegistry` arena and are referenced by `TypeId` everywhere.
|
|
369
|
-
The `type_var` variant represents unresolved generic type parameters during inference;
|
|
370
|
-
it is never present in HIR or LIR — monomorphization replaces every `type_var` with a
|
|
371
|
-
concrete type.
|
|
372
|
-
|
|
373
|
-
---
|
|
374
|
-
|
|
375
|
-
## 6. Symbol Table
|
|
376
|
-
|
|
377
|
-
Stacked scopes with a pre-populated global scope for builtins:
|
|
378
|
-
|
|
379
|
-
```mt
|
|
380
|
-
struct SymbolTable:
|
|
381
|
-
scopes: Vec[Scope]
|
|
382
|
-
|
|
383
|
-
# Each scope is anonymous. Resolution walks the Vec from the innermost scope
|
|
384
|
-
# outward (the index position encodes nesting depth). No parent pointer needed.
|
|
385
|
-
struct Scope:
|
|
386
|
-
types: Map[str, TypeBinding]
|
|
387
|
-
values: Map[str, ValueBinding]
|
|
388
|
-
functions: Map[str, FuncBinding]
|
|
389
|
-
|
|
390
|
-
struct TypeBinding:
|
|
391
|
-
kind: TypeBindingKind
|
|
392
|
-
type_id: TypeId # Index into TypeRegistry
|
|
393
|
-
visibility: Visibility
|
|
394
|
-
definition_span: SourceSpan
|
|
395
|
-
|
|
396
|
-
struct ValueBinding:
|
|
397
|
-
type_id: TypeId # Index into TypeRegistry
|
|
398
|
-
kind: ValueKind
|
|
399
|
-
is_mutable: bool
|
|
400
|
-
definition_span: SourceSpan
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
Module-level scope construction follows this order (same as the Ruby checker's phase
|
|
404
|
-
pipeline):
|
|
405
|
-
|
|
406
|
-
1. Install builtin types (bool, int, str, etc.)
|
|
407
|
-
2. Install imports (populate from imported module bindings)
|
|
408
|
-
3. Declare named types (register struct/variant/enum names — enables forward references)
|
|
409
|
-
4. Resolve type aliases and aggregate fields
|
|
410
|
-
5. Declare functions (register signatures — enables forward calls)
|
|
411
|
-
6. Type-check function bodies
|
|
412
|
-
|
|
413
|
-
---
|
|
414
|
-
|
|
415
|
-
## 7. Module Loading and Visibility
|
|
416
|
-
|
|
417
|
-
The Ruby compiler's module infrastructure (`module_loader.rb`, `module_binder.rb`,
|
|
418
|
-
`module_path_resolver.rb`, `module_roots.rb`) maps to three mtc modules:
|
|
419
|
-
|
|
420
|
-
**`module_path_resolver.mt`** — Given `import std.hash`, resolves to the filesystem
|
|
421
|
-
path `std/hash.mt`. Handles platform-specific variants (`name.linux.mt`),
|
|
422
|
-
`package.source_root` resolution, and in-memory source overrides.
|
|
423
|
-
|
|
424
|
-
**`module_loader.mt`** — Loads and analyzes a module and its transitive imports.
|
|
425
|
-
Returns a `ModuleGraph` containing all parsed and analyzed modules. Handles circular
|
|
426
|
-
imports via forward-declaration bindings, matching the Ruby compiler's two-pass
|
|
427
|
-
strategy.
|
|
428
|
-
|
|
429
|
-
**`module_binder.mt`** — Given a module's `SymbolTable` and `Visibility` annotations,
|
|
430
|
-
produces a public-only view that downstream modules can import. Partitions types,
|
|
431
|
-
values, functions, methods, and interface implementations into public/private sets.
|
|
432
|
-
|
|
433
|
-
---
|
|
434
|
-
|
|
435
|
-
## 8. Milk Tea Feature Mapping
|
|
436
|
-
|
|
437
|
-
| Milk Tea Feature | Compiler Use |
|
|
438
|
-
|---|---|
|
|
439
|
-
| **`variant`** | AST nodes, HIR nodes, LIR expressions, Type representation, DiagnosticKind |
|
|
440
|
-
| **`enum`** | TokenKind, BinOp, UnaryOp, AssignOp, Visibility, PlatformTarget, IntWidth, FloatWidth |
|
|
441
|
-
| **`flags`** | Token modifier flags (is_keyword, is_literal, is_assignment_start) |
|
|
442
|
-
| **`struct`** | Arena, SymbolTable, Scope, Binding, SourceSpan, CompileOptions, Pipeline, Module |
|
|
443
|
-
| **`Result[T, E]` + `?`** | Every pass return type; error propagation through the pipeline |
|
|
444
|
-
| **`Option[T]`** | Optional AST children (else branch, initializer, type annotation, import alias) |
|
|
445
|
-
| **`Vec[T]`** | Token stream, AST child vectors, declaration lists, statement blocks, diagnostic list |
|
|
446
|
-
| **`Map[K,V]`** | Symbol table scopes, type registry index, monomorphization cache, import table |
|
|
447
|
-
| **`Set[T]`** | Used-name tracking, reachability analysis, live-variable sets |
|
|
448
|
-
| **`interface` + `dyn[I]`** | Pass trait (`interface Pass: function run(ctx) -> Result[...]`), AST visitor dispatch |
|
|
449
|
-
| **`const function`** | `size_of(T)`, `align_of(T)`, `offset_of(T, field)` — type layout at compile time |
|
|
450
|
-
| **`when` / `inline if`** | Platform-conditional codegen (linux vs windows vs wasm target dispatch) |
|
|
451
|
-
| **`inline for` + `fields_of`** | Reflective struct field iteration for comparison, hash, format code generation |
|
|
452
|
-
| **`match`** | AST traversal, token dispatch in lexer, operator dispatch, pattern matching on IR nodes |
|
|
453
|
-
| **`array[T,N]`** | Fixed-size lookup tables (operator precedence, keyword trie, char classification) |
|
|
454
|
-
| **`span[T]`** | Zero-copy views over source text, token slices for diagnostics |
|
|
455
|
-
| **`str` / `cstr`** | Source text, identifiers, file paths; C ABI strings for external function calls |
|
|
456
|
-
| **`proc(...)`** | Per-pass callbacks (e.g., `ResolveIdentifier` in the const evaluator) |
|
|
457
|
-
| **`fn(...)`** | Function pointers for static dispatch tables (no-capture callbacks, vtable entries) |
|
|
458
|
-
| **`defer`** | Resource cleanup (file handles, arena deallocation, temp file removal) |
|
|
459
|
-
| **`own[T]`** | Owning heap pointers for module-level data (file contents, cached artifacts) |
|
|
460
|
-
| **`str_buffer[N]`** | C source assembly (building output line by line, no heap allocation per fragment) |
|
|
461
|
-
| **`static_assert`** | Compile-time invariants (type sizes, alignment, field offsets, struct layout) |
|
|
462
|
-
| **`@[test]` + `std.testing`** | Unit tests for lexer, parser, type checker, lowering passes |
|
|
463
|
-
| **`emit`** | Compile-time helpers generated from `const function` (codegen support) |
|
|
464
|
-
| **`SpatialGrid[T]`** | Future: incremental recompilation tracking (dirty region queries) |
|
|
465
|
-
|
|
466
|
-
The following features are valid Milk Tea but are **not used in v1** of mtc — they
|
|
467
|
-
target performance and parallelism, which are v2 concerns:
|
|
468
|
-
|
|
469
|
-
| Deferred Feature | Future Use |
|
|
470
|
-
|---|---|
|
|
471
|
-
| **`parallel for`** | Type-checking independent functions concurrently (no data dependencies) |
|
|
472
|
-
| **`parallel:`** | Concurrent lexing/parsing of independent imported modules |
|
|
473
|
-
| **`async function` + `await`** | LSP request handling (non-blocking I/O), parallel compilation orchestration |
|
|
474
|
-
| **`atomic[T]`** | Shared diagnostic counters across parallel passes |
|
|
475
|
-
| **`SoA[T, N]`** | Hot-path data layouts (cache-friendly token streams in lexer) |
|
|
476
|
-
| **`RingBuffer[T]`** | Work-stealing queues for parallel compilation |
|
|
477
|
-
|
|
478
|
-
---
|
|
479
|
-
|
|
480
|
-
## 9. C Runtime Helpers
|
|
481
|
-
|
|
482
|
-
The C backend must emit runtime support code that the generated C depends on. Rather
|
|
483
|
-
than linking a pre-compiled library, the self-hosted compiler emits helpers inline
|
|
484
|
-
into the generated C file (matching the Ruby compiler's approach):
|
|
485
|
-
|
|
486
|
-
| Helper | Purpose |
|
|
487
|
-
|---|---|
|
|
488
|
-
| `mt_fatal(message, expr, file, line)` | Abort with formatted error (used by bounds checks, `unimplemented`, `fatal()`) |
|
|
489
|
-
| `mt_format_*` (int, float, str, bool, hex, bin, oct) | Format primitives to `string.String` sinks (format string lowering) |
|
|
490
|
-
| `mt_async_frame_*` | Task frame allocation and state management (async CPS lowering) |
|
|
491
|
-
| `mt_proc_retain` / `mt_proc_release` | Ref-counted proc capture lifecycle |
|
|
492
|
-
| `mt_event_*` | Fixed-capacity listener arrays, emit dispatch, subscription management |
|
|
493
|
-
| `mt_loop_N` counters | Per-function loop guard injection (50M iteration cap, catches hangs) |
|
|
494
|
-
|
|
495
|
-
The `backend/runtime.mt` module is a collection of `const` string literals — one
|
|
496
|
-
per helper — selected for emission based on feature detection during the LIR walk.
|
|
497
|
-
|
|
498
|
-
---
|
|
499
|
-
|
|
500
|
-
## 10. Testing Strategy
|
|
501
|
-
|
|
502
|
-
Milk Tea has a built-in testing framework (`std.testing`, `@[test]` attribute, `mtc
|
|
503
|
-
test` runner). The self-hosted compiler's test suite uses this framework:
|
|
504
|
-
|
|
505
|
-
```mt
|
|
506
|
-
import std.testing as t
|
|
507
|
-
|
|
508
|
-
@[test]
|
|
509
|
-
function test_lex_keywords() -> t.Check:
|
|
510
|
-
let result = Lexer.lex("function if else return", "test.mt")
|
|
511
|
-
let tokens = result?
|
|
512
|
-
t.expect_equal[ptr_uint](tokens.len(), 4)?
|
|
513
|
-
return t.ok()
|
|
514
|
-
```
|
|
515
|
-
|
|
516
|
-
Test categories:
|
|
517
|
-
|
|
518
|
-
| Directory | Tests |
|
|
519
|
-
|---|---|
|
|
520
|
-
| `src/tests/lexer_test.mt` | Keyword recognition, char literals, string escapes, number formats, heredocs |
|
|
521
|
-
| `src/tests/parser_test.mt` | AST structure verification for each declaration and statement form |
|
|
522
|
-
| `src/tests/semantic_test.mt` | Type checking, name resolution, interface conformance, generic instantiation |
|
|
523
|
-
| `src/tests/type_test.mt` | Type equality, subtype checks, layout computation, type registiry operations |
|
|
524
|
-
| `src/tests/roundtrip_test.mt` | Compile .mt → C → compile C → run → assert exit code 0 |
|
|
525
|
-
|
|
526
|
-
The `tests/` directory (at the project root, outside `src/`) holds canary `.mt`
|
|
527
|
-
programs that exercise full language features. These are compiled by `mtc test` and
|
|
528
|
-
their exit codes verified against expected values. They also serve as the corpus for
|
|
529
|
-
self-compilation verification (Stage 5 bootstrap).
|
|
530
|
-
|
|
531
|
-
---
|
|
532
|
-
|
|
533
|
-
## 11. Bootstrapping Strategy
|
|
534
|
-
|
|
535
|
-
### Stage 1: Lex + Parse Only (Subset)
|
|
536
|
-
|
|
537
|
-
Write a lexer and parser in Milk Tea that can parse Milk Tea source and produce an AST.
|
|
538
|
-
Test by compiling with the Ruby `mtc` and running against the existing test corpus.
|
|
539
|
-
No semantic analysis yet.
|
|
540
|
-
|
|
541
|
-
- **Verification**: AST pretty-print round-trips on `examples/language_baseline.mt`
|
|
542
|
-
- **Files**: `token.mt`, `lexer/*.mt`, `ast.mt`, `parser/*.mt`, `diagnostics.mt`
|
|
543
|
-
|
|
544
|
-
### Stage 2: Semantic Analysis
|
|
545
|
-
|
|
546
|
-
Add name resolution, type checking, and monomorphization. Generics are resolved to
|
|
547
|
-
concrete instantiations during HIR construction. The output HIR need not lower to C
|
|
548
|
-
yet, just pass checking.
|
|
549
|
-
|
|
550
|
-
- **Verification**: All `examples/*.mt` files type-check without errors against `std/`
|
|
551
|
-
- **Files**: `hir.mt`, `symbol_table.mt`, `type_system/*.mt`, `semantic/*.mt`,
|
|
552
|
-
`module_loader.mt`, `module_path_resolver.mt`, `module_binder.mt`
|
|
553
|
-
|
|
554
|
-
### Stage 3: Lowering + C Backend
|
|
555
|
-
|
|
556
|
-
Add desugaring, CPS transform, and C code emission including runtime helpers.
|
|
557
|
-
|
|
558
|
-
- **Verification**: `examples/language_baseline.mt` produces a C file that compiles
|
|
559
|
-
with gcc/clang and produces identical runtime behavior to the Ruby `mtc` compiled
|
|
560
|
-
version
|
|
561
|
-
- **Files**: `lir.mt`, `lowering/*.mt`, `backend/c_backend.mt`, `backend/runtime.mt`
|
|
562
|
-
|
|
563
|
-
### Stage 4: Full Language
|
|
564
|
-
|
|
565
|
-
Implement every remaining feature (async CPS, `parallel for`, events, `emit`, `dyn[T]`,
|
|
566
|
-
struct patterns, format strings, etc.) until `mtc` passes the full test corpus. This is
|
|
567
|
-
the long tail — roughly 70% of the effort targets the last 30% of features.
|
|
568
|
-
|
|
569
|
-
- **Verification**: All `examples/*.mt` and `std/` modules compile and run correctly
|
|
570
|
-
- **Files**: `lowering/cps.mt`, `lowering/desugar.mt` (full)
|
|
571
|
-
|
|
572
|
-
### Stage 5: Self-Compile
|
|
573
|
-
|
|
574
|
-
At this point, `mtc` compiled by the Ruby `mtc` can compile `mtc`'s own source:
|
|
575
|
-
|
|
576
|
-
```sh
|
|
577
|
-
# First bootstrap: Ruby mtc compiles mtc
|
|
578
|
-
ruby-mtc build projects/mtc → ./build/mtc
|
|
579
|
-
|
|
580
|
-
# Second bootstrap: mtc compiles itself
|
|
581
|
-
./build/mtc build projects/mtc → ./build/mtc2
|
|
582
|
-
|
|
583
|
-
# Verify: the two binaries produce identical C for the full compiler
|
|
584
|
-
diff <(./build/mtc compile projects/mtc --no-cache) \
|
|
585
|
-
<(./build/mtc2 compile projects/mtc --no-cache)
|
|
586
|
-
```
|
|
587
|
-
|
|
588
|
-
Once identical output is achieved, the Ruby compiler is retired from the mtc build
|
|
589
|
-
chain. The self-hosted compiler can evolve independently.
|
|
590
|
-
|
|
591
|
-
### Stage 6: Secondary Backend (Future)
|
|
592
|
-
|
|
593
|
-
Once `mtc` is self-hosting, target a secondary backend — LLVM via the C API using
|
|
594
|
-
`external function` bindings, or directly emit machine code. This unlocks native
|
|
595
|
-
compilation speed, debug info (DWARF), and incremental compilation.
|
|
596
|
-
|
|
597
|
-
---
|
|
598
|
-
|
|
599
|
-
## 12. Implementation Risks and Mitigations
|
|
600
|
-
|
|
601
|
-
| Risk | Mitigation |
|
|
602
|
-
|---|---|
|
|
603
|
-
| **Bootstrapping gap**: Milk Tea lacks features that `mtc` needs | Use the Ruby `mtc` as a "supercompiler" — if `mtc` hits a compiler bug in the Ruby compiler during bootstrap, fix the Ruby compiler first. |
|
|
604
|
-
| **Performance of self-compiled code**: The C backend generates naive C; self-compiled `mtc` may be 5-10x slower | Optimize the generated C in the C backend first (fewer temporaries, better loop structure). LLVM backend eliminates this entirely. |
|
|
605
|
-
| **Parser complexity**: The Ruby parser is ~4200 lines of hand-written recursive descent | Start with a clean implementation targeting only the Milk Tea grammar subset needed for `mtc` itself. Avoid parsing external files initially. |
|
|
606
|
-
| **Debugging the self-hosted compiler**: Debugging a compiler compiled by itself is a hall-of-mirrors | Embed verbose tracing behind a `--debug-passes` flag. Use `static_assert` liberally for internal invariants. |
|
|
607
|
-
| **45K LOC of Ruby**: The Ruby compiler is large and deeply integrated | `mtc` is a clean-room rewrite. It does not need to match every feature from day one — only the subset needed to bootstrap itself. |
|
|
608
|
-
| **Standard library dependency**: `mtc` will import `std.*` heavily | The Ruby `mtc` already compiles `std/`. This is the least risky dependency — treat it as a given and focus on the compiler logic. |
|
|
609
|
-
| **Arena exhaustion**: Arena-backed `Vec`s can grow unboundedly during IR construction | Each IR stage gets a fresh arena per compilation unit. Large files may trigger `Vec` resizes but cannot silently corrupt — `Vec` handles its own growable storage. |
|
|
610
|
-
|
|
611
|
-
---
|
|
612
|
-
|
|
613
|
-
## 13. First Files to Write (Ordered)
|
|
614
|
-
|
|
615
|
-
1. **`src/mtc/diagnostics.mt`** — `Diagnostic`, `DiagnosticEngine`, `SourceSpan`. Every
|
|
616
|
-
other module depends on these types for error reporting.
|
|
617
|
-
|
|
618
|
-
2. **`src/mtc/token.mt`** — `TokenKind` enum, `Token` struct. No dependencies.
|
|
619
|
-
Verifiable by printing a hardcoded token list.
|
|
620
|
-
|
|
621
|
-
3. **`src/mtc/ast.mt`** — All AST variant and arena types. Depends on `token.mt` and
|
|
622
|
-
`diagnostics.mt`. Defines the data model for the entire compiler.
|
|
623
|
-
|
|
624
|
-
4. **`src/mtc/lexer/lexer.mt`** + **`char_tables.mt`** — Lexer. Outputs `Vec[Token]`.
|
|
625
|
-
Verifiable by dumping tokens for any `.mt` file.
|
|
626
|
-
|
|
627
|
-
5. **`src/mtc/parser/*.mt`** — Parser. Outputs `AstArena` (containing all AST nodes).
|
|
628
|
-
Verifiable by pretty-printing the AST.
|
|
629
|
-
|
|
630
|
-
6. **`src/mtc/type_system/types.mt`** — `Type` variant and `TypeRegistry`.
|
|
631
|
-
|
|
632
|
-
7. **`src/mtc/symbol_table.mt`** — `SymbolTable`, `Scope`, and binding structs.
|
|
633
|
-
|
|
634
|
-
8. **`src/mtc/module_path_resolver.mt`** — Import path resolution.
|
|
635
|
-
|
|
636
|
-
9. **`src/mtc/hir.mt`** — HIR type definitions.
|
|
637
|
-
|
|
638
|
-
10. **`src/mtc/semantic/*.mt`** — Semantic checker (AST → HIR), including
|
|
639
|
-
`monomorphize.mt`.
|
|
640
|
-
|
|
641
|
-
11. **`src/mtc/module_loader.mt`** + **`module_binder.mt`** — Module loading and
|
|
642
|
-
visibility.
|
|
643
|
-
|
|
644
|
-
12. **`src/mtc/lir.mt`** — LIR type definitions.
|
|
645
|
-
|
|
646
|
-
13. **`src/mtc/lowering/*.mt`** — Lowering passes (HIR → LIR).
|
|
647
|
-
|
|
648
|
-
14. **`src/mtc/backend/c_backend.mt`** + **`runtime.mt`** — C code and runtime emission.
|
|
649
|
-
|
|
650
|
-
15. **`src/main.mt`** + **`pipeline.mt`** — CLI entry point and pipeline orchestration.
|
|
651
|
-
|
|
652
|
-
Each file is independently testable as soon as its upstream dependencies are complete.
|
|
653
|
-
|
|
654
|
-
---
|
|
655
|
-
|
|
656
|
-
## 14. Key Architecture Decisions
|
|
657
|
-
|
|
658
|
-
### Arena + Handle over `ref[T]` in containers
|
|
659
|
-
|
|
660
|
-
Milk Tea's `ref[T]` restriction on containers forces the arena pattern. This is not a
|
|
661
|
-
workaround — it is the correct design. Arena-allocated AST/HIR nodes with index handles
|
|
662
|
-
yield better cache locality than pointer-chasing graphs, enable bulk deallocation, and
|
|
663
|
-
trivially support serialization (copy the `Vec[Node]` — done). The same pattern is used
|
|
664
|
-
by `rustc`, `swiftc`, and `clang`.
|
|
665
|
-
|
|
666
|
-
### Monomorphization during HIR construction, not during lowering
|
|
667
|
-
|
|
668
|
-
Generics are resolved to concrete types during the semantic pass (AST → HIR). This is
|
|
669
|
-
simpler than the Ruby compiler's inline-lowering approach and sufficient for a
|
|
670
|
-
single-compilation-unit compiler. The HIR is fully resolved — every type variable is
|
|
671
|
-
replaced, every generic call has concrete type arguments. This makes the lowering pass a
|
|
672
|
-
pure transformation with no type-level work.
|
|
673
|
-
|
|
674
|
-
If separate compilation of generics becomes necessary later, a dedicated
|
|
675
|
-
monomorphization pass can be inserted between HIR construction and lowering. The IR
|
|
676
|
-
separation already accommodates this.
|
|
677
|
-
|
|
678
|
-
### (Sugar) Inline methods in struct bodies
|
|
679
|
-
|
|
680
|
-
`mtc`'s Ruby compiler supports defining methods directly inside struct bodies —
|
|
681
|
-
no separate `extending` block required. The parser desugars inline `function` /
|
|
682
|
-
`editable function` / `static function` declarations into synthetic `ExtendingBlock`
|
|
683
|
-
nodes targeting the enclosing struct. This is pure syntactic sugar with zero
|
|
684
|
-
semantic or code-generation impact.
|
|
685
|
-
|
|
686
|
-
```mt
|
|
687
|
-
# These are equivalent:
|
|
688
|
-
struct Counter:
|
|
689
|
-
value: int
|
|
690
|
-
|
|
691
|
-
function read() -> int:
|
|
692
|
-
return this.value
|
|
693
|
-
|
|
694
|
-
# same as:
|
|
695
|
-
struct Counter:
|
|
696
|
-
value: int
|
|
697
|
-
|
|
698
|
-
extending Counter:
|
|
699
|
-
function read() -> int:
|
|
700
|
-
return this.value
|
|
701
|
-
```
|
|
702
|
-
|
|
703
|
-
Type parameters declared on the struct are in scope for all inline methods,
|
|
704
|
-
enabling generic methods without re-declaring type params.
|
|
705
|
-
|
|
706
|
-
```mt
|
|
707
|
-
struct Box[T]:
|
|
708
|
-
inner: T
|
|
709
|
-
|
|
710
|
-
function unwrap() -> T:
|
|
711
|
-
return this.inner
|
|
712
|
-
```
|
|
713
|
-
|
|
714
|
-
The self-hosted plan uses this sugar throughout for cleaner code layout — struct
|
|
715
|
-
definitions keep their core methods visually adjacent.
|
|
716
|
-
|
|
717
|
-
### C backend as primary target
|
|
718
|
-
|
|
719
|
-
The C backend is the pragmatic first target: it reuses the Ruby compiler's C runtime
|
|
720
|
-
conventions, is portable to any platform with a C compiler, and lets the self-hosted
|
|
721
|
-
compiler bootstrap on any architecture. LLVM is the strategic second target for native
|
|
722
|
-
compilation speed.
|
|
723
|
-
|
|
724
|
-
### Diagnostics-first design (LSP-ready, not LSP-included)
|
|
725
|
-
|
|
726
|
-
The `DiagnosticEngine` uses byte-offset `SourceSpan` values (not line:column pairs with
|
|
727
|
-
string interpolation), suitable for both CLI output and LSP protocol position encoding.
|
|
728
|
-
Error recovery in the parser produces a best-effort AST even with syntax errors. The
|
|
729
|
-
LSP server itself is deferred to v2, but the data model is designed to accommodate it
|
|
730
|
-
without refactoring.
|
|
731
|
-
|
|
732
|
-
### Symmetric module structure with the Ruby compiler
|
|
733
|
-
|
|
734
|
-
`module_loader.mt`, `module_path_resolver.mt`, and `module_binder.mt` mirror the Ruby
|
|
735
|
-
compiler's module infrastructure. This is intentional — the Ruby compiler's module
|
|
736
|
-
system has been hardened through real-world use and its design is sound. The self-hosted
|
|
737
|
-
compiler reimplements the same semantics in Milk Tea, not a new design.
|