mt-lang 0.3.26 → 0.3.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 95066d5ccb69388c431a7b4b963853211c3ef57d8e776de1f8468ecdc983899b
4
- data.tar.gz: 83a55caa84c9d54eda5b104727ce0ee4615b4780270d14d0cea4d2001b2fe9fd
3
+ metadata.gz: 20058db447f681f83bcc651d9a9772e79b23bd9f1677e5c33d2053bab536a14f
4
+ data.tar.gz: 204af7f1018ce7637690eddc2bc9a7f297fc1f10e59c1a573491bd3b7c0b0d1c
5
5
  SHA512:
6
- metadata.gz: a6e8f3cf4ed18ec8cd2bdbfbb8ee458f7662546619f31c55bb00123d22bb9150888e405a692cbeea9c67b10bad2e3c82c621efae9992223e1d0c9bd51281f0a2
7
- data.tar.gz: 7cb61c0021e8864897a5b23077dadf54586bf11b11a546ec9b19cbb59083dce4c60d7c8bd996c39380d1a80ea266a364008a1a3f2e3fba7f362ad57c2d97c1d0
6
+ metadata.gz: ec63256de42954560cedc8e5021370b8f45a64e083ebb839d3057d4ea93816a51d45f950283ece162152ad10be6f1bb6ec4aa54a6ad8ce7b2db107830dd1db6a
7
+ data.tar.gz: 616bc238b0ee79df60a7a09fc62b895b1ab7717a1116a6b96c50879549578ba0aab91fbb9a74e40aedfc559973c333a8bc2c2cc40d3d16b6913584a744230819
data/README.md CHANGED
@@ -249,6 +249,10 @@ struct Vec2:
249
249
  x: float
250
250
  y: float
251
251
 
252
+ # Methods may also be defined directly inside the struct body:
253
+ function length_sq() -> float:
254
+ return this.x * this.x + this.y * this.y
255
+
252
256
  @[packed]
253
257
  struct Header:
254
258
  tag: ubyte
@@ -310,6 +314,7 @@ variant Token:
310
314
  Rules:
311
315
 
312
316
  - `struct` and `opaque` may declare nominal interface conformance with `implements`.
317
+ - `struct` bodies may contain `function`, `editable function`, and `static function` declarations directly — they desugar to `extending` blocks targeting the enclosing struct. This is pure syntactic sugar; the compiler emits identical code as a separate `extending` block.
313
318
  - `attribute[target, ...]` declares reusable declaration attributes for `struct`, `field`, `callable`, `const`, `event`, `enum`, `flags`, `union`, and `variant` targets.
314
319
  - Attributes are applied with one or more leading `@[name(...)]` blocks. Built-in `packed`, `align(bytes)`, and `deprecated(message)` are predefined attributes.
315
320
  - `variant` arms may carry named payload fields.
@@ -358,11 +363,31 @@ Method kinds:
358
363
  - `editable function` -> editable receiver
359
364
  - `static function` -> no receiver
360
365
 
366
+ Methods may appear inside a struct body (desugared to an `extending` block) or in a separate
367
+ `extending` declaration:
368
+
369
+ ```mt
370
+ struct Counter:
371
+ value: int
372
+
373
+ function read() -> int:
374
+ return this.value
375
+
376
+ editable function bump() -> void:
377
+ this.value += 1
378
+
379
+ static function zero() -> Counter:
380
+ return Counter(value = 0)
381
+ ```
382
+
383
+ Both inline and `extending` forms lower to identical C code.
384
+
361
385
  Method notes:
362
386
 
363
387
  - Async methods are supported.
364
388
  - Generic methods are supported.
365
389
  - There is no constructor keyword. Names like `init` and `default` are ordinary static methods.
390
+ - Methods may be defined inside struct bodies directly (as syntactic sugar for `extending`) or in separate `extending` blocks. Both forms lower to the same C code; the inline form is preferred when the struct and its core methods appear in the same file.
366
391
 
367
392
  ## 7. Functions, Externals, And Foreign Functions
368
393
 
@@ -300,7 +300,25 @@ Callable and `ref[...]` rules:
300
300
  struct Vec2:
301
301
  x: float
302
302
  y: float
303
+ ```
304
+
305
+ `struct` bodies may also contain `function`, `editable function`, and `static function` declarations directly — they desugar to `extending` blocks targeting the enclosing struct. This is pure syntactic sugar and produces identical C code. See §3.6 for the method kind rules.
306
+
307
+ ```mt
308
+ struct Counter:
309
+ value: int
310
+
311
+ function read() -> int:
312
+ return this.value
313
+
314
+ editable function bump() -> void:
315
+ this.value += 1
316
+
317
+ static function zero() -> Counter:
318
+ return Counter(value = 0)
319
+ ```
303
320
 
321
+ ```mt
304
322
  union Number:
305
323
  i: int
306
324
  f: float
@@ -417,7 +435,25 @@ Rules:
417
435
 
418
436
  ### 3.6 Methods
419
437
 
438
+ Methods may appear directly inside a struct body (desugared to an `extending` block) or in a separate `extending` declaration. Both forms lower to identical C code.
439
+
440
+ ```mt
441
+ # Inline form (sugar)
442
+ struct Counter:
443
+ value: int
444
+
445
+ function read() -> int:
446
+ return this.value
447
+
448
+ editable function bump() -> void:
449
+ this.value += 1
450
+
451
+ static function zero() -> Counter:
452
+ return Counter(value = 0)
453
+ ```
454
+
420
455
  ```mt
456
+ # Equivalent extending form
421
457
  extending Counter:
422
458
  function read() -> int:
423
459
  return this.value
@@ -0,0 +1,737 @@
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.
data/lib/milk_tea/base.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  require "pathname"
4
4
 
5
5
  module MilkTea
6
- VERSION = "0.3.26"
6
+ VERSION = "0.3.28"
7
7
 
8
8
  def self.root
9
9
  @root ||= Pathname.new(File.expand_path("../..", __dir__))
@@ -369,7 +369,7 @@ module MilkTea
369
369
  AST::AttributeDecl.new(name: name_token.lexeme, targets:, params:, visibility:, line:, column: name_token.column)
370
370
  end
371
371
 
372
- def parse_struct_decl(packed: false, alignment: nil, visibility: :private, attributes: [])
372
+ def parse_struct_decl(packed: false, alignment: nil, visibility: :private, attributes: [], inline_methods: true)
373
373
  line = previous.line
374
374
  name_token = consume_name("expected struct name")
375
375
  name = name_token.lexeme
@@ -377,13 +377,32 @@ module MilkTea
377
377
  implements = parse_implements_clause
378
378
  c_name = parse_optional_c_name
379
379
  packed, alignment = parse_struct_layout_attributes(attributes) if attributes.any?
380
- members = parse_recoverable_block do
381
- parse_struct_member
380
+ receiver_type_param_names = type_params.map(&:name)
381
+ members = with_type_param_names(receiver_type_param_names) do
382
+ parse_recoverable_block do
383
+ parse_struct_member
384
+ end
382
385
  end
383
386
  fields = members.filter_map { |kind, member| member if kind == :field }
384
387
  events = members.filter_map { |kind, member| member if kind == :event }
385
388
  nested_types = members.filter_map { |kind, member| member if kind == :nested_type }
386
- AST::StructDecl.new(name:, type_params:, implements:, c_name:, fields:, events:, nested_types:, attributes:, packed:, alignment:, visibility:, lifetime_params:, line:, column: name_token.column)
389
+ methods = members.filter_map { |kind, member| member if kind == :method }
390
+ struct_decl = AST::StructDecl.new(name:, type_params:, implements:, c_name:, fields:, events:, nested_types:, attributes:, packed:, alignment:, visibility:, lifetime_params:, line:, column: name_token.column)
391
+
392
+ if inline_methods && methods.any?
393
+ type_ref_args = type_params.map do |tp|
394
+ AST::TypeArgument.new(
395
+ value: AST::TypeRef.new(name: AST::QualifiedName.new(parts: [tp.name]), arguments: [], nullable: false, line: tp.line, column: tp.column),
396
+ line: tp.line,
397
+ column: tp.column,
398
+ )
399
+ end
400
+ type_ref = AST::TypeRef.new(name: AST::QualifiedName.new(parts: [name]), arguments: type_ref_args, nullable: false, line: name_token.line, column: name_token.column)
401
+ extending_block = AST::ExtendingBlock.new(type_name: type_ref, methods:, line: name_token.line, column: name_token.column)
402
+ [struct_decl, extending_block]
403
+ else
404
+ struct_decl
405
+ end
387
406
  end
388
407
 
389
408
  def parse_struct_decl_params
@@ -429,8 +448,24 @@ module MilkTea
429
448
  [lifetime_params, type_params]
430
449
  end
431
450
 
451
+ def check_method_start?
452
+ saved = @current
453
+ match(:public)
454
+ match(:async)
455
+ match(:editable) if check(:editable)
456
+ match(:static) if check(:static)
457
+ result = check(:function)
458
+ @current = saved
459
+ result
460
+ end
461
+
432
462
  def parse_struct_member
433
463
  field_attributes = parse_attribute_applications
464
+
465
+ if check_method_start?
466
+ return [:method, parse_method_def(attributes: field_attributes)]
467
+ end
468
+
434
469
  visibility, visibility_token = parse_visibility
435
470
 
436
471
  if match(:event)
@@ -438,7 +473,7 @@ module MilkTea
438
473
  end
439
474
 
440
475
  if match(:struct)
441
- return [:nested_type, parse_struct_decl(visibility:, attributes: field_attributes)]
476
+ return [:nested_type, parse_struct_decl(visibility:, attributes: field_attributes, inline_methods: false)]
442
477
  end
443
478
 
444
479
  raise error(visibility_token, "public is only allowed on struct events") if visibility == :public
@@ -154,13 +154,23 @@ module MilkTea
154
154
  until eof?
155
155
  if errors
156
156
  begin
157
- declarations << parse_declaration
157
+ result = parse_declaration
158
+ if result.is_a?(Array)
159
+ declarations.concat(result)
160
+ else
161
+ declarations << result
162
+ end
158
163
  rescue ParseError => e
159
164
  errors << e
160
165
  synchronize_to_top_level_boundary
161
166
  end
162
167
  else
163
- declarations << parse_declaration
168
+ result = parse_declaration
169
+ if result.is_a?(Array)
170
+ declarations.concat(result)
171
+ else
172
+ declarations << result
173
+ end
164
174
  end
165
175
  skip_newlines
166
176
  end
@@ -164,7 +164,10 @@ module MilkTea
164
164
  end
165
165
  end
166
166
  check_redundant_type_annotation(statement)
167
- flag_redundant_widening_cast(statement.value) if statement.type && statement.value
167
+ if statement.type && statement.value
168
+ flag_redundant_widening_cast(statement.value)
169
+ flag_redundant_literal_cast(statement.value)
170
+ end
168
171
  record_ptr_candidate(statement)
169
172
  when AST::Assignment
170
173
  visit_expression(statement.value) # visit RHS first — reads in RHS count against dead-assignment
@@ -173,7 +176,10 @@ module MilkTea
173
176
  mark_mutated(statement.target)
174
177
  check_self_assignment(statement)
175
178
  check_noop_compound_assignment(statement)
176
- flag_redundant_widening_cast(statement.value) if statement.operator == "="
179
+ if statement.operator == "="
180
+ flag_redundant_widening_cast(statement.value)
181
+ flag_redundant_literal_cast(statement.value)
182
+ end
177
183
  when AST::IfStmt
178
184
  statement.branches.each do |branch|
179
185
  visit_expression(branch.condition)
@@ -223,7 +229,10 @@ module MilkTea
223
229
  @loop_depth -= 1
224
230
  when AST::ReturnStmt
225
231
  visit_expression(statement.value) if statement.value
226
- flag_redundant_widening_cast(statement.value) if statement.value
232
+ if statement.value
233
+ flag_redundant_widening_cast(statement.value)
234
+ flag_redundant_literal_cast(statement.value)
235
+ end
227
236
  when AST::DeferStmt
228
237
  with_scope { visit_statement_list(statement.body) }
229
238
  when AST::ExpressionStmt
@@ -278,6 +287,8 @@ module MilkTea
278
287
  visit_expression(expression.right)
279
288
  check_self_comparison(expression)
280
289
  check_redundant_bool_compare(expression)
290
+ check_redundant_binary_operand_cast(expression)
291
+ check_redundant_literal_operand_cast(expression)
281
292
  when AST::RangeExpr
282
293
  visit_expression(expression.start_expr)
283
294
  visit_expression(expression.end_expr)
@@ -1141,6 +1152,140 @@ module MilkTea
1141
1152
  emit_redundant_cast(value, target_name, "implicit widening makes this cast redundant")
1142
1153
  end
1143
1154
 
1155
+ INTEGER_TYPE_RANGES = {
1156
+ "byte" => (-128..127),
1157
+ "short" => (-32_768..32_767),
1158
+ "int" => (-(2**31)..2**31 - 1),
1159
+ "long" => (-(2**63)..2**63 - 1),
1160
+ "ubyte" => (0..255),
1161
+ "ushort" => (0..65_535),
1162
+ "uint" => (0..2**32 - 1),
1163
+ "ulong" => (0..2**64 - 1),
1164
+ "char" => (0..255),
1165
+ }.freeze
1166
+
1167
+ # Reports an integer-literal cast at a coercion slot as redundant when the
1168
+ # literal would implicitly coerce to the cast target: the compiler accepts
1169
+ # a fitting integer literal for any integer slot (`buf[0] = 1` for a
1170
+ # `ubyte` element), so `ubyte<-1` adds nothing. Out-of-range literals are
1171
+ # left alone because removing the cast would then be a type error.
1172
+ # Widening literal casts are already reported by flag_redundant_widening_cast.
1173
+ def flag_redundant_literal_cast(value)
1174
+ return unless value.is_a?(AST::PrefixCast)
1175
+ return unless @sema_facts
1176
+
1177
+ target_name = type_ref_name(value.target_type)
1178
+ return unless target_name
1179
+ range = INTEGER_TYPE_RANGES[target_name]
1180
+ return unless range
1181
+
1182
+ inner = value.expression
1183
+ return unless inner.is_a?(AST::IntegerLiteral)
1184
+ literal = inner.value
1185
+ return unless literal.is_a?(Integer)
1186
+ return unless range.cover?(literal)
1187
+
1188
+ inner_type = resolve_expr_type(inner)
1189
+ return unless inner_type.is_a?(Types::Primitive)
1190
+ return if inner_type.name == target_name # same-type handled elsewhere
1191
+ return if implicit_cast_allowed?(inner_type, target_name) # widening handled elsewhere
1192
+
1193
+ emit_redundant_cast(value, target_name, "integer literal coercion makes this cast redundant")
1194
+ end
1195
+
1196
+ BINARY_CAST_SAFE_OPS = %w[+ - * / % == != < <= > >=].freeze
1197
+
1198
+ # Reports a widening cast used directly as an operand of an arithmetic or
1199
+ # comparison operator when removing it provably leaves the operation's
1200
+ # result unchanged. An integer->float cast inside a float-family
1201
+ # expression promotes to the sibling's float type either way, so the cast
1202
+ # is redundant. Excluded:
1203
+ # * shifts (`<<`/`>>`) — the shifted operand's width is load-bearing;
1204
+ # * `double` in a `float` context — `float_y + double<-i` is `double`
1205
+ # with the cast but `float` without it;
1206
+ # * cast-chains such as `float<-x + float<-y` — each removal is only
1207
+ # safe if the sibling cast stays, so neither is independently
1208
+ # redundant.
1209
+ def check_redundant_binary_operand_cast(binary)
1210
+ return unless @sema_facts
1211
+ return unless BINARY_CAST_SAFE_OPS.include?(binary.operator)
1212
+
1213
+ check_binary_operand_cast(binary.left, binary.right)
1214
+ check_binary_operand_cast(binary.right, binary.left)
1215
+ end
1216
+
1217
+ def check_binary_operand_cast(operand, sibling)
1218
+ return unless operand.is_a?(AST::PrefixCast)
1219
+ return if sibling.is_a?(AST::PrefixCast)
1220
+
1221
+ target_type = resolve_expr_type(operand)
1222
+ return unless target_type.is_a?(Types::Primitive) && target_type.float?
1223
+
1224
+ inner_type = resolve_expr_type(operand.expression)
1225
+ return unless inner_type.is_a?(Types::Primitive) && inner_type.fixed_width_integer?
1226
+
1227
+ sibling_type = resolve_expr_type(sibling)
1228
+ return unless sibling_type.is_a?(Types::Primitive) && sibling_type.float?
1229
+ return if target_type.name == "double" && sibling_type.name == "float"
1230
+
1231
+ target_name = type_ref_name(operand.target_type)
1232
+ return unless target_name
1233
+
1234
+ emit_redundant_cast(operand, target_name, "implicit float widening makes this cast redundant")
1235
+ end
1236
+
1237
+ COMPARISON_CAST_OPS = %w[== != < <= > >=].freeze
1238
+
1239
+ # Reports an integer-literal cast used directly as a comparison operand
1240
+ # (e.g. `State.idle == ubyte<-0`). The literal coerces to the sibling's
1241
+ # type either way, so `D<-lit` adds nothing when the literal fits `D` and
1242
+ # the sibling is a float-family value, an integer the literal fits in, or
1243
+ # an enum (which compares against any integer literal). Out-of-range
1244
+ # literals are left alone: `ubyte<-300` wraps in `D`, which is not the
1245
+ # same value as the bare literal.
1246
+ def check_redundant_literal_operand_cast(binary)
1247
+ return unless @sema_facts
1248
+ return unless COMPARISON_CAST_OPS.include?(binary.operator)
1249
+
1250
+ check_comparison_literal_operand(binary.left, binary.right)
1251
+ check_comparison_literal_operand(binary.right, binary.left)
1252
+ end
1253
+
1254
+ def check_comparison_literal_operand(operand, sibling)
1255
+ return unless operand.is_a?(AST::PrefixCast)
1256
+
1257
+ target_name = type_ref_name(operand.target_type)
1258
+ return unless target_name
1259
+ target_range = INTEGER_TYPE_RANGES[target_name]
1260
+ return unless target_range
1261
+
1262
+ inner = operand.expression
1263
+ return unless inner.is_a?(AST::IntegerLiteral)
1264
+ literal = inner.value
1265
+ return unless literal.is_a?(Integer)
1266
+ return unless target_range.cover?(literal)
1267
+
1268
+ # Same-type casts are reported by check_redundant_cast.
1269
+ inner_type = resolve_expr_type(inner)
1270
+ return if inner_type.is_a?(Types::Primitive) && inner_type.name == target_name
1271
+
1272
+ sibling_type = resolve_expr_type(sibling)
1273
+ case sibling_type
1274
+ when Types::Primitive
1275
+ return unless sibling_type.float? || sibling_type.integer?
1276
+ if sibling_type.integer?
1277
+ sibling_range = INTEGER_TYPE_RANGES[sibling_type.name]
1278
+ return unless sibling_range && sibling_range.cover?(literal)
1279
+ end
1280
+ when Types::EnumBase
1281
+ nil
1282
+ else
1283
+ return
1284
+ end
1285
+
1286
+ emit_redundant_cast(operand, target_name, "integer literal coercion makes this cast redundant")
1287
+ end
1288
+
1144
1289
  def same_resolved_type?(left, right)
1145
1290
  return true if left.equal?(right)
1146
1291
 
@@ -1177,6 +1322,14 @@ module MilkTea
1177
1322
  target = find_primitive_type(target_name)
1178
1323
  return false unless target
1179
1324
  return false unless target.is_a?(Types::Primitive)
1325
+
1326
+ # Fixed-width integers widen implicitly to float/double (matching the
1327
+ # compiler's promotion, e.g. `float + int` or `return int` from a
1328
+ # `-> float` function). `char`/`bool` are not fixed-width integers and
1329
+ # have no implicit conversion to the float family; float->double is not
1330
+ # implicit either.
1331
+ return true if target.float? && inner_type.fixed_width_integer?
1332
+
1180
1333
  return false unless inner_type.fixed_width_integer? && target.fixed_width_integer?
1181
1334
 
1182
1335
  if inner_type.signed_integer? == target.signed_integer?
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.26
4
+ version: 0.3.28
5
5
  platform: ruby
6
6
  authors:
7
7
  - Long (Teefan) Tran
@@ -149,6 +149,7 @@ 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
152
153
  - lib/milk_tea.rb
153
154
  - lib/milk_tea/base.rb
154
155
  - lib/milk_tea/bindings.rb
@@ -624,7 +625,7 @@ metadata:
624
625
  homepage_uri: https://teefan.github.io/mt-lang/
625
626
  source_code_uri: https://github.com/teefan/mt-lang
626
627
  post_install_message: |
627
- Milk Tea 0.3.26 installed!
628
+ Milk Tea 0.3.28 installed!
628
629
 
629
630
  System requirements:
630
631
  - A C compiler (gcc or clang) must be available on PATH