Status: future · Priority: high
Note: This is a future plan, not a commitment. The syntax and API shown here are proposals — they can be completely different when actually implemented.
Morph compiles apps to either a C++ runtime or a completely Rust-native runtime, chosen at project creation:
morph init my-app --lang rust # Rust runtime (100% Rust, no C++ runtime code)
morph init my-app --lang c++ # C++ runtime (current default)When --lang rust is chosen, the C++ runtime is never used — a parallel runtime written entirely in Rust replaces it: windowing, OpenGL rendering, layout, style, reactivity, coroutines, networking, and the JS value model. The .mx files you write are identical — only the backend changes.
morph init my-app --lang rust # rust project (rust-toolchain.toml, Cargo-based build)
morph init my-app --lang c++ # c++ project (g++/CMake-based build, as today)c++ — existing projects and the whole runtime remain untouchedmorph doctor validates the matching toolchain: cargo + rustc (edition 2021+) for rust, g++ for c++src/App.mx ──► MorphParser ──► JSXWalker ──► IRBuilder ──► IRSerializer
│
┌────────────────────────┴───────────────┐
▼ ▼
[Dev: logic.rs → cdylib] [Build: Rust codegen]
dlopen hot reload Jinja2 .rs templates
cargo build → binarymorpher / morph-ir crates the C++ pipeline usesmorph-codegen) get a Rust sibling that emits .rs instead of .cpplogic-<hash>.so Rust cdylib loaded via dlopen — hot reload works exactly like today's logic.so (the dlopen/RTLD_NOLOAD machinery is language-agnostic)cargo build --release (static GLFW/FreeType/HarfBuzz story mirrors morph build --static)| C++ runtime | Rust runtime |
|---|---|
MorphNode tree + layout + style |
MorphNode struct tree + layout + style modules |
GLRenderer (batched quads, SDF shaders) |
OpenGL via glow (or raw FFI) — same shaders, same pipeline |
Compositor thread + lock-free RenderFrame |
Compositor on its own thread + crossbeam/atomics frame swap |
Signal<T> + effects |
Signal<T> with the same auto-subscription semantics |
morph::Task coroutines + next_frame |
Rust async (a small hand-rolled executor — no heavyweight runtime) |
morph::net::fetch on a worker thread |
HTTP on a worker thread (std TcpStream or reqwest) |
JsValue variant + JsNumber/JsString/JsArray/JsObject |
A JsValue enum with the same JS semantics (truthiness, ==, coercion) |
| FreeType + HarfBuzz text | freetype / harfbuzz-rs crates (or FFI to the same system libs) |
| OpenGL 3.3 renderer | glow today — or wgpu, which wraps Vulkan/Metal/D3D12/GL in one crate (see Graphics APIs) |
| Feature gates + linker GC | Rust's dead-code elimination (already excellent) |
Design goal: pixel-identical output between the C++ and Rust runtimes — same layout math, same shaders, same event semantics — so apps behave identically regardless of --lang.
Rust functions from JSX — the Rust mirror of the existing C++ import:
// src/math.rs
pub fn compute(a: f64, b: f64) -> f64 { a * b }// src/App.mx
import { compute } from './math.rs' // exactly like './file.cpp' today
export default function App() {
const [result, setResult] = morphState(0)
return (
<div>
<h1>{compute(6, 7)}</h1> {/* → 42 */}
</div>
)
}JSX functions from Rust — the codegen emits a generated bridge module (the Rust sibling of _morph_state.h):
// generated: _morph_state.rs (exposes signals + JSX functions to Rust)
use morph::prelude::*;
pub fn use_state() -> Signal<f64> { /* the app's signals */ }// src/controller.rs — your Rust logic drives the UI
pub fn increment_counter(state: &Signal<f64>) {
state.set(state.get() + 1.0); // JSX re-renders, like morphState
}#included/mod-ed into the generated translation unit — same mechanism as today's import { fn } from './file.cpp'native config block for rust: [dependencies]-style entries, extern crate paths, cflags/ldflags equivalents → forwarded to cargoThe TSToCppTranslator gets a sibling translator. Type mapping:
| TS | Rust |
|---|---|
string |
String |
number / int / double |
f64 / i64 / f64 |
boolean |
bool |
any |
JsValue (the Rust enum) |
Array<T> |
Vec<JsValue> |
object / Record |
JsObject (HashMap<String, JsValue>) |
Promise<T> |
async / a Result-like future |
MouseEvent / Element |
&MorphEvent / &MorphNode |
morph translate file.ts --lang rust emits .rs. The existing morph check diagnostics apply unchanged — they audit the JS surface, not the backend.
Related: the toolchain is already in Rust (Oxc parsing, native CLI, Python removed) — see Rust Compiler.
.mx files, JSX, CSS, Tailwind, morphState/morphEffect, fetch(), timersnode_modules/morph .d.ts — autocomplete is backend-agnosticmorph dev, morph build, morph run --static, morph check| Building block | State |
|---|---|
C++ runtime + interop pattern (import from './file.cpp', _morph_state.h) |
✅ Shipped — the blueprint to mirror |
| IR pipeline shared by both backends | ✅ Shipped |
dlopen hot-reload machinery (language-agnostic) |
✅ Shipped |
| Jinja2 codegen templates (C++ flavor) | ✅ Shipped |
| Rust runtime | ❌ Not started |
| Rust codegen templates + TS→Rust translator | ❌ Not started |
--lang rust in morph init + morph doctor cargo checks |
❌ Not started |
--lang rust flag, project template (Cargo.toml, rust-toolchain.toml), window + GLFW + GL context via glowexamples/calculator pixel-identical to C++JsValue enum, Signal<T> reactivity, effects, timersmorph translate --lang rust, JSX codegen in Rustimport from './math.rs' + generated _morph_state.rs (bidirectional)logic.rs cdylib + hot reload via dlopenmorph build --static parity, binary-size targetsglow, reqwest); the C++ runtime vendors everything, Rust should probably do the same (vendored crates or FFI to system libs)tokio; Morph's coroutine scheduler is deliberately tinymorph check — same JS audit, plus new rust-specific diagnostics (unsupported crate patterns?)--static stay <1 MB or does the target move?morph init --lang rust + morph doctor cargo checks + rust project templatemath.rs import + _morph_state.rs)cargo build --release parity with C++