Save your Scala apps from the LazyValpocalypse, with Łukasz Biały!
By Jakub Kozłowski
Summary
Topics Covered
- JDK26 Breaks Scala 3 Lazy Vals
- Bytecode Patching Fixes Legacy Lazy Vals
- VarHandles Required for Companion Pairs
- AI Engineers Entire Patching Tool
Full Transcript
Hello, welcome to another episode of Scala with friends. Today with me is Wukash Bawe, Scala developer advocate at Vertus Lab and we are going to talk about something terrible that's going to
happen to all your applications unless you do this one weird weird thing uh which we're going to learn about. So
Wukash, what's the problem that you're trying to save us from? As you know, Scala had a lot of difficulties with maintenance of uh compatibility and on
one of the fronts we have done a very very good job and that is that Scala has always supported JDK 1.8 that comes with
its own problem. The compatibility with JDK8 meant that we had the ability to use any version of Scala on the older
JDKs. So if you had a service running on
JDKs. So if you had a service running on a JDK8, you didn't have any necessity to bump the version of the JVM. You only
had to bump the versions of your of the Scala in your project. You had to bump the dependencies, but you didn't have to change the runtime in any way. Now most
of us are probably using newer JDKs, but this was one of the cornerstones of the compatibility story. And uh well I think it was a very good decision in the
end because it meant that um our already troubled story with migration between Scala 2 and Scala 3 didn't have this one additional bump. So what is this one
additional bump. So what is this one small problem that is related to the supported maintenance of of JDK 1.8?
Well JDK 1.8 late was the point where um we still didn't have any alternatives to um unsafe memory access unsafe heap
memory access I would like to add because this is exactly what sun.m meque
unsafe class does for you it allows you to mutate state on the heap of the GVM and well as the name implies it's a wildly unsafe operation but it allows
you to get a lot a lot of performance and basic specifically all of the advanced and and mature frameworks, libraries, solutions like Netty, like
Spring, they all use safe. It's it's a common thing to use it even though it was always mentioned with a grumble that you shouldn't use that it's in the sun misk namespace because
>> for a reason >> for a reason exactly and and it was a private thing that belonged to the JVM itself but well
performance is king so knowing the risks we all used unsafe class >> yeah like maybe it's not something that you would use in your own application because you are >> still working on a baseline that's
supposed to be quite performant and optimized.
>> Yes, exactly.
>> But these these foundations, they have to sometimes resort to some dirty tricks such as using internal unsafe uh APIs.
>> Yes. Exactly. So libraries, application solutions like Spring or compute solutions like Spark, they all have to do that, but you don't really have to touch that on your own. Usually it's
something that is very deeply into the call stack of of things that you use.
Now, Scala as a language didn't have that problem in the 2.x branch. There
was no bite code that was emitted by the compiler that would touch unsafe in anyway. But there was the idea that um
anyway. But there was the idea that um lazy valves are kind of slow because the version that was built into Scola.x was
using um synchronized blocks. So that
meant that they weren't really really fast, which was mostly thought about as not being a big deal. You usually don't
use lazy valves in in performance sensitive code. But well, it was raised
sensitive code. But well, it was raised by um functional programmers that oh it's not very fast. So maybe we should have a new implementation. And that's
how we arrived at SIP 20. So, SIP 20 said that we should rethink how we do lazy valve initialization. And the way
to do this was to build a completely different mechanism that wouldn't use synchronization block monitors that are built into JVM which are slow and just
use a state machine that would use comparent switch semantics. And well the best way to do that is to use unsafe because you can modify the offset in
memory directly. At the time um when
memory directly. At the time um when Scala 3 was being built the affforementioned cornerstone of compatibility with JDK.8 was
unquestionable. It wasn't even on the table of discussion whether we would drop the compatibility with JDK.8 Right?
And that means that we didn't have access to um new alternative to this. So
the new alternative came in JDK9. So
Java 9 introduced var handles and varandles are a safe solution to do that. It's a set of internal APIs of the
that. It's a set of internal APIs of the JVM that allow you to do this kind of memory modification in a way that is um more let's say idiomatic to the language
and and doesn't basically just you know go through raw memory. So it's it's much harder to introduce bugs and mistakes.
>> So it's a bit of an easier API not an internal API anymore. Yes, it's a public facing API that exposes some internal details but still in a principled or at
least relatively safe manner.
>> Yes, that's exactly the case. So given
that it arrived in Java 9, there was no way to do this to use this API without breaking the compatibility with JDK8. So
from the beginning of Scola free we had this new implementation of laser valves that used some mis unsafe
um and that means that we basically uh had to emit bite code that it didn't necessarily mention unsafe directly but
what you got in bite code was um a set of memory addresses or just a memory address that you you don't you can't really do anything um different than
just hip memory modification with that.
This wasn't really a problem if you think about it because unsafe is so uh popular and so widely used that there
was a lot of planning required for um JVM developers to even start thinking about deprecating this. Now this was a
long story. So we started with JDK 1.8
long story. So we started with JDK 1.8 date and we are now um in year 2025 and the current version of JDK is JDK 25
>> and it's also an LTS release for longer term stability because the last one was uh JDK 21 which didn't even have any kind of problem with this.
>> Yes. So we are currently on the fourth LTS release of Java platform >> after JDK >> after one 18 and in the time between
those releases Java platform team was able to actually start the process of deprecation of unsafe and this is a big deal for us unfortunately >> it's not good news for us
>> it's not good news for us especially with SIP 20 because um we have to somehow deal with the fact that the new
JDK26 that is coming out in March of the next year of 2026 is going to start throwing exceptions on
all unsafe accesses well to the class unsafe. So how does this situation
unsafe. So how does this situation impact us and how we can solve this?
First thing is that um Scala compiler team was well aware of this problem and the solution that was built into the language was to start emitting different
bite code for lazy balls and this has been implemented it has been merged and it is part of the upcoming Scala 3.8 release
>> so it will no longer emit bite code that refers to in any way to sun unsafe. Yes,
the new bite code that is going to be emitted is going to use our handles to handle uh lazy valves. This mechanic of
of um comparent switch that was done to to go through phases of the state machine. Now a problem that still
machine. Now a problem that still persists is that we had the Scala 3.3 LTS release
in the meantime and the communication that we have built was that well you can bump your libraries to Scala 3.3 and you're going to be basically safe. you
are going to be able to use those libraries in any project that uses Scala 3.3 or higher for the whole eternity
fee. Now this is where the actual
fee. Now this is where the actual problem is. So if you use um library
problem is. So if you use um library that was built with any Scola free version before 38 with a project that
uses JDK26 as the runtime, you are going to unfortunately encounter runtime exceptions on each lazy values that was built with the older compiler. The code
that you built, the code that you have in your application if you're using Scala 3.8 is going to work just fine.
The problem is that we have a lot of dependencies and we can't really um depend on all the maintainers that they will just you know quickly go and bump
the version to 3.8 so that the bite code that is submitted works. It's it's not a practical solution to the problem if you think about it because >> for many reasons right because 3.8 is
not even going to be an an LTS release.
>> Yes. 3.9 is going to be LTS.
>> Yeah. So like the earliest point where we might might expect something like this would be 3.9 and everyone would still have to re-release and then no one
using 3.4 3.5 6 7 uh would be able to to use these libraries because they are in a higher uh minor version.
>> Yes, exactly. Well, the problem is um in in practice it's it's much worse because we have those huge transitive dependency trees where you have libraries that
depend on multiple different artifacts and uh given that we have eviction rules, it's going to be hard. So the end result is >> and and it's it's not even like we can
just do nothing. we can revert the var thing because that is like the unsafe thing is not going to be available from JDK6.
>> It's going to for example >> so we have no great place like we have really no way to run everything on JDK26 that is the the main problem.
>> Yes.
>> Mhm.
>> So what can we do? So if you if you think about the structure of the problem um when you build your project and you want to run it on JDK26
there are just two situations that we can currently think of where you have to compose the full class path where you have to have all the artifacts. So your
artifacts the artifacts of your application and the modules that compose your application and all the libraries all the dependencies that you have.
There are two situations where you have to compose the full clasp. One is when you test your application and if you want to test it on JDK 26 the problem hits you
and the second is if you build an artifact that you want to deploy let's say on docker with JDK26 it's still going to have the same problem and the
problem is quite um pervasive and and it basically undermines the whole concept of type that scala has because what you're going to get is rather surprising
exception in some of the code buffs.
It's it doesn't even have to explode immediately on on startup because it depends on the place where lazy fall is initialized.
>> So what can we do about this problem?
Well, um bumping the required version of JDK in 38 is one of the things that we do. We support new uh structure of the
do. We support new uh structure of the bite code. But what do we do about all
bite code. But what do we do about all the existing code that has those broken um broken versions of of um bite code
that won't work with JDK26? The solution
that uh we came up with as a team was that well maybe we should just patch the bite code. So if you think about it,
bite code. So if you think about it, it's not a very difficult operation. In
the end, the patterns that Scola compiler has for the emitted bite code are rather stable. There are just few variants um that exist in the wild of
how lazy valves are encoded. So what we can do is um we can build a tool that will do the patching. It will take the class files of packages that you have
that will compose your class path in the end and then um bump them to the version that um uses the pattern that we
introduced in Scala 38. That way all the lazy valves that are going to exist in executed by code are going to match the exact structure of lazy balls as they
are in Scola 3.8. Mhm. So essentially
all the late vals at runtime are going to be turned into varles instead of unsafe.
>> Exactly.
>> As I was editing and waiting to publish this video, we got a new development.
The Scala compiler team decided that the change the new encoding of lazy valves should be backported to the LTS series.
So it will appear in 3.3.8 when it gets released as an optional compilation flag and only if you're targeting uh JDK 9 or above. This will
enable library authors to add some support for JDK26 and above at the cost of dropping support for JDK8, but it will not require them to upgrade all the
way to 3.8 or 3.9, which will be the next LTS or long-term support release.
However, this option even in 3.3.8 doesn't resolve the problem of all the libraries in the ecosystem having already been published using the unsafe
encoding of Lexi files. So the tool that we're talking about in this video is still relevant. It's still needed to
still relevant. It's still needed to support the whole ecosystem. Now let's
get back to the video.
>> So how can we build this in a way that we can guarantee that what we are doing is always working because if we don't we are going to introduce
very very um curious mistakes into the bite code of applications of our end users and that's the last thing that we would like
to do. So we had had to come up with a
to do. So we had had to come up with a solution that would allow us to build this software in a way that would allow
us to be 100% sure that what we are doing always works and works for all the possible um artifacts that are going to
be present on the user's class. We can't
know what kind of libraries and what versions of of Scala compiler was used to compile them. So we have to do this
in a very foolproof and very um secure way. The way to do this is to first
way. The way to do this is to first detect the lazy vase in a class file.
Remember that we don't really have any information about what is the version of the compiler that compiled any given class file. So we have to do this to by
class file. So we have to do this to by detecting what is the structure of lazy vos. Then we infer the version of the
vos. Then we infer the version of the compiler based on that structure of the implementation of the lazy bone. Uh and
once we have this information we know what is the structure and what is the transformation that we have to apply to actually get um 3.8 compliant version of
the bite code. This is the patching pipeline. Now the the the most important
pipeline. Now the the the most important bit here is to do the verification in a way that is also very very robust. First
step is that we have to verify that our detection logic is always working correctly. So for many very very many
correctly. So for many very very many many very different versions of the B code that is generated for lazy balls in different contexts in different use
cases we have to uh be 100% sure that we detect the correct version of the lazy val.
>> What kind of context could this be?
Could this be like a lazy val inside the method and the different one would be [clears throat] just a lazy val in a class right?
>> Well, the the easiest for example is probably just an object that contains a lazy val. If you want something that is
lazy val. If you want something that is um a bit different in terms of the emitted bite code. Um an abstract uh class that has its own lazy val that
gets overridden in a class that extends that abstract class gives you a different thing, gives you a different outcome. These are the simplest
outcome. These are the simplest situations.
>> There aren't many many very difficult situations, but I would say the most difficult are probably situations where
you have pairs. So, um in Scala you have companion classes and companion objects and they are treated as um sort of >> they have like a sibling relationship
>> like a sibling relationship.
>> Borrow stuff from each other.
>> Yes. And and in this case um what compiler for Scola free does uh is a bit different between the implementation
that we use currently in the range of versions from 3.3 LTS to current 373 release. they have the same bite code
release. they have the same bite code generated and [clears throat] um how offsets of those lazy val fields
are uh compiled into um those into that pair differs between how Scala 38 has to do this because of a requirement that
ver handles introduce. So ver handles only work when they are uh compiled into the same class they are patching.
So if you have a ver handle, it has to be in the same class um in which there's the offset that's going to be modified.
This is different between how it worked before. And I would say that this is the
before. And I would say that this is the most difficult case that we have encountered.
>> Is it the restriction from the Java runtime or >> it's a restriction of of the implementation of var handles themselves?
>> So if you get it wrong, does it just crash or does it do something weird?
>> Yes, it it it just doesn't work. So you
can't store offset and var handle in separate classes. They have to be
separate classes. They have to be together and this is different between how things are compiled.
>> And in Scala the object has its own class. So this is
class. So this is >> yes why it's a problem.
>> Yes. And and Scola Scala maintains this relationship the sibling relationship.
So they have some way of sharing things between them and things get compiled into um into different places.
>> But of course the JVN doesn't know about any of them.
>> Yes. it it didn't matter uh before now it will matter just because v handles have this new requirement so the compiler for 38 already does this
correctly and the patch also has to be aware of this so um so the patching tool will have to detect that there's a pair
and decide okay so this is compiled by 3.4 four version of the compiler, I have to move this logic. I have to move this offset from the object to the class.
And this is this is one of of um multiple scenarios that we have. Um
there's a number of of scenarios that are less complex than this problem, but are nevertheless um a bit more convoluted than just plain
object that has a lazy ball. Now the
second step of verification is that we have to be absolutely sure that when we are doing comparisons between lazy val implementations we do them semantically
because uh if you think about the structure of the lazy val byte code that gets emitted for example in um compiled
initial initialization code in cl C cl init uh function you get some stuff that gets into the middle of the synchronization
um logic of lazy vos. Um and we can't just compare them one to one because they can be different between versions of the compiler only because there are
other features of the scholar compiler that are at play. We can't also for the same reason use just you know um sha um hashes to to compare classes.
>> Yeah, that's even more uh brittle.
>> Yes, that that's even more brittle. Even
small single bite difference breaks the hashing. So
hashing. So >> So in other words, you cannot just compile something with 3.3 and then apply the patch and also compile it with just straight 3.8 and expect the the
class files to be the same even structurally because they there are other things that that are different.
Okay.
>> Exactly. So some some other Scala features are different. There are things that have changed >> new optimizations. izations that things that have changed transparently in the um Scala standard library that don't
break binary compatibility but they still end up in the internals. So the
stuff that doesn't really matter when you are talking about interfaces but it does matter when we are looking at the list of operations in a class. So what
we have to do we have to be absolutely sure that when we compare stuff just to be able to verify that we have actually applied the patch and we have we have migrated correctly. We have to make sure
migrated correctly. We have to make sure that this implementation of comparisons is semantic and works exactly on the on
the structure of lazy valves while ignoring everything else that isn't related to lazy walls themselves. Now
the last bit is that we have to verify that the code we have compiled that we have patched actually uh works exactly
the same way it worked before. Now given
that we had understanding of how to do this the question was well how to implement this and this is where um another strand of our work at Berto's
lab comes in. So this year we spent a lot of time thinking about how we can make Scala AI proof. And by AI proof I
mean how to make Scala the language to use with AI because given that the future seems to be that we are going to
use more and more tools that are based on various ML models, various AI solutions. Um
solutions. Um >> how do we still make Scala relevant in that future?
>> Exactly. And the good thing for us is that um it seems quite apparent that um statically typed languages have an edge
over scripting languages or languages with um more loose type systems. uh because there's just this help that you
get from the compiler that tells um the AI that well the thing that you wrote it just won't work. So what you want to
have with AI based tools is a set of tools that will inform the agent that the code is broken in some way. And
Scala is in a very very good position to help with this because well we have a lot of type level stuff that helps us write correct code and helps us enforce
invariance about many many different properties of the code by putting that into types. So I I don't want to really
into types. So I I don't want to really spend much more time about um talking about this providing proofs. There's a
very nice talk by Dr. Kucharavei that um was presented at Scaldes this year. I
think the talks from Scott should be already present on YouTube. So
>> let's hope they are.
>> Let's hope they are. Um so let's uh just direct our viewers to the talk of Dr. and and they can see they can see in
this talk that Scala has some very nice properties that are mostly based on the fact that we are already writing a lot of safe and um
>> sound >> sound code exactly [snorts] >> uh and also let's not forget that uh like the compiler is one thing that can tell a models or language language
models uh what happened what's wrong and suggest solutions potentially. But we
also have based on the compiler and what it already knows, we have the MCP model context protocol server in metals which can aid with even more uh like uh timely
information something that is useful in the given context.
>> Yes. So, so MCP in metals was one of the things that we did this year and um this was also one of the things that was um
fairly easy given that we have a lot of information about the code that user is trying to compile um from the compiler itself. So um
itself. So um >> and it all boils down to us having a very strong type system.
>> Exactly. So with all these precognitions and all this uh work with tooling, uh there was still a question that needed
an answer. And the question was whether
an answer. And the question was whether in practice the AI models can actually work faster and generate better code
when working with Scalum. And this
problem, this particular lazy vault problem, um I found it to be one of the best examples where we can do this verification
um of the progress that we have made with AI um because it's a problem where we can do very nice and very quick verification
of whether the output of the program is correct. So I set out about two weeks
correct. So I set out about two weeks ago to uh build mostly in my free time uh the tool that would do this. I I I
didn't intend to build a final you know production grade um version of this software. I just
wanted to verify whether we can well first do this and get something that works. Um so whether this idea of
works. Um so whether this idea of patching is actually um relevant that can be implemented safely and uh the second thing was I
wanted to see whether we can actually tell our customers that well hey if you use Scalo you are going to get better code that is safe that is sound that
works works correctly and that can be developed upon by the AI agents I have built this tool actually it's still a P.
But the rather interesting thing is that it's a project. It's the first project for me where I didn't actually use any
ID. I didn't modify the code manually.
ID. I didn't modify the code manually.
You could say that it is VIP coded. I
would say it's VIP engineer because [laughter] it actually I I leverage a lot of my understanding about how to build software to guide the model to do
things that make much more sense than things that it tried to do. But beside
the fact that I was guiding the model, it's a first project for me where I didn't actually, you know, manually modify stuff. I didn't autocomplete
modify stuff. I didn't autocomplete stuff. the cloud code tool that I was
stuff. the cloud code tool that I was using did all the work and wrote all the code.
>> Okay. So, can you show us how this patter of yours works?
>> Absolutely. It's still, as I mentioned before, P, but it does work in some of the cases, especially the abstract class case that we have already mentioned. So,
let's see how this example looks like.
And we can see that we have an abstract class. We have a class that extends this
class. We have a class that extends this abstract class. The abstract class has
abstract class. The abstract class has the lazy ball and we then have some code that uh shows us the outcome of the execution of this code. So we also
should see what version of Java we're using here and it's Java 24. It's quite
important because Java 24 is the first release which has this big warning that goes uh into standard error every time a
lazy val um will be encountered by the runtime. It basically warns you every
runtime. It basically warns you every single time anyone any piece of code uses unsafe. But lazy valves trigger
uses unsafe. But lazy valves trigger that because they do use that. So we can compile this code and run it.
Um let's compile it with Scola 3.7.3 which is the current latest version of Scala. We're going to compile it and we
Scala. We're going to compile it and we are adding print class because we are going to use this classpuff to avoid problems with Scala CNI being its own external skull application and polluting
our standard output.
So we just compile it and we can see that okay works. So we now have this class puff that we're going to use to run our application without scali
meddling it in our standard output. It's
a skull application so it uses lazy vals and therefore it would pollute our standard output. So now what we want to
standard output. So now what we want to do is run this with Java CP. The um main class is main because we're just using
the main annotation. And you can see that it does indeed trigger this warning about terminally deprecated method in
some miscon. So
some miscon. So >> and this is JDK 24. If we were on 26, >> it will >> eventually this will fail.
>> Yes, exactly. It will throw an exception and therefore crash your application. So
what we can now do is use our patch.
I have built um fetchure of that tool so that we don't have to deal with um ro API in code and we are going to patch
this. So you can see that it did go
this. So you can see that it did go through all the class files that were built by compiler and it did find there is indeed one lazy wall to be patched
and that there are other classes that well they don't have lazy walls so they don't have to undergo any patching. Now
what we can do now is to just run this um on CP run the same class path as we had run before because we're only replacing the classes that have been
patched. They are in
patched. They are in >> replace the class file in place.
>> Yes, the the class paths were replaced in place and you can see that there is no warning though.
>> Mhm. And we have a lot of those examples right now uh that show you different cases of how code can be structured and
this batch currently works. All of them compile correctly.
>> Okay. Nice. So uh you mentioned you have some setup to verify these uh these cases.
>> Sure.
>> Can you walk us through how uh this this process works?
>> Yes. So what we have to do is we have to verify that we are finding correct versions of those lazy valves. So by
versions I mean which version of the scalifree compiler compiled that piece of bite code. We then have to do those
correct semantic comparisons because um the bite code for lazy walls can contain code that is unrelated to lazy walls themselves. So we have to make sure that
themselves. So we have to make sure that we're only comparing the bits of barcode that are relevant and related to lazy valves themselves only only then. And
then the third bit that we have to test is actually that we are patching them correctly and patching um has to be testing patching has to be done in a way
that guarantees that well the resulting bite code not only looks correct but also behaves correctly. So it it means we have to test that the output is exactly the same as we expect it should
be.
>> That the lazy valve gets initialized when you wanted to and before doesn't happen twice.
>> Exactly.
>> Exactly. This is still work in progress but uh lazy valve detection already works and we have a set of different uh scala versions that we are testing
against. in the future it would
against. in the future it would obviously have to go through all the Scola versions that have been released any time in the past because um what is
very important is that you don't really know what was the version of the compiler that a library that was published to Maven central repository is you don't have that information
>> so there's no like attributes in the class file that would tell you that >> I'm not 100% sure I think you could probably infer that from um from tasty files
>> um but from the class files themselves you you don't have that information and as I mentioned before there are use cases where you won't even get this information because uh if you're uh
building a fat chair you only take class files so we can't really we can't really always depend on that >> right the tasty is not going to be in the in the fure
>> yes so if we want to make this patching work for all relevant use cases we have to depend on bite code only.
>> Now in this suite we detect that we are finding lazy valves in the expected version. So we take those examples we
version. So we take those examples we com we compile them with different versions of scala through scala cli. Um
we then verify that we have found lazy vo as expected in this p code as the version of the compiler that was used to compile this code. So the pattern of
byte code that you find in the jar corresponds to the pattern that you would expect to see as the output of not the jar but the the cost file corresponds to the format that this
compiler is supposed to yield.
>> Yes, exactly. Once we are able to always find that we have lazy balls and we can infer the version of the compiler. The
next bit we have to do is to make sure that we can compare them based on the semantics of lazy vals. So we have to make sure that all the irrelevant
um pieces of bite code that belong to other Scala features are completely ignored. And this is exactly what
ignored. And this is exactly what happens in this test suite. We verify
this um by doing comparisons against different versions and asserting whether that versions are correct against each
other. Uh we uh verify that all the
other. Uh we uh verify that all the other versions are not equal to 3.8.
This is obviously 3.8 RC1 because 3.8 uh wasn't released at the date when we are recording this video. We also do comparisons between two different
families of laser implementations and that's because um the versions between 3.0 and 32x
used bitmap based implementation and then uh the versions from 3.3 LTS on going um we're using a different implementation based on on an offset
>> a single offset for every lazy wall field. So I think it's important to
field. So I think it's important to highlight here that uh this fix also includes version prior to 3.3 because as far as I understand even 3.0 compiled
code can still run on the latest Scala versions. Yes,
versions. Yes, >> the backward compatibility story didn't start with LTS.
>> Yes. And this is the hard requirement that we have. We don't really know what is going to end up on the um final clasp
of the application user is running. one
can easily um use a library that's published to Maven Central and you have no idea what was the version of the compiler that actually built that. You
only see the suffix the underscore free >> and you know that it's you only know that it's either the current minor series ending patch or one of the
previous minor series up to well starting from 3.0 Oh. Uh, and Scala 3 can run together with Scout 213 in the same class path, but that's not a
problem because 213 didn't use unsafe at all, right? So, we don't care about
all, right? So, we don't care about that.
>> Yeah, we we for for 213 we always see that there are no lazy balls that we detect. So, it's not
detect. So, it's not >> because it's just synchronized.
>> Yes, exactly. So the last suite that we have is the suite that actually runs uh patching and tests that the patched
version works correctly. So there are a few steps that we have to do here. One
is that we have to compile examples with different multiple scala versions before 3.8. We also compile compile for free 8
3.8. We also compile compile for free 8 so that we can do semantic comparison between the patched version and the
version um and the free version and we also have to run them between versions to verify that the outputs are
still the same. So we have a set of Scala versions um patching currently works for one kind of um um family of
lazy valves. uh I didn't work on the
lazy valves. uh I didn't work on the implementation for uh the older versions. So we have only the 33 to 37
versions. So we have only the 33 to 37 version supported and we have a set of versions for which the patch has to be applied
and then um we actually uh do all this logic. We um run um compilation for all
logic. We um run um compilation for all the Scala versions. We create copies of the compiled uh class files. We patch them and in the
end we assert against um all the things that are interesting to us. So
um we have to verify that code um that has been patched is semantically identical to 3.8
as expected and that when it's executed against 38, it yields exactly the same result.
And [clears throat] with this testing harness, I've been able to use claw code to build a lot of examples and then diagnose different issues as they
arrived. It's still work in progress,
arrived. It's still work in progress, but given that it has been only like two main days of work and we already have 9,000 lines of code, that seems to work
pretty well. I would say that it is
pretty well. I would say that it is pretty helpful. I would say that it's it
pretty helpful. I would say that it's it it looks like a good direction.
Obviously, [clears throat] uh we are going to spend time going through the code that has been written by AI to verify that well, it's it makes sense.
It isn't just doing dumb things but well it's it's very early and we already have something that works pretty nicely.
>> And all this code the the test harness is also something uh that Claude created.
>> Yes.
>> As I as I mentioned there is not a single line of code that I wrote by hand in this project.
>> That would explain the 15 or so levels of indentation in [laughter] this file.
I'm I'm glad to see that it's still likes braces in in the code.
>> Yeah.
>> Where where it makes sense, but it doesn't always like I see it then that doesn't use a brace. So, yeah.
>> Oh, yeah. It's a it's a bit of mix and match. You can see that it uses new if
match. You can see that it uses new if >> it's a pragmatic decision.
>> I would say it's a it's a random decision, but if it works, it works for me. At least in in the scope of this
me. At least in in the scope of this experiment. Um anyway, I do have um
experiment. Um anyway, I do have um small test script that goes through all of that um very quickly and verifies all
those things. Let me quickly make it a
those things. Let me quickly make it a little bit more readable. Um it's bash unfortunately.
>> You cannot make it much more readable.
>> Yes, exactly. But uh what it does it goes through all the steps that I have mentioned. So um it compiles a project.
mentioned. So um it compiles a project.
Uh it does that in a clean working directory. Um creates a Scala file then
directory. Um creates a Scala file then compiles it with 3.6.4.
Uh then it gets the class path just as we did a moment ago. Uh then it makes a copy of the compiled class file so that we have a clean working directory for
patching. Then it inspects original bite
patching. Then it inspects original bite code to verify that it is indeed using the bite code that references on safe stuff.
And um then uh it runs the original byte code so that we can see the warning that JV JVM
does emit when it encounters unsafe usage.
Then uh it runs the assembly to patch the byte code inspects the patch by code so that you can see that handles are now
replacing some unanafe usage and in the end uh it does test the punch spite code by running it so that you can see both
the outcome and that there is no warning. Let's quickly run this script.
warning. Let's quickly run this script.
Um, how many dots is it going to take for us to get out of here? Only six.
Okay. And this is verifying something.
And yeah, so it's all pretty colorful as scripts written by I are, but it does exactly what we would like to see. So we
can see here that in fact um we did get a warning before patching. We see the output from the patcher and then we can see that the patched version has
absolutely no warning.
>> Mhm.
Just as we wanted.
>> Exactly.
>> Okay. So what are the next steps with this? Uh how do you actually plan for
this? Uh how do you actually plan for people to use this kind of pattern tool?
So once this is stable enough and obviously is reviewed by humans
what we will need to do is to inject this patching tool into two different slots. One is the class path that goes
slots. One is the class path that goes into testing and this is a bit more involved scenario because when your build tool
um runs your tests it just composes the class path and just pushes that to uh the JVM with the integration for the
testing framework. So in this case, if
testing framework. So in this case, if we wanted to just go for JAR files and patch them, it would mean that we would have to go through um the cache of jars
downloaded from Maven Central. And that
would be a rather risky move because you don't want to do that. And also mutating a cache is rather shady business. So
there is a way to deal with that. And
[clears throat] the way to do this is to build a so-called pre-made Java agent. a
pre-made Java agent um injects itself into hooks in the JVM that are executed before the main of the user load program
is executed. So what we want to do when
is executed. So what we want to do when we get into this place is to inject our code into class loaders uh that process
all the files that are loaded from any dependencies any class paths that are used by the application. And once we do that we do have access to the raw bite
code. We do get access to class files as
code. We do get access to class files as ari byte. So that's exactly the
ari byte. So that's exactly the interface that we expect. We can do this modification dynamically. And once we do
modification dynamically. And once we do that you can run any class path that composes scalar free application with
libraries coming from any scalar free versions on JDK26. It's going to u take a little um >> careful engineering.
>> Careful engineering >> and a lot of trial and error.
>> That's that's one thing I was I was going to say that it's going to yield a small penalty on the performance of test of tests themselves but in the end you get something that is running and you
don't have to um migrate everything panically to uh 3.9.
>> No.
So you are going to inject code into some specific brands of crossholder like URL crossoder or is it more universal?
>> I think we I think we have to um inject that into any class loader that gets initialized in JVM.
>> Mhm.
>> Um I don't really have certainty about this yet because it's still a piece of work to be done. The second problem is that um in the end user is going to
build some kind of um an assembly jar or maybe um just copy a set of jar files into some location
that will let's say end up on a docker image.
>> Mhm. And in this case um it's actually a very interesting point from the tooling perspective because once user gets to this point we
know almost in in almost all cases we know that this is a closed world scenario.
There is nothing else going to be added to this class. I said in almost all scenarios because people >> do like SPI or other
>> yes people people do different weird things plug-in systems for example so we we can't be sure about all of that but um if it's a you know run-of-the-mill
application that just has a complete class path that gets published to some kind of artifact like docker image like um assembly jar we can just intercept
all the class files that are going to this artifact rewrite them and then we have the certainty that everything works. For those other cases where there
works. For those other cases where there is going to be some dynamic loading of classes, you can always add this agent that you mentioned for test cases and it
should also do the work >> or maybe you can manually like wrap the class order that you use to load these plugins and >> yes yes that's that's also thing that we
could provide. So we could provide an
could provide. So we could provide an >> like a programmatic API for this.
>> Yes. Um a decorator basically for class loaders. Yes. [clears throat]
loaders. Yes. [clears throat]
>> Okay. What about when you do SPT run? I
guess that doesn't uh emit a fab jar.
>> Uh it's only a jar.
>> That's the same case as with test. So
you also have to inject a Java agent.
>> Mhm.
>> And in that case it should just rewrite all the Scala free class files that are read into the JVM.
>> Okay.
Uh yeah, very cool. And I'm very happy that we're doing something about this problem before uh JDK26 hits the shelves. Uh so that people even if they
shelves. Uh so that people even if they do get surprised because they have like an older SPT. Uh I suppose this uh could end up being kind of built in into the
build tools so that people don't have to do anything by default.
>> That's yes, that's exactly what I hope for. doing nothing is um a bit of a
for. doing nothing is um a bit of a negotiable position because um we obviously can't enable this by default.
>> We can't do this because um we don't really know what is the final runtime which is going to run the application.
Somebody might working on uh let's say JVM 21 on their local machine but still build Docker images that use JDK 1.8
date um for some reasons. You might for example have this scenario from the top of my head in big data clusters where usually you still get pretty
conservative approach to modifying runtime that is running all these services. So in those cases um we can't
services. So in those cases um we can't really have the certainty >> but we have discussed an option where you could potentially look at the target JVM uh flags.
>> Yes.
>> And if that shows you anything higher than or including nine and then you can enable it.
>> Yes. But um
>> but it's still not 100% foolproof. Yeah.
>> Yes. So with target higher than nine, we could provide information that you probably want to have that because you you're already emitting bite code that
won't run on 1.8, but there is still the problem of the performance difference between lazy valves implemented with v handles and unsafe. The v handles are
unfortunately a bit slower. Um but
that's a limitation that is coming from the platform itself. Um so if user has some performance sensitive code I would say that okay
maybe we shouldn't just you know move to 26 maybe we should use 25 and um run with unsafe as far as we can um and and
maybe maybe think about migrating rewriting code later on just to handle performance uh sensitive pieces of the code >> right so again that goes with the
compatibility story I guess like you do not do don't want to >> uh change things like mix things up randomly in the runtime you just give the users information that hey this
might be necessary to turn on you even let them I suppose silence that warning if they don't care about it >> or if they just do not intend to update >> their JDK and uh
>> so so the solution I'm partial to um and I see as the most reasonable one is that it should be an optin outside of the cases where the target is
explicitly set to 26 or higher and I mean JVM >> because um if you're emitting bite code for JDK 26 it doesn't matter you have to
use handles it won't work with unsafe anyway so we can decide that okay if the target is 26 you have to use this batch >> it's the least broken solution
>> yes it's the least broken solution for >> but you can still show like an info or warning just to tell hey we are actually rewriting this you you can silence >> definitely we have to do this we we can't be silent about this
transformation but um this is >> be silenced >> yes this is this is the only case where we can safely say okay this is we have to do this there's no other way for
everything else user has to decide the user has to get a warning and there should be an opt-in flag that okay just use this way of emitting bite code for default now this is a rather per large
piece of work because it has to happen across the ecosystem. We have free Scala based build tools build tools build tools in in quotes when talking about
Scala CLI but still >> um we have three different tools that are based on Scala. We have SPT, we have mill and sculi and [clears throat] they
all have to start having this you know patching um logic and and patching option and then there's Maven and then there's also Gradel. So
also Gradel. So >> yes, >> this is this is a cross ecosystem uh thing that has to happen and we'll see about that in the future. I'm sure if
you try hard enough, you'll even find users of like the closure build tools uh like lining and uh others also having Scala on the glass puff and eventually
upgrading their JDKs and getting surprised. So
surprised. So >> yeah, it is a much bigger effort than what I think. I I think that that one last problem of of very niche build
tools has to be handled um with some pretty good positioning in Google or [laughter] perplexity or whichever search engine you're using.
But it should be pretty easy to understand why you are getting problems with JDK26 with scholar.
>> Mhm.
Maybe we can talk to the JDK team itself and have them like include the POS.
Well, ideally they would include like a link to like an FAQ page or something when you get this error in ADK26 that could include a mention of Scala maybe have a link to
our pages you know instead of just being an exception and you know do this do that they link to some living document.
I don't know if if we have that reach maybe with this video. Uh but I suppose you you and the compiler team have a better uh better way to talk to them. Uh
I guess this is this is a nice uh place to end at. Uh this has been lazy valade or lazy val upgrade or as I like to call it lazy val apocalypse. Please tell me
in the comments which one you prefer uh because that name will stick. Uh we will make it happen. This has been Wukash B.
If you'd like to hear more from Lukash or from me or about the Lazy Val apocalypse, uh, let me know, uh, in the comment as well. Uh, thank you for watching and I'll see you in the next episode.
>> Thank you. Bye.
Loading video analysis...