Skip to content

Use String as the SourceFile content type - #26846

Draft
SolalPirelli wants to merge 11 commits into
scala:mainfrom
dotty-staging:solal/avoid-copies-1
Draft

Use String as the SourceFile content type#26846
SolalPirelli wants to merge 11 commits into
scala:mainfrom
dotty-staging:solal/avoid-copies-1

Conversation

@SolalPirelli

@SolalPirelli SolalPirelli commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Depends on #26826

Hopefully we can avoid copies + benefit from the JVM's compact string optimization. Also methods like indexOf no longer box.

0-5% speedup, see the last benchmark results in this PR.

Have you relied on LLM-based tools in this contribution?

No

How was the solution tested?

Covered by existing tests (this is a refactoring)

* the source is read from Tasty. */
def content(): Array[Char] =
if file == null then Array.emptyCharArray
override def textContent(): String =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

text is a shorter name, in case you are not content with this longer name.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a grand unified String theory that explains why this is good. It's a little complex though.

@sjrd

sjrd commented Aug 19, 2026

Copy link
Copy Markdown
Member

Should we be bolder and use an Array[Byte] with UTF-8 code units?

The string compaction works if the entire string is latin1. A single non-latin1 character in a source file (perhaps in a license header that's repeated in all files) destroys it.

@SolalPirelli

Copy link
Copy Markdown
Contributor Author

We could, perhaps with some AnyVal derivative that hides the exact algorithms, but wouldn't that need a lot of changes in the compiler so anything that is used alongside file contents is also an Array[Byte]? We don't want to have to convert strings to bytes everywhere so we can, e.g., search for them in a source file.

@sjrd

sjrd commented Aug 19, 2026

Copy link
Copy Markdown
Member

Yeah, probably. It might be worth thinking about a larger changeset for the future.

@dottybot

This comment was marked as outdated.

@dottybot

This comment was marked as outdated.

for vars <- ctx.property(ShownVars) do vars += this
if !ctx.settings.YccDebug.value then ""
else if isConst then ids ++ "(solved)"
else if isConst then ids + "(solved)"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmarking and profiling this PR turned into a general "let me look at what String-related stuff pops up" exercise.

We can't inline StringOps because the compiler might run with a different standard library.

Also, IMHO, it's weird to use ++ on strings when we're not explicitly looking at them as sequences of characters.

idx != -1
do
lineCount += 1
buf.writeNat(lineCount)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one comes from Haoyi's pickling-related perf PR. (I did not make the other changes to this function from that PR, namely inlining indexOf, because now that the content is a string, indexOf is a Java function, not a Scala stdlib extension like it is on arrays.)

def sourceChangeContext(addr: Addr = currentAddr)(using Context): Context = {
val path = sourcePathAt(addr)
if (path.nonEmpty) {
if (!path.isEmpty) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nonEmpty is a Scala extension

/** Do two target names match? An empty target name matches any other name. */
def matchesTargetName(other: Name) =
name == other || name.isEmpty || other.isEmpty
name.isEmpty || other.isEmpty || name == other

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor but alongside the other targetName change, noticed while profiling, this order seems to make more sense to avoid calling into string equality if we can

try new String(file.toByteArray, codec.charSet).toCharArray
catch case _: FileSystemException => Array.empty[Char]
try new String(file.toByteArray, codec.charSet)
catch case _: FileSystemException => ""

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will disappear in #26512 to call into the filesystem directly to read a string, hopefully avoiding this copy

def visitInternalName(internalName: String, offset: Int, length: Int): Unit = if (internalName != null && containsChar(internalName, offset, length, '$')) {
for (c <- getClassIfNested(internalName.substring(offset, length)))
def visitInternalName(internalName: String, beginIndex: Int, endIndex: Int): Unit = if (internalName != null && containsChar(internalName, beginIndex, endIndex, '$')) {
for (c <- getClassIfNested(internalName.substring(beginIndex, endIndex)))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

substring takes beginIndex, endIndex so the parameter names to this function were wrong, though it was called with the right values; and that means containsChar was wrong, though this seems to not have caused problems.

val start = i + 1 // skip the L
var seenDollar = false
while ({val ch = desc.charAt(i); seenDollar ||= (ch == '$'); ch != ';'}) i += 1
if (seenDollar)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the callee already checks for this so this check isn't needed

@dottybot

Copy link
Copy Markdown
Member

Benchmarks started. Workflow run.

@dottybot

Copy link
Copy Markdown
Member

Benchmarks completed. Overview.

@dottybot

Copy link
Copy Markdown
Member

Benchmarks started. Workflow run.

@dottybot

Copy link
Copy Markdown
Member

Benchmarks completed. Overview.

@SolalPirelli
SolalPirelli marked this pull request as ready for review August 26, 2026 06:54
@SolalPirelli
SolalPirelli marked this pull request as draft August 29, 2026 07:47
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.

4 participants