dreamstack/compiler/ds-cli/src/main.rs
enzotar a634152318 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

249 lines
7.7 KiB
Rust

/// DreamStack CLI — the compiler command-line interface.
///
/// Usage:
/// dreamstack build <file.ds> — compile to HTML+JS
/// dreamstack dev <file.ds> — dev server with hot reload
/// dreamstack check <file.ds> — analyze without emitting
use clap::{Parser, Subcommand};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Parser)]
#[command(name = "dreamstack")]
#[command(about = "The DreamStack UI compiler", version = "0.1.0")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Compile a .ds file to HTML+JS
Build {
/// Input .ds file
file: PathBuf,
/// Output directory (default: dist/)
#[arg(short, long, default_value = "dist")]
output: PathBuf,
},
/// Start a dev server with hot reload
Dev {
/// Input .ds file
file: PathBuf,
/// Port to serve on
#[arg(short, long, default_value_t = 3000)]
port: u16,
},
/// Type-check and analyze without compiling
Check {
/// Input .ds file
file: PathBuf,
},
}
fn main() {
let cli = Cli::parse();
match cli.command {
Commands::Build { file, output } => cmd_build(&file, &output),
Commands::Dev { file, port } => cmd_dev(&file, port),
Commands::Check { file } => cmd_check(&file),
}
}
fn compile(source: &str) -> Result<String, String> {
// 1. Lex
let mut lexer = ds_parser::Lexer::new(source);
let tokens = lexer.tokenize();
// Check for lexer errors
for tok in &tokens {
if let ds_parser::TokenKind::Error(msg) = &tok.kind {
return Err(format!("Lexer error at line {}: {}", tok.line, msg));
}
}
// 2. Parse
let mut parser = ds_parser::Parser::new(tokens);
let program = parser.parse_program().map_err(|e| e.to_string())?;
// 3. Analyze
let graph = ds_analyzer::SignalGraph::from_program(&program);
let views = ds_analyzer::SignalGraph::analyze_views(&program);
// 4. Codegen
let html = ds_codegen::JsEmitter::emit_html(&program, &graph, &views);
Ok(html)
}
fn cmd_build(file: &Path, output: &Path) {
println!("🔨 DreamStack build");
println!(" source: {}", file.display());
let source = match fs::read_to_string(file) {
Ok(s) => s,
Err(e) => {
eprintln!("❌ Could not read {}: {}", file.display(), e);
std::process::exit(1);
}
};
match compile(&source) {
Ok(html) => {
fs::create_dir_all(output).unwrap();
let out_path = output.join("index.html");
fs::write(&out_path, &html).unwrap();
println!(" output: {}", out_path.display());
println!("✅ Build complete! ({} bytes)", html.len());
println!("");
println!(" Open in browser:");
println!(" file://{}", fs::canonicalize(&out_path).unwrap().display());
}
Err(e) => {
eprintln!("❌ Compile error: {e}");
std::process::exit(1);
}
}
}
fn cmd_dev(file: &Path, port: u16) {
println!("🚀 DreamStack dev server");
println!(" watching: {}", file.display());
println!(" serving: http://localhost:{port}");
println!("");
let source = match fs::read_to_string(file) {
Ok(s) => s,
Err(e) => {
eprintln!("❌ Could not read {}: {}", file.display(), e);
std::process::exit(1);
}
};
let html = match compile(&source) {
Ok(html) => html,
Err(e) => {
eprintln!("❌ Compile error: {e}");
std::process::exit(1);
}
};
// Simple HTTP server
let server = tiny_http::Server::http(format!("0.0.0.0:{port}")).unwrap();
println!("✅ Server running at http://localhost:{port}");
println!(" Press Ctrl+C to stop");
println!("");
for request in server.incoming_requests() {
// Re-compile on each request for dev mode (simple hot-reload)
let current_html = if let Ok(src) = fs::read_to_string(file) {
compile(&src).unwrap_or_else(|e| {
format!("<html><body><pre style='color:red'>Compile error:\n{e}</pre></body></html>")
})
} else {
html.clone()
};
let response = tiny_http::Response::from_string(&current_html)
.with_header(
tiny_http::Header::from_bytes(&b"Content-Type"[..], &b"text/html; charset=utf-8"[..])
.unwrap(),
);
let _ = request.respond(response);
}
}
fn cmd_check(file: &Path) {
println!("🔍 DreamStack check");
println!(" file: {}", file.display());
let source = match fs::read_to_string(file) {
Ok(s) => s,
Err(e) => {
eprintln!("❌ Could not read {}: {}", file.display(), e);
std::process::exit(1);
}
};
// Lex
let mut lexer = ds_parser::Lexer::new(&source);
let tokens = lexer.tokenize();
let mut errors = 0;
for tok in &tokens {
if let ds_parser::TokenKind::Error(msg) = &tok.kind {
eprintln!(" ❌ Lexer error at line {}: {}", tok.line, msg);
errors += 1;
}
}
// Parse
let mut parser = ds_parser::Parser::new(tokens);
let program = match parser.parse_program() {
Ok(p) => p,
Err(e) => {
eprintln!("{}", e);
std::process::exit(1);
}
};
// Analyze
let graph = ds_analyzer::SignalGraph::from_program(&program);
let views = ds_analyzer::SignalGraph::analyze_views(&program);
println!("");
println!(" 📊 Signal Graph:");
for node in &graph.nodes {
let kind_str = match &node.kind {
ds_analyzer::SignalKind::Source => "source",
ds_analyzer::SignalKind::Derived => "derived",
ds_analyzer::SignalKind::Handler { event, .. } => "handler",
};
let deps: Vec<&str> = node.dependencies.iter().map(|d| d.signal_name.as_str()).collect();
if deps.is_empty() {
println!(" {} [{}]", node.name, kind_str);
} else {
println!(" {} [{}] ← depends on: {}", node.name, kind_str, deps.join(", "));
}
}
println!("");
println!(" 🖼️ Views:");
for view in &views {
println!(" {} ({} bindings)", view.name, view.bindings.len());
for binding in &view.bindings {
match &binding.kind {
ds_analyzer::BindingKind::TextContent { signal } => {
println!(" 📝 text bound to: {signal}");
}
ds_analyzer::BindingKind::EventHandler { element_tag, event, .. } => {
println!("{element_tag}.{event}");
}
ds_analyzer::BindingKind::Conditional { condition_signals } => {
println!(" ❓ conditional on: {}", condition_signals.join(", "));
}
ds_analyzer::BindingKind::StaticContainer { kind, child_count } => {
println!(" 📦 {kind} ({child_count} children)");
}
ds_analyzer::BindingKind::StaticText { text } => {
println!(" 📄 static: \"{text}\"");
}
}
}
}
let topo = graph.topological_order();
println!("");
println!(" 🔄 Propagation order: {:?}", topo.iter().map(|&id| &graph.nodes[id].name).collect::<Vec<_>>());
if errors == 0 {
println!("");
println!("✅ No errors found");
} else {
println!("");
eprintln!("{} error(s) found", errors);
std::process::exit(1);
}
}