Part of #856 (perf epic). Likely the single highest-leverage fix — shared root cause behind the strings (117×) and array (28×/23×) gaps.
Summary
In compiled mode, locals declared with a concrete static type are emitted with CLR type object, discarding the type. Because the IL local is object, every downstream operation is forced through a dynamic $Runtime helper (string concat → $Runtime::Add(object,object), array index → an isinst dispatch ladder, etc.) plus box/unbox, even though the type is statically known.
Evidence (emitted IL)
function stringWork(n){ let s:string=""; for(...) s = s + "ab"; ... }
.locals init ( [0] object, ... ) // <-- `s` is `object`, not `string`
...
IL_001e: ldloc.0
IL_001f: ldstr "ab"
IL_0024: call object $Runtime::Add(object, object) // dynamic add: must type-check both operands every iteration
function countPrimes(n){ const isPrime:boolean[]=[]; ... }
.locals init ( [0] object, ... ) // <-- `isPrime` is `object`, not List<bool>/bool[]
Compare: factorial/fibonacci, whose locals are typed float64, hit ~Node parity. The difference is entirely the local's CLR type.
Why this matters
string local → + can lower to String.Concat(string,string) and .length to a direct get_Length (no UnwrapStringReceiver).
List<bool>/List<double> local → element get/set lowers to direct get_Item/set_Item (kills the F3 ladder + boxing).
Investigate first (do NOT just "type the local")
The crucial unknown: is object deliberate or just unfinished? Candidate reasons to rule out before changing:
- The local may be reassignable to a different runtime type, or aliased into
object-typed slots.
- The ongoing
RuntimeValue boxing-elimination migration may intentionally defer this.
null/undefined/$Undefined placeholder values may need to flow through the same slot.
Where to look: the compiled local-slot type assignment (search DeclareLocal/LocalBuilder allocation in Compilation/, the numeric-slot taint pass referenced in prior work, and TypeProvider.cs). Determine the constraint, then specialize where provably safe (single static type, no cross-type reassignment, no escape into object slots).
Acceptance
- strings + count-primes locals emit concrete CLR types where provably monomorphic.
- No regression in
dotnet test, Test262, TS conformance.
Scope / priority
High leverage, medium-high risk (touches the closure/capture/array hot paths). Gate on full suites.
Part of #856 (perf epic). Likely the single highest-leverage fix — shared root cause behind the strings (117×) and array (28×/23×) gaps.
Summary
In compiled mode, locals declared with a concrete static type are emitted with CLR type
object, discarding the type. Because the IL local isobject, every downstream operation is forced through a dynamic$Runtimehelper (string concat →$Runtime::Add(object,object), array index → anisinstdispatch ladder, etc.) plus box/unbox, even though the type is statically known.Evidence (emitted IL)
function stringWork(n){ let s:string=""; for(...) s = s + "ab"; ... }function countPrimes(n){ const isPrime:boolean[]=[]; ... }Compare:
factorial/fibonacci, whose locals are typedfloat64, hit ~Node parity. The difference is entirely the local's CLR type.Why this matters
stringlocal →+can lower toString.Concat(string,string)and.lengthto a directget_Length(noUnwrapStringReceiver).List<bool>/List<double>local → element get/set lowers to directget_Item/set_Item(kills the F3 ladder + boxing).Investigate first (do NOT just "type the local")
The crucial unknown: is
objectdeliberate or just unfinished? Candidate reasons to rule out before changing:object-typed slots.RuntimeValueboxing-elimination migration may intentionally defer this.null/undefined/$Undefinedplaceholder values may need to flow through the same slot.Where to look: the compiled local-slot type assignment (search
DeclareLocal/LocalBuilderallocation inCompilation/, the numeric-slot taint pass referenced in prior work, andTypeProvider.cs). Determine the constraint, then specialize where provably safe (single static type, no cross-type reassignment, no escape intoobjectslots).Acceptance
dotnet test, Test262, TS conformance.Scope / priority
High leverage, medium-high risk (touches the closure/capture/array hot paths). Gate on full suites.