Skip to content

feat(runtime): refuse a runtime too old to run the compiled file - #27

Open
BuddhiLW wants to merge 2 commits into
fix/lazy-seq-forcingfrom
feat/runtime-version-guard
Open

feat(runtime): refuse a runtime too old to run the compiled file#27
BuddhiLW wants to merge 2 commits into
fix/lazy-seq-forcingfrom
feat/runtime-version-guard

Conversation

@BuddhiLW

@BuddhiLW BuddhiLW commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Closes CLJEL-VERSION-GUARD (20260905161109-65e51703). Stacked on #26
review that first; the base retargets to main once it merges.

The gap

clojure-elisp-runtime.el carried a MELPA ;; Version: header but defined no
constant, so nothing at run time could tell which runtime was loaded.

Harmless today, because the runtime is bundled beside its application and that
copy is always the one that compiled the file. It stops being harmless the
moment the runtime is package managed, which is the MELPA goal
(20260713171046-63c616a7): MELPA ships HEAD, MELPA Stable ships the latest
tag, so a 0.9 file can meet a 0.7 runtime.

The fix

The runtime defines clel-runtime-version, stamped from VERSION at regen
alongside the header it already stamps. Every emitted file opens with:

(eval-and-compile
  (require 'clojure-elisp-runtime)
  (unless (and (boundp 'clel-runtime-version)
               (version<= "0.7.2" clel-runtime-version))
    (error "clojure-elisp-runtime %s is too old for this file (needs %s)"
           (if (boundp 'clel-runtime-version) clel-runtime-version "(pre-0.7.2)")
           "0.7.2")))

Two decisions worth flagging:

boundp is load-bearing. A runtime older than 0.7.2 defines no constant at
all, so an unguarded read signals void-variable clel-runtime-version — which
is precisely the undiagnosable failure the guard exists to replace. Measured:
against the 0.7.1 runtime the message is clojure-elisp-runtime (pre-0.7.2) is too old for this file (needs 0.7.2).

The guard names a minimum, not the compiler version. version/minimum-runtime-version
is a deliberate constant, bumped only when emitted output stops working against
the previous runtime. Deriving it from VERSION would rewrite every compiled
file on every patch release, and would make the message untrue: a 0.7.2 file
does not require a 0.7.2 runtime, it requires whichever runtime first provided
what it uses.

Evidence at both rungs

  • Clojure: runtime_version_guard_test.clj asserts the emitted shape, the
    boundp guard, and that the runtime's stamped constant matches VERSION

  • Emacs: clojure-elisp-runtime-guard-test.el loads a real compiled file
    against three runtimes — current, older-versioned, and pre-guard — and reads
    the verdict from the process exit code

    Each case runs in a fresh Emacs. In a shared process require is a no-op
    after the first load and clel-runtime-version stays bound, so all three
    checks passed vacuously until this was fixed.

    The fixture is written by the compiler via make test-elisp and gitignored,
    not committed: a hand-written copy of emitter output would be a second source
    of truth for the guard's shape, free to drift from the emitter producing it.

  • 608 Clojure tests, 3058 assertions, 0 failures

  • 28 ERT tests across three suites, 0 unexpected

Output change

Every compiled file's header grows the guard. The one golden snapshot was
regenerated; its diff is the guard and nothing else.

The runtime carried a MELPA Version: header but defined no constant, so
nothing at run time could tell which runtime was loaded. It now defines
clel-runtime-version, stamped from VERSION at regen, and every emitted
file opens with a guarded require.

Harmless while the runtime is bundled beside its application, since that
copy always compiled the file. It stops being harmless once the runtime
is package managed, which is the MELPA goal: MELPA ships HEAD and MELPA
Stable ships the latest tag, so a 0.9 file can meet a 0.7 runtime. That
surfaces as void-function clel-count, which reads as a broken package,
or as a silently changed semantic - 0.7.2 lazy forcing being the live
example.

The boundp check is load-bearing: a runtime older than 0.7.2 defines no
constant, and an unguarded read would signal void-variable, which is the
undiagnosable failure the guard replaces.

The guard names version/minimum-runtime-version, a deliberate constant,
not the project version. Deriving it from VERSION would rewrite every
compiled file on every patch release and make the message untrue.

Tested at both rungs: the Clojure side asserts the emitted shape and the
stamped constant; test/elisp/clojure-elisp-runtime-guard-test.el loads a
real compiled file in a FRESH Emacs against three runtimes (current,
older, pre-guard) and reads the verdict from the exit code. The fixture
it loads is generated by the compiler via make, not committed, so it
cannot drift from the emitter it stands for.

Closes 20260905161109-65e51703.
A PR stacked on another branch got no run at all, and silently: an empty
check list looks the same as a passing one. #27 opened against
fix/lazy-seq-forcing and was never tested.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant