mt-lang 0.2.0

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.
Files changed (258) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -0
  3. data/AUTHORS +3 -0
  4. data/Gemfile +19 -0
  5. data/Gemfile.lock +82 -0
  6. data/LICENSE +21 -0
  7. data/README.md +1208 -0
  8. data/Rakefile +332 -0
  9. data/bin/mtc +8 -0
  10. data/bin/profile-mtc-checks +133 -0
  11. data/bin/tracy-profiler +0 -0
  12. data/docs/build-guide.md +536 -0
  13. data/docs/index.html +2778 -0
  14. data/docs/language-design.md +1519 -0
  15. data/docs/language-manual.md +1534 -0
  16. data/lib/milk_tea/base.rb +49 -0
  17. data/lib/milk_tea/bindings/bindgen/ast_parser.rb +398 -0
  18. data/lib/milk_tea/bindings/bindgen/declaration.rb +319 -0
  19. data/lib/milk_tea/bindings/bindgen/emitter.rb +387 -0
  20. data/lib/milk_tea/bindings/bindgen/overrides.rb +134 -0
  21. data/lib/milk_tea/bindings/bindgen/type_mapper.rb +622 -0
  22. data/lib/milk_tea/bindings/bindgen.rb +207 -0
  23. data/lib/milk_tea/bindings/cli.rb +121 -0
  24. data/lib/milk_tea/bindings/imported_bindings/defaults.rb +210 -0
  25. data/lib/milk_tea/bindings/imported_bindings/generator.rb +1114 -0
  26. data/lib/milk_tea/bindings/imported_bindings/method_source.rb +190 -0
  27. data/lib/milk_tea/bindings/imported_bindings/naming.rb +286 -0
  28. data/lib/milk_tea/bindings/imported_bindings.rb +215 -0
  29. data/lib/milk_tea/bindings/opengl_registry.rb +547 -0
  30. data/lib/milk_tea/bindings/raw_bindings/defaults.rb +1338 -0
  31. data/lib/milk_tea/bindings/raw_bindings.rb +269 -0
  32. data/lib/milk_tea/bindings/steamworks.rb +629 -0
  33. data/lib/milk_tea/bindings/upstream_sources.rb +352 -0
  34. data/lib/milk_tea/bindings/vendored_box2d.rb +79 -0
  35. data/lib/milk_tea/bindings/vendored_c_library.rb +322 -0
  36. data/lib/milk_tea/bindings/vendored_cjson.rb +52 -0
  37. data/lib/milk_tea/bindings/vendored_flecs.rb +78 -0
  38. data/lib/milk_tea/bindings/vendored_glfw.rb +77 -0
  39. data/lib/milk_tea/bindings/vendored_libuv.rb +75 -0
  40. data/lib/milk_tea/bindings/vendored_pcre2.rb +84 -0
  41. data/lib/milk_tea/bindings/vendored_raylib.rb +131 -0
  42. data/lib/milk_tea/bindings/vendored_sdl3.rb +78 -0
  43. data/lib/milk_tea/bindings/vendored_steamworks.rb +71 -0
  44. data/lib/milk_tea/bindings/vendored_tool.rb +71 -0
  45. data/lib/milk_tea/bindings/vendored_tools.rb +23 -0
  46. data/lib/milk_tea/bindings/vendored_tracy.rb +37 -0
  47. data/lib/milk_tea/bindings.rb +23 -0
  48. data/lib/milk_tea/core/ast.rb +311 -0
  49. data/lib/milk_tea/core/async_runtime_installer.rb +28 -0
  50. data/lib/milk_tea/core/binding_types.rb +18 -0
  51. data/lib/milk_tea/core/bindings/attribute_binding.rb +58 -0
  52. data/lib/milk_tea/core/bindings/function_binding.rb +5 -0
  53. data/lib/milk_tea/core/bindings/module_binding.rb +58 -0
  54. data/lib/milk_tea/core/bindings/value_binding.rb +21 -0
  55. data/lib/milk_tea/core/c_backend/aggregate_utils.rb +103 -0
  56. data/lib/milk_tea/core/c_backend/control_flow_emission.rb +412 -0
  57. data/lib/milk_tea/core/c_backend/expressions.rb +468 -0
  58. data/lib/milk_tea/core/c_backend/feature_detection.rb +483 -0
  59. data/lib/milk_tea/core/c_backend/reachability.rb +398 -0
  60. data/lib/milk_tea/core/c_backend/reinterpret.rb +226 -0
  61. data/lib/milk_tea/core/c_backend/runtime_helpers.rb +1080 -0
  62. data/lib/milk_tea/core/c_backend/statements.rb +563 -0
  63. data/lib/milk_tea/core/c_backend/type_collectors.rb +1545 -0
  64. data/lib/milk_tea/core/c_backend/type_declaration.rb +223 -0
  65. data/lib/milk_tea/core/c_backend/type_system.rb +345 -0
  66. data/lib/milk_tea/core/c_backend.rb +287 -0
  67. data/lib/milk_tea/core/compatibility_helpers.rb +79 -0
  68. data/lib/milk_tea/core/compile_time/const_eval.rb +187 -0
  69. data/lib/milk_tea/core/compile_time.rb +329 -0
  70. data/lib/milk_tea/core/control_flow/builder.rb +582 -0
  71. data/lib/milk_tea/core/control_flow/constant_propagation.rb +143 -0
  72. data/lib/milk_tea/core/control_flow/dataflow.rb +74 -0
  73. data/lib/milk_tea/core/control_flow/definite_assignment.rb +79 -0
  74. data/lib/milk_tea/core/control_flow/graph.rb +90 -0
  75. data/lib/milk_tea/core/control_flow/liveness.rb +24 -0
  76. data/lib/milk_tea/core/control_flow/nullability_flow.rb +43 -0
  77. data/lib/milk_tea/core/control_flow/reachability.rb +22 -0
  78. data/lib/milk_tea/core/control_flow/termination.rb +31 -0
  79. data/lib/milk_tea/core/control_flow.rb +34 -0
  80. data/lib/milk_tea/core/cst.rb +48 -0
  81. data/lib/milk_tea/core/cst_builder.rb +19 -0
  82. data/lib/milk_tea/core/ir.rb +85 -0
  83. data/lib/milk_tea/core/keywords.rb +97 -0
  84. data/lib/milk_tea/core/lexer/character_classes.rb +60 -0
  85. data/lib/milk_tea/core/lexer/format_strings.rb +225 -0
  86. data/lib/milk_tea/core/lexer/heredocs.rb +199 -0
  87. data/lib/milk_tea/core/lexer/indentation.rb +62 -0
  88. data/lib/milk_tea/core/lexer/numbers.rb +96 -0
  89. data/lib/milk_tea/core/lexer/recovery.rb +32 -0
  90. data/lib/milk_tea/core/lexer/strings.rb +167 -0
  91. data/lib/milk_tea/core/lexer/symbols.rb +71 -0
  92. data/lib/milk_tea/core/lexer/trivia.rb +53 -0
  93. data/lib/milk_tea/core/lexer.rb +430 -0
  94. data/lib/milk_tea/core/lowering/artifacts.rb +26 -0
  95. data/lib/milk_tea/core/lowering/async/analysis.rb +245 -0
  96. data/lib/milk_tea/core/lowering/async/lowering.rb +1399 -0
  97. data/lib/milk_tea/core/lowering/async/normalization.rb +459 -0
  98. data/lib/milk_tea/core/lowering/async.rb +714 -0
  99. data/lib/milk_tea/core/lowering/block.rb +1052 -0
  100. data/lib/milk_tea/core/lowering/calls.rb +1565 -0
  101. data/lib/milk_tea/core/lowering/declarations.rb +214 -0
  102. data/lib/milk_tea/core/lowering/dyn.rb +206 -0
  103. data/lib/milk_tea/core/lowering/events.rb +1054 -0
  104. data/lib/milk_tea/core/lowering/expressions.rb +1645 -0
  105. data/lib/milk_tea/core/lowering/foreign_cstr.rb +206 -0
  106. data/lib/milk_tea/core/lowering/functions.rb +242 -0
  107. data/lib/milk_tea/core/lowering/loops.rb +1087 -0
  108. data/lib/milk_tea/core/lowering/lowering_context.rb +80 -0
  109. data/lib/milk_tea/core/lowering/proc.rb +419 -0
  110. data/lib/milk_tea/core/lowering/resolve.rb +2516 -0
  111. data/lib/milk_tea/core/lowering/scans.rb +220 -0
  112. data/lib/milk_tea/core/lowering/str_buffer.rb +125 -0
  113. data/lib/milk_tea/core/lowering/utils.rb +1453 -0
  114. data/lib/milk_tea/core/lowering.rb +378 -0
  115. data/lib/milk_tea/core/module_binder.rb +181 -0
  116. data/lib/milk_tea/core/module_loader/errors.rb +21 -0
  117. data/lib/milk_tea/core/module_loader.rb +479 -0
  118. data/lib/milk_tea/core/module_path_resolver.rb +153 -0
  119. data/lib/milk_tea/core/module_roots.rb +88 -0
  120. data/lib/milk_tea/core/parser/attributes.rb +71 -0
  121. data/lib/milk_tea/core/parser/blocks.rb +119 -0
  122. data/lib/milk_tea/core/parser/declarations.rb +749 -0
  123. data/lib/milk_tea/core/parser/expressions.rb +624 -0
  124. data/lib/milk_tea/core/parser/recovery.rb +131 -0
  125. data/lib/milk_tea/core/parser/statements.rb +756 -0
  126. data/lib/milk_tea/core/parser/types.rb +271 -0
  127. data/lib/milk_tea/core/parser.rb +400 -0
  128. data/lib/milk_tea/core/prelude_installer.rb +29 -0
  129. data/lib/milk_tea/core/pretty_printer/ast_formatter.rb +917 -0
  130. data/lib/milk_tea/core/pretty_printer/base_formatter.rb +87 -0
  131. data/lib/milk_tea/core/pretty_printer/ir_formatter.rb +300 -0
  132. data/lib/milk_tea/core/pretty_printer.rb +17 -0
  133. data/lib/milk_tea/core/semantic_analyzer/analysis_context.rb +314 -0
  134. data/lib/milk_tea/core/semantic_analyzer/attributes.rb +184 -0
  135. data/lib/milk_tea/core/semantic_analyzer/calls.rb +992 -0
  136. data/lib/milk_tea/core/semantic_analyzer/expressions.rb +1735 -0
  137. data/lib/milk_tea/core/semantic_analyzer/flow_refinement.rb +355 -0
  138. data/lib/milk_tea/core/semantic_analyzer/foreign_functions.rb +155 -0
  139. data/lib/milk_tea/core/semantic_analyzer/function_binding.rb +354 -0
  140. data/lib/milk_tea/core/semantic_analyzer/generics.rb +383 -0
  141. data/lib/milk_tea/core/semantic_analyzer/interface_conformance.rb +78 -0
  142. data/lib/milk_tea/core/semantic_analyzer/module_context.rb +35 -0
  143. data/lib/milk_tea/core/semantic_analyzer/name_resolution.rb +1438 -0
  144. data/lib/milk_tea/core/semantic_analyzer/nullability.rb +421 -0
  145. data/lib/milk_tea/core/semantic_analyzer/statements.rb +1308 -0
  146. data/lib/milk_tea/core/semantic_analyzer/top_level.rb +588 -0
  147. data/lib/milk_tea/core/semantic_analyzer/type_compatibility.rb +307 -0
  148. data/lib/milk_tea/core/semantic_analyzer/type_declaration.rb +851 -0
  149. data/lib/milk_tea/core/semantic_analyzer.rb +327 -0
  150. data/lib/milk_tea/core/token.rb +26 -0
  151. data/lib/milk_tea/core/token_stream.rb +30 -0
  152. data/lib/milk_tea/core/types/layout.rb +243 -0
  153. data/lib/milk_tea/core/types/predicates.rb +609 -0
  154. data/lib/milk_tea/core/types/registry.rb +83 -0
  155. data/lib/milk_tea/core/types/types.rb +1696 -0
  156. data/lib/milk_tea/core/types/visitor.rb +442 -0
  157. data/lib/milk_tea/core.rb +25 -0
  158. data/lib/milk_tea/dap/backends/lldb_dap.rb +158 -0
  159. data/lib/milk_tea/dap/protocol.rb +58 -0
  160. data/lib/milk_tea/dap/server/breakpoints.rb +66 -0
  161. data/lib/milk_tea/dap/server/debug_map.rb +291 -0
  162. data/lib/milk_tea/dap/server/handlers.rb +374 -0
  163. data/lib/milk_tea/dap/server/launch.rb +261 -0
  164. data/lib/milk_tea/dap/server/lldb_backend.rb +380 -0
  165. data/lib/milk_tea/dap/server/pause_diagnostics.rb +109 -0
  166. data/lib/milk_tea/dap/server/utilities.rb +160 -0
  167. data/lib/milk_tea/dap/server/wire.rb +55 -0
  168. data/lib/milk_tea/dap/server.rb +131 -0
  169. data/lib/milk_tea/dap/session.rb +152 -0
  170. data/lib/milk_tea/dap.rb +6 -0
  171. data/lib/milk_tea/lsp/dependency_resolution.rb +52 -0
  172. data/lib/milk_tea/lsp/diagnostics.rb +611 -0
  173. data/lib/milk_tea/lsp/protocol.rb +104 -0
  174. data/lib/milk_tea/lsp/server/call_hierarchy.rb +274 -0
  175. data/lib/milk_tea/lsp/server/code_actions.rb +446 -0
  176. data/lib/milk_tea/lsp/server/code_lens.rb +97 -0
  177. data/lib/milk_tea/lsp/server/completion.rb +1099 -0
  178. data/lib/milk_tea/lsp/server/configuration.rb +167 -0
  179. data/lib/milk_tea/lsp/server/debug_info.rb +45 -0
  180. data/lib/milk_tea/lsp/server/definition.rb +779 -0
  181. data/lib/milk_tea/lsp/server/diagnostics_scheduling.rb +239 -0
  182. data/lib/milk_tea/lsp/server/execute_command.rb +25 -0
  183. data/lib/milk_tea/lsp/server/folding_range.rb +153 -0
  184. data/lib/milk_tea/lsp/server/formatting.rb +575 -0
  185. data/lib/milk_tea/lsp/server/hover.rb +1465 -0
  186. data/lib/milk_tea/lsp/server/inlay_hints.rb +204 -0
  187. data/lib/milk_tea/lsp/server/lifecycle.rb +234 -0
  188. data/lib/milk_tea/lsp/server/linked_editing_range.rb +43 -0
  189. data/lib/milk_tea/lsp/server/on_type_formatting.rb +73 -0
  190. data/lib/milk_tea/lsp/server/progress.rb +47 -0
  191. data/lib/milk_tea/lsp/server/references.rb +433 -0
  192. data/lib/milk_tea/lsp/server/rename.rb +598 -0
  193. data/lib/milk_tea/lsp/server/selection_range.rb +130 -0
  194. data/lib/milk_tea/lsp/server/semantic_tokens.rb +1745 -0
  195. data/lib/milk_tea/lsp/server/signature_help.rb +200 -0
  196. data/lib/milk_tea/lsp/server/text_documents.rb +125 -0
  197. data/lib/milk_tea/lsp/server/type_hierarchy.rb +167 -0
  198. data/lib/milk_tea/lsp/server/utilities.rb +520 -0
  199. data/lib/milk_tea/lsp/server.rb +415 -0
  200. data/lib/milk_tea/lsp/workspace/analysis.rb +209 -0
  201. data/lib/milk_tea/lsp/workspace/caches.rb +260 -0
  202. data/lib/milk_tea/lsp/workspace/collection.rb +98 -0
  203. data/lib/milk_tea/lsp/workspace/definition_index.rb +184 -0
  204. data/lib/milk_tea/lsp/workspace/dependency_graph.rb +282 -0
  205. data/lib/milk_tea/lsp/workspace/store.rb +154 -0
  206. data/lib/milk_tea/lsp/workspace/utilities.rb +490 -0
  207. data/lib/milk_tea/lsp/workspace.rb +127 -0
  208. data/lib/milk_tea/lsp.rb +13 -0
  209. data/lib/milk_tea/packages/atomic_write.rb +63 -0
  210. data/lib/milk_tea/packages/dependency_solver.rb +194 -0
  211. data/lib/milk_tea/packages/graph.rb +139 -0
  212. data/lib/milk_tea/packages/lock.rb +421 -0
  213. data/lib/milk_tea/packages/manager_cli.rb +485 -0
  214. data/lib/milk_tea/packages/manifest.rb +356 -0
  215. data/lib/milk_tea/packages/manifest_editor.rb +134 -0
  216. data/lib/milk_tea/packages/registry_metadata_provider.rb +34 -0
  217. data/lib/milk_tea/packages/registry_store.rb +420 -0
  218. data/lib/milk_tea/packages/services.rb +41 -0
  219. data/lib/milk_tea/packages/source_cache.rb +71 -0
  220. data/lib/milk_tea/packages/source_fetcher.rb +124 -0
  221. data/lib/milk_tea/packages/source_resolver.rb +389 -0
  222. data/lib/milk_tea/packages/version.rb +164 -0
  223. data/lib/milk_tea/packages.rb +16 -0
  224. data/lib/milk_tea/tooling/asset_pack.rb +141 -0
  225. data/lib/milk_tea/tooling/build.rb +1124 -0
  226. data/lib/milk_tea/tooling/build_cache.rb +240 -0
  227. data/lib/milk_tea/tooling/cli.rb +2688 -0
  228. data/lib/milk_tea/tooling/cst_formatter.rb +13 -0
  229. data/lib/milk_tea/tooling/debug_info_formatter.rb +456 -0
  230. data/lib/milk_tea/tooling/debug_map.rb +248 -0
  231. data/lib/milk_tea/tooling/docs_app.rb +369 -0
  232. data/lib/milk_tea/tooling/error_formatter.rb +86 -0
  233. data/lib/milk_tea/tooling/formatter.rb +787 -0
  234. data/lib/milk_tea/tooling/linter/doc_tags.rb +212 -0
  235. data/lib/milk_tea/tooling/linter/fix_engine.rb +237 -0
  236. data/lib/milk_tea/tooling/linter/flow_rules.rb +380 -0
  237. data/lib/milk_tea/tooling/linter/imports_platform.rb +841 -0
  238. data/lib/milk_tea/tooling/linter/release_rules.rb +744 -0
  239. data/lib/milk_tea/tooling/linter/reserved_names.rb +199 -0
  240. data/lib/milk_tea/tooling/linter/rules.rb +593 -0
  241. data/lib/milk_tea/tooling/linter/source_helpers.rb +407 -0
  242. data/lib/milk_tea/tooling/linter/trailing_comma.rb +139 -0
  243. data/lib/milk_tea/tooling/linter/visitors.rb +1286 -0
  244. data/lib/milk_tea/tooling/linter.rb +798 -0
  245. data/lib/milk_tea/tooling/project_scaffold.rb +82 -0
  246. data/lib/milk_tea/tooling/public/css/docs.css +166 -0
  247. data/lib/milk_tea/tooling/public/js/docs.js +94 -0
  248. data/lib/milk_tea/tooling/run.rb +408 -0
  249. data/lib/milk_tea/tooling/templates/wasm_shell.html +48 -0
  250. data/lib/milk_tea/tooling/toolchain_cli.rb +157 -0
  251. data/lib/milk_tea/tooling/views/404.erb +7 -0
  252. data/lib/milk_tea/tooling/views/index.erb +87 -0
  253. data/lib/milk_tea/tooling/views/layout.erb +69 -0
  254. data/lib/milk_tea/tooling/views/module.erb +97 -0
  255. data/lib/milk_tea/tooling/views/stdlib.erb +21 -0
  256. data/lib/milk_tea/tooling.rb +20 -0
  257. data/lib/milk_tea.rb +8 -0
  258. metadata +426 -0
@@ -0,0 +1,1519 @@
1
+ # Milk Tea Language Design
2
+
3
+ Milk Tea is a statically typed, indentation-based systems language for games.
4
+ It is shaped by a few fixed commitments:
5
+
6
+ - data layout and ABI boundaries stay explicit
7
+ - public types and control flow stay easy to read
8
+ - everyday code stays low-ceremony without hiding costs
9
+ - unsafe operations stay available, but always spelled directly
10
+
11
+ The output target is beautiful C. The generated C should be readable enough that a human can debug it, diff it, and ship it without feeling trapped inside a compiler's private IR.
12
+
13
+ ## Primary goals
14
+
15
+ 1. Be obvious to read. The language should look like code, not punctuation.
16
+ 2. Stay statically typed. Public APIs must be explicit and unsurprising.
17
+ 3. Feel good for game programming. Tight loops, structs, arrays, arenas, handles, and FFI should be first-class.
18
+ 4. Interoperate with C as a native citizen. C libraries are not a side feature; they are part of the core story.
19
+ 5. Generate C that mirrors the source closely. No hidden runtime, hidden heap traffic, or hidden dispatch.
20
+
21
+ ## Non-goals
22
+
23
+ - No exceptions
24
+ - No inheritance
25
+ - No hidden virtual dispatch
26
+ - No operator overloading beyond the built-in operators
27
+ - No trait system or typeclass constraint solver beyond explicit nominal interfaces
28
+ - No macro system that rewrites arbitrary ASTs
29
+ - No garbage collector
30
+ - No implicit conversions between unrelated primitive types
31
+ - No user-invisible allocation for strings, collections, closures, or method calls. If a surface allocates owned text or storage, the allocating surface must be spelled in source.
32
+
33
+ ## Design rules
34
+
35
+ ### 1. What you see is what runs
36
+
37
+ If code allocates, takes an address, dereferences a raw pointer, performs an FFI call, or enters unsafe territory, the source should say so directly.
38
+
39
+ FFI visibility belongs at the declaration site. Raw `external` files expose exact ABI types. Imported foreign declarations may project those raw types into ordinary Milk Tea types, but the projection rule, temporary-storage rule, and ownership rule must be declared there instead of repeated at every call site.
40
+
41
+ The same rule applies to text construction. Plain string literals and format string literals are borrowed `str` values. Any surface that builds owned text must say so explicitly, for example `std.fmt.format(f"...")` when ownership must escape.
42
+
43
+ ### 2. C is the ABI ground truth
44
+
45
+ Milk Tea must represent C structs, unions, enums, flags, pointers, arrays, callbacks, and calling conventions without lossy translation.
46
+
47
+ ### 3. Safe by default, unsafe by choice
48
+
49
+ The language should help with common mistakes, but it must not hide the machine model. Sharp operations are allowed, but only behind explicit syntax.
50
+
51
+ ### 4. Data-oriented design comes first
52
+
53
+ Plain structs, arrays, spans, pools, and arenas matter more than elaborate object systems.
54
+
55
+ ### 5. The language surface stays small
56
+
57
+ If a feature saves five lines but makes lowering, tooling, or debugging much harder, it does not belong in v1.
58
+
59
+ ### 6. One job, one canonical surface
60
+
61
+ Milk Tea should not accumulate multiple everyday spellings for the same job.
62
+
63
+ When two surfaces overlap, one must be the normal user-facing form and the other must be either a low-level escape hatch or an implementation detail.
64
+
65
+ The intended reductions are deliberate:
66
+
67
+ - borrowed text is `str`
68
+ - raw ABI text is `cstr`
69
+ - fixed-capacity mutable text is `str_buffer[N]`
70
+ - growable owned text is `std.string.String`
71
+ - raw character storage is `array[char, N]` or `span[char]`, not an alternate text object
72
+ - safe single-object aliasing is `ref[T]`
73
+ - raw writable pointers are `ptr[T]`
74
+ - owning heap pointers are `own[T]` — auto-dereferences like `ref` but storable, returnable, and nullable. `heap.must_alloc[T](count)` returns `own[T]`. Compiles to `T*`.
75
+ - raw read-only pointers are `const_ptr[T]`
76
+ - sized many-element borrows are `span[T]`
77
+ - pointer-like absence is `null`, not `zero[ptr[T]]` in typed nullable contexts
78
+ - imported foreign declarations own marshalling policy; raw `std.c.*` declarations stay exact
79
+
80
+ If a new feature introduces a second ordinary way to express the same concept, the language should delete one of them instead of documenting both.
81
+
82
+ The compile-time evaluation surface is described in [Compile-Time Evaluation](compile-time.md).
83
+
84
+ ## Overall shape
85
+
86
+ - Indentation defines blocks.
87
+ - A colon starts a block.
88
+ - Newlines terminate statements.
89
+ - Newlines inside `()` and `[]` do not terminate statements.
90
+ - A physical line that ends with a binary operator also continues onto the next line.
91
+ - Tabs are illegal in source files; indentation is 4 spaces.
92
+ - Trailing commas are allowed in multiline call arguments and aggregate literals. The linter additionally flags a redundant trailing comma in a call-argument list (`trailing-list-comma` hint); aggregate and array literals keep them for diff-friendliness.
93
+ - Comments use `#`.
94
+ - Documentation comments use `##` and are markdown blocks attached to the next declaration when no blank line intervenes.
95
+ - Names are `snake_case` for modules, variables, and functions.
96
+ - Types use `PascalCase`.
97
+ - Generated binding modules preserve C names exactly at the ABI layer.
98
+ - Imported foreign modules use normal Milk Tea naming.
99
+
100
+ Example:
101
+
102
+ ```mt
103
+ import std.math as math
104
+ import std.raylib as rl
105
+
106
+ const screen_width: int = 1280
107
+ const screen_height: int = 720
108
+
109
+ struct Player:
110
+ position: rl.Vector2
111
+ velocity: rl.Vector2
112
+ radius: float
113
+
114
+ extending Player:
115
+ editable function update(dt: float):
116
+ this.position.x += this.velocity.x * dt
117
+ this.position.y += this.velocity.y * dt
118
+
119
+ function main() -> int:
120
+ rl.init_window(screen_width, screen_height, "Milk Tea")
121
+ defer rl.close_window()
122
+
123
+ var player = Player(
124
+ position = rl.Vector2(x = 400.0, y = 300.0),
125
+ velocity = rl.Vector2(x = 140.0, y = 100.0),
126
+ radius = 18.0,
127
+ )
128
+
129
+ while not rl.window_should_close():
130
+ let dt = rl.get_frame_time()
131
+ player.update(dt)
132
+
133
+ rl.begin_drawing()
134
+ defer rl.end_drawing()
135
+
136
+ rl.clear_background(rl.BLACK)
137
+ rl.draw_circle_v(player.position, player.radius, rl.GOLD)
138
+
139
+ return 0
140
+ ```
141
+
142
+ For long expressions, the preferred style is the Python-like one: wrap inside delimiters so the continuation is structurally obvious. Operator-led continuation is supported, but it should remain the secondary style for cases where it is already clear.
143
+
144
+ ## Syntax and readability rules
145
+
146
+ Milk Tea should use a deliberately small punctuation set. The only everyday symbolic forms are:
147
+
148
+ - `:` for blocks
149
+ - `->` for function return types
150
+ - `.` for field and module access
151
+ - `[]` for indexing and generic arguments
152
+ - `?` for nullable types
153
+
154
+ Everything else should prefer words over symbols.
155
+
156
+ Address formation and dereference stay as word forms: `ref_of(expr)`, `const_ptr_of(expr)`, `ptr_of(expr)`, `read(ref_value)`, and `read(ptr_value)`.
157
+
158
+ ### Declarations
159
+
160
+ ```mt
161
+ import std.math
162
+
163
+ const gravity: float = 9.81
164
+
165
+ type EntityId = uint
166
+
167
+ struct Body:
168
+ id: EntityId
169
+ mass: float
170
+ velocity: float
171
+
172
+ enum BodyKind: ubyte
173
+ dynamic = 1
174
+ static = 2
175
+
176
+ flags DrawFlags: uint
177
+ visible = 1 << 0
178
+ selected = 1 << 1
179
+ ```
180
+
181
+ ### Variables
182
+
183
+ - `let` creates an immutable local binding.
184
+ - `var` creates a mutable local binding.
185
+ - `const` defines a compile-time constant.
186
+ - A typed local declaration without `= ...` zero-initializes that local. This is the normal local-storage form and is only valid for types that `zero[T]` already supports. Use `zero[T]` or `Type()` only when you need a value expression.
187
+ - Use `zero[T]` when you want raw zero-initialization. Use `default[T]` when you want a semantic default value supplied by an accessible `T.default()` associated function.
188
+ - When a generic API depends on that semantic default, call `default[T]` directly in the generic body and document the requirement in the API prose or naming.
189
+ - `init` is not special syntax. Constructor-like setup remains an ordinary static method naming convention such as `Type.init(...)`.
190
+ - For pointer-like absence, use a nullable type plus `null`. `zero[ptr[T]]` remains available for low-level zero-initialized pointer storage, but the compiler rejects it when the surrounding expected type is already a nullable pointer-like type. In those contexts, write `null`.
191
+
192
+ ```mt
193
+ let width: int = 1280
194
+ var score: int = 0
195
+ var name_input: str_buffer[64]
196
+ const max_players: int = 4
197
+ ```
198
+
199
+ Tuple literal and destructuring:
200
+
201
+ ```mt
202
+ let pair = (42, "hello") # positional tuple
203
+ let point = (x = 10, y = 20) # named tuple
204
+ let (a, b) = pair # destructure positional
205
+ let (x, y) = point # destructure named
206
+ let Vec2(x, y) = get_position() # struct destructure
207
+ ```
208
+
209
+ A function can return a tuple type: `function pair() -> (int, str):`.
210
+
211
+ Local type inference is allowed when the initializer makes the type obvious:
212
+
213
+ ```mt
214
+ let dt = rl.get_frame_time() # inferred as float
215
+ var player_count = 0 # inferred as int
216
+ ```
217
+
218
+ Guard binding is part of the local declaration surface:
219
+
220
+ ```mt
221
+ let handle = maybe_handle else:
222
+ return 1
223
+
224
+ let image = load_image(path) else:
225
+ return 1
226
+
227
+ let config = load_config() else as error:
228
+ return error
229
+
230
+ let _ = initialize_runtime() else:
231
+ return 1
232
+ ```
233
+
234
+ The initializer may be `T?`, `Option[T]`, or `Result[T, E]`. For nullable inputs the bound name is the non-null `T`; for `Option[T]` it is the `some.value`; for `Result[T, E]` it is the `success.value`, and `else as error:` optionally binds the `failure.error` value for the failure path. Use `let _ = expr else:` when the success path should be checked but not bound, including `Result[void, E]`. The `else` block must terminate control flow.
235
+
236
+ Public items should always spell their types out.
237
+
238
+ ### Functions
239
+
240
+ ```mt
241
+ function clamp(value: float, min_value: float, max_value: float) -> float:
242
+ if value < min_value:
243
+ return min_value
244
+ else if value > max_value:
245
+ return max_value
246
+ else:
247
+ return value
248
+ ```
249
+
250
+ There is no overloading. One function name maps to one callable symbol.
251
+
252
+ ### Methods
253
+
254
+ Methods are syntax sugar over namespaced functions. They do not imply objects, inheritance, or dynamic dispatch.
255
+
256
+ ```mt
257
+ struct Camera:
258
+ position: Vec2
259
+ zoom: float
260
+
261
+ extending Camera:
262
+ editable function move_by(delta: Vec2):
263
+ this.position.x += delta.x
264
+ this.position.y += delta.y
265
+
266
+ function world_scale() -> float:
267
+ return this.zoom
268
+
269
+ static function origin() -> Camera:
270
+ return Camera(position = Vec2(x = 0.0, y = 0.0), zoom = 1.0)
271
+ ```
272
+
273
+ Lowering rule, in emitted C:
274
+
275
+ - `camera.move_by(delta)` emits `game_Camera_move_by(&camera, delta)`.
276
+ - `camera.world_scale()` emits `game_Camera_world_scale(camera)`.
277
+ - `Camera.origin()` emits `game_Camera_origin()`.
278
+
279
+ Receiver rule:
280
+
281
+ - Plain `function` inside `extending T:` means an implicit `this: T` value receiver.
282
+ - `editable function` inside `extending T:` means an implicit writable `this` receiver and requires an addressable receiver.
283
+ - `static function` inside `extending T:` means there is no receiver.
284
+ - There is no hidden dynamic dispatch, vtable lookup, or heap allocation.
285
+
286
+ Value receivers are deliberate. They keep fluent calls on temporaries honest and let method calls lower directly to plain C value parameters. Writable methods are the only place where the compiler takes an address implicitly.
287
+
288
+ The rule is fixed and inspectable. There is no method lookup at runtime.
289
+
290
+ ### Interfaces
291
+
292
+ Interfaces are explicit nominal method-set contracts for static polymorphism.
293
+ They do not introduce inheritance, hidden dispatch, or a second object model.
294
+
295
+ ```mt
296
+ interface Damageable:
297
+ editable function take_damage(amount: int) -> void
298
+ function is_alive() -> bool
299
+
300
+ struct NPC implements Damageable:
301
+ name: str
302
+ hp: int
303
+
304
+ extending NPC:
305
+ editable function take_damage(amount: int):
306
+ this.hp -= amount
307
+
308
+ function is_alive() -> bool:
309
+ return this.hp > 0
310
+ ```
311
+
312
+ Conformance belongs on the nominal type declaration, not on an `extending` block.
313
+ That keeps the contract visible at the owning type and avoids import-sensitive structural matching.
314
+
315
+ Rules for interfaces in v1:
316
+
317
+ - `interface` bodies contain `function`, `editable function`, or `static function` signatures.
318
+ - Generic interfaces are supported: `interface Mapper[T]: function map(x: T) -> T`.
319
+ - Interface methods may not have bodies, fields, constants, default implementations, associated types, or inheritance.
320
+ - Interface methods may not declare their own type params or be `async`.
321
+ - `struct` and `opaque` declarations may implement zero or more interfaces.
322
+ - Multiple interfaces are allowed: `struct Boss implements Damageable, Drawable:`.
323
+ - A type implements an interface only when its declaration says so explicitly.
324
+ - Conformance matching uses method kind, receiver mutability when applicable, parameter types, return type, and asyncness.
325
+ - An `editable function` requirement must be satisfied by an editable method exactly.
326
+ - If two interfaces require the same method with the same signature, one implementation satisfies both. If the signatures differ, the declaration is rejected.
327
+ - Interface conformance is a compile-time fact, not a storage type.
328
+
329
+ Generic constraints use the same word form:
330
+
331
+ ```mt
332
+ function deal_area_damage[T implements Damageable](targets: span[T], amount: int):
333
+ for i in 0..targets.len:
334
+ if targets[i].is_alive():
335
+ targets[i].take_damage(amount)
336
+ ```
337
+
338
+ When one type parameter needs multiple interfaces, join them with `and` inside the type-parameter clause:
339
+
340
+ ```mt
341
+ function update_and_draw[T implements Updatable and Drawable](value: ref[T]):
342
+ value.update()
343
+ value.draw()
344
+ ```
345
+
346
+ Canonical hash, equality, and ordering hooks remain associated functions `T.hash(value: const_ptr[T]) -> uint`, `T.equal(left: const_ptr[T], right: const_ptr[T]) -> bool`, and `T.order(left: const_ptr[T], right: const_ptr[T]) -> int`. The built-in `hash[T](...)`, `equal[T](...)`, and `order[T](...)` forms lower directly to those associated functions after borrowing safe lvalues or forwarding existing refs and pointers.
347
+
348
+ The separate `hashes` and `equates` constraint keywords were removed because they were only contract spelling. They were not the thing that made the built-ins work; direct `hash[T](...)`, `equal[T](...)`, and now `order[T](...)` use already force the canonical hooks at specialization time.
349
+
350
+ Ordered std surfaces should now rely on the canonical tri-state `order[T](...)` hook instead of storing per-instance comparator callbacks when the element type already has a natural ordering. Current ordered surfaces include `std.binary_heap.BinaryHeap[T]`, the thin `std.priority_queue.PriorityQueue[T]` facade over it, `std.ordered_set.OrderedSet[T]` for unique ordered membership, and `std.ordered_map.OrderedMap[K, V]` for ordered associative storage.
351
+
352
+ `default[T]` requires an explicit `T.default()` provider at each use site. The separate `defaults` constraint was removed because it duplicated that requirement while lengthening generic signatures. If an API depends on semantic defaulting, say so in the API docs and call `default[T]` directly in the generic body.
353
+
354
+ Constraint checking happens after type inference and specialization, and constrained calls still lower to ordinary static method calls in generated C.
355
+ There is no witness table or vtable for constrained generics.
356
+
357
+ Bare interface names are not runtime value types in v1.
358
+ That means surfaces such as `var x: Damageable`, `array[Damageable, 10]`, fields of interface type, and returns of bare interface type are invalid.
359
+ Milk Tea supports runtime polymorphic interface values through `dyn[InterfaceName]`. The `dyn` keyword follows the same type constructor pattern as `ptr[T]`, `ref[T]`, and `span[T]`, making the indirection cost explicit in source.
360
+
361
+ Because fixed arrays copy by value, mutating interface-constrained collection code should usually take `span[T]` or `ref[array[T, N]]` rather than `array[T, N]` by value.
362
+
363
+ Iteration stays structural in v1 rather than going through a nominal `Iterator[T]` or `Iterable[T]` interface. Arrays, spans, and ranges keep their built-in behavior, and custom iterables participate by exposing the same method shape the compiler already recognizes: a non-editable `iter()` method on the iterable, then either `next() ->` nullable pointer-like item or `next() -> bool` together with `current()` on the iterator.
364
+
365
+ This is deliberate. Interfaces are compile-time-only nominal contracts and are not generic today, so introducing a central iterator interface hierarchy now would either erase the item type or duplicate type-specific interfaces. The standard library should instead standardize on one iterator convention:
366
+
367
+ - `collection.iter()` is the canonical traversal surface.
368
+ - Alternate traversals use explicit view methods such as `map.keys()`, `map.values()`, and `map.entries()`.
369
+ - Hash collections keep relying on the canonical `hash[T]` and `equal[T]` associated hooks instead of callback-style comparer objects.
370
+ - Ordered collections can now rely on the canonical `order[T]` hook instead of forcing per-instance comparator storage when the type already has a natural ordering.
371
+
372
+ ### Control flow
373
+
374
+ ```mt
375
+ if ready:
376
+ start_game()
377
+ else if wants_menu:
378
+ open_menu()
379
+ else:
380
+ show_intro()
381
+
382
+ while running:
383
+ tick()
384
+
385
+ for i in 0..count:
386
+ update_enemy(i)
387
+
388
+ for body in bodies:
389
+ simulate(body)
390
+
391
+ for entity, position in entities, positions:
392
+ integrate(entity, position)
393
+
394
+ match event.kind:
395
+ EventKind.quit:
396
+ return 0
397
+ EventKind.resize:
398
+ resize(event.width, event.height)
399
+ ```
400
+
401
+ Rules:
402
+
403
+ - Conditions must be `bool`. Integers and pointers do not become truthy implicitly.
404
+ - `match` must be exhaustive for enums.
405
+ - `break` and `continue` use ordinary loop control semantics.
406
+ - Single-form `for` accepts `start..stop`, `array[T, N]`, `span[T]`, and custom structural iterables.
407
+ - A custom structural iterable must expose non-editable `iter()` with no arguments, and its iterator must expose either `next() ->` nullable pointer-like item or `next() -> bool` plus `current()`.
408
+ - Parallel `for` accepts multiple array/span iterables and binds them in lockstep.
409
+ - Parallel `for` does not accept ranges, and iterable lengths must match.
410
+
411
+ Range-index assignment is also part of the control-flow-and-mutation surface when code wants an explicit fixed-width slice update:
412
+
413
+ ```mt
414
+ buf[0..3] = (1.0, 2.0, 3.0)
415
+ ```
416
+
417
+ The bounds are integer literals, the range is end-exclusive, and the right-hand tuple width must match the slice width exactly.
418
+
419
+ ### Useful structured features
420
+
421
+ Milk Tea should include a small number of control-flow features that materially improve systems code without hiding behavior.
422
+
423
+ #### `defer`
424
+
425
+ `defer` registers cleanup code at scope exit and lowers to obvious cleanup labels in C.
426
+
427
+ ```mt
428
+
429
+
430
+ function load_texture(path: str) -> Result[Texture, LoadError]:
431
+ let texture = rl.load_texture(path)
432
+
433
+ if texture.id == 0:
434
+ return Result[Texture, LoadError].failure(error= LoadError.file_not_found)
435
+
436
+ return Result[Texture, LoadError].success(value= texture)
437
+ ```
438
+
439
+ #### `unsafe`
440
+
441
+ `unsafe` is required for:
442
+
443
+ - raw pointer dereference and field access through raw pointers
444
+ - raw pointer arithmetic
445
+ - unchecked casts and bit reinterpretation
446
+ - pointer indexing
447
+ - raw ABI work that is not covered by a declared foreign import contract
448
+
449
+ ```mt
450
+ let p = unsafe: pixels + offset
451
+
452
+ unsafe:
453
+ let pixel = read(ptr[uint]<-p)
454
+ pixels = p
455
+ ```
456
+
457
+ The point is not to forbid sharp tools. The point is to mark them.
458
+ Calling a foreign import that already declares borrowed strings, ordinary `str` parameters, string lists, `out` parameters, or typed pointer projections is ordinary code. Crossing into raw `std.c.*` bindings, doing pointer reinterpretation, or manually walking foreign memory remains explicit low-level work.
459
+
460
+ #### `async` and `await`
461
+
462
+ Milk Tea has a task surface today, but it is intentionally narrow and explicit.
463
+
464
+ ```mt
465
+ async function child() -> int:
466
+ return 41
467
+
468
+ async function parent() -> int:
469
+ let value = await child()
470
+ return value + 1
471
+ ```
472
+
473
+ Current implemented shape:
474
+
475
+ - `async function` lifts its return type to `Task[T]`
476
+ - `await` is only valid inside async functions
477
+ - async entrypoint bootstrapping is compiler-owned, but async helpers stay explicit library surface; import `std.async as aio` when user code needs `sleep`, `work`, `completed`, `result`, `wait`, `run`, or runtime control
478
+ - `aio.wait(...)` and `aio.run(...)` accept direct task expressions as well as zero-arg task roots; the compiler rewrites the direct-task form into the deferred root shape automatically
479
+ - async bodies support ordinary local declarations, including `let ... else:`, assignments, returns, `if`, `while`, single-form and parallel `for`, `match`, `defer`, `unsafe`, and deferred cleanup bodies that `await`
480
+ - await placement is handled by the normalization pass which hoists nested awaits into `let` bindings, so `await` is supported in all expression contexts (call arguments, binary operations, if-expr, match-expr, member access, index access, format strings)
481
+
482
+ The default async model is a language-integrated entry boundary. `std.async` remains the explicit high-level helper surface for operations such as `sleep`, `work`, `completed`, `result`, `wait`, `wait_on`, and `with_runtime`, while the normal runtime model stays the single integrated libuv-backed runtime.
483
+
484
+ #### Concurrency: `parallel for` and `parallel:` blocks
485
+
486
+ Milk Tea has first-class compiler support for multithreading. The threading model is structured: all spawned work completes before the parent scope continues. There is no hidden thread pool, no garbage collector interaction, and no fire-and-forget. Every thread boundary is visible in source.
487
+
488
+ ```mt
489
+ # Data-parallel: each iteration runs on a separate CPU core
490
+ parallel for i in 0..entity_count:
491
+ positions[i] += velocities[i] * dt
492
+
493
+ # Structured fork-join: each do block runs on a separate thread
494
+ parallel:
495
+ textures = load_textures(path)
496
+ sounds = load_sounds(path)
497
+ ```
498
+
499
+ Design choices:
500
+
501
+ - **Structured, not fire-and-forget.** All `parallel for` and `parallel:` blocks are synchronous barriers — the calling thread blocks until all work completes. This guarantees that captured local variables remain alive for the duration and eliminates lifetime concerns without ownership annotations.
502
+ - **Compile-time safety.** `ref[T]` captures are rejected because mutable aliases across thread boundaries create data races. The `parallel:` block enforces single-writer-or-multiple-readers: if one statement writes a variable, no other block may read or write it.
503
+ - **Value capture, pointer-based arrays.** Scalars and spans are captured by value (the span's data pointer still references the original storage). Arrays are captured as pointers to their first element, so writes in the worker affect the original array.
504
+ - **`do` is a keyword.** It is recognized inside `parallel:` blocks to introduce each concurrent unit of work.
505
+ - **Real OS threads via libuv.** Thread dispatch uses `uv_thread_create` / `uv_thread_join`, consistent with `std.thread` and `std.sync`. CPU count is detected at runtime via `uv_cpu_info`. The first chunk runs on the calling thread to avoid unnecessary dispatch when the workload is small.
506
+ - **No forced dependency.** The build system automatically detects `parallel for` and `parallel:` usage via a sema-level flag and links libuv only when needed.
507
+
508
+ #### Atomics: `atomic[T]`
509
+
510
+ `atomic[T]` is a built-in type constructor for lock-free concurrent access to integer values. It lowers to C11 `_Atomic T` with `__atomic_*` builtins (GCC/Clang). All operations use sequential consistency.
511
+
512
+ ```mt
513
+ var counter: atomic[int]
514
+ counter.store(0)
515
+ let prev = counter.add(1)
516
+ let value = counter.load()
517
+ ```
518
+
519
+ `atomic[T]` is deliberately narrow: it supports only primitive integer types and `bool`. This avoids the complexity of atomic struct operations and keeps the C lowering simple and efficient. For more complex synchronization patterns, use `std.sync.Mutex`, `std.sync.Condition`, and `std.sync.Semaphore`.
520
+
521
+ ## Type system
522
+
523
+ The type system must stay simple, explicit, and close to C.
524
+
525
+ ### Primitive types
526
+
527
+ - `bool`
528
+ - `byte`
529
+ - `char`
530
+ - `byte`, `short`, `int`, `long`
531
+ - `ubyte`, `ushort`, `uint`, `ulong`
532
+ - `ptr_int`, `ptr_uint`
533
+ - `float`, `double`
534
+ - `void`
535
+ - `str`
536
+ - `cstr`
537
+ - `vec2`, `vec3`, `vec4` — float vectors
538
+ - `ivec2`, `ivec3`, `ivec4` — integer vectors
539
+ - `mat3`, `mat4` — column-major matrices
540
+ - `quat` — quaternion (memory-compatible with `vec4`)
541
+
542
+ Notes:
543
+
544
+ - Native vector, matrix, and quaternion types support aggregate construction: `vec3(x = 1.0, y = 2.0, z = 3.0)`, `quat(x = 0.0, y = 0.0, z = 0.0, w = 1.0)`, and column-wise `mat4(col0 = ..., col1 = ..., ...)`. Omitted fields default to zero.
545
+ - `str` is a UTF-8 string view, not a NUL-terminated C string.
546
+ - Every `str` value must contain valid UTF-8 bytes for its full length.
547
+ - `cstr` is the raw ABI-facing NUL-terminated C string type. It belongs primarily in raw `external` files and low-level interop code.
548
+ - `char` is the ABI-facing single-byte character type for C text and raw buffers. It is not a general arithmetic integer type.
549
+ - String literals produce `str`.
550
+ - Safe code does not fabricate `str` values from raw parts. Source code ordinarily obtains `str` values from literals, `str_buffer.as_str()`, slicing an existing `str`, imported foreign boundaries that declare borrowed text, or other compiler/runtime surfaces that preserve the UTF-8 invariant.
551
+ - Low-level code may construct `str(data = ..., len = ...)` only inside `unsafe`, and the caller is then responsible for pointer validity, lifetime, and the UTF-8 invariant.
552
+ - A string literal may satisfy an expected `cstr` directly when the compiler has contextual type information, such as a typed local, an `array[cstr, N]` element, or a borrowed C-string argument position, because static storage is known.
553
+ - `c"hello"` produces `cstr` with static storage for raw ABI work and low-level interop.
554
+ - `<<-TAG ... TAG` produces `str` and `c<<-TAG ... TAG` produces `cstr` for multiline block text. The content is dedented by the shared leading spaces of nonblank lines, and the newline before the terminator is preserved.
555
+ - Whitespace-adjacent `"..."` / `c"..."` literal segments concatenate exactly with no implicit separator, so long text can wrap (or be split on one line) without introducing another literal form.
556
+ - The VS Code extension may attach embedded grammars to specific heredoc tags such as `GLSL`, `VERT`, `FRAG`, `COMP`, `JSON`, `JSONC`, `SQL`, `HTML`, and `MT`, but that is editor-only highlighting. The `MT` tag recursively applies Milk Tea syntax highlighting to the heredoc content. It does not change runtime semantics, and SQL values should still use bound parameters rather than text interpolation.
557
+ - There is no first-party runtime JSONC normalization layer; editor JSONC highlighting remains separate from runtime parsing.
558
+
559
+ ### Composite types
560
+
561
+ ```mt
562
+ array[T, N] # fixed-size array
563
+ SoA[T, N] # Structure-of-Arrays: flatten struct fields into separate arrays
564
+ str_buffer[N] # fixed-capacity mutable UTF-8 text buffer
565
+ ptr[T] # raw pointer
566
+ span[T] # pointer + length view
567
+ fn(A, B) -> R # function pointer type
568
+ (T, U) # tuple type (positional or named fields)
569
+ ```
570
+
571
+ Examples:
572
+
573
+ ```mt
574
+ let pixels: span[ubyte]
575
+ let name_input: str_buffer[64]
576
+ let labels: array[str, 8]
577
+ let texture_ptr: ptr[Texture]
578
+ let normal_table: array[float, 256]
579
+ let callback: fn(ptr[void], int) -> void
580
+ ```
581
+
582
+ Notes:
583
+
584
+ - Fixed-array indexing is bounds-checked and safe by default.
585
+ - Safe array indexing requires an addressable array value; bind temporaries before indexing them.
586
+ - Safe indexing (`arr[i]`) aborts on out-of-bounds via `fatal`. Use `get(arr, i)` for recoverable bounds-checked access that returns `ptr[T]?`.
587
+ - When a `span[T]` boundary is expected, addressable `array[T, N]` values coerce directly. Arrays also expose `.as_span()` for explicit conversion when the target type is not a call boundary.
588
+ - `array[char, N]` and `span[char]` are the ordinary source-level forms for raw writable character storage and byte-oriented foreign buffers. They are not alternate text objects and should not grow a parallel everyday text API.
589
+ - `str_buffer[N]` is the one source-level mutable UTF-8 text type. It owns `N` writable text bytes plus an implementation-managed trailing NUL slot, tracks current text length, and refreshes that length when a writable buffer alias mutates the underlying storage.
590
+ - If low-level code needs to validate raw `array[char, N]` storage as text, that conversion belongs in an explicit helper or imported boundary, not as a built-in method family on raw arrays.
591
+ - Addressable `str_buffer[N]` values also coerce to `span[char]`, so writable foreign text APIs can still accept builders directly when they do not want a second application-facing text abstraction.
592
+ - `str_buffer[N]` is not an ABI type. Raw bindings still spell writable text as `ptr[char]` or `span[char]`; `str_buffer[N]` is the caller-side text object.
593
+ - `str_buffer[N]` has a built-in text surface: `.clear()`, `.assign(str)`, `.append(str)`, `.len()`, `.capacity()`, `.as_str()`, and `.as_cstr()`.
594
+ - `.assign(...)` replaces the current contents and traps at runtime if the new text exceeds capacity.
595
+ - `.append(...)` extends the current contents and traps at runtime if the appended text would exceed capacity.
596
+ - `.len()` returns the tracked text length, revalidating UTF-8 and rescanning for the trailing NUL if the builder was passed through a writable `span[char]` or `ptr[char]` alias.
597
+ - `.capacity()` reports the maximum writable text bytes, not counting the reserved trailing NUL slot.
598
+ - `.as_str()` and `.as_cstr()` borrow from the same builder storage and revalidate through that same dirty-refresh path before returning.
599
+ - `str.slice(start, len)` uses byte offsets and byte lengths, but both the start and end position must be UTF-8 code-unit boundaries or the slice traps at runtime.
600
+ - Ordinary string lists stay `array[str, N]` or `span[str]` in source. If an imported foreign declaration chooses that public surface for a raw `char **`, `span[cstr]`, or pointer-plus-length text-list API, the boundary owns the temporary marshalling.
601
+ - Imported foreign declarations may map `str_buffer[N] as ptr[char]` directly when the public surface wants writable UTF-8 text with fixed caller capacity.
602
+ - If the raw call also needs the caller buffer size, a `str_buffer[N]` public signature should pass `text_public.capacity() + 1` in the foreign mapping so the raw side sees the full writable byte count including the trailing NUL slot.
603
+ - `span[char] as ptr[char]` remains the right public surface when the writable storage is not semantically UTF-8 text or when the caller capacity is intentionally runtime-sized instead of part of the type.
604
+ - In an explicit foreign mapping, a parameter declared with `as` keeps the boundary value under its original name and exposes the public value as `<name>_public`.
605
+ - Pointer indexing follows the raw pointer model and requires `unsafe`.
606
+
607
+ ### Nullability
608
+
609
+ Milk Tea keeps nullability explicit. `T?` is valid for any type.
610
+
611
+ ```mt
612
+ let window: ptr[Window]? = null
613
+ let name: cstr? = null
614
+ let port: int? = null
615
+ ```
616
+
617
+ For pointer-like bases (`ptr[T]`, `const_ptr[T]`, `cstr`, `fn(...)`, `proc(...)`, opaque), `T?` is a nullable pointer and `null` reuses the null pointer as the absent value. For non-pointer value bases (`int`, `bool`, `float`, structs), `T?` is stored inline by value as a tagged optional — a presence flag plus the value. It copies by value with no hidden heap allocation, boxing, or pointer aliasing, which keeps value-type optionals consistent with the rest of the value-semantics model.
618
+ `null` expresses absence in any nullable context. When contextual typing already determines the nullable pointer-like type, prefer bare `null` over a typed form such as `null[ptr[Window]]`; use a typed `null[...]` (whose target must be pointer-like) only when the surrounding context does not provide the target type.
619
+ Do not use `zero[ptr[T]]` as a replacement for `null`; `null` expresses absence, while `zero[T]` is the generic value-initialization surface. When the expected type is already a nullable pointer-like type, the compiler rejects `zero[ptr[T]]` and requires `null`.
620
+ At an FFI boundary, only pointer-like `T?` is allowed: `external` and `foreign function` parameters and returns reject a non-pointer value nullable such as `int?` so the C ABI stays unambiguous — use `ptr[T]?` or an explicit struct instead.
621
+
622
+ ### User-defined types
623
+
624
+ #### Structs
625
+
626
+ Structs are plain data. Field order is preserved.
627
+
628
+ ```mt
629
+ struct Vec2:
630
+ x: float
631
+ y: float
632
+ ```
633
+
634
+ Structs may contain nested struct declarations, which become independently named types scoped inside the parent:
635
+
636
+ ```mt
637
+ struct Rectangle:
638
+ x: float
639
+ y: float
640
+
641
+ struct Edge:
642
+ start: float
643
+ end: float
644
+
645
+ top_edge: Edge
646
+ left_edge: Edge
647
+ ```
648
+
649
+ Nested structs are full types, not inline anonymous members. Their qualified name is `Parent.Nested`
650
+ (e.g., `Rectangle.Edge`). Inside the parent struct, bare names resolve to nested types. Outside,
651
+ use the qualified name. Nested structs may themselves contain nested structs. The generated C name
652
+ joins the path with underscores (e.g., `module_Rectangle_Edge`).
653
+
654
+ #### Enums
655
+
656
+ Enums always have an explicit backing type.
657
+
658
+ ```mt
659
+ enum WeaponKind: ubyte
660
+ sword = 1
661
+ bow = 2
662
+ wand = 3
663
+ ```
664
+
665
+ #### Flags
666
+
667
+ Flags are named bitmasks with a fixed integer backing type.
668
+
669
+ ```mt
670
+ flags WindowFlags: uint
671
+ fullscreen = 1 << 0
672
+ vsync = 1 << 1
673
+ borderless = 1 << 2
674
+ ```
675
+
676
+ #### Unions
677
+
678
+ Unions are allowed for FFI and low-level storage.
679
+
680
+ ```mt
681
+ union Value:
682
+ i: int
683
+ f: float
684
+ raw: ptr[void]
685
+ ```
686
+
687
+ Unions are untagged storage. The current implementation does not track an active field, so switching views is a low-level operation by design rather than a checked safe surface.
688
+
689
+ #### Variants
690
+
691
+ Variants are tagged unions. Each arm optionally carries named payload fields.
692
+
693
+ ```mt
694
+ variant Token:
695
+ ident(text: str)
696
+ number(value: int)
697
+ eof
698
+ ```
699
+
700
+ Arm constructors follow the same field-assignment form as struct literals. No-payload arms are bare member expressions. Match on a variant uses `as name` to bind a payload arm's fields. Generic variants are supported through specializations like `Box[int].some(value = 1)`.
701
+
702
+ #### Opaque types
703
+
704
+ Opaque types are essential for C handles.
705
+
706
+ ```mt
707
+ opaque SDL_Window
708
+ opaque ma_engine
709
+ ```
710
+
711
+ #### Type aliases
712
+
713
+ ```mt
714
+ type Seconds = float
715
+ type FileHandle = ptr[libc.FILE]
716
+ ```
717
+
718
+ ### Generics
719
+
720
+ Generics are useful, but they must stay boring. The compile-time surface is documented in [Compile-Time Evaluation](compile-time.md); generic bodies participate in that surface by using `when`, `inline for`, `inline while`, `inline match`, `inline if`, `type`-returning functions, `const function`, and block-bodied `const` initializers (`const X -> T: ...`) at the lexical positions where the compile-time rules allow them.
721
+
722
+ Allowed in v1:
723
+
724
+ - generic structs
725
+ - generic variants
726
+ - generic functions
727
+ - generic functions with explicit `implements` interface constraints
728
+ - explicit specialization calls
729
+ - monomorphized code generation
730
+
731
+ Not allowed in v1:
732
+
733
+ - structural or inferred generic constraints (use `implements` for nominal constraints; use reflection and `inline for` for ad-hoc structural checks)
734
+
735
+ Example:
736
+
737
+ ```mt
738
+ struct Slice[T]:
739
+ data: ptr[T]
740
+ len: ptr_uint
741
+
742
+ function first[T](items: Slice[T]) -> ptr[T]?:
743
+ if items.len == 0:
744
+ return null
745
+ return items.data
746
+
747
+ function capacity_of[N](buffer: str_buffer[N]) -> ptr_uint:
748
+ return buffer.capacity()
749
+
750
+ function explicit_capacity(buffer: str_buffer[32]) -> ptr_uint:
751
+ return capacity_of[32](buffer)
752
+ ```
753
+
754
+ Explicit specialization arguments may be type references like `bytes_for[int](4)` or numeric literals like `capacity_of[32](buffer)` when the generic parameter is used in a literal slot such as `str_buffer[N]` or `array[T, N]`.
755
+
756
+ ## Expressions and conversions
757
+
758
+ ### Operators
759
+
760
+ Built-in operators should match familiar C behavior where possible:
761
+
762
+ - arithmetic: `+ - * / %`
763
+ - comparison: `== != < <= > >=`
764
+ - boolean: `and or not`
765
+ - bitwise: `& | ^ ~ << >>`
766
+
767
+ No user-defined operator overloading.
768
+
769
+ Built-in vector, matrix, and quaternion types support component-wise arithmetic with the standard operators:
770
+
771
+ - Vectors (`vecN`/`ivecN`): `+`, `-`, `*` (component-wise same-type); `*`, `/` (scalar); unary `-`
772
+ - Matrices (`matN`): `+`, `-` (component-wise same-type); `*`, `/` (scalar); unary `-`
773
+ - Quaternions (`quat`): `+`, `-`, `*` (component-wise same-type); unary `-`
774
+
775
+ ### Casts
776
+
777
+ Conversions are explicit. Use `T<-expr` for ordinary conversions.
778
+
779
+ ```mt
780
+ let count64 = ulong<-count32
781
+ let value = float<-raw
782
+ let newline = char<-10
783
+ ```
784
+
785
+ For bit reinterpretation, use a separate form inside `unsafe`:
786
+
787
+ ```mt
788
+ unsafe:
789
+ let bits = reinterpret[uint](value)
790
+ ```
791
+
792
+ Binary arithmetic and numeric comparison operators may promote primitive operands to a common type locally.
793
+ This is limited to `+ - * / % == != < <= > >=` and does not change assignment, return, or aggregate-field typing rules.
794
+ Non-external call boundaries remain strict, but external calls may pass enum or flags values to same-width fixed-width integer parameters without an explicit cast for C ABI interop.
795
+ Mixed signed and unsigned integers still require an explicit cast.
796
+
797
+ `char` stays outside the general numeric-promotion rules. If code wants arithmetic on a character value, cast it to an integer type first. If code wants to write bytes back into a `char` buffer, either use `char<-...` explicitly or rely on the expected `char` boundary where a known `char` target is being initialized or assigned.
798
+
799
+ Example:
800
+
801
+ ```mt
802
+ let newline = char<-10
803
+
804
+ unsafe:
805
+ buffer[0] = 65
806
+ buffer[1] = newline
807
+ let code = int<-buffer[0]
808
+ ```
809
+
810
+ ### Literals
811
+
812
+ ```mt
813
+ 42
814
+ 0xff
815
+ 0b1010
816
+ 3.14159
817
+ "hello"
818
+ c"hello"
819
+ ```
820
+
821
+ Integer literals are untyped until context resolves them, defaulting to `int`.
822
+ Float literals default to `float` when unconstrained.
823
+
824
+ Typed contexts may adopt the expected numeric type for a literal directly. This is limited to literal typing, not general implicit conversion.
825
+
826
+ Exact compile-time numeric constants may also fit an explicit numeric target without a manual cast when the value is representable exactly in that target type. This applies to literals, named `const`s, and immutable locals whose initializer is itself a compile-time numeric constant. If the value would lose range or precision, the compiler rejects it; this is a type error, not a linter warning.
827
+
828
+ There is one additional narrow boundary rule for float-heavy code: a primitive integer expression may flow into an expected float type for an explicitly typed local declaration, an assignment to a float-typed lvalue (`=` and arithmetic compound assignment operators), a return expression from a float-returning function, an ordinary function argument with a float parameter type, or an aggregate/variant field initializer with a float field type.
829
+
830
+ This is still a boundary cast, not a general usual-arithmetic-conversions model. It does not widen public constant initialization or arbitrary expression typing. Integer arithmetic stays integer arithmetic until that final boundary cast, so `let ratio: float = hits / total` still performs integer division before the result is converted.
831
+
832
+ Foreign/external numeric boundaries are stricter: runtime implicit conversion is limited to provably lossless widening within the same numeric family. Truncating conversions and runtime implicit integer-to-float or float-to-integer conversions at foreign boundaries require an explicit cast. Exact compile-time numeric constants are still allowed when the target type can represent them exactly.
833
+
834
+ Examples of typed contexts:
835
+
836
+ - a declaration with an explicit type
837
+ - a function argument with a known parameter type
838
+ - a struct field initializer with a known field type
839
+ - a return expression with a known return type
840
+
841
+ This keeps `float`-heavy game code readable while preserving the rule that ordinary expressions do not silently convert between numeric types.
842
+
843
+ ### Composite literals
844
+
845
+ There are no constructors with hidden logic in v1. Aggregate construction uses field and element literals directly.
846
+ Multiline aggregate-style calls may use trailing commas so data-heavy code stays easy to diff.
847
+ Omitted fields in plain aggregate literals and omitted tail elements in fixed-array literals default to zero.
848
+ That means `Type()`, `Type(field = value)`, `array[T, N]()`, and `array[T, N](a, b)` are all just zero-default data literals, not constructor calls.
849
+ There are still no default field expressions, hidden initializer functions, or other non-local effects during construction.
850
+ Typed locals without `= ...` remain the declaration form for zero-initialized storage; these aggregate literals are the expression forms when code needs a value.
851
+
852
+ ```mt
853
+ let player = Player(
854
+ position = Vec2(x = 10.0, y = 20.0),
855
+ velocity = Vec2(x = 0.0, y = 0.0),
856
+ )
857
+
858
+ let origin = Player()
859
+
860
+ let palette = array[uint, 4](0xff0000ff, 0x00ff00ff, 0x0000ffff, 0xffffffff)
861
+ let grayscale = array[uint, 4](0x111111ff, 0x555555ff)
862
+ ```
863
+
864
+ This keeps data construction obvious and maps cleanly to C initializers.
865
+
866
+ ## Memory model
867
+
868
+ The memory model must feel like C with better defaults and cleaner surfaces.
869
+
870
+ ### Value semantics by default
871
+
872
+ - Scalars copy by value.
873
+ - Structs copy by value.
874
+ - Fixed arrays copy by value.
875
+ - Returning a struct is allowed and lowers to normal C return or out-parameter lowering as needed.
876
+
877
+ There is no hidden reference counting and no hidden heap boxing.
878
+
879
+ ### Explicit allocation
880
+
881
+ Heap allocation is always explicit and allocator-driven.
882
+
883
+ ```mt
884
+ import std.mem.heap as heap
885
+
886
+ function spawn_enemy(start: Vec2) -> own[Enemy]:
887
+ let enemy = heap.must_alloc[Enemy](1)
888
+ enemy.position = start
889
+ enemy.health = 100
890
+ return enemy
891
+ ```
892
+
893
+ The default heap allocation functions return nullable pointers because C allocation can fail: `alloc_bytes`, `alloc_zeroed_bytes`, `resize_bytes`, `alloc[T]`, `alloc_zeroed[T]`, and `resize[T]` all return `...?`. Code that wants explicit error handling checks for `null`. Code that wants simple fail-fast behavior uses `must_alloc*` or `must_resize*`, which fatal on allocation failure.
894
+
895
+ Allocator surfaces are semantic, not stylistic. Heap, arena, pool, and stack model different ownership and lifetime stories; the language should not add alias APIs that make them feel interchangeable.
896
+
897
+ Recommended standard memory surfaces:
898
+
899
+ - `std.mem.heap` for general allocation and the raw `*_bytes` boundary
900
+ - `std.mem.arena` for frame, level, and scratch lifetimes
901
+ - `std.mem.pool` for fixed-size object pools
902
+ - `std.mem.stack` for explicit temporary allocators
903
+
904
+ Typed allocation helpers live on the module surface where generic methods are not available yet. For example, `heap.must_alloc[Enemy](1)`, `arena.alloc[Enemy](ref_of(scratch), 4)`, `pool.alloc[Enemy](ref_of(objects))`, and `stack.alloc[Enemy](ref_of(temp), 2)` all stay explicit about allocator choice, while raw byte APIs remain available for lower-level storage work.
905
+
906
+ ### Pointers and references
907
+
908
+ Pointers are still first-class because game code needs raw memory and FFI. The source model is explicit and uniform.
909
+
910
+ ```mt
911
+ let position_ref = ref_of(player.position)
912
+ let position_ptr = ptr_of(position_ref)
913
+
914
+ position_ref.x += 1
915
+
916
+ unsafe:
917
+ position_ptr.x += 1
918
+ ```
919
+
920
+ Rules for safe references:
921
+
922
+ - `ref[T]` is a non-null writable safe alias to one live object.
923
+ - `ref_of(expr)` requires a mutable addressable lvalue source and produces `ref[T]`.
924
+ - calling a function that expects `ref[T]` may pass a mutable addressable `T` directly; the compiler borrows it as if `ref_of(expr)` had been written explicitly.
925
+ - `const_ptr_of(expr)` requires an addressable lvalue source and produces `const_ptr[T]` for read-only raw interop.
926
+ - `read(ref_value)` is safe and yields the referenced lvalue/value.
927
+ - `ptr_of(expr)` forms a writable raw pointer from a mutable safe lvalue.
928
+ - `ptr_of(ref_value)` converts an existing safe reference to `ptr[T]` explicitly.
929
+ - member access and method calls auto-project through refs, so `handle.field` and `handle.edit_method()` are the preferred forms.
930
+ - there is no implicit ref-to-value call conversion: if a function expects `T`, pass `read(handle)`.
931
+ - references do not support arithmetic, pointer indexing, or nullable semantics.
932
+ - writable references are non-escaping in the current implementation: they may be used in locals and non-external function parameters, and imported foreign parameters may expose `out` or `inout` boundary forms that lower to raw pointers, but refs themselves still cannot be stored, nested inside other types, returned, or used directly in raw `external` files.
933
+
934
+ Rules for raw pointers:
935
+
936
+ - `ptr[T]`, `ptr[T]?`, `const_ptr[T]`, and `const_ptr[T]?` are raw pointer values.
937
+ - there is no source `&expr`, `*ptr`, or `ptr->field`.
938
+ - spell writable address formation as `ref_of(expr)`, read-only raw address formation as `const_ptr_of(expr)`, and writable raw pointer formation as `ptr_of(expr)` when you truly need a raw pointer.
939
+ - `read(ptr)` dereferences a raw pointer and requires `unsafe`.
940
+ - `const_ptr[T]` is the read-only raw-pointer surface and lowers to C `const T*`. `const_ptr[void]` is valid and represents C `const void *`.
941
+ - `ptr.field` and `ptr.method()` access pointee fields and methods through a raw pointer and require `unsafe`.
942
+ - pointer arithmetic and pointer indexing remain `unsafe`.
943
+ - raw pointer offsets and indices may use ordinary integer expressions directly; code does not need a pre-emptive cast to `ptr_uint` just to write `ptr[i]` or `ptr + offset`.
944
+ - pointer comparison is explicit and never treated as boolean truthiness.
945
+ - `ptr[char]` is the ordinary representation for mutable C text and byte-oriented FFI buffers; writing control bytes such as NUL or newline uses `char` values, typically spelled with `char<-0` and `char<-10`.
946
+ - imported foreign declarations may project ABI-identical pointer forms at the boundary. A raw `ptr[void]` parameter may surface as `ptr[T]?` or an opaque handle type when the imported declaration says so. Reinterpretation inside user code still requires an explicit `T<-value` cast and, when dereferenced, `unsafe`.
947
+
948
+ References are separate from methods:
949
+
950
+ - plain methods still receive values.
951
+ - `editable methods` use the writable implicit receiver and require an addressable call target.
952
+ - `static function` methods receive nothing.
953
+ - `ref[T]` is for explicit aliasing in APIs, not hidden receiver lowering.
954
+
955
+ This gives the language clear aliasing tools instead of one overloaded surface:
956
+
957
+ - `ref[T]` for safe aliasing of one mutable object
958
+ - `own[T]` for owning heap pointers with auto-deref (storable, returnable, nullable)
959
+ - `ptr[T]` / `ptr[T]?` for writable raw memory and FFI
960
+ - `const_ptr[T]` / `const_ptr[T]?` for read-only raw memory and FFI
961
+ - `span[T]` for sized borrowed views over raw pointer data
962
+
963
+ ### Spans
964
+
965
+ Raw pointers are necessary. Spans are the readable everyday view.
966
+
967
+ ```mt
968
+ span[T] is conceptually:
969
+ data: ptr[T]
970
+ len: ptr_uint
971
+ ```
972
+
973
+ `span[T]` should be built into the language surface as a standard view type because it is the right default for arrays, buffers, decoded file content, vertex streams, and audio samples.
974
+
975
+ `span[T]` is the many-element view. `ref[T]` is the safe writable single-object alias, while `const_ptr[T]` is the raw read-only single-object pointer form.
976
+
977
+ For frequent pointer-plus-length construction in game code, construct spans directly with `span[T](data = ..., len = ...)`. If a project wants a shorter spelling, it should define a project-local helper instead of assuming a standard helper module.
978
+
979
+ ### Standard library foundation
980
+
981
+ The standard library follows the same rule as the language: no hidden allocation, no implicit ownership transfer, and no runtime machinery the source did not ask for.
982
+
983
+ Implemented core modules:
984
+
985
+ - `Option[T]` and `Result[T, E]` are standard library generic variant types (auto-imported via prelude) for APIs where explicit optional or fallible values are clearer than nullable pointers.
986
+ - `std.string.String` is the normal growable owned UTF-8 text surface. Its public API should mirror the mutable-text shape of `str_buffer[N]`: method-style `len`, `capacity`, `append`, `assign`, `clear`, `as_str`, `to_cstr`, and explicit constructors, not a parallel module-function vocabulary. Byte-level appends exist as low-level escape hatches, but `as_str` and `to_cstr` still enforce valid UTF-8 at the borrowed-text boundary.
987
+ - `std.str` provides borrowed string helpers: UTF-8 validation, byte lookup, prefix/suffix/equality, ASCII trimming, and byte search.
988
+ - `std.fmt` is the explicit formatting subsystem. It should be the single normal formatting engine for owned and fixed-capacity text rather than one option among many formatting styles. `f"..."` produces borrowed `str`; `fmt.format(f"...")` is the explicit owned-text allocation path when you need a `std.string.String`. Low-level append helpers remain implementation building blocks.
989
+ - `std.async` provides the first-party async runtime surface.
990
+
991
+ ### Lifetime story
992
+
993
+ Milk Tea should make lifetime choices explicit at the API level.
994
+
995
+ The model:
996
+
997
+ - stack values for local temporaries
998
+ - arenas for frame and level data
999
+ - pools for stable object storage
1000
+ - explicit heap allocation when needed
1001
+ - `unsafe` for manual aliasing and lifetime tricks
1002
+
1003
+ This is enough to write fast engine and game code without dragging the user through ownership proofs.
1004
+
1005
+ ## Events
1006
+
1007
+ Milk Tea has a built-in typed publisher/subscriber surface with fixed-capacity listener storage. The design follows the same rule as the rest of the language: no hidden allocation, no hidden heap traffic, and the cost of dispatch is visible in the source.
1008
+
1009
+ ### Why fixed capacity
1010
+
1011
+ Event declarations spell their capacity at the declaration site:
1012
+
1013
+ ```mt
1014
+ public event closed[4]
1015
+ public event resized[8](ResizeEvent)
1016
+ ```
1017
+
1018
+ This is deliberate. When `emit()` fires, the runtime snapshots all active listener slots into a **stack-allocated array** (`snapshots[capacity]`). That means:
1019
+
1020
+ - **Zero heap allocations** during dispatch — critical for real-time, audio, and embedded contexts
1021
+ - **Predictable latency** — dispatch time is bounded by capacity, not by an unbounded growable list
1022
+ - **No malloc/free churn** when subscribers join or leave frequently
1023
+
1024
+ If capacity were dynamic, every `subscribe()` would potentially need `realloc`, and `emit()` would snapshot a heap-allocated list. That is a hidden cost incompatible with the language's rule that allocation must be spelled in source.
1025
+
1026
+ The tradeoff is that `subscribe()` can fail with `EventError.full`. This is the same tradeoff as `array[T, N]` (fixed, stack-suitable) versus `Vec[T]` (growable, heap-managed). The right choice depends on the use case:
1027
+
1028
+ - **Single-observer UIs** (button click, window close): use a plain function-pointer field on the struct, not an event
1029
+ - **Small fixed fan-out** (game state changed, level loaded): capacity 8–32 is typically generous; the stack cost is negligible
1030
+ - **Unbounded observers** (logging hooks, inspector tooling): subscribe one dispatcher handler to the event, and maintain a dynamic observer list inside it
1031
+
1032
+ ### Why emit is module-private
1033
+
1034
+ `emit()` is only callable from within the module that declares the event. This is not about visibility — it is about **predictable side effects**. If any module could trigger emission, every subscriber anywhere in the program would become a potential side effect of that emit call. Restricting emission to the declaring module makes the control flow explicit: you can see who fires the event by looking at one file.
1035
+
1036
+ Subscribing and unsubscribing from a public event from another module is always allowed. Observing is safe; triggering is the privileged operation.
1037
+
1038
+ ### Slot lifetime model
1039
+
1040
+ The runtime stores listeners in a fixed array of slots, each with:
1041
+
1042
+ - an `active` flag
1043
+ - a `once` flag (for one-shot subscribers)
1044
+ - a `generation` counter (incremented on each new subscription into that slot)
1045
+ - a `listener` pointer
1046
+ - an optional `state` pointer (for stateful subscribers)
1047
+ - an optional `wait_frame` pointer (for async wait)
1048
+
1049
+ When `subscribe()` finds a free slot, it increments that slot's generation counter and returns a `Subscription` handle containing both the slot index and the generation. `unsubscribe(sub)` validates both the index and generation before deactivating the slot. This prevents use-after-free: if a slot is reused after an unsubscribe, old subscription handles become stale because their generation no longer matches.
1050
+
1051
+ `subscribe_once()` sets the `once` flag; the slot is automatically deactivated after the next emission fires.
1052
+
1053
+ ### Why no auto-grow
1054
+
1055
+ Growth is allocation. Allocation in the hot path (during `subscribe()` or `emit()`) is exactly what the language was designed to avoid. If you need an unbounded listener list, the idiomatic pattern is to build it yourself on top of the primitives — just as you would use `Vec[T]` when `array[T, N]` is too rigid. The event system is the `array[T, N]` of pub/sub: bounded, predictable, allocation-free.
1056
+
1057
+ ## Error handling
1058
+
1059
+ Exceptions do not belong here.
1060
+
1061
+ Preferred strategy:
1062
+
1063
+ - `Result[T, E]` for recoverable failures
1064
+ - `get(arr, i)` for recoverable bounds-checked array/span indexing that returns `ptr[T]?` (null on OOB)
1065
+ - `fatal("message")` for programmer errors and impossible states
1066
+ - explicit status codes for imported foreign APIs when that matches the C API better
1067
+
1068
+ Example:
1069
+
1070
+ ```mt
1071
+
1072
+
1073
+ enum LoadError: ubyte
1074
+ file_not_found = 1
1075
+ invalid_format = 2
1076
+
1077
+ function load_level(path: str, arena: ptr[Arena]) -> Result[Level, LoadError]:
1078
+ let json = read_text_file(path, arena)
1079
+ if json == null:
1080
+ return Result[Level, LoadError].failure(error= LoadError.file_not_found)
1081
+
1082
+ return parse_level(json)
1083
+ ```
1084
+
1085
+ No implicit exception paths. Every failure path is visible in the type or the code.
1086
+
1087
+ ## Modules and packaging
1088
+
1089
+ Source files should map directly to modules.
1090
+
1091
+ ```mt
1092
+ import std.raylib as rl
1093
+ import game.assets
1094
+ ```
1095
+
1096
+ Rules:
1097
+
1098
+ - one module identity per file, derived from its path
1099
+ - explicit imports only
1100
+ - no wildcard imports in v1
1101
+ - no cyclic imports
1102
+ - package naming stays filesystem-friendly
1103
+
1104
+ Recommended layout:
1105
+
1106
+ - `std.*` for core library modules and imported foreign modules
1107
+ - `std.c.*` for raw bindgen-generated C modules
1108
+ - project modules under their own root namespace
1109
+
1110
+ Handwritten wrappers may still exist for real policy or domain logic, but they are not the primary answer to FFI noise. The primary interop story should be raw `std.c.*` modules plus compiler-recognized imported foreign declarations.
1111
+
1112
+ ## FFI design
1113
+
1114
+ FFI is a core feature, not a bolt-on.
1115
+
1116
+ Milk Tea needs two interop surfaces, not one:
1117
+
1118
+ 1. a raw ABI surface for exact bindings
1119
+ 2. an imported foreign surface for ordinary Milk Tea code
1120
+
1121
+ The declaration site carries the boundary contract. Raw pointers, reinterpretation, and manual memory walking remain explicit.
1122
+
1123
+ ### Raw C bindings
1124
+
1125
+ Milk Tea needs a dedicated `external` file form for ABI-exact bindings.
1126
+
1127
+ Direct `external function` declarations are also allowed in ordinary modules for small manual ABI bridges, but generated and standard-library bindings should prefer full `external` files so the raw surface stays grouped, auditable, and easy to regenerate.
1128
+
1129
+ That dedicated file form is intentional. Raw binding modules carry module-level `include`, `link`, and `compiler_flag` directives and act as the exact ABI layer, while ordinary headerless files remain the normal policy-oriented module form. The distinction is there to keep raw ABI and build-surface concerns grouped instead of smearing them across ordinary modules.
1130
+
1131
+ ```mt
1132
+ external
1133
+
1134
+ link "raylib"
1135
+ include "raylib.h"
1136
+
1137
+ struct Vector2:
1138
+ x: float
1139
+ y: float
1140
+
1141
+ struct Color:
1142
+ r: ubyte
1143
+ g: ubyte
1144
+ b: ubyte
1145
+ a: ubyte
1146
+
1147
+ external function InitWindow(width: int, height: int, title: cstr) -> void
1148
+ external function WindowShouldClose() -> bool
1149
+ external function BeginDrawing() -> void
1150
+ external function EndDrawing() -> void
1151
+ external function DrawCircleV(center: Vector2, radius: float, color: Color) -> void
1152
+ ```
1153
+
1154
+ Capabilities required by the FFI surface:
1155
+
1156
+ - exact integer and float widths
1157
+ - exact struct field order
1158
+ - packed structs and explicit alignment
1159
+ - unions
1160
+ - opaque handles
1161
+ - function pointers and callbacks
1162
+ - `cdecl` and future calling convention markers
1163
+ - C varargs for APIs like `printf`
1164
+ - constant and macro import where clang can resolve the value
1165
+
1166
+ ### Bindgen
1167
+
1168
+ Bindings for libc, libm, raylib, SDL3, Box2D, stb, miniaudio, json-c, and similar libraries should be generated with clang.
1169
+
1170
+ Bindgen output rules:
1171
+
1172
+ 1. Preserve ABI-visible names exactly in raw `std.c.*` modules.
1173
+ 2. Emit the thinnest possible surface. No runtime marshaling.
1174
+ 3. Emit opaque types instead of guessing private layouts.
1175
+ 4. Emit comments or metadata that make the original C header traceable.
1176
+ 5. Prefer enums, flags, structs, unions, constants, and function declarations over clever wrappers.
1177
+ 6. When headers are clear enough, emit metadata that can drive generated imported foreign declarations for borrowed strings, transient strings, out parameters, spans, opaque handles, and release functions. If the header is not clear enough, stay raw instead of guessing.
1178
+
1179
+ The imported layer may also be generated, but only from an explicit checked-in policy file. clang bindgen is responsible for ABI facts; the policy file is responsible for semantic facts the header does not encode, such as `str as cstr`, `out`, `inout`, `consuming`, span fan-out, selected renames, and which raw declarations should remain exposed only through `std.c.*`.
1180
+
1181
+ The raw layer should be inspectable and boring. Handwritten wrappers are optional, but they are not the language's primary ergonomics mechanism.
1182
+
1183
+ ### Imported foreign declarations
1184
+
1185
+ Foreign call ergonomics are implemented directly through `foreign function` declarations with boundary projections (`as`), parameter modes (`in`, `out`, `inout`, `consuming`), and mapping expressions — no separate follow-on proposal document is needed.
1186
+
1187
+ Most application code should call imported foreign declarations, not raw `std.c.*` bindings.
1188
+
1189
+ Imported foreign modules are ordinary headerless files. They import a raw `std.c.*` module and re-export compiler-recognized `foreign function` declarations.
1190
+
1191
+ ```mt
1192
+ import std.c.raylib as c
1193
+
1194
+ public type Vector2 = c.Vector2
1195
+ public type Texture = c.Texture
1196
+ public type Color = c.Color
1197
+
1198
+ public const BLACK: Color = c.BLACK
1199
+ public const GOLD: Color = c.GOLD
1200
+
1201
+ public foreign function init_window(width: int, height: int, title: str as cstr) -> void = c.InitWindow
1202
+ public foreign function close_window() -> void = c.CloseWindow
1203
+ public foreign function window_should_close() -> bool = c.WindowShouldClose
1204
+ public foreign function get_frame_time() -> float = c.GetFrameTime
1205
+
1206
+ public foreign function load_texture(path: str as cstr) -> Texture = c.LoadTexture
1207
+ public foreign function load_file_data(file_name: str as cstr, out data_size: int) -> ptr[ubyte]? = c.LoadFileData
1208
+ public foreign function save_file_data(file_name: str as cstr, data: span[ubyte]) -> bool = c.SaveFileData(file_name, data.data, int<-data.len)
1209
+ public foreign function set_shader_value[T](shader: Shader, loc_index: int, in value: T as const_ptr[void], uniform_type: int) -> void = c.SetShaderValue
1210
+
1211
+ public foreign function mem_alloc[T](count: ptr_uint) -> ptr[T]? = c.MemAlloc(count * uint<-size_of(T))
1212
+ public foreign function mem_realloc[T](memory: ptr[T]?, count: ptr_uint) -> ptr[T]? = c.MemRealloc(memory, count * uint<-size_of(T))
1213
+ public foreign function mem_free[T](memory: ptr[T]?) -> void = c.MemFree(memory)
1214
+ ```
1215
+
1216
+ Types, enums, flags, and constants that need no boundary conversion should usually be re-exported with ordinary `public type`, `public const`, `enum`, or `flags` declarations. `foreign function` is for call boundaries, not for everything else in the module.
1217
+
1218
+ These declarations are not handwritten wrappers:
1219
+
1220
+ - one imported declaration maps directly to one foreign symbol
1221
+ - there is no extra function body, hidden dispatch, or policy object
1222
+ - lowering stays inspectable in generated C
1223
+ - boundary conversions are declared once on the import instead of repeated at every call site
1224
+
1225
+ Chosen v1 form:
1226
+
1227
+ - `foreign function` is a new declaration kind allowed in ordinary modules
1228
+ - the left side is the public Milk Tea signature
1229
+ - the right side is either a raw symbol name or a declarative raw call expression
1230
+ - `= c.Symbol` is shorthand for positional lowering when surface parameters map directly to the raw ABI after boundary conversion
1231
+ - `= c.Symbol(...)` is required when one surface parameter fans out into multiple raw arguments, when argument order changes, or when the raw API needs size arithmetic such as `count * size_of(T)`
1232
+
1233
+ The right side is deliberately not a normal function body. It is a restricted lowering clause. It may use:
1234
+
1235
+ - the referenced raw foreign symbol
1236
+ - imported parameters
1237
+ - field access such as `data.data` and `data.len`
1238
+ - `T<-expr`, `size_of`, `align_of`, literals, `null`, and simple arithmetic
1239
+
1240
+ It may not use:
1241
+
1242
+ - control flow
1243
+ - local declarations
1244
+ - arbitrary function calls
1245
+ - heap allocation
1246
+ - `unsafe` blocks
1247
+
1248
+ An imported foreign declaration must be able to express at least:
1249
+
1250
+ - a `str` parameter that lowers to `cstr` at the foreign boundary
1251
+ - ordinary `span[str]` or `array[str, N]` inputs for foreign string-list APIs
1252
+ - automatic transient boundary marshalling for dynamic text when the imported declaration chooses that public surface
1253
+ - `in`, `out`, and `inout` parameters that lower to raw pointers
1254
+ - release-style functions that consume a handle and null the caller binding afterward
1255
+ - pointer-plus-length views that lower from `span[T]`
1256
+ - identity ABI projections such as `ptr[T]?` to `ptr[void]` or `cstr?` to `ptr[char]?`
1257
+
1258
+ Parameter and boundary rules:
1259
+
1260
+ - `name: str as cstr` means the public Milk Tea type is `str`, while the raw foreign target expects `cstr` or `ptr[char]`
1261
+ - imported foreign declarations do not spell this surface as `str as ptr[char]`; the public text boundary stays `str as cstr` even when the raw callee argument type is `ptr[char]`
1262
+ - a string literal or existing `cstr` value may satisfy `str as cstr` without temporary storage
1263
+ - a dynamic `str` argument for `str as cstr` is materialized automatically at the foreign boundary for the duration of the call
1264
+ - imported foreign declarations may accept `span[str]` or `array[str, N]` for string-list APIs even when the raw callee wants `span[cstr]`, `span[ptr[char]]`, or pointer-plus-length forms; that marshalling belongs to the declaration, not to the call site
1265
+ - `in name: T` means the raw foreign target takes a read-only pointer; call sites pass an ordinary expression and the boundary lowers by taking a const address, materializing a temporary first when the expression is not directly addressable
1266
+ - `out name: T` means the raw foreign target takes a writable pointer; call sites pass an ordinary mutable addressable lvalue
1267
+ - `inout name: T` means the raw foreign target reads and writes through a pointer; call sites pass an ordinary mutable addressable lvalue
1268
+ - `consuming name: Handle` means the public Milk Tea parameter is a non-null opaque handle or `ptr[T]`, and v1 uses it only for release-style foreign calls that consume an existing nullable binding
1269
+ - plain parameters and return values require exact compatibility or an explicitly permitted identity ABI projection
1270
+
1271
+ #### Sema rules for foreign defs
1272
+
1273
+ The checker should treat `foreign function` as its own declaration kind with dedicated rules.
1274
+
1275
+ Declaration rules:
1276
+
1277
+ - `foreign function` is allowed only in ordinary modules, not inside `external`, `extending` blocks, or function bodies
1278
+ - the right-hand side must resolve to an imported raw external symbol from a `std.c.*` module
1279
+ - `= c.Symbol` is shorthand symbol mapping; `c.Symbol` must resolve to an imported `external function`
1280
+ - `= c.Symbol(...)` is declarative RHS mapping; the callee must resolve to an imported `external function`
1281
+ - the right-hand side may reference only declaration parameters and imported raw symbols from the module scope
1282
+ - a `consuming` parameter may not use `as`, must have a non-null `opaque Handle` or `ptr[T]` type, and any `foreign function` that has a `consuming` parameter must return `void`
1283
+
1284
+ Shorthand symbol mapping rules:
1285
+
1286
+ - the public parameter count must match the raw parameter count, excluding raw varargs
1287
+ - parameters map positionally in source order
1288
+ - each mapped pair must satisfy one of: exact type match, declared boundary mapping such as `str as cstr`, directional pointer mapping through `out` or `inout`, or an allowed identity ABI projection
1289
+ - an `in` parameter may use `as const_ptr[...]`, including `as const_ptr[void]`, to express read-only foreign borrows such as Raylib shader uniform values
1290
+ - aliasing a public imported-binding type to a different raw module type does not create a new identity ABI projection. Two raw structs from different `std.c.*` modules remain distinct unless the language grows an explicit foreign type-projection rule for that pair.
1291
+ - the public return type must either exactly match the raw return type or be reachable through an allowed identity ABI projection
1292
+ - shorthand mapping is rejected if any surface parameter must fan out into multiple raw arguments, if any raw argument needs arithmetic over more than one parameter, or if raw argument order differs from public argument order
1293
+
1294
+ Declarative RHS mapping rules:
1295
+
1296
+ - the RHS must be a raw call expression whose callee is an imported raw external function
1297
+ - raw call arity must match the raw target signature, respecting raw varargs rules
1298
+ - the checker analyzes each raw argument expression in an environment containing the public parameters
1299
+ - allowed RHS expression forms are: parameter identifiers, member access, index access, literals, `null`, `size_of`, `align_of`, `offset_of`, explicit `T<-expr` casts, and simple arithmetic or comparisons built from those forms
1300
+ - disallowed RHS forms are: control flow, local declarations, assignment, `unsafe`, `defer`, heap allocation, and arbitrary non-builtin function calls
1301
+ - every public parameter must be consumed by the RHS according to its mode
1302
+
1303
+ Parameter consumption rules:
1304
+
1305
+ - a plain parameter may be referenced one or more times in the RHS
1306
+ - a `span[T]` parameter may be split into `.data` and `.len`
1307
+ - an `out` parameter must appear exactly once as a bare parameter reference in the raw argument position that receives the writable pointer
1308
+ - an `in` parameter must appear exactly once as a bare parameter reference in the raw argument position that receives the read-only pointer
1309
+ - an `inout` parameter must appear exactly once as a bare parameter reference in the raw argument position that receives the mutable pointer
1310
+ - a `consuming` parameter lowers through the same identity value as a plain handle parameter; v1 ownership affects call-site validation and post-call flow, not the RHS mapping expression shape
1311
+ - a `str as cstr` parameter may appear only in raw argument positions whose target type is `cstr` or `ptr[char]`
1312
+
1313
+ Call-site checking rules:
1314
+
1315
+ - a call to a `foreign function` with `out` parameters requires an ordinary mutable addressable lvalue at the corresponding argument position
1316
+ - a call to a `foreign function` with `in` parameters accepts an ordinary expression at the corresponding argument position
1317
+ - a call to a `foreign function` with `inout` parameters requires an ordinary mutable addressable lvalue at the corresponding argument position
1318
+ - a call to a `foreign function` with `consuming` parameters requires a bare identifier naming a nullable local or parameter binding
1319
+ - the current flow type of that binding must already be the non-null handle type required by the `consuming` parameter
1320
+ - in v1, a foreign call with any `consuming` parameter must be a top-level expression statement; `defer`, local initializers, assignments, returns, and larger expressions are rejected
1321
+ - after a `consuming` foreign call, continuation flow refines each consumed binding to `null`
1322
+ - legacy call-site markers such as `in expr`, `out lvalue`, and `inout lvalue` are rejected semantically; boundary directionality lives on the imported declaration
1323
+ - `in` parameters accept ordinary expressions; if an expression is not addressable, lowering creates a short-lived temporary before taking its const address
1324
+ - `out` parameters require a mutable addressable lvalue and do not read the old value before the call
1325
+ - `inout` parameters require a mutable addressable lvalue and expose both the old and new value to the callee
1326
+ - automatic foreign text marshalling is part of imported-boundary checking, not an extra source clause
1327
+ - string literals and existing `cstr` values lower directly where possible, so the compiler only materializes temporary storage when the public argument actually needs it
1328
+
1329
+ Allowed identity ABI projections in v1 are deliberately narrow:
1330
+
1331
+ - `ptr[T]` or `ptr[T]?` to `ptr[void]` or `ptr[void]?`
1332
+ - `ptr[T]` or `ptr[T]?` to `const_ptr[void]` or `const_ptr[void]?` for read-only foreign pointer projections
1333
+ - `ptr[void]` or `ptr[void]?` to `opaque Handle` or `opaque Handle?` when the imported declaration chooses that handle surface
1334
+ - `ptr[char]` or `ptr[char]?` to `cstr` or `ptr[char]?` when mutability rules permit the raw direction
1335
+ - raw opaque pointer returns to typed pointer-like public returns when the representation is unchanged
1336
+
1337
+ Anything outside those cases requires the raw layer or an explicit later language feature. `foreign function` is not a general coercion system.
1338
+
1339
+ #### Lowering rules for foreign calls
1340
+
1341
+ `foreign function` lowers directly to the referenced raw call boundary. It does not lower as a normal Milk Tea function body.
1342
+
1343
+ Shorthand symbol mapping lowering:
1344
+
1345
+ - the call target is the referenced raw symbol
1346
+ - arguments are lowered left-to-right in public parameter order
1347
+ - each public argument lowers through its declared boundary rule and then into the matching raw parameter slot
1348
+ - if the return type needs an allowed identity ABI projection, the backend emits the minimal explicit C cast required by the target types
1349
+
1350
+ Declarative RHS mapping lowering:
1351
+
1352
+ - the lowering clause is expanded by substituting lowered public arguments into the restricted RHS expression tree
1353
+ - the expanded raw call becomes the emitted call target and raw argument list
1354
+ - field access such as `data.data` and `data.len` lowers exactly as the corresponding member access on the public argument value
1355
+ - builtin forms such as `T<-expr`, `size_of`, `align_of`, and `offset_of` lower exactly as they do elsewhere in the language
1356
+
1357
+ Automatic text marshalling lowering:
1358
+
1359
+ - if a public foreign boundary uses `str`, `span[str]`, or `array[str, N]` and the raw callee needs C-compatible text storage, the compiler materializes that storage automatically for the duration of the call
1360
+ - materialization happens in left-to-right argument order so evaluation stays inspectable
1361
+ - literal-backed and already-compatible arguments lower directly with no temporary storage
1362
+ - dynamic `str` values lower to temporary NUL-terminated C strings when needed
1363
+ - string lists lower to temporary C-string arrays or pointer-plus-length views when needed
1364
+ - the temporary storage is released immediately after the raw call completes
1365
+ - if code needs exact storage control or wants to avoid boundary marshalling entirely, it may call the raw `std.c.*` layer or pass already-compatible `cstr` / `span[char]` values directly
1366
+
1367
+ Directional pointer lowering:
1368
+
1369
+ - an `out` parameter lowers its call-site lvalue by taking its address at the raw call boundary without exposing `ptr_of(...)` in user source
1370
+ - an `in` parameter lowers its call-site argument by taking a const address at the raw call boundary without exposing `const_ptr_of(...)` or casts in user source; non-addressable operands lower through a visible temporary in statement-shaped foreign calls
1371
+ - an `inout` parameter lowers the same way as `out`, but sema preserves the read-write contract instead of pure output
1372
+ - if the raw target expects `ptr[void]` or another identity-projection pointer type, lowering inserts only the minimal cast required by the raw C signature
1373
+
1374
+ Release-function ownership lowering:
1375
+
1376
+ - a `consuming` argument lowers through the same raw boundary value as the corresponding plain handle argument
1377
+ - immediately after the raw foreign call completes, lowering emits `binding = null` for each consumed binding
1378
+ - the same statement updates continuation flow so later code sees that binding as `null`
1379
+ - v1 does not add a general move checker; this null-after-call rule is limited to bare nullable local or parameter bindings in top-level expression statements
1380
+
1381
+ Generated C quality rule:
1382
+
1383
+ - lowering may introduce short-lived temporaries for foreign text marshalling, result spilling, or identity casts
1384
+ - lowering should not emit an extra helper function for each `foreign function` in the ordinary case
1385
+ - the preferred output is one visible raw call site whose surrounding temporaries make the boundary work obvious in C
1386
+
1387
+ Call sites should read like this:
1388
+
1389
+ ```mt
1390
+ rl.init_window(screen_width, screen_height, "Milk Tea")
1391
+ let texture = rl.load_texture(path)
1392
+ let file_data = rl.load_file_data("storage.data", data_size)
1393
+ let success = rl.save_file_data("storage.data", bytes)
1394
+ let ints = rl.mem_alloc[int](16)
1395
+ ```
1396
+
1397
+ `str as cstr` and `span[str]` foreign boundaries use ordinary imported-call syntax. When the boundary needs synthesized temporary C-compatible storage or other statement-shaped setup, lowering hoists that work into visible temporary locals and branch-local control flow as needed, so nested call arguments, arithmetic, `if ...: ... else: ...` expressions, and short-circuit boolean expressions still read like ordinary Milk Tea while generated C stays explicit about the temporary storage.
1398
+
1399
+ `in`, `out`, and `inout` are declaration-side foreign-boundary forms, not imported-call expressions. They control how ordinary call-site arguments lower at the boundary without exposing `const_ptr_of(...)`, `ptr_of(...)`, or ABI casts in ordinary code.
1400
+
1401
+ This boundary rule is deliberate: declarations say how the boundary works, while raw pointer code remains explicitly low-level.
1402
+
1403
+ ### Strings and buffers at the FFI boundary
1404
+
1405
+ String and buffer rules must stay explicit, but the explicitness belongs in imported declarations, not in every call site:
1406
+
1407
+ - raw `external` files stay exact and continue to use `cstr`, `ptr[T]`, and `ptr[void]`
1408
+ - a string literal may satisfy a contextual `cstr` position directly; ordinary UI code should not need `c"..."` just to populate `cstr` locals, `array[cstr, N]`, or borrowed C-string arguments
1409
+ - converting a dynamic `str` or `span[str]` to foreign C-compatible text is automatic when an imported declaration chooses that public surface
1410
+ - `cstr` remains available for raw ABI work, returned native strings, and low-level code
1411
+ - pointer-plus-length APIs should import as `span[T]` when the native contract is a view rather than untyped memory
1412
+ - single-object output parameters should import as `out T` or `inout T` instead of naked pointer syntax
1413
+ - `ptr[void]` should stay in the raw layer, but imported declarations may project it to typed pointers or opaque handles when the ABI conversion is identity-only
1414
+
1415
+ This keeps the important distinction intact:
1416
+
1417
+ - source code thinks in `str`, `array[str, N]`, `span[str]`, and one mutable text buffer type
1418
+ - raw memory work still requires raw syntax
1419
+ - call sites stop spelling C representation details that the import declaration already knows
1420
+
1421
+ ### Callbacks
1422
+
1423
+ Callbacks must map directly to C function pointers.
1424
+
1425
+ ```mt
1426
+ type LogCallback = fn(level: int, message: cstr, user_data: ptr[void]) -> void
1427
+ ```
1428
+
1429
+ Capturing closures should not be lowered to hidden heap objects. If user state is needed, pass it explicitly as `user_data` at ABI boundaries or allocate explicit local storage such as `std.cell.alloc[T](...)` in ordinary Milk Tea code.
1430
+
1431
+ ## Data layout and ABI controls
1432
+
1433
+ The language must expose a small set of layout controls for interop and SIMD-friendly code.
1434
+
1435
+ ```mt
1436
+ @[packed]
1437
+ struct FileHeader:
1438
+ magic: array[ubyte, 4]
1439
+ version: ushort
1440
+
1441
+ @[align(16)]
1442
+ struct Mat4:
1443
+ m: array[float, 16]
1444
+ ```
1445
+
1446
+ Required controls:
1447
+
1448
+ - `@[packed]`
1449
+ - `@[align(n)]`
1450
+ - `attribute[target, ...]`
1451
+ - `size_of(T)`
1452
+ - `align_of(T)`
1453
+ - `offset_of(T, field)`
1454
+ - `static_assert(condition, message)`
1455
+
1456
+ These are not niche. They are mandatory for FFI and engine code.
1457
+
1458
+ ## Generated C quality bar
1459
+
1460
+ Milk Tea only succeeds if the generated C is respectable.
1461
+
1462
+ ### Generated C requirements
1463
+
1464
+ 1. Keep one obvious symbol mapping from source to C.
1465
+ 2. Preserve source names as much as the C target allows.
1466
+ 3. Emit straightforward local variables and control flow.
1467
+ 4. Lower methods to plain functions.
1468
+ 5. Lower `match` to `switch` where possible.
1469
+ 6. Lower `defer` to readable cleanup labels or scoped cleanup blocks.
1470
+ 7. Avoid a mandatory runtime beyond what is needed for startup, fatal hooks, and core helper intrinsics.
1471
+
1472
+ Example lowering:
1473
+
1474
+ Milk Tea:
1475
+
1476
+ ```mt
1477
+ struct Player:
1478
+ position: Vec2
1479
+ velocity: Vec2
1480
+
1481
+ extending Player:
1482
+ editable function update(dt: float):
1483
+ this.position.x += this.velocity.x * dt
1484
+ this.position.y += this.velocity.y * dt
1485
+ ```
1486
+
1487
+ Target C:
1488
+
1489
+ ```c
1490
+ typedef struct game_Vec2 {
1491
+ float x;
1492
+ float y;
1493
+ } game_Vec2;
1494
+
1495
+ typedef struct game_Player {
1496
+ game_Vec2 position;
1497
+ game_Vec2 velocity;
1498
+ } game_Player;
1499
+
1500
+ static void game_Player_update(game_Player* this, float dt) {
1501
+ this->position.x += this->velocity.x * dt;
1502
+ this->position.y += this->velocity.y * dt;
1503
+ }
1504
+ ```
1505
+
1506
+ This is the standard to protect. If the generated C becomes harder to read than hand-written C, the language has drifted.
1507
+
1508
+ ## Summary
1509
+
1510
+ Milk Tea should be a language where:
1511
+
1512
+ - the syntax is calm and readable
1513
+ - the type system is explicit but not academic
1514
+ - the memory model is fast and honest
1515
+ - pointers are available without shame
1516
+ - C libraries fit naturally
1517
+ - the generated C remains clean enough to trust
1518
+
1519
+ That combination is the point. The language should feel like a direct place to write systems and game code without being pushed into a second abstraction model just to stay productive.