From 106814f8a64d1e78829418b0a49d8639d8e8c3e5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:20:04 +0000 Subject: [PATCH] perf(repl): count a diagnostic's UTF-16 column without encoding the line Co-Authored-By: jason.han --- .../utf16-column-without-encoding.fixed.md | 1 + internal/frontend/repl/run.go | 38 +++++++++++++++---- internal/frontend/repl/utf16_column_test.go | 22 +++++++++++ 3 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 changes/unreleased/utf16-column-without-encoding.fixed.md create mode 100644 internal/frontend/repl/utf16_column_test.go diff --git a/changes/unreleased/utf16-column-without-encoding.fixed.md b/changes/unreleased/utf16-column-without-encoding.fixed.md new file mode 100644 index 0000000000..b9f9e3dc3c --- /dev/null +++ b/changes/unreleased/utf16-column-without-encoding.fixed.md @@ -0,0 +1 @@ +- `sysml` diagnostics count a diagnostic's UTF-16 column in place instead of encoding the line prefix, and convert a snippet's source to bytes once per snippet rather than once per diagnostic; same columns, fewer allocations per diagnostic. diff --git a/internal/frontend/repl/run.go b/internal/frontend/repl/run.go index a3b9381cb8..74f1ba6f65 100644 --- a/internal/frontend/repl/run.go +++ b/internal/frontend/repl/run.go @@ -6,7 +6,7 @@ import ( "path/filepath" "slices" "strings" - "unicode/utf16" + "unicode/utf8" "github.com/Open-MBEE/OpenSysML/internal/exec/analysis" "github.com/Open-MBEE/OpenSysML/internal/exec/runtime" @@ -295,14 +295,20 @@ func (s *Session) locate(diags []diag.Diagnostic) []Diagnostic { return nil } out := make([]Diagnostic, 0, len(diags)) - indexes := make(map[int]*source.LineIndex, len(s.snippets)) + type indexed struct { + src []byte + lines *source.LineIndex + } + indexes := make(map[int]indexed, len(s.snippets)) for _, d := range diags { sn, start := s.snippetAt(d.Span.Offset) - lines, ok := indexes[start] + idx, ok := indexes[start] if !ok { - lines = source.New(docName, []byte(sn.src)).Lines() - indexes[start] = lines + idx.src = []byte(sn.src) + idx.lines = source.New(docName, idx.src).Lines() + indexes[start] = idx } + lines := idx.lines p := lines.PosAt(d.Span.Offset - start) out = append(out, Diagnostic{ Severity: d.Severity.String(), @@ -310,10 +316,10 @@ func (s *Session) locate(diags []diag.Diagnostic) []Diagnostic { File: sn.origin, Line: p.Line, Column: p.Col, - UTF16Column: utf16Column([]byte(sn.src), d.Span.Offset-start, p.Col), + UTF16Column: utf16Column(idx.src, d.Span.Offset-start, p.Col), Pass: d.Source, Code: d.Code, - Fixes: locatedFixes(d.Fixes, []byte(sn.src), lines, start), + Fixes: locatedFixes(d.Fixes, idx.src, lines, start), }) } return out @@ -350,7 +356,23 @@ func utf16Column(content []byte, offset, col int) int { if col < 1 || lineStart < 0 || offset > len(content) { return col } - return len(utf16.Encode([]rune(string(content[lineStart:offset])))) + 1 + return utf16Units(content[lineStart:offset]) + 1 +} + +// utf16Units counts the UTF-16 code units of b without encoding it; an invalid +// byte counts as the one unit of the replacement character, as utf16.Encode does. +func utf16Units(b []byte) int { + n := 0 + for len(b) > 0 { + r, size := utf8.DecodeRune(b) + b = b[size:] + if r >= 0x10000 { + n += 2 + } else { + n++ + } + } + return n } // Suppress keeps the diagnostics the predicate accepts out of everything the diff --git a/internal/frontend/repl/utf16_column_test.go b/internal/frontend/repl/utf16_column_test.go new file mode 100644 index 0000000000..2b8834421c --- /dev/null +++ b/internal/frontend/repl/utf16_column_test.go @@ -0,0 +1,22 @@ +package repl + +import ( + "testing" + "unicode/utf16" +) + +// A diagnostic's UTF-16 column is counted in place: the same count utf16.Encode +// gives for ASCII, multi-byte, astral and invalid bytes, without allocating. +func TestUTF16ColumnCountsWithoutEncoding(t *testing.T) { + for _, line := range []string{"attribute x;", "part ü : Ü;", "part 𝒳 : 𝒴;", "part \xff : A;"} { + content := []byte(line + "\n") + offset := len(line) - 1 + want := len(utf16.Encode([]rune(line[:offset]))) + 1 + if got := utf16Column(content, offset, offset+1); got != want { + t.Errorf("utf16Column(%q) = %d, want %d", line, got, want) + } + if got := testing.AllocsPerRun(20, func() { utf16Column(content, offset, offset+1) }); got != 0 { + t.Errorf("utf16Column(%q) allocates %.0f times, want 0", line, got) + } + } +}