mt-lang 0.3.40 → 0.3.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/milk_tea/base.rb +1 -1
- data/lib/milk_tea/bindings/bindgen/declaration.rb +17 -0
- data/lib/milk_tea/bindings/bindgen/emitter.rb +44 -5
- data/lib/milk_tea/bindings/imported_bindings/defaults.rb +7 -0
- data/lib/milk_tea/bindings/raw_bindings/defaults.rb +22 -0
- data/lib/milk_tea/bindings/upstream_sources.rb +9 -0
- data/lib/milk_tea/bindings/vendored_box3d.rb +77 -0
- data/lib/milk_tea/bindings.rb +1 -0
- data/lib/milk_tea/core/c_backend/expressions.rb +21 -5
- data/lib/milk_tea/core/c_backend/feature_detection.rb +1 -1
- data/lib/milk_tea/core/c_backend/runtime_helpers.rb +8 -4
- data/lib/milk_tea/core/c_backend/statements.rb +1 -1
- data/lib/milk_tea/core/c_backend/type_collectors.rb +23 -4
- data/lib/milk_tea/core/lowering/async/async_lowering.rb +2 -6
- data/lib/milk_tea/core/lowering/async/frame_builder.rb +12 -5
- data/lib/milk_tea/core/lowering/async/normalization.rb +26 -5
- data/lib/milk_tea/core/module_loader.rb +41 -6
- data/lib/milk_tea/core/semantic_analyzer/expressions.rb +8 -2
- data/lib/milk_tea/core/semantic_analyzer/name_resolution.rb +3 -1
- data/lib/milk_tea/core/types/registry.rb +14 -2
- data/lib/milk_tea/lsp/server/code_actions.rb +13 -3
- data/lib/milk_tea/lsp/server/diagnostics_scheduling.rb +56 -12
- data/lib/milk_tea/lsp/server/text_documents.rb +18 -2
- data/lib/milk_tea/lsp/workspace/caches.rb +28 -1
- data/lib/milk_tea/lsp/workspace/dependency_graph.rb +31 -0
- data/lib/milk_tea/lsp/workspace/store.rb +7 -1
- data/lib/milk_tea/lsp/workspace.rb +6 -0
- data/lib/milk_tea/tooling/docs_app.rb +1 -1
- data/std/box3d.mt +784 -0
- data/std/c/box3d.mt +1871 -0
- data/std/c/miniaudio.mt +19 -28
- data/std/c/steamworks.mt +2 -5
- data/std/miniaudio.mt +9 -12
- data/std/steamworks.mt +0 -1
- metadata +5 -2
|
@@ -248,7 +248,13 @@ module MilkTea
|
|
|
248
248
|
raise_sema_error("unsupported expression #{expression.class.name}")
|
|
249
249
|
end
|
|
250
250
|
|
|
251
|
-
|
|
251
|
+
# Specialized generic-instance bodies share the same AST nodes across
|
|
252
|
+
# substitutions, so a node-id keyed cache cannot be populated there:
|
|
253
|
+
# the last-checked instance would overwrite every other instance's
|
|
254
|
+
# types and leak wrong types to lowering, which reads this cache.
|
|
255
|
+
# Regular functions carry an empty (but non-nil) substitution hash, so
|
|
256
|
+
# only a non-empty hash identifies an instance body.
|
|
257
|
+
@resolved_expr_types[@ctx.ast.node_ids[expression.object_id]] = type unless @current_type_substitutions&.any?
|
|
252
258
|
type
|
|
253
259
|
end
|
|
254
260
|
end
|
|
@@ -961,7 +967,7 @@ module MilkTea
|
|
|
961
967
|
callable_kind = resolution.kind
|
|
962
968
|
callable = resolution.value
|
|
963
969
|
receiver = resolution.receiver
|
|
964
|
-
@resolved_call_kinds[@ctx.ast.node_ids[expression.callee.object_id]] = callable_kind
|
|
970
|
+
@resolved_call_kinds[@ctx.ast.node_ids[expression.callee.object_id]] = callable_kind unless @current_type_substitutions&.any?
|
|
965
971
|
|
|
966
972
|
case callable_kind
|
|
967
973
|
when :function
|
|
@@ -695,7 +695,9 @@ module MilkTea
|
|
|
695
695
|
offset = CompileTime::Layout.offset_of(type, binding.const_value.field_name)
|
|
696
696
|
return unless offset
|
|
697
697
|
|
|
698
|
-
|
|
698
|
+
# Same node-id key collision as resolved_expr_types: instance bodies
|
|
699
|
+
# share AST nodes across substitutions, so never cache their values.
|
|
700
|
+
@const_values[@ctx.ast.node_ids[expression.object_id]] = offset unless @current_type_substitutions&.any?
|
|
699
701
|
end
|
|
700
702
|
|
|
701
703
|
def infer_offsetof_type(type_ref, field_name, scopes: nil)
|
|
@@ -41,16 +41,28 @@ module MilkTea
|
|
|
41
41
|
_intern([:string_view]) { StringView.new }
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
# Param signature for intern keys. Parameter#eql? is name-insensitive
|
|
45
|
+
# (assignability must not depend on parameter names), so arrays of
|
|
46
|
+
# Parameter objects would conflate fn(value: int) with fn(arg0: int) in
|
|
47
|
+
# the intern pool. Embedding names here keeps distinct signatures
|
|
48
|
+
# distinct across independent programs sharing a long-lived pool (the LSP
|
|
49
|
+
# never resets the registry between checks).
|
|
50
|
+
def self.param_signature(params)
|
|
51
|
+
params.map { |p| [p.name, p.type, p.mutable, p.passing_mode, p.boundary_type] }
|
|
52
|
+
end
|
|
53
|
+
|
|
44
54
|
def self.function(name, params:, return_type:, receiver_type: nil, receiver_editable: false, variadic: false, external: false)
|
|
45
55
|
params_frozen = params.freeze
|
|
46
|
-
|
|
56
|
+
param_key = param_signature(params_frozen)
|
|
57
|
+
_intern([:function, name, param_key, return_type, receiver_type, receiver_editable, variadic, external]) {
|
|
47
58
|
Function.new(name, params: params_frozen, return_type: return_type, receiver_type: receiver_type, receiver_editable: receiver_editable, variadic: variadic, external: external)
|
|
48
59
|
}
|
|
49
60
|
end
|
|
50
61
|
|
|
51
62
|
def self.proc(params:, return_type:)
|
|
52
63
|
params_frozen = params.freeze
|
|
53
|
-
|
|
64
|
+
param_key = param_signature(params_frozen)
|
|
65
|
+
_intern([:proc, param_key, return_type]) { Proc.new(params: params_frozen, return_type: return_type) }
|
|
54
66
|
end
|
|
55
67
|
|
|
56
68
|
def self.parameter(name, type, mutable: false, passing_mode: :plain, boundary_type: nil)
|
|
@@ -360,6 +360,8 @@ module MilkTea
|
|
|
360
360
|
end
|
|
361
361
|
|
|
362
362
|
def handle_document_diagnostic(params)
|
|
363
|
+
return { kind: 'full', items: [] } if request_cancelled?(@current_request_id)
|
|
364
|
+
|
|
363
365
|
uri = params.dig('textDocument', 'uri')
|
|
364
366
|
return { kind: 'full', items: [] } unless uri
|
|
365
367
|
|
|
@@ -404,6 +406,8 @@ module MilkTea
|
|
|
404
406
|
end
|
|
405
407
|
|
|
406
408
|
def handle_workspace_diagnostic(params)
|
|
409
|
+
return { items: [] } if request_cancelled?(@current_request_id)
|
|
410
|
+
|
|
407
411
|
progress = nil
|
|
408
412
|
if (work_done_token = params['workDoneToken'])
|
|
409
413
|
progress = create_progress_handle(@protocol, work_done_token)
|
|
@@ -416,7 +420,13 @@ module MilkTea
|
|
|
416
420
|
end
|
|
417
421
|
|
|
418
422
|
all_uris = @workspace.open_document_uris
|
|
419
|
-
items =
|
|
423
|
+
items = []
|
|
424
|
+
all_uris.each do |uri|
|
|
425
|
+
# Coarse cancellation: workspace/diagnostic can iterate many cold
|
|
426
|
+
# documents (~1s each); bail early when the client cancels so we do
|
|
427
|
+
# not sink CPU into results that will be discarded.
|
|
428
|
+
break if request_cancelled?(@current_request_id)
|
|
429
|
+
|
|
420
430
|
content = @workspace.get_content(uri)
|
|
421
431
|
next if content.empty?
|
|
422
432
|
|
|
@@ -426,10 +436,10 @@ module MilkTea
|
|
|
426
436
|
|
|
427
437
|
cached = @workspace_diagnostic_cache[uri]
|
|
428
438
|
if cached && cached[:result_id] == prev_map[uri] && cached[:fingerprint] == fingerprint
|
|
429
|
-
{ uri: uri, kind: 'unchanged', resultId: result_id, items: [], version: nil }
|
|
439
|
+
items << { uri: uri, kind: 'unchanged', resultId: result_id, items: [], version: nil }
|
|
430
440
|
else
|
|
431
441
|
@workspace_diagnostic_cache[uri] = { result_id: result_id, fingerprint: fingerprint }
|
|
432
|
-
{ uri: uri, kind: 'full', resultId: result_id, items: diagnostics, version: nil }
|
|
442
|
+
items << { uri: uri, kind: 'full', resultId: result_id, items: diagnostics, version: nil }
|
|
433
443
|
end
|
|
434
444
|
end
|
|
435
445
|
|
|
@@ -202,28 +202,72 @@ module MilkTea
|
|
|
202
202
|
end.join("\n")
|
|
203
203
|
end
|
|
204
204
|
|
|
205
|
+
# Declaration-prefix regex. Unlike the old form, it also covers the
|
|
206
|
+
# `const function` compound, async/foreign/external/editable/static
|
|
207
|
+
# modifiers, and `attribute` declarations. `static_assert` is not
|
|
208
|
+
# matched: `static` requires whitespace before the keyword, and a bare
|
|
209
|
+
# module-level statement cannot be a declaration.
|
|
210
|
+
SURFACE_DECL_LINE = %r{\A(?:(?:public|foreign|external|async|editable|static|const)\s+)*(?:function|struct|union|enum|flags|variant|interface|event|type|const|var|extending|opaque|attribute)\b}
|
|
211
|
+
|
|
212
|
+
# A bare `name: Type` line. Only struct/union fields (and continuation
|
|
213
|
+
# parameter lines) take this shape; local declarations use `let`/`var`,
|
|
214
|
+
# named arguments use `=`, and match-arm labels start with a keyword or
|
|
215
|
+
# pattern. Requires a type-like token after the colon so `_:` arm labels
|
|
216
|
+
# are not treated as surface.
|
|
217
|
+
SURFACE_FIELD_LINE = /\A[A-Za-z_][A-Za-z0-9_]*\s*:\s*[A-Za-z_\[\]]/
|
|
218
|
+
|
|
219
|
+
# Over-approximation of the module's externally-observable surface. It
|
|
220
|
+
# must never MISS a surface change (a miss leaves shared-cache analyses
|
|
221
|
+
# of dependents stale); false positives only trigger a harmless
|
|
222
|
+
# re-analysis. In addition to declaration lines it captures:
|
|
223
|
+
# - `@[...]` attributes (packed/align/deprecated change layout/docs)
|
|
224
|
+
# - multi-line declaration headers, so parameter/signature edits on
|
|
225
|
+
# continuation lines are detected
|
|
226
|
+
# - struct/union field lines (`name: Type`)
|
|
205
227
|
def dependency_export_surface_fingerprint(content)
|
|
206
|
-
content.to_s.
|
|
207
|
-
|
|
208
|
-
|
|
228
|
+
lines = content.to_s.lines.map(&:strip)
|
|
229
|
+
surface = []
|
|
230
|
+
i = 0
|
|
231
|
+
while i < lines.length
|
|
232
|
+
line = lines[i]
|
|
233
|
+
if line.empty? || line.start_with?('#')
|
|
234
|
+
i += 1
|
|
235
|
+
next
|
|
236
|
+
end
|
|
209
237
|
|
|
210
|
-
if
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
238
|
+
if line.start_with?('@[') || line.match?(SURFACE_DECL_LINE) || line.match?(SURFACE_FIELD_LINE)
|
|
239
|
+
surface << line
|
|
240
|
+
# Multi-line header: absorb continuation lines until the
|
|
241
|
+
# terminating ':' so edits inside the header are surfaced too.
|
|
242
|
+
unless line.end_with?(':')
|
|
243
|
+
i += 1
|
|
244
|
+
while i < lines.length
|
|
245
|
+
cont = lines[i]
|
|
246
|
+
break if cont.empty?
|
|
247
|
+
surface << cont
|
|
248
|
+
i += 1
|
|
249
|
+
break if cont.end_with?(':')
|
|
250
|
+
end
|
|
251
|
+
next
|
|
252
|
+
end
|
|
216
253
|
end
|
|
217
|
-
|
|
254
|
+
i += 1
|
|
255
|
+
end
|
|
256
|
+
surface.join("\n")
|
|
218
257
|
end
|
|
219
258
|
|
|
220
259
|
def dependency_refresh_required_for_edit?(changed_uri, previous_content, current_content)
|
|
221
260
|
return false if previous_content == current_content
|
|
222
261
|
return true if dependency_import_fingerprint(previous_content) != dependency_import_fingerprint(current_content)
|
|
223
262
|
|
|
224
|
-
|
|
225
|
-
|
|
263
|
+
# Keep the open-document dependency index fresh (related_open_document_uris
|
|
264
|
+
# updates it as a side effect) so dependent tracking stays accurate.
|
|
265
|
+
@workspace.related_open_document_uris(changed_uri)
|
|
226
266
|
|
|
267
|
+
# A surface edit can invalidate shared-cache analyses of NON-open
|
|
268
|
+
# dependents (their cached entries are recomputed against this module
|
|
269
|
+
# on their next pull), so clearing must not be gated on there being
|
|
270
|
+
# open dependents to refresh.
|
|
227
271
|
dependency_export_surface_fingerprint(previous_content) != dependency_export_surface_fingerprint(current_content)
|
|
228
272
|
end
|
|
229
273
|
|
|
@@ -43,7 +43,12 @@ module MilkTea
|
|
|
43
43
|
invalidate_document_caches(uri)
|
|
44
44
|
current_content = @workspace.get_content(uri)
|
|
45
45
|
refresh_open_document_dependency_state(uri, previous_content: previous_content, current_content: current_content)
|
|
46
|
-
|
|
46
|
+
# This server is always pull-based (diagnosticProvider), so the
|
|
47
|
+
# client's textDocument/diagnostic request asks for the full tier.
|
|
48
|
+
# Scheduling a lighter tier here computes in the worker and then
|
|
49
|
+
# duplicates the full lint on the request thread; keep the tiers
|
|
50
|
+
# aligned so the pull can serve the worker's result.
|
|
51
|
+
schedule_diagnostics(uri, lint_tier: :full) unless @workspace.background_document?(uri)
|
|
47
52
|
nil
|
|
48
53
|
end
|
|
49
54
|
|
|
@@ -65,12 +70,23 @@ module MilkTea
|
|
|
65
70
|
|
|
66
71
|
def handle_did_close(params)
|
|
67
72
|
uri = params['textDocument']['uri']
|
|
73
|
+
previous_content = @workspace.get_content(uri)
|
|
68
74
|
cancel_diagnostics(uri)
|
|
69
75
|
@workspace.close_document(uri)
|
|
70
76
|
invalidate_document_caches(uri)
|
|
71
77
|
@diagnostic_report_cache.delete(uri)
|
|
72
78
|
@workspace_diagnostic_cache.delete(uri)
|
|
73
|
-
|
|
79
|
+
# Once closed, the buffer is no longer authoritative. If it differed
|
|
80
|
+
# from disk, module analyses computed against it (for dependents and
|
|
81
|
+
# the file itself) are stale; drop the whole shared cache. Closing is
|
|
82
|
+
# a rare per-file event, so an unconditional clear is cheap and safe.
|
|
83
|
+
disk_content = begin
|
|
84
|
+
path = uri_to_path(uri)
|
|
85
|
+
path && File.file?(path) ? File.read(path) : nil
|
|
86
|
+
rescue StandardError
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
clear_shared_module_cache if previous_content != disk_content
|
|
74
90
|
unless defined?(@pull_diagnostics_active) && @pull_diagnostics_active
|
|
75
91
|
@protocol.write_notification('textDocument/publishDiagnostics', {
|
|
76
92
|
uri: uri,
|
|
@@ -99,14 +99,41 @@ module MilkTea
|
|
|
99
99
|
|
|
100
100
|
lock_wait_start = total_start ? monotonic_time : nil
|
|
101
101
|
if @facts_state_mutex.try_lock
|
|
102
|
+
# No analysis is in flight; computing here is fine (cold paths,
|
|
103
|
+
# tests). It does not block on other work.
|
|
102
104
|
begin
|
|
103
105
|
compute_snapshot.call
|
|
104
106
|
ensure
|
|
105
107
|
@facts_state_mutex.unlock
|
|
106
108
|
end
|
|
107
109
|
elsif allow_last_good_fallback && last_good_snapshot
|
|
108
|
-
|
|
110
|
+
# Facts exist from a prior pass; never block on the in-flight one.
|
|
109
111
|
snapshot = last_good_snapshot
|
|
112
|
+
cache_state = 'last_good'
|
|
113
|
+
elsif allow_last_good_fallback
|
|
114
|
+
# No facts yet and a background analysis is computing this document.
|
|
115
|
+
# Wait briefly (bounded) for it so the request usually returns fresh
|
|
116
|
+
# facts instead of a lexical fallback — without duplicating the work
|
|
117
|
+
# (we never run analysis here) or blocking indefinitely. Beyond the
|
|
118
|
+
# budget, serve nil and let the handler fall back.
|
|
119
|
+
deadline = monotonic_time + (IN_FLIGHT_FACTS_WAIT_MS / 1000.0)
|
|
120
|
+
acquired = false
|
|
121
|
+
while monotonic_time < deadline
|
|
122
|
+
if @facts_state_mutex.try_lock
|
|
123
|
+
acquired = true
|
|
124
|
+
break
|
|
125
|
+
end
|
|
126
|
+
sleep 0.02
|
|
127
|
+
end
|
|
128
|
+
if acquired
|
|
129
|
+
begin
|
|
130
|
+
compute_snapshot.call
|
|
131
|
+
ensure
|
|
132
|
+
@facts_state_mutex.unlock
|
|
133
|
+
end
|
|
134
|
+
else
|
|
135
|
+
cache_state = 'nil'
|
|
136
|
+
end
|
|
110
137
|
else
|
|
111
138
|
@facts_state_mutex.synchronize do
|
|
112
139
|
lock_wait_ms = elapsed_ms(lock_wait_start) if lock_wait_start
|
|
@@ -107,6 +107,37 @@ module MilkTea
|
|
|
107
107
|
@full_reverse_index_built = false
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
+
# Drop all cached module analyses (and the per-uri snapshots derived from
|
|
111
|
+
# them) when an analysis input that was captured in those analyses is no
|
|
112
|
+
# longer authoritative — e.g. a closed document whose buffer differed from
|
|
113
|
+
# disk. Kept rare: closing a file is infrequent, so a wholesale clear is
|
|
114
|
+
# safer than reasoning about which dependents could have observed the
|
|
115
|
+
# closed buffer. Open documents keep their last-known-good snapshots so
|
|
116
|
+
# they keep serving rich features while re-analysis lands.
|
|
117
|
+
def clear_shared_module_cache
|
|
118
|
+
@facts_state_mutex.synchronize do
|
|
119
|
+
@facts_cache_mutex.synchronize do
|
|
120
|
+
all_open = @document_state_mutex.synchronize { @open_documents.keys }
|
|
121
|
+
preserved_facts = all_open.each_with_object({}) do |open_uri, preserved|
|
|
122
|
+
facts = @last_good_facts_cache[open_uri]
|
|
123
|
+
preserved[open_uri] = facts if facts
|
|
124
|
+
end
|
|
125
|
+
preserved_snapshots = all_open.each_with_object({}) do |open_uri, preserved|
|
|
126
|
+
snapshot = @last_good_tooling_snapshot_cache[open_uri]
|
|
127
|
+
preserved[open_uri] = snapshot if snapshot
|
|
128
|
+
end
|
|
129
|
+
@shared_module_cache.clear
|
|
130
|
+
@facts_cache.clear
|
|
131
|
+
@tooling_snapshot_cache.clear
|
|
132
|
+
@diagnostics_cache.clear
|
|
133
|
+
@last_good_facts_cache.clear
|
|
134
|
+
@last_good_tooling_snapshot_cache.clear
|
|
135
|
+
preserved_snapshots.each { |open_uri, snapshot| @last_good_tooling_snapshot_cache[open_uri] = snapshot }
|
|
136
|
+
preserved_facts.each { |open_uri, facts| @last_good_facts_cache[open_uri] = facts }
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
110
141
|
def update_dependency_index(uri, facts)
|
|
111
142
|
imported_module_names = if facts
|
|
112
143
|
facts.imports.each_value.filter_map(&:name).to_set
|
|
@@ -72,7 +72,13 @@ module MilkTea
|
|
|
72
72
|
invalidate_cache(uri)
|
|
73
73
|
enqueue_definition_warmup(uri) unless background_document?(uri)
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
# The shared module cache is intentionally NOT cleared here. Imported
|
|
76
|
+
# module analyses only become stale when this file's dependency surface
|
|
77
|
+
# (imports or exported declarations) changes; a body-only edit leaves
|
|
78
|
+
# them valid. handle_did_change detects surface changes via
|
|
79
|
+
# dependency_refresh_required_for_edit? and clears the cache through
|
|
80
|
+
# refresh_import_dependent_caches. Clearing it on every keystroke forced
|
|
81
|
+
# a full re-analysis of every transitive module per edit.
|
|
76
82
|
dependent_uris = @facts_cache_mutex.synchronize do
|
|
77
83
|
all_open = @document_state_mutex.synchronize { @open_documents.keys }
|
|
78
84
|
dependent_open_document_uris_for(uri, all_open)
|
|
@@ -22,6 +22,12 @@ module MilkTea
|
|
|
22
22
|
DOCUMENT_SOURCES = %w[active-editor visible-editor background-document].freeze
|
|
23
23
|
PERF_LOG_THRESHOLD_MS = 1000
|
|
24
24
|
|
|
25
|
+
# How long a request thread waits (when no facts are available yet) for an
|
|
26
|
+
# in-flight background analysis to finish before falling back to lexical/
|
|
27
|
+
# nil results. Bounds the main-loop stall while usually returning fresh
|
|
28
|
+
# facts; request threads never run the analysis themselves in this window.
|
|
29
|
+
IN_FLIGHT_FACTS_WAIT_MS = Integer(ENV.fetch('MILK_TEA_LSP_FACTS_WAIT_MS', '500'))
|
|
30
|
+
|
|
25
31
|
# Token types that introduce a named definition, in order of precedence.
|
|
26
32
|
#
|
|
27
33
|
# NOTE: this list is intentionally minimal. Multi-keyword prefixes such as
|
|
@@ -18,7 +18,7 @@ module MilkTea
|
|
|
18
18
|
"Files & I/O" => %w[fs path stdio],
|
|
19
19
|
"System" => %w[ctype errno process time c],
|
|
20
20
|
"Network & HTTP" => %w[net http uri cookie curl],
|
|
21
|
-
"Game & Graphics" => %w[raylib box2d flecs enet cgltf cjson],
|
|
21
|
+
"Game & Graphics" => %w[raylib box2d box3d flecs enet cgltf cjson],
|
|
22
22
|
"Algorithms & AI" => %w[fsm behavior_tree goap],
|
|
23
23
|
"Database & Matching" => %w[sqlite3 pcre2],
|
|
24
24
|
"Utilities" => %w[cli terminal span spatial asset_pack],
|