Notes
2025/07/06

RC week 7: introduction to live reloading with replay semantics

Most modern software is developed by writing code in text files, running and testing the code, then stopping the running code to modify the text files, in a continuous write-run-stop cycle. Minimizing the time it takes to run code can greatly improve the overall development speed, for example by using a language that is fast to compile. But even in the absence of long compile times, rebuilding the state during each cycle can have a large impact on the iteration speed, especially in interactive programs that might depend on a lot of user input, where it quickly becomes tedious to rerun the same program over and over again.

Live reloading code (sometimes known as hot reloading) is an alternative approach that keeps some or all of the program state alive between modifications of the code, so that the modified code can reuse previously computed results or user input without having to run the program from the start. Existing approaches generally fall into two categories:

Given that fast feedback cycles are generally desirable when developing software, one might ask why most languages only provide limited support for live reloading in the form of libraries and frameworks, if they provide support for it at all. Why do we not see more adoption of live reloading at the language level? Going further, why are write-run-stop cycles still the norm in software development, instead of being able to go from empty source file to fully working software without ever stopping a running program?

One answer is that the issue of stale state becomes much harder to solve in languages that support live reloading at the language level: Since a new version of a file or module might modify existing functions that have been used by other parts of the code, the live state of the system could contain data that is impossible to generate with the current version of the code and yet persists after a live reload. If the system were to be restarted from scratch at this point, the current version of the code would never be able to reach the same live state. In systems with unrestricted live reload, the textual code in the source files thus stops being the single source of truth, because the live state of the system might contain and depend on older versions with no counterparts in the source files. As a result, programmers have to actively pay attention not just to the source files but also to the code in the live state of the system and how it is being modified, which considerably complicates the mental model of the system as a whole.

A good example are Lisp systems, which traditionally deemphasize source files in favor of REPL-driven development. While experienced Lisp programmers often have a good mental model of when the current live session may end up containing stale state (and thus require a restart), novice programmers can quickly end up in a state that works in the current session, only to break due to missing function definitions (or definitions in the wrong order) when the program is restarted.

What would it take for a language to support live reloading at the language level in such a way that entire programs can be developed starting from an empty source file, without ever having to restart the live system and without ever ending up with stale state? Such an approach would exhibit replay semantics: Semantics that guarantee that no matter when and what code is reloaded, the live state of the system can always be reached by replaying the live session from the start, by only using the most recently loaded version of the code.

A guessing game example

Here's an example of how a live reload session could look like. How exactly the effects (such as random(), read_num_stdin(), print()) are handled will not be discussed here, as this is where different live reload systems will differ.

Let's start with an empty file for a guessing game:

// guess_number (v0)

At first, the main logic of a guessing game is sketched out:

// guess_number (v1)
const secret = random();
const guess = read_num_stdin();
if (guess < secret) {
    print("<");
} else if (guess > secret) {
    print(">");
} else {
    print("success");
}

This only allows a single guess, so let's wrap everything in a loop:

// guess_number (v2)
while (true) {
    const secret = random();
    const guess = read_num_stdin();
    if (guess < secret) {
        print("<");
    } else if (guess > secret) {
        print(">");
    } else {
        print("success");
    }
}

Oops, we made a mistake, the random() function should only be called once:

// guess_number (v3)
const secret = random();
while (true) {
    const guess = read_num_stdin();
    if (guess < secret) {
        print("<");
    } else if (guess > secret) {
        print(">");
    } else {
        print("success");
    }
}

Better. Now we can improve the printed messages a bit:

// guess_number (v4)
const secret = random();
while (true) {
    const guess = read_num_stdin();
    if (guess < secret) {
        print("Your guess is < my number.");
    } else if (guess > secret) {
        print("Your guess is > my number.");
    } else {
        print("You guessed my number!");
    }
}

Let's split this into two files and communicate between them using effect handlers:

// guess_number (v5)
const secret = random();
let attempt = 1;
while (true) {
    try {
        exec("guess_inner.js");
    } handle (get_secret()) {
        resume(secret);
    } handle (get_attempt()) {
        resume(attempt);
    }
    attempt += 1;
}

// guess_inner (v0)
const guess = read_num_stdin();
print("Attempt " + attempt);
if (guess < secret) {
    print("Your guess is < my number.");
} else if (guess > secret) {
    print("Your guess is > my number.");
} else {
    print("You guessed my number!");
}

As a last step, let's only print the message starting with the second attempt:

// guess_inner (v1)
const guess = read_num_stdin();
if (attempt > 1) {
    print("Attempt " + attempt);
}
if (guess < secret) {
    print("Your guess is < my number.");
} else if (guess > secret) {
    print("Your guess is > my number.");
} else {
    print("You guessed my number!");
}