Module loads carry no cache; share evaluate_source
use once kept a per-process result map keyed by canonical absolute path,
returned on every subsequent load of the same file. The decision: module
loads carry no cross-session cache — use and source re-read and
re-evaluate the file on every call — choosing freshness over a memoisation
that, in a shell, buys nothing.
Why the cache was not load-bearing
The cache was keyed by canonical path with no invalidation hook, so the only thing it changed was when a file’s bindings were observed — and every rationale for keeping that around dissolves in a shell.
- Staleness was the cost it imposed. In a long-lived REPL, editing a module
and re-
useing it served the old bindings; the file’s side effects ran only on first load. The escape hatch was “restart the process” — i.e. discard the cache the cache existed to preserve. - The cross-import dedup never fires across processes. Scripts are separate processes with independent caches, so a module imported by two scripts is loaded twice regardless. The cache could only dedup within one process.
- Within one process the win is rare and cheap to forgo. The REPL is
human-paced — re-parsing a small module between prompts is imperceptible — and
an in-run diamond (one run
useing the same module along two paths) is uncommon. Where it matters, a caller binds the result once withlet.
The guards are retained and now load-bearing
Cacheless re-evaluation must still terminate. The cycle stack
(modules.stack) and depth bound (modules.depth, MAX_SOURCE_DEPTH) in
evaluate_source are kept: a file that uses or sources itself is rejected
as a circular dependency, and unbounded nesting trips the depth limit. With no
cache absorbing the second visit, these are the only thing standing between a
self-referential module and divergence.
use becomes a wrapper over the shared core
evaluate_source is the single guarded parse + elaborate + evaluate core
(it is also what the capability-file loader uses). use and source now share
it and a read_and_normalize helper (check_fs_read + read + line-ending
normalisation, with the same not-found / permission / other messages for both
verbs), differing only in two intentional axes:
- Scope and return.
sourceevaluates into the caller’s scope — its bindings leak — and yields the file’s terminal value;usepushes a fresh scope, routes throughevaluate_source, and projects the top scope into a Map (dropping_-prefixed names). - Path policy.
useresolves a module by name — script-relative, then aRAL_PATHfind_filefallback;sourceruns a file by path, script-relative only. The distinction is the point and stays split.
Folding use onto evaluate_source also retired its duplicated cycle guard,
check_source, and ScriptContextGuard block, and gave it the depth bound it
previously lacked. The Modules state lost its cache field (and its wire
mirror in WireModules), and Modules::return_to — which propagated only the
cache child→parent — went with it.
See also builtins,
capabilities,
docs/SPEC.md §8 (Modules).