https://github.com/typst/comemo
Incremental computation through constrained memoization.
https://github.com/typst/comemo
constraints incremental memoization tracked
Last synced: 8 months ago
JSON representation
Incremental computation through constrained memoization.
- Host: GitHub
- URL: https://github.com/typst/comemo
- Owner: typst
- License: apache-2.0
- Created: 2022-09-06T10:28:56.000Z (over 3 years ago)
- Default Branch: main
- Last Pushed: 2024-11-04T10:33:05.000Z (about 1 year ago)
- Last Synced: 2025-05-15T02:34:50.365Z (8 months ago)
- Topics: constraints, incremental, memoization, tracked
- Language: Rust
- Homepage:
- Size: 86.9 KB
- Stars: 508
- Watchers: 5
- Forks: 22
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE-APACHE
Awesome Lists containing this project
README
# comemo
[](https://crates.io/crates/comemo)
[](https://docs.rs/comemo)
Incremental computation through constrained memoization.
```toml
[dependencies]
comemo = "0.4"
```
A _memoized_ function caches its return values so that it only needs to be
executed once per set of unique arguments. This makes for a great optimization
tool. However, basic memoization is rather limited. For more advanced use cases
like incremental compilers, it lacks the necessary granularity. Consider, for
example, the case of the simple `.calc` scripting language. Scripts in this
language consist of a sum of numbers and `eval` statements that reference other
`.calc` scripts. A few examples are:
- `alpha.calc`: `"2 + eval beta.calc"`
- `beta.calc`: `"2 + 3"`
- `gamma.calc`: `"8 + 3"`
We can easily write an interpreter that computes the output of a `.calc` file:
```rust
/// Evaluate a `.calc` script.
fn evaluate(script: &str, files: &Files) -> i32 {
script
.split('+')
.map(str::trim)
.map(|part| match part.strip_prefix("eval ") {
Some(path) => evaluate(&files.read(path), files),
None => part.parse::().unwrap(),
})
.sum()
}
impl Files {
/// Read a file from storage.
fn read(&self, path: &str) -> String {
...
}
}
```
But what if we want to make this interpreter _incremental,_ meaning that it only
recomputes a script's result if it or any of its dependencies change? Basic
memoization won't help us with this because the interpreter needs the whole set
of files as input—meaning that a change to any file invalidates all memoized
results.
This is where comemo comes into play. It implements _constrained memoization_
with more fine-grained access tracking. To use it, we can just:
- Add the `#[memoize]` attribute to the `evaluate` function.
- Add the `#[track]` attribute to the impl block of `Files`.
- Wrap the `files` argument in comemo's `Tracked` container.
This instructs comemo to memoize the evaluation and to automatically track all
file accesses during a memoized call. As a result, we can reuse the result of a
`.calc` script evaluation as long as its dependencies stay the same—even if
other files change.
```rust
use comemo::{memoize, track, Tracked};
/// Evaluate a `.calc` script.
#[memoize]
fn evaluate(script: &str, files: Tracked) -> i32 {
...
}
#[track]
impl Files {
/// Read a file from storage.
fn read(&self, path: &str) -> String {
...
}
}
```
For the full example see [`examples/calc.rs`][calc].
[calc]: https://github.com/typst/comemo/blob/main/examples/calc.rs
## License
This crate is dual-licensed under the MIT and Apache 2.0 licenses.