542 lines
19 KiB
Markdown
542 lines
19 KiB
Markdown
# DreamStack: Reinventing the UI from First Principles
|
|
|
|
> *What if we threw away every assumption about web UI frameworks and started over — with the sole goal of creating ultra-reactive, dynamic interfaces?*
|
|
|
|
---
|
|
|
|
## Implementation Status ✅
|
|
|
|
DreamStack is **real and running** — 8 Rust crates, 205 tests, 51 compilable examples, 14 registry components, ~7KB runtime.
|
|
|
|
```
|
|
.ds source → ds-parser → ds-analyzer → ds-codegen → JavaScript
|
|
↓ ↓
|
|
ds-types ds-layout
|
|
(type checker) (Cassowary solver)
|
|
```
|
|
|
|
### What Works Today
|
|
|
|
| Feature | Syntax | Status |
|
|
|---------|--------|--------|
|
|
| Signals | `let count = 0` | ✅ Fine-grained, auto-tracked |
|
|
| Derived | `let doubled = count * 2` | ✅ `DS.derived()`, lazy evaluation |
|
|
| Interpolation | `"Count: {count}"` | ✅ Reactive `DS.effect()` |
|
|
| Conditional | `when count > 5 -> text "hi"` | ✅ Mount/unmount |
|
|
| If/else | `if x then a else b` | ✅ |
|
|
| Match | `match state \| Loading -> ...` | ✅ |
|
|
| List rendering | `for item in items -> text item` | ✅ Reactive, keyed |
|
|
| Components | `component Card(title) = ...` | ✅ Reactive props (getter fns) |
|
|
| Component import | `import { Badge } from "./badge"` | ✅ Recursive resolution |
|
|
| Effects | `effect fetch(id): Result` / `perform` | ✅ Algebraic |
|
|
| Springs | `spring(target: 0, stiffness: 300)` | ✅ RK4 physics |
|
|
| Layout | `layout { sidebar.width == 250 }` | ✅ Cassowary solver |
|
|
| Types | `type PositiveInt = Int where x > 0` | ✅ Refinement types |
|
|
| Router | `route "/path" -> body` / `navigate` | ✅ Hash-based |
|
|
| Two-way binding | `input { bind: name }` | ✅ Signal ↔ input |
|
|
| Async resources | `DS.resource()` / `DS.fetchJSON()` | ✅ Loading/Ok/Err |
|
|
| Physics scene | `scene { gravity_y: g } [ circle {...} ]` | ✅ Rapier2D WASM |
|
|
| Constraints | `constrain el.width = expr` | ✅ Reactive solver |
|
|
| Streaming (signal) | `stream on "ws://..." { mode: signal }` | ✅ Bidirectional, versioned diffs |
|
|
| Streaming (receiver) | `stream from "ws://..."` | ✅ Auto-reconnect, field select |
|
|
| Streaming (WebRTC) | `transport: webrtc` | ✅ P2P data channel + WS fallback |
|
|
| Stream output filter | `output: paddleX, ballY, score` | ✅ Skip internal signals |
|
|
| Dev server | `dreamstack dev app.ds` | ✅ File watcher + HMR polling |
|
|
| Playground | `dreamstack playground` | ✅ Monaco editor + live preview |
|
|
| CLI check | `dreamstack check app.ds` | ✅ Signal graph visualization |
|
|
| TSX converter | `dreamstack convert component.tsx` | ✅ React → DreamStack |
|
|
| Component registry | `dreamstack add badge` | ✅ 14 components |
|
|
| Timer merging | `every 33 -> ...` (multiple) | ✅ Single `setInterval` per interval |
|
|
| Sound | `play_tone(440, 60)` | ✅ Built-in oscillator |
|
|
|
|
### CLI Commands
|
|
|
|
```bash
|
|
dreamstack build app.ds # Compile to HTML+JS
|
|
dreamstack dev app.ds # Dev server with hot reload (port 3000)
|
|
dreamstack check app.ds # Analyze signal graph, type check
|
|
dreamstack stream app.ds # Compile + serve with streaming
|
|
dreamstack playground # Monaco editor playground (port 4000)
|
|
dreamstack add badge # Add a registry component
|
|
dreamstack add --list # List all available components
|
|
dreamstack convert file.tsx # Convert React/TSX → DreamStack
|
|
dreamstack convert button --shadcn # Convert from shadcn/ui registry
|
|
dreamstack init my-app # Initialize new project
|
|
```
|
|
|
|
### Registry Components (14)
|
|
|
|
| Component | File | Description |
|
|
|-----------|------|-------------|
|
|
| Alert | `alert.ds` | Status messages with variants |
|
|
| Avatar | `avatar.ds` | Profile images with sizes |
|
|
| Badge | `badge.ds` | Status indicators (success/warning/error/info) |
|
|
| Button | `button.ds` | Click actions with variants |
|
|
| Card | `card.ds` | Content containers |
|
|
| Dialog | `dialog.ds` | Modal overlays |
|
|
| Input | `input.ds` | Text inputs with binding |
|
|
| Progress | `progress.ds` | Progress bars |
|
|
| Select | `select.ds` | Dropdown selects |
|
|
| Separator | `separator.ds` | Visual dividers |
|
|
| Stat | `stat.ds` | Statistic displays (value, label, trend) |
|
|
| Tabs | `tabs.ds` | Tabbed navigation |
|
|
| Toast | `toast.ds` | Notification toasts |
|
|
| Toggle | `toggle.ds` | On/off switches |
|
|
|
|
### Examples (48 compilable .ds files)
|
|
|
|
**Games:**
|
|
- `game-pong.ds` — Multiplayer Pong with keyboard, sound, streaming
|
|
- `game-breakout.ds` — Breakout with 5 rows, collision, score, streaming
|
|
- `game-snake.ds` — Snake game
|
|
- `game-reaction.ds` — Reaction time test
|
|
|
|
**Streaming:**
|
|
- `streaming-counter.ds` — Counter synced across tabs
|
|
- `streaming-clock.ds` — Clock streamed to viewers
|
|
- `streaming-dashboard.ds` — Real-time dashboard via relay
|
|
- `streaming-stats.ds` — Signal stats viewer
|
|
- `streaming-mood.ds` — Collaborative mood board
|
|
- `streaming-physics.ds` — Physics scene streaming
|
|
- `streaming-webrtc.ds` — P2P WebRTC streaming
|
|
- `streaming-receiver.ds` — Generic stream receiver
|
|
- `pong-viewer.ds` — Spectator view for Pong
|
|
- `game-viewer.ds` — Generic game viewer
|
|
|
|
**Audio:**
|
|
- `step-sequencer.ds` — 16-step drum machine with Web Audio API
|
|
- `beats-viewer.ds` — Spectator view for step sequencer
|
|
|
|
**UI Patterns:**
|
|
- `counter.ds` — 3-line counter
|
|
- `list.ds` — Dynamic list with add/remove
|
|
- `todo.ds` / `todomvc.ds` — TodoMVC
|
|
- `form.ds` — Form with validation
|
|
- `router-demo.ds` — Multi-page routing
|
|
- `dashboard.ds` — Dashboard layout
|
|
- `component-gallery.ds` — Component showcase
|
|
- `showcase.ds` — Full feature showcase
|
|
- `project-manager.ds` — Kanban-style project tracker
|
|
- `springs.ds` — Spring physics animations
|
|
|
|
**Language Features:**
|
|
- `language-features.ds` — Comprehensive syntax demo
|
|
- `refined-types.ds` — Refinement type guards
|
|
- `import-demo.ds` — Component imports
|
|
- `slot-demo.ds` — Component children/slots
|
|
- `each-demo.ds` — `each` loop rendering
|
|
- `when-else-demo.ds` — Conditional rendering
|
|
- `multi-action.ds` / `timer-multi-action.ds` — Multi-statement handlers
|
|
- `callback-demo.ds` — Callback props
|
|
- `physics.ds` — Rapier2D physics scene
|
|
- `bench-signals.ds` — Signal performance benchmarks
|
|
|
|
### DreamStack vs React
|
|
|
|
| | **DreamStack** | **React** |
|
|
|---|---|---|
|
|
| Reactivity | Fine-grained signals, surgical DOM | VDOM diff, re-render subtrees |
|
|
| State | `count += 1` direct | `setState(c => c+1)` immutable |
|
|
| Derived | `let d = count * 2` auto | `useMemo(() => ..., [deps])` manual |
|
|
| Effects | Auto-tracked, algebraic | `useEffect(..., [deps])` manual |
|
|
| Conditional | `when x -> text "y"` | `{x && <span>y</span>}` |
|
|
| Lists | `for item in items -> ...` | `{items.map(i => ...)}` |
|
|
| Router | `route "/path" -> body` | `react-router` (external) |
|
|
| Forms | `input { bind: name }` | `useState` + `onChange` (manual) |
|
|
| Animation | Built-in springs | framer-motion (external) |
|
|
| Physics | Built-in Rapier2D scene | matter.js (external) |
|
|
| Layout | Built-in Cassowary | CSS only |
|
|
| Streaming | Built-in bidirectional | WebSocket libraries (external) |
|
|
| Types | Native refinement types | TypeScript (external) |
|
|
| Bundle | **~7KB** | **~175KB** |
|
|
| Ecosystem | 48 examples, 14 components | Massive |
|
|
|
|
### Benchmarks (signal propagation)
|
|
|
|
| Benchmark | Ops/sec | Signals |
|
|
|-----------|---------|---------|
|
|
| Wide Fan-Out (1→1000) | 46K | 1,001 |
|
|
| Deep Chain (100 deep) | 399K | 100 |
|
|
| Diamond Dependency | 189K | 4 |
|
|
| Batch Update (50) | 61K | 50 |
|
|
| Mixed Read/Write | 242K | 10 |
|
|
|
|
---
|
|
|
|
## Quick Start
|
|
|
|
### Counter (3 lines of logic)
|
|
|
|
```
|
|
let count = 0
|
|
|
|
view counter =
|
|
column [
|
|
text "Count: {count}"
|
|
button "+" { click: count += 1 }
|
|
]
|
|
```
|
|
|
|
### Multiplayer Pong (40 lines)
|
|
|
|
```
|
|
let ballX = 300
|
|
let ballY = 200
|
|
let bvx = 4
|
|
let bvy = 3
|
|
let p1y = 180
|
|
|
|
on keydown(ev) -> p1y = if ev.key == "ArrowUp" then p1y - 30 else p1y
|
|
|
|
every 33 -> ballX = ballX + bvx
|
|
every 33 -> ballY = ballY + bvy
|
|
every 33 -> bvx = if ballX > 590 then -4 else bvx
|
|
every 33 -> bvy = if ballY < 5 then 3 else bvy
|
|
|
|
stream pong on "ws://localhost:9100/peer/pong" {
|
|
mode: signal,
|
|
output: ballX, ballY, p1y
|
|
}
|
|
|
|
view pong =
|
|
stack { style: "position:relative; width:600px; height:400px; background:#1a1a2e" } [
|
|
column { style: "...ball styles...", top: ballY, left: ballX } []
|
|
column { style: "...paddle...", top: p1y } []
|
|
]
|
|
```
|
|
|
|
### Spectator View (5 lines)
|
|
|
|
```
|
|
let game = stream from "ws://localhost:9100/peer/pong"
|
|
select [ballX, ballY, p1y]
|
|
|
|
view viewer =
|
|
stack { style: "..." } [
|
|
column { style: "...ball...", top: game.ballY, left: game.ballX } []
|
|
column { style: "...paddle...", top: game.p1y } []
|
|
]
|
|
```
|
|
|
|
### Run It
|
|
|
|
```bash
|
|
# Install
|
|
cargo install --path compiler/ds-cli
|
|
|
|
# Build
|
|
dreamstack build examples/counter.ds -o dist
|
|
|
|
# Dev server (with hot reload)
|
|
dreamstack dev examples/game-pong.ds
|
|
|
|
# With streaming
|
|
cargo run -p ds-stream # Tab 1: start relay
|
|
dreamstack dev examples/game-pong.ds # Tab 2: play
|
|
open dist/index.html # Tab 3: spectate (pong-viewer.ds)
|
|
```
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
### Compiler Pipeline
|
|
|
|
```
|
|
┌─────────────┐
|
|
.ds source ───────►│ ds-parser │──► AST (Program)
|
|
└──────┬──────┘
|
|
│
|
|
┌──────▼──────┐
|
|
│ ds-analyzer │──► SignalGraph (DAG)
|
|
└──────┬──────┘ + DomBindings
|
|
│ + SignalManifest
|
|
┌──────▼──────┐
|
|
│ ds-codegen │──► Single-file HTML+JS
|
|
└──────┬──────┘ (~7KB runtime)
|
|
│
|
|
┌──────▼──────┐
|
|
│ ds-cli │──► dev/build/stream/check
|
|
└─────────────┘
|
|
```
|
|
|
|
### Signal Graph (Compile-Time)
|
|
|
|
```
|
|
Signal: count (Source)
|
|
├──► Derived: doubled (DS.derived)
|
|
│ └──► [DOM] TextNode "Doubled: {doubled}"
|
|
├──► [DOM] TextNode "Count: {count}"
|
|
└──► [Stream] _streamDiff("count", count.value)
|
|
```
|
|
|
|
- **No virtual DOM.** Compiler maps signals → specific DOM nodes at build time
|
|
- **No re-rendering.** Components execute once, set up reactive bindings
|
|
- **No dependency arrays.** Compiler infers all dependencies from code
|
|
- **Timer merging.** All `every 33` statements share one `setInterval`
|
|
|
|
### Reactive Runtime
|
|
|
|
```javascript
|
|
// Source signal
|
|
const count = DS.signal(0);
|
|
|
|
// Derived signal (auto-updates)
|
|
const doubled = DS.derived(() => count.value * 2);
|
|
|
|
// Effect (auto-tracked, re-runs on dependency change)
|
|
DS.effect(() => { el.textContent = `Count: ${count.value}`; });
|
|
|
|
// Component props: getter functions for live reactivity
|
|
DS_Badge({ label: () => `Score: ${score.value}` });
|
|
```
|
|
|
|
### Stream Protocol (16-byte binary header)
|
|
|
|
```
|
|
┌────────┬─────────┬──────────┬────────────┬───────┬────────┬────────┐
|
|
│ type │ flags │ seq │ timestamp │ width │ height │ length │
|
|
│ u8 │ u8 │ u16 │ u32 │ u16 │ u16 │ u32 │
|
|
└────────┴─────────┴──────────┴────────────┴───────┴────────┴────────┘
|
|
```
|
|
|
|
| Mode | What's Sent | Bandwidth |
|
|
|------|-------------|-----------|
|
|
| `signal` (default) | JSON diffs of changed signals | ~2 KB/s |
|
|
| `delta` | XOR + RLE compressed pixel deltas | ~50 KB/s |
|
|
| `pixel` | Raw RGBA framebuffer | ~30 MB/s |
|
|
|
|
Features:
|
|
- **Bidirectional sync** with per-signal version counters
|
|
- **Output filtering** — only listed signals are broadcast
|
|
- **Exponential reconnect** — 2s → 4s → 8s, max 30s, reset on success
|
|
- **WebRTC data channel** with WebSocket fallback
|
|
- **Deduplication** — skip unchanged values
|
|
|
|
---
|
|
|
|
## Vision
|
|
|
|
React was revolutionary in 2013. But it carries a decade of compromises: the virtual DOM, hooks with manual dependency arrays, re-rendering entire subtrees, CSS from 1996, animations bolted on as an afterthought. DreamStack asks: **what would we build today if none of that existed?**
|
|
|
|
The answer is a unified system where **UI is data, reactivity is automatic, effects are composable values, layout is constraint-based, animation is physics-native, and the editor and runtime are one.**
|
|
|
|
---
|
|
|
|
## The Language
|
|
|
|
Not Clojure. Not TypeScript. A new language that steals the best ideas from everywhere.
|
|
|
|
### Core Properties
|
|
|
|
| Property | Inspiration | Why |
|
|
|---|---|---|
|
|
| **Homoiconic** | Clojure, Lisp | UI = data. Code = data. Everything is transformable |
|
|
| **Refinement types** | Liquid Haskell | `type Percent = Int where value >= 0 and value <= 100` |
|
|
| **Algebraic effects** | Eff, Koka, OCaml 5 | Side effects as first-class, composable, interceptable values |
|
|
| **Reactive by default** | Svelte, Solid, Excel | No `useState`, no subscriptions. Assignment *is* the API |
|
|
| **Structural typing** | TypeScript, Go | Flexible composition without class hierarchies |
|
|
| **Compiled to JS** | Svelte, Solid | Near-zero runtime overhead |
|
|
|
|
### Syntax Reference
|
|
|
|
```
|
|
-- Signals (mutable state)
|
|
let count = 0
|
|
let name = "world"
|
|
let items = [1, 2, 3]
|
|
|
|
-- Derived signals (auto-updated)
|
|
let doubled = count * 2
|
|
let greeting = "Hello, {name}!"
|
|
|
|
-- Refinement types
|
|
type PositiveInt = Int where value > 0
|
|
let score: PositiveInt = 10
|
|
|
|
-- Event handlers
|
|
on keydown(ev) -> count = if ev.key == "ArrowUp" then count + 1 else count
|
|
|
|
-- Timers
|
|
every 33 -> ballX = ballX + velocity
|
|
|
|
-- Conditional rendering
|
|
when count > 10 -> text "High!"
|
|
|
|
-- Pattern matching
|
|
match state
|
|
"loading" -> spinner
|
|
"error" -> text "Failed"
|
|
_ -> text "Ready"
|
|
|
|
-- For loops
|
|
for item, i in items -> text "{i}: {item}"
|
|
|
|
-- Components
|
|
component Card(title, children) =
|
|
column { variant: "card" } [
|
|
text title { variant: "card-title" }
|
|
slot
|
|
]
|
|
|
|
-- Imports
|
|
import { Badge, Card } from "../registry/components/badge"
|
|
|
|
-- Routes
|
|
route "/" -> text "Home"
|
|
route "/about" -> text "About"
|
|
|
|
-- Springs
|
|
let x = spring(target: 0, stiffness: 300, damping: 30)
|
|
|
|
-- Streaming
|
|
stream app on "ws://localhost:9100/peer/app" {
|
|
mode: signal,
|
|
output: count, name
|
|
}
|
|
|
|
-- Receiving streams
|
|
let remote = stream from "ws://localhost:9100/peer/app"
|
|
select [count, name]
|
|
|
|
-- Views
|
|
view main =
|
|
column [
|
|
text "Hello {name}"
|
|
button "Click" { click: count += 1 }
|
|
]
|
|
```
|
|
|
|
---
|
|
|
|
## Effect System: Algebraic Effects
|
|
|
|
```
|
|
effect fetchUser(id: UserId): Result<User, ApiError>
|
|
|
|
view profile(id: UserId) =
|
|
let user = perform fetchUser(id)
|
|
match user
|
|
Loading -> skeleton-loader
|
|
Ok(u) -> column [ avatar u.photo, text u.name ]
|
|
Err(e) -> error-card e | with retry: perform fetchUser(id)
|
|
```
|
|
|
|
---
|
|
|
|
## Layout: Constraint-Based
|
|
|
|
```
|
|
layout dashboard {
|
|
sidebar.width == 250
|
|
main.x == sidebar.x + sidebar.width
|
|
main.width == parent.width - sidebar.width [strong]
|
|
}
|
|
```
|
|
|
|
Uses a Cassowary constraint solver. Constraints are reactive — when viewport changes, layout re-solves in a single pass.
|
|
|
|
---
|
|
|
|
## Animation: Physics-Based Springs
|
|
|
|
```
|
|
let panel_x = spring(target: 0, stiffness: 300, damping: 30)
|
|
|
|
on toggle_sidebar ->
|
|
panel_x.target = if open then 250 else 0
|
|
|
|
view sidebar =
|
|
panel { x: panel_x } [ nav-items ]
|
|
```
|
|
|
|
Springs are interruptible, gesture-driven, and composable through the same signal system.
|
|
|
|
---
|
|
|
|
## Physics: Rapier2D WASM
|
|
|
|
```
|
|
let gravity_y = 980
|
|
|
|
view main =
|
|
scene { width: 700, height: 450, gravity_y: gravity_y } [
|
|
circle { x: 200, y: 80, radius: 35, color: "#8b5cf6" }
|
|
rect { x: 500, y: 100, width: 80, height: 50, color: "#10b981" }
|
|
]
|
|
|
|
button "Anti-Gravity" { click: gravity_y = -500 }
|
|
```
|
|
|
|
---
|
|
|
|
## Streaming: Built-In Multiplayer
|
|
|
|
Any DreamStack app becomes multiplayer with one declaration:
|
|
|
|
```
|
|
stream pong on "ws://localhost:9100/peer/pong" {
|
|
mode: signal,
|
|
output: ballX, ballY, paddleY, score
|
|
}
|
|
```
|
|
|
|
The compiler automatically:
|
|
1. Connects to the relay via WebSocket
|
|
2. Wraps signal mutations with `_streamDiff()` calls
|
|
3. Sends versioned JSON diffs (conflict resolution via version counters)
|
|
4. Filters to only broadcast `output` signals (skips internals like velocity)
|
|
5. Reconnects with exponential backoff on disconnect
|
|
|
|
Viewers connect with `stream from`:
|
|
```
|
|
let game = stream from "ws://localhost:9100/peer/pong"
|
|
select [ballX, ballY, paddleY, score]
|
|
```
|
|
|
|
---
|
|
|
|
## Comparison
|
|
|
|
| Capability | React | Svelte 5 | Solid.js | Flutter | **DreamStack** |
|
|
|---|---|---|---|---|---|
|
|
| Reactivity | Pull (VDOM diff) | Runes | Signals | Rebuild | **Compile-time DAG** |
|
|
| Side effects | `useEffect` + deps | `$effect` | `createEffect` | Lifecycle | **Algebraic effects** |
|
|
| Layout | CSS (external) | CSS (scoped) | CSS (external) | Constraints | **Cassowary (native)** |
|
|
| Animation | 3rd party | 3rd party | 3rd party | Physics (native) | **Springs (native)** |
|
|
| Streaming | WebSocket libs | WebSocket libs | WebSocket libs | None | **Built-in bidirectional** |
|
|
| Runtime | ~175KB | ~2KB | ~7KB | ~2MB | **~7KB** |
|
|
| Type safety | TypeScript | TypeScript | TypeScript | Dart | **Refinement types** |
|
|
| UI = Data | No (JSX) | No (templates) | No (JSX) | No (widgets) | **Yes (homoiconic)** |
|
|
|
|
---
|
|
|
|
## Design Philosophy
|
|
|
|
1. **The compiler is the framework.** Move work from runtime to compile time
|
|
2. **Reactivity is the default.** Every value is live. You opt *out*, not *in*
|
|
3. **Effects are values.** First-class, composable, interceptable
|
|
4. **Layout and animation are core.** Not CSS bolt-ons or third-party libraries
|
|
5. **The editor and runtime are one.** Bidirectional structural editing
|
|
6. **UI is data, all the way down.** If you can't `map` over your UI, your abstraction is wrong
|
|
7. **Any bitstream in → any bitstream out.** The UI is just one codec
|
|
|
|
---
|
|
|
|
## Next Steps
|
|
|
|
### Phase 1: Polish (estimated ~2h)
|
|
- [ ] **Better error messages** — source context with line + caret in parser errors
|
|
- [ ] **Route integration test** — multi-page example exercising `route` + `navigate`
|
|
- [ ] **WebRTC integration test** — verify P2P data channel transport
|
|
- [ ] **Layout constraint test** — example using Cassowary solver
|
|
|
|
### Phase 2: New Examples (estimated ~2h)
|
|
- [ ] **Tetris** — stress-test grid, timers, collision, streaming
|
|
- [ ] **Todo with sync** — CRUD + bidirectional streaming sync
|
|
- [ ] **Snake polish** — cleanup existing game, add streaming
|
|
|
|
### Phase 3: Ecosystem (estimated ~1.5h)
|
|
- [ ] **Component gallery** — live showcase of all 14 components
|
|
- [ ] **Crates.io publish** — `cargo install dreamstack`
|
|
- [ ] **Self-hosted playground** — deploy playground as web app
|