mt-lang 0.3.38 → 0.3.40
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/docs/lsp-performance.md +347 -0
- data/lib/milk_tea/base.rb +1 -1
- data/lib/milk_tea/core/ast.rb +2 -2
- data/lib/milk_tea/core/bindings/attribute_binding.rb +1 -6
- data/lib/milk_tea/core/bindings/module_binding.rb +1 -1
- data/lib/milk_tea/core/intrinsics.rb +23 -1
- data/lib/milk_tea/core/lowering/functions.rb +3 -0
- data/lib/milk_tea/core/lowering/resolve.rb +4 -65
- data/lib/milk_tea/core/lowering/utils.rb +0 -17
- data/lib/milk_tea/core/lowering.rb +1 -2
- data/lib/milk_tea/core/module_binder.rb +4 -15
- data/lib/milk_tea/core/module_loader.rb +0 -2
- data/lib/milk_tea/core/parser/declarations.rb +24 -17
- data/lib/milk_tea/core/semantic_analyzer/analysis_context.rb +2 -111
- data/lib/milk_tea/core/semantic_analyzer/expressions.rb +1 -2
- data/lib/milk_tea/core/semantic_analyzer/function_binding.rb +26 -17
- data/lib/milk_tea/core/semantic_analyzer/name_resolution.rb +26 -42
- data/lib/milk_tea/core/semantic_analyzer/type_compatibility.rb +0 -59
- data/lib/milk_tea/core/semantic_analyzer/type_declaration.rb +0 -2
- data/lib/milk_tea/core/types/predicates.rb +57 -0
- data/lib/milk_tea/core/types.rb +0 -4
- data/lib/milk_tea/lsp/diagnostics.rb +15 -5
- data/lib/milk_tea/lsp/server/code_actions.rb +0 -4
- data/lib/milk_tea/lsp/server/completion.rb +100 -77
- data/lib/milk_tea/lsp/server/diagnostics_scheduling.rb +4 -6
- data/lib/milk_tea/lsp/server/formatting.rb +13 -3
- data/lib/milk_tea/lsp/server/hover.rb +321 -41
- data/lib/milk_tea/lsp/server/lifecycle.rb +12 -0
- data/lib/milk_tea/lsp/server/references.rb +3 -1
- data/lib/milk_tea/lsp/server/semantic_tokens.rb +55 -16
- data/lib/milk_tea/lsp/server/text_documents.rb +2 -2
- data/lib/milk_tea/lsp/server/type_hierarchy.rb +2 -2
- data/lib/milk_tea/lsp/server/utilities.rb +4 -0
- data/lib/milk_tea/lsp/server.rb +9 -0
- data/lib/milk_tea/lsp/workspace/analysis.rb +14 -3
- data/lib/milk_tea/lsp/workspace/caches.rb +17 -1
- data/lib/milk_tea/lsp/workspace/collection.rb +4 -0
- data/lib/milk_tea/lsp/workspace/module_index.rb +134 -0
- data/lib/milk_tea/lsp/workspace/store.rb +9 -4
- data/lib/milk_tea/lsp/workspace.rb +8 -0
- data/lib/milk_tea/tooling/formatter.rb +2 -3
- data/lib/milk_tea/tooling/linter/fix_engine.rb +46 -0
- data/lib/milk_tea/tooling/linter/rules.rb +32 -0
- data/lib/milk_tea/tooling/linter.rb +4 -0
- data/lib/milk_tea/tooling.rb +0 -1
- metadata +4 -3
- data/lib/milk_tea/tooling/cst_formatter.rb +0 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3a57605301758f1ff1307cfb5aa7300bb73876be082b9736f853bf14dce18352
|
|
4
|
+
data.tar.gz: 8d540b3326a74c0a8370d5799b19eadb78f8d579899ffdfed263847bbfa0c367
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c7ff36f0b7a609a7c4dbd69264c70ee7dca22201d520753e254609a3009844c23572c578cbdb5802afdd04897559b578f1e539b5c9c6c2a97c64f16f5e286062
|
|
7
|
+
data.tar.gz: 1987f20a6c919b3c8341a339c6473292bf31866a07ffe1805707153b077a282284686366dd299eeb02e91de23a83eec1b8aaa837fc901eadefe58acf54df07a9
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
# LSP Performance Research
|
|
2
|
+
|
|
3
|
+
Research into how to drastically improve Milk Tea LSP performance, especially
|
|
4
|
+
`textDocument/completion`, grounded in the profile from
|
|
5
|
+
`test/tooling/lsp/server/all_endpoints_benchmark.rb` and the industry-standard
|
|
6
|
+
techniques used by clangd, SourceKit-LSP, Deno's LSP, Shopify's ruby-lsp,
|
|
7
|
+
typescript-language-server, and Roslyn.
|
|
8
|
+
|
|
9
|
+
## 1. Profile snapshot (pre-optimization baseline)
|
|
10
|
+
|
|
11
|
+
> The table below is the **pre-optimization baseline** measured against the
|
|
12
|
+
> original implementation. Every row it describes has since been addressed; see
|
|
13
|
+
> §8 for current measurements. Retained for reference.
|
|
14
|
+
|
|
15
|
+
Measured on Ruby 4.0.3 + YJIT (via `RUBY_YJIT_ENABLE=1`) against a synthetic
|
|
16
|
+
multi-module workspace (10 iterations per endpoint, facts pre-warmed). The
|
|
17
|
+
`didOpen`/`didChange` rows use the benchmark's tiny scratch file; all other
|
|
18
|
+
rows use the 3-module main file. `didOpen`/`didChange` cost grows with module
|
|
19
|
+
complexity, so the real-world figures for large modules will be higher than
|
|
20
|
+
the scratch-file numbers shown here:
|
|
21
|
+
|
|
22
|
+
| endpoint | avg ms (run-to-run range) | dominant stage |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `textDocument/completion` (import line) | 130–200 | `import_context` |
|
|
25
|
+
| `textDocument/didOpen` (scratch file) | 15–30 | eager facts + diagnostics enqueue |
|
|
26
|
+
| `milkTea/debugInfo` | 10–20 | re-parse + semantic tokens rebuild |
|
|
27
|
+
| `textDocument/didChange` (scratch file) | 9–14 | eager facts + dependency refresh |
|
|
28
|
+
| `textDocument/documentSymbol` | 5–18 (max 41–63) | symbols + AST enrichment |
|
|
29
|
+
| `textDocument/formatting` | 3–7 | full-file formatter |
|
|
30
|
+
| `textDocument/completion` (body) | 0.5–0.9 | facts-driven, cached |
|
|
31
|
+
| all other request endpoints | < 4 | mostly cached |
|
|
32
|
+
|
|
33
|
+
Numbers vary run-to-run (the benchmark's diagnostic workers are stopped for
|
|
34
|
+
measurement, but GC and filesystem cache warm-up still add noise), so ranges
|
|
35
|
+
are shown rather than single values. The one pathological hotspot is completion
|
|
36
|
+
on an `import` line; everything else is healthy once facts are warm.
|
|
37
|
+
|
|
38
|
+
## 2. The completion hotspot: `import_completions`
|
|
39
|
+
|
|
40
|
+
> Historical analysis of the original hotspot. The filesystem walk described
|
|
41
|
+
> here was replaced by the persistent module index (§8, item 1); `module_dir_contains_mt?`
|
|
42
|
+
> no longer exists.
|
|
43
|
+
|
|
44
|
+
`lib/milk_tea/lsp/server/completion.rb` ran **on every keystroke** when the
|
|
45
|
+
current line starts with `import `. For each module root it:
|
|
46
|
+
|
|
47
|
+
1. walked the full directory tree recursively via `module_dir_contains_mt?`
|
|
48
|
+
to decide whether a subdirectory is importable, and
|
|
49
|
+
2. stat'ed every entry.
|
|
50
|
+
|
|
51
|
+
Measured cost against this repo's tree: **4,720 directories and 573 `.mt`
|
|
52
|
+
files, ~130–220 ms, zero caching, every keystroke**. The same scan was repeated
|
|
53
|
+
for each module root returned by `roots_for_path` (for a `/tmp` path this
|
|
54
|
+
resolved to the single repo root; a package workspace with `std` and project
|
|
55
|
+
roots would repeat the scan per root). There was no persistent module index.
|
|
56
|
+
|
|
57
|
+
## 3. Ruby constraints that shape the solution
|
|
58
|
+
|
|
59
|
+
- **GVL means threads don't parallelize Ruby CPU work.** The LSP already spawns
|
|
60
|
+
diagnostics workers and a definition-warmup thread, but CPU-heavy sema
|
|
61
|
+
(`SemanticAnalyzer`) is serialized on the GVL. Background threads help only
|
|
62
|
+
for I/O-bound work and debouncing, not for raw parallelism.
|
|
63
|
+
- **`Dir.glob`/`Dir.children` release the GVL frequently.** Since Ruby 3.4
|
|
64
|
+
(`ruby/ruby#20587`, `#21119`) directory iteration releases the GVL per entry;
|
|
65
|
+
when *another* thread is CPU-heavy, `Dir.glob` gets dramatically slower
|
|
66
|
+
(reported up to 50×). The LSP's own background threads can therefore make an
|
|
67
|
+
uncached directory walk *worse* — another reason to stop walking the
|
|
68
|
+
filesystem on the hot path.
|
|
69
|
+
- **mtime-based cache invalidation is a common Ruby approach** (Bootsnap's
|
|
70
|
+
load-path cache), but it is **not** a single `stat` per root: directory mtime
|
|
71
|
+
only changes for files added/removed in that exact directory, so Bootsnap
|
|
72
|
+
records and re-validates the mtime of *every* scanned directory (its author
|
|
73
|
+
estimates "thousands of stat(2) syscalls" on large repos). For this LSP the
|
|
74
|
+
cleaner invalidation source is the existing `didChangeWatchedFiles` events;
|
|
75
|
+
see §4.1.
|
|
76
|
+
- **YJIT helps.** Hot Ruby loops (token classification, AST walks, prefix
|
|
77
|
+
filtering) benefit measurably; the server should be launched with YJIT
|
|
78
|
+
enabled (the launcher at `lib/milk_tea/tooling/cli/commands/lsp.rb` currently
|
|
79
|
+
does not force it, but inherits the process default).
|
|
80
|
+
- **Bounded, lazy per-item work.** Fetching documentation and resolving
|
|
81
|
+
definition tokens *for every candidate on every request* multiplies the cost
|
|
82
|
+
by the candidate count (see §4.4).
|
|
83
|
+
|
|
84
|
+
## 4. Industry-standard techniques, mapped to this codebase
|
|
85
|
+
|
|
86
|
+
### 4.1 Build a persistent module index (highest impact)
|
|
87
|
+
|
|
88
|
+
All serious LSPs index the workspace once and serve queries from memory:
|
|
89
|
+
|
|
90
|
+
- **clangd** maintains a `SymbolIndex` (file index + background index) layered
|
|
91
|
+
behind a `MergedIndex`; completion for global symbols reads the index, not
|
|
92
|
+
the AST.
|
|
93
|
+
- **SourceKit-LSP** maintains an index store for cross-file queries (definitions,
|
|
94
|
+
references, call hierarchy). Notably, completion does **not** use the index
|
|
95
|
+
store — it operates on the current file's AST plus its prepared target — which
|
|
96
|
+
keeps completion latency independent of index staleness.
|
|
97
|
+
- **Shopify ruby-lsp** builds a `RubyIndexer` — a prefix tree of all indexed
|
|
98
|
+
constants/methods, populated once at `initialized`, invalidated by file
|
|
99
|
+
watching, and reused by completion, definition, hover, and workspace symbol.
|
|
100
|
+
They specifically replaced a recursive visitor with a queue-based collector
|
|
101
|
+
for a ~25% indexing speedup (`ruby-lsp#1171`) and replaced prefix-tree
|
|
102
|
+
recursion with an explicit queue (`ruby-lsp#3401`).
|
|
103
|
+
|
|
104
|
+
**Recommendation:** add a `ModuleIndex` to the LSP `Workspace` that, on
|
|
105
|
+
`initialized` and on `workspace/didChangeWatchedFiles`, scans each module root
|
|
106
|
+
once and records the importable module names (`{root => {name => [path]}}`).
|
|
107
|
+
`import_completions` then filters the in-memory index by the typed prefix —
|
|
108
|
+
reducing the ~130–220 ms filesystem walk to a sub-millisecond hash lookup. This
|
|
109
|
+
also feeds `workspace/symbol` and global completion candidates.
|
|
110
|
+
|
|
111
|
+
On invalidation, prefer the LSP's existing `didChangeWatchedFiles` events (the
|
|
112
|
+
server already registers for them) over directory-mtime revalidation. A single
|
|
113
|
+
root-directory mtime `stat` is **not** sufficient: directory mtime only bumps
|
|
114
|
+
when a file is added/removed in *that* directory — a nested change such as
|
|
115
|
+
adding `std/sub/new/lib.mt` leaves the root directory's mtime unchanged.
|
|
116
|
+
Bootsnap's load-path cache handles this by recording and re-validating the
|
|
117
|
+
mtime of *every* scanned directory (still potentially thousands of `stat`
|
|
118
|
+
calls, per its author's analysis); the cheaper correct contract here is
|
|
119
|
+
event-driven invalidation via `didChangeWatchedFiles`, with mtime checks as a
|
|
120
|
+
fallback only for roots the editor is not watching.
|
|
121
|
+
|
|
122
|
+
### 4.2 Completion sessions + server-side re-filtering (SourceKit-LSP pattern)
|
|
123
|
+
|
|
124
|
+
SourceKit-LSP holds a **completion session** per (file, location): the full
|
|
125
|
+
candidate list is computed once, and subsequent requests with
|
|
126
|
+
`triggerKind == triggerFromIncompleteCompletions` re-filter the cached list by
|
|
127
|
+
the (longer) typed prefix instead of recomputing. Results carry
|
|
128
|
+
`isIncomplete: true` so the editor keeps re-querying cheaply while typing. It
|
|
129
|
+
also caps results (`completion-max-results=200`) to bound serialization cost.
|
|
130
|
+
|
|
131
|
+
**Recommendation:** key a completion-candidate cache by
|
|
132
|
+
`[uri, position.line, content.hash, completion_branch]`. On
|
|
133
|
+
`triggerFromIncompleteCompletions` with an unchanged line/context, filter the
|
|
134
|
+
cached candidate pool by the new prefix instead of rebuilding from `facts`.
|
|
135
|
+
`MAX_COMPLETION_ITEMS` (200) already exists and should be honored before
|
|
136
|
+
serialization (it currently is). This collapses N keystroke re-computations
|
|
137
|
+
into one compute + N cheap filters.
|
|
138
|
+
|
|
139
|
+
### 4.3 Cache completion item data; keep `resolve` cheap (Deno / tls / Roslyn)
|
|
140
|
+
|
|
141
|
+
- Deno added a short-lived `HashMap` cache for completion-item resolution:
|
|
142
|
+
1200 ms → 75 ms (`denoland/deno#27831`).
|
|
143
|
+
- typescript-language-server sends a small `cacheId` per item and resolves
|
|
144
|
+
`completionItem/resolve` against a server-side map, cutting response size
|
|
145
|
+
from 620 KB to 200 KB (`tls#768`).
|
|
146
|
+
- Roslyn's optimized completion list reduced serialization ~1.8× by not
|
|
147
|
+
round-tripping large `data` payloads.
|
|
148
|
+
|
|
149
|
+
**Recommendation:** the global branch already builds `data: {uri, name}` and
|
|
150
|
+
`handle_completion_resolve` does a `find_definition_token_global` per item —
|
|
151
|
+
fine for small modules, but it multiplies with candidate count. Cache resolved
|
|
152
|
+
documentation per `[uri, name]` (see §4.4) and avoid re-resolving definitions
|
|
153
|
+
already in the workspace definition index.
|
|
154
|
+
|
|
155
|
+
### 4.4 Stop fetching docs per candidate per keystroke
|
|
156
|
+
|
|
157
|
+
`handle_completion` (`completion.rb:330`) calls
|
|
158
|
+
`completion_function_documentation` (→ `find_definition_token_global` +
|
|
159
|
+
`doc_comment_data_for_definition`) for **every function** on **every request**
|
|
160
|
+
(`hover.rb:858`). The definition index makes each lookup fast, but it is
|
|
161
|
+
O(candidates) work repeated per keystroke and it forces definition-index
|
|
162
|
+
lookups even for items the user will never expand.
|
|
163
|
+
|
|
164
|
+
**Recommendation:** (a) memoize documentation per `[uri, name]` across requests
|
|
165
|
+
(a request-scoped cache already exists inside the branch, but it resets every
|
|
166
|
+
keystroke — promote it to a server-level cache invalidated by document change);
|
|
167
|
+
(b) only resolve docs for items the client actually resolves via
|
|
168
|
+
`completionItem/resolve`, leaving `documentation` out of the initial list.
|
|
169
|
+
This mirrors how ruby-lsp makes comment/doc collection lazy (`ruby-lsp#2547`)
|
|
170
|
+
and how Roslyn keeps large per-item payloads out of the initial completion list
|
|
171
|
+
(`dotnet/roslyn#52123`).
|
|
172
|
+
|
|
173
|
+
### 4.5 Defer heavy per-edit analysis off the request thread
|
|
174
|
+
|
|
175
|
+
`didOpen`/`didChange` (`store.rb:38`, `store.rb:82`) synchronously call
|
|
176
|
+
`warm_document_facts` → `get_facts` on the request thread: ~9–30 ms per edit
|
|
177
|
+
as measured on a tiny scratch file, and the cost grows with module complexity.
|
|
178
|
+
Note the eager warm is **not** duplicating the diagnostics sema pass:
|
|
179
|
+
`collect_diagnostics` reads `@tooling_snapshot_cache` and hands the cached
|
|
180
|
+
snapshot to `Diagnostics.collect` (`collection.rb:21,35,53`), which reuses it
|
|
181
|
+
via `sema_snapshot ||= ...` (`diagnostics.rb:88`) instead of re-running the
|
|
182
|
+
analysis. The eager warm is what *populates* that cache; the diagnostics
|
|
183
|
+
workers then collect lint/parse errors off the request thread using it.
|
|
184
|
+
|
|
185
|
+
**Recommendation:** keep the *fast* parts of `didChange` synchronous (content
|
|
186
|
+
apply, cache invalidation, dependency fingerprint check) but move the eager
|
|
187
|
+
facts analysis to the debounced background path, serving `last_good_facts`
|
|
188
|
+
until the background pass completes. This shifts the ~10 ms sema cost from the
|
|
189
|
+
keystroke-critical request thread onto the debounce timer. It does **not** make
|
|
190
|
+
`didChange` "microseconds": applying the edit, invalidating the cache, and
|
|
191
|
+
refreshing the dependency index remain synchronous — measured at ~3.8 ms on a
|
|
192
|
+
tiny file with `warm_document_facts` disabled. The win is moving the dominant
|
|
193
|
+
sema/import-resolution cost (which dominates on real modules) out of the
|
|
194
|
+
request thread, not eliminating the synchronous floor.
|
|
195
|
+
|
|
196
|
+
Requests that need fresh facts (hover/definition/completion) today already fall
|
|
197
|
+
back to `last_good_facts` when a recompute is in flight
|
|
198
|
+
(`workspace/caches.rb` `get_tooling_snapshot`, `try_lock` + last-good
|
|
199
|
+
fallback), which is the standard "index is eventually consistent" model used by
|
|
200
|
+
clangd and SourceKit-LSP. Moving the eager warm to the same background path
|
|
201
|
+
keeps that behavior while making the per-keystroke cost the synchronous floor
|
|
202
|
+
rather than full sema.
|
|
203
|
+
|
|
204
|
+
### 4.6 `milkTea/debugInfo` and `documentSymbol`
|
|
205
|
+
|
|
206
|
+
- `debugInfo` re-parses and rebuilds full semantic tokens every call
|
|
207
|
+
(`debug_info.rb`). It is a debugging endpoint, not hot-path; optionally reuse
|
|
208
|
+
`@semantic_tokens_cache` instead of `build_semantic_token_entries`.
|
|
209
|
+
- `documentSymbol` shows a wide latency spread (avg 5–18 ms, occasional 41–63 ms
|
|
210
|
+
spikes). The flat symbol list is cached (`get_symbols`), but
|
|
211
|
+
`enrich_with_children` (`formatting.rb`) walks the AST and rebuilds child
|
|
212
|
+
symbols on every request, so the spikes are consistent with enrichment
|
|
213
|
+
dominating the tail. Cache the enriched outline keyed by `content.hash` and
|
|
214
|
+
only re-enrich when content changes.
|
|
215
|
+
|
|
216
|
+
## 5. Recommended priority order
|
|
217
|
+
|
|
218
|
+
1. **Module index for `import_completions`** — removes the only >100 ms
|
|
219
|
+
endpoint; ~130–220 ms → sub-ms on the worst case. (§4.1)
|
|
220
|
+
2. **Completion session re-filtering + `isIncomplete`** — turns per-keystroke
|
|
221
|
+
recomputation into per-keystroke filtering. (§4.2)
|
|
222
|
+
3. **Promote per-function doc cache to server scope** and make doc loading
|
|
223
|
+
lazy via `completionItem/resolve`. (§4.4)
|
|
224
|
+
4. **Defer eager facts in `didChange`/`didOpen` to the background debounce.**
|
|
225
|
+
(§4.5)
|
|
226
|
+
5. **Cache the enriched `documentSymbol` outline.** (§4.6)
|
|
227
|
+
6. **Cache completion item data / keep resolve cheap.** (§4.3)
|
|
228
|
+
|
|
229
|
+
## 6. Testing strategy
|
|
230
|
+
|
|
231
|
+
- Extend `all_endpoints_benchmark.rb` (already committed) with:
|
|
232
|
+
- a large synthetic module root (e.g. 50 dirs × 30 modules) to exercise the
|
|
233
|
+
module index;
|
|
234
|
+
- a `triggerFromIncompleteCompletions` sequence measuring per-keystroke
|
|
235
|
+
filtering vs. full recompute;
|
|
236
|
+
- an edit-loop benchmark (didChange + follow-up hover) to validate the
|
|
237
|
+
deferred-facts change.
|
|
238
|
+
- Add regression tests asserting `import_completions` returns stable results
|
|
239
|
+
before/after module creation/deletion (the invalidation contract).
|
|
240
|
+
- Reuse the existing `MILK_TEA_LSP_PERF` stage breakdowns to confirm the
|
|
241
|
+
`import_context` and `facts` stages collapse after each change.
|
|
242
|
+
|
|
243
|
+
## 7. Non-goals / rejected alternatives
|
|
244
|
+
|
|
245
|
+
- **Ractor-based parallelism**: the sema pipeline shares mutable state
|
|
246
|
+
(`Types::Registry`, `@shared_module_cache`); wrapping it in `Ractor` is a
|
|
247
|
+
rewrite, not an optimization, and GVL-bound Ruby makes it premature.
|
|
248
|
+
- **Persistent on-disk index (clangd `.idx`, SourceKit index store)**: the
|
|
249
|
+
index formats here are memory-only today; on-disk persistence adds a cache-
|
|
250
|
+
invalidation protocol for little gain until workspace sizes demand it.
|
|
251
|
+
- **Replacing `Dir.children` recursion with `Dir.glob("**/*.mt")`**: faster per
|
|
252
|
+
scan but still a full-tree walk per keystroke; the module index makes it
|
|
253
|
+
unnecessary. `Dir.scan` (Ruby 4.1, `ruby/ruby#16153`, yields the child type
|
|
254
|
+
via `dirent.d_type` without N+1 `stat`s, ~2× faster scans) is a future
|
|
255
|
+
accelerator for the index build itself, not a hot-path fix.
|
|
256
|
+
|
|
257
|
+
## 8. Implementation status
|
|
258
|
+
|
|
259
|
+
Implemented against the priority order in §5:
|
|
260
|
+
|
|
261
|
+
1. **Persistent module index** (`lib/milk_tea/lsp/workspace/module_index.rb`).
|
|
262
|
+
Built once per module root (on `initialized` and lazily on first use),
|
|
263
|
+
refreshed by `workspace/didChangeWatchedFiles` create/delete events
|
|
264
|
+
(deduped per root per event batch), and rebuilt on workspace-folder
|
|
265
|
+
changes. `import_completions` filters the in-memory index instead of
|
|
266
|
+
walking the tree. `Dir.glob("**/*.mt")` replaces the per-entry `stat`
|
|
267
|
+
recursion; measured build cost on this repo's tree is ~1 ms.
|
|
268
|
+
2. **Completion sessions** (`handle_completion`). The candidate pool for a
|
|
269
|
+
`[uri, line]` is cached with its line-prefix context; a
|
|
270
|
+
`triggerFromIncompleteCompletions` request whose prefix is a strict
|
|
271
|
+
extension re-filters the cached pool instead of recomputing. A prefix
|
|
272
|
+
shrink or line-context change falls back to a full recompute. Bounded to
|
|
273
|
+
64 entries (FIFO eviction).
|
|
274
|
+
3. **Server-scoped completion doc + resolve caches.** `completion_function_documentation`
|
|
275
|
+
results are memoized per `[uri, name]` across requests (previously a
|
|
276
|
+
per-request cache reset every keystroke); `completionItem/resolve` results
|
|
277
|
+
are memoized the same way. Both invalidated on document change.
|
|
278
|
+
4. **Deferred `didOpen`/`didChange` facts.** `open_document` and
|
|
279
|
+
`apply_incremental_changes` keep the synchronous floor (content apply, cache
|
|
280
|
+
invalidation, dependency refresh) but skip the eager sema/import-resolution
|
|
281
|
+
warm for both the keystroke path and file open (`handle_did_open` and
|
|
282
|
+
`handle_did_change` pass `warm_facts: false`). The debounced diagnostics
|
|
283
|
+
worker computes facts in the background; requests serve `last_good_facts`
|
|
284
|
+
until it lands. This keeps the request thread responsive while the first
|
|
285
|
+
analysis of a large import-heavy module (e.g. `examples/language_baseline.mt`,
|
|
286
|
+
~2.4 s cold) is in flight.
|
|
287
|
+
5. **Cached `documentSymbol` outline.** The enriched outline (AST child
|
|
288
|
+
enrichment + module hierarchy) is cached keyed by content hash, removing
|
|
289
|
+
the enrichment walk from repeat requests.
|
|
290
|
+
6. **Cached completion resolve.** See #3.
|
|
291
|
+
Plus two fixes surfaced by the new benchmark sections: `workspace/symbol`
|
|
292
|
+
skips re-indexing when the on-disk file set is unchanged, and the module
|
|
293
|
+
index ignores watched-file events for open documents (matching
|
|
294
|
+
`apply_watched_file_change`).
|
|
295
|
+
7. **Eventually-consistent semantic tokens.** `handle_semantic_tokens_full`/
|
|
296
|
+
`range`/`delta` never run or wait on sema: they consume `Workspace#peek_facts`
|
|
297
|
+
(cached or last-good facts, never blocking) so the request thread stays
|
|
298
|
+
responsive while facts are recomputed after an edit. The semantic-token cache
|
|
299
|
+
is keyed by content hash **and the facts object identity**, so tokens built
|
|
300
|
+
from the lexical fallback (facts not yet computed) are never served once
|
|
301
|
+
facts land — a stale-cache bug where edited tokens stayed lexical until the
|
|
302
|
+
next content change. When the diagnostics worker lands fresh facts it sends a
|
|
303
|
+
`workspace/semanticTokens/refresh` notification so the editor re-fetches and
|
|
304
|
+
gets analyzed highlighting.
|
|
305
|
+
8. **Faster facts-driven token build.** `enclosing_completion_frame`,
|
|
306
|
+
`type_parameter_names_in_scope`, and `known_type_name?` are memoized per
|
|
307
|
+
`[facts, line]`/`[facts, name]` for the duration of one token build, turning
|
|
308
|
+
the O(tokens × frames) per-token scans into O(1) lookups. Measured build for
|
|
309
|
+
`examples/language_baseline.mt`: ~370 ms → ~110 ms steady-state.
|
|
310
|
+
9. **Diagnostics facts-path fix.** `Diagnostics.collect` skipped producing facts
|
|
311
|
+
for files whose parse recovered with errors because the loader's
|
|
312
|
+
`check_program_collecting` pass poisoned the module cache; the diagnostics
|
|
313
|
+
worker then silently kept serving stale last-good facts. The program check is
|
|
314
|
+
now skipped for parse-error files (resolving imports directly), making
|
|
315
|
+
`collect_diagnostics` facts consistent with the `analyze_document` path.
|
|
316
|
+
Also: the module index is refreshed on `workspace/willRenameFiles` so a
|
|
317
|
+
renamed module never leaves a stale import-completion entry.
|
|
318
|
+
|
|
319
|
+
Measured with `all_endpoints_benchmark.rb` (10 iterations, same run config as
|
|
320
|
+
§1):
|
|
321
|
+
|
|
322
|
+
| endpoint | before | after |
|
|
323
|
+
|---|---|---|
|
|
324
|
+
| `textDocument/completion` (import line) | ~135 ms | **~0.9 ms** |
|
|
325
|
+
| `textDocument/didChange` (scratch file) | ~10 ms | **~1.6 ms** |
|
|
326
|
+
| `textDocument/didOpen` (scratch file) | ~15 ms | **~0.3 ms** (facts deferred to worker) |
|
|
327
|
+
| `textDocument/documentSymbol` | ~5.4 ms avg / ~44 ms max | **~0 ms** (cached outline) |
|
|
328
|
+
| `textDocument/completion` (1500-module root) | n/a | **~1.3 ms** (indexed) |
|
|
329
|
+
| `textDocument/completion` (incomplete-trigger, 6 keystrokes) | n/a | ~46 ms worst case with diagnostics workers stopped; workers running, the debounced facts pass keeps per-keystroke cost at the didChange floor |
|
|
330
|
+
|
|
331
|
+
First-open flow for a large import-heavy module (`examples/language_baseline.mt`,
|
|
332
|
+
53 KB / 2062 lines, cold analysis ~2.1 s): `didOpen` returns in <1 ms on the
|
|
333
|
+
request thread, the editor renders lexical highlighting immediately, and the
|
|
334
|
+
background diagnostics worker computes facts (~2.1 s) and pushes a
|
|
335
|
+
`workspace/semanticTokens/refresh`; the follow-up `semanticTokens/full` request
|
|
336
|
+
serves the analyzed highlighting. The request thread is never blocked by the
|
|
337
|
+
initial analysis.
|
|
338
|
+
|
|
339
|
+
Regression tests: `test/tooling/lsp/workspace_test.rb` (module-index build,
|
|
340
|
+
create/delete and rename invalidation, current-file exclusion),
|
|
341
|
+
`test/tooling/lsp/server/completion_test.rb` (import completion create/delete
|
|
342
|
+
contract, `triggerFromIncompleteCompletions` re-filtering and the prefix-shrink
|
|
343
|
+
recompute guard), and `test/tooling/lsp/server/semantic_tokens_test.rb`
|
|
344
|
+
(semantic tokens rebuild when facts land after open — the stale-cache
|
|
345
|
+
regression). The LSP test client forces facts deterministically before
|
|
346
|
+
semantic-token requests because didOpen defers analysis to the background
|
|
347
|
+
worker.
|
data/lib/milk_tea/base.rb
CHANGED
data/lib/milk_tea/core/ast.rb
CHANGED
|
@@ -111,8 +111,8 @@ module MilkTea
|
|
|
111
111
|
InterfaceDecl = Data.define(:name, :type_params, :methods, :visibility, :line, :column) do
|
|
112
112
|
def initialize(name:, type_params: [], methods:, visibility:, line: nil, column: nil) = super
|
|
113
113
|
end
|
|
114
|
-
ExtendingBlock = Data.define(:type_name, :methods, :line, :column) do
|
|
115
|
-
def initialize(type_name:, methods:, line: nil, column: nil) = super
|
|
114
|
+
ExtendingBlock = Data.define(:type_name, :methods, :line, :column, :inline) do
|
|
115
|
+
def initialize(type_name:, methods:, line: nil, column: nil, inline: false) = super
|
|
116
116
|
end
|
|
117
117
|
InterfaceMethodDecl = Data.define(:name, :params, :return_type, :kind, :async, :attributes, :line, :column) do
|
|
118
118
|
def initialize(name:, params:, return_type:, kind:, async:, attributes: [], line: nil, column: nil) = super
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module MilkTea
|
|
4
4
|
module Bindings
|
|
5
|
-
AttributeBinding = Data.define(:name, :targets, :params, :module_name, :builtin
|
|
5
|
+
AttributeBinding = Data.define(:name, :targets, :params, :module_name, :builtin)
|
|
6
6
|
BUILTIN_ATTRIBUTE_NAMES = %w[packed align deprecated test expect_fatal].freeze
|
|
7
7
|
|
|
8
8
|
def self.builtin_attribute_binding(name, types)
|
|
@@ -14,7 +14,6 @@ module MilkTea
|
|
|
14
14
|
params: [].freeze,
|
|
15
15
|
module_name: nil,
|
|
16
16
|
builtin: true,
|
|
17
|
-
ast: nil,
|
|
18
17
|
)
|
|
19
18
|
when "align"
|
|
20
19
|
AttributeBinding.new(
|
|
@@ -23,7 +22,6 @@ module MilkTea
|
|
|
23
22
|
params: [Types::Registry.parameter("bytes", types.fetch("ptr_uint"))].freeze,
|
|
24
23
|
module_name: nil,
|
|
25
24
|
builtin: true,
|
|
26
|
-
ast: nil,
|
|
27
25
|
)
|
|
28
26
|
when "deprecated"
|
|
29
27
|
AttributeBinding.new(
|
|
@@ -32,7 +30,6 @@ module MilkTea
|
|
|
32
30
|
params: [Types::Registry.parameter("message", types.fetch("str"))].freeze,
|
|
33
31
|
module_name: nil,
|
|
34
32
|
builtin: true,
|
|
35
|
-
ast: nil,
|
|
36
33
|
)
|
|
37
34
|
when "test"
|
|
38
35
|
AttributeBinding.new(
|
|
@@ -41,7 +38,6 @@ module MilkTea
|
|
|
41
38
|
params: [].freeze,
|
|
42
39
|
module_name: nil,
|
|
43
40
|
builtin: true,
|
|
44
|
-
ast: nil,
|
|
45
41
|
)
|
|
46
42
|
when "expect_fatal"
|
|
47
43
|
AttributeBinding.new(
|
|
@@ -50,7 +46,6 @@ module MilkTea
|
|
|
50
46
|
params: [].freeze,
|
|
51
47
|
module_name: nil,
|
|
52
48
|
builtin: true,
|
|
53
|
-
ast: nil,
|
|
54
49
|
)
|
|
55
50
|
end
|
|
56
51
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module MilkTea
|
|
4
4
|
module Bindings
|
|
5
|
-
ModuleBinding = Data.define(:name, :types, :type_declarations, :interfaces, :attributes, :attribute_applications, :values, :functions, :methods, :implemented_interfaces, :imports, :private_types, :private_interfaces, :private_attributes, :private_values, :private_functions, :private_methods
|
|
5
|
+
ModuleBinding = Data.define(:name, :types, :type_declarations, :interfaces, :attributes, :attribute_applications, :values, :functions, :methods, :implemented_interfaces, :imports, :private_types, :private_interfaces, :private_attributes, :private_values, :private_functions, :private_methods) do
|
|
6
6
|
def private_type?(name)
|
|
7
7
|
private_types.key?(name)
|
|
8
8
|
end
|
|
@@ -84,5 +84,27 @@ module MilkTea
|
|
|
84
84
|
def string_literal_cstr_compatibility?(expression, expected_type)
|
|
85
85
|
expression.is_a?(AST::StringLiteral) && !expression.cstring && expected_type == BUILTIN_CSTR
|
|
86
86
|
end
|
|
87
|
+
|
|
88
|
+
def current_nested_types
|
|
89
|
+
@current_nested_types
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Resolves the receiver type's short name (self-reference) and its own
|
|
93
|
+
# nested types as a bare-name scope for method bodies and signatures.
|
|
94
|
+
def method_receiver_nested_scope(receiver_type)
|
|
95
|
+
return nil unless receiver_type.respond_to?(:name)
|
|
96
|
+
|
|
97
|
+
nested = receiver_type.respond_to?(:nested_types) ? receiver_type.nested_types : nil
|
|
98
|
+
scope = { receiver_type.name => receiver_type }
|
|
99
|
+
scope = scope.merge(nested) if nested && !nested.empty?
|
|
100
|
+
scope
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Looks up a bare type name, consulting the active method-receiver nested
|
|
104
|
+
# scope before the module type namespace so nested types self-reference
|
|
105
|
+
# by their short name inside their own methods.
|
|
106
|
+
def lookup_named_type(name)
|
|
107
|
+
(@current_nested_types && @current_nested_types[name]) || @ctx.types[name]
|
|
108
|
+
end
|
|
87
109
|
end
|
|
88
|
-
end
|
|
110
|
+
end
|
|
@@ -118,8 +118,10 @@ module MilkTea
|
|
|
118
118
|
parameter_setup = []
|
|
119
119
|
previous_type_substitutions = @ctx.current_type_substitutions
|
|
120
120
|
previous_value_type_params = @ctx.current_value_type_params
|
|
121
|
+
previous_nested_types = @current_nested_types
|
|
121
122
|
@ctx.current_type_substitutions = binding.type_substitutions
|
|
122
123
|
@ctx.current_value_type_params = resolve_value_type_params(decl.type_params)
|
|
124
|
+
@current_nested_types = method_receiver_nested_scope(binding.declared_receiver_type)
|
|
123
125
|
|
|
124
126
|
return lower_async_function_decl(binding, receiver_type:) if binding.async
|
|
125
127
|
|
|
@@ -179,6 +181,7 @@ module MilkTea
|
|
|
179
181
|
ensure
|
|
180
182
|
@ctx.current_type_substitutions = previous_type_substitutions
|
|
181
183
|
@ctx.current_value_type_params = previous_value_type_params
|
|
184
|
+
@current_nested_types = previous_nested_types
|
|
182
185
|
end
|
|
183
186
|
|
|
184
187
|
def resolve_value_type_params(type_params)
|
|
@@ -444,7 +444,7 @@ module MilkTea
|
|
|
444
444
|
return [:compile_time_builtin, callee.name, nil, compile_time_builtin_function_type(callee.name, arguments, env)]
|
|
445
445
|
end
|
|
446
446
|
|
|
447
|
-
type =
|
|
447
|
+
type = lookup_named_type(callee.name)
|
|
448
448
|
if type.is_a?(Types::Struct) || type.is_a?(Types::StringView) || task_type?(type) || type.is_a?(Types::Vector) || type.is_a?(Types::Matrix) || type.is_a?(Types::Quaternion)
|
|
449
449
|
return [:struct_literal, nil, nil, type]
|
|
450
450
|
end
|
|
@@ -606,7 +606,6 @@ module MilkTea
|
|
|
606
606
|
return [:array_as_span, nil, callee.receiver, Types::Registry.span(array_element_type(field_receiver_type))]
|
|
607
607
|
end
|
|
608
608
|
|
|
609
|
-
member_type = field_receiver_type.respond_to?(:field) ? field_receiver_type.field(callee.member) : nil
|
|
610
609
|
member_type = field_receiver_type.respond_to?(:field) ? field_receiver_type.field(callee.member) : nil
|
|
611
610
|
return [:callable_value, nil, nil, member_type, nil] if callable_type?(member_type)
|
|
612
611
|
|
|
@@ -1055,7 +1054,7 @@ module MilkTea
|
|
|
1055
1054
|
end
|
|
1056
1055
|
end
|
|
1057
1056
|
|
|
1058
|
-
def type_contains_array_storage?(type
|
|
1057
|
+
def type_contains_array_storage?(type)
|
|
1059
1058
|
visitor = Types::ContainsArrayStorageVisitor.new
|
|
1060
1059
|
visitor.visit(type)
|
|
1061
1060
|
visitor.found?
|
|
@@ -1118,62 +1117,6 @@ module MilkTea
|
|
|
1118
1117
|
expected_type || null_type
|
|
1119
1118
|
end
|
|
1120
1119
|
|
|
1121
|
-
def common_numeric_type(left_type, right_type)
|
|
1122
|
-
left_type = left_type.backing_type if left_type.is_a?(Types::EnumBase)
|
|
1123
|
-
right_type = right_type.backing_type if right_type.is_a?(Types::EnumBase)
|
|
1124
|
-
return left_type if left_type == right_type
|
|
1125
|
-
return unless left_type.is_a?(Types::Primitive) && right_type.is_a?(Types::Primitive)
|
|
1126
|
-
return unless left_type.numeric? && right_type.numeric?
|
|
1127
|
-
|
|
1128
|
-
return common_integer_type(left_type, right_type) if left_type.integer? && right_type.integer?
|
|
1129
|
-
return wider_float_type(left_type, right_type) if left_type.float? && right_type.float?
|
|
1130
|
-
|
|
1131
|
-
float_type, integer_type = left_type.float? ? [left_type, right_type] : [right_type, left_type]
|
|
1132
|
-
return unless integer_type.integer? && integer_type.fixed_width_integer?
|
|
1133
|
-
|
|
1134
|
-
float_type
|
|
1135
|
-
end
|
|
1136
|
-
|
|
1137
|
-
def common_integer_type(left_type, right_type)
|
|
1138
|
-
left_type = left_type.backing_type if left_type.is_a?(Types::EnumBase)
|
|
1139
|
-
right_type = right_type.backing_type if right_type.is_a?(Types::EnumBase)
|
|
1140
|
-
return left_type if left_type == right_type
|
|
1141
|
-
return unless left_type.is_a?(Types::Primitive) && right_type.is_a?(Types::Primitive)
|
|
1142
|
-
return unless left_type.integer? && right_type.integer?
|
|
1143
|
-
return unless left_type.fixed_width_integer? && right_type.fixed_width_integer?
|
|
1144
|
-
|
|
1145
|
-
# Mirrors the semantic analyzer's rule: same signedness picks the wider
|
|
1146
|
-
# type; mixed signed/unsigned promotes to the narrowest signed type that
|
|
1147
|
-
# holds both operands' full ranges (a strictly-wider signed type covers
|
|
1148
|
-
# an unsigned operand, otherwise widen to the next signed width). Mixing
|
|
1149
|
-
# with a 64-bit unsigned type has no safe signed common type.
|
|
1150
|
-
if left_type.signed_integer? == right_type.signed_integer?
|
|
1151
|
-
return left_type.integer_width >= right_type.integer_width ? left_type : right_type
|
|
1152
|
-
end
|
|
1153
|
-
|
|
1154
|
-
signed_type, unsigned_type = if left_type.signed_integer?
|
|
1155
|
-
[left_type, right_type]
|
|
1156
|
-
else
|
|
1157
|
-
[right_type, left_type]
|
|
1158
|
-
end
|
|
1159
|
-
|
|
1160
|
-
return signed_type if signed_type.integer_width > unsigned_type.integer_width
|
|
1161
|
-
|
|
1162
|
-
signed_type_above_width(unsigned_type.integer_width)
|
|
1163
|
-
end
|
|
1164
|
-
|
|
1165
|
-
def signed_type_above_width(width)
|
|
1166
|
-
case width
|
|
1167
|
-
when 8 then @ctx.types.fetch("short")
|
|
1168
|
-
when 16 then @ctx.types.fetch("int")
|
|
1169
|
-
when 32 then @ctx.types.fetch("long")
|
|
1170
|
-
end
|
|
1171
|
-
end
|
|
1172
|
-
|
|
1173
|
-
def wider_float_type(left_type, right_type)
|
|
1174
|
-
left_type.float_width >= right_type.float_width ? left_type : right_type
|
|
1175
|
-
end
|
|
1176
|
-
|
|
1177
1120
|
def aggregate_arithmetic_result_type(operator, left_type, right_type)
|
|
1178
1121
|
if left_type.is_a?(Types::Vector) && right_type.is_a?(Types::Vector) && left_type.name == right_type.name
|
|
1179
1122
|
return left_type
|
|
@@ -1217,7 +1160,7 @@ module MilkTea
|
|
|
1217
1160
|
when AST::Identifier
|
|
1218
1161
|
return current_type_params[expression.name] if current_type_params.key?(expression.name)
|
|
1219
1162
|
|
|
1220
|
-
|
|
1163
|
+
lookup_named_type(expression.name)
|
|
1221
1164
|
when AST::MemberAccess
|
|
1222
1165
|
return nil unless expression.receiver.is_a?(AST::Identifier)
|
|
1223
1166
|
|
|
@@ -1870,10 +1813,6 @@ module MilkTea
|
|
|
1870
1813
|
def top_level_function(name)
|
|
1871
1814
|
@lowerer.instance_variable_get(:@ctx).functions&.[](name)
|
|
1872
1815
|
end
|
|
1873
|
-
|
|
1874
|
-
def raise_sema_error(message)
|
|
1875
|
-
raise CompileTime::Error, message
|
|
1876
|
-
end
|
|
1877
1816
|
end
|
|
1878
1817
|
|
|
1879
1818
|
def evaluate_reflection_target_argument(expression, env:)
|
|
@@ -2532,7 +2471,7 @@ module MilkTea
|
|
|
2532
2471
|
elsif parts.length == 1 && type_params.key?(parts.first)
|
|
2533
2472
|
type_params.fetch(parts.first)
|
|
2534
2473
|
elsif parts.length == 1
|
|
2535
|
-
type =
|
|
2474
|
+
type = lookup_named_type(parts.first)
|
|
2536
2475
|
raise LoweringError.new("unknown type #{parts.first}", line: 0, column: 0, path: @ctx.current_analysis_path) unless type
|
|
2537
2476
|
raise LoweringError.new("generic type #{parts.first} requires type arguments", line: 0, column: 0, path: @ctx.current_analysis_path) if type.is_a?(Types::GenericStructDefinition) || type.is_a?(Types::GenericVariantDefinition)
|
|
2538
2477
|
|
|
@@ -155,18 +155,6 @@ module MilkTea
|
|
|
155
155
|
receiver_type
|
|
156
156
|
end
|
|
157
157
|
|
|
158
|
-
def collection_loop_type(type)
|
|
159
|
-
super
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def collection_loop_binding_type(iterable_type, element_type)
|
|
163
|
-
super
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def collection_loop_ref_element_type?(type)
|
|
167
|
-
super
|
|
168
|
-
end
|
|
169
|
-
|
|
170
158
|
def collection_loop_item_value(iterable_ref, iterable_type, index_ref, element_type)
|
|
171
159
|
if array_type?(iterable_type)
|
|
172
160
|
IR::Index.new(receiver: iterable_ref, index: index_ref, type: element_type)
|
|
@@ -248,11 +236,6 @@ module MilkTea
|
|
|
248
236
|
raise LoweringError.new("cannot index #{receiver_type}", line: 0, column: 0, path: @ctx.current_analysis_path)
|
|
249
237
|
end
|
|
250
238
|
|
|
251
|
-
def contains_type_var?(type)
|
|
252
|
-
super
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
|
|
256
239
|
def stored_ref_supported_type?(type, visited = {})
|
|
257
240
|
return true unless type
|
|
258
241
|
|
|
@@ -100,7 +100,6 @@ module MilkTea
|
|
|
100
100
|
include Intrinsics
|
|
101
101
|
|
|
102
102
|
attr_accessor :bypass_sema_type_cache
|
|
103
|
-
attr_reader :recorded_expr_types
|
|
104
103
|
|
|
105
104
|
include Lowering::Scans
|
|
106
105
|
include Lowering::Declarations
|
|
@@ -122,6 +121,7 @@ module MilkTea
|
|
|
122
121
|
@program = program
|
|
123
122
|
@ctx = LoweringContext.new
|
|
124
123
|
@artifacts = Artifacts.new
|
|
124
|
+
@error_type = Types::Error.new
|
|
125
125
|
@synthetic_proc_counter = 0
|
|
126
126
|
@parallel_for_counter = 0
|
|
127
127
|
@async_binding_counter = 0
|
|
@@ -130,7 +130,6 @@ module MilkTea
|
|
|
130
130
|
end
|
|
131
131
|
|
|
132
132
|
def lower
|
|
133
|
-
@recorded_expr_types = {} if @bypass_sema_type_cache
|
|
134
133
|
ir_program, _modules, _synths = lower_and_assemble
|
|
135
134
|
ir_program
|
|
136
135
|
end
|