<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>ZJBlog Blog</title>
        <link>https://samuel-malec.github.io/blog</link>
        <description>ZJBlog Blog</description>
        <lastBuildDate>Sun, 27 Sep 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <item>
            <title><![CDATA[Graal Memory Model 🧠]]></title>
            <link>https://samuel-malec.github.io/blog/2026/09/27/graal-memory/intro</link>
            <guid>https://samuel-malec.github.io/blog/2026/09/27/graal-memory/intro</guid>
            <pubDate>Sun, 27 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[This post looks at how the Graal compiler tracks memory effects in its IR, and why function calls often get in the way of optimizations. The examples come from Native Image, the ahead-of-time compiler inside GraalVM.]]></description>
            <content:encoded><![CDATA[<p><em>This post looks at how the Graal compiler tracks memory effects in its IR, and why function calls often get in the way of optimizations. The examples come from Native Image, the ahead-of-time compiler inside <a href="https://www.graalvm.org/" target="_blank" rel="noopener noreferrer" class="">GraalVM</a>.</em></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="graal-ir">Graal IR<a href="https://samuel-malec.github.io/blog/2026/09/27/graal-memory/intro#graal-ir" class="hash-link" aria-label="Direct link to Graal IR" title="Direct link to Graal IR" translate="no">​</a></h2>
<p>Graal IR is the intermediate representation of the Graal compiler. Both the Graal JIT and Native Image use it.
It is a graph-based IR, more precisely a <a href="https://softlib.rice.edu/pub/CRPC-TRs/reports/CRPC-TR93366-S.pdf" target="_blank" rel="noopener noreferrer" class="">sea-of-nodes</a> (SoN) representation.
Each <a href="https://github.com/oracle/graal/blob/54a25dd6aba9bd7732247a7d1971f9bbd496ec2f/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/graph/Node.java" target="_blank" rel="noopener noreferrer" class="">node</a> roughly corresponds to a high-level instruction, such as an addition, a memory access, or a function call.</p>
<p>There are two kinds of nodes:</p>
<ul>
<li class=""><a href="https://github.com/oracle/graal/blob/54a25dd6aba9bd7732247a7d1971f9bbd496ec2f/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/FixedNode.java" target="_blank" rel="noopener noreferrer" class="">Fixed</a> nodes are pinned to a specific point in the control flow. They are connected to their neighbours via <code>successor</code> edges. <code>Invoke</code>, <code>If</code>, <code>Return</code>, and nodes with side effects are typically fixed.</li>
<li class=""><a href="https://github.com/oracle/graal/blob/5d2611cedf4c733020519b2f8feeb18a66344a5a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/calc/FloatingNode.java#L34" target="_blank" rel="noopener noreferrer" class="">Floating</a> nodes are constrained only by their data dependencies, which are represented by <code>input</code> edges.</li>
</ul>
<p>To see what Graal IR looks like, take this Java program:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">public class Fibonacci {</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    public static int rec(int n, int a, int b) {</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        if (n == 0) {</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">            return a;</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        }</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        return rec(n - 1, b, a + b);</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    public static int fib(int n) {</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        return rec(n, 0, 1);</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    public static void main(String[] args) {</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        System.out.println(fib(5));</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>The picture below shows the Graal IR of <code>Fibonacci::rec</code>. Control edges are red, data edges are blue or black.
Control flow starts at the unique <code>Start</code> node. A <code>Begin</code> node roughly marks the beginning of a basic block.</p>
<p>You may notice that two control edges leave the <code>Invoke!#Fibonacci.rec</code> node.
This is because any Java call can throw an exception.
So the call gets two successors: one for the normal return, and one for the exception path.
<code>rec</code> has no exception handler, so the exception path ends in an <code>Unwind</code> node, which passes the exception on to the caller.
This has nothing to do with recursion. Any call that may throw looks like this.</p>
<p><img decoding="async" loading="lazy" src="https://samuel-malec.github.io/assets/images/fibonacci_ir-2165fba5c598ae6b3a59f202f7045f44.png" width="1053" height="989" class="img_ev3q"></p>
<p>For more examples of Graal IR, take a look at Chris Seaton's <a href="https://chrisseaton.com/truffleruby/basic-graal-graphs/" target="_blank" rel="noopener noreferrer" class="">blog post</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="location-identities">Location Identities<a href="https://samuel-malec.github.io/blog/2026/09/27/graal-memory/intro#location-identities" class="hash-link" aria-label="Direct link to Location Identities" title="Direct link to Location Identities" translate="no">​</a></h2>
<p>Graal tracks memory effects with <a href="https://github.com/oracle/graal/blob/5d2611cedf4c733020519b2f8feeb18a66344a5a/sdk/src/org.graalvm.word/src/org/graalvm/word/LocationIdentity.java#L55" target="_blank" rel="noopener noreferrer" class=""><code>LocationIdentity</code></a>.
A location identity names the part of memory an operation works with.
Two operations can only interfere if their locations <em>overlap</em>.
The main kinds are:</p>
<ul>
<li class=""><code>LocationIdentity.any()</code> means the operation <em>may touch any memory</em>. It overlaps with every other location.</li>
<li class=""><code>FieldLocationIdentity</code> is one identity per declared field. It is shared by all objects of that class. So <code>a.x</code> and <code>b.x</code> share a location, but <code>a.x</code> and <code>a.y</code> never do.</li>
<li class=""><code>NamedLocationIdentity</code> is a named "family" of locations. Examples are all <code>int[]</code> elements, or a group of internal VM fields.</li>
</ul>
<p>Two special cases are worth knowing:</p>
<ul>
<li class="">Some locations are <em>immutable</em>, for example the length of an array. Nothing ever writes to them after allocation, so reading them needs no ordering at all.</li>
<li class=""><code>LocationIdentity.init()</code> is used for writes that initialize a freshly allocated object. Nobody else can see that object yet, so these writes don't interfere with normal reads.</li>
</ul>
<p>A node that <em>reads</em> memory implements <code>MemoryAccess</code>. It has a location and a <a href="https://github.com/oracle/graal/blob/54a25dd6aba9bd7732247a7d1971f9bbd496ec2f/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/memory/FixedAccessNode.java#L123" target="_blank" rel="noopener noreferrer" class=""><code>lastLocationAccess</code></a> input, which points to the most recent node that changed that location.
A node that <em>may change</em> memory implements <code>SingleMemoryKill</code> or <code>MultiMemoryKill</code>, depending on how many locations it kills.
Such nodes are called <em>kills</em>. Writes are kills, but so are calls and memory barriers.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="calls-kill-everything">Calls Kill Everything<a href="https://samuel-malec.github.io/blog/2026/09/27/graal-memory/intro#calls-kill-everything" class="hash-link" aria-label="Direct link to Calls Kill Everything" title="Direct link to Calls Kill Everything" translate="no">​</a></h2>
<p>A call is an <a href="https://github.com/oracle/graal/blob/54a25dd6aba9bd7732247a7d1971f9bbd496ec2f/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/InvokeNode.java#L71" target="_blank" rel="noopener noreferrer" class=""><code>InvokeNode</code></a>.
It is a <code>SingleMemoryKill</code>, and its location <a href="https://github.com/oracle/graal/blob/54a25dd6aba9bd7732247a7d1971f9bbd496ec2f/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/InvokeNode.java#L92-L94" target="_blank" rel="noopener noreferrer" class="">defaults to <code>any()</code></a>.
So a call kills everything.
The same holds for <a href="https://github.com/oracle/graal/blob/54a25dd6aba9bd7732247a7d1971f9bbd496ec2f/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/InvokeWithExceptionNode.java#L69" target="_blank" rel="noopener noreferrer" class=""><code>InvokeWithExceptionNode</code></a>.</p>
<p>This is a safe default. The compiler doesn't know what the callee does, so it assumes the worst.
Once the call is inlined, the callee's real reads and writes become visible, and the <code>any()</code> kill disappears.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="floating-reads-and-scheduling">Floating Reads and Scheduling<a href="https://samuel-malec.github.io/blog/2026/09/27/graal-memory/intro#floating-reads-and-scheduling" class="hash-link" aria-label="Direct link to Floating Reads and Scheduling" title="Direct link to Floating Reads and Scheduling" translate="no">​</a></h2>
<p>Fixed nodes keep their position in the control flow. Location identities don't change that.
Location identities matter once reads become <em>floating</em>.</p>
<p>The phase that does this is <a href="https://github.com/oracle/graal/blob/54a25dd6aba9bd7732247a7d1971f9bbd496ec2f/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/phases/common/FloatingReadPhase.java#L86" target="_blank" rel="noopener noreferrer" class=""><code>FloatingReadPhase</code></a>.
It walks the fixed nodes and keeps a map from each location to its last kill.
Each read becomes a floating read, and its <code>lastLocationAccess</code> points to the last kill of <em>its own</em> location.
Where control flow merges, the phase adds a memory phi for each location that differs between the paths.
After this phase, reads float, but writes stay fixed.</p>
<p>The result is that a read of <code>a.x</code> no longer cares about an earlier write to <code>b.y</code>.
It is only ordered after the last write to field <code>x</code>.
This lets the compiler remove duplicate reads and move reads out of loops.</p>
<p>Later, the scheduler has to give every floating node a place in the control flow again.
For a floating read, there are two limits:</p>
<ul>
<li class=""><strong>The earliest point</strong> comes from the <code>lastLocationAccess</code> edge. The read cannot move above the last kill of its location.</li>
<li class=""><strong>The latest point</strong> has no edge in the graph. The read must not move below the <em>next</em> kill of an overlapping location. The scheduler works this out itself: it checks the blocks between the two points, and pulls the read up if one of them kills its location.</li>
</ul>
<p>Note that two reads of the same location don't need any order between them. Only reads and kills must stay in order.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-any-hurts">Where <code>any()</code> Hurts<a href="https://samuel-malec.github.io/blog/2026/09/27/graal-memory/intro#where-any-hurts" class="hash-link" aria-label="Direct link to where-any-hurts" title="Direct link to where-any-hurts" translate="no">​</a></h2>
<p>Because a call kills <code>any()</code>, it blocks every optimization that needs to move a memory access past it.
In the two examples below, the compiler could know that the call doesn't touch a certain location.
But Graal IR doesn't record this, so the optimization is blocked.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="example-i-loop-invariant-read">Example I: Loop-Invariant Read<a href="https://samuel-malec.github.io/blog/2026/09/27/graal-memory/intro#example-i-loop-invariant-read" class="hash-link" aria-label="Direct link to Example I: Loop-Invariant Read" title="Direct link to Example I: Loop-Invariant Read" translate="no">​</a></h3>
<p>Consider:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">public class ExampleI {</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    static class Counter { int value; }</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    static class Config { int limit; }</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    static void bump(Counter counter) {</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        counter.value++;</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    static void process(Counter counter, Config config, int n) {</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        for (int i = 0; i &lt; n; i++) {</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">            bump(counter);</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">            if (i &gt; config.limit) {</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">                break;</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">            }</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        }</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    public static void main(String[] args) {</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        Config config = new Config();</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        Counter counter = new Counter();</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        config.limit = 10;</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        process(counter, config, 10);</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        System.out.println(counter.value);</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>Nothing in the loop writes to <code>config.limit</code>, so the read is loop invariant.
We would like to hoist it out of the loop.</p>
<p>In our build, <code>bump</code> was not inlined, so the call stays in the graph.</p>
<p>The loop starts at the <code>8 LoopBegin</code> node.
The memory operations <code>23</code>, <code>24</code>, and <code>31</code> stay in order, and the read of <code>config.limit</code> is not hoisted.</p>
<p><img decoding="async" loading="lazy" src="https://samuel-malec.github.io/assets/images/exampleI_graal_memory_model-6d72669b6b0a542bbb403fc940013fa8.png" width="1103" height="1195" class="img_ev3q"></p>
<p>This is the <code>any()</code> problem from above.
<code>Config.limit</code> and <code>Counter.value</code> are different fields, so their location identities don't overlap.
The call to <code>bump</code> kills <code>any()</code>, though, and <code>any()</code> overlaps with everything.
So the loop body kills <code>Config.limit</code> on every iteration, as far as the compiler knows.
The read has to stay after the call, inside the loop.</p>
<p>If <code>bump</code> were inlined, the only kill in the loop would be the write to <code>Counter.value</code>.
The read of <code>config.limit</code> could then be moved out of the loop.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="summary">Summary<a href="https://samuel-malec.github.io/blog/2026/09/27/graal-memory/intro#summary" class="hash-link" aria-label="Direct link to Summary" title="Direct link to Summary" translate="no">​</a></h2>
<ul>
<li class=""><code>LocationIdentity</code> names the part of memory an operation works with. Accesses to different locations don't interfere.</li>
<li class=""><code>FloatingReadPhase</code> lets reads float. Each read is tied only to the last kill of its own location.</li>
<li class="">The scheduler places reads between the last kill and the next kill of their location.</li>
<li class="">Calls kill <code>any()</code> by default. Unless the call is inlined, this blocks optimizations like hoisting reads out of loops.</li>
</ul>]]></content:encoded>
            <category>graal-vm</category>
        </item>
        <item>
            <title><![CDATA[Dagon 🐙]]></title>
            <link>https://samuel-malec.github.io/blog/2026/09/13/qthu/intro</link>
            <guid>https://samuel-malec.github.io/blog/2026/09/13/qthu/intro</guid>
            <pubDate>Sun, 13 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Dagon is a compiler toolchain that transforms a subset of JavaScript all the way down to QuickJS bytecode, through an intermediate language called Cthulhu.]]></description>
            <content:encoded><![CDATA[<p>Dagon is a compiler toolchain that transforms a subset of JavaScript all the way down to QuickJS bytecode, through an intermediate language called <a href="https://is.muni.cz/auth/th/paeh4/zjb.pdf" target="_blank" rel="noopener noreferrer" class="">Cthulhu</a>.
The source code is on <a href="https://github.com/samuel-malec/dagon" target="_blank" rel="noopener noreferrer" class="">github</a>.</p>
<p>Two compilers make up the pipeline: <code>js2ct</code> lowers JavaScript into Cthulhu IR, and <code>ct2qjs</code> lowers Cthulhu IR into QuickJS bytecode.
You can try out and experiment with the complation pipeline below.
Edit the source and every tab updates live. The produced QuickJS bytecode is actually executed.</p>
<div class="loading_FG3c">Loading qthu pipeline explorer…</div>]]></content:encoded>
            <category>Cthulhu</category>
        </item>
        <item>
            <title><![CDATA[Compiler Dungeon ️⚙️]]></title>
            <link>https://samuel-malec.github.io/blog/2026/09/03/compiler-dungeon/intro</link>
            <guid>https://samuel-malec.github.io/blog/2026/09/03/compiler-dungeon/intro</guid>
            <pubDate>Thu, 03 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[This is an educational compiler built to demonstrate the core ideas in compiler theory. It targets a]]></description>
            <content:encoded><![CDATA[<p>This is an educational compiler built to demonstrate the core ideas in compiler theory. It targets a
small language expressive enough to write real programs with, while staying small enough
that every stage of the pipeline fits in your head at once. The source is on
<a href="https://github.com/samuel-malec/compiler-dungeon" target="_blank" rel="noopener noreferrer" class="">github</a>, and the language itself is documented
<a href="https://github.com/samuel-malec/compiler-dungeon/blob/main/docs/language.md" target="_blank" rel="noopener noreferrer" class="">here</a>.</p>
<p>I'll be writing more detailed posts on individual parts of this pipeline over time. This post is the
front door: an interactive compiler embedded directly in the page, so you can see how a program
actually moves through each stage before reading about any of them in depth.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="interactive-compiler">Interactive compiler<a href="https://samuel-malec.github.io/blog/2026/09/03/compiler-dungeon/intro#interactive-compiler" class="hash-link" aria-label="Direct link to Interactive compiler" title="Direct link to Interactive compiler" translate="no">​</a></h2>
<p>Edit the source below, then step through the pipeline with the buttons underneath it. The
representations (Tokens, AST, HIR, IR, SSA) are what your program <em>looks like</em> at each point; SCCP and
DCE are optimization passes that <em>rewrite</em> the SSA form rather than being representations of their
own, so selecting one shows a before/after instead of pretending it's just another tab.</p>
<div class="loading_EfkP">Loading compiler explorer…</div>]]></content:encoded>
            <category>Compiler Dungeon</category>
        </item>
        <item>
            <title><![CDATA[Side-Effect Analysis 👀]]></title>
            <link>https://samuel-malec.github.io/blog/2026/08/17/side-effect-analysis/intro</link>
            <guid>https://samuel-malec.github.io/blog/2026/08/17/side-effect-analysis/intro</guid>
            <pubDate>Mon, 17 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Making memory side effects ordinary values in a compiler's intermediate representation]]></description>
            <content:encoded><![CDATA[<p><em>Making memory side effects ordinary values in a compiler's intermediate representation</em></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="overview">Overview<a href="https://samuel-malec.github.io/blog/2026/08/17/side-effect-analysis/intro#overview" class="hash-link" aria-label="Direct link to Overview" title="Direct link to Overview" translate="no">​</a></h2>
<p>Whole-program, sound, interprocedural analyses are really hard to implement efficiently and correctly.
They have to work on the entire program at once and handle every detail that comes with that: context sensitivity, function summaries, recursion, virtual calls, and so on.
That's why industrial compilers do most of their reasoning one function at a time, and rely on inlining to see a little further.</p>
<p>We want to sidestep this difficulty rather than solve it.
Our goal is to make side effects <em>explicit in the program representation</em>, so that analyses and optimizations can reason about them as ordinary data dependencies.
This post introduces the core idea, the representation we build it on, and the plan for trying it out in a real compiler.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="core-idea">Core Idea<a href="https://samuel-malec.github.io/blog/2026/08/17/side-effect-analysis/intro#core-idea" class="hash-link" aria-label="Direct link to Core Idea" title="Direct link to Core Idea" translate="no">​</a></h2>
<p>The main idea is to represent side effects as explicit values.
A conventional function can be viewed as:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">f : arguments → result</span><br></div></code></pre></div></div>
<p>A function with an explicit <em>state</em> (or <em>effect</em>) token is the same function with one extra argument and one extra result:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">f : state × arguments → result × state</span><br></div></code></pre></div></div>
<p>The state token stands for the part of the program state that the function may observe or modify.
For I/O, for example, we can introduce a token called <code>world</code>:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">io_read  : world × fd → value × world</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">io_write : world × fd × value → world</span><br></div></code></pre></div></div>
<p>The important property is that the state is now a <em>value</em>, and values have data dependencies.
In the sequence</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">w1 ← io_write(w0, fd, 3)</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">w2 ← io_write(w1, fd, 4)</span><br></div></code></pre></div></div>
<p>the second write needs <code>w1</code>, which only the first write produces: <code>w0 → io_write → w1 → io_write → w2</code>.
The order of the two writes no longer depends on the order of the statements. It is data flow, the same kind of dependency as the one between <code>x = a + b</code> and <code>y = x * 2</code>.</p>
<p>A nice side effect (pun intended): the signature now says what a function does.
A function that takes a <code>world</code> may do I/O. A function that takes no token at all is pure.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="from-io-to-the-heap">From I/O to the heap<a href="https://samuel-malec.github.io/blog/2026/08/17/side-effect-analysis/intro#from-io-to-the-heap" class="hash-link" aria-label="Direct link to From I/O to the heap" title="Direct link to From I/O to the heap" translate="no">​</a></h3>
<p>The same trick works for memory.
We add a <code>heap</code> token and thread it through every load and store:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">store : heap × ref × value → heap</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">load  : heap × ref → value × heap</span><br></div></code></pre></div></div>
<p>With a single <code>heap</code>, though, <em>every</em> memory operation ends up on one chain, so everything stays ordered after everything else.
Instead, we split the heap into non-overlapping parts and give each part its own token:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">counter_heap   -- the fields of Counter objects</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">logger_heap    -- the fields of Logger objects</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">config_heap    -- the fields of Config objects</span><br></div></code></pre></div></div>
<p>Now look at two stores to unrelated objects:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">c1 ← store(c0, counter.value, 1)     -- consumes and produces counter_heap</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">l1 ← store(l0, logger.count, 7)      -- consumes and produces logger_heap</span><br></div></code></pre></div></div>
<p>Neither store needs anything the other one produces. There is no data dependency between them, so we are free to swap them, just as we could swap two pure operations like <code>a + b</code> and <code>c * d</code>.
Operations on the <em>same</em> part keep their order. A load of <code>counter.value</code> needs the <code>counter_heap</code> produced by the store before it, so it can't move above that store.</p>
<p>In short: <strong>operations connected by data flow must keep their order. Operations with no data path between them can be moved freely.</strong>
Code motion, and as we will see, removing dead code, then comes down to following data flow.
This only works if <em>every</em> effect is in the representation. Exceptions are effects too, and we will come back to them.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ssu">SSU<a href="https://samuel-malec.github.io/blog/2026/08/17/side-effect-analysis/intro#ssu" class="hash-link" aria-label="Direct link to SSU" title="Direct link to SSU" translate="no">​</a></h2>
<p>So what kind of representation suits these tokens? Well, let's construct it together.
The first important thing we must realize is that an effect token (heap, I/O, ...) is a perfect example of a <em>linear</em> value: it must be used <strong>exactly once</strong>.
A token can't be <strong>duplicated</strong>. Two operations consuming the same <code>w0</code> would mean two different futures of the same world, and the program only gets one.
And it also can't be <strong>dropped</strong>. Throwing <code>w1</code> away would mean forgetting that the write happened.</p>
<p>These are exactly the properties we would intuitively expect from an "effect" type.</p>
<p>Now suppose we track a <code>world</code> token through an <code>if-else</code>, where each branch does some I/O</p>
<p>When control flow <em>splits</em>, <code>w0</code> has to go into both branches. That looks like a duplication, the one thing we just ruled out.
It's fine because only one of the branches actually runs. So this isn't a real copy, it's a <em>fork</em> into two mutually exclusive paths,
and we treat it as a different operation from duplication.</p>
<p>When control flow <em>merges</em>, we have <code>w1</code> from one branch and <code>w2</code> from the other, and we must <code>join</code> them into a single <code>w3</code>.
If that looks familiar, it should: it's a phi-node, exactly as in SSA.</p>
<p>This brings us to <strong>SSU</strong>, <em>static single use</em> form.
It is SSA with one extra rule: every value is <em>used</em> exactly once, not just defined exactly once.
If an ordinary value (an integer, a reference) is needed twice, the program has to say so with an explicit <code>dup</code>.
Effect tokens can't be <code>dup</code>-ed at all; the only way to split one is a <code>fork</code> at a branch.</p>
<p>Why bother?
Because in SSU every use of a value is visible, so the question <em>"who observes this value?"</em> can be answered by reading the graph.
For heap tokens, that is exactly what we want to know. Take:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">c1 ← store(c0, counter.value, 1)</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">...                                  -- anything that doesn't use c1</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">c2 ← store(c1, counter.value, 2)</span><br></div></code></pre></div></div>
<p>If the second store is the <em>only</em> use of <code>c1</code>, nothing ever observes the value <code>1</code>, so the first store is dead, and we know that without running any analysis.
If something in between could read <code>counter.value</code>, it would have to take <code>counter_heap</code>, which would give <code>c1</code> a second use.
We will meet this example again with real code.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="implementation">Implementation<a href="https://samuel-malec.github.io/blog/2026/08/17/side-effect-analysis/intro#implementation" class="hash-link" aria-label="Direct link to Implementation" title="Direct link to Implementation" translate="no">​</a></h2>
<p>The concrete experimental setting for this work is <strong>Native Image</strong>, the ahead-of-time compiler in GraalVM.
Two of its parts matter to us:</p>
<ul>
<li class="">a whole-program <strong>points-to analysis</strong>, which works out which objects each reference may point to, and along the way, which fields each method reads and writes;</li>
<li class="">the <strong>Graal compiler</strong>, which then compiles each method using Graal IR, a sea-of-nodes representation.</li>
</ul>
<p>I wrote a <a class="" href="https://samuel-malec.github.io/blog/2026/09/27/graal-memory/intro">separate post</a> about how Graal models memory, and I'll refer to it several times.
The short version: Graal treats every call as if it could change <em>any</em> memory, and that blocks optimizations around calls.
The obvious fix is to tell the compiler what a call actually changes.
But the IR can only record what a call <em>writes</em>. Nothing in it can say what a call <em>reads</em> (well, there have been some attempts such as <a href="https://github.com/oracle/graal/pull/12559" target="_blank" rel="noopener noreferrer" class="">TESA</a>).
Some optimizations, such as removing the dead store above, need exactly that, which is why we want both sides in the representation.
In the following sections, I will provide insights of the implementation and discuss important decisions made along the way.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="exporting-native-images-points-to-information">Exporting Native Image's Points-to Information<a href="https://samuel-malec.github.io/blog/2026/08/17/side-effect-analysis/intro#exporting-native-images-points-to-information" class="hash-link" aria-label="Direct link to Exporting Native Image's Points-to Information" title="Direct link to Exporting Native Image's Points-to Information" translate="no">​</a></h2>
<p>Native Image runs its points-to analysis as one step of building an image.
Starting from <code>main</code>, it works out:</p>
<ul>
<li class="">which methods are <strong>reachable</strong>, meaning they might be invoked at run time;</li>
<li class="">for every call site, which methods it may <strong>actually call</strong>, even when the call is virtual;</li>
<li class="">the <strong>type flows</strong>: which types of objects may flow through each value in the program.</li>
</ul>
<p>Almost all of this is useful to us.
From it, we export, for each method, the fields it reads, the fields it writes, and the methods it may call.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-the-export-plugs-in">Where the export plugs in<a href="https://samuel-malec.github.io/blog/2026/08/17/side-effect-analysis/intro#where-the-export-plugs-in" class="hash-link" aria-label="Direct link to Where the export plugs in" title="Direct link to Where the export plugs in" translate="no">​</a></h3>
<p>We don't need an image, only the analysis.
Conveniently, the Graal repository also packages the points-to analysis as a <em>standalone</em> tool: the same analysis without the image build around it, running on an ordinary JVM against any classpath with a <code>main</code>.
Our exporter is a small Java program that uses it as a library.
It runs the analysis until it reaches its fixpoint, then walks the results and writes them out as JSON.
Nothing inside Graal calls the exporter, and nothing has to: once the analysis returns, every fact is final, and we just read them.
Exporting one of our examples takes a few seconds.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="which-graph-we-export">Which graph we export<a href="https://samuel-malec.github.io/blog/2026/08/17/side-effect-analysis/intro#which-graph-we-export" class="hash-link" aria-label="Direct link to Which graph we export" title="Direct link to Which graph we export" translate="no">​</a></h3>
<p>The analysis doesn't work on bytecode directly.
It parses every reachable method into a Graal IR graph and builds its type flows from that graph.
We export that same graph, rebuilt the same way, so that the graph and the facts agree: the callees attached to a call node are the ones the analysis found for exactly that call.</p>
<p>Before building the type flows, the analysis tidies each graph up a little:</p>
<ul>
<li class=""><strong>canonicalization</strong> folds constants and simplifies expressions;</li>
<li class=""><strong>conditional elimination</strong> removes conditions that are already known, mostly repeated null checks;</li>
<li class=""><strong>partial escape analysis</strong> removes allocations of objects that never escape the method.</li>
</ul>
<p>There is no inlining.
The standalone analyzer doesn't inline anything before the analysis (a full Native Image build would inline small methods at this point), and none of the compiler's later phases run.
A call in the source is still a call in the graph, and that's what we want: calls are where the interesting effects hide.</p>
<p>Escape analysis has one visible consequence.
An object that never escapes disappears completely, but one that does escape is only created at the point where it escapes, in a form our translation doesn't handle yet.
It can be switched off with <code>-H:-EscapeAnalysisBeforeAnalysis</code>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-we-export">What we export<a href="https://samuel-malec.github.io/blog/2026/08/17/side-effect-analysis/intro#what-we-export" class="hash-link" aria-label="Direct link to What we export" title="Direct link to What we export" translate="no">​</a></h3>
<p>The export has two parts.
The first is the <strong>facts</strong>, for every reachable method, JDK included:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token property">"Examples.countInto(LExamples$Logger;LExamples$Link;)V"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token property">"reads"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token string" style="color:rgb(195, 232, 141)">"Examples$Link.next"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"Examples$Logger.count"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token property">"writes"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token string" style="color:rgb(195, 232, 141)">"Examples$Logger.count"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token property">"callees"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token string" style="color:rgb(195, 232, 141)">"Examples.countInto(LExamples$Logger;LExamples$Link;)V"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token property">"allocates"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token boolean" style="color:rgb(255, 88, 116)">false</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token property">"unknown"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token property">"unresolved"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><br></div></code></pre></div></div>
<p>Methods are named together with their JVM descriptor, so overloads stay apart: our examples have two <code>countInto</code>s, one for a <code>Counter</code> and one for a <code>Logger</code>.
Besides reads, writes and callees, each method says whether it allocates, which of its memory effects can't be named by a field (a monitor, a <code>volatile</code> access, a fence), and which of its calls the analysis couldn't resolve.
The signatures need all three, as we'll see.</p>
<p>The second part is the <strong>graphs</strong> of the program's own methods.
Graal IR is a sea of nodes with no fixed order, so we ask Graal to schedule each graph and export the result: basic blocks with their predecessors and successors, and in every block, its nodes in order.
A call node carries what the analysis found out about it:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token property">"id"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token number" style="color:rgb(247, 140, 108)">27</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> </span><span class="token property">"op"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"InvokeWithException"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> </span><span class="token property">"args"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token number" style="color:rgb(247, 140, 108)">1</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> </span><span class="token number" style="color:rgb(247, 140, 108)">25</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> </span><span class="token property">"bci"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token number" style="color:rgb(247, 140, 108)">19</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> </span><span class="token property">"target"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"Examples.countInto(LExamples$Logger;LExamples$Link;)V"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> </span><span class="token property">"callees"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token string" style="color:rgb(195, 232, 141)">"Examples.countInto(LExamples$Logger;LExamples$Link;)V"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><br></div></code></pre></div></div>
<p>Here <code>1</code> is <code>logger</code> and <code>25</code> is the load of <code>link.next</code>.
Everything after this point, the signatures and the translation, works on this JSON alone.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="things-we-learned-the-hard-way">Things we learned the hard way<a href="https://samuel-malec.github.io/blog/2026/08/17/side-effect-analysis/intro#things-we-learned-the-hard-way" class="hash-link" aria-label="Direct link to Things we learned the hard way" title="Direct link to Things we learned the hard way" translate="no">​</a></h3>
<p>At first we exported facts only for the program's own methods.
Then every constructor, and every method that creates an object, looked like it could touch anything, because every constructor calls <code>Object.&lt;init&gt;</code>, which had no facts.
So now every reachable method gets facts, and the JDK's fields go into the rest of memory.</p>
<p>The analysis records field accesses in its type flows, but only for the kinds of values it tracks, and <code>float</code> and <code>double</code> aren't among them.
So the exporter also reads field loads and stores off each graph; otherwise a method that writes a <code>double</code> field would look like it writes nothing at all.</p>
<p>And the analysis believes what it sees.
<code>System.out</code> is set by native code, so as far as the analysis can tell, it is always <code>null</code>.
It concludes that every <code>System.out.println</code> throws, and treats the code after it as unreachable.
That's why the <code>println</code>s in our examples come last.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="heap-modelling--partitioning">Heap Modelling &amp; partitioning<a href="https://samuel-malec.github.io/blog/2026/08/17/side-effect-analysis/intro#heap-modelling--partitioning" class="hash-link" aria-label="Direct link to Heap Modelling &amp; partitioning" title="Direct link to Heap Modelling &amp; partitioning" translate="no">​</a></h2>
<p>The simplest heap model is the one from the start of this post: a single <code>heap</code> token, threaded through every load, every store and every call.
It is trivially sound, because nothing can ever be reordered, and it is exactly as pessimistic as that sounds.
Every memory operation sits on one long chain, and every call is ordered against everything, which is more or less where Graal stands whenever it meets a call.
To do better, we cut the heap into pieces.</p>
<p>Cutting is only safe if the pieces really are separate.
A partitioning is <strong>sound</strong> when every memory location belongs to <em>exactly one</em> partition.
At least one, so that no store slips past the tokens unnoticed.
And at most one, so that two operations on the same location can never end up on two different chains, where they would look independent and could be swapped.</p>
<p>So how do we cut?
The overview split the heap by class (<code>counter_heap</code>, <code>logger_heap</code>).
The implementation goes one step finer and gives <strong>every field of the program its own partition</strong>, named by its declaring class and name: <code>Counter.value</code>, <code>Counter.other</code>, <code>Logger.count</code>.
Fields are attractive because Java's type system already guarantees they are disjoint: <code>counter.value</code> and <code>logger.count</code> are different memory no matter which objects <code>counter</code> and <code>logger</code> point to.
We don't have to ask the points-to analysis to know that, so we can't be misled by it either.
(The ways around the type system, such as <code>Unsafe</code>, JNI and reflection, end up in code we treat as touching everything, as we'll see shortly.)
The price is that all <code>Counter</code> objects share one partition: stores to <code>a.value</code> and <code>b.value</code> stay ordered even when <code>a</code> and <code>b</code> are different objects.
Splitting further, say by allocation site, would make soundness depend on the points-to analysis being right.
That's a story for another post.</p>
<p>Arrays get one partition per element kind: <code>int[]</code>, <code>long[]</code>, <code>char[]</code> and so on, since an <code>int</code> element never lives in a <code>long[]</code>.
There are two wrinkles.
<code>boolean[]</code> and <code>byte[]</code> share a partition, because the JVM accesses both with the same instructions (<code>baload</code> and <code>bastore</code>).
And all arrays of references share a single <code>Object[]</code> partition, because Java arrays are covariant: a <code>String[]</code> can be passed around as an <code>Object[]</code>, so a store through an <code>Object[]</code> may well land in a <code>String[]</code>.</p>
<p>Everything else goes into one last partition, <strong>the rest of memory</strong>, written <code>*</code>.
It holds the JDK's own fields (there are thousands of them, and what we care about is the program's), plus memory that no field names at all, like the allocator's bookkeeping.
That's why an allocation touches <code>*</code>.</p>
<p>Here is the dead store from the SSU section again, this time as real code:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">static void deadStoreOtherField(Counter counter) {</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    counter.value = 1;   // dead</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    counter.other = 7;</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    counter.value = 2;</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>With a single heap token, all three stores form one chain, and the heap produced by the first store is consumed by the store to <code>other</code>, which doesn't overwrite <code>value</code>.
With a token per field, the chains come apart:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">v1 ← store(v0, counter.value, 1)</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">o1 ← store(o0, counter.other, 7)</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">v2 ← store(v1, counter.value, 2)</span><br></div></code></pre></div></div>
<p>The only use of <code>v1</code> is another store to the same field, so nothing can ever observe the <code>1</code>: the first store is dead.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="computing-method-signatures">Computing method signatures<a href="https://samuel-malec.github.io/blog/2026/08/17/side-effect-analysis/intro#computing-method-signatures" class="hash-link" aria-label="Direct link to Computing method signatures" title="Direct link to Computing method signatures" translate="no">​</a></h3>
<p>Loads and stores know their partition from the field they access.
Calls are the interesting part.
A call has to take the token of every partition the callee <em>might</em> touch, including everything the callee calls in turn.
That set of partitions is the method's <strong>signature</strong>, and in Cthulhu it becomes the function's type: one heap token in and one out for each partition in the signature.</p>
<p>We compute signatures from the exported facts.
Every method starts with the partitions its own field and array accesses touch.
Then we keep adding the signatures of its callees, until nothing changes anymore.
This is where the points-to analysis comes in after all: it provides the call graph, so a virtual call contributes every method it may dispatch to.
Recursion is no problem.
The sets only ever grow and there are finitely many partitions, so the iteration has to stop.</p>
<p>Take a recursive method that counts the links of a list into a logger:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">static void countInto(Logger logger, Link link) {</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    if (link != null) {</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        logger.count++;</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        countInto(logger, link.next);</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>It reads <code>Link.next</code> and reads and writes <code>Logger.count</code>, and its recursive call adds nothing new, so its signature is exactly those two partitions.
A caller passes it those two tokens and nothing else:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">static int forwardAcrossWritingCall(Counter counter, Logger logger, Link list) {</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    counter.value = 5;</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    countInto(logger, list);</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    return counter.value;</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">}</span><br></div></code></pre></div></div>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">v1     ← store(v0, counter.value, 5)</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">n1, l1 ← countInto(n0, l0, logger, list)   -- Link.next, Logger.count</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">x, v2  ← load(v1, counter.value)</span><br></div></code></pre></div></div>
<p>The load takes <code>v1</code>, which comes straight from the store, so it must read <code>5</code>.
That's store-to-load forwarding, read directly off the data flow.
With one heap token, the call would sit between the store and the load, and we would have to load again.</p>
<p>Sometimes we simply don't know what a method does.
It may call a native method, which has no bytecode and therefore no facts.
The points-to analysis may have found no target for a call.
Or the method may do something the facts can't express in terms of fields: enter a monitor, access a <code>volatile</code> field, issue a memory fence, or poke at memory through <code>Unsafe</code>.
Monitors and volatiles don't write any particular field, but they order memory as other threads see it, so they have to stay ordered against everything.
In all of these cases the method <strong>touches everything</strong>: it takes every token, and so does every method that calls it, directly or not.
Unknown effects are never assumed absent.
That keeps the signatures sound, though not always precise.
And the precision is sobering: in a small test program whose reachable code, JDK included, has 1,734 methods, 1,034 of them still touch everything, mostly because somewhere down the call graph they reach a native method.
Teaching the analysis what common natives actually do is high on the list.</p>]]></content:encoded>
        </item>
    </channel>
</rss>