mt-lang 0.3.26 → 0.3.27

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.
@@ -0,0 +1,768 @@
1
+ # Self-Host Plan
2
+
3
+ Status: **Planning** — see `docs/self-host-progress.md` for step-by-step tracking.
4
+
5
+ ## 0. Goal
6
+
7
+ Port the Milk Tea compiler from Ruby to Milk Tea. The result must achieve a
8
+ **fixed point**: stage2-generated C is byte-identical to stage3-generated C,
9
+ proving the compiler is self-consistent. The working reference commit from the
10
+ previous self-host incarnation that achieved this is `a28545a7`.
11
+
12
+ Ruby host: `v0.3.26`, ~86K lines across 256 files.
13
+ Scope: core compiler pipeline (lex → parse → sema → lower → C backend) plus
14
+ build tooling, bootstrap, and test suite. LSP, DAP, bindgen, and the packages
15
+ subsystem are deferred to post-fixed-point phases.
16
+
17
+ ## 1. Architecture
18
+
19
+ ### 1.1 Pipeline
20
+
21
+ ```
22
+ Source (.mt) → Lexer → Parser → Module Loader → Semantic Analyzer → Control Flow → Lowering → IR → C Backend → C code
23
+ ↑ ↑
24
+ Module Binder Compile-Time Eval
25
+ Type System Intrinsics
26
+ AST / Token / IR
27
+ ```
28
+
29
+ | Stage | Ruby source | Self-host module | Lines (Ruby) |
30
+ |-------|-------------|-----------------|-------------|
31
+ | AST / Token / CST / Keywords | `ast.rb` (369), `token.rb` (43), `cst.rb` (64), `keywords.rb` (97) | `mtc/parser/ast.mt`, `mtc/lexer/token.mt`, `mtc/lexer/keywords.mt` | 573 |
32
+ | Lexer | `lexer.rb` (427) + `lexer/` (949) | `mtc/lexer/` | 1,376 |
33
+ | Parser | `parser.rb` (442) + `parser/` (2,788) | `mtc/parser/` | 3,230 |
34
+ | Type System | `types.rb` (1,727) + `types/` (1,448) | `mtc/semantic/types.mt` | 3,175 |
35
+ | Module Loader & Resolver | `module_loader.rb` (717), `module_path_resolver.rb` (152), `module_roots.rb` (86) | `mtc/loader/` | 955 |
36
+ | Module Binder & Bindings | `module_binder.rb` (187), `bindings.rb` (13) + `bindings/` (148) | `mtc/loader/binder.mt` | 348 |
37
+ | Semantic Analyzer | `semantic_analyzer.rb` (355) + `semantic_analyzer/` (10,262) | `mtc/semantic/` | 10,617 |
38
+ | Control Flow | `control_flow.rb` (34) + `control_flow/` (1,148) | `mtc/semantic/control_flow/` | 1,182 |
39
+ | Compile-Time Evaluator | `compile_time.rb` (533) | `mtc/semantic/compile_time.mt` | 533 |
40
+ | Intrinsics | `intrinsics.rb` (87), `flow_scope.rb` (13) | `mtc/semantic/intrinsics.mt` | 100 |
41
+ | Lowering | `lowering.rb` (378) + `lowering/` (15,201) | `mtc/lowering/` | 15,579 |
42
+ | IR | `ir.rb` (88) | `mtc/ir.mt` | 88 |
43
+ | C Backend | `c_backend.rb` (322) + `c_backend/` (5,826) | `mtc/c_backend/` | 6,148 |
44
+ | Pretty Printer | `pretty_printer.rb` (17) + `pretty_printer/` (1,292) | `mtc/pretty_printer/` | 1,309 |
45
+ | S-expression Dumper | `sexpr_dumper.rb` (344) | `mtc/sexpr_dumper.mt` | 344 |
46
+ | Prelude / Async Installers | `prelude_installer.rb` (29), `async_runtime_installer.rb` (26) | merged into module loader / lowering | 55 |
47
+ | Debug Info Formatter | `debug_info_formatter.rb` (454) | `mtc/debug_info.mt` | 454 |
48
+ | CLI Args + Shared Helpers | in `cli.rb` (900+) | folded into `mtc/main.mt` | ~900 |
49
+ | CLI sub-commands | `cli/commands/` (22 files) | folded into `mtc/main.mt` | ~2,500 |
50
+ | Build / Run / Cache | `build.rb`, `build_cache.rb`, `run.rb` | `mtc/build.mt`, `mtc/build_cache.mt` | ~2,500 |
51
+ | **Core total** | **45,268** | | |
52
+
53
+ ### 1.2 Module Structure
54
+
55
+ ```
56
+ projects/mtc/
57
+ src/mtc/
58
+ main.mt — CLI entrypoint and command dispatch
59
+ build.mt — build orchestration (compile → C → binary)
60
+ build_cache.mt — incremental compilation cache
61
+ version_info.mt — git revision stamp placeholder
62
+ ast.mt — AST node definitions (shared by parser, sema, lowering, C backend)
63
+ ir.mt — IR data types (pure data, no logic)
64
+ sexpr_dumper.mt — S-expression serializer for tokens, AST, types, IR
65
+ lexer/
66
+ token_kinds.mt — TokenKind enum
67
+ token.mt — Token struct, LexDiagnostic
68
+ keywords.mt — keyword lookup table
69
+ lexer.mt — core lexer (lines, tokens, indentation)
70
+ parser/
71
+ token_stream.mt — SyntaxTokenStream wrapper
72
+ state.mt — parser state, diagnostics
73
+ literal_parsing.mt — number/string/char literal parsing
74
+ parser.mt — recursive-descent parser
75
+ loader/
76
+ path_resolver.mt — module path resolution with platform variants
77
+ errors.mt — error types
78
+ binder.mt — ModuleBinding construction from Analysis
79
+ module_loader.mt — transitive import loading, cycle handling, sema orchestration
80
+ semantic/
81
+ types.mt — Type variant, predicates, layout helpers, rendering
82
+ scope.mt — lexical scope tracking
83
+ expressions.mt — expression type inference helpers
84
+ diagnostics.mt — diagnostic struct
85
+ emit_expansion.mt — emit statement expansion
86
+ type_compatibility.mt — assignability, coercion, narrowing rules
87
+ intrinsics.mt — built-in callable handler (fatal, ref_of, read, size_of, etc.)
88
+ compile_time.mt — const/const function evaluator
89
+ analyzer.mt — semantic analysis (declarations + function bodies)
90
+ control_flow/
91
+ builder.mt — CFG construction
92
+ definite_assignment.mt — definite-assignment analysis
93
+ reachability.mt — return-on-all-paths, termination
94
+ nullability.mt — nullable flow narrowing
95
+ constprop.mt — constant propagation
96
+ lowering/
97
+ utils.mt — C name mangling (~20 functions), type classification helpers, range/match utilities
98
+ async.mt — async CPS lowering + root-main synthesis
99
+ lowering.mt — AST → IR transformation (main lowering pass, single module initially)
100
+ c_backend/
101
+ c_backend.mt — C code generation from IR; includes C keyword sanitization (C_KEYWORDS + sanitize_c_identifier)
102
+ pretty_printer/
103
+ ast_formatter.mt — AST → formatted text output
104
+ ir_formatter.mt — IR → text output
105
+ test/
106
+ lexer_test.mt
107
+ parser_test.mt
108
+ semantic_test.mt
109
+ lowering_test.mt
110
+ c_backend_test.mt
111
+ module_loader_test.mt
112
+ path_resolver_test.mt
113
+ ast_formatter_test.mt
114
+ scope_test.mt
115
+ linter_test.mt
116
+ fix_engine_test.mt
117
+ completion_test.mt
118
+ ```
119
+
120
+ ### 1.3 Key Data Representations
121
+
122
+ **Type system** — a `variant` (tagged union) mirroring Ruby's `Types::Base` hierarchy:
123
+
124
+ ```mt
125
+ public variant Type:
126
+ ty_primitive(name: str)
127
+ ty_str
128
+ ty_error
129
+ ty_nullable(base: ptr[Type])
130
+ ty_named(name: str, module_name: str)
131
+ ty_imported(module_name: str, name: str, args: span[Type])
132
+ ty_generic(name: str, args: span[Type])
133
+ ty_function(params: span[Type], return_type: ptr[Type], variadic: bool, is_proc: bool)
134
+ ty_tuple(elements: span[Type], field_names: Option[span[str]])
135
+ ty_var(name: str) — unresolved type variable
136
+ ty_dyn(iface: str) — dyn[InterfaceName]
137
+ ty_opaque(module_name, name)
138
+ ty_literal_int(value: long) — compile-time integer type arg
139
+ ```
140
+
141
+ **AST** — structs for non-recursive decls + `ptr` indirection for recursive nodes:
142
+
143
+ - `SourceFile` struct: module name, kind, directives, declarations span
144
+ - `TypeRef` variant: named type, generic type, function type, pointer type, etc.
145
+ - Expression variants: identifier, literal, binary, call, member access, etc.
146
+ - Statement variants: let/var decl, if, while, for, match, return, etc.
147
+
148
+ **IR** — a decoupled variant between Lowering and CBackend (mirrors `ir.rb`):
149
+
150
+ - `Expr` variant: name, member, index, call, unary, binary, conditional, literal, etc.
151
+ - `Stmt` variant: local, assignment, block, if, switch, while, for, return, etc.
152
+ - Program struct: includes, constants, globals, opaques, structs, enums, variants, functions
153
+
154
+ **Symbol tables** — `std.map.Map[str, ...]` for every lookup: functions, struct fields, type aliases, member keys, method signatures, value types, interface bindings. Keys are always `str`.
155
+
156
+ ## 2. Memory Management Strategy
157
+
158
+ The Ruby compiler has GC. The self-host must manage memory explicitly. Three
159
+ allocation tiers map to three lifetimes:
160
+
161
+ | Tier | Lifetime | Mechanism | Usage |
162
+ |------|----------|-----------|-------|
163
+ | Per-compilation | Entire `build` invocation | `std.mem.arena` (bump allocator) | Source text, AST nodes, IR nodes, type objects, most temporaries |
164
+ | Per-module | Single module parse/check | Arena mark/reset | Per-file lexer output, parser AST, sema Analysis |
165
+ | Persistent | Cross-module, whole program | `std.map.Map` + `std.vec.Vec` (owning heap) | Loader's `Program`, retained analyses, binding maps |
166
+
167
+ **Arena discipline:** At the start of each module's parse → sema → lower pass,
168
+ mark the arena. Before emitting IR for the next module, reset to that mark.
169
+ The loader retains per-module `Analysis` values in Vec/Map storage (owning
170
+ heap); everything else is arena-allocated and bulk-freed.
171
+
172
+ `std.mem.arena.Arena` provides:
173
+ - `create(capacity_bytes)` / `create_aligned(capacity_bytes, alignment)` — initialize
174
+ - `alloc_bytes(size)` / `alloc_bytes_aligned(size, alignment)` — bump-allocate
175
+ - `mark()` → `ptr_uint` — save position
176
+ - `reset(mark)` — rewind to saved mark
177
+
178
+ **String ownership:** The lexer produces token lexemes as `str` slices into the
179
+ source `string.String`. The source buffer is arena-allocated and outlives all
180
+ slices. No string copies during lexing or parsing. Diagnostic messages and
181
+ cross-module names use `string.String` (owning heap).
182
+
183
+ **Vec and Map lifecycle:** Dynamic collections used as symbol tables and IR
184
+ span sources require explicit `release()`. The convention is: the loader owns
185
+ the top-level `Program` and calls `release()` at teardown; all intermediate
186
+ Vec/Map values are arena-allocated and their release is handled by arena reset.
187
+
188
+ ## 3. Bootstrap Architecture
189
+
190
+ ### 3.1 Stage Model
191
+
192
+ ```
193
+ Stage0 (Ruby host or pre-built snapshot binary)
194
+ ↓ compiles
195
+ Stage1 (build/stage1/mtc) — carries stage0 code-gen artifacts, not reproducible
196
+ ↓ compiles
197
+ Stage2 (build/stage2/mtc) — clean, distributable artifact
198
+ ↓ compiles
199
+ Stage3 (build/stage3/mtc) — verification only
200
+
201
+ diff stage2.c stage3.c — must be empty (fixed point)
202
+ ```
203
+
204
+ All stages read the same in-tree `std/` sources. There is no stage-specific
205
+ standard library — stdlib is source-only.
206
+
207
+ ### 3.2 Stage0 Resolution
208
+
209
+ Resolution order in `tools/bootstrap.sh`:
210
+
211
+ 1. `--bootstrap PATH` (explicit CLI argument, highest priority)
212
+ 2. `$MTC_BOOTSTRAP` environment variable
213
+ 3. `bin/bootstrap-mtc` in the repo root (user-placed binary)
214
+ 4. `ruby -Ilib bin/mtc` (Ruby host — the development fallback)
215
+ 5. Error: "No bootstrap compiler found"
216
+
217
+ ### 3.3 Bootstrap Script
218
+
219
+ ```sh
220
+ tools/bootstrap.sh [OPTIONS]
221
+
222
+ Options:
223
+ --bootstrap PATH Path to stage0 mtc binary (default: auto-detect)
224
+ --stage {1,2,3} Build target stage (default: 3)
225
+ --no-verify Skip stage3 fixed-point check
226
+ --profile {debug,release} Build profile (default: debug)
227
+ --keep-c Save generated C files alongside binaries
228
+ -j N Parallel jobs for C compilation (default: nproc)
229
+ ```
230
+
231
+ Key invariants:
232
+ - `--no-cache` on every stage build — prevents cache poisoning across stages
233
+ - `-I .` to make repo-root `std/` visible as a module root
234
+ - Git revision injected into `version_info.mt` via `sed` before each build
235
+ - Fixed-point verification via `diff stage2.c stage3.c`
236
+ - Stage2 self-test: stage2 builds itself, producing a working binary
237
+
238
+ ### 3.4 Development Shortcuts
239
+
240
+ ```sh
241
+ # Fast iteration: just stage1, no verification
242
+ tools/bootstrap.sh --stage 1 --no-verify
243
+
244
+ # Verify only: assumes stage2 already built
245
+ tools/bootstrap.sh --verify-only
246
+
247
+ # Self-test: stage2 builds itself
248
+ tools/bootstrap.sh --self-test
249
+ ```
250
+
251
+ ## 4. Implementation Plan
252
+
253
+ ### Phase 0: Foundation (types, IR, AST, token definitions)
254
+
255
+ **Goal:** Data structures and utilities that every subsequent phase depends on.
256
+
257
+ | Step | Module | Lines (Ruby) | Verify |
258
+ |------|--------|-------------|--------|
259
+ | 0a | `lexer/token_kinds.mt` — TokenKind enum (all token types) | — | Compiles |
260
+ | 0b | `lexer/token.mt` — Token struct, TriviaToken, LexDiagnostic | 43 | Compiles |
261
+ | 0c | `lexer/keywords.mt` — keyword string → TokenKind lookup table | 97 | Unit tests |
262
+ | 0d | `ast.mt` — full AST: SourceFile, all Decl variants, TypeRef, Expr, Stmt | 369 | Compiles |
263
+ | 0e | `semantic/types.mt` — Type variant, classification predicates, rendering, layout helpers | 3,175 | Unit tests |
264
+ | 0f | `ir.mt` — IR Program, Expr, Stmt, all declaration structs. Imports `mtc.ast` (for `ast.ModuleKind`) and `mtc.semantic.types` (for `types.Type` in field/param definitions). | 88 | Compiles (no logic) |
265
+ | 0g | `sexpr_dumper.mt` — S-expression serialization for tokens, AST, types, and IR; byte-identical output vs Ruby. Used by `mtc lex --sexpr`, `mtc parse --sexpr`, and `mtc lower` for diff-based verification at every phase. | 344 | Diff vs Ruby S-expr output |
266
+
267
+ **Exit criteria:** All types compile under `mtc check`. The types module handles
268
+ the full type model: primitive lookup tables (integer widths, float widths),
269
+ type classification (`is_numeric`, `is_bool`, `is_void`, `contains_error`),
270
+ rendering (`type_to_string`), and layout helpers from `types/layout.rb`.
271
+
272
+ ### Phase 1: Lexer
273
+
274
+ **Goal:** Byte-identical token stream vs Ruby lexer.
275
+
276
+ | Step | Module | Verify |
277
+ |------|--------|--------|
278
+ | 1a | `lexer.mt` — character classification, newline/indent handling, line continuation | `lexer_test.mt` |
279
+ | 1b | Identifier and keyword lexing, operator recognition | `lexer_test.mt` |
280
+ | 1c | Number literals (decimal, hex, binary, float, suffixes) | `lexer_test.mt` |
281
+ | 1d | String literals, c-strings, character literals, escapes | `lexer_test.mt` |
282
+ | 1e | Heredocs (plain, c-prefixed, f-prefixed), format strings | `lexer_test.mt` |
283
+ | 1f | Indent/dedent stack, EOF emission, `lex_reporting` with error collection | `lexer_test.mt` |
284
+
285
+ **Exit criteria:** `mtc lex <file>` produces byte-identical token output vs
286
+ Ruby for `examples/language_baseline.mt` and all test fixtures.
287
+
288
+ ### Phase 2: Parser
289
+
290
+ **Goal:** Byte-identical AST vs Ruby parser.
291
+
292
+ | Step | Module | Verify |
293
+ |------|--------|--------|
294
+ | 2a | `token_stream.mt` — token stream abstraction | Compiles |
295
+ | 2b | `state.mt` — parser state, diagnostic collection | Compiles |
296
+ | 2c | `literal_parsing.mt` — number/string/char/heredoc literal AST construction | Parser tests |
297
+ | 2d | `ast.mt` — full AST node definitions (SourceFile, Decl, TypeRef, Expr, Stmt) | Compiles |
298
+ | 2e | `parser.mt` — type parsing, attribute parsing | Parser tests |
299
+ | 2f | Expression parsing (precedence climbing), call, member access, specialization | Parser tests |
300
+ | 2g | Statement parsing (let, var, if, while, for, match, return, defer, etc.) | Parser tests |
301
+ | 2h | Declaration parsing (function, struct, enum, variant, interface, etc.) | Parser tests |
302
+ | 2i | Top-level parsing, import resolution, `parse_source_file` | Parser tests |
303
+ | 2j | Error recovery, `parse_collecting_errors` | Parser tests |
304
+
305
+ **Exit criteria:** `mtc parse <file>` produces byte-identical AST vs Ruby for
306
+ all examples and test fixtures. Error recovery matches Ruby for malformed input.
307
+
308
+ ### Phase 3: Path Resolver
309
+
310
+ **Goal:** Platform-specific file resolution, import path → filesystem path
311
+ mapping. This module has **no dependency on the semantic analyzer** — it only
312
+ uses `std.fs`, `std.path`, `std.string`. It can be ported immediately after the
313
+ parser, before the full sema stack.
314
+
315
+ | Step | Module | Verify |
316
+ |------|--------|--------|
317
+ | 3a | `path_resolver.mt` — `Platform` enum, `platform_suffix`, `resolve_source_path`, `resolve_module_path`, `infer_module_name` | `path_resolver_test.mt` |
318
+
319
+ **Exit criteria:** Platform-specific file resolution (`.linux.mt`, `.wasm.mt`)
320
+ works; import path `a.b.c` resolves to correct filesystem path.
321
+
322
+ ### Phase 4: Semantic Analyzer
323
+
324
+ **Goal:** Type-check all declarations and function bodies. Error diagnostics
325
+ match Ruby for all examples and test fixtures. Ruby reference: `semantic_analyzer.rb` (355) + 16 subfiles (10,262) = 10,617 lines.
326
+
327
+ | Step | Module | Verify |
328
+ |------|--------|--------|
329
+ | 4a | `scope.mt` — lexical scope stack, name resolution helpers | `scope_test.mt` |
330
+ | 4b | `diagnostics.mt` — diagnostic struct, formatting | Compiles |
331
+ | 4c | `type_compatibility.mt` — assignability, coercion, narrowing, widening rules | Semantic tests |
332
+ | 4d | `expressions.mt` — type inference for expressions (read/write target resolution) | Semantic tests |
333
+ | 4e | `intrinsics.mt` — built-in callable handler: classifies calls to `fatal`, `ref_of`, `ptr_of`, `read`, `size_of`, `align_of`, `offset_of`, `reinterpret`, `zero`, `default`, `hash`, `equal`, `order`, `get`, `adapt`, etc. and resolves their types | Semantic tests |
334
+ | 4f | `compile_time.mt` — const/const function evaluator: evaluates integer/float/boolean/string literals, unary/binary ops, if-expressions, const function calls, size_of/align_of/offset_of, handles `ReturnValue` early exit | Semantic tests |
335
+ | 4g | `analyzer.mt` — top-level declaration pass (structs, enums, functions, interfaces, events) | Semantic tests |
336
+ | 4h | Function body checking (statements, return type, `?` propagation, guard narrowing) | Semantic tests |
337
+ | 4i | Interface conformance checking, generic instantiation, constraints | Semantic tests |
338
+ | 4j | `emit_expansion.mt` — emit statement lowering within const functions | Semantic tests |
339
+
340
+ **Exit criteria:** `mtc check <file>` produces byte-identical error diagnostics
341
+ vs Ruby for `examples/language_baseline.mt` and all test fixtures. Clean
342
+ programs produce zero diagnostics.
343
+
344
+ ### Phase 4a: Control Flow Analysis
345
+
346
+ **Goal:** Validate control flow correctness. Ruby reference: `control_flow.rb` (34) +
347
+ `control_flow/` (9 subfiles, 1,148 lines) = 1,182 lines. Runs after semantic
348
+ analysis on each function body.
349
+
350
+ | Step | Module | Verify |
351
+ |------|--------|--------|
352
+ | 4a1 | `builder.mt` — CFG construction from AST function bodies, block sequencing | Control flow tests |
353
+ | 4a2 | `termination.mt` / `reachability.mt` — return-on-all-paths verification, unreachable code detection | Control flow tests |
354
+ | 4a3 | `definite_assignment.mt` — mutable/immutable tracking, let vs var, guard narrowing with `else:` | Control flow tests |
355
+ | 4a4 | `nullability_flow.mt` — nullable flow narrowing: `if p != null` narrows to non-null in then-branch, `if p == null` narrows in else-branch | Control flow tests |
356
+ | 4a5 | `constant_propagation.mt` — constant propagation for `when`/`inline if` discriminant evaluation | Control flow tests |
357
+
358
+ **Exit criteria:** Byte-identical control flow diagnostics (missing return,
359
+ unreachable code) vs Ruby for all examples.
360
+
361
+ ### Phase 4b: Module Binder
362
+
363
+ ### Phase 4b: Module Binder
364
+
365
+ **Goal:** Construct `ModuleBinding` from a completed `Analysis`, filtering
366
+ public/private visibility. Depends on `analyzer.Analysis` being fully built
367
+ (Phase 4) and control flow validated (Phase 4a). Ruby reference:
368
+ `module_binder.rb` (187) + `bindings/` (148) = 335 lines.
369
+
370
+ | Step | Module | Verify |
371
+ |------|--------|--------|
372
+ | 4b1 | `binder.mt` — ModuleBinding construction: copies public FnSig, FieldEntry, type aliases, method sigs, interface info, static members, match case names, and implemented interfaces from Analysis into the binding; constructs private_* counterparts from Analysis fields not marked public | Semantic tests |
373
+
374
+ **Exit criteria:** `ModuleBinding` correctly filters public declarations.
375
+ Cross-module tests produce correct import resolution.
376
+
377
+ ### Phase 4c: Module Loader
378
+
379
+ **Goal:** Transitive import resolution, dependency ordering, cycle handling.
380
+ Depends on the parser (Phase 2), semantic analyzer (Phase 4), path resolver
381
+ (Phase 3), and binder (Phase 4b). Ruby reference: `module_loader.rb` (717 lines).
382
+
383
+ | Step | Module | Verify |
384
+ |------|--------|--------|
385
+ | 4c1 | `module_loader.mt` — `LoadedModule` struct, `LoadDiagnostic` struct, `Program` struct, `check_program()` entry point (parse all imports → topo sort → sema each → bind each → return) | `module_loader_test.mt` |
386
+ | 4c2 | Circular import handling (forward bindings → real analysis → re-check) | `module_loader_test.mt` |
387
+
388
+ **Exit criteria:** Multi-file programs with circular imports parse and order
389
+ correctly. `check_program` returns a valid `Program` with all modules analyzed.
390
+
391
+ ### Phase 5: Lowering
392
+
393
+ **Goal:** AST → IR transformation. Ruby reference: `lowering.rb` (378) +
394
+ `lowering/` (16 subfiles, 15,201 lines) = 15,579 lines. Keep as a single
395
+ module initially — sub-file splitting is a code-organization concern that can
396
+ be deferred to post-fixed-point. The previous working self-host (commit
397
+ `a28545a7`) proved this approach: a 16,498-line `lowering.mt` was correct and
398
+ maintainable.
399
+
400
+ | Step | Module | Verify |
401
+ |------|--------|--------|
402
+ | 5a | `utils.mt` — C name mangling (~20 functions: module, function, enum member, field, value, imported, external C names), type qualification, struct/enum member C name lookup | Lowering tests |
403
+ | 5b | `lowering.mt` — constant and global lowering | Lowering tests |
404
+ | 5c | Type declaration lowering (struct, enum, union, variant, opaque) | Lowering tests |
405
+ | 5d | Expression lowering (literals, binary, call, member access, index, conditional, reinterpret, specialization) | Lowering tests |
406
+ | 5e | Statement lowering (let, var, if, while, for, match, return, defer, unsafe, `?` propagation) | Lowering tests |
407
+ | 5f | Function definition lowering, entry-point synthesis, foreign function boundary | Lowering tests |
408
+ | 5g | `async.mt` — CPS transformation, frame builder, root-main synthesis for async entrypoints | Lowering tests |
409
+ | 5h | Proc closure lowering, dyn interface dispatch, str_buffer lowering, format string lowering | Lowering tests |
410
+
411
+ **Exit criteria:** `mtc lower <file>` produces byte-identical IR vs Ruby for
412
+ all examples. The lowering handles `resolved_expr_types` and
413
+ `resolved_call_kinds` from the retained Analysis for precise type resolution.
414
+
415
+ ### Phase 6: C Backend
416
+
417
+ **Goal:** IR → C source. Ruby reference: `c_backend.rb` (322) + `c_backend/`
418
+ (12 subfiles, 5,826 lines) = 6,148 lines.
419
+
420
+ | Step | Module | Verify |
421
+ |------|--------|--------|
422
+ | 6a | Preamble emission (includes, conditional helpers, runtime helpers), C keyword table (`C_KEYWORDS`) and `sanitize_c_identifier` — every field/member name emitted to C passes through this; must be called at struct/union/variant field declarations, member access, `offsetof`, designated initializers, and equality helpers — all 18 sites (see `type_declaration.rb`, `expressions.rb`, `runtime_helpers.rb`) | C backend tests |
423
+ | 6b | Type declaration emission (enum, flags, struct, union, variant, opaque, type aliases) | C backend tests |
424
+ | 6c | Expression emission (name, literal, binary, call, member, index, reinterpret) | C backend tests |
425
+ | 6d | Statement emission (block, if, switch/match, while, for, return, defer, goto/label) | C backend tests |
426
+ | 6e | Function definition emission, format string lowering, simd lowering | C backend tests |
427
+ | 6f | Prelude type emission (Option/Result structs, nullable opt types), aggregate field construction | C backend tests |
428
+ | 6g | String literal constant section, per-type equality helpers, variant equality helpers | C backend tests |
429
+
430
+ **Exit criteria:** `mtc emit-c <file>` produces byte-identical C vs Ruby for
431
+ `examples/language_baseline.mt` and all examples.
432
+
433
+ ### Phase 6a: Pretty Printer
434
+
435
+ **Goal:** AST → formatted text and IR → text for debugging and testing. Ruby
436
+ reference: `pretty_printer.rb` (17) + `pretty_printer/` (3 subfiles, 1,292
437
+ lines) = 1,309 lines.
438
+
439
+ | Step | Module | Verify |
440
+ |------|--------|--------|
441
+ | 6a1 | `ast_formatter.mt` — AST → Milk Tea source text (round-trip formatting) | `ast_formatter_test.mt` |
442
+ | 6a2 | `ir_formatter.mt` — IR → text for `mtc lower` output | Diff vs Ruby `mtc lower` |
443
+
444
+ **Exit criteria:** `mtc parse file.mt | mtc format` round-trips identically.
445
+ `mtc lower` IR text output is byte-identical vs Ruby.
446
+
447
+ ### Phase 7: Build System and CLI
448
+
449
+ **Goal:** A working compiler binary that builds itself. Ruby reference:
450
+ `tooling/cli.rb` (~900 lines), `cli/commands/` (22 files), `build.rb`,
451
+ `build_cache.rb`, `run.rb`.
452
+
453
+ **CLI dispatch** — a flat `if cmd == ...` chain in `main.mt` routing to handler
454
+ functions. Manual argv iteration, no option-parsing library. Each sub-command
455
+ (`lex`, `parse`, `check`, `lower`, `emit-c`, `build`, `run`, `test`, `format`,
456
+ `debug`, `lint`) is a function that parses its arguments from a `span[str]`.
457
+
458
+ | Step | Module | Verify |
459
+ |------|--------|--------|
460
+ | 7a | `main.mt` — CLI dispatch for `lex --sexpr`, `parse --sexpr`, `check`, `lower`, `emit-c` (no-build verification commands) | Diff S-expr output vs Ruby |
461
+ | 7b | `build.mt` — process spawning for CC, cache integration, linker flags, platform detection | Self-build |
462
+ | 7c | `build_cache.mt` — incremental compilation cache, content-hash keying, artifact storage | Unit tests |
463
+ | 7d | `main.mt` (continued) — `build`, `run`, `test`, `format`, `debug`, `lint` sub-commands | CLI smoke tests |
464
+ | 7e | `version_info.mt` — git revision stamp placeholder (replaced by `sed` in bootstrap script) | Bootstrap script |
465
+
466
+ **Exit criteria:** Stage1 builds and produces working `build/stage1/mtc`.
467
+
468
+ ### Phase 8: Bootstrap and Fixed-Point
469
+
470
+ **Goal:** The compiler is self-consistent.
471
+
472
+ | Step | Verify |
473
+ |------|--------|
474
+ | 8a | `tools/bootstrap.sh` — 3-stage build script | Stage2 builds |
475
+ | 8b | Fixed-point check: `diff stage2.c stage3.c` is empty | Byte-identical |
476
+ | 8c | Self-test: stage2 builds itself to a working binary | Binary runs |
477
+ | 8d | Test suite passes under stage2: `build/stage2/mtc test projects/mtc` | All green |
478
+
479
+ **Exit criteria:** Stage2.c == stage3.c. Stage2 self-builds. Stage2 passes
480
+ all self-host tests.
481
+
482
+ ### Phase 9+: LSP, DAP, Bindgen, Linter (Post-Fixed-Point)
483
+
484
+ These subsystems can be added incrementally without breaking the fixed point:
485
+
486
+ | Subsystem | Modules | Lines (est.) |
487
+ |-----------|---------|-------------|
488
+ | LSP server | `mtc/lsp/` (15–20 modules) | ~5,000 |
489
+ | DAP server | `mtc/dap/` (5–7 modules) | ~1,000 |
490
+ | Bindgen | `mtc/bindgen.mt` | ~1,000 |
491
+ | Imported bindings | `mtc/imported_bindings/` (5–7 modules) | ~3,000 |
492
+ | Linter | `mtc/linter/` (6–8 modules) | ~5,000 |
493
+ | Pretty printer (AST/IR) | `mtc/pretty_printer/` | ~2,000 |
494
+ | Packages subsystem | `mtc/packages/` | ~3,000 |
495
+
496
+ ## 5. Key Technical Decisions
497
+
498
+ ### 5.1 Lowering: Single Module Initially
499
+
500
+ The lowering pass is the largest and most interconnected component (15,579 lines
501
+ in 16 Ruby sub-files). Start as a single `lowering/lowering.mt` module.
502
+ Sub-file splitting is a code-organization concern that can be deferred to
503
+ post-fixed-point. The previous working self-host (commit `a28545a7`) proved
504
+ this approach: a 16,498-line `lowering.mt` was correct and maintainable.
505
+
506
+ ### 5.2 Type System: Conservative Analyzer, Structural Lowering
507
+
508
+ The semantic analyzer is intentionally conservative: anything it cannot resolve
509
+ degrades to `ty_error` (compatible with everything). The lowering walks the
510
+ AST directly with access to retained `Analysis` data (function signatures,
511
+ `resolved_expr_types`, `resolved_call_kinds`). When the analyzer's permissive
512
+ types are insufficient, the lowering resolves types structurally.
513
+
514
+ This two-tier approach means the analyzer catches structural errors (duplicate
515
+ declarations, immutable-assignment violations, module inconsistencies) while
516
+ the lowering handles the precise type resolution needed for C code generation.
517
+
518
+ ### 5.3 IR as Pure Data
519
+
520
+ The IR (`ir.mt`) contains no lowering or emission logic. It is a frozen shape
521
+ of variant types that both the Lowering and CBackend depend on. This
522
+ decoupling means neither stage depends on the other's internals — they share
523
+ only the IR contract.
524
+
525
+ ### 5.4 String Slices, Not Copies
526
+
527
+ The lexer produces token lexemes as `str` slices borrowing from the source
528
+ buffer. The parser constructs AST identifiers and string literals as `str`
529
+ slices into the same buffer. No string heap-allocation during lexing or
530
+ parsing. Only cross-module names (diagnostic messages, binding keys) need
531
+ owning `string.String` values.
532
+
533
+ ### 5.5 No Exception Handling — Result Types
534
+
535
+ Milk Tea has no exceptions. Every fallible operation returns `Result[T, E]`
536
+ or `Option[T]`. The `?` operator propagates failures. `let ... else:`
537
+ provides early-exit guard clauses. This is more verbose than Ruby's
538
+ `raise`/`rescue` but structurally identical — every `raise` in Ruby maps to
539
+ `return Result.failure`, and every `rescue` maps to `match` on the result.
540
+
541
+ Error collection (e.g., module_loader's collect-and-continue pattern) uses
542
+ explicit `Vec[Diagnostic]` accumulation rather than rescued exceptions.
543
+
544
+ ### 5.6 No Ruby Metaprogramming Equivalents
545
+
546
+ The Ruby compiler uses:
547
+ - `Data.define` → Milk Tea `struct` + `extending` with default parameter values
548
+ - `case`/`when` on AST types → `match` on variant arms
549
+ - `Hash` with symbol keys → `Map[str, V]` with string keys
550
+ - `.each`/`.map`/`.select` → `for` loops or `iter` chains
551
+ - `require`/`require_relative` → `import` statements
552
+ - `gsub`/`split` → `std.str` operations and PCRE2 for regex cases (~59 occurrences)
553
+ - String interpolation `"#{x}"` → `f"#{x}"` format strings
554
+
555
+ ### 6.7 Process Spawning
556
+
557
+ `std.process.mt` provides `ChildProcess` for spawning CC and linker
558
+ invocations. `std.stdio.mt` wraps C `FILE*` for reading/writing files.
559
+ Platform-specific file-system operations use `std/fs.linux.mt` /
560
+ `std/fs.windows.mt` (resolved at import time by the platform suffix mechanism).
561
+
562
+ ### 6.8 CLI Dispatch
563
+
564
+ The Ruby CLI (`tooling/cli.rb`, ~900 lines) uses a flat `case command` dispatch
565
+ with manual `$stdin`-less argument parsing. Each sub-command module (22 files
566
+ under `cli/commands/`) is included into the CLI class. The self-host flattens
567
+ this into `main.mt`: a single `main(args: span[str])` function with an
568
+ `if cmd == "lex": ... else if cmd == "parse": ...` dispatch chain, each branch
569
+ calling into a dedicated handler function.
570
+
571
+ **Argument parsing** iterates through `args` manually with a mutable counter —
572
+ no option-parsing library:
573
+
574
+ ```mt
575
+ function build_command(args: span[str]) -> int:
576
+ var roots = vec.Vec[str].create()
577
+ defer: roots.release()
578
+ var c_compiler = "cc"
579
+ var output_override: Option[str] = Option[str].none
580
+ var use_cache = true
581
+ var ai: ptr_uint = 1
582
+ while ai < args.len:
583
+ let arg = args[ai]
584
+ if arg == "-o":
585
+ output_override = Option[str].some(value = args[ai + 1])
586
+ ai += 2
587
+ continue
588
+ if arg == "--no-cache":
589
+ use_cache = false
590
+ ai += 1
591
+ continue
592
+ # positional operand
593
+ roots.push(arg)
594
+ ai += 1
595
+ # ... use parsed values ...
596
+ ```
597
+
598
+ Every `Vec`, `String`, `Map` allocation uses `defer x.release()` for cleanup.
599
+ Error return paths use `let x = expr else: return 1` guards. Format output uses
600
+ `stdio.print_format(c"format %.*s\n", int<-(s.len), s.data)` — the `%.*s`
601
+ C-printf format for non-null-terminated `str` values.
602
+
603
+ The shared CLI conventions are:
604
+ - `-I DIR` / `--root DIR` — module search roots (repeatable)
605
+ - `--platform linux|windows|wasm` — target platform
606
+ - `--sexpr` — S-expression output (lex, parse, lower commands); this replaces
607
+ the `--machine` flag from the previous self-host at commit `a28545a7`
608
+ - `--no-cache` / `--keep-c PATH` / `-o OUTPUT` / `--cc CC` — build flags
609
+ - `--clean` / `--sanitize` / `--profile debug|release` — build options
610
+ - `--timeout SECONDS` / `--mem MB` / `--format human|tap|junit` — test flags
611
+ - Trailing `--` separates compiler flags from program arguments (run command)
612
+ - `-Werror` — treat warnings as errors (check command)
613
+ - `--locked` / `--frozen` — accepted for CLI compatibility but no-ops until
614
+ package-graph resolution is implemented
615
+
616
+ **Build flow** (`build.mt`): `module_loader.check_program()` → `lowering.lower()`
617
+ → `c_backend.generate_c()` → `fs.write_text()` → build CC argument list as a
618
+ `Vec[str]` → `process.capture(command.as_span())` → check exit code.
619
+
620
+ **Test flow** (`main.mt` test_command): discover `@[test]` functions in
621
+ discovered `.mt` files → build each test file in-process → spawn each test
622
+ binary sandboxed with memory/timeout limits → parse exit code (0 = pass,
623
+ non-zero = fail, 124 = timeout, 137 = OOM).
624
+
625
+ ### 6.9 S-expression Verification Format
626
+
627
+ The self-host's primary verification mechanism is byte-identical S-expression
628
+ output against the Ruby compiler. The `mtc lex --sexpr`, `mtc parse --sexpr`,
629
+ and `mtc lower` commands produce S-expression text that must match Ruby's
630
+ `SexprDumper` output character-for-character.
631
+
632
+ Key serialization invariants that must be preserved:
633
+
634
+ ```mtc lex --sexpr file.mt
635
+ (token :type :identifier :lexeme "main" :literal nil :line 1 :column 0 ...)
636
+ ```
637
+
638
+ ```mtc parse --sexpr file.mt
639
+ (SourceFile :module_name "test" :module_kind :module
640
+ :declarations ((FunctionDef :name "main" ...)))
641
+ ```
642
+
643
+ ```mtc lower file.mt
644
+ (Program :module_name "test"
645
+ :includes ((Include :header "...")))
646
+ ```
647
+
648
+ Critical formatting rules:
649
+ - Token types use hyphens (`plus-equal`, `shift-left`) not underscores
650
+ - Float literals use the lexeme string (the raw source text, not the parsed
651
+ `double` value), to preserve precision and avoid platform-specific rounding
652
+ - String escaping: `\\` for backslash, `\"` for quote, `\n` `\t` `\r` for control chars
653
+ - Types objects render as `(Types::ShortName :field value ...)`
654
+ - Symbols render as `:name` with hyphens
655
+ - Hashes and Sets are silently skipped (not serialized)
656
+ - `nil`, `true`, `false` render as bare atoms
657
+
658
+ ## 6. Risk Register
659
+
660
+ | Risk | Likelihood | Impact | Mitigation |
661
+ |------|-----------|--------|------------|
662
+ | Arena size underestimation causing OOM | Medium | High | Start with 16 MB arenas; add growth/fallback to heap allocation |
663
+ | Type resolution bugs surfaced only at C emission | High | Medium | `ty_error` is permissive; structural lowering resolves at emit time |
664
+ | Map/Vector iterator invalidation during mutable iteration | Medium | High | Two-pass algorithms: collect keys first, then mutate |
665
+ | Order-dependent lowering (module load order affects type resolution) | Medium | High | Always look up types in the owning module first; fall back to global scan |
666
+ | C keyword sanitization gaps | High | Low | Centralize sanitization in `c_backend.mt` as a single `sanitize_c_identifier` function + `C_KEYWORDS` constant; every emission site (declarations, member access, offsetof, initializers, equality helpers) must call it — the Ruby compiler at commit `8f743bc6` defines the exhaustive call-site list of 18 locations across `type_declaration.rb`, `expressions.rb`, and `runtime_helpers.rb` |
667
+ | Fixed-point breakage from non-deterministic map iteration | Medium | Critical | Use `LinkedMap` where insertion order matters; sort keys otherwise |
668
+ | Compiler performance degradation vs Ruby | High | Medium | Arena allocation is faster than GC; string slices eliminate copies; profile after fixed point |
669
+ | Debugging C output from compiler bugs | High | Medium | `--keep-c` saves C for inspection; `diff` against Ruby C output pinpoints divergence |
670
+
671
+ ## 7. Verification Strategy
672
+
673
+ ### 7.1 Per-Phase Verification
674
+
675
+ Every phase produces testable output via `--sexpr`. The Ruby compiler serves as
676
+ the oracle. The previous self-host at `a28545a7` used a `--machine` flag for
677
+ human-readable token tables; the new self-host uses Ruby's current `--sexpr`
678
+ S-expression format, which produces deterministic, diffable output:
679
+
680
+ | Phase | Test Command | Oracle |
681
+ |-------|-------------|--------|
682
+ | Lexer | `diff <(ruby mtc lex --sexpr file.mt) <(stage1/mtc lex --sexpr file.mt)` | Ruby SexprDumper |
683
+ | Parser | `diff <(ruby mtc parse --sexpr file.mt) <(stage1/mtc parse --sexpr file.mt)` | Ruby SexprDumper |
684
+ | Semantic | `diff <(ruby mtc check file.mt 2>&1) <(stage1/mtc check file.mt 2>&1)` | Ruby sema diagnostics |
685
+ | Lowering | `diff <(ruby mtc lower file.mt) <(stage1/mtc lower file.mt)` | Ruby SexprDumper IR |
686
+ | C Backend | `diff <(ruby mtc emit-c file.mt) <(stage1/mtc emit-c file.mt)` | Ruby C backend |
687
+ | Build | `diff stage2.c stage3.c` | Stage2 C output |
688
+ | Runtime | Run compiled examples; compare stdout and exit code | Ruby-compiled binary |
689
+
690
+ ### 7.2 Language Baseline
691
+
692
+ `examples/language_baseline.mt` (1,935 lines) exercises the complete language
693
+ surface. It is the primary integration test. Every phase must produce
694
+ byte-identical output vs Ruby for this file.
695
+
696
+ ### 7.3 Test Suite
697
+
698
+ The self-host test suite mirrors the Ruby test suite's coverage:
699
+
700
+ | Test file | Scope |
701
+ |-----------|-------|
702
+ | `lexer_test.mt` | Token stream correctness, error recovery, edge cases |
703
+ | `parser_test.mt` | AST structure, error recovery, all declaration/expression/statement forms |
704
+ | `semantic_test.mt` | Type checking, interface conformance, generics, control flow |
705
+ | `lowering_test.mt` | IR structure, C name mangling, lowering passes |
706
+ | `c_backend_test.mt` | C output correctness, preamble, type emission |
707
+ | `module_loader_test.mt` | Multi-file programs, circular imports, platform resolution |
708
+ | `path_resolver_test.mt` | Platform-specific file resolution |
709
+ | `ast_formatter_test.mt` | AST → text round-trip |
710
+ | `scope_test.mt` | Lexical scope resolution |
711
+ | `linter_test.mt` | Lint rule detection and auto-fixes |
712
+ | `fix_engine_test.mt` | Text edit engine |
713
+ | `completion_test.mt` | LSP completion |
714
+
715
+ ## 8. Standard Library Dependencies
716
+
717
+ The self-host compiler depends on the following `std` modules (all present in
718
+ the current standard library):
719
+
720
+ | Module | Used for |
721
+ |--------|---------|
722
+ | `std.mem.arena` | Per-compilation arena allocation |
723
+ | `std.mem.heap` | Owning heap allocation (Vec backing, Map backing) |
724
+ | `std.vec` | Dynamic arrays (token lists, declaration spans, argument lists) |
725
+ | `std.map` | Symbol tables, type caches, binding maps |
726
+ | `std.string` | Owned string building (diagnostics, C output, cross-module names) |
727
+ | `std.str` | String view operations (equality, starts_with, ends_with, slicing) |
728
+ | `std.fmt` | Format string building, integer/float rendering |
729
+ | `std.fs` | File system (read source, write C output, discover test files) |
730
+ | `std.path` | Path manipulation (join, basename, extension stripping) |
731
+ | `std.process` | Process spawning (CC invocation, linker) |
732
+ | `std.stdio` | C FILE* I/O, printf for debug output |
733
+ | `std.terminal` | Colored terminal output for diagnostics |
734
+ | `std.hash` | `hash[str]` and `equal[str]` for Map keying |
735
+ | `std.iter` | Composable iteration over collections |
736
+ | `std.json` | LSP/DAP protocol serialization (post-fixed-point) |
737
+ | `std.pcre2` | Regular expressions for version parsing (post-fixed-point) |
738
+ | `std.testing` | Test framework (`@[test]`, assertions) |
739
+
740
+ ## 9. Configuration
741
+
742
+ ### 9.1 Package Manifest
743
+
744
+ ```toml
745
+ # projects/mtc/package.toml
746
+ [package]
747
+ name = "mtc"
748
+ version = "0.1.0"
749
+ source_root = "src"
750
+
751
+ [profile]
752
+ default = "debug"
753
+
754
+ [platform]
755
+ default = "linux"
756
+
757
+ [build]
758
+ entry = "src/mtc/main.mt"
759
+ ```
760
+
761
+ ### 9.2 Bootstrap Environment
762
+
763
+ | Variable | Purpose | Default |
764
+ |----------|---------|---------|
765
+ | `MTC_BOOTSTRAP` | Path to stage0 mtc binary | auto-detect |
766
+ | `CC` | C compiler for native builds | `cc` |
767
+ | `MTC_BUILD_DIR` | Build output directory | `build/` |
768
+ | `XDG_CACHE_HOME` | Cache root for build cache | `~/.cache` |