/// JavaScript emitter — generates executable JS from DreamStack AST + signal graph.
use ds_parser::*;
use ds_analyzer::{SignalGraph, SignalKind, AnalyzedView, InitialValue};
pub struct JsEmitter {
output: String,
indent: usize,
node_id_counter: usize,
}
impl JsEmitter {
pub fn new() -> Self {
Self {
output: String::new(),
indent: 0,
node_id_counter: 0,
}
}
/// Generate a complete HTML page with embedded runtime and compiled app.
pub fn emit_html(program: &Program, graph: &SignalGraph, views: &[AnalyzedView]) -> String {
let mut emitter = Self::new();
let app_js = emitter.emit_program(program, graph, views);
format!(
r#"
DreamStack App
"#
)
}
/// Generate the application JS module.
pub fn emit_program(&mut self, program: &Program, graph: &SignalGraph, views: &[AnalyzedView]) -> String {
self.output.clear();
self.emit_line("// DreamStack compiled output");
self.emit_line("// Generated by dreamstack compiler v0.1.0");
self.emit_line("");
self.emit_line("(function() {");
self.indent += 1;
// Phase 1: Create all signals
self.emit_line("// ── Signals ──");
for node in &graph.nodes {
match &node.kind {
SignalKind::Source => {
let init = match &node.initial_value {
Some(InitialValue::Int(n)) => format!("{n}"),
Some(InitialValue::Float(n)) => format!("{n}"),
Some(InitialValue::Bool(b)) => format!("{b}"),
Some(InitialValue::String(s)) => format!("\"{s}\""),
None => "null".to_string(),
};
self.emit_line(&format!("const {} = DS.signal({});", node.name, init));
}
SignalKind::Derived => {
// Find the let declaration to get the expression
let expr = self.find_let_expr(program, &node.name);
if let Some(expr) = expr {
let js_expr = self.emit_expr(expr);
self.emit_line(&format!(
"const {} = DS.derived(() => {});",
node.name, js_expr
));
}
}
SignalKind::Handler { .. } => {} // Handled later
}
}
self.emit_line("");
// Phase 2: Build views
self.emit_line("// ── Views ──");
for decl in &program.declarations {
if let Declaration::View(view) = decl {
self.emit_view(view, graph);
}
}
// Phase 3: Event handlers
self.emit_line("");
self.emit_line("// ── Handlers ──");
for decl in &program.declarations {
if let Declaration::OnHandler(handler) = decl {
self.emit_handler(handler);
}
}
// Phase 4: Mount to DOM
self.emit_line("");
self.emit_line("// ── Mount ──");
if let Some(view) = views.first() {
self.emit_line(&format!(
"document.getElementById('ds-root').appendChild(view_{}());",
view.name
));
}
self.indent -= 1;
self.emit_line("})();");
self.output.clone()
}
fn emit_view(&mut self, view: &ViewDecl, graph: &SignalGraph) {
self.emit_line(&format!("function view_{}() {{", view.name));
self.indent += 1;
let root_id = self.emit_view_expr(&view.body, graph);
self.emit_line(&format!("return {};", root_id));
self.indent -= 1;
self.emit_line("}");
}
/// Emit a view expression and return the variable name of the created DOM node.
fn emit_view_expr(&mut self, expr: &Expr, graph: &SignalGraph) -> String {
match expr {
Expr::Container(container) => {
let node_var = self.next_node_id();
let tag = match &container.kind {
ContainerKind::Column => "div",
ContainerKind::Row => "div",
ContainerKind::Stack => "div",
ContainerKind::Panel => "div",
ContainerKind::Form => "form",
ContainerKind::List => "ul",
ContainerKind::Custom(s) => s,
};
let class = match &container.kind {
ContainerKind::Column => "ds-column",
ContainerKind::Row => "ds-row",
ContainerKind::Stack => "ds-stack",
ContainerKind::Panel => "ds-panel",
_ => "",
};
self.emit_line(&format!("const {} = document.createElement('{}');", node_var, tag));
if !class.is_empty() {
self.emit_line(&format!("{}.className = '{}';", node_var, class));
}
// Handle container props
for (key, val) in &container.props {
let js_val = self.emit_expr(val);
if graph.name_to_id.contains_key(&js_val) || self.is_signal_ref(&js_val) {
self.emit_line(&format!(
"DS.effect(() => {{ {}.style.{} = {}.value + 'px'; }});",
node_var, key, js_val
));
} else {
self.emit_line(&format!("{}.style.{} = {};", node_var, key, js_val));
}
}
// Emit children
for child in &container.children {
let child_var = self.emit_view_expr(child, graph);
self.emit_line(&format!("{}.appendChild({});", node_var, child_var));
}
node_var
}
Expr::Element(element) => {
let node_var = self.next_node_id();
let html_tag = match element.tag.as_str() {
"text" => "span",
"button" => "button",
"input" => "input",
"image" | "avatar" => "img",
"link" => "a",
"label" => "label",
"spinner" => "div",
"skeleton" => "div",
_ => "div",
};
self.emit_line(&format!("const {} = document.createElement('{}');", node_var, html_tag));
self.emit_line(&format!("{}.className = 'ds-{}';", node_var, element.tag));
// Handle text content / arguments
for arg in &element.args {
match arg {
Expr::StringLit(s) => {
if let Some(StringSegment::Literal(text)) = s.segments.first() {
self.emit_line(&format!(
"{}.textContent = \"{}\";",
node_var,
text.replace('"', "\\\"")
));
}
}
Expr::Ident(name) => {
// Reactive text binding!
self.emit_line(&format!(
"DS.effect(() => {{ {}.textContent = {}.value; }});",
node_var, name
));
}
_ => {
let js = self.emit_expr(arg);
self.emit_line(&format!("{}.textContent = {};", node_var, js));
}
}
}
// Handle props (event handlers, attributes)
for (key, val) in &element.props {
match key.as_str() {
"click" | "input" | "change" | "submit" | "keydown" | "keyup" | "mousedown" | "mouseup" => {
let handler_js = self.emit_event_handler_expr(val);
let dom_event = match key.as_str() {
"click" => "click",
"input" => "input",
"change" => "change",
"submit" => "submit",
"keydown" => "keydown",
"keyup" => "keyup",
"mousedown" => "mousedown",
"mouseup" => "mouseup",
_ => key,
};
self.emit_line(&format!(
"{}.addEventListener('{}', (e) => {{ {}; DS.flush(); }});",
node_var, dom_event, handler_js
));
}
"class" => {
let js = self.emit_expr(val);
self.emit_line(&format!("{}.className += ' ' + {};", node_var, js));
}
_ => {
let js = self.emit_expr(val);
self.emit_line(&format!("{}.setAttribute('{}', {});", node_var, key, js));
}
}
}
node_var
}
Expr::When(cond, body) => {
let anchor_var = self.next_node_id();
let container_var = self.next_node_id();
let cond_js = self.emit_expr(cond);
self.emit_line(&format!("const {} = document.createComment('when');", anchor_var));
self.emit_line(&format!("let {} = null;", container_var));
// Build the conditional child
self.emit_line("DS.effect(() => {");
self.indent += 1;
self.emit_line(&format!("const show = {};", cond_js));
self.emit_line(&format!("if (show && !{}) {{", container_var));
self.indent += 1;
let child_var = self.emit_view_expr(body, graph);
self.emit_line(&format!("{} = {};", container_var, child_var));
self.emit_line(&format!(
"{}.parentNode.insertBefore({}, {}.nextSibling);",
anchor_var, container_var, anchor_var
));
self.indent -= 1;
self.emit_line(&format!("}} else if (!show && {}) {{", container_var));
self.indent += 1;
self.emit_line(&format!("{}.remove();", container_var));
self.emit_line(&format!("{} = null;", container_var));
self.indent -= 1;
self.emit_line("}");
self.indent -= 1;
self.emit_line("});");
anchor_var
}
Expr::Match(scrutinee, arms) => {
let container_var = self.next_node_id();
self.emit_line(&format!("const {} = document.createElement('div');", container_var));
self.emit_line(&format!("{}.className = 'ds-match';", container_var));
let scrutinee_js = self.emit_expr(scrutinee);
self.emit_line("DS.effect(() => {");
self.indent += 1;
self.emit_line(&format!("const val = {};", scrutinee_js));
self.emit_line(&format!("{}.innerHTML = '';", container_var));
for (i, arm) in arms.iter().enumerate() {
let prefix = if i == 0 { "if" } else { "} else if" };
let pattern_js = self.emit_pattern_check(&arm.pattern, "val");
self.emit_line(&format!("{} ({}) {{", prefix, pattern_js));
self.indent += 1;
let child = self.emit_view_expr(&arm.body, graph);
self.emit_line(&format!("{}.appendChild({});", container_var, child));
self.indent -= 1;
}
self.emit_line("}");
self.indent -= 1;
self.emit_line("});");
container_var
}
// Fallback: just create a text node
_ => {
let node_var = self.next_node_id();
let js = self.emit_expr(expr);
self.emit_line(&format!(
"const {} = document.createTextNode({});",
node_var, js
));
node_var
}
}
}
fn emit_handler(&mut self, handler: &OnHandler) {
let handler_js = self.emit_event_handler_expr(&handler.body);
if let Some(param) = &handler.param {
self.emit_line(&format!(
"DS.onEvent('{}', ({}) => {{ {}; DS.flush(); }});",
handler.event, param, handler_js
));
} else {
self.emit_line(&format!(
"DS.onEvent('{}', () => {{ {}; DS.flush(); }});",
handler.event, handler_js
));
}
}
// ── Expression emitters ─────────────────────────────
fn emit_expr(&self, expr: &Expr) -> String {
match expr {
Expr::IntLit(n) => format!("{n}"),
Expr::FloatLit(n) => format!("{n}"),
Expr::BoolLit(b) => format!("{b}"),
Expr::StringLit(s) => {
if s.segments.len() == 1 {
if let StringSegment::Literal(text) = &s.segments[0] {
return format!("\"{}\"", text.replace('"', "\\\""));
}
}
// Template literal with interpolation
let mut parts = Vec::new();
for seg in &s.segments {
match seg {
StringSegment::Literal(text) => parts.push(text.clone()),
StringSegment::Interpolation(expr) => {
parts.push(format!("${{{}}}", self.emit_expr(expr)));
}
}
}
format!("`{}`", parts.join(""))
}
Expr::Ident(name) => format!("{name}.value"),
Expr::DotAccess(base, field) => {
let base_js = self.emit_expr(base);
format!("{base_js}.{field}")
}
Expr::BinOp(left, op, right) => {
let l = self.emit_expr(left);
let r = self.emit_expr(right);
let op_str = match op {
BinOp::Add => "+",
BinOp::Sub => "-",
BinOp::Mul => "*",
BinOp::Div => "/",
BinOp::Mod => "%",
BinOp::Eq => "===",
BinOp::Neq => "!==",
BinOp::Lt => "<",
BinOp::Gt => ">",
BinOp::Lte => "<=",
BinOp::Gte => ">=",
BinOp::And => "&&",
BinOp::Or => "||",
};
format!("({l} {op_str} {r})")
}
Expr::UnaryOp(op, inner) => {
let inner_js = self.emit_expr(inner);
match op {
UnaryOp::Neg => format!("(-{inner_js})"),
UnaryOp::Not => format!("(!{inner_js})"),
}
}
Expr::Call(name, args) => {
let args_js: Vec = args.iter().map(|a| self.emit_expr(a)).collect();
format!("{}({})", name, args_js.join(", "))
}
Expr::If(cond, then_b, else_b) => {
let c = self.emit_expr(cond);
let t = self.emit_expr(then_b);
let e = self.emit_expr(else_b);
format!("({c} ? {t} : {e})")
}
Expr::Lambda(params, body) => {
let body_js = self.emit_expr(body);
format!("({}) => {}", params.join(", "), body_js)
}
Expr::Record(fields) => {
let fields_js: Vec = fields
.iter()
.map(|(k, v)| format!("{}: {}", k, self.emit_expr(v)))
.collect();
format!("{{ {} }}", fields_js.join(", "))
}
Expr::List(items) => {
let items_js: Vec = items.iter().map(|i| self.emit_expr(i)).collect();
format!("[{}]", items_js.join(", "))
}
_ => "null".to_string(),
}
}
/// Emit an event handler expression (assignment, etc.)
fn emit_event_handler_expr(&self, expr: &Expr) -> String {
match expr {
Expr::Assign(target, op, value) => {
let target_js = match target.as_ref() {
Expr::Ident(name) => name.clone(),
Expr::DotAccess(base, field) => {
format!("{}.{}", self.emit_expr(base), field)
}
_ => self.emit_expr(target),
};
let value_js = self.emit_expr(value);
match op {
AssignOp::Set => format!("{target_js}.value = {value_js}"),
AssignOp::AddAssign => format!("{target_js}.value += {value_js}"),
AssignOp::SubAssign => format!("{target_js}.value -= {value_js}"),
}
}
Expr::Block(exprs) => {
let stmts: Vec = exprs.iter().map(|e| self.emit_event_handler_expr(e)).collect();
stmts.join("; ")
}
_ => self.emit_expr(expr),
}
}
/// Emit a pattern matching condition check.
fn emit_pattern_check(&self, pattern: &Pattern, scrutinee: &str) -> String {
match pattern {
Pattern::Wildcard => "true".to_string(),
Pattern::Ident(name) => {
// Bind: always true, but assign
format!("(({name} = {scrutinee}), true)")
}
Pattern::Constructor(name, _fields) => {
format!("{scrutinee} === '{name}'")
}
Pattern::Literal(expr) => {
let val = self.emit_expr(expr);
format!("{scrutinee} === {val}")
}
}
}
// ── Helpers ──────────────────────────────────────────
fn next_node_id(&mut self) -> String {
let id = self.node_id_counter;
self.node_id_counter += 1;
format!("n{id}")
}
fn emit_line(&mut self, line: &str) {
for _ in 0..self.indent {
self.output.push_str(" ");
}
self.output.push_str(line);
self.output.push('\n');
}
fn find_let_expr<'a>(&self, program: &'a Program, name: &str) -> Option<&'a Expr> {
for decl in &program.declarations {
if let Declaration::Let(let_decl) = decl {
if let_decl.name == name {
return Some(&let_decl.value);
}
}
}
None
}
fn is_signal_ref(&self, expr: &str) -> bool {
// Simple heuristic: if it's a single identifier, it's likely a signal
expr.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
}
}
impl Default for JsEmitter {
fn default() -> Self {
Self::new()
}
}
/// Minimal CSS reset and layout classes.
const CSS_RESET: &str = r#"
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
background: #0a0a0f;
color: #e2e8f0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
#ds-root {
width: 100%;
max-width: 800px;
padding: 2rem;
}
.ds-column {
display: flex;
flex-direction: column;
gap: 1rem;
}
.ds-row {
display: flex;
flex-direction: row;
gap: 1rem;
align-items: center;
}
.ds-stack {
position: relative;
}
.ds-panel {
position: absolute;
}
.ds-text, .ds-text span {
font-size: 1.25rem;
color: #e2e8f0;
}
.ds-button, button.ds-button {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 12px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 4px 15px rgba(99, 102, 241, 0.3);
}
.ds-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4);
}
.ds-button:active {
transform: translateY(0);
}
.ds-input, input.ds-input {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #e2e8f0;
padding: 0.75rem 1rem;
border-radius: 12px;
font-size: 1rem;
outline: none;
transition: border-color 0.2s;
}
.ds-input:focus {
border-color: #6366f1;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
}
.ds-spinner {
width: 2rem;
height: 2rem;
border: 3px solid rgba(255,255,255,0.1);
border-top-color: #6366f1;
border-radius: 50%;
animation: ds-spin 0.8s linear infinite;
}
@keyframes ds-spin {
to { transform: rotate(360deg); }
}
@keyframes ds-fade-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.ds-fade-in {
animation: ds-fade-in 0.3s ease-out;
}
"#;
/// The DreamStack client-side reactive runtime (~3KB).
const RUNTIME_JS: &str = r#"
const DS = (() => {
// ── Signal System ──
let currentEffect = null;
let batchDepth = 0;
let pendingEffects = new Set();
class Signal {
constructor(initialValue) {
this._value = initialValue;
this._subscribers = new Set();
}
get value() {
if (currentEffect) {
this._subscribers.add(currentEffect);
}
return this._value;
}
set value(newValue) {
if (this._value === newValue) return;
this._value = newValue;
if (batchDepth > 0) {
for (const sub of this._subscribers) {
pendingEffects.add(sub);
}
} else {
for (const sub of [...this._subscribers]) {
sub._run();
}
}
}
}
class Derived {
constructor(fn) {
this._fn = fn;
this._value = undefined;
this._dirty = true;
this._subscribers = new Set();
this._effect = new Effect(() => {
this._value = this._fn();
this._dirty = false;
// Notify our subscribers
if (batchDepth > 0) {
for (const sub of this._subscribers) {
pendingEffects.add(sub);
}
} else {
for (const sub of [...this._subscribers]) {
sub._run();
}
}
});
this._effect._run();
}
get value() {
if (currentEffect) {
this._subscribers.add(currentEffect);
}
return this._value;
}
}
class Effect {
constructor(fn) {
this._fn = fn;
this._disposed = false;
}
_run() {
if (this._disposed) return;
const prev = currentEffect;
currentEffect = this;
try {
this._fn();
} finally {
currentEffect = prev;
}
}
dispose() {
this._disposed = true;
}
}
// ── Public API ──
function signal(value) {
return new Signal(value);
}
function derived(fn) {
return new Derived(fn);
}
function effect(fn) {
const eff = new Effect(fn);
eff._run();
return eff;
}
function batch(fn) {
batchDepth++;
try {
fn();
} finally {
batchDepth--;
if (batchDepth === 0) {
flush();
}
}
}
function flush() {
const effects = [...pendingEffects];
pendingEffects.clear();
for (const eff of effects) {
eff._run();
}
}
// ── Event System ──
const eventHandlers = {};
function onEvent(name, handler) {
if (!eventHandlers[name]) eventHandlers[name] = [];
eventHandlers[name].push(handler);
}
function emit(name, data) {
const handlers = eventHandlers[name];
if (handlers) {
batch(() => {
for (const h of handlers) {
h(data);
}
});
}
}
return { signal, derived, effect, batch, flush, onEvent, emit, Signal, Derived, Effect };
})();
"#;