Incremental Computing by Differential Execution
Abstract
Incremental computing offers the potential for significant performance gains by efficiently updating computations in response to changing data. However, traditional approaches are either problem-specific or use an inefficient all-or-nothing strategy of rerunning affected computations entirely. This paper presents differential semantics, a novel approach that directly embeds the propagation of changes into the semantics of a general-purpose programming language. Given a precise description of input changes, differential semantics rules define how these changes are tracked and propagated through core language constructs like assignments, conditionals, and loops to produce corresponding output changes. We formalize differential semantics and verify key properties, including correctness, using the Rocq proof assistant. We also develop and formally prove a set of optimizations, particularly for loop handling, that enable asymptotic performance improvements. An implementation of the semantics as a differential interpreter achieves order-of-magnitude speedups over recomputation on the Bellman-Ford shortest path algorithm.
Keywords and phrases:
Incremental computing, differential semantics, programming language design, formal verification, big-step semanticsCopyright and License:
![[Uncaptioned image]](x1.png)
2012 ACM Subject Classification:
Theory of computation Operational semantics ; Software and its engineering Formal language definitions ; Software and its engineering Formal methods ; Software and its engineering Incremental compilersFunding:
This work is supported by ERC grant AutoInc (https://doi.org/10.3030/101125325).Supplementary Material:
Software (ECOOP 2025 Artifact Evaluation approved artifact): https://doi.org/10.4230/DARTS.11.2.13Editors:
Jonathan Aldrich and Alexandra SilvaSeries and Publisher:

1 Introduction
The field of incremental computing aims to develop programming abstractions for describing incremental computations: Computations that react to changes of their inputs efficiently. Most existing approaches follow the idea of selective recomputing [22]: They track which subcomputations are affected by an input change transitively and rerun these subcomputations when their up-to-date result is needed [5, 13]. However, this is an all-or-nothing approach: If a subcomputation is affected in any way, it has to be rerun. For example, consider a grammar checker that processes a large text document represented as a character string. Any change to the text will trigger significant recomputations. Better incremental computing must exploit how the input was changed, not just if it was changed.
In this paper, we propose incremental computing through differential execution. Here and in the remainder of this paper, we use differential to mean “dealing with differences”. Hence, differential execution means to execute a program given a description of how its inputs are changed. We can exploit these change descriptions in two ways. First, we can use dedicated execution rules that react to input changes directly. For example, when the iteration count of a loop is increased, it is sometimes sufficient to execute the extra loop iterations without repeating the previous ones. Second, we can integrate change-processing language primitives, which are often asymptotically faster than their non-incremental counterpart. For example, we can integrate differential operators from relational algebra, as known from live view maintenance in database systems. This paper focuses on the first aspect: How to provide differential execution rules that exploit how inputs have changed.
We study differential execution based on the big-step operational semantics of a small imperative language that features variable assignments, loops, and conditionals. We explain how to incrementalize these features by developing a differential big-step semantics that reacts to changes in the initial variable assignment. Specifically, while executes statement in store , we define a differential semantics that executes based on the store change . We have implemented our formal model of differential execution in Rocq and use it to reason about the differential behavior of programs. First, we prove that the differential semantics does not get stuck and yields the same result as a recomputation from scratch:
And second, we can use the formal semantics to justify a number of important optimizations. For example, we prove that the differential semantics may skip statements that do not read changed variables from , and we develop provably correct optimizations for the differential execution of loops and conditionals. Compared to prior work on the incremental lambda calculus [9], which also processes change descriptions, we are the first to consider imperative language features and use a completely different methodology: differential semantics instead of type-driven program derivation targeting a standard semantics.
While this paper focuses on the formal development, we also want to demonstrate that the differential semantics enables efficient incremental computations. Unfortunately, a direct implementation of the differential semantics in an interpreter is inefficient because the semantics lacks caching of previously computed information. In particular, the differential semantics expects the original store as input, which has to be computed for each statement unless we cache it. Therefore, we develop a differential interpreter based on the differential semantics, but incorporating caching and some verified optimizations. We demonstrate the efficiency of our differential interpreter empirically based on an imperative implementation of the Bellman-Ford shortest-path algorithm. Given changes to the initial edge weights, our differential interpreter computes updates in the shortest paths orders of magnitude faster than a recomputation could. The differential interpreter is implemented in Scala 3 and available open-source.111https://gitlab.rlp.net/plmz/artifacts/autoinc-interp-implementation-ecoop25
In summary, this paper makes the following contributions:
-
We define a static typing discipline for changes of a language’s values, including typed patching and diffing (Section 3).
-
We introduce the approach of differential execution based on the differential big-step semantics of an imperative language. We prove that the differential semantics preserves types, makes progress, and yields the correct result (Section 4).
-
We propose, develop, and verify optimizations for differential execution that skip unaffected statements and improve the running time of loops and conditionals asymptotically (Section 5).
2 Differential Execution: Goals and Challenges
An incremental computation reacts to input changes and derives output changes . How can we automatically provide given the original function ?
In this work, we assume is given through its source code , such that when . Our goal is to define a differential semantics that can be used to process changes of the program input. Note that only the input can change; the source code is not subject to change in this work. We can thus define when , since we will see that the differential semantics is deterministic. The challenge of course is to define the differential semantics such that it is not only correct but also has efficient incremental performance when reacting to input changes. In this section, we motivate our approach through examples.
2.1 Differential execution for epidemiological models
In this paper, we explore differential execution for a statement-based imperative programming language called Imp. Imp features numeric and Boolean values that can be used in assignments, conditionals, and repeat loops, which iterate a loop body a fixed number of times
We use a big-step reduction semantics that takes a program and an initial store as input, and yields an output store . We can use this language to compute epidemiological models.
Epidemiological models are essential tools for understanding the spread of infectious diseases and informing public health interventions [15]. For example, consider a simplified model that simulates disease spread over a fixed number of days:
We can now predict how a disease spreads in days given an initial infectious population and assuming a transmissibility of 1(a) shows the content of the store after iteration of the loop (we omit n and because they are constant). This constitutes the initial, non-incremental run of the program. Note that shows the initial state of the model and is the final prediction.
When epidemiologists revise the initial parameters of the model, we want to update our prediction efficiently. In the following, we discuss three change scenarios. First, consider we want to change the initial infected population from to . The differential store in 1(b) models this change. Our goal is to determine how the final prediction changes, that is, what is . To this end, we propose the use of differential execution: Essentially, we rerun the program but only consider and compute changes. Starting from , differential execution of the loop body computes , showing how and are affected by the initial change of . Differential execution continues to iterate the loop body until we reach . We observe that 5 additional infections on day 0 lead to 68 additional infections on day 6.
Providing the correct differential result is easy: Reexecute the program and compute the outputs’ difference. The challenge for differential execution is to compute output changes efficiently by avoiding recomputations. For example, an increase of in does not affect , which is why does not occur in 1(b). Indeed, as we prove in Section 5, statements that do not read changed variables can be skipped during differential execution. It is shortcuts like this that we need to discover to make differential execution efficient.
For example, consider epidemiologists now want to know what happens after instead of days. We encode this change in of 1(c). Note that we assume the prior change of remains in place; changes occur in sequence and have to be undone explicitly if so desired. How can differential execution handle the change of efficiently? In general, it is necessary to repeat each loop iteration to ensure input changes are propagated correctly. However, in our case, only determines the number of iterations, but it is not used inside the loop body. We prove in Section 5 that in this case we can skip all previous iterations and directly continue with the new ones. Since iterations 7, 8, and 9 have not been executed before, we have to compute the resulting stores from scratch. The final result is then defined by . In general, when the original loop count is increased by , we only need to perform instead of iterations, which yields significant speedups when .
Lastly, consider epidemiologists want to rollback from to . This is a case where incremental computing has to trade-off memory and running time. If we still have cached, we can compute by . Otherwise, we may have to do more recomputation. In our study of a formal semantics for differential execution, we ignore all concerns regarding caching. Later, in Section 6, we discuss how we implemented the formal semantics in an efficient interpreter and what caching strategies we considered.
2.2 Branch switching and worst-case complexity
A particular challenge for all kinds of incremental computing is what we call branch switching. A branch switch occurs when a change to the input data alters the control flow, causing a different branch of a conditional to be executed compared to the original run. For a simple example, consider this extreme case:
When x changes from negative to positive, the entire behavior of the program shifts: Instead of running foo, we end up running bar. This has two important consequences. First, the worst-case complexity of differential execution is at best equal to regular execution. Second, we must retain (or be able to reconstruct) the original state that is valid at the beginning of a branch. This state is necessary when a branch switch occurs, as input for executing the other branch. This indicates a lower bound on the memory necessary for differential execution if we want to avoid reconstructing previously seen states.
Does this mean that all hope is lost? No, not at all. Many conditionals do not amount to a reconfiguration of the program, but have a rather limited scope. For example, an improved epidemiological model may consider emergency measures, which are active when infections exceed a threshold of 40 patients:
When we change the input variable , this can now affect the control flow and lead to branch switching. However, the cost of re-executing one of the branches is limited in this case, and only results in different subsequent changes to . One interesting issue that persists is that branch switching requires diffing between the original and the new state to determine the subsequent changes. In Section 5, we present an optimization that avoids this cost when branches execute similar code.
3 Values and Changes
While regular program execution operates on values, differential execution operates on changes. That is, changes are first-class objects in differential execution. Since the definition of changes is language-specific, we first introduce general rules for how to design well-behaved changes for a statically typed base language. We then define specific changes for our imperative language Imp.
3.1 Change validity, patching, and diffing
Changes must support two fundamental operations: diffing to produce changes from values (), and patching to apply changes to values (. Patching and diffing should act as inverses: and . These laws are elemental for differential execution as they justify optimizations that avoid redundant work.
However, handling changes correctly is subtle and it is easy to break the second property. For example, consider and for the naturals with . Then,
The problem is that we tried to patch with , which is invalid because does not yield a natural number, and defaulting to contradicts our inversion properties. The same problem occurs for other data types, such as strings, sets, and lists. For example, we get . Again, we tried to apply a patch that has an undefined behavior, and defaulting to the empty string breaks our inversion laws. Therefore, we follow the incremental lambda calculus [9] and restrict patching to valid changes. But rather than using Coq’s dependent typing, we rely on a dedicated typing judgment . Patching and diffing then must satisfy the following properties:
Patching is only defined when and yields a value of the same type as . Diffing is defined for any and of the same type, and it yields a valid change that can be applied to . When defining differential execution, it is our responsibility to define changes , validity , patching , and diffing such that these typing rules hold.
On top of these typing rules for patching and diffing, we can now stipulate the inversion properties we require.
Property 3.1 (Patch-Diff Inversion).
For any well-typed values and , patching inverts diffing: .
Property 3.2 (Diff-Patch Inversion).
For any value and valid change , diffing inverts patching: .
Note how the latter property uses equivalence on changes rather than equality. This is due to the fact that changes often have non-unique representations and diffing might choose any equivalent representation. For example, and are equivalent but not equal. In general, iff for all , and .
The above properties justify our use of noc, representing no change. Specifically, for any , we have and therefore . In practice, it is usually easy to decide if a given is equivalent to noc.
3.2 Numeric and Boolean changes
Our imperative language Imp uses unsigned integers and Boolean values. We define changes for each type:
Numeric changes consist of no change (), increasing (), and decreasing (), where is any natural number. Boolean changes consist of no change () and negation (neg). Changes are only valid for an appropriately typed value, and is only valid for numbers larger or equal to :
Whenever , we can patch while retaining its type as required by T-Patch. Moreover, differencing ensures as required by V-Diff.
We have modeled these changes and their operations in Rocq, where we proved they satisfy the respective typing rules as well as patch-diff and diff-patch inversion.
3.3 Lifting to stores
We have established how patching and diffing operate on individual values, but our imperative programs range over variables that map to values. Therefore, we need to lift changes and their operations from single values to entire stores that map variables to values. To this end, we define differential stores , which map variables to value changes. Differential stores must satisfy the same properties as differential values: well-typed patching and diffing according to T-Patch and V-Diff, such that patch-diff and diff-patch inversion hold.
We start off by requiring stores to be well-typed given a store type . We define store typing and differential store validity pointwise:
Then the following definitions of patching and diffing satisfy T-Patch and V-Diff:
Patch-diff inversion holds because
and conversely diff-patch inversion is due to
Again, we have modeled differential stores in Rocq and proved the above properties formally there. This shows how to lift changes from values to stores, which provides the foundation for our differential semantics. The Rocq formalization is provided at
4 Differential Semantics: A Foundation for Incremental Computing
Incremental computing is all about eliminating redundant work in the face of input changes. In the pursuit of efficiency, incremental computing tries to cut as many corners as possible while retaining correctness. Our differential style of incremental computing is particularly susceptible to correctness errors, because we need to capture the precise difference of each computation output. In this section, we introduce the idea of differential semantics to capture the incremental behavior of a program. Specifically, we develop a differential semantics for the imperative language Imp from Section 2 with expressions and statements.
A differential semantics establishes a sound baseline for incremental computing, but it is not usually optimal. Instead, we separately introduce optimizations for the efficient incremental execution of programs and prove them correct with respect to the baseline differential semantics. To support the modular definition and verification of optimizations, we formulate the differential semantics compositionally for each language construct. But first, let us review the standard big-step semantics of our imperative language Imp.
4.1 Standard big-step semantics of Imp
We already introduced the syntax of Imp expressions, statements, and stores in Section 2. Figure 2 shows the standard semantics for Imp using two judgments. For expressions, we write to denote that expression evaluates to value under store . Expressions can read but cannot change the content of the store. The last reduction rule for expressions handles binary expressions . Imp supports a few binary operators , and we assume that follows the standard arithmetic interpretation of these operators.
For statements, we write to denote that statement executes under store , yielding a possibly updated store . In these rules, the notation represents the store obtained from by updating the mapping of variable to value , while leaving all other mappings unchanged. For repeat loops, we skip the body when the iteration count is zero. Otherwise, we recursively evaluate the loop with a decremented count, followed by running the body. We opted for this left-recursive loop unrolling to simplify some of the proofs.
4.2 Differential semantics for Imp expressions
A differential semantics describes how a program’s output changes in reaction to input changes. The inputs of our Imp programs are stores . We have described store changes in Subsection 3.3. The output of an Imp program is also a store, but we first only consider Imp expressions. An Imp expression computes a value , hence the differential semantics for Imp expressions computes a value change . To this end, we define a differential reduction relation to compute how ’s output changes:
The first rules handle an expression that is a Boolean or numeric constant. The value of constant expressions can not change, hence the output is a no change (noc). A variable changes in accordance with . In particular, when an input is changed, reading from that input yields its change, which can then be propagated.
The last rule handles binary operators and delegates their differential behavior to dedicated functions . We allow these functions to use the original input values (, ) and their changes (, ). For example, for a differential multiplication , we need to know the original arguments to compute the output change . While our implementation supports various operators, in this paper and the Rocq formalization we focus on establishing the core differential semantics. A complete treatment of differential operators is outside the scope of this paper.
Note that we required the original store during the differential execution of expressions only to recover the original arguments of binary operators. In places like these, we can expect an actual implementation to cache the previous evaluation result of each operand where needed. We highlight opportunities for caching in the reduction rules by putting preconditions in parentheses and slightly shading their background.
4.3 A differential semantics for Imp statements
We define the differential semantics for statements using the relation
which executes statement under differential store and original store to yield an updated differential store . The inference rules are shown in Figure 3.
Rule D-Assign evaluates the assigned expressions differentially and then updates the differential store. The store update is a destructive update, overwriting the previous change assigned to . Together with rule D-Seq, this realizes change propagation behavior. For example, assume a program with . Rule D-Seq runs the assignments in order. The first assignment sets because . Then the second assignment sets because Note that D-Seq also runs the first statement non-incrementally, but only to obtain , which is needed as input for . The highlighting of this and other preconditions in Figure 3 indicates that the precondition’s result can be retrieved from a cache in an actual implementation.
For conditional statements, we generally distinguish two situations: branch constant and branch switching. A conditional behaves branch-constant if the value of the condition does not change, handled by rules D-IF and D-IF. These rules simply propagate changes to the relevant branch that was selected originally. Rules D-IF and D-IF handle branch switching, which happens when the condition’s value changes. If the original value of the condition was true and now becomes false, then rule D-IF fires and performs branch switching as introduced in Subsection 2.2. Originally, we have executed , but now we need to run . Therefore, we execute from scratch and compute how its output differs from . Rule D-IF is analogous and handles the case when the condition switches from false to true.
Finally, we provide four rules to handle repeat loops. The differential semantics for depends crucially on how the iteration count changes. When the differential evaluation of yields , the iteration count remains the same as in the original execution. In this case, we replay the loop but only do change propagation. If the iteration count is zero (), we simply return the input differential store unchanged, as no iterations need to be replayed. For a positive count (), we unroll the loop recursively followed by a differential run of the loop body . The resulting captures the cumulative effect of differentially executing all iterations.
When the iteration count is decreased (), the process is more complicated. First, we propagate the changes for iterations to obtain . Then we retrieve the original output after iterations and patch it with . This represents the updated output after iterations, but we need to know how it changed compared to iterations. To this end, we compute the difference with , which is the original output after iterations.
Lastly, when the iteration count grows (), we use a two-phase strategy. First, we execute the original iterations differentially to obtain . However, from here on out, we are dealing with iterations that have not occurred before. Therefore, in the second phase, we execute the additional iterations non-incrementally. To this end, we compute the updated output after iterations using and feed it as input for the remaining iterations. Finally, we compute the difference with , which was the original output.
Example.
Consider the following program and stores: with initial store . The original output is . Now consider we apply the following input changes . The example demonstrates how differential execution handles both loop counter changes () and variable changes (). Specifically, here rule D-Rep-Inc is leading the execution and proceeds in the following steps:
-
1.
Execute original 5 iterations differentially: .
-
2.
Compute patched state after 5 iterations: .
-
3.
Execute new iterations (k = 2) from to obtain .
-
4.
Compute differential output: .
4.4 Properties of differential semantics
Our differential semantics aims to replace recomputation with change-based execution. That is, when the original input is changed to , the differential semantics must be able to process directly. For this to work, we need three guarantees:
- Completeness:
-
Whenever a program can execute under the original and the updated , then the differential semantics must derive an output change for .
- Output validity:
-
The output change must be valid for the original output.
- From-scratch consistency
-
Patching the original output with the output changes is consistent with a recomputation starting with .
That is: We can obtain changes using the differential semantics, the changes can be used to patch the original output, and the patched output is correct. We have formalized these properties in Rocq and have a mechanized proof that shows our differential semantics satisfies them. Here, we only present the formal formulation of the properties.
In general, we only make claims about well-typed programs . The typing relation for statements and expressions is standard: variables are global and cannot change type. With this, we can formulate the properties of differential semantics.
Theorem 4.1 (Completeness).
Given a well-typed program , well-typed store , and valid differential store . Whenever and for arbitrary and , then for some .
Proof sketch.
By induction on the typing derivation of statement . For base cases like Assign, we directly construct differential derivations. For compound statements like Seq and If, we apply induction hypotheses to substatements and compose their results. The critical case is Repeat, where we handle changes to the iteration count by carefully tracking execution states for each iteration. Completeness ensures we find output changes . Note that for our Imp language, the original and update execution always succeeds for well-typed programs and stores, but richer languages may include run-time errors and divergence For Imp, completeness ensures that we have defined enough reduction rules to handle all well-typed execution states.
Validity guarantees that differential execution produces changes that are compatible with the values they will be patched with. This ensures we can safely apply all changes produced during execution.
Theorem 4.2 (Output validity).
Given a well-typed program , well-typed store , and valid differential store . Whenever and , then is valid for the original output.
Proof sketch.
By induction on the differential semantics derivation, showing each rule preserves validity when input changes are valid. Critical cases include branch switching and loop optimizations, where the type system ensures changes remain compatible with their target values.
From-scratch consistency is our key correctness property – it proves that differential execution produces the same final results as re-running the program with changed inputs. This means users can trust that differential execution is just an optimization that does not affect program behavior.
Theorem 4.3 (From-Scratch Consistency).
Given a well-typed program , well-typed store , and valid differential store . Whenever and , then .
Proof sketch.
By induction on differential semantics, proving patched original outputs match recomputation results. The key insight relies on our change operations satisfying algebraic properties like patch-diff and diff-patch inversion, ensuring consistency across all language constructs.
We have formally verified all three theorems using the Rocq proof assistant. The complete mechanized proofs are available in the repository referenced in Section 3.
4.5 Mechanized Rocq formalization
We formalized the differential semantics of Imp in Rocq and proved completeness, output validity, and from-scratch consistency. The Rocq code is available open-source.222https://gitlab.rlp.net/plmz/artifacts/autoinc-interp-formalization-ecoop25 The Rocq formalization consists of approximately 1,450 lines of code and was important in shaping our theory of changes. A key challenge emerged when proving the diff-patch inversion property (Property 3.2) introduced in Section 3. While standard equality was sufficient for proving the patch-diff property (Property 3.1), we discovered it was inadequate for the diff-patch case. We needed a custom equivalence relation because multiple differential values (noc, incr 0, decr 0) can represent the same change semantically
The consistency proof (Theorem 4.3) was particularly challenging, especially for the increment case of repeat statements. The complexity of this case revealed that our initial induction hypothesis wasn’t strong enough – an observation we only gained after successfully proving all other cases. This led to a reformulation of the theorem with a stronger induction hypothesis.
In our opinion, for the number of results we proved, our Rocq code is reasonably compact. This was achieved through careful proof automation: we developed custom tactics for common proof patterns and registered them as hints for Rocq’s eauto system. This automation strategy significantly reduced the proof size.
5 Optimizations as Alternative Reduction Rules
To enhance the efficiency of our differential semantics, we introduce several optimizations which allow us to avoid unnecessary computations during execution. We present them as alternative reduction rules within our differential semantics. Each optimization we present is a shortcut: it lets us compute the same result more efficiently. To ensure these shortcuts are correct, we must prove they are admissible. This means that whenever we use an optimization rule to derive a result, we can also derive the same result (perhaps less efficiently) using only the standard rules from Figure 3. Formally, we define admissibility of a rule as follows.
Definition 5.1 (Rule Admissibility).
An optimization rule with premises and conclusion is admissible if for any well-typed statement , whenever holds, there exists a derivation using only standard rules that concludes .
Admissibility ensures optimizations are behavior-preserving shortcuts. In the following sections, we present several optimizations. We have proved the admissibility for each of these rules in Rocq, working only with well-typed programs since well-typedness is essential for ensuring the soundness of our optimizations. We categorize the various optimizations into three main types: short-circuiting optimizations, loop optimizations, and branch switching optimizations, and discuss each in detail.
5.1 Short-Circuiting Rules
Short-circuiting rules enable bypassing differential computations entirely under certain conditions. We begin with a special case where all variables of a store map to no change. Then we generalize this rule to handle programs where only referenced variables are unchanged.
The -Store rule shows that when every variable in the differential store maps to noc, differential execution preserves the input differential store unchanged. This optimization is useful because when no variables have changed at all, we can completely bypass differential execution, avoiding differential computations entirely. The rule is fundamentally sound since a program operating on unchanged inputs must produce unchanged outputs. The premise in the rule establishes that executes successfully in the standard semantics, which is necessary for proving the rule’s admissibility.
Consider the following program:
With the differential store , the rule lets us skip differential execution entirely.
To handle more realistic scenarios where a differential store might contain changes to many variables but a specific program fragment only references unchanged variables, we need to precisely characterize when a fragment is unaffected by changes. We do this through the No-Change relation.
Definition 5.2 (No-Change Relation).
The no-change relation for expressions and statements indicates that under typing context and differential store , all variables referenced in or map to of their respective type in . This relation is defined inductively by the rules in Figure 4.
Using this relation, we formulate our second optimization rule as follows: The No-Change rule generalizes the -Store rule: when no variables referenced in are affected by changes (), any differential execution of must preserve the input differential store. This optimization is particularly valuable as it allows us to skip computations even when some variables in the program have changed, as long as they aren’t referenced in the code being optimized.
Consider our earlier program fragment in a larger context:
With differential store , the No-Change rule lets us skip the first two assignments even though changes elsewhere in the program. This local reasoning about variable usage avoids wasteful expression evaluation – we don’t compute changes that would inevitably be noc.
5.2 Loop Optimization Rules
Loop constructs often dominate execution time in incremental computations. We present three optimization rules for loops, each handling a different scenario where full recomputation can be avoided. We start with loop idempotence, then present rules for handling changes to iteration counts.
The Loop-Idemp rule provides a significant optimization opportunity: when we can prove that a loop body preserves both the store and differential store in a single iteration and there is no change in the number of loop iterations, we can completely skip executing the remaining iterations. This is valuable because without this rule, we would need to execute each iteration even though we know the final result would be unchanged.
Consider a loop with operations that cancel out:
When , we can skip all iterations since each iteration preserves the store through these reversing computations. While this example is deliberately simple, the Loop-Idemp rule becomes particularly powerful with richer language features. For instance, in a language with arrays and objects, this rule would recognize loops that appear to modify data structures but actually preserve their state, allowing us to skip such loops entirely when we can prove that their operations are self-cancelling.
Our two most powerful loop optimizations address scenarios where the loop count changes, but the loop body remains unaffected by changes in the differential store. That is, all variables in the loop are assigned noc in the input differential store. This optimization reduces computational effort by focusing only on the additional iterations introduced by the change in the loop count, thereby avoiding redundant execution of the unchanged loop body.
The Loop-Incr rule handles increases in iteration count. When the loop count increases by iterations and the loop body references no changed variables (), we can optimize by doing the following.
-
Reusing the result of the original execution
-
Computing only the additional iterations from to get
-
Computing the differential results as
The differential output captures how the loop’s output differs after running the additional iterations. With cached results from the original execution, we only compute the new iterations: – precisely the work that must be done. This optimization avoids redoing unchanged iterations while still performing the genuinely new computation. However, when any variable read in the loop body changes (for example, if an array being summed changes), we cannot apply this optimization. This restriction is necessary – changes to variables in the loop body would produce different results in every iteration, invalidating our assumption that earlier iterations remain unchanged and in process invalidating the cached result .
Consider the following example where we accumulate a sum with a variable count:
With , both sum and i are unchanged in the input store, allowing us to safely reuse cached results. After the original 5 iterations, we compute just 2 more iterations with values we haven’t seen before. If instead we had , the change to i would affect every iteration’s computation, forcing us to recompute all iterations with the new initial value.
When all premises remain as in the increment case, but with replacing the increment, we obtain an elegant formulation for the decrement case as shown below.
For decreased iteration counts, the Loop-Decr rule is even more efficient than the increment case. If we have cached the original computation, we have access to both the original output and the intermediate store after the iteration. Then, we can compute the final result directly with just a patch and a diff operation, without any loop execution. This makes the decrement case even more efficient than the increment case, where we still needed to perform additional iterations. Again, this optimization requires , as changes to loop body variables would invalidate our cached states.
Using the same summation example but with decreasing from 5 to 3 and , we don’t need to compute anything new from the normal execution. We can directly use our cached state after 3 iterations to compute the change.
In both the increment and the decrement case, we see that caching the original computation is important for efficiency. We discuss various strategies for caching computations in Section 6.1.
5.3 Branch Switching Rules
One of the main challenges in differential execution is handling branch switches efficiently, as we typically need to execute both branches and compare their stores. Consider a common pattern in imperative programs, where an if-statement contains only assignments to the same variable in both branches:
With branch switching, the standard differential semantics would compute the result by diffing the resulting stores corresponding to the two different branches that are taken. However, since only variable x changes between branches, we can update just one entry instead of the entire store. This targeted update of x based on the expressions’ difference provides orders of magnitude in speedup over store-wide diffing, especially pronounced for large stores. We develop two optimizations here based on this insight.
The If-T-F rule handles the case where a condition switches from true to false. The rule requires both stores to be well-typed and the conditional statement itself to be well-typed. When the conditional expression switches from true to false and both branches are assignments to the same variable , we can directly compute the change to as , where is ’s value in the original store and is ’s value in the patched store.
For our example, suppose was initially true and we have causing a branch switch. Instead of recomputing the entire store, we only need to update the entry corresponding to as . However, if branches modified different variables (e.g., x = z + 1 vs y = z - 1), this optimization cannot apply – the changes aren’t localized to a single variable.
The If-F-T rule handles the symmetric case where the condition switches from false to true. Its structure mirrors the true-to-false case, but computes since we are switching to the first branch.
This optimization significantly improves performance when stores are large, but changes are localized. In our Bellman-Ford implementation (Figure 5), this optimization avoids diffing the entire shortest-path table on each branch switch within the inner loop’s conditional, leading to substantial speedups as shown in Section 7.
6 Implementing a Differential Interpreter
In this section, we describe the implementation of a differential interpreter based on the differential semantics. The implementation is written in Scala 3, is open source and available at https://gitlab.rlp.net/plmz/artifacts/autoinc-interp-implementation-ecoop25.
6.1 Caching to avoid Re-Computation
The differential semantics requires access to previous execution states when handling control flow constructs. Consider an if statement where branch switching occurs, that is, when the condition’s value changes from true to false or vice versa. We reiterate the relevant rule D-IF from Figure 3, highlighting in gray the computations that can be cached from the previous execution:
Applying this rule requires performing new computations while accessing cached results from the previous execution. Specifically, we must differentially evaluate the condition to get neg and execute the new branch , while we can reuse the original condition evaluation result and the result of executing . A similar requirement exists for repeat statements, which need stores from previous iterations to enable differential execution. This dependency on previous states explains why our differential reduction relation takes the previous store as input.
Re-computing previous results and propagating old states throughout differential execution incurs significant performance overhead. To address this, we develop an initializing interpreter that caches execution states. However, uniquely identifying cached states requires more than just statement identifiers, particularly for loops. Consider where executes five times – each iteration operates on a different store and thus requires its own cached state.
We solve this through path-sensitive identifiers that precisely track loop nesting structure. A path starts at root (nesting level 0) and records each enclosing loop’s iteration count. For example, path indicates execution within the first iteration of an inner loop, nested within the second iteration of an outer loop. Our cache maps each execution point to its corresponding program state: , where UID uniquely identifies statements in the program, captures the loop nesting context through iteration counts, and represents the program state we need to cache.
Our caching strategy for conditional statements preserves three key pieces: the store before evaluating the condition, the condition’s evaluated result, and the store after executing the chosen branch. This enables direct access to previously computed states, eliminating redundant recomputation during differential execution.
For repeat statements, we implement two strategies: a basic strategy that caches the initial store, iteration count, and final store, and an enhanced strategy that additionally caches intermediate states after each iteration. The enhanced strategy enables efficient implementation of the decrement optimization (Section 5.2) by providing direct access to any intermediate store from the original execution, eliminating the need for additional computation.
6.2 Additional Language-Features
We have implemented a differential interpreter that supports more constructs than present in the formalization. In addition to integer and boolean values, we also support string values. In the same vein, the language supports more than just the addition operator. It can call arbitrary operators based on an operator interface which requires an init function and an inc function:
The type Change[T] marks a change of value type T. Note that operators themselves can keep track of local state that is required for implementing efficient incremental operators. Additionally, the language enables first-order function calls of user-defined top-level functions. Each function ranges over its own local store.
One major difference is that the language supports a mutable 2-dimensional table ranging over integers which is global to all functions. To this end, the language supports a table-write statement and a table-read expression . Note that we currently assume that the indices cannot change between the initialization and a differential run. This is a restriction we want to address in the future work.
6.3 Applying Optimizations
To enable efficient differential execution, we implemented various optimizations. We describe optimizations in the formalization as overlapping rules, where we decide manually which rule to apply. Our implementation always applies the optimization rule whenever possible. One could imagine that there are multiple applicable optimization rules. This leads to optimization strategies which select the optimization rule to apply at a given point at execution time. We want to investigate optimization strategies in future work.
We implemented the short-circuiting optimizations from Section 5.1 by preemptively computing read/written variables within statements and expressions. Because our language supports a 2-dimensional global table, this analysis extends to tracking table cell accesses. Building on the branch switching optimization from Section 5.3, we handle table operations efficiently:
When branches differ only in their expressions ( vs ), we can avoid expensive table diffing operations by directly computing where and .
7 Case Study: Shortest-Path with Bellman-Ford Algorithm
To evaluate how precise change descriptions enable efficient incremental computation of dynamic algorithms, we implemented the Bellman-Ford single-source shortest-path algorithm [6] in our language (Figure 5). Our implementation represents the graph using an adjacency matrix stored in a global 2-dimensional table, with shortest path weights from source node src to node stored at indices .
7.1 Setup
To evaluate our differential interpreter, we designed a graph structure that exhibits interesting incremental behavior. The graph is a cycle where we can configure its size by setting the number of nodes :
The weight of the back edge is chosen to introduce new shortest paths, ensuring that small input changes can trigger significant output changes. We compute shortest paths starting from node 1 for different values of and evaluate two types of changes:
-
(i)
Adding an edge with weight . This introduces an alternative path that does not affect the shortest path solution.
-
(ii)
Adding six edges forming cross-connections: , , , , , and , each with weight . Despite creating multiple new paths, these additions do not affect the shortest path solution.
We conducted experiments on an Apple M4 Pro chip with 24GB memory running 64-bit OSX 15.1.1 and OpenJDK 23.0.1. Using the JMH benchmarking framework333https://github.com/openjdk/jmh, we performed 5 warmup runs followed by 10 measurement runs for each configuration.
7.2 Results
In Figure 6, we compare the running times of differential interpretation against normal interpretation on the patched input. On the left we show the running times for change (i) and on the right we show them for change (ii). The differential interpreter outperforms normal interpretation for change (i). While Bellman-Ford has a time complexity of , the differential interpreter can skip most iterations because we employ an optimization that skips loop iterations when the changed table entries are not affected by the iteration. Since change (i) modifies just a single table entry without introducing new shortest paths, we can skip almost all iterations, yielding significant running time savings. The same benefits apply to change (ii), though to a lesser degree as fewer iterations are skippable.
A second critical optimization avoids diffing when branch switches occur, significantly reducing the differential interpreter’s running time. Without this optimization, all branch switches for the inner if statement within the nested repeat loops would require diffing the old and new result tables, incurring an cost. Instead, we apply constant-time updates to the current delta table.
To evaluate stability over extended use, we executed 40 consecutive changes (Change (ii)) on graphs of varying sizes (10-150 nodes). We measured performance at five points in this sequence (1, 10, 20, 30, and 40 change), with 5 warmup and 10 measurement runs per point. After filtering outliers using the interquartile range (IQR) method, our analysis confirms that execution times remain consistent across the change sequence for each graph size. The Coefficient of Variation (CV), which measures relative variability as standard deviation divided by mean, is remarkably low (average 8%, maximum 13%), indicating that our differential approach maintains stable performance throughout extended use without degradation. While execution times scale with graph size as expected, the consistent performance within each size category demonstrates that repeated incremental updates do not compromise efficiency, further justifying the one-time initialization cost.
In Figure 7 we compare the running times of the initializing interpreter to normal interpretation on the initial input. On the left we show the running times for change (i) and on the right for change (ii). The graphs show that initialization requires significantly more time than normal interpretation. Normal interpretation takes roughly 1.5s for processing the initial graph where . In contrast, the initialization interpreter requires roughly 15s, a 10x increase. This overhead comes from caching states by inserting entries into hash tables. This overhead is an implementation artifact that we believe can be substantially reduced through engineering improvements and smarter caching strategies.
These results demonstrate that differential interpretation of dynamic algorithms like Bellman-Ford is feasible and can achieve order-of-magnitude speedups, provided we have: 1) optimizations that reduce work by skipping unaffected program fragments, enabled by precise change descriptors, and 2) efficient caching strategies to minimize initialization overhead. Additionally, reducing initialization time remains an important area for improvement.
8 Related Work
Incremental computation has been extensively studied, with various approaches aiming to efficiently update outputs in response to changes in inputs. In this section, we discuss work related to our approach, namely dynamic algorithms, incremental computing by selective recomputing, incremental computing by differential updating, and the incremental lambda calculus.
Dynamic algorithms are designed to handle arbitrary changes to inputs by maintaining data structures that can be efficiently updated [19]. They achieve efficiency through carefully designed update operations that maintain complex invariants specific to each problem domain, as demonstrated in problems like dynamic planar convex hulls [8, 20] and dynamic minimum spanning trees [14, 16]. While these algorithms can offer optimal incremental performance for specific problems, their development requires significant effort and domain expertise [22].
Our approach differs by embedding incrementality into the language semantics rather than relying on specialized data structures. Where dynamic algorithms require manual development of update operations for each problem, our differential semantics provides a systematic approach to deriving incremental behavior for programs written in our imperative language. This language-based approach demonstrates how incremental computation can be treated as a fundamental programming language feature rather than a specialized algorithmic concern.
Selective recomputing techniques aim to reuse previous results by identifying and rerunning only the affected parts of a computation when inputs change [10, 22]. Early methods like function memoization [21, 1] cache function calls and reuse results, but they struggle with programs that have internal state or complex control flow.
Self-adjusting computation [5, 4], representing the state-of-the-art in selective recomputing, introduces sophisticated dependency tracking using dynamic dependency graphs to identify precisely which computations need to be redone. Their change propagation algorithm then selectively recomputes only the affected portions of these dependency graphs. This approach has been enhanced to improve efficiency through data structure-level dependency tracking [3] and extended to handle imperative features [2], but maintains the core mechanism of dependency tracking. Despite these advances, the fundamental dependency tracking approach and its inherent disadvantages remain unchanged. Adapton [13] extends this line of work with demand-driven evaluation and better support for cyclic dependencies, but still faces similar overhead challenges from maintaining complex runtime structures.
Where selective recomputation focuses on identifying which computations to redo through dependency information, our differential semantics takes a fundamentally different approach by directly modeling how changes propagate through program constructs. This direct modeling enables more efficient handling of control flow changes: instead of rebuilding and traversing complex dependency graphs when loop bounds or conditions change, our semantics directly computes the precise differential effect of the modified control flow. The key distinction is that while self-adjusting computation and its extensions determine what to recompute, our differential semantics specifies how changes transform program states through the language semantics itself.
The incremental lambda calculus (ILC) [9, 11] provides a theoretical foundation for incrementalizing purely functional programs through type-directed program transformation. For each function , it derives a derivative function that computes output changes from input changes, executing these derivative programs using standard semantics.
Our approach differs fundamentally from ILC by providing a differential semantics that directly executes programs on changes without requiring program transformation. This distinction has important practical implications – optimizing our differential semantics requires only modifying semantic rules, while ILC must optimize its program transformation process to generate more efficient derivative code. Another obvious difference between our approaches is that while ILC provides powerful techniques for incrementalizing higher-order functional programs, our differential semantics offers direct support for imperative features.
Differential updating originated in databases to maintain materialized views efficiently [7]. For each relational operator, they derive rules that translate changes to input relations into changes to output relations, avoiding full recomputation. View maintenance systems formalize these as delta rules [12] with algebraic properties that ensure correctness: a delta rule applied to input changes must produce the same result as recomputing with updated inputs. This approach has been particularly successful for Datalog, where differential evaluation enables efficient incremental maintenance of recursive views [18].
Our work shares the fundamental insight of deriving change-propagation rules but applies it to programming language constructs rather than relational operators. Like database approaches, we ensure correctness through algebraic properties, but our properties focus on language-level operations rather than relational algebra. This enables incrementalization of general-purpose programs while maintaining the mathematical rigor of differential database techniques.
Differential dataflow [17] extends dataflow systems to support incremental updates through data provenance tracking. While effective for data-parallel computations, it requires programs to be expressed in a restricted dataflow model. Our approach provides similar benefits for general imperative programs through language-level change tracking, making incremental computation accessible to mainstream programming.
9 Conclusion
This paper presents differential semantics, which exploits precise change information rather than using all-or-nothing selective recomputing. Rather than treating incrementality as a separate concern, we embed it directly in the programming language through semantic rules that precisely track and propagate changes. Our key contributions include a type-safe theory of changes that ensures semantic consistency, a differential semantics for an imperative language that handles both data and control flow changes, and a set of verified optimizations that make this approach practical. Through our implementation as a differential interpreter, validated on the Bellman-Ford algorithm, we demonstrate that our approach enables order-of-magnitude speedups compared to recomputation. These results establish that principled language design can make incremental computation both automatic and efficient. Future work could extend these foundations to richer language features like functions and recursion, moving toward general-purpose languages where incrementality is the default rather than a specialized capability.
References
- [1] Martín Abadi, Butler Lampson, and Jean-Jacques Lévy. Analysis and caching of dependencies. In Proceedings of the 23rd ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, pages 83–91, 1996. doi:10.1145/232627.232638.
- [2] U. A. Acar, A. Ahmed, and M. Blume. Imperative self-adjusting computation. SIGPLAN Not., 43(1):309–322, 2008.
- [3] U. A. Acar, G. Blelloch, R. Ley-Wild, K. Tangwongsan, and D. Turkoglu. Traceable data types for self-adjusting computation. SIGPLAN Not., 45(6):483–496, 2010. doi:10.1145/1806596.1806650.
- [4] Umut A. Acar. Self-Adjusting Computation. PhD thesis, Carnegie Mellon University, 2005.
- [5] Umut A. Acar, Guy E. Blelloch, and Robert Harper. Adaptive functional programming. In Proceedings of the 29th ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, pages 247–259, 2002. doi:10.1145/503272.503296.
- [6] Richard Bellman. On a routing problem. Quarterly of applied mathematics, 16(1):87–90, 1958.
- [7] John A. Blakeley, Per-Åke Larson, and Frank Wm. Tompa. Efficiently updating materialized views. In Proceedings of the 1986 ACM SIGMOD International Conference on Management of Data, pages 61–71, 1986.
- [8] Gerth Stølting Brodal and Riko Jacob. Dynamic planar convex hull. In Proceedings of the 43rd Annual Symposium on Foundations of Computer Science, pages 617–626, 2002. doi:10.1109/SFCS.2002.1181985.
- [9] Yan Cai, Paolo G. Giarrusso, Tillmann Rendel, and Klaus Ostermann. A theory of changes for higher-order languages: Incrementalizing lambda-calculi by static differentiation. In Proceedings of the 35th ACM SIGPLAN Conference on Programming Language Design and Implementation, pages 145–155, 2014.
- [10] Allan Demers, Thomas Reps, and Tim Teitelbaum. Incremental evaluation for attribute grammars with application to syntax-directed editors. In Proceedings of the 8th ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, pages 105–116, 1981. doi:10.1145/567532.567544.
- [11] Paolo G. Giarrusso, Yann Régis-Gianas, and Philipp Schuster. Incremental lambda-calculus in cache-transfer style: Static memoization by program transformation. In Proceedings of the 28th European Symposium on Programming, pages 553–580, 2019.
- [12] Ashish Gupta, Inderpal Singh Mumick, and V. S. Subrahmanian. Maintaining views incrementally. In Proceedings of the 1993 ACM SIGMOD International Conference on Management of Data, pages 157–166, 1993. doi:10.1145/170035.170066.
- [13] Michael A. Hammer, Kyle Y. J. Phang, Michael Hicks, and Jeffrey S. Foster. Adapton: Composable, demand-driven incremental computation. In Proceedings of the 35th ACM SIGPLAN Conference on Programming Language Design and Implementation, pages 156–166, 2014.
- [14] Monika Rauch Henzinger and Valerie King. Algorithmic aspects of dynamic graph connectivity. Algorithmica, 22(1–2):119–146, 1998.
- [15] Herbert W. Hethcote. The mathematics of infectious diseases. SIAM Review, 42(4):599–653, 2000. doi:10.1137/S0036144500371907.
- [16] Jacob Holm, Kristian de Lichtenberg, and Mikkel Thorup. Poly-logarithmic deterministic fully-dynamic algorithms for connectivity, minimum spanning tree, 2-edge, and biconnectivity. J. ACM, 48:723–760, 2001. doi:10.1145/502090.502095.
- [17] Frank McSherry, Derek G. Murray, Rebecca Isaacs, and Michael Isard. Differential dataflow. In Proceedings of the 6th Biennial Conference on Innovative Data Systems Research, pages 1–11, 2013.
- [18] Boris Motik, Yavor Nenov, Robert Piro, and Ian Horrocks. Incremental update of datalog materialisation: The backward/forward algorithm. In Proceedings of the Twenty-Ninth AAAI Conference on Artificial Intelligence, pages 1560–1568, 2015.
- [19] Mark H. Overmars. The Design of Dynamic Data Structures. Springer-Verlag, 1983.
- [20] Mark H. Overmars and Jan van Leeuwen. Maintenance of configurations in the plane. Journal of Computer and System Sciences, 23(2):166–204, 1981. doi:10.1016/0022-0000(81)90012-X.
- [21] William Pugh and Tim Teitelbaum. Incremental computation via function caching. In Proceedings of the 16th ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, pages 315–328, 1989. doi:10.1145/75277.75305.
- [22] Ganesan Ramalingam and Thomas Reps. A categorized bibliography on incremental computation. In Proceedings of the 20th ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, pages 502–510, 1993.