🦖 Rex: De Rex Programmeertaal
De workflow-runtime verbindt vier kernconcepten die bijzonder nuttig zijn bij het verwerken van wetenschappelijke data:
- Een echte functionele taal: Hiermee worden control flow, data flow, hergebruik en foutafhandeling onderdeel van één compacte, expressieve taal, in plaats van een mengeling van YAML, shell-scripts en applicatiespecifieke configuraties.
- Content-addressable storage (CAS): Elk opgeslagen input- en output-artefact wordt geïdentificeerd door zijn BLAKE3-hash. Bestanden en directory-structuren zijn onveranderlijke waarden, waardoor tussenproducten tussen tools kunnen worden doorgegeven zonder afhankelijk te zijn van gedeelde bestandsnamen of mutabele werkmappen.
- Getypeerde Tool API's: Deze stellen domeinconcepten beschikbaar, zoals videocodecs, PDF-structuren, beeldbewerkingen en outputformaten. Rex-programma's construeren geldige tool-aanvragen in plaats van het samenstellen van shell-commando-strings.
- Geïsoleerde Docker-uitvoering: Elke tool-aanroep kan worden uitgevoerd in een schone, afgeschermde container die alleen de gedeclareerde inputs bevat. Voor een snellere ontwikkelcyclus kunnen ook lokaal geïnstalleerde tools worden gebruikt.
Deze eigenschappen maken workflowdefinities beknopt, inspecteerbaar en geschikt voor parallelle uitvoering. Daarnaast creëren ze een strikte scheiding tussen de logica van een analyse en de besturingssysteemprocessen die deze uitvoeren.
Rex is bovendien zeer geschikt als doeltaal voor door LLM's gegenereerde workflows: statische types geven snelle en duidelijke feedback, pure code is gemakkelijker te inspecteren, en de gesloten tool-boundary beperkt strikt wat gegenereerde programma's de host kunnen laten uitvoeren.
Projectstatus: De main branch bevat het werk in uitvoering voor Rex v4 en is momenteel geversioneerd als 3.9.x. rex-workflow is nieuw en is in actieve ontwikkeling. De oudere productieversie van de core Rex-taal is beschikbaar bij talo/rex.
Waarom een functionele taal voor workflows?
Veel workflowsystemen beginnen met een directed acyclic graph (DAG) en bouwen geleidelijk hun eigen expressiesyntaxis, templates, conditionele logica, loops en pluginmodellen uit. Rex begint direct met een compacte, algemene taal. Het biedt Hindley–Milner type-inferentie, algebraïsche datatypen (ADTs), records, pattern matching, parametrische polymorfisme, type-classes, hogere-orde functies, recursie en modules.
Voor wetenschappelijk werk en dataverwerking is dit essentieel, omdat echte pipelines zelden een statische reeks commando's blijven. Ze moeten analyses kunnen mappen over een cohort, observaties groeperen, vertakken op basis van metadata, domeinspecifieke foutinformatie behouden, verschillende tools combineren en herbruikbare methoden verpakken.
Deze operaties zijn natuurlijk in een functioneel programma:
let
observations = [3.0, -1.0, 12.0, 7.0, 20.0],
selected = filter (\value -> value >= 0.0) observations,
normalized = map (\value -> value / 20.0) selected
in
foldl (\total value -> total + value) 0.0 normalized
Rex gebruikt strict evaluation, maar expressies en functies zijn puur: hun betekenis hangt niet af van verborgen mutabele status in de taal. Hierdoor kan de evaluator onafhankelijke asynchrone aanroepen gelijktijdig uitvoeren zonder dat de gebruiker threads, futures, locks, async/await-syntaxis of callback-grafieken hoeft te beheren. Sequentiële afhankelijkheden worden uitgedrukt door het resultaat van de één door te geven aan de volgende; onafhankelijk werk blijft onafhankelijk in de broncode.
Zuiverheid verbetert ook de controleerbaarheid. De argumenten van een functie beschrijven de data die het kan gebruiken, het resultaattype beschrijft wat het kan produceren, en een algebraïsch datatype kan elk verwacht resultaat opsommen. Tool-modules behouden dit model door gewone getypeerde waarden terug te geven, zoals:
Result Media FF.FfmpegErrorResult Q.PdfOutput Q.QpdfErrorResult P.TextFile P.PopplerError
Verwachte ongeldige aanvragen en fouten in tool-processen kunnen dus binnen de workflow worden herkend en afgehandeld. Fouten in de opslag, executor-fouten en andere infrastructuurproblemen blijven evaluatiefouten, waardoor domeinfouten strikt gescheiden blijven van fouten in de runtime zelf.
Statische types voorkomen fouten in de workflow-structuur
Tool-opties worden gerepresenteerd door records en algebraïsche datatypen in plaats van ongestructureerde maps. Zodra een hash is ingekapseld in een semantisch artefact-type, kan een Image niet per ongeluk worden meegegeven waar een Media wordt verwacht; een codec-optie kan niet worden verward met een beeldbewerking; en een resultaat met meerdere bestanden moet als zodanig worden behandeld.
Hoewel ruwe geïmporteerde hashes correct geclassificeerd moeten worden door de workflow, en een tool een fout rapporteert als de opgeslagen bytes geen geldige input zijn, vangt de compiler structurele fouten in de bedrading op voordat er een duur extern proces wordt gestart.
Types zijn vooral waardevol wanneer workflows worden gegenereerd of gewijzigd door software. Een LLM of een ander programma kan een Rex-workflow voorstellen, de parser en type-checker draaien, en precieze diagnostiek gebruiken om deze te repareren voordat er een tool wordt uitgevoerd.
Functionele compositie schaalt verder dan een DAG-bestand
Rex-workflows kunnen herhaalde logica factoriseren in functies, domeintypes definiëren, collecties transformeren met map en folds, recursie gebruiken voor hiërarchische data en gestructureerde fouten behouden over tool-grenzen heen. Het resultaat is een programma dat kan meegroeien met een analyse, in plaats van een configuratiebestand dat uiteindelijk een externe templating-taal nodig heeft.
Content-addressable data
Wetenschappelijke workflows zijn eenvoudiger te analyseren wanneer artefacten waarden zijn en geen mutabele locaties. rex-workflow bevat een content-addressable store (CAS) waarin elk object wordt benoemd door de BLAKE3-hash van zijn bytes.
Het datamodel kent twee soorten objecten:
- Blob: Een ondoorzichtige reeks bytes, zoals een afbeelding, video, PDF, tabel, model, logboek of elk ander bestand.
- Tree: Een deterministisch gecodeerde map van namen naar blob- of tree-verwijzingen. Trees representeren directories, datasets met meerdere bestanden, geëxtraheerde collecties afbeeldingen en geneste resultaten.
Elke tree-entry registreert het type, de hash en de grootte. Trees kunnen andere trees bevatten, waardoor één root-hash een volledige onveranderlijke directory-hiërarchie identificeert. De groottes zijn cumulatief; een entry die naar een andere tree verwijst, bevat de totale grootte van alles wat daarin zit.
Dataflow proces: Host bestand/directory → rex store import → BLAKE3 blob/tree hash → Getypeerde Rex waarden en tool-aanroepen → Nieuwe blob/tree hashes → rex store export → Host bestand/directory
Dit model biedt verschillende voordelen:
- Stabiele identiteit: Dezelfde bytes produceren altijd dezelfde hash, ongeacht de oorspronkelijke bestandsnaam of machine.
- Onveranderlijkheid: Bestaande inputs kunnen niet worden overschreven. Een transformatie creëert een nieuw object en geeft een nieuwe hash terug.
- Deduplicatie: Het schrijven van content die al aanwezig is, verwijst naar het bestaande adres in plaats van een tweede logisch object te creëren.
- Eenduidige overdracht: Een tool consumeert een exact object en geeft de exacte identiteiten van de outputs terug. Er is geen twijfel over welke revisie van een pad is gelezen.
- Natuurlijke compositie: Een output-hash van de ene tool is direct bruikbaar door een andere tool, zonder dat een tussenbestand geëxporteerd en opnieuw geïmporteerd hoeft te worden.
- Draagbare opslag: Dezelfde API kan een lokale bestandssysteem-store, een in-memory store of een cloud-hosted S3-bucket gebruiken.
De CAS is bewust afgestemd op de functionele taal: het maken van een nieuw artefact muteert geen oud artefact, en het aanroepen van put met dezelfde content geeft dezelfde waarde terug. Een update van een directory wordt gerepresenteerd door nieuwe trees te creëren langs het gewijzigde pad, vergelijkbaar met het objectmodel van Git.
Content-addressing is een belangrijke basis voor caching en provenance, maar het is geen wondermiddel. Een hash identificeert bytes; het registreert op zichzelf niet welke workflow, parameters, tool-versie of container deze heeft geproduceerd. Ook kan een externe tool non-deterministisch zijn, zelfs als de inputs onveranderlijk zijn. Rex maakt de artefact-boundary expliciet, zodat caching en provenance rigoureus kunnen worden opgebouwd in plaats van afgeleid te worden uit mutabele paden.
Store-operaties beschikbaar voor Rex-programma's
De ingebouwde std.storage module stelt onveranderlijke data direct beschikbaar:
import std.storage (*);
let
report = put_string "analysis complete",
files = dict_from_entries [("report.txt", (Blob, report))],
result_tree = put_tree files
in
result_tree
Programma's kunnen gebruikmaken van putstring, putbytes, puttree, getstring, getbytes en gettree. De gedeelde std.artifacts module kapselt hashes in met semantische betekenis via Media, Image, Pdf en JsonFile; tool-specifieke resultaattypes voegen metadata toe waar nodig.
import std.artifacts (Pdf);
fn as_pdf (content: Hash) -> Pdf = Pdf { content = content };
Het construeren van een artefact classificeert een CAS-blob, maar inspecteert de bytes niet. De consumerende tool blijft verantwoordelijk voor de validatie of de opgeslagen content een ondersteunde representatie heeft.
Tools als getypeerde capabilities
Rex biedt geen algemeen shell-commando aan workflow-programma's. In plaats daarvan registreert de host modules waarvan de functies en types de ondersteunde operaties beschrijven. De huidige workflow-catalogus bevat:
| Rex module | Runtime programma's | Geselecteerde mogelijkheden |
|---|---|---|
tools.ffmpeg | FFmpeg, FFprobe | Transcoderen en remuxen van media, audio/frames extraheren, thumbnails maken, concateneren, muxen, segmenteren, HLS/DASH packagen, metadata probe-en, packets/frames inspecteren. |
tools.gnuplot | Gnuplot | Renderen van getypeerde figuren vanuit inline curves, error bars, bands, bars, histogrammen, heatmaps, vectoren, labels, point clouds, paths, surfaces en annotaties. |
tools.graphviz | Graphviz dot | Renderen van semantische gerichte of ongerichte grafen met getypeerde attributen, gedeclareerde nodes, binaire edges, ports, labels, defaults en geneste subgrafen. |
tools.imagemagick | ImageMagick | Genereren en transformeren van afbeeldingen, batch-converteren, identificeren, vergelijken, composieten, montage, pixels extraheren en formaten/capabilities opvragen. |
tools.qpdf | QPDF | PDF's controleren, pagina's tellen, gestructureerde JSON exporteren, transformeren of lineariseren, pagina's mergen/splitten en overlays/underlays toepassen. |
tools.poppler | pdfinfo, pdftotext, pdftocairo, pdfimages | PDF-metadata parsen, tekst en woord-geometrie extraheren, pagina's renderen, afbeeldingen extraheren en embedded images inspecteren. |
De specifieke functies dekken veelvoorkomende operaties, terwijl FFmpeg en ImageMagick ook getypeerde algemene render-API's bieden voor complexe filtergrafen, meerdere inputs, geordende beeldbewerkingen en meerdere outputs.
Achter de schermen compileert een module elke aanvraag naar een ToolExecutionPlan. Plannen kunnen alleen refereren aan:
- Een programma in de gesloten tool-catalogus.
- Literale argumenten gegenereerd door de getypeerde module.
- Gedeclareerde CAS blob- of tree-inputs.
- Gedeclareerde output-slots en output-types.
- Optionele standaard-input afkomstig uit de CAS.
Een Rex-workflow kan geen executable path, container-image, host-mount, werkmap of willekeurige Docker-optie kiezen. Deze blijven host-beleid. Deze scheiding is nuttig voor onbetrouwbare of gegenereerde workflows, omdat het toevoegen van een nieuwe operatie een expliciete Rust API-beslissing is in plaats van een accidentele uitbreiding van shell-toegang.
Tool-outputs worden ook gecontroleerd bij de boundary. De executor weet of er een enkel bestand, een genummerde reeks, een directory of een tree wordt verwacht. Symbolische links en speciale bestanden worden geweigerd voordat de output recursief in de CAS wordt geïmporteerd.
Lokale en Docker-uitvoering
Hetzelfde getypeerde tool-plan kan worden uitgevoerd door verschillende back-ends die door de host zijn gekozen. De workflow zelf verandert niet.
Lokale processen
Lokale uitvoering is de standaard: rex --store-path ./store run workflow.rex --inputs inputs.json
Dit creëert een tijdelijke werkruimte, materialiseert de gedeclareerde CAS-inputs, voert het gecatalogiseerde executable uit dat op de host is geïnstalleerd, en importeert de gedeclareerde outputs. Dit is handig tijdens ontwikkeling en maakt gebruik van de codecs, delegates en fonts op die machine. Het is echter geen OS-sandbox; het proces heeft de rechten en omgeving van de host-gebruiker.
Geïsoleerde Docker-containers
Docker-uitvoering houdt de workflow API identiek, maar plaatst elke tool-aanroep in een schone container: rex --store-path ./store run workflow.rex --inputs inputs.json --tool-executor docker
De container-executor is opzettelijk restrictief. Voor elke aanroep:
- Worden alleen de in het plan gedeclareerde CAS-inputs gematerialiseerd.
- Wordt
/work/inputsread-only en/work/outputsread-write gemount. - Wordt een grootte-beperkte
noexec,nosuid,nodevtmpfs gebruikt voor/work/tmp. - Worden de CAS, repository, huidige directory, home directory, Docker socket, systeem-fonts of willekeurige host-paden niet gemount.
- Worden netwerktoegang, Linux capabilities, privilege escalation en health checks uitgeschakeld.
- Wordt een read-only image root gebruikt, samen met een PID-limiet en de numerieke UID/GID van de host-gebruiker.
- Wordt een headless C locale, UTC tijdzone, tijdelijke home en gecontroleerde cache-locaties meegegeven.
- Wordt de uitvoeringstijd en de gevangen stdout/stderr begrensd.
- Wordt de container expliciet verwijderd bij voltooiing, timeout of annulering.
De meegeleverde tool-images beperken het aanvalsoppervlak verder. De FFmpeg API biedt bijvoorbeeld geen netwerkbronnen, capture-devices of hardware-acceleratie. ImageMagick bevat een defence-in-depth policy die netwerk, desktop/capture/print, indirect-path, MSL en MVG coders uitschakelt, terwijl de gedocumenteerde headless formaten behouden blijven.
Containers zijn nuttig voor zowel isolatie als reproduceerbaarheid. Ze maken de tool-runtime, libraries, codecs en OS-omgeving een expliciete deployment-keuze. De repository bouwt momenteel development-images lokaal met tags zoals rex-tool-ffmpeg:local. De executor gebruikt --pull=never, zodat het uitvoeren van een workflow nooit contact maakt met een registry.
Snelstart
Je hebt een recente Rust toolchain nodig. Docker met Buildx is vereist voor het container-profiel; voor lokale uitvoering installeer je de relevante tool-suites op de host.
- Build de workflow binary en maak een lokale store:
cargo build -p rex-workflow mkdir -p store
- Build en inspecteer de Docker tool-images:
target/debug/rex tools build target/debug/rex tools inspect
- Voer een voorbeeld uit (PNG gradient genereren met ImageMagick):
target/debug/rex --store-path ./store run rex-workflow/examples/imagemagick/generate_gradient.rex --tool-executor docker
Het JSON-resultaat bevat de BLAKE3 content-hash van de gegenereerde afbeelding.
- Export het blob-bestand naar een conventioneel bestand:
target/debug/rex --store-path ./store store export <content-hash> gradient.png
- Verwerken van een bestaand bestand:
Importeer het bestand eerst: target/debug/rex --store-path ./store store import photo.jpg. Gebruik de resulterende hash in inputs.json en draai bijvoorbeeld het resize-voorbeeld.
CLI Overzicht
De workflow binary heet rex:
rex [--store-path PATH] store import PATHrex [--store-path PATH] store export HASH PATHrex [--store-path PATH] store cat HASH[/PATH]rex [--store-path PATH] store ls HASH[/PATH]rex [--store-path PATH] store resolve-path HASH[/PATH]rex [--store-path PATH] run FILE [--inputs JSON] [--raw-output] [--tool-executor local|docker]rex tools buildrex tools inspectrex tools cleanup [--include-running]
run leest een .rex programma, parset en type-checkt dit, zet een plat JSON-object om naar de getypeerde parameters van main, voert de workflow uit en rendert het resultaat als JSON. --raw-output print een string-resultaat zonder JSON-quoting.
Workflow Voorbeelden
De repository bevat diverse voorbeelden die allemaal worden gecontroleerd door de rex-workflow test suite:
- FFmpeg: gegenereerde video/audio, transcoding, stream copying, probing, inspectie, muxing, concatenatie, filtering, frame-extractie, segmentatie en HLS/DASH.
- Gnuplot: curves, error bars, bands, categorische bars, histogrammen, heatmaps, vectoren, labels, point clouds, paths en surface representaties.
- Graphviz: getypeerde DOT-graaf constructie, subgraph compositie en SVG rendering.
- ImageMagick: beeldgeneratie, resizing, thumbnails, conversie, metadata, tekenen, compositie, vergelijking, montage, frame-extractie, batch processing en raw pixels.
- QPDF: validatie, JSON-inspectie, linearisatie en pagina-merging.
- Poppler: metadata, tekst- en woord-geometrie, pagina-rendering en afbeelding-extractie.
- Gecombineerd: Multi-stage media workflows met directe CAS-backed overdracht tussen ImageMagick en FFmpeg.
- Storage: Recursieve traversal en rendering van onveranderlijke directory-trees.
Executie-architectuur
Een workflow-run doorloopt verschillende strikt gescheiden fasen:
- Parse:
rex-parserzet broncode om in eenCompilationUnit. - Typecheck:
rex-typesysteminferert en controleert het programma, imports, main-inputs, tool-opties en resultaten. - Decode inputs: De CLI converteert JSON-velden naar de concrete Rex-types gedeclareerd door
main. - Evaluate:
rex-engineevalueert pure expressies en plant geïnjecteerde asynchrone functies in. De runtime state bevat een CAS-implementatie en een tool-executor. - Compile tool requests: Een
tools.*module converteert semantische Rex-waarden naar een geslotenToolExecutionPlanmet expliciete input- en output-slots. - Execute: De lokale of Docker-backend materialiseert CAS-inputs in een specifieke workspace en voert het gecatalogiseerde programma uit.
- Capture artifacts: Gedeclareerde outputs worden gevalideerd, recursief geïmporteerd in de CAS en teruggegeven aan Rex als hashes in semantische types.
- Encode the result: De uiteindelijke getypeerde Rex-waarde wordt geconverteerd naar JSON voor de aanroeper.
Embedding workflows in Rust
rex-workflow is zowel een CLI als een library. Een applicatie kan zelf de store en executor kiezen, JSON-inputs leveren en Rex-broncode evalueren:
use rex_workflow::{
run::eval_rex,
state::State,
storage::store::Store,
};
let store = Store::new_with_filesystem("./store".into());
let state = State::local(store);
let inputs = serde_json::json!({ "input": input_hash });
let result = eval_rex(source, Some(inputs), state).await?;
Stores kunnen in-memory zijn of gebruikmaken van object_store::ObjectStore (voor cloud-storage). Embedders kunnen de Docker-executor selecteren met een expliciete image-policy of de ToolExecutor trait implementeren om execution-plans naar een andere sandbox of worker-service te routeren.
Rex als embedded taal
Het workflow-systeem is gebouwd op de algemene Rex embedding API. Buiten rex-workflow kan een Rust-applicatie gebruikersprogramma's parsen en type-checken, synchrone of asynchrone native functies injecteren en deze evalueren met applicatie-gedefinieerde state. De macros #[derive(Rex)], #[rex::export] en #[rex::module] vormen de brug tussen Rust types/API's en Rex.
Taal en Tooling
Rex kan onafhankelijk van rex-workflow worden gebruikt voor pure berekeningen of als embedded scripting taal. Er is een browser-playground beschikbaar of de standalone CLI:
cargo run -p rex-cli --bin rex_cli -- -c 'map (\n -> n * n) [1, 2, 3, 4]'
De repository bevat daarnaast:
- Een LSP-server en VS Code extensie.
- Een browser-runtime gecompileerd naar WebAssembly.
- Een standaard prelude met collectie-operaties, type-classes en functionele abstracties.
- Een tutorial, taalreferentie, formele semantiek en architectuurnoten.
- Fuzz-targets en regressie-suites.
Workspace Crates
Dit project is een Cargo workspace met de volgende belangrijkste crates:
rex-workflow: CAS workflow runtime, getypeerde tool-modules, executors en de CLI.rex: Entry point voor embedding in Rust applicaties.rex-parser: Parser die eenCompilationUnitproduceert.rex-ast: Gedeelde syntax tree nodes, symbolen en spans.rex-typesystem: Hindley–Milner inferentie, ADTs en type classes.rex-engine: Getypeerde evaluator en standaard prelude.rex-proc-macro: Bridges tussen Rust en Rex.rex-cli: Standalone taal CLI.rex-lsp&rex-vscode: Language server en editor extensie.rex-wasm&rex-mdbook: Browser runtime en interactieve documentatie.rex-fuzz: Fuzz harnesses.rex-util: Hulpfuncties voor import, hashing en modules.
Ontwerpgrenzen en huidige scope
Rex is bewust geen shell-wrapper. De tool-catalogus is gesloten, host-paden ontbreken in tool-API's en het Docker-beleid wordt niet beheerd via de workflow-broncode. Dit beperkt de directe flexibiliteit van willekeurige commando-uitvoering, maar levert een boundary op die getypeerd, gereviewd, getest en geïsoleerd kan worden.
De huidige implementatie van rex-workflow is een lokale runtime en embeddable library, nog geen gedistribueerde scheduler of complete provenance-database. Het doel is om single-host workflows correct, componeerbaar en veilig te maken voordat gedistribueerde coördinatie wordt toegevoegd.
Groetjes,