/// 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 }; })(); "#;