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:
8
src/analysis/mod.rs
Normal file
8
src/analysis/mod.rs
Normal 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
556
src/analysis/view_deps.rs
Normal 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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user