Skip to content

Stop the local-player barrier from refusing inlining on control taint - #1284

Merged
Frotty merged 3 commits into
big-lua-opt-updatefrom
fix/inliner-local-player-barrier
Sep 3, 2026
Merged

Stop the local-player barrier from refusing inlining on control taint#1284
Frotty merged 3 commits into
big-lua-opt-updatefrom
fix/inliner-local-player-barrier

Conversation

@Frotty

@Frotty Frotty commented Sep 3, 2026

Copy link
Copy Markdown
Member

What

ImInliner refused to inline any function whose return fact LocalPlayerContextAnalyzer had marked. That fact fires for every function reachable from a client-local branch, transitively over the call graph. In a map that links the standard library this is most of the call graph.

Measured with -Dwurst.inliner.log=true on a stdlib-linked probe (release flags):

before after
call sites refused by local_player_context_barrier 577 of 1678 10
distinct functions refused 105 5

Before: max, min, headSlot, groupSlot, cellAt, cellCoordX, unit_getX, __wurst_intDiv and other pure leaves stayed as calls inside the spatial index's query loops. After: the five remaining refusals are init_Player, PingMinimapForPlayer, GetPlayableMapRect, GetCurrentCameraBoundsMapRectBJ and InitMapRects, each of which reaches a client-local native.

Why control taint is the wrong question for inlining

Substituting a callee body at a call site runs it under exactly the control the call already had, so nothing crosses a client-local boundary. The passes that do move code (BranchMerger, TempMerger, LocalMerger, ConstantAndCopyPropagation) run after inlining and re-analyse the inlined program, where that control is explicit. They keep reading the full graph and are unchanged.

What must remain a call is a function whose result is client-local by its own body: it calls a client-local native directly, or returns a value derived from one.

How

LocalPlayerContextAnalyzer keeps a second dependency map, dataDependents, that receives every ordinary edge but not:

  • control edges (addEnclosingControlDependency), for the reason above;
  • call-site argument-to-parameter edges (addCallArgumentDependency). The analysis is context-insensitive: a parameter fact merges the arguments of every call site, so one max(...) call anywhere with a client-local argument would taint max and everything computed from its result.

propagateDataFacts() walks that graph from the same sources and publishes only RETURN facts into localPlayerDataDependentReturns. functionInliningIsLocalPlayerSensitive is now isClientLocalValueSource || functionsDirectlyUsingLocalPlayer || localPlayerDataDependentReturns.

Two simpler rules were tried and rejected, recorded in LUA_HOT_PATH_SPEC.md: USE facts only (breaks testInlineAnnotation, because with the stdlib linked print reaches GetLocalPlayer), and data-only returns with the ordinary parameter edges (still barriers max, min, headSlot).

Tests

  • New OptimizerTests.pureHelperReachableFromLocalPlayerBranchIsStillInlined (Jass _inl.j): a pure helper called once under a GetLocalPlayer branch inlines at both sites; a wrapper that calls GetLocalPlayer stays a call.
  • New LuaBackendAuditTests.pureHelpersReachableFromLocalPlayerBranchInlineIntoLuaHotLoops (release Lua): index arithmetic inlines into a query loop; the GetLocalPlayer wrapper stays a call.
  • Both red before the change, green after.
  • Unchanged and passing: functionUsingGetLocalPlayerMustNotBeInlined, testInlineAnnotation, all localPlayer* tests.
  • Focused suites (OptimizerTests, LuaBackendAuditTests, LuaTranslationTests, InterpreterTests, LuaTypecastingTests): 327 tests, 0 failures.
  • Full suite: 1903 tests, 1 failure in SmallCheckViaJUnitCoreTestNG, which passes when run alone. The fuzz programs all write the same fixed test-output/CompilerFuzzTestsSC_assertCompilesForBothBackends_*.j filename, and the reported error was a truncated function keyword: a torn file from concurrent writers, pre-existing and unrelated.

Also in this PR

LUA_HOT_PATH_SPEC.md and an AGENTS.md §7 "Lua performance policy" section, committed separately. The spec covers this change (Task 2) and the remaining emission costs found in the same investigation: array-read coercion wrappers, vararg table.pack, div/mod helper chains, and the inliner rating formula refusing tiny popular helpers.

Four measured costs in the emitted Lua for the standard library's spatial
index: coercion wrappers on every typed array read, a local-player inlining
barrier that refuses most of the call graph, vararg calls that allocate a
table per call, and integer div/mod as helper chains. The spec gives each a
root cause, the exact change, the assertions that encode the old behaviour,
and acceptance criteria. AGENTS.md states the policy the spec implements:
emitted constructs are consumed by Wurst code, so nothing defends against
foreign writes, and Lua-native mechanisms replace emulation.
The analysis marks a function's return fact whenever the function is
reachable from a client-local branch, transitively over the call graph, and
the inliner treated that fact as a reason not to inline. In a program which
links the standard library that is most of the call graph: on the spatial
index probe 577 of 1678 call sites were refused, among them max, min and
the pure index arithmetic in the query loops.

Control taint is the wrong question for inlining. Substituting a body at a
call site runs it under exactly the control the call already had, so nothing
crosses a client-local boundary; the passes which do move code run after
inlining and re-analyse the inlined program. What must stay a call is a
function whose result is client-local by its own body: it calls a
client-local native directly, or returns a value derived from one.

That second half needs a data graph without two kinds of edge. Control edges
are the ones above. Call-site argument-to-parameter edges have to go too:
the analysis is context-insensitive, so one max(...) call anywhere with a
client-local argument would taint max and everything computed from its
result. Both edge kinds stay in the full graph every other consumer reads.

Measured on the same probe: 10 refusals on 5 functions, each of which
reaches a client-local native.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-03T14:25:59.495369Z 61eaca0 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Frotty
Frotty merged commit 48b3084 into big-lua-opt-update Sep 3, 2026
3 checks passed
@Frotty
Frotty deleted the fix/inliner-local-player-barrier branch September 3, 2026 18:57
Frotty added a commit that referenced this pull request Sep 4, 2026
* Stop the local-player barrier from refusing inlining on control taint (#1284)

* Optimize Lua array reads and div/mod emission (#1288)

* Optimize Lua array reads and div-mod intrinsics

Remove typed primitive array normalization, invert legacy assertions to require raw reads, and keep erased-generic normalization intact. Emit raw div/mod primitives directly as Lua operators without helper definitions.

* Track Lua numeric intrinsics by IM identity

* Give vararg calls a fixed-arity copy on Lua (#1286)

* Inline small Lua helpers regardless of popularity (#1289)

* Deduplicate Lua callback adapters (#1290)

* Deduplicate Lua callback adapters

* Preserve renamed Lua callback targets

* Bound Lua inlining by register pressure (#1291)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant