Thicket data repository for the EEG
1{
2 "id": "https://lucasma8795.github.io/blog/2025/07/04/effects-scheduling-w01",
3 "title": "Effects-based scheduling for the OCaml compiler - w01",
4 "link": "https://lucasma8795.github.io/blog/2025/07/04/effects-scheduling-w01.html",
5 "updated": "2025-07-04T08:00:00",
6 "published": "2025-07-04T08:00:00",
7 "summary": "This is a series of blog posts documenting my progress for an internship at the University of Cambridge. This project explores the potential of using OCaml\u2019s effect handlers and domains in place of the current separate build system (dune, make) to self-schedule compilation of missing dependencies on-the-fly.",
8 "content": "<p>This is a series of blog posts documenting my progress for an internship at the University of Cambridge. This project explores the potential of using OCaml\u2019s <a href=\"https://ocaml.org/manual/5.3/effects.html\">effect handlers</a> and <a href=\"https://ocaml.org/manual/5.3/parallelism.html\">domains</a> in place of the current separate build system (dune, make) to self-schedule compilation of missing dependencies on-the-fly.</p>\n\n<p>My knowledge with functional programming, at this point, basically only came from the <a href=\"https://www.cl.cam.ac.uk/teaching/2425/FoundsCS/\">CST 1A Foundations course</a>. To catch up, much of the first few days were spent studying the <a href=\"https://ocaml.org/manual/5.3/effects.html\">OCaml effect handler</a>, and the rest were spent poking around in the OCaml compiler. Here is what I\u2019ve picked up so far:</p>\n\n<h3>Continuations</h3>\n\n<p>A <a href=\"https://en.wikipedia.org/wiki/First-class_citizen\">first-class</a> continuation <code>k</code>, informally, is a callable that represents \u201cthe rest of the computation\u201d, held at a given point in execution. In other words, it is a snapshot of the control flow at a given moment. This is made explicit in the <a href=\"https://en.wikipedia.org/wiki/Continuation-passing_style\">continuation-passing style (CPS)</a> of a program, where control is passed explicitly in the form of continuations <code>k : 'a -> unit</code>, where <code>'a</code> is the type of an intermediate result:</p>\n\n<div><div><pre><code><span>let</span> <span>eq</span> <span>x</span> <span>y</span> <span>k</span> <span>=</span> <span>k</span> <span>(</span><span>x</span> <span>=</span> <span>y</span><span>)</span>\n<span>let</span> <span>sub</span> <span>x</span> <span>y</span> <span>k</span> <span>=</span> <span>k</span> <span>(</span><span>x</span> <span>-</span> <span>y</span><span>)</span>\n<span>let</span> <span>mul</span> <span>x</span> <span>y</span> <span>k</span> <span>=</span> <span>k</span> <span>(</span><span>x</span> <span>*</span> <span>y</span><span>)</span>\n\n<span>let</span> <span>rec</span> <span>factorial</span> <span>n</span> <span>k</span> <span>=</span>\n <span>eq</span> <span>n</span> <span>0</span> <span>(</span><span>fun</span> <span>b</span> <span>-></span>\n <span>if</span> <span>b</span> <span>then</span>\n <span>k</span> <span>1</span>\n <span>else</span>\n <span>sub</span> <span>n</span> <span>1</span> <span>(</span><span>fun</span> <span>m</span> <span>-></span>\n <span>factorial</span> <span>m</span> <span>(</span><span>fun</span> <span>x</span> <span>-></span>\n <span>mul</span> <span>n</span> <span>x</span> <span>k</span><span>)))</span>\n\n<span>(* 120 should appear in stdout *)</span>\n<span>factorial</span> <span>5</span> <span>(</span><span>fun</span> <span>ret</span> <span>-></span> <span>Printf</span><span>.</span><span>printf</span> <span>\"%d</span><span>\\n</span><span>\"</span> <span>ret</span><span>)</span>\n</code></pre></div></div>\n\n<p>This is somewhat analogous to <code>setjmp</code>/<code>longjmp</code> in C.</p>\n\n<p>(side note: notice that in CPS, all calls must be tail-calls!)</p>\n\n<h3>OCaml algebraic effect handlers</h3>\n\n<p><em>Delimited continuations</em> generalize continuations, in the sense that we now capture the context only up to a delimiter (read: slice of a stack frame). Naturally, unlike continuations, <em>delimited</em> continuations can meaningfully return values, and not just <code>unit</code>.</p>\n\n<p>OCaml (algebraic) effect handlers generalize <a href=\"https://ocaml.org/docs/error-handling\">exception handlers</a>, in the sense that the handler is provided with the delimited continuation of the call site, whereas exceptions do not have access to a \u201ccontinuation mechanism\u201d. Here is a nice example, courtesy of <a href=\"https://github.com/ocaml-multicore/ocaml-effects-tutorial\">this tutorial</a>:</p>\n\n<div><div><pre><code><span>type</span> <span>_</span> <span>Effect</span><span>.</span><span>t</span> <span>+=</span> <span>Conversion_failure</span> <span>:</span> <span>string</span> <span>-></span> <span>int</span> <span>Effect</span><span>.</span><span>t</span>\n\n<span>let</span> <span>int_of_string</span> <span>l</span> <span>=</span>\n <span>try</span> <span>int_of_string</span> <span>l</span> <span>with</span>\n <span>|</span> <span>Failure</span> <span>_</span> <span>-></span> <span>perform</span> <span>(</span><span>Conversion_failure</span> <span>l</span><span>)</span>\n\n<span>let</span> <span>rec</span> <span>sum_up</span> <span>acc</span> <span>=</span>\n <span>let</span> <span>l</span> <span>=</span> <span>input_line</span> <span>stdin</span> <span>in</span>\n <span>acc</span> <span>:=</span> <span>!</span><span>acc</span> <span>+</span> <span>int_of_string</span> <span>l</span><span>;</span>\n <span>sum_up</span> <span>acc</span>\n\n<span>let</span> <span>()</span> <span>=</span>\n <span>let</span> <span>acc</span> <span>=</span> <span>ref</span> <span>0</span> <span>in</span>\n <span>match_with</span> <span>sum_up</span> <span>acc</span>\n <span>{</span>\n <span>effc</span> <span>=</span> <span>(</span><span>fun</span> <span>(</span><span>type</span> <span>c</span><span>)</span> <span>(</span><span>eff</span><span>:</span> <span>c</span> <span>Effect</span><span>.</span><span>t</span><span>)</span> <span>-></span>\n <span>match</span> <span>eff</span> <span>with</span>\n <span>|</span> <span>Conversion_failure</span> <span>s</span> <span>-></span>\n <span>Some</span> <span>(</span>\n <span>fun</span> <span>(</span><span>k</span><span>:</span> <span>(</span><span>c</span><span>,_</span><span>)</span> <span>continuation</span><span>)</span> <span>-></span> <span>continue</span> <span>k</span> <span>0</span>\n <span>)</span>\n <span>|</span> <span>_</span> <span>-></span> <span>None</span>\n <span>);</span>\n <span>exnc</span> <span>=</span> <span>(</span><span>function</span>\n <span>|</span> <span>End_of_file</span> <span>-></span> <span>Printf</span><span>.</span><span>printf</span> <span>\"Sum is %d</span><span>\\n</span><span>\"</span> <span>!</span><span>acc</span>\n <span>|</span> <span>e</span> <span>-></span> <span>raise</span> <span>e</span>\n <span>);</span>\n <span>retc</span> <span>=</span> <span>fun</span> <span>_</span> <span>-></span> <span>failwith</span> <span>\"impossible\"</span>\n <span>}</span>\n</code></pre></div></div>\n\n<p>Here, <code>match_with f v h</code> runs the computation <code>f v</code> in the given handler <code>h</code>, and handles the effect <code>Conversion_failure</code> when it is invoked (c.f. <code>try</code>/<code>with</code>).</p>\n\n<p>Effects are performed (invoked) with the <code>perform : 'a Effect.t -> 'a</code> primitive (c.f. <code>raise : exn -> 'a</code>), which hands over control flow to the corresponding delimiting effect handler, and the continuation <code>k</code> is resumed with the <code>continue : ('a, 'b) continuation -> 'a -> 'b</code> primitive. (The type <code>('a, 'b) continuation</code> can be mentally processed as <code>'a -> 'b</code> but used exclusively for effects, as far as I can tell.)</p>\n\n<h4>\u2026how is this type-checked?</h4>\n\n<p>Effects are declared by adding constructors to an <a href=\"https://ocaml.org/manual/5.3/extensiblevariants.html\">extensible variant type</a> defined in the <code>Effect</code> module. In short, extensible variant types are <a href=\"https://dev.realworldocaml.org/variants.html\">variant types</a> which can be extended with new variant constructors at runtime , with the <code>+=</code> operator. As an aside, this is also how one could extend the built-in exception type <code>exn</code>:</p>\n\n<div><div><pre><code><span>type</span> <span>exn</span> <span>+=</span> <span>Invalid_argument</span> <span>of</span> <span>string</span>\n<span>type</span> <span>exn</span> <span>+=</span> <span>Out_of_memory</span>\n</code></pre></div></div>\n\n<p>(there is of course the <code>exception</code> keyword that one should probably use instead!)</p>\n\n<p>Effects are strongly typed, but the effect handler needs to be able to match against multiple effects at once, and since constructors can be added at runtime, the handler must be generic over every possible effect type (and so we must match against the wildcard <code>_</code>). A <code>None</code> return value means to exhibit transparent behaviour (ignore the effect), and allow it to be captured by an effect handler lower down the call stack. (OCaml effects are unchecked, i.e.: it is a runtime error if an effect is ultimately not handled.)</p>\n\n<p>The syntax <code>fun (type c) (eff: c Effect.t) -> ...</code> makes use of <a href=\"https://ocaml.org/manual/5.3/locallyabstract.html\">locally abstract types</a>. This is required for type inference here, when different branches of the pattern-matching have possibly different <code>c</code> (the type of <code>c</code> is \u201clocally collapsed\u201d inside a branch when we have a match). It follows that the scope of <code>c</code> cannot escape a branch.</p>\n\n<p>While reading on this, I <a href=\"https://stackoverflow.com/questions/69144536/what-is-the-difference-between-a-and-type-a-and-when-to-use-each\">came</a> <a href=\"https://discuss.ocaml.org/t/locally-abstract-type-polymorphism-and-function-signature/4523\">across</a> another interesting construct: explicit polymorphism. Turns out, if we write the following in a module interface:</p>\n\n<div><div><pre><code><span>(* foo.mli *)</span>\n<span>val</span> <span>foo</span> <span>:</span> <span>'</span><span>a</span> <span>*</span> <span>'</span><span>b</span> <span>-></span> <span>'</span><span>a</span>\n</code></pre></div></div>\n\n<p>This would mean what one would think it means: for all types <code>'a</code> and <code>'b</code>, <code>foo</code> must be able to take in a 2-tuple of type <code>'a * 'b</code> and return a result of type <code>'a</code>. However, if we instead write the following in a module implementation:</p>\n\n<div><div><pre><code><span>(* bar.ml *)</span>\n<span>let</span> <span>bar</span> <span>:</span> <span>'</span><span>a</span> <span>*</span> <span>'</span><span>b</span> <span>-></span> <span>'</span><span>a</span> <span>=</span> <span>fun</span> <span>(</span><span>x</span><span>,</span><span>y</span><span>)</span> <span>-></span> <span>x</span> <span>+</span> <span>y</span>\n</code></pre></div></div>\n\n<p><code>bar</code> would have the type signature <code>int * int -> int</code>, i.e.: <code>'a</code> and <code>'b</code> are both refined into <code>int</code>. This is because in a module implementation, instead of having implicit universal quantifiers in the type signature as we would normally expect, the type checker interprets this as \u201cthere exists types <code>'a</code> and <code>'b</code> that satisfies the definition\u201d.</p>\n\n<p>To force it to take a polymorphic type signature, we declare the polymorphism explicitly, with:</p>\n\n<div><div><pre><code><span>(* bar.ml *)</span>\n<span>let</span> <span>bar</span> <span>:</span> <span>'</span><span>a</span> <span>'</span><span>b</span><span>.</span> <span>'</span><span>a</span> <span>*</span> <span>'</span><span>b</span> <span>-></span> <span>'</span><span>a</span> <span>=</span> <span>fun</span> <span>(</span><span>x</span><span>,</span><span>y</span><span>)</span> <span>-></span> <span>x</span> <span>+</span> <span>y</span>\n<span>(* read: forall types 'a and 'b, ... *)</span>\n</code></pre></div></div>\n\n<p>which now fails to compile, as expected.</p>\n\n<h4>\u2026surely this has (significant) overhead?</h4>\n\n<p>No. (I hope so!)</p>\n\n<p>OCaml delimited continuations are implemented on top of <em>fibers</em>: small runtime-managed, heap-allocated, dynamically resized call stacks. If we install two effect handlers (corresponding to the two arrows), just before doing a <code>perform</code> in <code>foo</code>, we have the following execution stack:</p>\n\n<div><div><pre><code>+-----+ +-----+ +-----+\n| | | | | |\n| baz |<--| bar |<--| foo |\n| | | | | |\n+-----+ +-----+ +-----+ <- stack_pointer\n</code></pre></div></div>\n\n<p>Suppose that then the effect is performed and being handled in <code>baz</code>. We then have the following stack:</p>\n\n<div><div><pre><code>+-----+ +-----+ +-----+\n| | | | | | +-+\n| baz | | bar |<--| foo |<--|k|\n| | | | | | +-+\n+-----+ <- stack_pointer +-----+ +-----+\n</code></pre></div></div>\n\n<p>The delimited continuation <code>k</code> here is an object on the heap that corresponds to the suspended computation. When the continuation is resumed, the stack is restored to the previous state. (we can safely do this since continuations are <em>one-shot</em> \u2013 they can only be resumed at most once). Notice that it was not necessary to copy any stack frames in the capture and resumption of a continuation; my guess is that they probably have around the same cost as a normal function call?</p>\n\n<h3>So what is it that I\u2019m doing?</h3>\n\n<p>The original project proposal <a href=\"https://anil.recoil.org/ideas/effects-scheduling-ocaml-compiler\">can be found here</a>.</p>\n\n<p>Currently, the compiler is built with an external build system <a href=\"https://en.wikipedia.org/wiki/Make_(software)\">Make</a>. Compilation units naturally form a directed acyclic graph of (immediate) dependencies, and this is generated and saved in a text file <code>.depend</code>. In the Makefile, one can add dependencies to build rules, and thus the build system knows to launch a compiler instance for every compilation unit in dependency order.</p>\n\n<p>The project aims to explore the potential of taking that ability away from the build system, and instead get the OCaml compiler to effectively \u201cdiscover\u201d the dependency order itself, via launching a copy of itself when it discovers that a dependency is missing.</p>\n\n<h3>Progress so far</h3>\n\n<p>I have <a href=\"https://github.com/lucasma8795/ocaml/commit/708d64a9b5b650b9208c8da85e5ffdd95e8b7bab\">hoisted</a> all the logic in <code>driver/Load_path.ml</code> up to <code>main.ml</code> via effects (performing effects in <code>Load_path.ml</code> and installing an effect handler at <code>main.ml</code>. The point of this is to get the relevant path resolution logic from being buried deep inside the compiler, to just below surface level.</p>\n\n<p>I have also successfully performed a <a href=\"https://en.wikipedia.org/wiki/Bootstrapping\">bootstrap cycle</a>, where one builds a compiler with a previously stable version of itself.</p>\n\n<p>The logical next step would be to experiment with code that launches a copy of the compiler whenever a dependency has not been compiled, and eventually merge that with my existing code\u2026</p>",
9 "content_type": "html",
10 "author": {
11 "name": "",
12 "email": null,
13 "uri": null
14 },
15 "categories": [
16 "ocaml-effects-scheduling"
17 ],
18 "source": "https://lucasma8795.github.io/blog/feed/ocaml-effects-scheduling.xml"
19}