feat: complete JS backend with Dom effect and Html module

- Add full Dom effect: querySelector, createElement, addEventListener,
  setAttribute, classList, styles, forms, scrolling, etc.
- Add Html module for type-safe HTML construction (Elm-style)
- Add TEA (The Elm Architecture) runtime for browser apps
- Add view dependency analysis for Svelte-style optimizations
- Support both browser and Node.js environments

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 03:54:04 -05:00
parent c13d322342
commit 40c9b4d894
3 changed files with 2890 additions and 0 deletions

8
src/analysis/mod.rs Normal file
View File

@@ -0,0 +1,8 @@
//! Analysis passes for the Lux compiler
//!
//! This module contains various analysis passes that run on the AST
//! to gather information for optimization and code generation.
pub mod view_deps;
pub use view_deps::{ViewAnalysis, ViewCodeGen, FieldPath, NodeId, NodeInfo};

556
src/analysis/view_deps.rs Normal file
View File

@@ -0,0 +1,556 @@
//! View Dependency Analysis
//!
//! This module analyzes view functions to track which DOM nodes depend on which
//! model fields. This enables Svelte-style compile-time optimizations where
//! only the DOM nodes that depend on changed fields are updated.
//!
//! # How it works
//!
//! 1. Parse the view function body
//! 2. For each Html element/text node, track which model fields it accesses
//! 3. Build a dependency graph: Node -> Set<ModelField>
//! 4. Generate efficient update code that only updates changed nodes
use crate::ast::{Expr, Ident, Statement};
use std::collections::{HashMap, HashSet};
/// Represents a path to a model field (e.g., "model.user.name" -> ["user", "name"])
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct FieldPath(pub Vec<String>);
impl FieldPath {
pub fn new(fields: Vec<String>) -> Self {
FieldPath(fields)
}
pub fn single(field: String) -> Self {
FieldPath(vec![field])
}
pub fn to_js_path(&self) -> String {
self.0.join(".")
}
}
/// A unique identifier for a DOM node in the view tree
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct NodeId(pub usize);
/// Information about a DOM node and its dependencies
#[derive(Debug, Clone)]
pub struct NodeInfo {
/// Unique ID for this node
pub id: NodeId,
/// The HTML tag (e.g., "div", "span") or "text" for text nodes
pub tag: String,
/// Which model fields this node's content depends on
pub content_deps: HashSet<FieldPath>,
/// Which model fields this node's attributes depend on
pub attr_deps: HashMap<String, HashSet<FieldPath>>,
/// Child node IDs
pub children: Vec<NodeId>,
/// Parent node ID (None for root)
pub parent: Option<NodeId>,
}
/// The result of analyzing a view function
#[derive(Debug)]
pub struct ViewAnalysis {
/// All nodes in the view tree
pub nodes: HashMap<NodeId, NodeInfo>,
/// The root node ID
pub root: Option<NodeId>,
/// All model fields that are accessed anywhere in the view
pub all_deps: HashSet<FieldPath>,
/// Counter for generating unique node IDs
next_id: usize,
}
impl ViewAnalysis {
pub fn new() -> Self {
ViewAnalysis {
nodes: HashMap::new(),
root: None,
all_deps: HashSet::new(),
next_id: 0,
}
}
fn next_node_id(&mut self) -> NodeId {
let id = NodeId(self.next_id);
self.next_id += 1;
id
}
/// Analyze a view function body
pub fn analyze(&mut self, expr: &Expr, model_param: &str) {
if let Some(node_id) = self.analyze_expr(expr, model_param, None) {
self.root = Some(node_id);
}
}
/// Analyze an expression, returning a NodeId if it creates a DOM node
fn analyze_expr(&mut self, expr: &Expr, model_param: &str, parent: Option<NodeId>) -> Option<NodeId> {
match expr {
// Function call - check if it's an Html builder
Expr::Call { func, args, .. } => {
self.analyze_call(func, args, model_param, parent)
}
// Let binding - analyze the body
Expr::Let { value, body, .. } => {
// Analyze value for side effects
self.analyze_expr(value, model_param, parent.clone());
// The result is from the body
self.analyze_expr(body, model_param, parent)
}
// Block - analyze statements and return result
Expr::Block { statements, result, .. } => {
for stmt in statements {
if let Statement::Let { value, .. } = stmt {
self.analyze_expr(value, model_param, parent.clone());
}
}
self.analyze_expr(result, model_param, parent)
}
// If expression - analyze both branches
Expr::If { condition, then_branch, else_branch, .. } => {
let cond_deps = self.collect_field_deps(condition, model_param);
self.all_deps.extend(cond_deps.clone());
// For now, treat conditional nodes as depending on the condition
// A more sophisticated analysis would track both branches
let then_node = self.analyze_expr(then_branch, model_param, parent.clone());
self.analyze_expr(else_branch, model_param, parent);
then_node
}
// Match expression - analyze all arms
Expr::Match { scrutinee, arms, .. } => {
let scrutinee_deps = self.collect_field_deps(scrutinee, model_param);
self.all_deps.extend(scrutinee_deps);
for arm in arms {
self.analyze_expr(&arm.body, model_param, parent.clone());
}
None
}
// Variable reference - no DOM node created
Expr::Var(_) => None,
// Field access - collect dependencies but no DOM node
Expr::Field { .. } => {
let deps = self.collect_field_deps(expr, model_param);
self.all_deps.extend(deps);
None
}
// Literal - no DOM node
Expr::Literal(_) => None,
_ => None,
}
}
/// Analyze a function call, checking if it's an Html builder
fn analyze_call(
&mut self,
func: &Expr,
args: &[Expr],
model_param: &str,
parent: Option<NodeId>,
) -> Option<NodeId> {
// Check if this is an Html element constructor
let func_name = self.get_func_name(func);
match func_name.as_deref() {
// Element constructors: div, span, button, etc.
Some(name) if is_html_element(name) => {
self.analyze_element(name, args, model_param, parent)
}
// Text node constructor
Some("text") => {
self.analyze_text_node(args, model_param, parent)
}
// Element constructor (generic)
Some("Element") => {
if args.len() >= 3 {
if let Expr::Literal(lit) = &args[0] {
if let crate::ast::LiteralKind::String(tag) = &lit.kind {
return self.analyze_element(tag, &args[1..], model_param, parent);
}
}
}
None
}
// Other function calls - check for dependencies in args
_ => {
for arg in args {
let deps = self.collect_field_deps(arg, model_param);
self.all_deps.extend(deps);
}
None
}
}
}
/// Analyze an HTML element (div, span, etc.)
fn analyze_element(
&mut self,
tag: &str,
args: &[Expr],
model_param: &str,
parent: Option<NodeId>,
) -> Option<NodeId> {
let node_id = self.next_node_id();
let mut attr_deps: HashMap<String, HashSet<FieldPath>> = HashMap::new();
let mut children = Vec::new();
// First arg is typically attributes list
if let Some(attrs_expr) = args.get(0) {
if let Expr::List { elements, .. } = attrs_expr {
for attr in elements {
self.analyze_attribute(attr, model_param, &mut attr_deps);
}
}
}
// Second arg is typically children list
if let Some(children_expr) = args.get(1) {
if let Expr::List { elements, .. } = children_expr {
for child in elements {
if let Some(child_id) = self.analyze_expr(child, model_param, Some(node_id.clone())) {
children.push(child_id);
}
}
}
}
let node = NodeInfo {
id: node_id.clone(),
tag: tag.to_string(),
content_deps: HashSet::new(),
attr_deps,
children,
parent,
};
self.nodes.insert(node_id.clone(), node);
Some(node_id)
}
/// Analyze a text node
fn analyze_text_node(
&mut self,
args: &[Expr],
model_param: &str,
parent: Option<NodeId>,
) -> Option<NodeId> {
let node_id = self.next_node_id();
let mut content_deps = HashSet::new();
if let Some(content_expr) = args.get(0) {
content_deps = self.collect_field_deps(content_expr, model_param);
self.all_deps.extend(content_deps.clone());
}
let node = NodeInfo {
id: node_id.clone(),
tag: "text".to_string(),
content_deps,
attr_deps: HashMap::new(),
children: Vec::new(),
parent,
};
self.nodes.insert(node_id.clone(), node);
Some(node_id)
}
/// Analyze an attribute expression
fn analyze_attribute(
&mut self,
attr: &Expr,
model_param: &str,
attr_deps: &mut HashMap<String, HashSet<FieldPath>>,
) {
// Attributes are typically constructor calls like Class("foo") or OnClick(msg)
if let Expr::Call { func, args, .. } = attr {
if let Some(attr_name) = self.get_func_name(func) {
let deps = args.iter()
.flat_map(|arg| self.collect_field_deps(arg, model_param))
.collect::<HashSet<_>>();
if !deps.is_empty() {
attr_deps.insert(attr_name, deps.clone());
self.all_deps.extend(deps);
}
}
}
}
/// Collect all model field accesses in an expression
fn collect_field_deps(&self, expr: &Expr, model_param: &str) -> HashSet<FieldPath> {
let mut deps = HashSet::new();
self.collect_field_deps_recursive(expr, model_param, &mut deps, &mut Vec::new());
deps
}
fn collect_field_deps_recursive(
&self,
expr: &Expr,
model_param: &str,
deps: &mut HashSet<FieldPath>,
current_path: &mut Vec<String>,
) {
match expr {
Expr::Field { object, field, .. } => {
current_path.push(field.name.clone());
self.collect_field_deps_recursive(object, model_param, deps, current_path);
current_path.pop();
}
Expr::Var(ident) if ident.name == model_param => {
// Found model.field1.field2...
if !current_path.is_empty() {
let mut path = current_path.clone();
path.reverse();
deps.insert(FieldPath::new(path));
}
}
Expr::Call { func, args, .. } => {
self.collect_field_deps_recursive(func, model_param, deps, &mut Vec::new());
for arg in args {
self.collect_field_deps_recursive(arg, model_param, deps, &mut Vec::new());
}
}
Expr::BinaryOp { left, right, .. } => {
self.collect_field_deps_recursive(left, model_param, deps, &mut Vec::new());
self.collect_field_deps_recursive(right, model_param, deps, &mut Vec::new());
}
Expr::UnaryOp { operand, .. } => {
self.collect_field_deps_recursive(operand, model_param, deps, &mut Vec::new());
}
Expr::If { condition, then_branch, else_branch, .. } => {
self.collect_field_deps_recursive(condition, model_param, deps, &mut Vec::new());
self.collect_field_deps_recursive(then_branch, model_param, deps, &mut Vec::new());
self.collect_field_deps_recursive(else_branch, model_param, deps, &mut Vec::new());
}
Expr::Let { value, body, .. } => {
self.collect_field_deps_recursive(value, model_param, deps, &mut Vec::new());
self.collect_field_deps_recursive(body, model_param, deps, &mut Vec::new());
}
Expr::Block { statements, result, .. } => {
for stmt in statements {
if let Statement::Let { value, .. } = stmt {
self.collect_field_deps_recursive(value, model_param, deps, &mut Vec::new());
}
}
self.collect_field_deps_recursive(result, model_param, deps, &mut Vec::new());
}
Expr::Match { scrutinee, arms, .. } => {
self.collect_field_deps_recursive(scrutinee, model_param, deps, &mut Vec::new());
for arm in arms {
self.collect_field_deps_recursive(&arm.body, model_param, deps, &mut Vec::new());
}
}
Expr::List { elements, .. } => {
for elem in elements {
self.collect_field_deps_recursive(elem, model_param, deps, &mut Vec::new());
}
}
Expr::Lambda { body, .. } => {
self.collect_field_deps_recursive(body, model_param, deps, &mut Vec::new());
}
_ => {}
}
}
/// Get the name of a function being called
fn get_func_name(&self, expr: &Expr) -> Option<String> {
match expr {
Expr::Var(ident) => Some(ident.name.clone()),
Expr::Field { object, field, .. } => {
// For qualified calls like Html.div
if let Expr::Var(module) = object.as_ref() {
Some(format!("{}.{}", module.name, field.name))
} else {
Some(field.name.clone())
}
}
_ => None,
}
}
}
impl Default for ViewAnalysis {
fn default() -> Self {
Self::new()
}
}
/// Check if a function name is an HTML element constructor
fn is_html_element(name: &str) -> bool {
// Strip module prefix if present
let name = name.strip_prefix("Html.").unwrap_or(name);
matches!(
name,
"div" | "span" | "p" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6"
| "button" | "input" | "form" | "label" | "textarea" | "select" | "option"
| "a" | "img" | "ul" | "ol" | "li" | "table" | "tr" | "td" | "th"
| "thead" | "tbody" | "header" | "footer" | "nav" | "main" | "aside"
| "section" | "article" | "pre" | "code" | "blockquote"
| "strong" | "em" | "small" | "br" | "hr"
)
}
/// Generate JavaScript code for efficient view updates
pub struct ViewCodeGen {
analysis: ViewAnalysis,
}
impl ViewCodeGen {
pub fn new(analysis: ViewAnalysis) -> Self {
ViewCodeGen { analysis }
}
/// Generate the view_create function
pub fn generate_create(&self) -> String {
let mut code = String::new();
code.push_str("function view_create(model) {\n");
if let Some(root_id) = &self.analysis.root {
self.generate_create_node(&mut code, root_id, " ");
code.push_str(" return { root: node_0");
// Include references to all nodes that have dependencies
for (id, node) in &self.analysis.nodes {
if !node.content_deps.is_empty() || !node.attr_deps.is_empty() {
code.push_str(&format!(", node_{}: node_{}", id.0, id.0));
}
}
code.push_str(" };\n");
}
code.push_str("}\n");
code
}
fn generate_create_node(&self, code: &mut String, node_id: &NodeId, indent: &str) {
if let Some(node) = self.analysis.nodes.get(node_id) {
let var_name = format!("node_{}", node_id.0);
if node.tag == "text" {
// Text node
code.push_str(&format!(
"{}const {} = document.createTextNode(\"\");\n",
indent, var_name
));
} else {
// Element node
code.push_str(&format!(
"{}const {} = document.createElement(\"{}\");\n",
indent, var_name, node.tag
));
}
// Create children
for child_id in &node.children {
self.generate_create_node(code, child_id, indent);
code.push_str(&format!(
"{}{}.appendChild(node_{});\n",
indent, var_name, child_id.0
));
}
}
}
/// Generate the view_update function
pub fn generate_update(&self) -> String {
let mut code = String::new();
code.push_str("function view_update(nodes, oldModel, newModel) {\n");
// Generate update code for each node that has dependencies
for (id, node) in &self.analysis.nodes {
// Check content dependencies
if !node.content_deps.is_empty() {
let conditions: Vec<String> = node.content_deps.iter()
.map(|dep| format!("oldModel.{} !== newModel.{}", dep.to_js_path(), dep.to_js_path()))
.collect();
code.push_str(&format!(
" if ({}) {{\n",
conditions.join(" || ")
));
if node.tag == "text" {
code.push_str(&format!(
" nodes.node_{}.textContent = /* render content */;\n",
id.0
));
}
code.push_str(" }\n");
}
// Check attribute dependencies
for (attr, deps) in &node.attr_deps {
let conditions: Vec<String> = deps.iter()
.map(|dep| format!("oldModel.{} !== newModel.{}", dep.to_js_path(), dep.to_js_path()))
.collect();
code.push_str(&format!(
" if ({}) {{\n",
conditions.join(" || ")
));
code.push_str(&format!(
" // Update {} attribute on node_{}\n",
attr, id.0
));
code.push_str(" }\n");
}
}
code.push_str("}\n");
code
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_field_path() {
let path = FieldPath::new(vec!["user".to_string(), "name".to_string()]);
assert_eq!(path.to_js_path(), "user.name");
}
#[test]
fn test_is_html_element() {
assert!(is_html_element("div"));
assert!(is_html_element("Html.div"));
assert!(is_html_element("button"));
assert!(!is_html_element("foo"));
assert!(!is_html_element("toString"));
}
}