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.
- checksums.yaml +7 -0
- data/.ruby-version +1 -0
- data/AUTHORS +3 -0
- data/Gemfile +19 -0
- data/Gemfile.lock +82 -0
- data/LICENSE +21 -0
- data/README.md +1208 -0
- data/Rakefile +332 -0
- data/bin/mtc +8 -0
- data/bin/profile-mtc-checks +133 -0
- data/bin/tracy-profiler +0 -0
- data/docs/build-guide.md +536 -0
- data/docs/index.html +2778 -0
- data/docs/language-design.md +1519 -0
- data/docs/language-manual.md +1534 -0
- data/lib/milk_tea/base.rb +49 -0
- data/lib/milk_tea/bindings/bindgen/ast_parser.rb +398 -0
- data/lib/milk_tea/bindings/bindgen/declaration.rb +319 -0
- data/lib/milk_tea/bindings/bindgen/emitter.rb +387 -0
- data/lib/milk_tea/bindings/bindgen/overrides.rb +134 -0
- data/lib/milk_tea/bindings/bindgen/type_mapper.rb +622 -0
- data/lib/milk_tea/bindings/bindgen.rb +207 -0
- data/lib/milk_tea/bindings/cli.rb +121 -0
- data/lib/milk_tea/bindings/imported_bindings/defaults.rb +210 -0
- data/lib/milk_tea/bindings/imported_bindings/generator.rb +1114 -0
- data/lib/milk_tea/bindings/imported_bindings/method_source.rb +190 -0
- data/lib/milk_tea/bindings/imported_bindings/naming.rb +286 -0
- data/lib/milk_tea/bindings/imported_bindings.rb +215 -0
- data/lib/milk_tea/bindings/opengl_registry.rb +547 -0
- data/lib/milk_tea/bindings/raw_bindings/defaults.rb +1338 -0
- data/lib/milk_tea/bindings/raw_bindings.rb +269 -0
- data/lib/milk_tea/bindings/steamworks.rb +629 -0
- data/lib/milk_tea/bindings/upstream_sources.rb +352 -0
- data/lib/milk_tea/bindings/vendored_box2d.rb +79 -0
- data/lib/milk_tea/bindings/vendored_c_library.rb +322 -0
- data/lib/milk_tea/bindings/vendored_cjson.rb +52 -0
- data/lib/milk_tea/bindings/vendored_flecs.rb +78 -0
- data/lib/milk_tea/bindings/vendored_glfw.rb +77 -0
- data/lib/milk_tea/bindings/vendored_libuv.rb +75 -0
- data/lib/milk_tea/bindings/vendored_pcre2.rb +84 -0
- data/lib/milk_tea/bindings/vendored_raylib.rb +131 -0
- data/lib/milk_tea/bindings/vendored_sdl3.rb +78 -0
- data/lib/milk_tea/bindings/vendored_steamworks.rb +71 -0
- data/lib/milk_tea/bindings/vendored_tool.rb +71 -0
- data/lib/milk_tea/bindings/vendored_tools.rb +23 -0
- data/lib/milk_tea/bindings/vendored_tracy.rb +37 -0
- data/lib/milk_tea/bindings.rb +23 -0
- data/lib/milk_tea/core/ast.rb +311 -0
- data/lib/milk_tea/core/async_runtime_installer.rb +28 -0
- data/lib/milk_tea/core/binding_types.rb +18 -0
- data/lib/milk_tea/core/bindings/attribute_binding.rb +58 -0
- data/lib/milk_tea/core/bindings/function_binding.rb +5 -0
- data/lib/milk_tea/core/bindings/module_binding.rb +58 -0
- data/lib/milk_tea/core/bindings/value_binding.rb +21 -0
- data/lib/milk_tea/core/c_backend/aggregate_utils.rb +103 -0
- data/lib/milk_tea/core/c_backend/control_flow_emission.rb +412 -0
- data/lib/milk_tea/core/c_backend/expressions.rb +468 -0
- data/lib/milk_tea/core/c_backend/feature_detection.rb +483 -0
- data/lib/milk_tea/core/c_backend/reachability.rb +398 -0
- data/lib/milk_tea/core/c_backend/reinterpret.rb +226 -0
- data/lib/milk_tea/core/c_backend/runtime_helpers.rb +1080 -0
- data/lib/milk_tea/core/c_backend/statements.rb +563 -0
- data/lib/milk_tea/core/c_backend/type_collectors.rb +1545 -0
- data/lib/milk_tea/core/c_backend/type_declaration.rb +223 -0
- data/lib/milk_tea/core/c_backend/type_system.rb +345 -0
- data/lib/milk_tea/core/c_backend.rb +287 -0
- data/lib/milk_tea/core/compatibility_helpers.rb +79 -0
- data/lib/milk_tea/core/compile_time/const_eval.rb +187 -0
- data/lib/milk_tea/core/compile_time.rb +329 -0
- data/lib/milk_tea/core/control_flow/builder.rb +582 -0
- data/lib/milk_tea/core/control_flow/constant_propagation.rb +143 -0
- data/lib/milk_tea/core/control_flow/dataflow.rb +74 -0
- data/lib/milk_tea/core/control_flow/definite_assignment.rb +79 -0
- data/lib/milk_tea/core/control_flow/graph.rb +90 -0
- data/lib/milk_tea/core/control_flow/liveness.rb +24 -0
- data/lib/milk_tea/core/control_flow/nullability_flow.rb +43 -0
- data/lib/milk_tea/core/control_flow/reachability.rb +22 -0
- data/lib/milk_tea/core/control_flow/termination.rb +31 -0
- data/lib/milk_tea/core/control_flow.rb +34 -0
- data/lib/milk_tea/core/cst.rb +48 -0
- data/lib/milk_tea/core/cst_builder.rb +19 -0
- data/lib/milk_tea/core/ir.rb +85 -0
- data/lib/milk_tea/core/keywords.rb +97 -0
- data/lib/milk_tea/core/lexer/character_classes.rb +60 -0
- data/lib/milk_tea/core/lexer/format_strings.rb +225 -0
- data/lib/milk_tea/core/lexer/heredocs.rb +199 -0
- data/lib/milk_tea/core/lexer/indentation.rb +62 -0
- data/lib/milk_tea/core/lexer/numbers.rb +96 -0
- data/lib/milk_tea/core/lexer/recovery.rb +32 -0
- data/lib/milk_tea/core/lexer/strings.rb +167 -0
- data/lib/milk_tea/core/lexer/symbols.rb +71 -0
- data/lib/milk_tea/core/lexer/trivia.rb +53 -0
- data/lib/milk_tea/core/lexer.rb +430 -0
- data/lib/milk_tea/core/lowering/artifacts.rb +26 -0
- data/lib/milk_tea/core/lowering/async/analysis.rb +245 -0
- data/lib/milk_tea/core/lowering/async/lowering.rb +1399 -0
- data/lib/milk_tea/core/lowering/async/normalization.rb +459 -0
- data/lib/milk_tea/core/lowering/async.rb +714 -0
- data/lib/milk_tea/core/lowering/block.rb +1052 -0
- data/lib/milk_tea/core/lowering/calls.rb +1565 -0
- data/lib/milk_tea/core/lowering/declarations.rb +214 -0
- data/lib/milk_tea/core/lowering/dyn.rb +206 -0
- data/lib/milk_tea/core/lowering/events.rb +1054 -0
- data/lib/milk_tea/core/lowering/expressions.rb +1645 -0
- data/lib/milk_tea/core/lowering/foreign_cstr.rb +206 -0
- data/lib/milk_tea/core/lowering/functions.rb +242 -0
- data/lib/milk_tea/core/lowering/loops.rb +1087 -0
- data/lib/milk_tea/core/lowering/lowering_context.rb +80 -0
- data/lib/milk_tea/core/lowering/proc.rb +419 -0
- data/lib/milk_tea/core/lowering/resolve.rb +2516 -0
- data/lib/milk_tea/core/lowering/scans.rb +220 -0
- data/lib/milk_tea/core/lowering/str_buffer.rb +125 -0
- data/lib/milk_tea/core/lowering/utils.rb +1453 -0
- data/lib/milk_tea/core/lowering.rb +378 -0
- data/lib/milk_tea/core/module_binder.rb +181 -0
- data/lib/milk_tea/core/module_loader/errors.rb +21 -0
- data/lib/milk_tea/core/module_loader.rb +479 -0
- data/lib/milk_tea/core/module_path_resolver.rb +153 -0
- data/lib/milk_tea/core/module_roots.rb +88 -0
- data/lib/milk_tea/core/parser/attributes.rb +71 -0
- data/lib/milk_tea/core/parser/blocks.rb +119 -0
- data/lib/milk_tea/core/parser/declarations.rb +749 -0
- data/lib/milk_tea/core/parser/expressions.rb +624 -0
- data/lib/milk_tea/core/parser/recovery.rb +131 -0
- data/lib/milk_tea/core/parser/statements.rb +756 -0
- data/lib/milk_tea/core/parser/types.rb +271 -0
- data/lib/milk_tea/core/parser.rb +400 -0
- data/lib/milk_tea/core/prelude_installer.rb +29 -0
- data/lib/milk_tea/core/pretty_printer/ast_formatter.rb +917 -0
- data/lib/milk_tea/core/pretty_printer/base_formatter.rb +87 -0
- data/lib/milk_tea/core/pretty_printer/ir_formatter.rb +300 -0
- data/lib/milk_tea/core/pretty_printer.rb +17 -0
- data/lib/milk_tea/core/semantic_analyzer/analysis_context.rb +314 -0
- data/lib/milk_tea/core/semantic_analyzer/attributes.rb +184 -0
- data/lib/milk_tea/core/semantic_analyzer/calls.rb +992 -0
- data/lib/milk_tea/core/semantic_analyzer/expressions.rb +1735 -0
- data/lib/milk_tea/core/semantic_analyzer/flow_refinement.rb +355 -0
- data/lib/milk_tea/core/semantic_analyzer/foreign_functions.rb +155 -0
- data/lib/milk_tea/core/semantic_analyzer/function_binding.rb +354 -0
- data/lib/milk_tea/core/semantic_analyzer/generics.rb +383 -0
- data/lib/milk_tea/core/semantic_analyzer/interface_conformance.rb +78 -0
- data/lib/milk_tea/core/semantic_analyzer/module_context.rb +35 -0
- data/lib/milk_tea/core/semantic_analyzer/name_resolution.rb +1438 -0
- data/lib/milk_tea/core/semantic_analyzer/nullability.rb +421 -0
- data/lib/milk_tea/core/semantic_analyzer/statements.rb +1308 -0
- data/lib/milk_tea/core/semantic_analyzer/top_level.rb +588 -0
- data/lib/milk_tea/core/semantic_analyzer/type_compatibility.rb +307 -0
- data/lib/milk_tea/core/semantic_analyzer/type_declaration.rb +851 -0
- data/lib/milk_tea/core/semantic_analyzer.rb +327 -0
- data/lib/milk_tea/core/token.rb +26 -0
- data/lib/milk_tea/core/token_stream.rb +30 -0
- data/lib/milk_tea/core/types/layout.rb +243 -0
- data/lib/milk_tea/core/types/predicates.rb +609 -0
- data/lib/milk_tea/core/types/registry.rb +83 -0
- data/lib/milk_tea/core/types/types.rb +1696 -0
- data/lib/milk_tea/core/types/visitor.rb +442 -0
- data/lib/milk_tea/core.rb +25 -0
- data/lib/milk_tea/dap/backends/lldb_dap.rb +158 -0
- data/lib/milk_tea/dap/protocol.rb +58 -0
- data/lib/milk_tea/dap/server/breakpoints.rb +66 -0
- data/lib/milk_tea/dap/server/debug_map.rb +291 -0
- data/lib/milk_tea/dap/server/handlers.rb +374 -0
- data/lib/milk_tea/dap/server/launch.rb +261 -0
- data/lib/milk_tea/dap/server/lldb_backend.rb +380 -0
- data/lib/milk_tea/dap/server/pause_diagnostics.rb +109 -0
- data/lib/milk_tea/dap/server/utilities.rb +160 -0
- data/lib/milk_tea/dap/server/wire.rb +55 -0
- data/lib/milk_tea/dap/server.rb +131 -0
- data/lib/milk_tea/dap/session.rb +152 -0
- data/lib/milk_tea/dap.rb +6 -0
- data/lib/milk_tea/lsp/dependency_resolution.rb +52 -0
- data/lib/milk_tea/lsp/diagnostics.rb +611 -0
- data/lib/milk_tea/lsp/protocol.rb +104 -0
- data/lib/milk_tea/lsp/server/call_hierarchy.rb +274 -0
- data/lib/milk_tea/lsp/server/code_actions.rb +446 -0
- data/lib/milk_tea/lsp/server/code_lens.rb +97 -0
- data/lib/milk_tea/lsp/server/completion.rb +1099 -0
- data/lib/milk_tea/lsp/server/configuration.rb +167 -0
- data/lib/milk_tea/lsp/server/debug_info.rb +45 -0
- data/lib/milk_tea/lsp/server/definition.rb +779 -0
- data/lib/milk_tea/lsp/server/diagnostics_scheduling.rb +239 -0
- data/lib/milk_tea/lsp/server/execute_command.rb +25 -0
- data/lib/milk_tea/lsp/server/folding_range.rb +153 -0
- data/lib/milk_tea/lsp/server/formatting.rb +575 -0
- data/lib/milk_tea/lsp/server/hover.rb +1465 -0
- data/lib/milk_tea/lsp/server/inlay_hints.rb +204 -0
- data/lib/milk_tea/lsp/server/lifecycle.rb +234 -0
- data/lib/milk_tea/lsp/server/linked_editing_range.rb +43 -0
- data/lib/milk_tea/lsp/server/on_type_formatting.rb +73 -0
- data/lib/milk_tea/lsp/server/progress.rb +47 -0
- data/lib/milk_tea/lsp/server/references.rb +433 -0
- data/lib/milk_tea/lsp/server/rename.rb +598 -0
- data/lib/milk_tea/lsp/server/selection_range.rb +130 -0
- data/lib/milk_tea/lsp/server/semantic_tokens.rb +1745 -0
- data/lib/milk_tea/lsp/server/signature_help.rb +200 -0
- data/lib/milk_tea/lsp/server/text_documents.rb +125 -0
- data/lib/milk_tea/lsp/server/type_hierarchy.rb +167 -0
- data/lib/milk_tea/lsp/server/utilities.rb +520 -0
- data/lib/milk_tea/lsp/server.rb +415 -0
- data/lib/milk_tea/lsp/workspace/analysis.rb +209 -0
- data/lib/milk_tea/lsp/workspace/caches.rb +260 -0
- data/lib/milk_tea/lsp/workspace/collection.rb +98 -0
- data/lib/milk_tea/lsp/workspace/definition_index.rb +184 -0
- data/lib/milk_tea/lsp/workspace/dependency_graph.rb +282 -0
- data/lib/milk_tea/lsp/workspace/store.rb +154 -0
- data/lib/milk_tea/lsp/workspace/utilities.rb +490 -0
- data/lib/milk_tea/lsp/workspace.rb +127 -0
- data/lib/milk_tea/lsp.rb +13 -0
- data/lib/milk_tea/packages/atomic_write.rb +63 -0
- data/lib/milk_tea/packages/dependency_solver.rb +194 -0
- data/lib/milk_tea/packages/graph.rb +139 -0
- data/lib/milk_tea/packages/lock.rb +421 -0
- data/lib/milk_tea/packages/manager_cli.rb +485 -0
- data/lib/milk_tea/packages/manifest.rb +356 -0
- data/lib/milk_tea/packages/manifest_editor.rb +134 -0
- data/lib/milk_tea/packages/registry_metadata_provider.rb +34 -0
- data/lib/milk_tea/packages/registry_store.rb +420 -0
- data/lib/milk_tea/packages/services.rb +41 -0
- data/lib/milk_tea/packages/source_cache.rb +71 -0
- data/lib/milk_tea/packages/source_fetcher.rb +124 -0
- data/lib/milk_tea/packages/source_resolver.rb +389 -0
- data/lib/milk_tea/packages/version.rb +164 -0
- data/lib/milk_tea/packages.rb +16 -0
- data/lib/milk_tea/tooling/asset_pack.rb +141 -0
- data/lib/milk_tea/tooling/build.rb +1124 -0
- data/lib/milk_tea/tooling/build_cache.rb +240 -0
- data/lib/milk_tea/tooling/cli.rb +2688 -0
- data/lib/milk_tea/tooling/cst_formatter.rb +13 -0
- data/lib/milk_tea/tooling/debug_info_formatter.rb +456 -0
- data/lib/milk_tea/tooling/debug_map.rb +248 -0
- data/lib/milk_tea/tooling/docs_app.rb +369 -0
- data/lib/milk_tea/tooling/error_formatter.rb +86 -0
- data/lib/milk_tea/tooling/formatter.rb +787 -0
- data/lib/milk_tea/tooling/linter/doc_tags.rb +212 -0
- data/lib/milk_tea/tooling/linter/fix_engine.rb +237 -0
- data/lib/milk_tea/tooling/linter/flow_rules.rb +380 -0
- data/lib/milk_tea/tooling/linter/imports_platform.rb +841 -0
- data/lib/milk_tea/tooling/linter/release_rules.rb +744 -0
- data/lib/milk_tea/tooling/linter/reserved_names.rb +199 -0
- data/lib/milk_tea/tooling/linter/rules.rb +593 -0
- data/lib/milk_tea/tooling/linter/source_helpers.rb +407 -0
- data/lib/milk_tea/tooling/linter/trailing_comma.rb +139 -0
- data/lib/milk_tea/tooling/linter/visitors.rb +1286 -0
- data/lib/milk_tea/tooling/linter.rb +798 -0
- data/lib/milk_tea/tooling/project_scaffold.rb +82 -0
- data/lib/milk_tea/tooling/public/css/docs.css +166 -0
- data/lib/milk_tea/tooling/public/js/docs.js +94 -0
- data/lib/milk_tea/tooling/run.rb +408 -0
- data/lib/milk_tea/tooling/templates/wasm_shell.html +48 -0
- data/lib/milk_tea/tooling/toolchain_cli.rb +157 -0
- data/lib/milk_tea/tooling/views/404.erb +7 -0
- data/lib/milk_tea/tooling/views/index.erb +87 -0
- data/lib/milk_tea/tooling/views/layout.erb +69 -0
- data/lib/milk_tea/tooling/views/module.erb +97 -0
- data/lib/milk_tea/tooling/views/stdlib.erb +21 -0
- data/lib/milk_tea/tooling.rb +20 -0
- data/lib/milk_tea.rb +8 -0
- 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.
|