Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
111 commits
Select commit Hold shift + click to select a range
3626ebb
fix: keep diagnostics visible and stop stale index reads
Pollux12 Aug 16, 2026
96af967
chore: add a latency test tool
Pollux12 Aug 16, 2026
bf10b1a
chore: measure the release binary
Pollux12 Aug 16, 2026
e7c05fd
perf: get dependency levels from the analysis order
Pollux12 Aug 16, 2026
d59e7fc
perf: cache syntax id to node lookups
Pollux12 Aug 17, 2026
9dee49d
perf: cache local function call sites
Pollux12 Aug 17, 2026
1dc8fd8
perf: return SmolStr instead of String from name lookups
Pollux12 Aug 17, 2026
e7d18b8
refactor: find closure return statements once
Pollux12 Aug 17, 2026
c3fbc35
perf: cache inherited member parameter lookups
Pollux12 Aug 17, 2026
60f242b
Merge branch 'feat/analysis-refactor' into perf/incremental-analysis-…
Pollux12 Aug 17, 2026
038e68e
chore: format
Pollux12 Aug 17, 2026
90bba5c
fix: resolve self to its scripted class
Pollux12 Aug 17, 2026
204f01c
fix: keep walking sibling super types
Pollux12 Aug 17, 2026
f96f03a
Merge branch 'feat/analysis-refactor' into perf/incremental-analysis-…
Pollux12 Aug 17, 2026
34f4ca9
Merge branch 'main' into perf/incremental-analysis-cost
Pollux12 Aug 18, 2026
a5674ec
feat: support out parameters that are not fields
Pollux12 Aug 18, 2026
bfb83b0
perf: local function call site cache
Pollux12 Aug 19, 2026
8ae9027
perf: inherited parameter cache
Pollux12 Aug 19, 2026
40f929d
fix: refuse rename on self
Pollux12 Aug 19, 2026
4b7c5a2
fix: keep the dirty flag set during change
Pollux12 Aug 19, 2026
5d42a59
perf: limit size of syntax node cache
Pollux12 Aug 19, 2026
ccb5b4e
perf: collect net flows from index
Pollux12 Aug 20, 2026
aa49156
perf: look up a global by key
Pollux12 Aug 20, 2026
8c7295a
fix: infinite unresolve loop
Pollux12 Aug 20, 2026
ef6e75a
perf: skip re-deriving a return type that is already known
Pollux12 Aug 20, 2026
ca7ddb4
perf: cache closure flow walk
Pollux12 Aug 20, 2026
1aabc60
fix: run the benchmark and determinism tools on a bigger stack
Pollux12 Aug 20, 2026
d6ff473
feat: additional logging for stalled runs
Pollux12 Aug 20, 2026
e51618c
fix: missing diagnostics on initial startup
Pollux12 Aug 20, 2026
c4bc332
perf: lookup dynamic key members by key
Pollux12 Aug 20, 2026
d015e65
perf: find member node by syntax id
Pollux12 Aug 20, 2026
ce1363d
perf: build checker name lists once per run
Pollux12 Aug 20, 2026
4c9b5a7
perf: find function closure by offset
Pollux12 Aug 20, 2026
858e6d1
fix: scoped class declaration being unordered
Pollux12 Aug 20, 2026
513c0d4
perf: index hash maps
Pollux12 Aug 20, 2026
9c87fd4
feat: additional debug output during runs
Pollux12 Aug 20, 2026
97f415f
chore: formatter and docs
Pollux12 Aug 20, 2026
b3ecd37
fix: nil check hiding undefined methods
Pollux12 Aug 20, 2026
ce32f64
feat: narrow a field to its subclass
Pollux12 Aug 20, 2026
fa79965
perf: filter declarations before cloning
Pollux12 Aug 21, 2026
17e4f16
perf: fxhash the module index
Pollux12 Aug 21, 2026
c2937d3
perf: skip declarations that own no members
Pollux12 Aug 21, 2026
c43e474
perf: cache whether a call returns never
Pollux12 Aug 21, 2026
f93dcc7
perf: fxhash more db indexes
Pollux12 Aug 21, 2026
3495703
fix: build the determinism tool on linux
Pollux12 Aug 21, 2026
02b06a2
fix: nil check hiding undefined dot calls
Pollux12 Aug 21, 2026
b333258
fix: net flows through local functions
Pollux12 Aug 21, 2026
231ccc3
fix: parameter cache going stale
Pollux12 Aug 21, 2026
583b7fb
fix: member lookup for keys with dots
Pollux12 Aug 21, 2026
3f31261
fix: narrowing feeding on its own result
Pollux12 Aug 21, 2026
2101ae0
fix: unresolve items sharing one identity
Pollux12 Aug 21, 2026
8c71429
fix: unsorted output from hash maps
Pollux12 Aug 21, 2026
b855163
fix: log lines split into pieces
Pollux12 Aug 21, 2026
4dcac4d
fix: progress reports blocking analysis
Pollux12 Aug 21, 2026
c575dbb
fix: rename offered then refused
Pollux12 Aug 21, 2026
a696c8f
fix: document versions behind an async lock
Pollux12 Aug 21, 2026
a71b6c3
fix: debounce loop respawning forever
Pollux12 Aug 21, 2026
d36f13f
perf: log less while diagnosing files
Pollux12 Aug 21, 2026
0e48c2d
test: count flow walks instead of timing them
Pollux12 Aug 21, 2026
45bec86
test: check the analysis order
Pollux12 Aug 21, 2026
2379116
fix: nested sample phases turning sampling off
Pollux12 Aug 21, 2026
c8fee21
fix: latency harness never failing
Pollux12 Aug 21, 2026
2449565
fix: progress report arriving after the task ends
Pollux12 Aug 21, 2026
488eaeb
fix: clippy failures on rust 1.98
Pollux12 Aug 21, 2026
ad852fb
perf: read branch headers without bodies
Pollux12 Aug 22, 2026
eabfce7
fix: table field key differing by analysis order
Pollux12 Aug 22, 2026
0cb65bd
perf: join unions without rebuilding them
Pollux12 Aug 22, 2026
2ddd52a
perf: answer requests before the edit ripple ends
Pollux12 Aug 22, 2026
b6cb82e
perf: filter completion members without walking the file
Pollux12 Aug 22, 2026
59ba5b2
test: gate index against a no-op re-index
Pollux12 Aug 22, 2026
410c9b1
test: benchmark the workspace the editor opens
Pollux12 Aug 22, 2026
2ef283a
test: measure real edits in the latency harness
Pollux12 Aug 22, 2026
ed49821
fix: member attach dropped after one retry
Pollux12 Aug 22, 2026
a9e6239
fix: local typed from an unindexed dynamic field
Pollux12 Aug 22, 2026
e06fbc2
test: list files a no-op re-index covers
Pollux12 Aug 22, 2026
acb4124
test: show what the index drifted from
Pollux12 Aug 22, 2026
2eb8dcd
test: gate index against an edit and its reversal
Pollux12 Aug 22, 2026
b6bc7a6
test: stop index gates masking each other
Pollux12 Aug 22, 2026
64d6cfb
fix: global type from a partial writer set
Pollux12 Aug 22, 2026
6c8d808
fix: unknown callback return outranking closure body
Pollux12 Aug 22, 2026
0602ad0
docs: record what the index drift is
Pollux12 Aug 22, 2026
61e4389
fix: global members homed by batch walk order
Pollux12 Aug 22, 2026
91ffd39
fix: table key type from first sorted writer
Pollux12 Aug 22, 2026
06d526b
fix: write widened by siblings the batch reached
Pollux12 Aug 22, 2026
c0da859
fix: late write contributing no evidence
Pollux12 Aug 22, 2026
e69064e
fix: _G loop variable typed from a snapshot
Pollux12 Aug 22, 2026
9f0ce84
fix: slot decided by alias or owner arrival
Pollux12 Aug 22, 2026
a5b29aa
fix: return floored by first expression reached
Pollux12 Aug 22, 2026
e0bf284
style: format contribution-group snapshot insert
Pollux12 Aug 22, 2026
95c9e6a
fix: pairs key union from indices written so far
Pollux12 Aug 23, 2026
95b9b27
fix: literal widened by an untyped sibling
Pollux12 Aug 23, 2026
bf78d58
fix: field lookup ended by a typeless entry
Pollux12 Aug 23, 2026
478950f
fix: deferred write narrowing a decl inline would not
Pollux12 Aug 23, 2026
5c9ffcc
fix: slot widened by writes under an if
Pollux12 Aug 23, 2026
cd0827c
fix: or answered with an empty truthy half
Pollux12 Aug 23, 2026
0eff688
fix: retry-finished write missing visibility marks
Pollux12 Aug 23, 2026
63aed26
test: measure re-index idempotency and expansion
Pollux12 Aug 23, 2026
7e4266e
test: check whether a re-index converges
Pollux12 Aug 23, 2026
e2302b0
fix: undefined global read erasing the local
Pollux12 Aug 23, 2026
310aedd
fix: or keeping an unknown left arm
Pollux12 Aug 23, 2026
81f7e28
test: re-derive each file against settled index
Pollux12 Aug 23, 2026
f56b98c
perf: let requests read before the ripple takes the lock
Pollux12 Aug 23, 2026
8a75f36
test: gate a deferred ripple against separate ripples
Pollux12 Aug 23, 2026
e701198
perf: hold the ripple until typing stops
Pollux12 Aug 23, 2026
bec3afc
test: keep the ripple out of self-only edit profiles
Pollux12 Aug 23, 2026
a269750
perf: reuse vgui parent resolutions for untouched files
Pollux12 Aug 23, 2026
61de75d
perf: run small file batches without spawning workers
Pollux12 Aug 23, 2026
c02ff99
perf: use the fast hasher for flow index maps
Pollux12 Aug 23, 2026
2d0774d
perf: skip re-applying aliases already recorded
Pollux12 Aug 23, 2026
f1c9cf2
perf: look up type cache references instead of scanning
Pollux12 Aug 23, 2026
392673b
perf: highlight the edited file without waiting on others
Pollux12 Aug 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- `crates/glua_doc_cli`, `crates/schema_to_glua`, and `tools/schema_json_gen`: documentation and schema tooling.
- `tools/benchmark`: large-workspace benchmark. It requires `BENCH_CODEBASE` and `BENCH_ANNOTATIONS`.
- `tools/determinism`: diagnostic determinism harness. It requires `DET_CODEBASE` and `DET_ANNOTATIONS`, and answers whether re-analysing a workspace yields the same diagnostics as building it cold. See the module docs for the stage list.
- `tools/lsp_latency.js`: interactive latency harness. It requires `LSP_CODEBASE` and `LSP_ANNOTATIONS`, and drives a real `glua_ls` binary over stdio using the capabilities and cancellation behaviour VS Code actually uses. Reports completion and diagnostic latency settled versus mid-edit, and asserts that a cancelled diagnostic pull never returns an empty full report (which clears a file's diagnostics in VS Code). Use it before and after any change to reindexing or to the freshness gates — those costs are invisible to unit tests.
- `docs/mintlify`: user documentation. Follow its nested `AGENTS.md` for changes under that tree.

## Analysis Architecture
Expand Down Expand Up @@ -51,8 +52,9 @@
- Call-role and annotation-driven tests should load the relevant builtins; otherwise they may pass while bypassing the real metadata path.
- Typical test commands are `cargo test -p glua_code_analysis <test_name>`, `cargo test -p glua_code_analysis`, and `cargo test`.
- Use `glua_check` JSON output for before/after corpus diagnostic comparisons. The benchmark measures performance; it is not a diagnostics oracle.
- Changes to indexes, cached inference, or the unresolve/resolution passes must keep incremental re-analysis equal to a cold build. Verify with `cargo run --release -p determinism` on a real workspace; `repeat`, `order`, `fresh`, `reindex`, `allreindex` and `mainexpand` are expected to report IDENTICAL. The two edit gates are `noopedit` (formerly `edit`) and `realedit`. `noopedit` gates the semantic no-op skip and nothing more: its edit pair is semantically unchanged, so the update path skips the re-index outright and the stage verifies that skipping preserves state — include at least one wide-expansion target (e.g. CityRP `gamemode/core/sh_util.lua`, 1300 files) so the skip is exercised where it matters most. `realedit` is the gate for incremental re-analysis itself, since its edit changes what the file means; set `DET_EDIT_FIND` and `DET_EDIT_REPLACE` or it skips and nothing gates re-analysis. It passes today — CityRP measures IDENTICAL at 11,655 entries with `members dropped=0 gained=0` — so treat any divergence as a regression you introduced, not as a known gap. Note it is far more sensitive than the other gates: changes to the unresolve waves, the infer-cache lifetime, or member ownership can break `realedit` while all seven others still report IDENTICAL. `mainreindex`, `exact`, `split:N` and `editmid` are bisect stages, not gates: the first three run `reindex_files_without_expansion`, which skips production's convergence passes, and `editmid` forces a real offset-shifting re-analysis of the expansion (the batch-composition confluence gap), so they are expected to diverge and only matter for localising a failure the gates already caught. Re-run it before and after, because a change can make a stage identical by *degrading* the cold build rather than by fixing the re-index.
- Changes to indexes, cached inference, or the unresolve/resolution passes must keep incremental re-analysis equal to a cold build. Verify with `cargo run --release -p determinism` on a real workspace; `repeat`, `order`, `fresh`, `reindex`, `allreindex` and `mainexpand` are expected to report IDENTICAL. A third gate, `indexrepeat`, re-indexes each target with its text untouched and requires the **index** to come back identical; the diagnostic gates cannot see index drift, because re-analysis can attach different members or settle a decl's type differently and still produce the same diagnostics. It does **not** pass today (CityRP: 80 type caches, 2 signatures, 11 class members change on a no-op re-index) and that drift is why incremental work cannot be skipped — every "did this actually change?" test answers yes — so treat any *growth* in those counts as yours. Use `DET_TARGETS=gamemode/core/sh_data.lua` as the working repro: it expands to **4 files** and reproduces the same defect at 4/0/3, which is far cheaper to iterate on than the 1306-file one. The remaining drift sits in three readers, each proven by trace: the sibling-widening cache (`lua/stats.rs`, where `visible_member_count_for_owner_key` is 2 cold and 1 warm, so `lookup_widening_cache` returns `FirstSighting` and `get_widened_member_assignment_type` is never called, so nothing arms the settled retry); first-writer-wins on a decl slot (`common/mod.rs:206-215` deliberately keeps an `any`/`unknown` decl cache, pinned by three tests, so the slot is claimed by whichever writer arrives first and an unrelated later assignment can seed it); and attach-candidate lifetime (`analyzer/mod.rs:317-377`, whose retry list lives in a context that dies when `analyze()` returns, making member *existence* batch-dependent — it owns all 11 class-member drifts). Two dead ends already paid for: arming the settled retry from the `FirstSighting` arm fixes two entries and takes the 1306 gate from 80 to **144**, because it widens members cold previously left alone; and `rederive_contributed_member_assignments` cannot fix the widening class at all, because its `take_while` merges each writer only against *earlier* writers so a first writer is never re-derived. The defect is that analysis output depends on how the workspace was *batched*, not on the source alone: `remove_index(batch)` runs before `update_index(batch)`, so a file sees out-of-batch neighbours complete but in-batch neighbours empty until the walk reaches them. A whole-workspace batch hides everything and so reproduces the cold build exactly (`allreindex` and `mainexpand` are both byte-identical to cold); a four-file batch hides almost nothing and lands somewhere else. It is not edit-specific — `split:4` builds the same workspace cold in four batches and produces 299 different diagnostics against `split:1`. Do not "fix" it by re-indexing everything on an edit: that forces the whole-workspace batch, costs more than a cold build, and freezes the least-informed answer. `editrevert` is the drift gate for the *other* edit path. It applies a real edit through `update_file_by_uri` and then takes it back out; the source ends where it started, so the index and the diagnostics have to as well, and it needs no ground-truth build because the pre-edit index is the truth. It covers what the others cannot — `indexrepeat` re-indexes with the text untouched and so never exercises an edit's invalidation, and `noopedit`'s pair is semantically neutral, so the update path skips the re-index outright. It does **not** pass today: on CityRP an edit-and-revert of `gamemode/core/sh_util.lua` leaves 79 type caches, 3 signatures and 11 class member lists moved, plus 2 `need-check-nil` diagnostics. Treat growth in those counts as yours. Both index gates build their own analysis rather than sharing the caller's, because each re-indexes in place and leaves a converged index behind — sharing one let whichever ran second measure against the other's converged state and report a clean 0, which hid the drift rather than removing it. The two edit gates are `noopedit` (formerly `edit`) and `realedit`. `noopedit` gates the semantic no-op skip and nothing more: its edit pair is semantically unchanged, so the update path skips the re-index outright and the stage verifies that skipping preserves state — include at least one wide-expansion target (e.g. CityRP `gamemode/core/sh_util.lua`, 1300 files) so the skip is exercised where it matters most. `realedit` is the gate for incremental re-analysis itself, since its edit changes what the file means; set `DET_EDIT_FIND` and `DET_EDIT_REPLACE` or it skips and nothing gates re-analysis. It does **not** pass today, and the divergence is a known gap rather than something you introduced — but it is a small, fixed one, so measure it before and after your change and treat any *growth* as yours. On CityRP, editing `gamemode/core/sh_util.lua` (`function cityrp.util.Bind(self, callback)` gaining a parameter) gives `cold_edited -> warm: removed=0 added=2`, both `need-check-nil` in `plugins/cwweapons` (`cw_base/shared.lua:999`, `cw_m249_official/shared.lua:304`). The cause is visible in the index diff: a signature is identified by its position, an edit moves the positions of every signature after it, and `CallSiteParamIndex` contributions that *target* a moved signature are only dropped when the contributing file is itself re-indexed. A contributor outside the reindex expansion keeps pointing at the old position, so `rebuild_derived_state` splits one parameter's inferred type across the stale signature id and the current one (`15167` keeps one union arm, `15174` the other). Losing an arm widens callers' inferences to include `Unknown`, which is what makes `need-check-nil` fire downstream. Fixing it means invalidating contributions by *target* file and pulling those contributors into the reindex expansion, which changes the expansion set, so measure the benchmark as well when you do. Note it is far more sensitive than the other gates: changes to the unresolve waves, the infer-cache lifetime, or member ownership can break `realedit` while all seven others still report IDENTICAL. `mainreindex`, `exact`, `split:N` and `editmid` are bisect stages, not gates: the first three run `reindex_files_without_expansion`, which skips production's convergence passes, and `editmid` forces a real offset-shifting re-analysis of the expansion (the batch-composition confluence gap), so they are expected to diverge and only matter for localising a failure the gates already caught. Re-run it before and after, because a change can make a stage identical by *degrading* the cold build rather than by fixing the re-index.
- Performance changes require profiling or a targeted before/after benchmark. Use `GLUALS_PROFILE=1` for phase timings and `cargo run --release -p benchmark` for the large-workspace harness.
- For a sampling profile use `samply` (ETW-based on Windows, so it prompts for admin elevation on every run; the user has to approve it). Three things have to be right or you get a useless profile: build with `CARGO_PROFILE_RELEASE_DEBUG=1 cargo build --release -p benchmark` so the PDB exists, run the binary from `target/release` (samply resolves the PDB by the relative path recorded in the exe, so it only finds it from that directory), and do **not** pass `--main-thread-only` — the tools run analysis on a spawned big-stack thread, so the main thread only shows a join. A working invocation is `cd target/release && BENCH_CODEBASE=<path> samply record --save-only --unstable-presymbolicate -o <out>.json.gz ./benchmark.exe`. That writes `<out>.json.gz` plus a `<out>.json.syms.json` sidecar; the profile itself holds only addresses, so symbol names come from joining the two by `libs[].debugName` and the frame address against each module's `symbol_table` rva ranges.
- Performance is extremely important; the language server must be quick and responsive on large workspaces without loss of functionality. You are to always optimise at the root cause of performance issues. Things such as budgets, string based prefilters / guards and other similar "hacks" are unacceptable since they will regress functionality in large or complex codebases.

## Commands
Expand Down
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ emmylua_codestyle = { path = "vendor/emmylua_codestyle" }

[profile.profiling]
inherits = "release"
debug = 1
debug = 2
strip = "none"

# Lint configuration for the entire workspace
Expand Down
20 changes: 18 additions & 2 deletions crates/glua_check/src/bin/glua_check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,24 @@ use std::error::Error;
#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error + Sync + Send>> {
/// Analysis recurses over deeply nested syntax, and a Windows process main
/// thread has a far smaller stack than a spawned one.
fn main() -> Result<(), Box<dyn Error + Sync + Send>> {
std::thread::Builder::new()
.stack_size(256 * 1024 * 1024)
.spawn(|| {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("tokio runtime should build")
.block_on(run())
})
.expect("glua_check worker thread should spawn")
.join()
.expect("glua_check worker thread should not panic")
}

async fn run() -> Result<(), Box<dyn Error + Sync + Send>> {
let cmd_args = CmdArgs::parse();
run_check(cmd_args).await
}
5 changes: 5 additions & 0 deletions crates/glua_code_analysis/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ include = [
[lib]
doctest = false

[features]
# Cross-checks the type-cache reverse index against a full scan on every query.
# Off by default: it makes incremental expansion quadratic again.
verify_type_cache_refs = []

[dev-dependencies]
googletest.workspace = true

Expand Down
Loading