Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions changes/unreleased/utf16-column-without-encoding.fixed.md
Original file line number Diff line number Diff line change
@@ -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.
38 changes: 30 additions & 8 deletions internal/frontend/repl/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -295,25 +295,31 @@ 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(),
Message: d.Message,
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
Expand Down Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions internal/frontend/repl/utf16_column_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Loading