Het artikel bespreekt twee belangrijke nieuwe ontwikkelingen binnen Jolt: de introductie van programma-images en de implementatie van portable Scheme backends.
Programma-images dienen als een 'black box recorder' voor software. In plaats van te vertrouwen op traditionele logging, waarbij men vooraf moet definiëren welke data relevant is, maakt jolt.image het mogelijk om de volledige status van een programma naar schijf te dumpen bij een fout. Deze images zijn architectuur-agnostisch, waardoor een crash op een server kan worden overgezet naar een lokale REPL voor diepgaand onderzoek. Daarnaast biedt dit potentieel voor onderwijs en samenwerking, omdat men een levend object (de programmastatus) kan versturen in plaats van enkel broncode.
Portable Scheme backends zorgen ervoor dat Jolt is ontkoppeld van de Chez-runtime. De architectuur is nu verdeeld over een portable Core, een Adapter-contract voor host-capaciteiten en target-specifieke bestanden. Dankzij deze modulaire opbouw kan Jolt nu draaien op verschillende implementaties:
- Gambit: Maakt het mogelijk om Jolt in de browser uit te voeren via JavaScript.
- Chez: Biedt snelheid, echte threads en native compilatie.
- Stalin: Geschikt voor kleine, geoptimaliseerde binaries op beperkte apparaten.
Het kernpunt is dat dezelfde Clojure-code nu draait op diverse omgevingen, afhankelijk van de gekozen runtime en diens capaciteiten.
Programma-images en portable Scheme backends voor Jolt
Images: een black box voor je programma
Wie ooit een productiesysteem zoals een webapplicatie heeft moeten ondersteunen, kent de waarde van goede logging. Meestal verspreid je statements door de code, verzendt deze naar een doorzoekbare plek en gebruik je ze als kruimels wanneer er iets kapotgaat. Bij fouten in een systeem moet je eerst reconstrueren wat het programma deed om te begrijpen wat er is gebeurd. Als de logging een kritiek stuk informatie heeft gemist, verandert het onderzoek in giswerk en pogingen om de status te reconstrueren die de fout veroorzaakte. Dit kan een bijzonder frustrerende ervaring zijn wanneer een productiesysteem om drie uur 's nachts uitvalt.
Het probleem met logging is dat het je dwingt de vraag al te kennen voordat je het antwoord weet. Elke logregel is een projectie van de programmastatus die vooraf is gekozen, vaak bestaande uit twee of drie zaken die relevant leken op het moment dat de call werd geschreven. Als de werkelijke oorzaak in het vierde element zat, vertelt de log niets van waarde. Je kunt niet teruggaan en een andere vraag stellen, omdat de waarden weg zijn; je hebt waarschijnlijk geen manier om erbij te komen, zelfs als ze nog in het geheugen staan. Je bent beperkt tot de weergave die je oorspronkelijk hebt bepaald bij het schrijven van de code.
Als gevolg hiervan neigen mensen naar defensief over-loggen. Dit creëert een ander probleem: een volume aan logs dat ruis toevoegt en het traceren bemoeilijkt, terwijl vaak nog steeds de velden ontbreken die er echt toe deden.
Maar wat als er een betere manier was, waarbij je helemaal niet op logging hoefde te vertrouwen? Dit is precies wat jolt.image mogelijk maakt. In plaats van vooraf te kiezen wat je vastlegt, kun je simpelweg de gehele status van het programma op het moment van de fout naar schijf dumpen. Vervolgens kopieer je het bestand naar je lokale machine, laad je de status in de REPL en onderzoek je wat er is gebeurd.
In de praktijk ziet dat er als volgt uit. Om de status te dumpen wanneer er een fout optreedt, hoef je alleen image/dump! aan te roepen in de exception handler:
(try
(process-batch! batch)
(catch Exception e
(image/dump! (str "crash-" (random-uuid) ".jimg")
{:error (Throwable->map e)
:batch batch
:pending @work-queue})
(throw e)))
Of je kunt de enumeratie volledig overslaan en het hele programma nemen:
(catch Exception e
(image/dump-world! "crash.jimg")
(throw e))
dump-world! doorloopt de var-tabel en schrijft de root van elke data-var, zodat niets in je code vooraf hoeft te declareren waaruit de status bestaat. De image is architectuur-agnostisch, dus een image geschreven op een arm64-server kan probleemloos worden hersteld op een x86-64 desktop. Zodra je het bestand naar je lokale machine hebt gekopieerd, open je het in een REPL:
$ jolt repl
user=> (require '[jolt.image :as image])
user=> (image/restore-world! "crash.jimg")
412
user=> (filter #(nil? (:price %)) @app.core/current-batch)
({:id 4182, :sku "B-77", ...})
Wat terugkomt zijn de waarden die in het geheugen aanwezig waren op het moment dat het probleem optrad. Maps, records, cycli en gedeelde structuren blijven intact met hun respectievelijke metadata, en functies blijven aanroepbaar. Een benoemde functie verwijst naar de actieve versie, terwijl een anonieme closure wordt meegenomen als bronvorm samen met de gevangen waarden om bij herstel opnieuw te worden gecompileerd. Je kunt dus daadwerkelijk de functie die faalde, uitvoeren op de data die faalde in de REPL en precies zien wat er misging. Je kunt nu een strikt grotere set vragen stellen dan welk logbestand dan ook kan beantwoorden, zonder dat je deze vooraf hoefde te kennen.
Je kunt het programma-image beschouwen als een black box recorder. Net zoals een vliegtuig instrumentstatussen opslaat zodat onderzoekers achteraf kunnen bepalen waar ze naar moeten kijken, geeft een programma-image alle informatie die nodig is om het probleem te debuggen.
Een scherpe lezer vraagt zich wellicht af hoe deze aanpak omgaat met open resources, zoals een socket of een file port, die niet geserialiseerd kunnen worden. De gekozen oplossing was om dump-world! deze standaard als stub records weg te schrijven. Zodra de image is hersteld, kun je ze opsommen door (image/stubs) aan te roepen, en vervolgens ofwel een resolver registreren die ze opnieuw opent, of handmatig live waarden invoegen vanuit de REPL met (image/register-stub-resolver! kind-or-pred f).
Een beperking is dat een closure over een compile-time constante weigert te dumpen, omdat de constante wordt samengevoegd in de gecompileerde code en niet kan worden hersteld, terwijl de opgeslagen bron deze nog steeds nodig heeft. Closures gebouwd door partial en comp hebben om een vergelijkbare reden hetzelfde probleem. De header van de image wordt bij het lezen gecontroleerd, zodat je geen verouderde data krijgt van een incompatibele build. Terwijl dump-world! alles dumpt wat mogelijk is, is dump! standaard strikt en geeft het pad naar het problematische object aan in plaats van iets subtiel onvolledig weg te schrijven. Dit is nuttig in gevallen waarin je expliciet wilt dat de volledige status beschikbaar is.
Een programma naar iemand anders sturen
Hoewel post-mortem debugging een overduidelijke use case is, is een andere interessante toepassing het faciliteren van samenwerking. Een image is een programmastatus die je aan een ander persoon kunt overhandigen, wat intrigerende mogelijkheden opent.
Voor onderwijs betekent dit dat je iemand direct in het midden van een draaiend systeem kunt plaatsen, met echte data geladen, zonder dat zij iets hoeven te bouwen of te seeden. Stel je voor dat je kunt zeggen: "hier is de image met de pipeline halverwege geïmplementeerd; ga naar stage 2 om het te bekijken." Voor een bugrapport betekent het dat een collega jouw probleem exact kan reproduceren, omdat ze simpelweg jouw status laden. Ze kunnen het onderzoeken, iets veranderen, het opnieuw dumpen en het naar jou terugsturen.
Dit is een totaal andere relatie met een programma dan we gewend zijn; traditioneel wordt broncode behandeld als het artefact dat we rondsturen. Jolt verschuift dit dichter naar hoe Smalltalk en de Lisp-machines werkten, en hoe save-lisp-and-die vandaag de dag nog steeds werkt in Common Lisp. Het programma is een levend object dat je behoudt en dat in de loop van de tijd kan evolueren, in tegenstelling tot een recept dat je kunt uitvoeren.
Duurzame sessie-status
Een andere interessante toepassing is het gebruik in desktop-apps, waarbij de gebruiker zijn sessie kan opslaan en na het herstarten van de app precies terugkeert naar waar hij was gebleven. Ik heb een TodoMVC-voorbeeld gebouwd om te illustreren hoe dat er in de praktijk uitziet: je kunt klikken, taken toevoegen of wijzigen, vervolgens de status dumpen en deze opnieuw laden in een nieuwe instantie.
Portable Scheme backends
Oorspronkelijk was mijn doel voor Jolt om een Clojure-implementatie op Chez Scheme te bouwen. Jack Rusher wees er echter op dat het mogelijk zou zijn om een portable runtime en compiler te extraheren, waardoor Chez slechts één van de verschillende targets wordt. Gambit was de logische keuze voor een tweede host, aangezien deze een JavaScript-backend heeft, wat het mogelijk maakt om Jolt in een browser te draaien. Op de officiële site kun je nu een interactieve REPL uitproberen; dit is Jolt die in de browser draait.
De refactor splitste het Scheme-gedeelte van de compiler in drie verschillende lagen:
- De Core: Geschreven in portable Scheme en implementeert collecties, sequenties, de reader, de printer, vars en multimethods. Dit is regulier Scheme dat door elke serieuze implementatie ongewijzigd wordt uitgevoerd.
- Het Adapter-contract: Hier komt de host naar voren. Elke host-capaciteit verloopt via een
sa-* entry point. Het contractbestand bevat 72 namen gegroepeerd in tiers:
- Systeem (klokken, omgeving, exit)
- Threads
- Eval
- Introspect (continuation frames voor backtraces)
- FFI
- Native-compile
- Image
Een target kan een tier implementeren of deze eerlijk degraderen, zodat een ontbrekende capaciteit een foutmelding met bericht geeft of leeg terugkeert. Deze laatste eigenschap is cruciaal om partiële ports bruikbaar te maken. De Gambit-versie draait met ffi, native-compile en image gedegradeerd, en declareert zijn capaciteiten expliciet.
- Target-owned files: Dit zijn de twee onderdelen die niemand kan delen: de adapter zelf en de hash kernel. De Chez-versie gebruikt bijvoorbeeld onveilige fixnum-operaties die andere Schemes anders spellen. Aan de compilerzijde verlopen target-specifieke verschillen via een primitieve tabel, waarbij het hoofdelement het
unsafe-op prefix is. Een target die dit mapt naar een lege string krijgt overal gecontroleerde operaties die aangeven of ze veilig, portable of trager zijn.
Het dialect-specifieke werk is kleiner dan je zou denken en minder spannend dan je zou hopen. Het meeste houdt in: het mappen van records naar hun parent types, de hashtable API, fx-operatie spellingen, de vorm van error-objects, en ervoor zorgen dat de hash-functie bit-identieke output produceert ten opzichte van Chez. De Gambit-port weegt ongeveer 6.000 regels; een groot deel daarvan, inclusief de seed zelf, is gegenereerd op Chez in plaats van vanaf nul geschreven. Het cross-minten van de seed vanuit een bekende werkende build is de truc die voorkomt dat een nieuwe target zichzelf moet bootstrapen.
Waarom meer dan één Scheme waardevol is
Het directe voordeel is het bereik. Gambit compileert naar één enkel JavaScript-bestand, waardoor Jolt nu in een browser draait. Hierdoor is er een REPL op de voorpagina van de site die gebruikmaakt van de echte compiler en standaardbibliotheek die direct aan de clientzijde wordt uitgevoerd. Het is niet bijzonder snel, maar werkt prima voor een demo.
Scheme is een hele familie van talen met verschillende dialecten die elk zijn geoptimaliseerd voor andere use cases. De diepere waarde ligt dus in het openstellen van een ecosysteem van implementaties die verschillende keuzes hebben gemaakt. Ze delen allemaal de kernsemantiek van de taal, maar elk dialect geeft er een eigen draai aan en biedt een runtime die is geoptimaliseerd voor specifieke scenario's. Jolt kan nu meeliften op dit hele ecosysteem door er een Clojure-laag bovenop te plaatsen.
Chez is een uitstekende standaard omdat het snel is, relatief klein, rijk aan functies en beschikt over echte threads, een FFI en native compilatie. Gambit brengt je naar JavaScript en C. Ondertussen is een whole-program optimiserede compiler in de Stalin-lijn een heel ander verhaal; deze biedt agressieve closure- en type-analyse, wat resulteert in een zeer kleine output die geschikt is voor een klein binary op een beperkt apparaat waar opstarttijd en memory footprint dominant zijn. Zo'n compiler heeft doorgaans geen runtime eval, wat diskwalificerend zou klinken als je niet zou merken dat de seed al cross-gemint is op Chez. Op deze manier kan de compiler op één Scheme leven, terwijl het gegenereerde programma op een andere draait.
Het belangrijkste punt hier is dat het programma dezelfde Clojure blijft, ongeacht welke host je target. Wat verandert zijn de beschikbare capaciteiten, die in het contract moeten worden vermeld om duidelijke grenzen te trekken voor wat door elke runtime kan worden uitgedrukt. Dankzij veel bestaande Scheme-implementaties kan dezelfde code nu draaien op een server, in een browser-tab, als een klein statisch binary, of ingebed in bestaande programma's.
Een programma zou niet gevangen moeten zitten in het proces dat het startte, noch getrouwd moeten zijn met de runtime waarvoor het oorspronkelijk is gecompileerd. Lisps waren altijd bedoeld om flexibel te zijn, en Jolt omarmt deze filosofie.
Programma-images en portable Scheme backends voor Jolt
Images: een black box voor je programma
Wie ooit een productiesysteem zoals een webapplicatie heeft moeten ondersteunen, kent de waarde van goede logging. Meestal verspreid je statements door de code, verzendt deze naar een doorzoekbare plek en gebruik je ze als kruimels wanneer er iets kapotgaat. Bij fouten in een systeem moet je eerst reconstrueren wat het programma deed om te begrijpen wat er is gebeurd. Als de logging een kritiek stuk informatie heeft gemist, verandert het onderzoek in giswerk en pogingen om de status te reconstrueren die de fout veroorzaakte. Dit kan een bijzonder frustrerende ervaring zijn wanneer een productiesysteem om drie uur 's nachts uitvalt.
Het probleem met logging is dat het je dwingt de vraag al te kennen voordat je het antwoord weet. Elke logregel is een projectie van de programmastatus die vooraf is gekozen, vaak bestaande uit twee of drie zaken die relevant leken op het moment dat de call werd geschreven. Als de werkelijke oorzaak in het vierde element zat, vertelt de log niets van waarde. Je kunt niet teruggaan en een andere vraag stellen, omdat de waarden weg zijn; je hebt waarschijnlijk geen manier om erbij te komen, zelfs als ze nog in het geheugen staan. Je bent beperkt tot de weergave die je oorspronkelijk hebt bepaald bij het schrijven van de code.
Als gevolg hiervan neigen mensen naar defensief over-loggen. Dit creëert een ander probleem: een volume aan logs dat ruis toevoegt en het traceren bemoeilijkt, terwijl vaak nog steeds de velden ontbreken die er echt toe deden.
Maar wat als er een betere manier was, waarbij je helemaal niet op logging hoefde te vertrouwen? Dit is precies wat jolt.image mogelijk maakt. In plaats van vooraf te kiezen wat je vastlegt, kun je simpelweg de gehele status van het programma op het moment van de fout naar schijf dumpen. Vervolgens kopieer je het bestand naar je lokale machine, laad je de status in de REPL en onderzoek je wat er is gebeurd.
In de praktijk ziet dat er als volgt uit. Om de status te dumpen wanneer er een fout optreedt, hoef je alleen image/dump! aan te roepen in de exception handler:
(try
(process-batch! batch)
(catch Exception e
(image/dump! (str "crash-" (random-uuid) ".jimg")
{:error (Throwable->map e)
:batch batch
:pending @work-queue})
(throw e)))
Of je kunt de enumeratie volledig overslaan en het hele programma nemen:
(catch Exception e
(image/dump-world! "crash.jimg")
(throw e))
dump-world! doorloopt de var-tabel en schrijft de root van elke data-var, zodat niets in je code vooraf hoeft te declareren waaruit de status bestaat. De image is architectuur-agnostisch, dus een image geschreven op een arm64-server kan probleemloos worden hersteld op een x86-64 desktop. Zodra je het bestand naar je lokale machine hebt gekopieerd, open je het in een REPL:
$ jolt repl
user=> (require '[jolt.image :as image])
user=> (image/restore-world! "crash.jimg")
412
user=> (filter #(nil? (:price %)) @app.core/current-batch)
({:id 4182, :sku "B-77", ...})
Wat terugkomt zijn de waarden die in het geheugen aanwezig waren op het moment dat het probleem optrad. Maps, records, cycli en gedeelde structuren blijven intact met hun respectievelijke metadata, en functies blijven aanroepbaar. Een benoemde functie verwijst naar de actieve versie, terwijl een anonieme closure wordt meegenomen als bronvorm samen met de gevangen waarden om bij herstel opnieuw te worden gecompileerd. Je kunt dus daadwerkelijk de functie die faalde, uitvoeren op de data die faalde in de REPL en precies zien wat er misging. Je kunt nu een strikt grotere set vragen stellen dan welk logbestand dan ook kan beantwoorden, zonder dat je deze vooraf hoefde te kennen.
Je kunt het programma-image beschouwen als een black box recorder. Net zoals een vliegtuig instrumentstatussen opslaat zodat onderzoekers achteraf kunnen bepalen waar ze naar moeten kijken, geeft een programma-image alle informatie die nodig is om het probleem te debuggen.
Een scherpe lezer vraagt zich wellicht af hoe deze aanpak omgaat met open resources, zoals een socket of een file port, die niet geserialiseerd kunnen worden. De gekozen oplossing was om dump-world! deze standaard als stub records weg te schrijven. Zodra de image is hersteld, kun je ze opsommen door (image/stubs) aan te roepen, en vervolgens ofwel een resolver registreren die ze opnieuw opent, of handmatig live waarden invoegen vanuit de REPL met (image/register-stub-resolver! kind-or-pred f).
Een beperking is dat een closure over een compile-time constante weigert te dumpen, omdat de constante wordt samengevoegd in de gecompileerde code en niet kan worden hersteld, terwijl de opgeslagen bron deze nog steeds nodig heeft. Closures gebouwd door partial en comp hebben om een vergelijkbare reden hetzelfde probleem. De header van de image wordt bij het lezen gecontroleerd, zodat je geen verouderde data krijgt van een incompatibele build. Terwijl dump-world! alles dumpt wat mogelijk is, is dump! standaard strikt en geeft het pad naar het problematische object aan in plaats van iets subtiel onvolledig weg te schrijven. Dit is nuttig in gevallen waarin je expliciet wilt dat de volledige status beschikbaar is.
Een programma naar iemand anders sturen
Hoewel post-mortem debugging een overduidelijke use case is, is een andere interessante toepassing het faciliteren van samenwerking. Een image is een programmastatus die je aan een ander persoon kunt overhandigen, wat intrigerende mogelijkheden opent.
Voor onderwijs betekent dit dat je iemand direct in het midden van een draaiend systeem kunt plaatsen, met echte data geladen, zonder dat zij iets hoeven te bouwen of te seeden. Stel je voor dat je kunt zeggen: "hier is de image met de pipeline halverwege geïmplementeerd; ga naar stage 2 om het te bekijken." Voor een bugrapport betekent het dat een collega jouw probleem exact kan reproduceren, omdat ze simpelweg jouw status laden. Ze kunnen het onderzoeken, iets veranderen, het opnieuw dumpen en het naar jou terugsturen.
Dit is een totaal andere relatie met een programma dan we gewend zijn; traditioneel wordt broncode behandeld als het artefact dat we rondsturen. Jolt verschuift dit dichter naar hoe Smalltalk en de Lisp-machines werkten, en hoe save-lisp-and-die vandaag de dag nog steeds werkt in Common Lisp. Het programma is een levend object dat je behoudt en dat in de loop van de tijd kan evolueren, in tegenstelling tot een recept dat je kunt uitvoeren.
Duurzame sessie-status
Een andere interessante toepassing is het gebruik in desktop-apps, waarbij de gebruiker zijn sessie kan opslaan en na het herstarten van de app precies terugkeert naar waar hij was gebleven. Ik heb een TodoMVC-voorbeeld gebouwd om te illustreren hoe dat er in de praktijk uitziet: je kunt klikken, taken toevoegen of wijzigen, vervolgens de status dumpen en deze opnieuw laden in een nieuwe instantie.
Portable Scheme backends
Oorspronkelijk was mijn doel voor Jolt om een Clojure-implementatie op Chez Scheme te bouwen. Jack Rusher wees er echter op dat het mogelijk zou zijn om een portable runtime en compiler te extraheren, waardoor Chez slechts één van de verschillende targets wordt. Gambit was de logische keuze voor een tweede host, aangezien deze een JavaScript-backend heeft, wat het mogelijk maakt om Jolt in een browser te draaien. Op de officiële site kun je nu een interactieve REPL uitproberen; dit is Jolt die in de browser draait.
De refactor splitste het Scheme-gedeelte van de compiler in drie verschillende lagen:
- De Core: Geschreven in portable Scheme en implementeert collecties, sequenties, de reader, de printer, vars en multimethods. Dit is regulier Scheme dat door elke serieuze implementatie ongewijzigd wordt uitgevoerd.
- Het Adapter-contract: Hier komt de host naar voren. Elke host-capaciteit verloopt via een
sa-* entry point. Het contractbestand bevat 72 namen gegroepeerd in tiers:
- Systeem (klokken, omgeving, exit)
- Threads
- Eval
- Introspect (continuation frames voor backtraces)
- FFI
- Native-compile
- Image
Een target kan een tier implementeren of deze eerlijk degraderen, zodat een ontbrekende capaciteit een foutmelding met bericht geeft of leeg terugkeert. Deze laatste eigenschap is cruciaal om partiële ports bruikbaar te maken. De Gambit-versie draait met ffi, native-compile en image gedegradeerd, en declareert zijn capaciteiten expliciet.
- Target-owned files: Dit zijn de twee onderdelen die niemand kan delen: de adapter zelf en de hash kernel. De Chez-versie gebruikt bijvoorbeeld onveilige fixnum-operaties die andere Schemes anders spellen. Aan de compilerzijde verlopen target-specifieke verschillen via een primitieve tabel, waarbij het hoofdelement het
unsafe-op prefix is. Een target die dit mapt naar een lege string krijgt overal gecontroleerde operaties die aangeven of ze veilig, portable of trager zijn.
Het dialect-specifieke werk is kleiner dan je zou denken en minder spannend dan je zou hopen. Het meeste houdt in: het mappen van records naar hun parent types, de hashtable API, fx-operatie spellingen, de vorm van error-objects, en ervoor zorgen dat de hash-functie bit-identieke output produceert ten opzichte van Chez. De Gambit-port weegt ongeveer 6.000 regels; een groot deel daarvan, inclusief de seed zelf, is gegenereerd op Chez in plaats van vanaf nul geschreven. Het cross-minten van de seed vanuit een bekende werkende build is de truc die voorkomt dat een nieuwe target zichzelf moet bootstrapen.
Waarom meer dan één Scheme waardevol is
Het directe voordeel is het bereik. Gambit compileert naar één enkel JavaScript-bestand, waardoor Jolt nu in een browser draait. Hierdoor is er een REPL op de voorpagina van de site die gebruikmaakt van de echte compiler en standaardbibliotheek die direct aan de clientzijde wordt uitgevoerd. Het is niet bijzonder snel, maar werkt prima voor een demo.
Scheme is een hele familie van talen met verschillende dialecten die elk zijn geoptimaliseerd voor andere use cases. De diepere waarde ligt dus in het openstellen van een ecosysteem van implementaties die verschillende keuzes hebben gemaakt. Ze delen allemaal de kernsemantiek van de taal, maar elk dialect geeft er een eigen draai aan en biedt een runtime die is geoptimaliseerd voor specifieke scenario's. Jolt kan nu meeliften op dit hele ecosysteem door er een Clojure-laag bovenop te plaatsen.
Chez is een uitstekende standaard omdat het snel is, relatief klein, rijk aan functies en beschikt over echte threads, een FFI en native compilatie. Gambit brengt je naar JavaScript en C. Ondertussen is een whole-program optimiserede compiler in de Stalin-lijn een heel ander verhaal; deze biedt agressieve closure- en type-analyse, wat resulteert in een zeer kleine output die geschikt is voor een klein binary op een beperkt apparaat waar opstarttijd en memory footprint dominant zijn. Zo'n compiler heeft doorgaans geen runtime eval, wat diskwalificerend zou klinken als je niet zou merken dat de seed al cross-gemint is op Chez. Op deze manier kan de compiler op één Scheme leven, terwijl het gegenereerde programma op een andere draait.
Het belangrijkste punt hier is dat het programma dezelfde Clojure blijft, ongeacht welke host je target. Wat verandert zijn de beschikbare capaciteiten, die in het contract moeten worden vermeld om duidelijke grenzen te trekken voor wat door elke runtime kan worden uitgedrukt. Dankzij veel bestaande Scheme-implementaties kan dezelfde code nu draaien op een server, in een browser-tab, als een klein statisch binary, of ingebed in bestaande programma's.
Een programma zou niet gevangen moeten zitten in het proces dat het startte, noch getrouwd moeten zijn met de runtime waarvoor het oorspronkelijk is gecompileerd. Lisps waren altijd bedoeld om flexibel te zijn, en Jolt omarmt deze filosofie.