Scala 3.4.0 rewrite options question

Hi, I’m Thanh from the lichess.org team. We’re trying to upgrade our main server (Lichess’s Lila) to Scala 3.4.0. And we have too many deprecation warnings. Even though the compiler offers to rewrite most of them for us, it doesn’t always do a good job. There are too many places where the compiler produced faulty code or the rewritten code is not in the way we like it (adding back stick for infix functions for example). To be fair, Lila is a big code base, and using many “exotic” style/pattern of coding. But we hope to make it more idiomatic.

My question is can we control which rewrite rules that compiler uses when we have (“-rewrite”, “-source:future-migration”)? I’d like to use individual rewrite rule to test which one works. Because as I observe some of the rewrite is good like replace with with &, remove trailing underscore _, but not rewrite with fewer braces (we would want that if it’s not broken) or infix operator (because the back stick is ugly :sweat_smile:).

Here is the “draft” pr for the upgrade: Scala 3.4.0 by lenguyenthanh · Pull Request #14851 · lichess-org/lila · GitHub.

2 Likes

In order to make the upgrade process smoother, we used scalafmt’s AvoidInfix in this pr but this was not enough. We need to manually fix the code as scalafmt crashes on many places. It required us to fix some code manually before we can run it again. But even when it ran successfully for the whole projects there are still many deprecated infix function calls left.

For a large project I think it’s best to stay with the LTS release, which is 3.3.3 at the moment. LTS is stable for longer, Next moves faster and changes a lot of things.

Do you urgently need some new 3.4 features? It might take a bit longer for the rewriting to get better. There will be more changes to the syntax on Next soon

This is annoying a lot of people, but I’m afraid we’ll all have to live with it…

3 Likes

No, We don’t. It’s just our tendency to live on the edge, I remember that we ran 3.3.0-RC* in production for months before its final release.

I believe the recommendation is stay on LTS line if you’re libraries and move on if you’re applications and you don’t mind fast pace of changes. We definitely don’t afraid the changing pace, We want to move on as soon as possible to use the most advance compiler and to avoid any dramatically change later on.

I would rather discourage this combination, under -source:future or -source:future-migration you would not get warnings about changed implicit resolutions, but just a plain error about missing implicit instead. See 9. The following change is currently enabled in -source future: on Changes in Implicit Resolution
Compilation errors then prevent applying -rewrite changes.
If there isn’t anything required from -source:future in your codebase then downgrade to -source:3.4-migration during the upgrade

1 Like

Thanks, It makes sense! I applied your recommendation and also disable -indent option, the rewrite works fine.

Now if I could disable infix rewrite, I’ll be very happy.

One more note: Lila is tested weekly in the Open Community Build, you might be affected by the implicit resolution changes, as this warning suggests Weekly build A #40 · VirtusLab/community-build3@d1c72d2 · GitHub otherwise, there should be no other issues with upgrading to 3.4.0 or even latest nightly (We force -source:3.X-migration whenever possible, as these stuff is typically non critical, and there less then dozen of projects that actually require -source:future right now)

1 Like

Thanks, I saw a lot of implicit resolution warnings. I guess I have to fix them manually, that’s fine tho.

For the infix rewrites, I believe there are better options than ScalaFmt:

  • If the methods in question are defined in your project, and you prefer the infix notation, you might want to declare them with an infix modifier.
  • Use the compiler’s -rewrite option, that one has no issues I know of. Generally, migration to new versions is driven by the compiler team, so it might be that other tools that we don’t control lag behind.
1 Like

There are some functions that we definitely want to add infix modifier for them. But most of other functions We want to rewrite to keep.

Yes the -rewrite option works but back sticks are not the style We like. I believe it the same for most of people.

3 Likes

Understood. So you want to rewrite something like xs map f to xs.map(f) ? That makes sense to me.

Yes, it would be the awesome.

I though scalafmt could do it but it didn’t It fixed a lot but not all.

The new infix rules have been the biggest pain point by far in my codebase, in part because there’s no way to allow clean infix Java (or Scala libraries which you don’t personally control), and sometimes infix Java makes code far easier to read (if it’s math-like, for instance). I just run with -Wconf:msg=is not declared infix:s (dots instead of spaces if I’m using scala-cli header comments).

I also make use of whitespace to highlight parallel constructions, which rewrite messes up. For instance (real examples are more complex):

def later(t: Double):   Instant = myInstant  +   t
def later(t: Duration): Instant = myInstant plus t

Backticks mess this up, and if it’s not at the end of the line. parens also mess this up. I’m really not motivated to make a bunch of manual fixes so that my code is harder to read.

If the lichess codebase already has a lot of infix, and infix makes understanding clearer, as is the case with my code, I’d suggest considering a similar approach–just suppress the warnings, and if you feel like it, gradually evolve towards less or no infix.

If you need the automatic rewrite to fix other things, perhaps generating a diff of the full rewrite, then selecting only those diffs that didn’t just add backticks, and applying only the reduced diff, is the way to go.

1 Like

Just to check that it wasn’t hard, I auto-rewrote everything in my codebase in a new branch, did a git diff > my.patch, used the repl to load and select patches that weren’t just adding backticks, wrote it out again, and did git apply new.patch in another new branch. No surprises. So that approach seems straightforward enough. Not a full solution, but it’s a “rewrite everything but infix” that works.

I’ll share the code if anyone would like.

1 Like

Sound like a great work around for now, pls share your script!

So for the sake of it, We merged Scala 3.4.0 PR yesterday, and it’ll be in production soon.

We used -rewrite, -source:3.4-migration and some manually fixed for implicit resolution & eta expansion warning. Also converted a b c to a.b(c).

Now we’re ready for the next Scala version.

Thank you all for your comments and suggestions!

3 Likes

Just want to say your Lichess project is awesome. :rocket: (Sorry for violating the forum rules a little, but just pressing “like” does not cover it.) It’s amazing!

2 Likes

Thanks for your kind words, I’ll share them to the team, they will definitely love it!

2 Likes

Oh dear, sorry! I stopped paying attention to the thread. Anyway, it sounds like the rewrite was successful enough!

Just for reference, here’s a runnable command-line patch editor (if you want it to not ignore whitespace, edit out the trims in the collect statements). Run with scala-cli CutPatch.scala -- in.patch out.patch.

//> using scala 3.4.0
//> using dep com.github.ichoran::kse3-eio:0.2.11
//> using mainClass kerrr.cutpatch.Main
//> using options -Wconf:msg=is.not.declared.infix:s

package kerrr.cutpatch

import kse.basics.{given, _}
import kse.basics.intervals.{given, _}
import kse.flow.{given, _}
import kse.eio.{given, _}

object Main {
  def differsByBackticks(has: String, hasnt: String): Boolean =
    var i = 0
    var j = 0
    var result = true
    while i < has.length && j < hasnt.length && result do
      if has(i) == hasnt(j) then
        i += 1
        j += 1
      else if has(i) == '`' then
        i += 1
      else
        result = false
    result && i == has.length && j == hasnt.length

  def justAddsBackticks(lines: Array[String]): Boolean =
    val pos = lines.collect{ case s if s startsWith "+ " => s.drop(2).trim }
    val neg = lines.collect{ case s if s startsWith "- " => s.drop(2).trim }
    (pos zip neg).forall{ case (p, n) => differsByBackticks(p, n) } && pos.length == neg.length

  def run(args: Array[String]): Unit Or Err = Err.Or:
    if args.length != 2 then Err.break("Specify patch file as input and target file as output")
    val source = args(0).path
    val target = args(1).path
    if target.exists then Err.break(s"For safety, please use a target that doesn't already exist")
    val content = source.slurp.?
    val diffs = content.where(_ startsWith "diff")
    val atats = content.where(_ startsWith "@@")
    val pts = (diffs ++ atats ++ Array(content.length)).sorted
    val intervals = pts.selectOp(1 to End){ (p, i) => Iv(pts(i-1), p) }.
      filter(iv =>
        iv.length > 0 && ((content(iv.i0) startsWith "diff") || !justAddsBackticks(content.select(iv)))
      )
    val newContent = intervals.breakable.copyOp{ (iv, i) =>
      shortcut.skipIf:
        content(iv.i0).startsWith("diff") && (i+1 >= intervals.length || content(intervals(i+1).i0).startsWith("diff"))
      content.select(iv)
    }
    println(s"Writing ${newContent.count(_.head startsWith "@@")} of ${atats.length} edits\nfrom ${source.real}\n  to ${target.real}")
    newContent.iterator.flatten.createAt(target).?

  def main(args: Array[String]): Unit =
    run(args).foreachAlt{ e =>
      println("Error:")
      println(e)
      sys.exit(1)
    }
}

One could make it dependency-free with a few tweaks to array operations and such.

1 Like

Thanks, and yes, the migration was successful! Btw, I checked your kse project, It looks really nice!

1 Like