8.4. Caching
Mark any labeled call site cache: true in the program YAML and
its result is memoized to disk. The next call with equivalent
inputs against unchanged source code is served from the cache rather
than recomputed.
The freshness check is content-based, not time-based. Morloc has
no notion of "the source file is newer than the cache entry, so
re-run." Editing a comment in an unrelated function will not
invalidate any cache. Copying the program to a new path will not
either. Two builds on two machines that emit byte-identical pool
sources share the same cache namespace. This is a deliberate
departure from make, snakemake, nextflow, and similar tools
that use mtime as a freshness signal and routinely re-run the
world after a git checkout or a clock skew.
The build parameters passed with -X (see the build parameters
section) also participate in the key, since they can change the
compiled output without changing the pool source — switching a
Futhark backend or adding a compiler flag is treated as a distinct
build rather than a cache hit.
8.4.1. Declaring a cached call
labeled-groups:
expensive_step: { cache: true }
foo xs = expensive_step@slowfn xs
Every call into expensive_step@slowfn is memoized under the
expensive_step cache label. The same group config also controls
per-step logging (log: true); the two flags are independent and
may be combined.
8.4.2. What goes in the hash
A cached entry is keyed by:
call_key = xxh64(pool_source_fingerprint, midx, arg_content_hashes ...)
where:
-
pool_source_fingerprintisxxh64of the rendered pool source text, seed-chained over the contents of any files listed underhash-include:in the program YAML. Editing the body of the cached function, of any function it calls, of any imported module that compiled into the same pool, or of any declared external data file, all shift this fingerprint. -
midxis the compiler-assigned manifold id, deterministic per build. -
arg_content_hashesare content-aware hashes of each argument’s value, walked through its msgpack schema. Two structurally equal inputs hash the same regardless of how their packet stored the data (inline bytes, shared-memory pointer, or temp file), and pointer bits are never themselves hashed.
The freshness test is therefore: same code + same code dependencies + same input values → cache hit. Anything else → miss.
8.4.3. Storage layout
The cache lives under one of these directories, in resolution order:
| Source | Notes |
|---|---|
|
Explicit override. Useful for Docker bind mounts and shared filesystems where SLURM workers need access to the same cache. |
|
If |
|
Default. |
Inside, two file types coexist:
. ~/.cache/morloc/cache/
│
├── expensive_step/
│ ├── 3f4a91d62b08e7d2.packet
│ └── b772aa1c0e4ef801.packet
├── another_label/
│ └── ...
└── data/
├── ef46db3751d8e999.dat
└── ...
-
<label>/<call_key>.packetis a small morloc data packet (PACKET_TYPE_DATA, source=FILE, format=MSGPACK) that points to a dat file underdata/. One entry per distinct cached call. -
data/<data_hash>.datholds the cached value as schema-agnostic msgpack bytes. The filename isxxh64of those bytes, so equal return values consolidate onto one on-disk copy regardless of which label cached them or which pool language produced them.
The content-addressed split gives natural deduplication: a Python
pool and an R pool that both cache the same dataset (say, a genome
pulled from an external database) write the data once. A source
edit that produces the same return value writes a new .packet
pointer but reuses the existing .dat.
8.4.4. Reading and writing
On lookup, the runtime reads the .packet file and returns its
bytes; the language pool’s get_value follows the FILE-source
pointer through to the dat file. The on-disk format is opaque to
the language pool — it sees only bytes that round-trip through the
standard packet API, so future format changes (inline-small
optimization, compression, alternative backends) require no
recompile of pool code.
On store, the runtime materializes the input packet to msgpack
(dereferencing any shared-memory or file-backed payload), hashes
those bytes to choose the dat filename, skips the write if an
identical dat already exists, and finally writes the small pointer
packet under the label dir. Both writes go through an atomic tmp
rename, so a partial file is never observed by another reader and
concurrent writers to the same key cannot corrupt the on-disk
state.
A cached call whose foreign body raises is not memoized — the cache wrap only stores on the happy path. A re-run with the same inputs fails the same way rather than silently serving a stale entry.