Rasa, Capabilities, and Controlled Execution
The first Rasa technical note: why Rasa keeps source semantics pure while making host effects explicit, inspectable, and portable across native and Wasm execution.
Rasa is a Clojure-family language, but the point is not to clone a host-shaped Clojure runtime. The point is to keep the parts that make Clojure enduring: small syntax, values over objects, interactive development, sequence-shaped composition, and a practical bias toward programs that can be inspected while they are being built.
The design pressure is different now. A modern language cannot treat the host as an invisible ambient power source. Filesystems, network access, clocks, model bytes, browser handles, process state, and GPU resources must be visible to the compiler and to operators. WebAssembly, WIT, WASI, browsers, native runtimes, and optimizing backends all make the same demand in different words: source semantics should stay pure, while host effects should be admitted deliberately.
This first post is about that boundary. The anchor is deliberately small: reading a text file, transforming it, writing it back, and proving the same source works through native ras and the Wasmtime-backed ras-wasmtime product. Small examples expose architecture better than grand diagrams when the system is young.
The Philosophy
Rasa separates semantic authority from representation.
Source code should not know whether a value lives in a Rust heap, Wasm linear memory, a future WasmGC object graph, or a native-tier compiled frame. Those are runtime and backend decisions. They are important, but they are not source semantics.
The same rule applies to effects. Source should not silently gain filesystem access because the current implementation happens to run inside a native process. If a program reads a file, the compiler and runtime should be able to say:
- which source operation requested the read;
- which capability identity was involved;
- which policy admitted or denied it;
- which provider artifact will implement it for the selected runtime product;
- which boundary movement mode carries the values across the host edge;
- whether the selected lane is native, Wasm host, browser, or unsupported.
That makes a simple Rasa form more than a piece of syntax. It becomes the start of a controlled execution path.
The Phase Shape
Rasa does not mutate one tree until every phase is done. The pipeline is shaped around distinct products and sidecar evidence:
bytes -> source -> reader -> syntax -> expand -> analyze -> types -> ir
-> facts -> checks -> capability plan -> runtime product -> eval
The reader and expansion phases own syntax-shaped program trees. Analysis owns analysis nodes. IR owns the backend-independent executable tree. Facts, checks, capability manifests, host links, and plans are evidence attached to that work, not ad hoc strings scraped from output.
That discipline has a cost. It is more engineering than a thin evaluator. But it buys something Rasa wants early: the ability to inspect why code is accepted, why it is rejected, and where host power enters the program.
The decision rule is:
node-essential meaning -> node-local field
derived or cross-cutting meaning -> node-keyed evidence or sidecar
human or tool view -> projection
From the outside, this should feel like a common analyzed program view becoming more precise as the pipeline advances. The same source spans and program identities accumulate reader forms, expansion results, analysis facts, IR, checks, capability observations, and plans. Internally, Rasa avoids pretending that one mutable AST owns all of that meaning. The shared thing is the program identity and the evidence graph around it, not a giant object that every phase rewrites in place.
This matters for the IO example because read-text is not just a function call. It becomes a source callsite, a capability requirement, a policy decision, a provider confirmation, and then a runtime boundary call.
The Source Experience
Here is the whole user program:
(ns examples.text.file-text-pipeline
(:require [rasa.io :as io]
[rasa.string :as str]))
(def input-path
"examples/text/input.txt")
(def output-path
"target/rasa-text-output.txt")
(def input
(io/read-text {:path input-path}))
(def normalized
(str/replace (str/upper-case input) " " "-"))
(def write-result
(io/write-text {:path output-path} normalized))
(def written-back
(io/read-text {:path output-path}))
[:input input
:normalized normalized
:write-status (:status write-result)
:output-path (:path write-result)
:storage (:storage write-result)
:chars (:chars write-result)
:written-back written-back]
There is no user-visible host/call. There is no runtime conditional that says "if native do this, if Wasm do that". The program asks for rasa.io, and Rasa owns the rest of the boundary explanation.
The implementation of that source API is intentionally small:
(ns rasa.io)
(defn read-text
"Reads eager finite text from an admitted source."
[source]
(host/call :rasa.io/read-text source))
(defn write-text
"Writes eager finite text to an admitted sink."
[sink text]
(host/call :rasa.io/write-text sink text))
The source namespace gives users ordinary functions. The host/call form is not the end-user programming model here; it is the narrow bridge used by the Rasa-owned IO namespace to cross into an admitted capability.
The Capability Contract
The public capability vocabulary lives under capabilities/rasa.io. The catalog says what the operations mean at the boundary:
{:capabilities
[{:id :rasa.io/read-text
:signature {:args [{:name :source :type :map}]
:returns :string}
:effects [:fs/read :may-suspend]
:boundary-mode :copy-value
:policy [:fs/read]
:targets {:native {:availability :available}
:wasmtime {:availability :available
:requires [:wasi/filesystem]}
:wasm-browser {:availability :requires-adapter
:adapter-kind :javascript
:reason :browser-needs-admitted-file-or-buffer-adapter}}}
{:id :rasa.io/write-text
:signature {:args [{:name :sink :type :map}
{:name :text :type :string}]
:returns :map}
:effects [:fs/write :may-suspend]
:boundary-mode :copy-value
:policy [:fs/write]
:targets {:native {:availability :available}
:wasmtime {:availability :available
:requires [:wasi/filesystem]}
:wasm-browser {:availability :requires-adapter
:adapter-kind :javascript
:reason :browser-needs-admitted-file-or-buffer-adapter}}}]}
There are a few important choices in that data.
The capability IDs are the source-facing identities: :rasa.io/read-text and :rasa.io/write-text. There is no parallel hidden vocabulary that users and tools need to translate.
The effects are explicit. Reading text is :fs/read; writing text is :fs/write. Both may suspend in future async-capable lowerings, but Rasa does not make the user write async-colored source for this.
The boundary mode is :copy-value. Small source maps and strings are encoded across the provider boundary and decoded back as Rasa values. That is a contract, not a host implementation accident.
The target availability is explicit. Native and Wasm host are available. Browser is not lied about. Browser filesystem access requires a browser-specific adapter, and until that adapter exists the browser target must say why it is not available.
Provider Artifacts
The provider declaration connects capability identities to artifacts:
{:package/id rasa.io
:package/version "0.1.0"
:abi :rasa-provider-abi-v1
:codec :rasa-data-v1
:capabilities [{:id :rasa.io/read-text
:result-class :value
:signature {:args [{:name :source :type :map}]
:returns :string}}
{:id :rasa.io/write-text
:result-class :value
:signature {:args [{:name :sink :type :map}
{:name :text :type :string}]
:returns :map}}]
:artifacts {:native {:kind :native-dylib
:path "native/librasa_io_provider.dylib"}
:wasmtime {:kind :wasm-component
:path "wasmtime/rasa_io_provider.wasm"
:wit {:package rasa:provider@0.1.0
:world provider}
:requires [:wasi/filesystem]}}}
The Rust source for this provider lives in crates/world/providers/io. That name is deliberate. providers/io.ras is Rasa metadata; provider-rust is implementation source. The provider implements the admitted boundary, but it does not own the language-level meaning of IO.
Native Flow
When native ras runs the file pipeline, the flow is:
- The loader reads the entry source.
- The namespace form loads
rasa.ioandrasa.string. - The reader, expander, analyzer, IR, facts, and checks produce phase data.
- Calls to
io/read-textandio/write-textare seen as calls to Rasa-owned capability identities. - The capability planner checks the
rasa.iocatalog and the selected policy. - Provider confirmation finds the native dynamic library.
- Evaluation runs in the native runtime.
- The provider reads and writes UTF-8 files through the host filesystem.
- Values return to Rasa as normal strings and maps.
Run it:
just rasa-io-provider-package
rm -f target/rasa-text-output.txt
target/release/ras run examples/text/file-text-pipeline.ras
cat target/rasa-text-output.txt
The rendered result should include:
[:input "alpha beta rasa compiler\n"
:normalized "ALPHA-BETA-RASA-COMPILER\n"
:write-status :written
:output-path "target/rasa-text-output.txt"
:storage :host-filesystem
:chars 25
:written-back "ALPHA-BETA-RASA-COMPILER\n"]
And the host file should contain:
ALPHA-BETA-RASA-COMPILER
Inspect the capability plan:
target/release/ras run --phase plan examples/text/file-text-pipeline.ras
The compact output should name both capabilities:
Capability plan: ok (native)
Capabilities:
- rasa.io/read-text available calls=1 effects=fs/read,may-suspend policy=fs/read boundary=copy-value
- rasa.io/write-text available calls=1 effects=fs/write,may-suspend policy=fs/write boundary=copy-value
That is the core point. The filesystem read is not ambient. It is visible, planned, and admitted.
Wasm Host Flow
The same source also runs through ras-wasmtime.
The product shape is:
ras native command
ras-wasmtime native Wasmtime launcher
ras.wasm Wasm component runtime artifact
The Wasm runtime is not the old raw pointer ABI. ras.wasm is a Wasm component with WIT session exports. The host launcher loads it, supplies the package data, and routes admitted capability calls to provider components.
For the IO example, the Wasm provider is also a component:
target/rasa-packages/rasa.io/wasmtime/rasa_io_provider.wasm
Run the same source:
rm -f target/rasa-text-output.txt
target/release/ras-wasmtime run examples/text/file-text-pipeline.ras
cat target/rasa-text-output.txt
The expected Rasa value is the same as native. The file should exist on disk. This is not a virtual workspace. For the wasmtime target, the provider is given a WASI filesystem context by the host, so the admitted provider operation can write the host file.
Confirm the provider:
target/release/ras-wasmtime run --phase confirm examples/text/file-text-pipeline.ras
The confirmation should identify the Wasm provider artifact:
{:id :rasa.io/write-text
:status :confirmed
:result-class :value
:artifact-kind :wasm-component
:artifact-path ".../target/rasa-packages/rasa.io/wasmtime/rasa_io_provider.wasm"
:codec :rasa-data-v1
:abi :rasa-provider-abi-v1}
The source did not change. The runtime product changed.
Policy Denial
The same plan can be denied:
target/release/ras run --phase plan --policy deny-fs examples/text/file-text-pipeline.ras
target/release/ras-wasmtime run --phase plan --policy deny-fs examples/text/file-text-pipeline.ras
Both lanes should report a blocked plan:
Capability plan: blocked (wasmtime)
Capabilities:
- rasa.io/read-text policy-denied calls=1 effects=fs/read,may-suspend policy=fs/read boundary=copy-value reason=policy-denied
- rasa.io/write-text policy-denied calls=1 effects=fs/write,may-suspend policy=fs/write boundary=copy-value reason=policy-denied
That is a useful failure. It tells the operator which capability was requested, which effect was involved, and why execution did not proceed.
REPL Proof
Rasa is meant to stay interactive. The same capability story works in REPL sessions.
Native:
cargo run --quiet -p rasa-repl --example io_repl_checkpoint
cat target/rasa-repl-native-output.txt
Wasm host:
cargo run --quiet -p rasa-runtime-wasmtime --example wasmtime-io-repl-checkpoint
cat target/rasa-repl-wasm-output.txt
The checkpoint evaluates the same sequence of forms:
(ns repl.io.checkpoint
(:require [rasa.io :as io]
[rasa.string :as str]))
(def input
(io/read-text {:path "examples/text/input.txt"}))
(def normalized
(str/replace (str/upper-case input) " " "-"))
(io/write-text {:path "target/rasa-repl-wasm-output.txt"} normalized)
(io/read-text {:path "target/rasa-repl-wasm-output.txt"})
The important part is persistence. The input and normalized definitions are kept in the session, and the admitted capability call works later in that same session. Runtime-Wasm uses a guest session in ras.wasm; native uses the native REPL session. The source interaction remains the same.
You can also try this manually:
target/release/ras repl
or:
target/release/ras-wasmtime repl
Paste the forms one by one. Use different output paths if both sessions are open.
Installation Shape
The product installer now carries the resources needed to make this work outside the workspace:
bin/ras
bin/ras-wasmtime
lib/rasa/ras.wasm
share/rasa/lib/rasa/core.ras
share/rasa/lib/rasa/string.ras
share/rasa/lib/rasa/io.ras
share/rasa/capabilities/rasa.io/package.ras
share/rasa/capabilities/rasa.io/native/librasa_io_provider.dylib
share/rasa/capabilities/rasa.io/wasmtime/rasa_io_provider.wasm
Smoke-test an install:
target/release/ras install user --prefix target/rasa-install-smoke
target/rasa-install-smoke/bin/ras install doctor --prefix target/rasa-install-smoke
target/rasa-install-smoke/bin/ras run examples/text/file-text-pipeline.ras
target/rasa-install-smoke/bin/ras-wasmtime run examples/text/file-text-pipeline.ras
The installed commands discover the source libraries and Rasa-owned capability roots from the prefix. End users should not need workspace-only flags for core Rasa capabilities.
Why This Matters
The IO example is intentionally modest. It is not a web framework, an actor runtime, a package manager, or a database client. It is a proof of the shape Rasa needs before those things become honest.
The source is small:
(io/read-text {:path "examples/text/input.txt"})
But the system can explain it:
source function -> capability identity -> effect -> policy -> provider artifact
-> boundary mode -> runtime product -> returned Rasa value
That is the design center. Rasa should let programs stay simple while making the compiler and runtime honest about the outside world.
Future posts will go deeper into the phase model, Wasm components and WIT, WasmGC lowering, source-defined core functions, conformance pressure, and the path from small capability examples to useful applications.
For now, the first milestone is this: a Rasa program can ask for IO, the system can prove what that means, policy can allow or deny it, and the same source can run natively or through the Wasm host product without changing the language surface.