feat: DreamStack compiler foundation — Phase 0/1
Complete compiler pipeline from .ds source to reactive browser apps:
- ds-parser: lexer (string interpolation, operators, keywords) + recursive
descent parser with operator precedence + full AST types
- ds-analyzer: signal graph extraction (source/derived classification),
topological sort for glitch-free propagation, DOM binding analysis
- ds-codegen: JavaScript emitter with embedded reactive runtime (~3KB
signal/derived/effect system) and dark-theme CSS design system
- ds-cli: build (compile to HTML+JS), dev (live server), check (analyze)
Verified working: source signals, derived signals, event handlers,
conditional rendering (when), 12 unit tests passing, 6.8KB output.
2026-02-25 00:03:06 -08:00
|
|
|
/// JavaScript emitter — generates executable JS from DreamStack AST + signal graph.
|
|
|
|
|
|
2026-02-25 01:33:28 -08:00
|
|
|
use std::collections::HashSet;
|
feat: DreamStack compiler foundation — Phase 0/1
Complete compiler pipeline from .ds source to reactive browser apps:
- ds-parser: lexer (string interpolation, operators, keywords) + recursive
descent parser with operator precedence + full AST types
- ds-analyzer: signal graph extraction (source/derived classification),
topological sort for glitch-free propagation, DOM binding analysis
- ds-codegen: JavaScript emitter with embedded reactive runtime (~3KB
signal/derived/effect system) and dark-theme CSS design system
- ds-cli: build (compile to HTML+JS), dev (live server), check (analyze)
Verified working: source signals, derived signals, event handlers,
conditional rendering (when), 12 unit tests passing, 6.8KB output.
2026-02-25 00:03:06 -08:00
|
|
|
use ds_parser::*;
|
|
|
|
|
use ds_analyzer::{SignalGraph, SignalKind, AnalyzedView, InitialValue};
|
|
|
|
|
|
|
|
|
|
pub struct JsEmitter {
|
|
|
|
|
output: String,
|
|
|
|
|
indent: usize,
|
|
|
|
|
node_id_counter: usize,
|
2026-02-25 01:33:28 -08:00
|
|
|
/// Non-signal local variables (e.g., for-in loop vars)
|
|
|
|
|
local_vars: HashSet<String>,
|
feat: DreamStack compiler foundation — Phase 0/1
Complete compiler pipeline from .ds source to reactive browser apps:
- ds-parser: lexer (string interpolation, operators, keywords) + recursive
descent parser with operator precedence + full AST types
- ds-analyzer: signal graph extraction (source/derived classification),
topological sort for glitch-free propagation, DOM binding analysis
- ds-codegen: JavaScript emitter with embedded reactive runtime (~3KB
signal/derived/effect system) and dark-theme CSS design system
- ds-cli: build (compile to HTML+JS), dev (live server), check (analyze)
Verified working: source signals, derived signals, event handlers,
conditional rendering (when), 12 unit tests passing, 6.8KB output.
2026-02-25 00:03:06 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl JsEmitter {
|
|
|
|
|
pub fn new() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
output: String::new(),
|
|
|
|
|
indent: 0,
|
|
|
|
|
node_id_counter: 0,
|
2026-02-25 01:33:28 -08:00
|
|
|
local_vars: HashSet::new(),
|
feat: DreamStack compiler foundation — Phase 0/1
Complete compiler pipeline from .ds source to reactive browser apps:
- ds-parser: lexer (string interpolation, operators, keywords) + recursive
descent parser with operator precedence + full AST types
- ds-analyzer: signal graph extraction (source/derived classification),
topological sort for glitch-free propagation, DOM binding analysis
- ds-codegen: JavaScript emitter with embedded reactive runtime (~3KB
signal/derived/effect system) and dark-theme CSS design system
- ds-cli: build (compile to HTML+JS), dev (live server), check (analyze)
Verified working: source signals, derived signals, event handlers,
conditional rendering (when), 12 unit tests passing, 6.8KB output.
2026-02-25 00:03:06 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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#"<!DOCTYPE html>
|
|
|
|
|
<html lang="en">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
<title>DreamStack App</title>
|
|
|
|
|
<style>
|
|
|
|
|
{CSS_RESET}
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<div id="ds-root"></div>
|
|
|
|
|
<script>
|
|
|
|
|
{RUNTIME_JS}
|
|
|
|
|
</script>
|
|
|
|
|
<script>
|
|
|
|
|
{app_js}
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>"#
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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}\""),
|
2026-02-25 01:33:28 -08:00
|
|
|
None => {
|
|
|
|
|
// Fall back to the let declaration expression
|
|
|
|
|
// (handles List, Record, etc.)
|
|
|
|
|
if let Some(expr) = self.find_let_expr(program, &node.name) {
|
|
|
|
|
self.emit_expr(expr)
|
|
|
|
|
} else {
|
|
|
|
|
"null".to_string()
|
|
|
|
|
}
|
|
|
|
|
}
|
feat: DreamStack compiler foundation — Phase 0/1
Complete compiler pipeline from .ds source to reactive browser apps:
- ds-parser: lexer (string interpolation, operators, keywords) + recursive
descent parser with operator precedence + full AST types
- ds-analyzer: signal graph extraction (source/derived classification),
topological sort for glitch-free propagation, DOM binding analysis
- ds-codegen: JavaScript emitter with embedded reactive runtime (~3KB
signal/derived/effect system) and dark-theme CSS design system
- ds-cli: build (compile to HTML+JS), dev (live server), check (analyze)
Verified working: source signals, derived signals, event handlers,
conditional rendering (when), 12 unit tests passing, 6.8KB output.
2026-02-25 00:03:06 -08:00
|
|
|
};
|
|
|
|
|
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("");
|
|
|
|
|
|
2026-02-25 01:33:28 -08:00
|
|
|
// Phase 2a: Component functions
|
|
|
|
|
self.emit_line("// ── Components ──");
|
|
|
|
|
for decl in &program.declarations {
|
|
|
|
|
if let Declaration::Component(comp) = decl {
|
|
|
|
|
let params = comp.props.iter()
|
|
|
|
|
.map(|p| p.name.clone())
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join(", ");
|
|
|
|
|
self.emit_line(&format!("function component_{}({}) {{", comp.name, params));
|
|
|
|
|
self.indent += 1;
|
|
|
|
|
let child_var = self.emit_view_expr(&comp.body, graph);
|
|
|
|
|
self.emit_line(&format!("return {};", child_var));
|
|
|
|
|
self.indent -= 1;
|
|
|
|
|
self.emit_line("}");
|
|
|
|
|
self.emit_line("");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Phase 2b: Build views
|
feat: DreamStack compiler foundation — Phase 0/1
Complete compiler pipeline from .ds source to reactive browser apps:
- ds-parser: lexer (string interpolation, operators, keywords) + recursive
descent parser with operator precedence + full AST types
- ds-analyzer: signal graph extraction (source/derived classification),
topological sort for glitch-free propagation, DOM binding analysis
- ds-codegen: JavaScript emitter with embedded reactive runtime (~3KB
signal/derived/effect system) and dark-theme CSS design system
- ds-cli: build (compile to HTML+JS), dev (live server), check (analyze)
Verified working: source signals, derived signals, event handlers,
conditional rendering (when), 12 unit tests passing, 6.8KB output.
2026-02-25 00:03:06 -08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-25 07:54:00 -08:00
|
|
|
// Phase 4: Route view functions
|
|
|
|
|
let routes: Vec<_> = program.declarations.iter()
|
|
|
|
|
.filter_map(|d| if let Declaration::Route(r) = d { Some(r) } else { None })
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
if !routes.is_empty() {
|
|
|
|
|
self.emit_line("");
|
|
|
|
|
self.emit_line("// ── Routes ──");
|
|
|
|
|
for (i, route) in routes.iter().enumerate() {
|
|
|
|
|
self.emit_line(&format!("function route_view_{}() {{", i));
|
|
|
|
|
self.indent += 1;
|
|
|
|
|
let child = self.emit_view_expr(&route.body, graph);
|
|
|
|
|
self.emit_line(&format!("return {};", child));
|
|
|
|
|
self.indent -= 1;
|
|
|
|
|
self.emit_line("}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Phase 5: Mount to DOM
|
feat: DreamStack compiler foundation — Phase 0/1
Complete compiler pipeline from .ds source to reactive browser apps:
- ds-parser: lexer (string interpolation, operators, keywords) + recursive
descent parser with operator precedence + full AST types
- ds-analyzer: signal graph extraction (source/derived classification),
topological sort for glitch-free propagation, DOM binding analysis
- ds-codegen: JavaScript emitter with embedded reactive runtime (~3KB
signal/derived/effect system) and dark-theme CSS design system
- ds-cli: build (compile to HTML+JS), dev (live server), check (analyze)
Verified working: source signals, derived signals, event handlers,
conditional rendering (when), 12 unit tests passing, 6.8KB output.
2026-02-25 00:03:06 -08:00
|
|
|
self.emit_line("");
|
|
|
|
|
self.emit_line("// ── Mount ──");
|
2026-02-25 07:54:00 -08:00
|
|
|
|
|
|
|
|
if !routes.is_empty() {
|
|
|
|
|
// Router-based mounting
|
|
|
|
|
self.emit_line("const __root = document.getElementById('ds-root');");
|
|
|
|
|
self.emit_line("let __currentView = null;");
|
|
|
|
|
|
|
|
|
|
// Emit any non-route views as layout (e.g., nav bar)
|
|
|
|
|
for view in views {
|
|
|
|
|
self.emit_line(&format!("__root.appendChild(view_{}());", view.name));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.emit_line("const __routeContainer = document.createElement('div');");
|
|
|
|
|
self.emit_line("__routeContainer.className = 'ds-route-container';");
|
|
|
|
|
self.emit_line("__root.appendChild(__routeContainer);");
|
|
|
|
|
self.emit_line("");
|
|
|
|
|
self.emit_line("DS.effect(() => {");
|
|
|
|
|
self.indent += 1;
|
|
|
|
|
self.emit_line("const path = DS.route.value;");
|
|
|
|
|
self.emit_line("__routeContainer.innerHTML = '';");
|
|
|
|
|
|
|
|
|
|
for (i, route) in routes.iter().enumerate() {
|
|
|
|
|
let branch = if i == 0 { "if" } else { "} else if" };
|
|
|
|
|
self.emit_line(&format!(
|
|
|
|
|
"{} (DS.matchRoute(\"{}\", path)) {{",
|
|
|
|
|
branch, route.path
|
|
|
|
|
));
|
|
|
|
|
self.indent += 1;
|
|
|
|
|
self.emit_line(&format!("__routeContainer.appendChild(route_view_{}());", i));
|
|
|
|
|
self.indent -= 1;
|
|
|
|
|
}
|
|
|
|
|
self.emit_line("}");
|
|
|
|
|
|
|
|
|
|
self.indent -= 1;
|
|
|
|
|
self.emit_line("});");
|
|
|
|
|
} else if let Some(view) = views.first() {
|
feat: DreamStack compiler foundation — Phase 0/1
Complete compiler pipeline from .ds source to reactive browser apps:
- ds-parser: lexer (string interpolation, operators, keywords) + recursive
descent parser with operator precedence + full AST types
- ds-analyzer: signal graph extraction (source/derived classification),
topological sort for glitch-free propagation, DOM binding analysis
- ds-codegen: JavaScript emitter with embedded reactive runtime (~3KB
signal/derived/effect system) and dark-theme CSS design system
- ds-cli: build (compile to HTML+JS), dev (live server), check (analyze)
Verified working: source signals, derived signals, event handlers,
conditional rendering (when), 12 unit tests passing, 6.8KB output.
2026-02-25 00:03:06 -08:00
|
|
|
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) => {
|
2026-02-25 01:33:28 -08:00
|
|
|
if self.local_vars.contains(name) {
|
|
|
|
|
// Non-reactive local variable (e.g., for-in loop var)
|
|
|
|
|
self.emit_line(&format!(
|
|
|
|
|
"{}.textContent = {};",
|
|
|
|
|
node_var, name
|
|
|
|
|
));
|
|
|
|
|
} else {
|
|
|
|
|
// Reactive text binding!
|
|
|
|
|
self.emit_line(&format!(
|
|
|
|
|
"DS.effect(() => {{ {}.textContent = {}.value; }});",
|
|
|
|
|
node_var, name
|
|
|
|
|
));
|
|
|
|
|
}
|
feat: DreamStack compiler foundation — Phase 0/1
Complete compiler pipeline from .ds source to reactive browser apps:
- ds-parser: lexer (string interpolation, operators, keywords) + recursive
descent parser with operator precedence + full AST types
- ds-analyzer: signal graph extraction (source/derived classification),
topological sort for glitch-free propagation, DOM binding analysis
- ds-codegen: JavaScript emitter with embedded reactive runtime (~3KB
signal/derived/effect system) and dark-theme CSS design system
- ds-cli: build (compile to HTML+JS), dev (live server), check (analyze)
Verified working: source signals, derived signals, event handlers,
conditional rendering (when), 12 unit tests passing, 6.8KB output.
2026-02-25 00:03:06 -08:00
|
|
|
}
|
|
|
|
|
_ => {
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-25 01:33:28 -08:00
|
|
|
// ForIn reactive list: `for item in items -> body`
|
|
|
|
|
Expr::ForIn { item, index, iter, body } => {
|
|
|
|
|
let container_var = self.next_node_id();
|
|
|
|
|
let iter_js = self.emit_expr(iter);
|
|
|
|
|
let iter_var = self.next_node_id(); // unique name to avoid shadowing
|
|
|
|
|
|
|
|
|
|
self.emit_line(&format!("const {} = document.createElement('div');", container_var));
|
|
|
|
|
self.emit_line(&format!("{}.className = 'ds-for-list';", container_var));
|
|
|
|
|
|
|
|
|
|
// Reactive effect that re-renders the list when the iterable changes
|
|
|
|
|
self.emit_line("DS.effect(() => {");
|
|
|
|
|
self.indent += 1;
|
|
|
|
|
self.emit_line(&format!("const {} = {};", iter_var, iter_js));
|
|
|
|
|
self.emit_line(&format!("{}.innerHTML = '';", container_var));
|
|
|
|
|
|
|
|
|
|
let idx_var = index.as_deref().unwrap_or("_idx");
|
|
|
|
|
self.emit_line(&format!("const __list = ({0} && {0}.value !== undefined) ? {0}.value : (Array.isArray({0}) ? {0} : []);", iter_var));
|
|
|
|
|
self.emit_line(&format!("__list.forEach(({item}, {idx_var}) => {{"));
|
|
|
|
|
self.indent += 1;
|
|
|
|
|
|
|
|
|
|
// Mark loop vars as non-reactive so text bindings use plain access
|
|
|
|
|
self.local_vars.insert(item.clone());
|
|
|
|
|
if let Some(idx) = index {
|
|
|
|
|
self.local_vars.insert(idx.clone());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let child_var = self.emit_view_expr(body, graph);
|
|
|
|
|
self.emit_line(&format!("{}.appendChild({});", container_var, child_var));
|
|
|
|
|
|
|
|
|
|
// Clean up
|
|
|
|
|
self.local_vars.remove(item);
|
|
|
|
|
if let Some(idx) = index {
|
|
|
|
|
self.local_vars.remove(idx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.indent -= 1;
|
|
|
|
|
self.emit_line("});");
|
|
|
|
|
self.indent -= 1;
|
|
|
|
|
self.emit_line("});");
|
|
|
|
|
|
|
|
|
|
container_var
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Component usage: `<Card title="hello" />`
|
|
|
|
|
Expr::ComponentUse { name, props, children } => {
|
|
|
|
|
let args = props.iter()
|
|
|
|
|
.map(|(k, v)| self.emit_expr(v))
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join(", ");
|
|
|
|
|
let node_var = self.next_node_id();
|
|
|
|
|
self.emit_line(&format!("const {} = component_{}({});", node_var, name, args));
|
|
|
|
|
node_var
|
|
|
|
|
}
|
|
|
|
|
|
feat: DreamStack compiler foundation — Phase 0/1
Complete compiler pipeline from .ds source to reactive browser apps:
- ds-parser: lexer (string interpolation, operators, keywords) + recursive
descent parser with operator precedence + full AST types
- ds-analyzer: signal graph extraction (source/derived classification),
topological sort for glitch-free propagation, DOM binding analysis
- ds-codegen: JavaScript emitter with embedded reactive runtime (~3KB
signal/derived/effect system) and dark-theme CSS design system
- ds-cli: build (compile to HTML+JS), dev (live server), check (analyze)
Verified working: source signals, derived signals, event handlers,
conditional rendering (when), 12 unit tests passing, 6.8KB output.
2026-02-25 00:03:06 -08:00
|
|
|
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<String> = args.iter().map(|a| self.emit_expr(a)).collect();
|
2026-02-25 07:54:00 -08:00
|
|
|
// Built-in functions map to DS.xxx()
|
|
|
|
|
let fn_name = match name.as_str() {
|
|
|
|
|
"navigate" => "DS.navigate",
|
|
|
|
|
_ => name,
|
|
|
|
|
};
|
|
|
|
|
format!("{}({})", fn_name, args_js.join(", "))
|
feat: DreamStack compiler foundation — Phase 0/1
Complete compiler pipeline from .ds source to reactive browser apps:
- ds-parser: lexer (string interpolation, operators, keywords) + recursive
descent parser with operator precedence + full AST types
- ds-analyzer: signal graph extraction (source/derived classification),
topological sort for glitch-free propagation, DOM binding analysis
- ds-codegen: JavaScript emitter with embedded reactive runtime (~3KB
signal/derived/effect system) and dark-theme CSS design system
- ds-cli: build (compile to HTML+JS), dev (live server), check (analyze)
Verified working: source signals, derived signals, event handlers,
conditional rendering (when), 12 unit tests passing, 6.8KB output.
2026-02-25 00:03:06 -08:00
|
|
|
}
|
|
|
|
|
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<String> = fields
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|(k, v)| format!("{}: {}", k, self.emit_expr(v)))
|
|
|
|
|
.collect();
|
|
|
|
|
format!("{{ {} }}", fields_js.join(", "))
|
|
|
|
|
}
|
|
|
|
|
Expr::List(items) => {
|
|
|
|
|
let items_js: Vec<String> = 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<String> = 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);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-25 07:54:00 -08:00
|
|
|
// ── Keyed List Reconciliation ──
|
|
|
|
|
function keyedList(container, getItems, keyFn, renderFn) {
|
|
|
|
|
let nodeMap = new Map();
|
|
|
|
|
const eff = effect(() => {
|
|
|
|
|
const raw = getItems();
|
|
|
|
|
const items = (raw && raw.value !== undefined) ? raw.value : (Array.isArray(raw) ? raw : []);
|
|
|
|
|
const newMap = new Map();
|
|
|
|
|
const frag = document.createDocumentFragment();
|
|
|
|
|
items.forEach((item, idx) => {
|
|
|
|
|
const key = keyFn ? keyFn(item, idx) : idx;
|
|
|
|
|
let node = nodeMap.get(key);
|
|
|
|
|
if (!node) {
|
|
|
|
|
node = renderFn(item, idx);
|
|
|
|
|
}
|
|
|
|
|
newMap.set(key, node);
|
|
|
|
|
frag.appendChild(node);
|
|
|
|
|
});
|
|
|
|
|
container.innerHTML = '';
|
|
|
|
|
container.appendChild(frag);
|
|
|
|
|
nodeMap = newMap;
|
|
|
|
|
});
|
|
|
|
|
return eff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Router ──
|
|
|
|
|
const _route = new Signal(window.location.hash.slice(1) || '/');
|
|
|
|
|
window.addEventListener('hashchange', () => {
|
|
|
|
|
_route.value = window.location.hash.slice(1) || '/';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function navigate(path) {
|
|
|
|
|
window.location.hash = path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function matchRoute(pattern, path) {
|
|
|
|
|
if (pattern === path) return {};
|
|
|
|
|
const patParts = pattern.split('/');
|
|
|
|
|
const pathParts = path.split('/');
|
|
|
|
|
if (patParts.length !== pathParts.length) return null;
|
|
|
|
|
const params = {};
|
|
|
|
|
for (let i = 0; i < patParts.length; i++) {
|
|
|
|
|
if (patParts[i].startsWith(':')) {
|
|
|
|
|
params[patParts[i].slice(1)] = pathParts[i];
|
|
|
|
|
} else if (patParts[i] !== pathParts[i]) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return params;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { signal, derived, effect, batch, flush, onEvent, emit,
|
|
|
|
|
keyedList, route: _route, navigate, matchRoute,
|
|
|
|
|
Signal, Derived, Effect };
|
feat: DreamStack compiler foundation — Phase 0/1
Complete compiler pipeline from .ds source to reactive browser apps:
- ds-parser: lexer (string interpolation, operators, keywords) + recursive
descent parser with operator precedence + full AST types
- ds-analyzer: signal graph extraction (source/derived classification),
topological sort for glitch-free propagation, DOM binding analysis
- ds-codegen: JavaScript emitter with embedded reactive runtime (~3KB
signal/derived/effect system) and dark-theme CSS design system
- ds-cli: build (compile to HTML+JS), dev (live server), check (analyze)
Verified working: source signals, derived signals, event handlers,
conditional rendering (when), 12 unit tests passing, 6.8KB output.
2026-02-25 00:03:06 -08:00
|
|
|
})();
|
|
|
|
|
"#;
|