From 77418dda7c14ecea2c2b417e50289fe52d434df4 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Fri, 9 Oct 2026 22:12:53 +0200 Subject: [PATCH] Collapse single-element edit loops into one splice Several fixers removed or inserted a contiguous run of tokens one element at a time via RemoveAt/InsertAt in a loop. Each call shifted the whole tail once, so a k-token span cost k tail moves, making those rules O(n^2) on large files where memmove dominated the profile. Replace the per-element loops with a single ReplaceRange or the new InsertSliceAt, so the tail shifts once per span. The edited token set is identical, so output is byte-for-byte unchanged. BenchmarkFixPipeline: 1class ~99.8us -> ~90.7us (-9%) 8classes ~636us -> ~600us (-6%) 64classes ~6.61ms -> ~6.12ms (-7%) 256classes ~49.3ms -> ~44.5ms (-10%) --- blink/internal/fixer/rules/array_notation.go | 4 +--- .../internal/fixer/rules/control_structure_braces.go | 4 +--- blink/internal/fixer/rules/gen2_ops.go | 4 +--- blink/internal/fixer/rules/gen3_types.go | 4 +--- blink/internal/fixer/rules/gen5_lang.go | 4 +--- blink/internal/fixer/rules/gen_imports.go | 4 +--- blink/internal/fixer/rules/gen_ops_inc.go | 12 +++--------- blink/internal/fixer/rules/imports_unused.go | 4 +--- blink/internal/fixer/rules/modifier_keywords.go | 4 +--- blink/internal/fixer/rules/no_alternative_syntax.go | 5 +---- blink/internal/fixer/rules/operator_linebreak.go | 7 ++----- blink/internal/tokens/stream.go | 6 ++++++ 12 files changed, 20 insertions(+), 42 deletions(-) diff --git a/blink/internal/fixer/rules/array_notation.go b/blink/internal/fixer/rules/array_notation.go index 8b1ecd11be..d855125b16 100644 --- a/blink/internal/fixer/rules/array_notation.go +++ b/blink/internal/fixer/rules/array_notation.go @@ -38,9 +38,7 @@ func convertLongArray(s *tokens.Stream, name string) bool { } s.SetValue(closeIdx, "]") s.SetValue(j, "[") - for k := j - 1; k >= i; k-- { - s.RemoveAt(k) - } + s.ReplaceRange(i, j-1, nil) changed = true } return changed diff --git a/blink/internal/fixer/rules/control_structure_braces.go b/blink/internal/fixer/rules/control_structure_braces.go index 54bd997f68..b5bd9edce3 100644 --- a/blink/internal/fixer/rules/control_structure_braces.go +++ b/blink/internal/fixer/rules/control_structure_braces.go @@ -139,9 +139,7 @@ func csbIsAltSyntax(s *tokens.Stream, controlIndex int) bool { // csbInsert inserts toks at pos, preserving their order. func csbInsert(s *tokens.Stream, pos int, toks []token.Token) { - for i, t := range toks { - s.InsertAt(pos+i, t) - } + s.InsertSliceAt(pos, toks) } // csbFindParenthesisEnd returns the ")" index of the control's condition, or the diff --git a/blink/internal/fixer/rules/gen2_ops.go b/blink/internal/fixer/rules/gen2_ops.go index a31173e692..8b58d384a2 100644 --- a/blink/internal/fixer/rules/gen2_ops.go +++ b/blink/internal/fixer/rules/gen2_ops.go @@ -76,9 +76,7 @@ func (AssignNullCoalescingToCoalesceEqual) Fix(s *tokens.Stream) bool { // `= $a ??` becomes `??=`: retag the `=` and drop the duplicate operand // and operator. s.SetValue(i, "??=") - for k := opIdx; k >= i+1; k-- { - s.RemoveAt(k) - } + s.ReplaceRange(i+1, opIdx, nil) changed = true } return changed diff --git a/blink/internal/fixer/rules/gen3_types.go b/blink/internal/fixer/rules/gen3_types.go index f7692a2f9c..ac41ecbd55 100644 --- a/blink/internal/fixer/rules/gen3_types.go +++ b/blink/internal/fixer/rules/gen3_types.go @@ -272,9 +272,7 @@ func nullableTypeRemove(s *tokens.Stream, start, end int, hasNullable bool) bool } else { return false } - for j := hi; j >= lo; j-- { - s.RemoveAt(j) - } + s.ReplaceRange(lo, hi, nil) return true } diff --git a/blink/internal/fixer/rules/gen5_lang.go b/blink/internal/fixer/rules/gen5_lang.go index 23e4c25a39..a4fbab8ce4 100644 --- a/blink/internal/fixer/rules/gen5_lang.go +++ b/blink/internal/fixer/rules/gen5_lang.go @@ -142,9 +142,7 @@ func (EmptyLoopBody) Fix(s *tokens.Stream) bool { // Replace "{" with ";" and drop the "}" plus the whitespace-only interior, // so "{ }" collapses cleanly to ";". s.SetValue(braceOpen, ";") - for k := braceClose; k > braceOpen; k-- { - s.RemoveAt(k) - } + s.ReplaceRange(braceOpen+1, braceClose, nil) changed = true } return changed diff --git a/blink/internal/fixer/rules/gen_imports.go b/blink/internal/fixer/rules/gen_imports.go index 7e5b2c417d..6f30efc361 100644 --- a/blink/internal/fixer/rules/gen_imports.go +++ b/blink/internal/fixer/rules/gen_imports.go @@ -65,9 +65,7 @@ func removeUnneededAliases(s *tokens.Stream, from, semi int) bool { if seg > from && alias < end && s.At(seg).Kind == token.Ident && s.At(alias).Kind == token.Ident && s.At(seg).Value == s.At(alias).Value { - for r := alias; r >= seg+1; r-- { - s.RemoveAt(r) - } + s.ReplaceRange(seg+1, alias, nil) end -= alias - seg k = seg + 1 changed = true diff --git a/blink/internal/fixer/rules/gen_ops_inc.go b/blink/internal/fixer/rules/gen_ops_inc.go index bd6da8a7bf..c65d47593a 100644 --- a/blink/internal/fixer/rules/gen_ops_inc.go +++ b/blink/internal/fixer/rules/gen_ops_inc.go @@ -255,9 +255,7 @@ func (StandardizeIncrement) Fix(s *tokens.Stream) bool { // Drop ` 1` (operator..number), then the inline gap before it, then // prepend the increment operator at the chain root. - for k := numIdx; k >= i; k-- { - s.RemoveAt(k) - } + s.ReplaceRange(i, numIdx, nil) if lv+1 < s.Len() && s.At(lv+1).Kind == token.Whitespace && !hasNewline(s.At(lv+1).Value) { s.RemoveAt(lv + 1) } @@ -322,9 +320,7 @@ func (IncrementStyle) Fix(s *tokens.Stream) bool { } // Move the operator in front of the variable, dropping the inline gap. - for k := i; k >= lv+1; k-- { - s.RemoveAt(k) - } + s.ReplaceRange(lv+1, i, nil) s.InsertAt(lv, token.Token{Kind: token.Punct, Value: t.Value}) changed = true i = lv @@ -420,9 +416,7 @@ func (LongToShorthandOperator) Fix(s *tokens.Stream) bool { // `= $a ` becomes `=`: retag the `=` and drop the duplicate // operand and operator. s.SetValue(i, s.At(opIdx).Value+"=") - for k := opIdx; k >= i+1; k-- { - s.RemoveAt(k) - } + s.ReplaceRange(i+1, opIdx, nil) changed = true } return changed diff --git a/blink/internal/fixer/rules/imports_unused.go b/blink/internal/fixer/rules/imports_unused.go index f3af05b907..e23d620386 100644 --- a/blink/internal/fixer/rules/imports_unused.go +++ b/blink/internal/fixer/rules/imports_unused.go @@ -360,9 +360,7 @@ func matchCloseBrace(s *tokens.Stream, open int) int { } func removeImport(s *tokens.Stream, im importInfo) { - for r := im.semi; r >= im.start; r-- { - s.RemoveAt(r) - } + s.ReplaceRange(im.start, im.semi, nil) // drop just the statement's own line break, keeping any blank line if im.start < s.Len() && s.At(im.start).Kind == token.Whitespace { if v := s.At(im.start).Value; strings.HasPrefix(v, "\n") { diff --git a/blink/internal/fixer/rules/modifier_keywords.go b/blink/internal/fixer/rules/modifier_keywords.go index 6eeb61abaf..01dfb266a6 100644 --- a/blink/internal/fixer/rules/modifier_keywords.go +++ b/blink/internal/fixer/rules/modifier_keywords.go @@ -133,9 +133,7 @@ func orderModifierKeywords(s *tokens.Stream, m int) bool { repl = append(repl, token.Token{Kind: token.Whitespace, Value: " "}) } if after == m { - for _, tk := range slices.Backward(repl) { - s.InsertAt(m, tk) - } + s.InsertSliceAt(m, repl) return true } s.ReplaceRange(m, after-1, repl) diff --git a/blink/internal/fixer/rules/no_alternative_syntax.go b/blink/internal/fixer/rules/no_alternative_syntax.go index 30ae26551f..ed92c7b984 100644 --- a/blink/internal/fixer/rules/no_alternative_syntax.go +++ b/blink/internal/fixer/rules/no_alternative_syntax.go @@ -162,10 +162,7 @@ func nasAddBraces(s *tokens.Stream, keyword token.Token, index, colonIndex int) // nasReplace replaces the single token at pos with items, preserving order. func nasReplace(s *tokens.Stream, pos int, items []token.Token) { - s.RemoveAt(pos) - for i, t := range items { - s.InsertAt(pos+i, t) - } + s.ReplaceRange(pos, pos, items) } // nasNextParen returns the index of the first "(" after index, or -1. diff --git a/blink/internal/fixer/rules/operator_linebreak.go b/blink/internal/fixer/rules/operator_linebreak.go index 6f2c72c147..71721ac00e 100644 --- a/blink/internal/fixer/rules/operator_linebreak.go +++ b/blink/internal/fixer/rules/operator_linebreak.go @@ -149,11 +149,8 @@ func moveOperatorToLineStart(s *tokens.Stream, opIndices []int, nextM int) { s.SetValue(idx, "") } - at := nextM - for _, c := range clones { - s.InsertAt(at, c) - at++ - } + s.InsertSliceAt(nextM, clones) + at := nextM + len(clones) if hadSpaceBefore { s.InsertAt(at, token.Token{Kind: token.Whitespace, Value: " "}) } diff --git a/blink/internal/tokens/stream.go b/blink/internal/tokens/stream.go index ed611c4d41..a428c062e6 100644 --- a/blink/internal/tokens/stream.go +++ b/blink/internal/tokens/stream.go @@ -41,6 +41,12 @@ func (s *Stream) InsertAt(i int, t token.Token) { s.toks = slices.Insert(s.toks, i, t) } +// InsertSliceAt inserts ts at index i in one splice, so the tail shifts once +// instead of once per element. +func (s *Stream) InsertSliceAt(i int, ts []token.Token) { + s.toks = slices.Insert(s.toks, i, ts...) +} + // MatchForward returns the index of the delimiter matching the opener at i // ("(", "{" or "["), or -1 if there is none. Strings and comments are single // tokens, so scanning punctuation is safe.