//! JavaScript code generation backend for Lux //! //! Compiles Lux programs to JavaScript for browser and Node.js execution. //! //! ## Compilation Strategy //! //! Lux source → Parse → Type check → Generate JavaScript → Run in browser/Node //! //! ## Runtime Type Representations //! //! | Lux Type | JavaScript Type | //! |----------|-----------------| //! | Int | `number` (BigInt for large values) | //! | Float | `number` | //! | Bool | `boolean` | //! | String | `string` | //! | Unit | `undefined` | //! | List | `Array` | //! | Option | `{tag: "Some", value: T} \| {tag: "None"}` | //! | Result | `{tag: "Ok", value: T} \| {tag: "Err", error: E}` | //! | Closure | `function` (native JS closures) | //! | ADT | `{tag: "VariantName", field0: ..., field1: ...}` | //! //! ## Effects //! //! Effects are compiled to handler objects passed as parameters: //! ```javascript //! async function fetchUser_lux(handlers, id) { //! return await handlers.Http.get(`/api/users/${id}`); //! } //! ``` use crate::ast::*; use std::collections::{HashMap, HashSet}; /// JavaScript code generation errors #[derive(Debug, Clone)] pub struct JsGenError { pub message: String, #[allow(dead_code)] pub span: Option, } impl std::fmt::Display for JsGenError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "JS codegen error: {}", self.message) } } impl std::error::Error for JsGenError {} /// The JavaScript backend code generator pub struct JsBackend { /// Generated JavaScript code output: String, /// Current indentation level indent: usize, /// Known function names functions: HashSet, /// Counter for generating unique names name_counter: usize, /// Mapping from variant names to their parent type name variant_to_type: HashMap, /// Mapping from (type_name, variant_name) to field types variant_field_types: HashMap<(String, String), Vec>, /// Functions that use effects (have handlers parameter) effectful_functions: HashSet, /// Whether we're currently inside an effectful function has_handlers: bool, /// Variable substitutions for let binding var_substitutions: HashMap, /// Effects actually used in the program (for tree-shaking runtime) used_effects: HashSet, /// Extern function names mapped to their JS names extern_fns: HashMap, /// Extern let names mapped to their JS names extern_lets: HashMap, } impl JsBackend { pub fn new() -> Self { // Initialize built-in type variants let mut variant_to_type = HashMap::new(); variant_to_type.insert("Some".to_string(), "Option".to_string()); variant_to_type.insert("None".to_string(), "Option".to_string()); variant_to_type.insert("Ok".to_string(), "Result".to_string()); variant_to_type.insert("Err".to_string(), "Result".to_string()); Self { output: String::new(), indent: 0, functions: HashSet::new(), name_counter: 0, variant_to_type, variant_field_types: HashMap::new(), effectful_functions: HashSet::new(), has_handlers: false, var_substitutions: HashMap::new(), used_effects: HashSet::new(), extern_fns: HashMap::new(), extern_lets: HashMap::new(), } } /// Generate JavaScript code from a Lux program pub fn generate(&mut self, program: &Program) -> Result { self.output.clear(); // First pass: collect all function names, types, and effects for decl in &program.declarations { match decl { Declaration::Function(f) => { self.functions.insert(f.name.name.clone()); if !f.effects.is_empty() { self.effectful_functions.insert(f.name.name.clone()); } } Declaration::Type(t) => { self.collect_type(t)?; } Declaration::ExternFn(ext) => { let js_name = ext .js_name .clone() .unwrap_or_else(|| ext.name.name.clone()); self.extern_fns.insert(ext.name.name.clone(), js_name); self.functions.insert(ext.name.name.clone()); } Declaration::ExternLet(ext) => { let js_name = ext .js_name .clone() .unwrap_or_else(|| ext.name.name.clone()); self.extern_lets.insert(ext.name.name.clone(), js_name); } _ => {} } } // Collect used effects for tree-shaking self.collect_used_effects(program); // Emit runtime helpers (tree-shaken based on used effects) self.emit_runtime(); // Emit type constructors for decl in &program.declarations { if let Declaration::Type(t) = decl { self.emit_type_constructors(t)?; } } // Emit functions for decl in &program.declarations { if let Declaration::Function(f) = decl { self.emit_function(f)?; } } // Check if any top-level let calls main (to avoid double invocation) let has_main_call = program.declarations.iter().any(|decl| { if let Declaration::Let(l) = decl { self.expr_calls_main(&l.value) } else { false } }); // Emit top-level let bindings and expressions if program.declarations.iter().any(|d| matches!(d, Declaration::Let(_))) { self.writeln("// Top-level bindings"); for decl in &program.declarations { if let Declaration::Let(l) = decl { self.emit_top_level_let(l)?; } } } // Emit main function call if it exists and not already called if self.functions.contains("main") && !has_main_call { self.writeln(""); self.writeln("// Entry point"); if self.effectful_functions.contains("main") { self.writeln("main_lux(Lux.defaultHandlers);"); } else { self.writeln("main_lux();"); } } Ok(self.output.clone()) } /// Collect all effects used in the program for runtime tree-shaking fn collect_used_effects(&mut self, program: &Program) { for decl in &program.declarations { match decl { Declaration::Function(f) => { for effect in &f.effects { self.used_effects.insert(effect.name.clone()); } self.collect_effects_from_expr(&f.body); } Declaration::Let(l) => { self.collect_effects_from_expr(&l.value); } Declaration::Handler(h) => { self.used_effects.insert(h.effect.name.clone()); for imp in &h.implementations { self.collect_effects_from_expr(&imp.body); } } _ => {} } } } /// Recursively collect effect names from an expression fn collect_effects_from_expr(&mut self, expr: &Expr) { match expr { Expr::EffectOp { effect, args, .. } => { self.used_effects.insert(effect.name.clone()); for arg in args { self.collect_effects_from_expr(arg); } } Expr::Run { expr, handlers, .. } => { self.collect_effects_from_expr(expr); for (effect, handler) in handlers { self.used_effects.insert(effect.name.clone()); self.collect_effects_from_expr(handler); } } Expr::Call { func, args, .. } => { self.collect_effects_from_expr(func); for arg in args { self.collect_effects_from_expr(arg); } } Expr::Lambda { body, effects, .. } => { for effect in effects { self.used_effects.insert(effect.name.clone()); } self.collect_effects_from_expr(body); } Expr::Let { value, body, .. } => { self.collect_effects_from_expr(value); self.collect_effects_from_expr(body); } Expr::If { condition, then_branch, else_branch, .. } => { self.collect_effects_from_expr(condition); self.collect_effects_from_expr(then_branch); self.collect_effects_from_expr(else_branch); } Expr::Match { scrutinee, arms, .. } => { self.collect_effects_from_expr(scrutinee); for arm in arms { self.collect_effects_from_expr(&arm.body); if let Some(guard) = &arm.guard { self.collect_effects_from_expr(guard); } } } Expr::Block { statements, result, .. } => { for stmt in statements { match stmt { Statement::Expr(e) => self.collect_effects_from_expr(e), Statement::Let { value, .. } => self.collect_effects_from_expr(value), } } self.collect_effects_from_expr(result); } Expr::BinaryOp { left, right, .. } => { self.collect_effects_from_expr(left); self.collect_effects_from_expr(right); } Expr::UnaryOp { operand, .. } => { self.collect_effects_from_expr(operand); } Expr::Field { object, .. } => { self.collect_effects_from_expr(object); } Expr::TupleIndex { object, .. } => { self.collect_effects_from_expr(object); } Expr::Record { spread, fields, .. } => { if let Some(s) = spread { self.collect_effects_from_expr(s); } for (_, expr) in fields { self.collect_effects_from_expr(expr); } } Expr::Tuple { elements, .. } | Expr::List { elements, .. } => { for el in elements { self.collect_effects_from_expr(el); } } Expr::Resume { value, .. } => { self.collect_effects_from_expr(value); } Expr::Literal(_) | Expr::Var(_) => {} } } /// Emit the Lux runtime, tree-shaken based on used effects fn emit_runtime(&mut self) { let uses_console = self.used_effects.contains("Console"); let uses_random = self.used_effects.contains("Random"); let uses_time = self.used_effects.contains("Time"); let uses_http = self.used_effects.contains("Http"); let uses_dom = self.used_effects.contains("Dom"); let uses_html = self.used_effects.contains("Html") || uses_dom; self.writeln("// Lux Runtime"); self.writeln("const Lux = {"); self.indent += 1; // Core helpers — always emitted self.writeln("Some: (value) => ({ tag: \"Some\", value }),"); self.writeln("None: () => ({ tag: \"None\" }),"); self.writeln(""); self.writeln("Ok: (value) => ({ tag: \"Ok\", value }),"); self.writeln("Err: (error) => ({ tag: \"Err\", error }),"); self.writeln(""); self.writeln("Cons: (head, tail) => [head, ...tail],"); self.writeln("Nil: () => [],"); self.writeln(""); // Default handlers — only include effects that are used self.writeln("defaultHandlers: {"); self.indent += 1; if uses_console { self.emit_console_handler(); } if uses_random { self.emit_random_handler(); } if uses_time { self.emit_time_handler(); } if uses_http { self.emit_http_handler(); } if uses_dom { self.emit_dom_handler(); } self.indent -= 1; self.writeln("},"); // HTML rendering — only if Html or Dom effects are used if uses_html { self.emit_html_helpers(); } // TEA runtime — only if Dom is used if uses_dom { self.emit_tea_runtime(); } self.indent -= 1; self.writeln("};"); self.writeln(""); } fn emit_console_handler(&mut self) { self.writeln("Console: {"); self.indent += 1; self.writeln("print: (msg) => console.log(msg),"); self.writeln("readLine: () => {"); self.indent += 1; self.writeln("if (typeof require !== 'undefined') {"); self.indent += 1; self.writeln("const readline = require('readline');"); self.writeln("const rl = readline.createInterface({ input: process.stdin, output: process.stdout });"); self.writeln("return new Promise(resolve => rl.question('', answer => { rl.close(); resolve(answer); }));"); self.indent -= 1; self.writeln("}"); self.writeln("return prompt('') || '';"); self.indent -= 1; self.writeln("},"); self.writeln("readInt: () => parseInt(Lux.defaultHandlers.Console.readLine(), 10)"); self.indent -= 1; self.writeln("},"); } fn emit_random_handler(&mut self) { self.writeln("Random: {"); self.indent += 1; self.writeln("int: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min,"); self.writeln("bool: () => Math.random() < 0.5,"); self.writeln("float: () => Math.random()"); self.indent -= 1; self.writeln("},"); } fn emit_time_handler(&mut self) { self.writeln("Time: {"); self.indent += 1; self.writeln("now: () => Date.now(),"); self.writeln("sleep: (ms) => new Promise(resolve => setTimeout(resolve, ms))"); self.indent -= 1; self.writeln("},"); } fn emit_http_handler(&mut self) { self.writeln("Http: {"); self.indent += 1; self.writeln("get: async (url) => {"); self.indent += 1; self.writeln("try {"); self.indent += 1; self.writeln("const response = await fetch(url);"); self.writeln("const body = await response.text();"); self.writeln("const headers = [];"); self.writeln("response.headers.forEach((v, k) => headers.push([k, v]));"); self.writeln("return Lux.Ok({ status: response.status, body, headers });"); self.indent -= 1; self.writeln("} catch (e) {"); self.indent += 1; self.writeln("return Lux.Err(e.message);"); self.indent -= 1; self.writeln("}"); self.indent -= 1; self.writeln("},"); self.writeln("post: async (url, body) => {"); self.indent += 1; self.writeln("try {"); self.indent += 1; self.writeln("const response = await fetch(url, { method: 'POST', body });"); self.writeln("const respBody = await response.text();"); self.writeln("const headers = [];"); self.writeln("response.headers.forEach((v, k) => headers.push([k, v]));"); self.writeln("return Lux.Ok({ status: response.status, body: respBody, headers });"); self.indent -= 1; self.writeln("} catch (e) {"); self.indent += 1; self.writeln("return Lux.Err(e.message);"); self.indent -= 1; self.writeln("}"); self.indent -= 1; self.writeln("},"); self.writeln("postJson: async (url, json) => {"); self.indent += 1; self.writeln("try {"); self.indent += 1; self.writeln("const response = await fetch(url, {"); self.indent += 1; self.writeln("method: 'POST',"); self.writeln("headers: { 'Content-Type': 'application/json' },"); self.writeln("body: JSON.stringify(json)"); self.indent -= 1; self.writeln("});"); self.writeln("const body = await response.text();"); self.writeln("const headers = [];"); self.writeln("response.headers.forEach((v, k) => headers.push([k, v]));"); self.writeln("return Lux.Ok({ status: response.status, body, headers });"); self.indent -= 1; self.writeln("} catch (e) {"); self.indent += 1; self.writeln("return Lux.Err(e.message);"); self.indent -= 1; self.writeln("}"); self.indent -= 1; self.writeln("}"); self.indent -= 1; self.writeln("},"); } fn emit_dom_handler(&mut self) { self.writeln("Dom: {"); self.indent += 1; // Query selectors self.writeln("querySelector: (selector) => {"); self.indent += 1; self.writeln("if (typeof document === 'undefined') return Lux.None();"); self.writeln("const el = document.querySelector(selector);"); self.writeln("return el ? Lux.Some(el) : Lux.None();"); self.indent -= 1; self.writeln("},"); self.writeln("querySelectorAll: (selector) => {"); self.indent += 1; self.writeln("if (typeof document === 'undefined') return [];"); self.writeln("return Array.from(document.querySelectorAll(selector));"); self.indent -= 1; self.writeln("},"); self.writeln("getElementById: (id) => {"); self.indent += 1; self.writeln("if (typeof document === 'undefined') return Lux.None();"); self.writeln("const el = document.getElementById(id);"); self.writeln("return el ? Lux.Some(el) : Lux.None();"); self.indent -= 1; self.writeln("},"); self.writeln("createElement: (tag) => {"); self.indent += 1; self.writeln("if (typeof document === 'undefined') return null;"); self.writeln("return document.createElement(tag);"); self.indent -= 1; self.writeln("},"); self.writeln("createTextNode: (text) => {"); self.indent += 1; self.writeln("if (typeof document === 'undefined') return null;"); self.writeln("return document.createTextNode(text);"); self.indent -= 1; self.writeln("},"); self.writeln("appendChild: (parent, child) => {"); self.indent += 1; self.writeln("if (parent && child) parent.appendChild(child);"); self.indent -= 1; self.writeln("},"); self.writeln("removeChild: (parent, child) => {"); self.indent += 1; self.writeln("if (parent && child) parent.removeChild(child);"); self.indent -= 1; self.writeln("},"); self.writeln("replaceChild: (parent, newChild, oldChild) => {"); self.indent += 1; self.writeln("if (parent && newChild && oldChild) parent.replaceChild(newChild, oldChild);"); self.indent -= 1; self.writeln("},"); self.writeln("insertBefore: (parent, newNode, refNode) => {"); self.indent += 1; self.writeln("if (parent && newNode) parent.insertBefore(newNode, refNode);"); self.indent -= 1; self.writeln("},"); self.writeln("setTextContent: (el, text) => {"); self.indent += 1; self.writeln("if (el) el.textContent = text;"); self.indent -= 1; self.writeln("},"); self.writeln("getTextContent: (el) => {"); self.indent += 1; self.writeln("return el ? el.textContent : '';"); self.indent -= 1; self.writeln("},"); self.writeln("setInnerHtml: (el, html) => {"); self.indent += 1; self.writeln("if (el) el.innerHTML = html;"); self.indent -= 1; self.writeln("},"); self.writeln("getInnerHtml: (el) => {"); self.indent += 1; self.writeln("return el ? el.innerHTML : '';"); self.indent -= 1; self.writeln("},"); self.writeln("setAttribute: (el, name, value) => {"); self.indent += 1; self.writeln("if (el) el.setAttribute(name, value);"); self.indent -= 1; self.writeln("},"); self.writeln("getAttribute: (el, name) => {"); self.indent += 1; self.writeln("if (!el) return Lux.None();"); self.writeln("const val = el.getAttribute(name);"); self.writeln("return val !== null ? Lux.Some(val) : Lux.None();"); self.indent -= 1; self.writeln("},"); self.writeln("removeAttribute: (el, name) => {"); self.indent += 1; self.writeln("if (el) el.removeAttribute(name);"); self.indent -= 1; self.writeln("},"); self.writeln("hasAttribute: (el, name) => {"); self.indent += 1; self.writeln("return el ? el.hasAttribute(name) : false;"); self.indent -= 1; self.writeln("},"); self.writeln("addClass: (el, className) => {"); self.indent += 1; self.writeln("if (el) el.classList.add(className);"); self.indent -= 1; self.writeln("},"); self.writeln("removeClass: (el, className) => {"); self.indent += 1; self.writeln("if (el) el.classList.remove(className);"); self.indent -= 1; self.writeln("},"); self.writeln("toggleClass: (el, className) => {"); self.indent += 1; self.writeln("if (el) el.classList.toggle(className);"); self.indent -= 1; self.writeln("},"); self.writeln("hasClass: (el, className) => {"); self.indent += 1; self.writeln("return el ? el.classList.contains(className) : false;"); self.indent -= 1; self.writeln("},"); self.writeln("setStyle: (el, property, value) => {"); self.indent += 1; self.writeln("if (el) el.style[property] = value;"); self.indent -= 1; self.writeln("},"); self.writeln("getStyle: (el, property) => {"); self.indent += 1; self.writeln("return el ? el.style[property] : '';"); self.indent -= 1; self.writeln("},"); self.writeln("getValue: (el) => {"); self.indent += 1; self.writeln("return el ? el.value : '';"); self.indent -= 1; self.writeln("},"); self.writeln("setValue: (el, value) => {"); self.indent += 1; self.writeln("if (el) el.value = value;"); self.indent -= 1; self.writeln("},"); self.writeln("isChecked: (el) => {"); self.indent += 1; self.writeln("return el ? el.checked : false;"); self.indent -= 1; self.writeln("},"); self.writeln("setChecked: (el, checked) => {"); self.indent += 1; self.writeln("if (el) el.checked = checked;"); self.indent -= 1; self.writeln("},"); self.writeln("addEventListener: (el, event, handler) => {"); self.indent += 1; self.writeln("if (el) el.addEventListener(event, handler);"); self.indent -= 1; self.writeln("},"); self.writeln("removeEventListener: (el, event, handler) => {"); self.indent += 1; self.writeln("if (el) el.removeEventListener(event, handler);"); self.indent -= 1; self.writeln("},"); self.writeln("focus: (el) => {"); self.indent += 1; self.writeln("if (el && el.focus) el.focus();"); self.indent -= 1; self.writeln("},"); self.writeln("blur: (el) => {"); self.indent += 1; self.writeln("if (el && el.blur) el.blur();"); self.indent -= 1; self.writeln("},"); self.writeln("getBody: () => {"); self.indent += 1; self.writeln("if (typeof document === 'undefined') return null;"); self.writeln("return document.body;"); self.indent -= 1; self.writeln("},"); self.writeln("getHead: () => {"); self.indent += 1; self.writeln("if (typeof document === 'undefined') return null;"); self.writeln("return document.head;"); self.indent -= 1; self.writeln("},"); self.writeln("getWindow: () => {"); self.indent += 1; self.writeln("if (typeof window === 'undefined') return null;"); self.writeln("return window;"); self.indent -= 1; self.writeln("},"); self.writeln("alert: (msg) => {"); self.indent += 1; self.writeln("if (typeof alert !== 'undefined') alert(msg);"); self.indent -= 1; self.writeln("},"); self.writeln("confirm: (msg) => {"); self.indent += 1; self.writeln("if (typeof confirm !== 'undefined') return confirm(msg);"); self.writeln("return false;"); self.indent -= 1; self.writeln("},"); self.writeln("prompt: (msg, defaultValue) => {"); self.indent += 1; self.writeln("if (typeof prompt !== 'undefined') {"); self.indent += 1; self.writeln("const result = prompt(msg, defaultValue || '');"); self.writeln("return result !== null ? Lux.Some(result) : Lux.None();"); self.indent -= 1; self.writeln("}"); self.writeln("return Lux.None();"); self.indent -= 1; self.writeln("},"); self.writeln("scrollTo: (x, y) => {"); self.indent += 1; self.writeln("if (typeof window !== 'undefined') window.scrollTo(x, y);"); self.indent -= 1; self.writeln("},"); self.writeln("scrollIntoView: (el) => {"); self.indent += 1; self.writeln("if (el && el.scrollIntoView) el.scrollIntoView();"); self.indent -= 1; self.writeln("},"); self.writeln("getBoundingClientRect: (el) => {"); self.indent += 1; self.writeln("if (!el) return { top: 0, left: 0, width: 0, height: 0, right: 0, bottom: 0 };"); self.writeln("const rect = el.getBoundingClientRect();"); self.writeln("return { top: rect.top, left: rect.left, width: rect.width, height: rect.height, right: rect.right, bottom: rect.bottom };"); self.indent -= 1; self.writeln("},"); self.writeln("getWindowSize: () => {"); self.indent += 1; self.writeln("if (typeof window === 'undefined') return { width: 0, height: 0 };"); self.writeln("return { width: window.innerWidth, height: window.innerHeight };"); self.indent -= 1; self.writeln("}"); self.indent -= 1; self.writeln("},"); } fn emit_html_helpers(&mut self) { self.writeln(""); self.writeln("// HTML rendering"); self.writeln("renderHtml: (node) => {"); self.indent += 1; self.writeln("if (!node) return '';"); self.writeln("if (typeof node === 'string') return Lux.escapeHtml(node);"); self.writeln("if (node.tag === 'text') return Lux.escapeHtml(node.content);"); self.writeln("if (node.tag === 'raw') return node.html;"); self.writeln("if (node.tag === 'empty') return '';"); self.writeln("if (node.tag === 'fragment') return node.children.map(Lux.renderHtml).join('');"); self.writeln(""); self.writeln("const tag = node.tag;"); self.writeln("const attrs = (node.attrs || []).filter(a => a.attr).map(a => ` ${a.attr}=\"${Lux.escapeHtml(a.value)}\"`).join('');"); self.writeln("const events = (node.attrs || []).filter(a => a.event);"); self.writeln(""); self.writeln("// Self-closing tags"); self.writeln("if (['br', 'hr', 'img', 'input', 'meta', 'link'].includes(tag)) {"); self.indent += 1; self.writeln("return `<${tag}${attrs} />`;"); self.indent -= 1; self.writeln("}"); self.writeln(""); self.writeln("const children = (node.children || []).map(Lux.renderHtml).join('');"); self.writeln("return `<${tag}${attrs}>${children}`;"); self.indent -= 1; self.writeln("},"); self.writeln(""); self.writeln("// Escape HTML special characters"); self.writeln("escapeHtml: (str) => {"); self.indent += 1; self.writeln("if (typeof str !== 'string') return String(str);"); self.writeln("return str"); self.indent += 1; self.writeln(".replace(/&/g, '&')"); self.writeln(".replace(//g, '>')"); self.writeln(".replace(/\"/g, '"')"); self.writeln(".replace(/'/g, ''');"); self.indent -= 1; self.indent -= 1; self.writeln("},"); self.writeln(""); self.writeln("// Render HTML to DOM element"); self.writeln("renderToDom: (node) => {"); self.indent += 1; self.writeln("if (typeof document === 'undefined') return null;"); self.writeln("if (!node) return null;"); self.writeln("if (typeof node === 'string') return document.createTextNode(node);"); self.writeln("if (node.tag === 'text') return document.createTextNode(node.content);"); self.writeln("if (node.tag === 'raw') {"); self.indent += 1; self.writeln("const wrapper = document.createElement('div');"); self.writeln("wrapper.innerHTML = node.html;"); self.writeln("return wrapper.firstChild;"); self.indent -= 1; self.writeln("}"); self.writeln("if (node.tag === 'empty') return document.createTextNode('');"); self.writeln("if (node.tag === 'fragment') {"); self.indent += 1; self.writeln("const frag = document.createDocumentFragment();"); self.writeln("for (const child of node.children) {"); self.indent += 1; self.writeln("const el = Lux.renderToDom(child);"); self.writeln("if (el) frag.appendChild(el);"); self.indent -= 1; self.writeln("}"); self.writeln("return frag;"); self.indent -= 1; self.writeln("}"); self.writeln(""); self.writeln("const el = document.createElement(node.tag);"); self.writeln(""); self.writeln("// Set attributes"); self.writeln("for (const attr of (node.attrs || [])) {"); self.indent += 1; self.writeln("if (attr.attr) {"); self.indent += 1; self.writeln("el.setAttribute(attr.attr, attr.value);"); self.indent -= 1; self.writeln("}"); self.writeln("if (attr.event && attr.handler) {"); self.indent += 1; self.writeln("el.addEventListener(attr.event, attr.handler);"); self.indent -= 1; self.writeln("}"); self.indent -= 1; self.writeln("}"); self.writeln(""); self.writeln("// Append children"); self.writeln("for (const child of (node.children || [])) {"); self.indent += 1; self.writeln("const childEl = Lux.renderToDom(child);"); self.writeln("if (childEl) el.appendChild(childEl);"); self.indent -= 1; self.writeln("}"); self.writeln(""); self.writeln("return el;"); self.indent -= 1; self.writeln("},"); } fn emit_tea_runtime(&mut self) { self.writeln(""); self.writeln("// The Elm Architecture (TEA) runtime"); self.writeln("app: (config) => {"); self.indent += 1; self.writeln("const { init, update, view, root } = config;"); self.writeln("let model = init;"); self.writeln("let rootEl = typeof root === 'string' ? document.querySelector(root) : root;"); self.writeln("let currentVdom = null;"); self.writeln("let currentDom = null;"); self.writeln(""); self.writeln("const dispatch = (msg) => {"); self.indent += 1; self.writeln("model = update(model, msg);"); self.writeln("render();"); self.indent -= 1; self.writeln("};"); self.writeln(""); self.writeln("const render = () => {"); self.indent += 1; self.writeln("const newVdom = view(model, dispatch);"); self.writeln("const newDom = Lux.renderToDom(newVdom);"); self.writeln("if (currentDom) {"); self.indent += 1; self.writeln("rootEl.replaceChild(newDom, currentDom);"); self.indent -= 1; self.writeln("} else {"); self.indent += 1; self.writeln("rootEl.innerHTML = '';"); self.writeln("rootEl.appendChild(newDom);"); self.indent -= 1; self.writeln("}"); self.writeln("currentVdom = newVdom;"); self.writeln("currentDom = newDom;"); self.indent -= 1; self.writeln("};"); self.writeln(""); self.writeln("// Initial render"); self.writeln("if (rootEl) render();"); self.writeln(""); self.writeln("return { dispatch, getModel: () => model };"); self.indent -= 1; self.writeln("},"); self.writeln(""); self.writeln("// Simple TEA app (string-based view)"); self.writeln("simpleApp: (config) => {"); self.indent += 1; self.writeln("const { init, update, view, root } = config;"); self.writeln("let model = init;"); self.writeln("let rootEl = typeof root === 'string' ? document.querySelector(root) : root;"); self.writeln(""); self.writeln("const dispatch = (msg) => {"); self.indent += 1; self.writeln("model = update(model, msg);"); self.writeln("render();"); self.indent -= 1; self.writeln("};"); self.writeln(""); self.writeln("// Make dispatch globally available for onclick handlers"); self.writeln("window.dispatch = dispatch;"); self.writeln(""); self.writeln("const render = () => {"); self.indent += 1; self.writeln("if (rootEl) rootEl.innerHTML = view(model);"); self.indent -= 1; self.writeln("};"); self.writeln(""); self.writeln("if (rootEl) render();"); self.writeln("return { dispatch, getModel: () => model };"); self.indent -= 1; self.writeln("},"); self.writeln(""); self.writeln("// Basic diff - checks if model fields changed"); self.writeln("hasChanged: (oldModel, newModel, ...paths) => {"); self.indent += 1; self.writeln("for (const path of paths) {"); self.indent += 1; self.writeln("const parts = path.split('.');"); self.writeln("let oldVal = oldModel, newVal = newModel;"); self.writeln("for (const part of parts) {"); self.indent += 1; self.writeln("oldVal = oldVal?.[part];"); self.writeln("newVal = newVal?.[part];"); self.indent -= 1; self.writeln("}"); self.writeln("if (oldVal !== newVal) return true;"); self.indent -= 1; self.writeln("}"); self.writeln("return false;"); self.indent -= 1; self.writeln("},"); } /// Collect type information from a type declaration fn collect_type(&mut self, decl: &TypeDecl) -> Result<(), JsGenError> { if let TypeDef::Enum(variants) = &decl.definition { for variant in variants { self.variant_to_type .insert(variant.name.name.clone(), decl.name.name.clone()); // Store field types let field_types: Vec = match &variant.fields { VariantFields::Unit => vec![], VariantFields::Tuple(types) => { types.iter().map(|_| "any".to_string()).collect() } VariantFields::Record(fields) => { fields.iter().map(|_| "any".to_string()).collect() } }; self.variant_field_types.insert( (decl.name.name.clone(), variant.name.name.clone()), field_types, ); } } Ok(()) } /// Emit constructor functions for ADT variants fn emit_type_constructors(&mut self, decl: &TypeDecl) -> Result<(), JsGenError> { if let TypeDef::Enum(variants) = &decl.definition { self.writeln(&format!("// {} constructors", decl.name.name)); for variant in variants { match &variant.fields { VariantFields::Unit => { // Unit variant: const None_lux = { tag: "None" }; self.writeln(&format!( "const {}_lux = {{ tag: \"{}\" }};", variant.name.name, variant.name.name )); } VariantFields::Tuple(types) => { // Tuple variant: function Some_lux(value) { return { tag: "Some", field0: value }; } let params: Vec = (0..types.len()) .map(|i| format!("field{}", i)) .collect(); let fields: Vec = params .iter() .enumerate() .map(|(i, p)| format!("field{}: {}", i, p)) .collect(); self.writeln(&format!( "function {}_lux({}) {{ return {{ tag: \"{}\", {} }}; }}", variant.name.name, params.join(", "), variant.name.name, fields.join(", ") )); } VariantFields::Record(fields) => { // Record variant: function Foo_lux(a, b) { return { tag: "Foo", a, b }; } let params: Vec = fields.iter().map(|f| f.name.name.clone()).collect(); let field_inits: Vec = params.iter().map(|p| format!("{}: {}", p, p)).collect(); self.writeln(&format!( "function {}_lux({}) {{ return {{ tag: \"{}\", {} }}; }}", variant.name.name, params.join(", "), variant.name.name, field_inits.join(", ") )); } } } self.writeln(""); } Ok(()) } /// Emit a function declaration fn emit_function(&mut self, func: &FunctionDecl) -> Result<(), JsGenError> { let func_name = self.mangle_name(&func.name.name); let is_effectful = !func.effects.is_empty(); // Build parameter list let mut params: Vec = func.params.iter().map(|p| p.name.name.clone()).collect(); // Effectful functions get handlers as first parameter if is_effectful { params.insert(0, "handlers".to_string()); } // Function declaration self.writeln(&format!( "function {}({}) {{", func_name, params.join(", ") )); self.indent += 1; // Set context for effect handling let prev_has_handlers = self.has_handlers; self.has_handlers = is_effectful; // Save and clear var substitutions for this function scope let saved_substitutions = self.var_substitutions.clone(); self.var_substitutions.clear(); // Emit function body let body_code = self.emit_expr(&func.body)?; self.writeln(&format!("return {};", body_code)); self.has_handlers = prev_has_handlers; self.var_substitutions = saved_substitutions; self.indent -= 1; self.writeln("}"); self.writeln(""); Ok(()) } /// Emit a top-level let binding fn emit_top_level_let(&mut self, let_decl: &LetDecl) -> Result<(), JsGenError> { let val = self.emit_expr(&let_decl.value)?; let var_name = &let_decl.name.name; if var_name == "_" { // Wildcard binding: just execute for side effects self.writeln(&format!("{};", val)); } else { self.writeln(&format!("const {} = {};", var_name, val)); // Register the variable for future use self.var_substitutions .insert(var_name.clone(), var_name.clone()); } Ok(()) } /// Emit an expression and return the JavaScript code fn emit_expr(&mut self, expr: &Expr) -> Result { match expr { Expr::Literal(lit) => self.emit_literal(lit), Expr::Var(ident) => { // Check for variable substitution if let Some(subst) = self.var_substitutions.get(&ident.name) { return Ok(subst.clone()); } // Check if this is a unit constructor if let Some(type_name) = self.variant_to_type.get(&ident.name) { // Use Lux.* for built-in types (Option, Result) if type_name == "Option" || type_name == "Result" { Ok(format!("Lux.{}()", ident.name)) } else { Ok(format!("{}_lux", ident.name)) } } else if self.functions.contains(&ident.name) { // Function reference (used as value) Ok(self.mangle_name(&ident.name)) } else if let Some(js_name) = self.extern_lets.get(&ident.name) { // Extern let: use JS name directly (no mangling) Ok(js_name.clone()) } else { Ok(self.escape_js_keyword(&ident.name)) } } Expr::BinaryOp { op, left, right, .. } => { let l = self.emit_expr(left)?; let r = self.emit_expr(right)?; // Check for string concatenation if matches!(op, BinaryOp::Add | BinaryOp::Concat) { if self.is_string_expr(left) || self.is_string_expr(right) { return Ok(format!("({} + {})", l, r)); } } // ++ on lists: use .concat() if matches!(op, BinaryOp::Concat) { return Ok(format!("{}.concat({})", l, r)); } let op_str = match op { BinaryOp::Add => "+", BinaryOp::Sub => "-", BinaryOp::Mul => "*", BinaryOp::Div => "/", BinaryOp::Mod => "%", BinaryOp::Eq => "===", BinaryOp::Ne => "!==", BinaryOp::Lt => "<", BinaryOp::Le => "<=", BinaryOp::Gt => ">", BinaryOp::Ge => ">=", BinaryOp::And => "&&", BinaryOp::Or => "||", BinaryOp::Concat => unreachable!("handled above"), BinaryOp::Pipe => { // Pipe operator: x |> f becomes f(x) return Ok(format!("{}({})", r, l)); } }; Ok(format!("({} {} {})", l, op_str, r)) } Expr::UnaryOp { op, operand, .. } => { let val = self.emit_expr(operand)?; let op_str = match op { UnaryOp::Neg => "-", UnaryOp::Not => "!", }; Ok(format!("({}{})", op_str, val)) } Expr::If { condition, then_branch, else_branch, .. } => { // Check if branches contain statements that need if-else instead of ternary let needs_block = self.expr_has_statements(then_branch) || self.expr_has_statements(else_branch); if needs_block { // Use if-else statement for branches with statements let result_var = format!("_if_result_{}", self.fresh_name()); let cond = self.emit_expr(condition)?; self.writeln(&format!("let {};", result_var)); self.writeln(&format!("if ({}) {{", cond)); self.indent += 1; let then_val = self.emit_expr(then_branch)?; self.writeln(&format!("{} = {};", result_var, then_val)); self.indent -= 1; self.writeln("} else {"); self.indent += 1; let else_val = self.emit_expr(else_branch)?; self.writeln(&format!("{} = {};", result_var, else_val)); self.indent -= 1; self.writeln("}"); Ok(result_var) } else { // Simple ternary for pure expressions let cond = self.emit_expr(condition)?; let then_val = self.emit_expr(then_branch)?; let else_val = self.emit_expr(else_branch)?; Ok(format!("({} ? {} : {})", cond, then_val, else_val)) } } Expr::Let { name, value, body, .. } => { let val = self.emit_expr(value)?; if name.name == "_" { // Wildcard binding: just execute for side effects self.writeln(&format!("{};", val)); } else { let var_name = format!("{}_{}", name.name, self.fresh_name()); self.writeln(&format!("const {} = {};", var_name, val)); // Add substitution self.var_substitutions .insert(name.name.clone(), var_name.clone()); } let body_result = self.emit_expr(body)?; // Remove substitution if name.name != "_" { self.var_substitutions.remove(&name.name); } Ok(body_result) } Expr::Call { func, args, .. } => { // Check for List module calls if let Expr::Field { object, field, .. } = func.as_ref() { if let Expr::Var(module_name) = object.as_ref() { if module_name.name == "List" { return self.emit_list_operation(&field.name, args); } if module_name.name == "Map" { return self.emit_map_operation(&field.name, args); } if module_name.name == "Ref" { return self.emit_ref_operation(&field.name, args); } } } // Int/Float module operations if let Expr::Field { object, field, .. } = func.as_ref() { if let Expr::Var(module_name) = object.as_ref() { if module_name.name == "Int" { let arg = self.emit_expr(&args[0])?; match field.name.as_str() { "toFloat" => return Ok(arg), "toString" => return Ok(format!("String({})", arg)), _ => {} } } if module_name.name == "Float" { let arg = self.emit_expr(&args[0])?; match field.name.as_str() { "toInt" => return Ok(format!("Math.trunc({})", arg)), "toString" => return Ok(format!("String({})", arg)), _ => {} } } } } // Check for built-in functions if let Expr::Var(ident) = func.as_ref() { if ident.name == "toString" { let arg = self.emit_expr(&args[0])?; return Ok(format!("String({})", arg)); } if ident.name == "print" { let arg = self.emit_expr(&args[0])?; return Ok(format!("console.log({})", arg)); } } let arg_strs: Result, _> = args.iter().map(|a| self.emit_expr(a)).collect(); let args_str = arg_strs?.join(", "); match func.as_ref() { Expr::Var(ident) if self.functions.contains(&ident.name) => { let js_func_name = self.mangle_name(&ident.name); let is_effectful = self.effectful_functions.contains(&ident.name); if is_effectful && self.has_handlers { // Use the handlers variable from substitutions, or default "handlers" let handlers_name = self .var_substitutions .get("handlers") .cloned() .unwrap_or_else(|| "handlers".to_string()); if args_str.is_empty() { Ok(format!("{}({})", js_func_name, handlers_name)) } else { Ok(format!("{}({}, {})", js_func_name, handlers_name, args_str)) } } else if is_effectful { if args_str.is_empty() { Ok(format!("{}(Lux.defaultHandlers)", js_func_name)) } else { Ok(format!("{}(Lux.defaultHandlers, {})", js_func_name, args_str)) } } else { Ok(format!("{}({})", js_func_name, args_str)) } } Expr::Var(ident) if self.variant_to_type.contains_key(&ident.name) => { // ADT constructor call // Use Lux.* for built-in types (Option, Result) let type_name = self.variant_to_type.get(&ident.name).unwrap(); if type_name == "Option" || type_name == "Result" { Ok(format!("Lux.{}({})", ident.name, args_str)) } else { Ok(format!("{}_lux({})", ident.name, args_str)) } } _ => { // Generic function call let func_code = self.emit_expr(func)?; Ok(format!("{}({})", func_code, args_str)) } } } Expr::EffectOp { effect, operation, args, .. } => { // Special case: List module operations (not an effect) if effect.name == "List" { return self.emit_list_operation(&operation.name, args); } // Special case: String module operations (not an effect) if effect.name == "String" { return self.emit_string_operation(&operation.name, args); } // Special case: Option module operations (not an effect) if effect.name == "Option" { return self.emit_option_operation(&operation.name, args); } // Special case: Math module operations (not an effect) if effect.name == "Math" { return self.emit_math_operation(&operation.name, args); } // Special case: Int module operations if effect.name == "Int" { let arg = self.emit_expr(&args[0])?; match operation.name.as_str() { "toFloat" => return Ok(arg), // JS numbers are already floats "toString" => return Ok(format!("String({})", arg)), _ => {} } } // Special case: Float module operations if effect.name == "Float" { let arg = self.emit_expr(&args[0])?; match operation.name.as_str() { "toInt" => return Ok(format!("Math.trunc({})", arg)), "toString" => return Ok(format!("String({})", arg)), _ => {} } } // Special case: Result module operations (not an effect) if effect.name == "Result" { return self.emit_result_operation(&operation.name, args); } // Special case: Json module operations (not an effect) if effect.name == "Json" { return self.emit_json_operation(&operation.name, args); } // Special case: Map module operations (not an effect) if effect.name == "Map" { return self.emit_map_operation(&operation.name, args); } // Special case: Ref module operations (not an effect) if effect.name == "Ref" { return self.emit_ref_operation(&operation.name, args); } // Special case: Html module operations (not an effect) if effect.name == "Html" { return self.emit_html_operation(&operation.name, args); } let arg_strs: Result, _> = args.iter().map(|a| self.emit_expr(a)).collect(); let args_str = arg_strs?.join(", "); if self.has_handlers { // Use the handlers variable from substitutions, or default "handlers" let handlers_name = self .var_substitutions .get("handlers") .cloned() .unwrap_or_else(|| "handlers".to_string()); Ok(format!( "{}.{}.{}({})", handlers_name, effect.name, operation.name, args_str )) } else { Ok(format!( "Lux.defaultHandlers.{}.{}({})", effect.name, operation.name, args_str )) } } Expr::Lambda { params, body, effects, .. } => { let param_names: Vec = params.iter().map(|p| p.name.name.clone()).collect(); // If lambda has effects, it takes handlers let all_params = if !effects.is_empty() { let mut p = vec!["handlers".to_string()]; p.extend(param_names); p } else { param_names }; // Save state let prev_has_handlers = self.has_handlers; let saved_substitutions = self.var_substitutions.clone(); self.has_handlers = !effects.is_empty(); // Register lambda params as themselves (override any outer substitutions) for p in &all_params { self.var_substitutions.insert(p.clone(), p.clone()); } // Capture any statements emitted during body evaluation let output_start = self.output.len(); let prev_indent = self.indent; self.indent += 1; let body_code = self.emit_expr(body)?; self.writeln(&format!("return {};", body_code)); // Extract body statements and restore output let body_statements = self.output[output_start..].to_string(); self.output.truncate(output_start); self.indent = prev_indent; // Restore state self.has_handlers = prev_has_handlers; self.var_substitutions = saved_substitutions; let indent_str = " ".repeat(self.indent); Ok(format!( "(function({}) {{\n{}{}}})", all_params.join(", "), body_statements, indent_str, )) } Expr::Match { scrutinee, arms, .. } => self.emit_match(scrutinee, arms), Expr::Block { statements, result, .. } => { // Emit each statement for stmt in statements { match stmt { Statement::Expr(expr) => { let code = self.emit_expr(expr)?; self.writeln(&format!("{};", code)); } Statement::Let { name, value, .. } => { let val = self.emit_expr(value)?; if name.name == "_" { self.writeln(&format!("{};", val)); } else { let var_name = format!("{}_{}", name.name, self.fresh_name()); self.writeln(&format!("const {} = {};", var_name, val)); self.var_substitutions .insert(name.name.clone(), var_name.clone()); } } } } // Emit result self.emit_expr(result) } Expr::Record { spread, fields, .. } => { let mut parts = Vec::new(); if let Some(spread_expr) = spread { let spread_code = self.emit_expr(spread_expr)?; parts.push(format!("...{}", spread_code)); } for (name, expr) in fields { let val = self.emit_expr(expr)?; parts.push(format!("{}: {}", name.name, val)); } Ok(format!("{{ {} }}", parts.join(", "))) } Expr::Tuple { elements, .. } => { let elem_strs: Result, _> = elements.iter().map(|e| self.emit_expr(e)).collect(); Ok(format!("[{}]", elem_strs?.join(", "))) } Expr::List { elements, .. } => { let elem_strs: Result, _> = elements.iter().map(|e| self.emit_expr(e)).collect(); Ok(format!("[{}]", elem_strs?.join(", "))) } Expr::Field { object, field, .. } => { let obj = self.emit_expr(object)?; Ok(format!("{}.{}", obj, field.name)) } Expr::TupleIndex { object, index, .. } => { let obj = self.emit_expr(object)?; Ok(format!("{}[{}]", obj, index)) } Expr::Run { expr, handlers, .. } => { // Create handler object, merging with defaults let handlers_var = format!("_handlers_{}", self.fresh_name()); if handlers.is_empty() { // No custom handlers, use defaults self.writeln(&format!( "const {} = Lux.defaultHandlers;", handlers_var )); } else { // Merge custom handlers with defaults let handler_strs: Result, _> = handlers .iter() .map(|(name, handler_expr)| { let handler_code = self.emit_expr(handler_expr)?; Ok(format!("{}: {}", name.name, handler_code)) }) .collect(); let custom_handlers = handler_strs?.join(", "); self.writeln(&format!( "const {} = {{ ...Lux.defaultHandlers, {} }};", handlers_var, custom_handlers )); } // Set has_handlers and emit the expression let prev_has_handlers = self.has_handlers; self.has_handlers = true; // For function calls inside run, we need to pass the handlers // Save the current handlers variable name for use in calls let saved_substitutions = self.var_substitutions.clone(); self.var_substitutions .insert("handlers".to_string(), handlers_var.clone()); // Emit the expression with the custom handlers let inner_code = self.emit_expr(expr)?; self.var_substitutions = saved_substitutions; self.has_handlers = prev_has_handlers; Ok(inner_code) } Expr::Resume { value, .. } => { let val = self.emit_expr(value)?; Ok(format!("resume({})", val)) } } } /// Emit a match expression fn emit_match(&mut self, scrutinee: &Expr, arms: &[MatchArm]) -> Result { let scrutinee_code = self.emit_expr(scrutinee)?; let scrutinee_var = format!("_match_{}", self.fresh_name()); let result_var = format!("_result_{}", self.fresh_name()); self.writeln(&format!("const {} = {};", scrutinee_var, scrutinee_code)); self.writeln(&format!("let {};", result_var)); for (i, arm) in arms.iter().enumerate() { let condition = self.emit_pattern_condition(&arm.pattern, &scrutinee_var)?; let bindings = self.emit_pattern_bindings(&arm.pattern, &scrutinee_var)?; if i == 0 { self.writeln(&format!("if ({}) {{", condition)); } else { self.writeln(&format!("}} else if ({}) {{", condition)); } self.indent += 1; // Emit bindings for (name, code) in &bindings { self.writeln(&format!("const {} = {};", name, code)); self.var_substitutions.insert(name.clone(), name.clone()); } // Emit guard if present if let Some(guard) = &arm.guard { let guard_code = self.emit_expr(guard)?; self.writeln(&format!("if ({}) {{", guard_code)); self.indent += 1; } // Emit body let body_code = self.emit_expr(&arm.body)?; self.writeln(&format!("{} = {};", result_var, body_code)); if arm.guard.is_some() { self.indent -= 1; self.writeln("}"); } // Remove bindings for (name, _) in &bindings { self.var_substitutions.remove(name); } self.indent -= 1; } self.writeln("} else {"); self.indent += 1; self.writeln(&format!( "throw new Error('Non-exhaustive match on ' + JSON.stringify({}));", scrutinee_var )); self.indent -= 1; self.writeln("}"); Ok(result_var) } /// Emit a condition that checks if a pattern matches fn emit_pattern_condition( &self, pattern: &Pattern, scrutinee: &str, ) -> Result { match pattern { Pattern::Wildcard(_) => Ok("true".to_string()), Pattern::Var(_) => Ok("true".to_string()), Pattern::Literal(lit) => { let lit_code = self.emit_literal(lit)?; Ok(format!("{} === {}", scrutinee, lit_code)) } Pattern::Constructor { name, fields, .. } => { let mut conditions = vec![format!("{}.tag === \"{}\"", scrutinee, name.name)]; for (i, field_pattern) in fields.iter().enumerate() { // Use named fields for built-in types let field_access = match (name.name.as_str(), i) { ("Some", 0) => format!("{}.value", scrutinee), ("Ok", 0) => format!("{}.value", scrutinee), ("Err", 0) => format!("{}.error", scrutinee), _ => format!("{}.field{}", scrutinee, i), }; let field_cond = self.emit_pattern_condition(field_pattern, &field_access)?; if field_cond != "true" { conditions.push(field_cond); } } Ok(conditions.join(" && ")) } Pattern::Record { fields, .. } => { let mut conditions = vec![]; for (field_name, field_pattern) in fields { let field_access = format!("{}.{}", scrutinee, field_name.name); let field_cond = self.emit_pattern_condition(field_pattern, &field_access)?; if field_cond != "true" { conditions.push(field_cond); } } if conditions.is_empty() { Ok("true".to_string()) } else { Ok(conditions.join(" && ")) } } Pattern::Tuple { elements, .. } => { let mut conditions = vec![]; for (i, elem_pattern) in elements.iter().enumerate() { let elem_access = format!("{}[{}]", scrutinee, i); let elem_cond = self.emit_pattern_condition(elem_pattern, &elem_access)?; if elem_cond != "true" { conditions.push(elem_cond); } } if conditions.is_empty() { Ok("true".to_string()) } else { Ok(conditions.join(" && ")) } } } } /// Extract variable bindings from a pattern fn emit_pattern_bindings( &self, pattern: &Pattern, scrutinee: &str, ) -> Result, JsGenError> { let mut bindings = vec![]; match pattern { Pattern::Wildcard(_) => {} Pattern::Var(ident) => { bindings.push((ident.name.clone(), scrutinee.to_string())); } Pattern::Literal(_) => {} Pattern::Constructor { name, fields, .. } => { for (i, field_pattern) in fields.iter().enumerate() { // Use named fields for built-in types let field_access = match (name.name.as_str(), i) { ("Some", 0) => format!("{}.value", scrutinee), ("Ok", 0) => format!("{}.value", scrutinee), ("Err", 0) => format!("{}.error", scrutinee), _ => format!("{}.field{}", scrutinee, i), }; let field_bindings = self.emit_pattern_bindings(field_pattern, &field_access)?; bindings.extend(field_bindings); } } Pattern::Record { fields, .. } => { for (field_name, field_pattern) in fields { let field_access = format!("{}.{}", scrutinee, field_name.name); let field_bindings = self.emit_pattern_bindings(field_pattern, &field_access)?; bindings.extend(field_bindings); } } Pattern::Tuple { elements, .. } => { for (i, elem_pattern) in elements.iter().enumerate() { let elem_access = format!("{}[{}]", scrutinee, i); let elem_bindings = self.emit_pattern_bindings(elem_pattern, &elem_access)?; bindings.extend(elem_bindings); } } } Ok(bindings) } /// Emit List module operations fn emit_list_operation( &mut self, operation: &str, args: &[Expr], ) -> Result { match operation { "map" => { let list = self.emit_expr(&args[0])?; let func = self.emit_expr(&args[1])?; Ok(format!("{}.map({})", list, func)) } "filter" => { let list = self.emit_expr(&args[0])?; let pred = self.emit_expr(&args[1])?; Ok(format!("{}.filter({})", list, pred)) } "foldl" | "fold" => { let list = self.emit_expr(&args[0])?; let init = self.emit_expr(&args[1])?; let func = self.emit_expr(&args[2])?; Ok(format!("{}.reduce({}, {})", list, func, init)) } "foldr" => { let list = self.emit_expr(&args[0])?; let init = self.emit_expr(&args[1])?; let func = self.emit_expr(&args[2])?; Ok(format!("{}.reduceRight({}, {})", list, func, init)) } "length" => { let list = self.emit_expr(&args[0])?; Ok(format!("{}.length", list)) } "head" => { let list = self.emit_expr(&args[0])?; Ok(format!( "({}.length > 0 ? Lux.Some({}[0]) : Lux.None())", list, list )) } "tail" => { let list = self.emit_expr(&args[0])?; Ok(format!( "({}.length > 0 ? Lux.Some({}.slice(1)) : Lux.None())", list, list )) } "isEmpty" => { let list = self.emit_expr(&args[0])?; Ok(format!("{}.length === 0", list)) } "append" => { let list1 = self.emit_expr(&args[0])?; let list2 = self.emit_expr(&args[1])?; Ok(format!("[...{}, ...{}]", list1, list2)) } "reverse" => { let list = self.emit_expr(&args[0])?; Ok(format!("[...{}].reverse()", list)) } "range" => { let start = self.emit_expr(&args[0])?; let end = self.emit_expr(&args[1])?; Ok(format!( "Array.from({{ length: {} - {} + 1 }}, (_, i) => {} + i)", end, start, start )) } "sort" => { let list = self.emit_expr(&args[0])?; Ok(format!( "[...{}].sort((a, b) => a < b ? -1 : a > b ? 1 : 0)", list )) } "sortBy" => { let list = self.emit_expr(&args[0])?; let func = self.emit_expr(&args[1])?; Ok(format!("[...{}].sort({})", list, func)) } "find" => { let list = self.emit_expr(&args[0])?; let pred = self.emit_expr(&args[1])?; Ok(format!( "((__l) => {{ const __r = __l.find({}); return __r !== undefined ? Lux.Some(__r) : Lux.None(); }})({})", pred, list )) } "findIndex" => { let list = self.emit_expr(&args[0])?; let pred = self.emit_expr(&args[1])?; Ok(format!( "((__l) => {{ const __i = __l.findIndex({}); return __i !== -1 ? Lux.Some(__i) : Lux.None(); }})({})", pred, list )) } "any" => { let list = self.emit_expr(&args[0])?; let pred = self.emit_expr(&args[1])?; Ok(format!("{}.some({})", list, pred)) } "all" => { let list = self.emit_expr(&args[0])?; let pred = self.emit_expr(&args[1])?; Ok(format!("{}.every({})", list, pred)) } "zip" => { let list1 = self.emit_expr(&args[0])?; let list2 = self.emit_expr(&args[1])?; Ok(format!( "((__a, __b) => __a.slice(0, Math.min(__a.length, __b.length)).map((__x, __i) => [__x, __b[__i]]))({})", format!("{}, {}", list1, list2) )) } "flatten" => { let list = self.emit_expr(&args[0])?; Ok(format!("{}.flat()", list)) } "contains" => { let list = self.emit_expr(&args[0])?; let elem = self.emit_expr(&args[1])?; Ok(format!( "{}.some(__x => JSON.stringify(__x) === JSON.stringify({}))", list, elem )) } "take" => { let list = self.emit_expr(&args[0])?; let n = self.emit_expr(&args[1])?; Ok(format!("{}.slice(0, Math.max(0, {}))", list, n)) } "drop" => { let list = self.emit_expr(&args[0])?; let n = self.emit_expr(&args[1])?; Ok(format!("{}.slice(Math.max(0, {}))", list, n)) } "forEach" => { let list = self.emit_expr(&args[0])?; let func = self.emit_expr(&args[1])?; Ok(format!("({}.forEach({}), undefined)", list, func)) } _ => Err(JsGenError { message: format!("Unknown List operation: {}", operation), span: None, }), } } /// Emit String module operations (not effects) fn emit_string_operation( &mut self, operation: &str, args: &[Expr], ) -> Result { match operation { "length" => { let s = self.emit_expr(&args[0])?; Ok(format!("{}.length", s)) } "concat" => { let s1 = self.emit_expr(&args[0])?; let s2 = self.emit_expr(&args[1])?; Ok(format!("({} + {})", s1, s2)) } "substring" | "slice" => { let s = self.emit_expr(&args[0])?; let start = self.emit_expr(&args[1])?; let end = self.emit_expr(&args[2])?; Ok(format!("{}.slice({}, {})", s, start, end)) } "charAt" => { let s = self.emit_expr(&args[0])?; let idx = self.emit_expr(&args[1])?; Ok(format!("{}.charAt({})", s, idx)) } "indexOf" => { let s = self.emit_expr(&args[0])?; let sub = self.emit_expr(&args[1])?; Ok(format!("{}.indexOf({})", s, sub)) } "contains" => { let s = self.emit_expr(&args[0])?; let sub = self.emit_expr(&args[1])?; Ok(format!("{}.includes({})", s, sub)) } "startsWith" => { let s = self.emit_expr(&args[0])?; let prefix = self.emit_expr(&args[1])?; Ok(format!("{}.startsWith({})", s, prefix)) } "endsWith" => { let s = self.emit_expr(&args[0])?; let suffix = self.emit_expr(&args[1])?; Ok(format!("{}.endsWith({})", s, suffix)) } "toUpperCase" | "upper" => { let s = self.emit_expr(&args[0])?; Ok(format!("{}.toUpperCase()", s)) } "toLowerCase" | "lower" => { let s = self.emit_expr(&args[0])?; Ok(format!("{}.toLowerCase()", s)) } "trim" => { let s = self.emit_expr(&args[0])?; Ok(format!("{}.trim()", s)) } "split" => { let s = self.emit_expr(&args[0])?; let delim = self.emit_expr(&args[1])?; Ok(format!("{}.split({})", s, delim)) } "join" => { let list = self.emit_expr(&args[0])?; let sep = self.emit_expr(&args[1])?; Ok(format!("{}.join({})", list, sep)) } "replace" => { let s = self.emit_expr(&args[0])?; let from = self.emit_expr(&args[1])?; let to = self.emit_expr(&args[2])?; Ok(format!("{}.replace({}, {})", s, from, to)) } "replaceAll" => { let s = self.emit_expr(&args[0])?; let from = self.emit_expr(&args[1])?; let to = self.emit_expr(&args[2])?; Ok(format!("{}.replaceAll({}, {})", s, from, to)) } "repeat" => { let s = self.emit_expr(&args[0])?; let n = self.emit_expr(&args[1])?; Ok(format!("{}.repeat({})", s, n)) } "isEmpty" => { let s = self.emit_expr(&args[0])?; Ok(format!("{}.length === 0", s)) } "chars" => { let s = self.emit_expr(&args[0])?; Ok(format!("[...{}]", s)) } "lines" => { let s = self.emit_expr(&args[0])?; Ok(format!("{}.split(\"\\n\")", s)) } _ => Err(JsGenError { message: format!("Unknown String operation: {}", operation), span: None, }), } } /// Emit Option module operations (not effects) fn emit_option_operation( &mut self, operation: &str, args: &[Expr], ) -> Result { match operation { "isSome" => { let opt = self.emit_expr(&args[0])?; Ok(format!("({}.tag === \"Some\")", opt)) } "isNone" => { let opt = self.emit_expr(&args[0])?; Ok(format!("({}.tag === \"None\")", opt)) } "unwrap" => { let opt = self.emit_expr(&args[0])?; Ok(format!("{}.value", opt)) } "unwrapOr" | "getOrElse" => { let opt = self.emit_expr(&args[0])?; let default = self.emit_expr(&args[1])?; Ok(format!( "({}.tag === \"Some\" ? {}.value : {})", opt, opt, default )) } "map" => { let opt = self.emit_expr(&args[0])?; let func = self.emit_expr(&args[1])?; Ok(format!( "({}.tag === \"Some\" ? Lux.Some({}({}.value)) : Lux.None())", opt, func, opt )) } "flatMap" | "andThen" => { let opt = self.emit_expr(&args[0])?; let func = self.emit_expr(&args[1])?; Ok(format!( "({}.tag === \"Some\" ? {}({}.value) : Lux.None())", opt, func, opt )) } _ => Err(JsGenError { message: format!("Unknown Option operation: {}", operation), span: None, }), } } /// Emit Math module operations fn emit_math_operation( &mut self, operation: &str, args: &[Expr], ) -> Result { match operation { // Constants "pi" => Ok("Math.PI".to_string()), "e" => Ok("Math.E".to_string()), "infinity" => Ok("Infinity".to_string()), "negInfinity" => Ok("-Infinity".to_string()), "nan" => Ok("NaN".to_string()), // Basic operations "abs" => { let x = self.emit_expr(&args[0])?; Ok(format!("Math.abs({})", x)) } "floor" => { let x = self.emit_expr(&args[0])?; Ok(format!("Math.floor({})", x)) } "ceil" => { let x = self.emit_expr(&args[0])?; Ok(format!("Math.ceil({})", x)) } "round" => { let x = self.emit_expr(&args[0])?; Ok(format!("Math.round({})", x)) } "trunc" => { let x = self.emit_expr(&args[0])?; Ok(format!("Math.trunc({})", x)) } "sign" => { let x = self.emit_expr(&args[0])?; Ok(format!("Math.sign({})", x)) } // Power and roots "pow" => { let base = self.emit_expr(&args[0])?; let exp = self.emit_expr(&args[1])?; Ok(format!("Math.pow({}, {})", base, exp)) } "sqrt" => { let x = self.emit_expr(&args[0])?; Ok(format!("Math.sqrt({})", x)) } "cbrt" => { let x = self.emit_expr(&args[0])?; Ok(format!("Math.cbrt({})", x)) } "exp" => { let x = self.emit_expr(&args[0])?; Ok(format!("Math.exp({})", x)) } "log" | "ln" => { let x = self.emit_expr(&args[0])?; Ok(format!("Math.log({})", x)) } "log10" => { let x = self.emit_expr(&args[0])?; Ok(format!("Math.log10({})", x)) } "log2" => { let x = self.emit_expr(&args[0])?; Ok(format!("Math.log2({})", x)) } // Trigonometry "sin" => { let x = self.emit_expr(&args[0])?; Ok(format!("Math.sin({})", x)) } "cos" => { let x = self.emit_expr(&args[0])?; Ok(format!("Math.cos({})", x)) } "tan" => { let x = self.emit_expr(&args[0])?; Ok(format!("Math.tan({})", x)) } "asin" => { let x = self.emit_expr(&args[0])?; Ok(format!("Math.asin({})", x)) } "acos" => { let x = self.emit_expr(&args[0])?; Ok(format!("Math.acos({})", x)) } "atan" => { let x = self.emit_expr(&args[0])?; Ok(format!("Math.atan({})", x)) } "atan2" => { let y = self.emit_expr(&args[0])?; let x = self.emit_expr(&args[1])?; Ok(format!("Math.atan2({}, {})", y, x)) } "sinh" => { let x = self.emit_expr(&args[0])?; Ok(format!("Math.sinh({})", x)) } "cosh" => { let x = self.emit_expr(&args[0])?; Ok(format!("Math.cosh({})", x)) } "tanh" => { let x = self.emit_expr(&args[0])?; Ok(format!("Math.tanh({})", x)) } // Min/max "min" => { let a = self.emit_expr(&args[0])?; let b = self.emit_expr(&args[1])?; Ok(format!("Math.min({}, {})", a, b)) } "max" => { let a = self.emit_expr(&args[0])?; let b = self.emit_expr(&args[1])?; Ok(format!("Math.max({}, {})", a, b)) } "clamp" => { let x = self.emit_expr(&args[0])?; let min = self.emit_expr(&args[1])?; let max = self.emit_expr(&args[2])?; Ok(format!("Math.min(Math.max({}, {}), {})", x, min, max)) } // Checks "isNaN" => { let x = self.emit_expr(&args[0])?; Ok(format!("Number.isNaN({})", x)) } "isFinite" => { let x = self.emit_expr(&args[0])?; Ok(format!("Number.isFinite({})", x)) } "isInfinite" => { let x = self.emit_expr(&args[0])?; Ok(format!("(!Number.isFinite({}) && !Number.isNaN({}))", x, x)) } // Conversion "toInt" => { let x = self.emit_expr(&args[0])?; Ok(format!("Math.trunc({})", x)) } "toFloat" => { let x = self.emit_expr(&args[0])?; Ok(format!("Number({})", x)) } _ => Err(JsGenError { message: format!("Unknown Math operation: {}", operation), span: None, }), } } /// Emit Result module operations fn emit_result_operation( &mut self, operation: &str, args: &[Expr], ) -> Result { match operation { "isOk" => { let result = self.emit_expr(&args[0])?; Ok(format!("({}.tag === \"Ok\")", result)) } "isErr" => { let result = self.emit_expr(&args[0])?; Ok(format!("({}.tag === \"Err\")", result)) } "unwrap" => { let result = self.emit_expr(&args[0])?; Ok(format!("{}.value", result)) } "unwrapErr" => { let result = self.emit_expr(&args[0])?; Ok(format!("{}.error", result)) } "unwrapOr" => { let result = self.emit_expr(&args[0])?; let default = self.emit_expr(&args[1])?; Ok(format!( "({}.tag === \"Ok\" ? {}.value : {})", result, result, default )) } "map" => { let result = self.emit_expr(&args[0])?; let func = self.emit_expr(&args[1])?; Ok(format!( "({}.tag === \"Ok\" ? Lux.Ok({}({}.value)) : {})", result, func, result, result )) } "mapErr" => { let result = self.emit_expr(&args[0])?; let func = self.emit_expr(&args[1])?; Ok(format!( "({}.tag === \"Err\" ? Lux.Err({}({}.error)) : {})", result, func, result, result )) } "flatMap" | "andThen" => { let result = self.emit_expr(&args[0])?; let func = self.emit_expr(&args[1])?; Ok(format!( "({}.tag === \"Ok\" ? {}({}.value) : {})", result, func, result, result )) } "toOption" => { let result = self.emit_expr(&args[0])?; Ok(format!( "({}.tag === \"Ok\" ? Lux.Some({}.value) : Lux.None())", result, result )) } _ => Err(JsGenError { message: format!("Unknown Result operation: {}", operation), span: None, }), } } /// Emit Json module operations fn emit_json_operation( &mut self, operation: &str, args: &[Expr], ) -> Result { match operation { // Parse JSON string to value (returns Result) "parse" => { let s = self.emit_expr(&args[0])?; Ok(format!( "(function() {{ try {{ return Lux.Ok(JSON.parse({})); }} catch(e) {{ return Lux.Err(e.message); }} }})()", s )) } // Stringify value to JSON string "stringify" => { let v = self.emit_expr(&args[0])?; Ok(format!("JSON.stringify({})", v)) } // Pretty print with indentation "prettyPrint" => { let v = self.emit_expr(&args[0])?; Ok(format!("JSON.stringify({}, null, 2)", v)) } // Get a field from a JSON object (returns Option) "get" => { let obj = self.emit_expr(&args[0])?; let key = self.emit_expr(&args[1])?; Ok(format!( "({}[{}] !== undefined ? Lux.Some({}[{}]) : Lux.None())", obj, key, obj, key )) } // Get a field with a default value "getOr" => { let obj = self.emit_expr(&args[0])?; let key = self.emit_expr(&args[1])?; let default = self.emit_expr(&args[2])?; Ok(format!( "({}[{}] !== undefined ? {}[{}] : {})", obj, key, obj, key, default )) } // Check if key exists "hasKey" => { let obj = self.emit_expr(&args[0])?; let key = self.emit_expr(&args[1])?; Ok(format!("({} in {})", key, obj)) } // Get all keys of an object "keys" => { let obj = self.emit_expr(&args[0])?; Ok(format!("Object.keys({})", obj)) } // Get all values of an object "values" => { let obj = self.emit_expr(&args[0])?; Ok(format!("Object.values({})", obj)) } // Get entries as list of [key, value] pairs "entries" => { let obj = self.emit_expr(&args[0])?; Ok(format!("Object.entries({})", obj)) } // Create object from entries "fromEntries" => { let entries = self.emit_expr(&args[0])?; Ok(format!("Object.fromEntries({})", entries)) } // Check if value is null "isNull" => { let v = self.emit_expr(&args[0])?; Ok(format!("({} === null)", v)) } // Check if value is an array "isArray" => { let v = self.emit_expr(&args[0])?; Ok(format!("Array.isArray({})", v)) } // Check if value is an object "isObject" => { let v = self.emit_expr(&args[0])?; Ok(format!( "(typeof {} === 'object' && {} !== null && !Array.isArray({}))", v, v, v )) } // Get type of JSON value as string "typeOf" => { let v = self.emit_expr(&args[0])?; Ok(format!( "(Array.isArray({}) ? 'array' : {} === null ? 'null' : typeof {})", v, v, v )) } _ => Err(JsGenError { message: format!("Unknown Json operation: {}", operation), span: None, }), } } /// Emit Map module operations using JS Map fn emit_map_operation( &mut self, operation: &str, args: &[Expr], ) -> Result { match operation { "new" => Ok("new Map()".to_string()), "set" => { let map = self.emit_expr(&args[0])?; let key = self.emit_expr(&args[1])?; let val = self.emit_expr(&args[2])?; Ok(format!( "(function() {{ var m = new Map({}); m.set({}, {}); return m; }})()", map, key, val )) } "get" => { let map = self.emit_expr(&args[0])?; let key = self.emit_expr(&args[1])?; Ok(format!( "({0}.has({1}) ? Lux.Some({0}.get({1})) : Lux.None())", map, key )) } "contains" => { let map = self.emit_expr(&args[0])?; let key = self.emit_expr(&args[1])?; Ok(format!("{}.has({})", map, key)) } "remove" => { let map = self.emit_expr(&args[0])?; let key = self.emit_expr(&args[1])?; Ok(format!( "(function() {{ var m = new Map({}); m.delete({}); return m; }})()", map, key )) } "keys" => { let map = self.emit_expr(&args[0])?; Ok(format!("Array.from({}.keys()).sort()", map)) } "values" => { let map = self.emit_expr(&args[0])?; Ok(format!( "Array.from({0}.entries()).sort(function(a,b) {{ return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0; }}).map(function(e) {{ return e[1]; }})", map )) } "size" => { let map = self.emit_expr(&args[0])?; Ok(format!("{}.size", map)) } "isEmpty" => { let map = self.emit_expr(&args[0])?; Ok(format!("({}.size === 0)", map)) } "fromList" => { let list = self.emit_expr(&args[0])?; Ok(format!("new Map({}.map(function(t) {{ return [t[0], t[1]]; }}))", list)) } "toList" => { let map = self.emit_expr(&args[0])?; Ok(format!( "Array.from({}.entries()).sort(function(a,b) {{ return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0; }})", map )) } "merge" => { let m1 = self.emit_expr(&args[0])?; let m2 = self.emit_expr(&args[1])?; Ok(format!("new Map([...{}, ...{}])", m1, m2)) } _ => Err(JsGenError { message: format!("Unknown Map operation: {}", operation), span: None, }), } } fn emit_ref_operation( &mut self, operation: &str, args: &[Expr], ) -> Result { match operation { "new" => { let val = self.emit_expr(&args[0])?; Ok(format!("({{value: {}}})", val)) } "get" => { let r = self.emit_expr(&args[0])?; Ok(format!("({}.value)", r)) } "set" => { let r = self.emit_expr(&args[0])?; let val = self.emit_expr(&args[1])?; Ok(format!("({}.value = {}, undefined)", r, val)) } "update" => { let r = self.emit_expr(&args[0])?; let f = self.emit_expr(&args[1])?; Ok(format!("({0}.value = {1}({0}.value), undefined)", r, f)) } _ => Err(JsGenError { message: format!("Unknown Ref operation: {}", operation), span: None, }), } } /// Emit Html module operations for type-safe HTML construction fn emit_html_operation( &mut self, operation: &str, args: &[Expr], ) -> Result { match operation { // Text node "text" => { let content = self.emit_expr(&args[0])?; Ok(format!( "{{ tag: \"text\", content: {} }}", content )) } // Raw HTML (unsafe) "raw" => { let html = self.emit_expr(&args[0])?; Ok(format!( "{{ tag: \"raw\", html: {} }}", html )) } // Empty node "empty" => Ok("{ tag: \"empty\" }".to_string()), // Fragment (list of nodes) "fragment" => { let children = self.emit_expr(&args[0])?; Ok(format!( "{{ tag: \"fragment\", children: {} }}", children )) } // Standard HTML elements - all take (attrs, children) "div" | "span" | "p" | "a" | "button" | "input" | "form" | "label" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "ul" | "ol" | "li" | "table" | "tr" | "td" | "th" | "thead" | "tbody" | "header" | "footer" | "nav" | "main" | "aside" | "section" | "article" | "pre" | "code" | "blockquote" | "strong" | "em" | "small" | "img" | "video" | "audio" | "canvas" | "svg" | "select" | "option" | "textarea" | "fieldset" | "legend" => { let attrs = self.emit_expr(&args[0])?; let children = self.emit_expr(&args[1])?; Ok(format!( "{{ tag: \"{}\", attrs: {}, children: {} }}", operation, attrs, children )) } // Self-closing elements "br" | "hr" => { Ok(format!( "{{ tag: \"{}\", attrs: [], children: [] }}", operation )) } // Element with custom tag "element" => { let tag = self.emit_expr(&args[0])?; let attrs = self.emit_expr(&args[1])?; let children = self.emit_expr(&args[2])?; Ok(format!( "{{ tag: {}, attrs: {}, children: {} }}", tag, attrs, children )) } // Attribute constructors "class" => { let value = self.emit_expr(&args[0])?; Ok(format!("{{ attr: \"class\", value: {} }}", value)) } "id" => { let value = self.emit_expr(&args[0])?; Ok(format!("{{ attr: \"id\", value: {} }}", value)) } "style" => { let value = self.emit_expr(&args[0])?; Ok(format!("{{ attr: \"style\", value: {} }}", value)) } "href" => { let value = self.emit_expr(&args[0])?; Ok(format!("{{ attr: \"href\", value: {} }}", value)) } "src" => { let value = self.emit_expr(&args[0])?; Ok(format!("{{ attr: \"src\", value: {} }}", value)) } "alt" => { let value = self.emit_expr(&args[0])?; Ok(format!("{{ attr: \"alt\", value: {} }}", value)) } "type" => { let value = self.emit_expr(&args[0])?; Ok(format!("{{ attr: \"type\", value: {} }}", value)) } "name" => { let value = self.emit_expr(&args[0])?; Ok(format!("{{ attr: \"name\", value: {} }}", value)) } "value" => { let value = self.emit_expr(&args[0])?; Ok(format!("{{ attr: \"value\", value: {} }}", value)) } "placeholder" => { let value = self.emit_expr(&args[0])?; Ok(format!("{{ attr: \"placeholder\", value: {} }}", value)) } "disabled" => { Ok("{ attr: \"disabled\", value: \"\" }".to_string()) } "checked" => { Ok("{ attr: \"checked\", value: \"\" }".to_string()) } "readonly" => { Ok("{ attr: \"readonly\", value: \"\" }".to_string()) } "required" => { Ok("{ attr: \"required\", value: \"\" }".to_string()) } "autofocus" => { Ok("{ attr: \"autofocus\", value: \"\" }".to_string()) } // Custom attribute "attr" => { let name = self.emit_expr(&args[0])?; let value = self.emit_expr(&args[1])?; Ok(format!("{{ attr: {}, value: {} }}", name, value)) } // Data attribute "data" => { let name = self.emit_expr(&args[0])?; let value = self.emit_expr(&args[1])?; Ok(format!("{{ attr: \"data-\" + {}, value: {} }}", name, value)) } // Event handlers "onClick" => { let handler = self.emit_expr(&args[0])?; Ok(format!("{{ event: \"click\", handler: {} }}", handler)) } "onInput" => { let handler = self.emit_expr(&args[0])?; Ok(format!("{{ event: \"input\", handler: {} }}", handler)) } "onChange" => { let handler = self.emit_expr(&args[0])?; Ok(format!("{{ event: \"change\", handler: {} }}", handler)) } "onSubmit" => { let handler = self.emit_expr(&args[0])?; Ok(format!("{{ event: \"submit\", handler: {} }}", handler)) } "onKeyDown" => { let handler = self.emit_expr(&args[0])?; Ok(format!("{{ event: \"keydown\", handler: {} }}", handler)) } "onKeyUp" => { let handler = self.emit_expr(&args[0])?; Ok(format!("{{ event: \"keyup\", handler: {} }}", handler)) } "onKeyPress" => { let handler = self.emit_expr(&args[0])?; Ok(format!("{{ event: \"keypress\", handler: {} }}", handler)) } "onFocus" => { let handler = self.emit_expr(&args[0])?; Ok(format!("{{ event: \"focus\", handler: {} }}", handler)) } "onBlur" => { let handler = self.emit_expr(&args[0])?; Ok(format!("{{ event: \"blur\", handler: {} }}", handler)) } "onMouseOver" => { let handler = self.emit_expr(&args[0])?; Ok(format!("{{ event: \"mouseover\", handler: {} }}", handler)) } "onMouseOut" => { let handler = self.emit_expr(&args[0])?; Ok(format!("{{ event: \"mouseout\", handler: {} }}", handler)) } "onMouseDown" => { let handler = self.emit_expr(&args[0])?; Ok(format!("{{ event: \"mousedown\", handler: {} }}", handler)) } "onMouseUp" => { let handler = self.emit_expr(&args[0])?; Ok(format!("{{ event: \"mouseup\", handler: {} }}", handler)) } // Custom event "on" => { let event = self.emit_expr(&args[0])?; let handler = self.emit_expr(&args[1])?; Ok(format!("{{ event: {}, handler: {} }}", event, handler)) } // Render HTML to string "render" => { let node = self.emit_expr(&args[0])?; Ok(format!("Lux.renderHtml({})", node)) } // Render HTML to DOM (returns Element) "renderToDom" => { let node = self.emit_expr(&args[0])?; Ok(format!("Lux.renderToDom({})", node)) } _ => Err(JsGenError { message: format!("Unknown Html operation: {}", operation), span: None, }), } } /// Emit a literal value fn emit_literal(&self, lit: &Literal) -> Result { match &lit.kind { LiteralKind::Int(n) => Ok(n.to_string()), LiteralKind::Float(f) => { if f.is_infinite() { if *f > 0.0 { Ok("Infinity".to_string()) } else { Ok("-Infinity".to_string()) } } else if f.is_nan() { Ok("NaN".to_string()) } else { Ok(format!("{}", f)) } } LiteralKind::String(s) => { // Escape special characters for JS string let escaped = s .replace('\\', "\\\\") .replace('"', "\\\"") .replace('\n', "\\n") .replace('\r', "\\r") .replace('\t', "\\t"); Ok(format!("\"{}\"", escaped)) } LiteralKind::Char(c) => { // Chars become single-character strings in JS Ok(format!("\"{}\"", c)) } LiteralKind::Bool(b) => Ok(if *b { "true" } else { "false" }.to_string()), LiteralKind::Unit => Ok("undefined".to_string()), } } /// Check if an expression produces a string fn is_string_expr(&self, expr: &Expr) -> bool { match expr { Expr::Literal(lit) => matches!(lit.kind, LiteralKind::String(_)), Expr::Call { func, .. } => { if let Expr::Var(ident) = func.as_ref() { ident.name == "toString" } else { false } } Expr::BinaryOp { op, left, right, .. } => { matches!(op, BinaryOp::Add | BinaryOp::Concat) && (self.is_string_expr(left) || self.is_string_expr(right)) } _ => false, } } /// Check if an expression contains statements that emit code before the result fn expr_has_statements(&self, expr: &Expr) -> bool { match expr { Expr::Block { statements, .. } => !statements.is_empty(), Expr::Let { .. } => true, Expr::Match { .. } => true, Expr::If { then_branch, else_branch, .. } => { self.expr_has_statements(then_branch) || self.expr_has_statements(else_branch) } _ => false, } } /// Check if an expression calls the main function fn expr_calls_main(&self, expr: &Expr) -> bool { match expr { Expr::Call { func, .. } => { if let Expr::Var(ident) = func.as_ref() { if ident.name == "main" { return true; } } false } Expr::Run { expr, .. } => self.expr_calls_main(expr), Expr::Let { value, body, .. } => { self.expr_calls_main(value) || self.expr_calls_main(body) } Expr::Block { statements, result, .. } => { for stmt in statements { if let Statement::Let { value, .. } = stmt { if self.expr_calls_main(value) { return true; } } } self.expr_calls_main(result) } _ => false, } } /// Mangle a Lux name to a valid JavaScript name fn mangle_name(&self, name: &str) -> String { // Extern functions use their JS name directly (no mangling) if let Some(js_name) = self.extern_fns.get(name) { return js_name.clone(); } format!("{}_lux", name) } /// Escape JavaScript reserved keywords fn escape_js_keyword(&self, name: &str) -> String { match name { "break" | "case" | "catch" | "continue" | "debugger" | "default" | "delete" | "do" | "else" | "finally" | "for" | "function" | "if" | "in" | "instanceof" | "new" | "return" | "switch" | "this" | "throw" | "try" | "typeof" | "var" | "void" | "while" | "with" | "class" | "const" | "enum" | "export" | "extends" | "import" | "super" | "implements" | "interface" | "let" | "package" | "private" | "protected" | "public" | "static" | "yield" | "await" | "async" => { format!("{}_", name) } _ => name.to_string(), } } /// Generate a fresh unique name fn fresh_name(&mut self) -> usize { let n = self.name_counter; self.name_counter += 1; n } /// Write a line with current indentation fn writeln(&mut self, s: &str) { for _ in 0..self.indent { self.output.push_str(" "); } self.output.push_str(s); self.output.push('\n'); } } impl Default for JsBackend { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; use crate::parser::Parser; use std::process::Command; #[test] fn test_literal_int() { let backend = JsBackend::new(); let lit = Literal { kind: LiteralKind::Int(42), span: Span::default(), }; assert_eq!(backend.emit_literal(&lit).unwrap(), "42"); } #[test] fn test_literal_string() { let backend = JsBackend::new(); let lit = Literal { kind: LiteralKind::String("hello\nworld".to_string()), span: Span::default(), }; assert_eq!(backend.emit_literal(&lit).unwrap(), "\"hello\\nworld\""); } #[test] fn test_literal_bool() { let backend = JsBackend::new(); let lit = Literal { kind: LiteralKind::Bool(true), span: Span::default(), }; assert_eq!(backend.emit_literal(&lit).unwrap(), "true"); } #[test] fn test_mangle_name() { let backend = JsBackend::new(); assert_eq!(backend.mangle_name("foo"), "foo_lux"); assert_eq!(backend.mangle_name("main"), "main_lux"); } #[test] fn test_escape_keywords() { let backend = JsBackend::new(); assert_eq!(backend.escape_js_keyword("class"), "class_"); assert_eq!(backend.escape_js_keyword("foo"), "foo"); } /// Helper to compile Lux source to JS and run in Node.js fn compile_and_run(source: &str) -> Result { let program = Parser::parse_source(source).map_err(|e| format!("Parse error: {}", e))?; let mut backend = JsBackend::new(); let js_code = backend .generate(&program) .map_err(|e| format!("Codegen error: {}", e))?; let output = Command::new("node") .arg("-e") .arg(&js_code) .output() .map_err(|e| format!("Node.js error: {}", e))?; if !output.status.success() { return Err(format!( "Node.js execution failed:\n{}", String::from_utf8_lossy(&output.stderr) )); } Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) } #[test] fn test_js_factorial() { let source = r#" fn factorial(n: Int): Int = if n <= 1 then 1 else n * factorial(n - 1) fn main(): Unit with {Console} = Console.print("Result: " + toString(factorial(5))) "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Result: 120"); } #[test] fn test_js_fibonacci() { let source = r#" fn fib(n: Int): Int = if n <= 1 then n else fib(n - 1) + fib(n - 2) fn main(): Unit with {Console} = Console.print("fib(10) = " + toString(fib(10))) "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "fib(10) = 55"); } #[test] fn test_js_adt_and_pattern_matching() { let source = r#" type Tree = | Leaf(Int) | Node(Tree, Tree) fn sumTree(tree: Tree): Int = match tree { Leaf(n) => n, Node(left, right) => sumTree(left) + sumTree(right) } let tree = Node(Node(Leaf(1), Leaf(2)), Leaf(3)) fn main(): Unit with {Console} = Console.print("Sum: " + toString(sumTree(tree))) "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Sum: 6"); } #[test] fn test_js_option_type() { let source = r#" fn safeDivide(a: Int, b: Int): Option = if b == 0 then None else Some(a / b) fn showResult(opt: Option): String = match opt { Some(n) => "Got: " + toString(n), None => "None" } fn main(): Unit with {Console} = { Console.print(showResult(safeDivide(10, 2))) Console.print(showResult(safeDivide(10, 0))) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Got: 5\nNone"); } #[test] fn test_js_closures() { let source = r#" fn makeAdder(x: Int): fn(Int): Int = fn(y: Int): Int => x + y let add5 = makeAdder(5) fn main(): Unit with {Console} = Console.print("5 + 10 = " + toString(add5(10))) "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "5 + 10 = 15"); } #[test] fn test_js_higher_order_functions() { let source = r#" fn apply(f: fn(Int): Int, x: Int): Int = f(x) fn double(x: Int): Int = x * 2 fn main(): Unit with {Console} = Console.print("double(21) = " + toString(apply(double, 21))) "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "double(21) = 42"); } #[test] fn test_js_list_operations() { let source = r#" let nums = [1, 2, 3, 4, 5] let doubled = List.map(nums, fn(x: Int): Int => x * 2) let sum = List.foldl(doubled, 0, fn(acc: Int, x: Int): Int => acc + x) fn main(): Unit with {Console} = Console.print("Sum of doubled: " + toString(sum)) "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Sum of doubled: 30"); } #[test] fn test_js_pipe_operator() { let source = r#" fn double(x: Int): Int = x * 2 fn addOne(x: Int): Int = x + 1 let result = 5 |> double |> addOne fn main(): Unit with {Console} = Console.print("Result: " + toString(result)) "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Result: 11"); } #[test] fn test_js_records() { let source = r#" let point = { x: 10, y: 20 } let sum = point.x + point.y fn main(): Unit with {Console} = Console.print("Sum: " + toString(sum)) "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Sum: 30"); } #[test] fn test_js_string_concatenation() { let source = r#" let name = "World" let greeting = "Hello, " + name + "!" fn main(): Unit with {Console} = Console.print(greeting) "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Hello, World!"); } #[test] fn test_js_random_int() { let source = r#" fn main(): Unit with {Console, Random} = { let n = Random.int(1, 10) // Just verify it runs without error and produces a number Console.print("Random: " + toString(n)) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert!(output.starts_with("Random: ")); } #[test] fn test_js_random_bool() { let source = r#" fn main(): Unit with {Console, Random} = { let b = Random.bool() Console.print("Bool: " + toString(b)) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert!(output == "Bool: true" || output == "Bool: false"); } #[test] fn test_js_random_float() { let source = r#" fn main(): Unit with {Console, Random} = { let f = Random.float() // Float should be between 0 and 1 Console.print("Float generated") } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Float generated"); } #[test] fn test_js_time_now() { let source = r#" fn main(): Unit with {Console, Time} = { let t = Time.now() // Should be a positive timestamp if t > 0 then Console.print("Time works") else Console.print("Time failed") } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Time works"); } // ======================================================================== // String Module Tests // ======================================================================== #[test] fn test_js_string_length() { let source = r#" fn main(): Unit with {Console} = { let s = "hello" Console.print("Length: " + toString(String.length(s))) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Length: 5"); } #[test] fn test_js_string_concat() { let source = r#" fn main(): Unit with {Console} = { let result = String.concat("Hello, ", "World!") Console.print(result) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Hello, World!"); } #[test] fn test_js_string_slice() { let source = r#" fn main(): Unit with {Console} = { let s = "Hello, World!" Console.print(String.slice(s, 0, 5)) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Hello"); } #[test] fn test_js_string_char_at() { let source = r#" fn main(): Unit with {Console} = { let s = "abc" Console.print(String.charAt(s, 1)) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "b"); } #[test] fn test_js_string_index_of() { let source = r#" fn main(): Unit with {Console} = { let s = "hello world" Console.print("Index: " + toString(String.indexOf(s, "world"))) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Index: 6"); } #[test] fn test_js_string_contains() { let source = r#" fn main(): Unit with {Console} = { let s = "hello world" if String.contains(s, "world") then Console.print("Found") else Console.print("Not found") } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Found"); } #[test] fn test_js_string_starts_ends_with() { let source = r#" fn main(): Unit with {Console} = { let s = "hello world" let starts = String.startsWith(s, "hello") let ends = String.endsWith(s, "world") if starts && ends then Console.print("Both") else Console.print("Neither") } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Both"); } #[test] fn test_js_string_case() { let source = r#" fn main(): Unit with {Console} = { let s = "Hello World" Console.print(String.toUpperCase(s)) Console.print(String.toLowerCase(s)) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "HELLO WORLD\nhello world"); } #[test] fn test_js_string_trim() { let source = r#" fn main(): Unit with {Console} = { let s = " hello " Console.print("[" + String.trim(s) + "]") } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "[hello]"); } #[test] fn test_js_string_split_join() { let source = r#" fn main(): Unit with {Console} = { let s = "a,b,c" let parts = String.split(s, ",") let rejoined = String.join(parts, "-") Console.print(rejoined) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "a-b-c"); } #[test] fn test_js_string_replace() { let source = r#" fn main(): Unit with {Console} = { let s = "hello world" Console.print(String.replace(s, "world", "lux")) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "hello lux"); } #[test] fn test_js_string_replace_all() { let source = r#" fn main(): Unit with {Console} = { let s = "a-b-c-d" Console.print(String.replaceAll(s, "-", "_")) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "a_b_c_d"); } #[test] fn test_js_string_repeat() { let source = r#" fn main(): Unit with {Console} = { Console.print(String.repeat("ab", 3)) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "ababab"); } #[test] fn test_js_string_is_empty() { let source = r#" fn main(): Unit with {Console} = { if String.isEmpty("") then Console.print("empty") else Console.print("not empty") } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "empty"); } #[test] fn test_js_string_chars() { let source = r#" fn main(): Unit with {Console} = { let chars = String.chars("abc") Console.print("Len: " + toString(List.length(chars))) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Len: 3"); } #[test] fn test_js_string_lines() { let source = r#" fn main(): Unit with {Console} = { let text = "line1 line2 line3" let lines = String.lines(text) Console.print("Lines: " + toString(List.length(lines))) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Lines: 3"); } // ======================================================================== // Option Module Tests // ======================================================================== #[test] fn test_js_option_is_some_none() { let source = r#" fn main(): Unit with {Console} = { let some = Some(42) let none: Option = None if Option.isSome(some) then Console.print("some is Some") else Console.print("some is None") if Option.isNone(none) then Console.print("none is None") else Console.print("none is Some") } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "some is Some\nnone is None"); } #[test] fn test_js_option_unwrap() { let source = r#" fn main(): Unit with {Console} = { let opt = Some(42) Console.print("Value: " + toString(Option.unwrap(opt))) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Value: 42"); } #[test] fn test_js_option_unwrap_or() { let source = r#" fn main(): Unit with {Console} = { let some = Some(42) let none: Option = None Console.print("Some: " + toString(Option.unwrapOr(some, 0))) Console.print("None: " + toString(Option.unwrapOr(none, 99))) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Some: 42\nNone: 99"); } #[test] fn test_js_option_map() { let source = r#" fn main(): Unit with {Console} = { let opt = Some(21) let doubled = Option.map(opt, fn(x: Int): Int => x * 2) match doubled { Some(n) => Console.print("Doubled: " + toString(n)), None => Console.print("None") } } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Doubled: 42"); } #[test] fn test_js_option_map_none() { let source = r#" fn main(): Unit with {Console} = { let opt: Option = None let doubled = Option.map(opt, fn(x: Int): Int => x * 2) match doubled { Some(n) => Console.print("Doubled: " + toString(n)), None => Console.print("Still None") } } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Still None"); } #[test] fn test_js_option_flat_map() { let source = r#" fn safeDivide(a: Int, b: Int): Option = if b == 0 then None else Some(a / b) fn main(): Unit with {Console} = { let opt = Some(100) let result = Option.flatMap(opt, fn(x: Int): Option => safeDivide(x, 5)) match result { Some(n) => Console.print("Result: " + toString(n)), None => Console.print("None") } } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Result: 20"); } #[test] fn test_js_option_get_or_else() { let source = r#" fn main(): Unit with {Console} = { let none: Option = None Console.print("Value: " + toString(Option.getOrElse(none, 42))) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Value: 42"); } // ======================================================================== // Math Module Tests // ======================================================================== #[test] fn test_js_math_basic() { let source = r#" fn main(): Unit with {Console} = { Console.print("abs: " + toString(Math.abs(-5))) Console.print("floor: " + toString(Math.floor(3.7))) Console.print("ceil: " + toString(Math.ceil(3.2))) Console.print("round: " + toString(Math.round(3.5))) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "abs: 5\nfloor: 3\nceil: 4\nround: 4"); } #[test] fn test_js_math_power_roots() { let source = r#" fn main(): Unit with {Console} = { Console.print("pow: " + toString(Math.pow(2, 3))) Console.print("sqrt: " + toString(Math.sqrt(16))) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "pow: 8\nsqrt: 4"); } #[test] fn test_js_math_min_max() { let source = r#" fn main(): Unit with {Console} = { Console.print("min: " + toString(Math.min(3, 7))) Console.print("max: " + toString(Math.max(3, 7))) Console.print("clamp: " + toString(Math.clamp(15, 0, 10))) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "min: 3\nmax: 7\nclamp: 10"); } #[test] fn test_js_math_trig() { let source = r#" fn main(): Unit with {Console} = { let zero = Math.sin(0) let one = Math.cos(0) if zero == 0 && one == 1 then Console.print("Trig works") else Console.print("Trig failed") } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Trig works"); } #[test] fn test_js_math_constants() { let source = r#" fn main(): Unit with {Console} = { let pi = Math.pi() let e = Math.e() if pi > 3.14 && pi < 3.15 then Console.print("pi ok") else Console.print("pi bad") if e > 2.71 && e < 2.72 then Console.print("e ok") else Console.print("e bad") } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "pi ok\ne ok"); } #[test] fn test_js_math_checks() { let source = r#" fn main(): Unit with {Console} = { if Math.isFinite(42) then Console.print("42 is finite") else Console.print("42 is not finite") if Math.isNaN(0 / 0) then Console.print("0/0 is NaN") else Console.print("0/0 is not NaN") } "#; let output = compile_and_run(source).expect("Should compile and run"); // Note: 0/0 in JS is NaN, but in integer division it may differ assert!(output.contains("42 is finite")); } #[test] fn test_js_math_log() { let source = r#" fn main(): Unit with {Console} = { let ln_e = Math.ln(Math.e()) if ln_e > 0.99 && ln_e < 1.01 then Console.print("ln(e) = 1") else Console.print("ln(e) != 1") } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "ln(e) = 1"); } // ======================================================================== // Result Module Tests // ======================================================================== #[test] fn test_js_result_is_ok_err() { let source = r#" fn main(): Unit with {Console} = { let ok: Result = Ok(42) let err: Result = Err("error") if Result.isOk(ok) then Console.print("ok is Ok") else Console.print("ok is Err") if Result.isErr(err) then Console.print("err is Err") else Console.print("err is Ok") } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "ok is Ok\nerr is Err"); } #[test] fn test_js_result_unwrap() { let source = r#" fn main(): Unit with {Console} = { let ok: Result = Ok(42) Console.print("Value: " + toString(Result.unwrap(ok))) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Value: 42"); } #[test] fn test_js_result_unwrap_or() { let source = r#" fn main(): Unit with {Console} = { let ok: Result = Ok(42) let err: Result = Err("error") Console.print("Ok: " + toString(Result.unwrapOr(ok, 0))) Console.print("Err: " + toString(Result.unwrapOr(err, 99))) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Ok: 42\nErr: 99"); } #[test] fn test_js_result_map() { let source = r#" fn main(): Unit with {Console} = { let ok: Result = Ok(21) let doubled = Result.map(ok, fn(x: Int): Int => x * 2) match doubled { Ok(n) => Console.print("Doubled: " + toString(n)), Err(e) => Console.print("Error: " + e) } } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Doubled: 42"); } #[test] fn test_js_result_map_err() { let source = r#" fn main(): Unit with {Console} = { let err: Result = Err("oops") let mapped = Result.mapErr(err, fn(e: String): String => "Error: " + e) match mapped { Ok(n) => Console.print("Got: " + toString(n)), Err(e) => Console.print(e) } } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Error: oops"); } #[test] fn test_js_result_flat_map() { let source = r#" fn safeDivide(a: Int, b: Int): Result = if b == 0 then Err("division by zero") else Ok(a / b) fn main(): Unit with {Console} = { let ok: Result = Ok(100) let result = Result.flatMap(ok, fn(x: Int): Result => safeDivide(x, 5)) match result { Ok(n) => Console.print("Result: " + toString(n)), Err(e) => Console.print("Error: " + e) } } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Result: 20"); } #[test] fn test_js_result_to_option() { let source = r#" fn main(): Unit with {Console} = { let ok: Result = Ok(42) let err: Result = Err("error") match Result.toOption(ok) { Some(n) => Console.print("Ok->Some: " + toString(n)), None => Console.print("Ok->None") } match Result.toOption(err) { Some(n) => Console.print("Err->Some: " + toString(n)), None => Console.print("Err->None") } } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Ok->Some: 42\nErr->None"); } // ======================================================================== // JSON Module Tests // ======================================================================== #[test] fn test_js_json_stringify() { let source = r#" fn main(): Unit with {Console} = { let obj = { name: "Alice", age: 30 } Console.print(Json.stringify(obj)) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert!(output.contains("Alice") && output.contains("30")); } #[test] fn test_js_json_parse() { let source = r#" fn main(): Unit with {Console} = { let json = "[1, 2, 3]" match Json.parse(json) { Ok(arr) => Console.print("Parsed: " + toString(List.length(arr))), Err(e) => Console.print("Parse error: " + e) } } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Parsed: 3"); } #[test] fn test_js_json_parse_error() { let source = r#" fn main(): Unit with {Console} = { let bad = "[invalid" match Json.parse(bad) { Ok(obj) => Console.print("Parsed ok"), Err(e) => Console.print("Got error") } } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Got error"); } #[test] fn test_js_json_get() { let source = r#" fn main(): Unit with {Console} = { let obj = { name: "Bob", age: 25 } match Json.get(obj, "name") { Some(v) => Console.print("Name: " + v), None => Console.print("Not found") } } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Name: Bob"); } #[test] fn test_js_json_get_or() { let source = r#" fn main(): Unit with {Console} = { let obj = { name: "Bob" } let age = Json.getOr(obj, "age", 0) Console.print("Age: " + toString(age)) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Age: 0"); } #[test] fn test_js_json_has_key() { let source = r#" fn main(): Unit with {Console} = { let obj = { name: "Bob" } if Json.hasKey(obj, "name") then Console.print("has name") else Console.print("no name") if Json.hasKey(obj, "age") then Console.print("has age") else Console.print("no age") } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "has name\nno age"); } #[test] fn test_js_json_keys_values() { let source = r#" fn main(): Unit with {Console} = { let obj = { a: 1, b: 2 } let keys = Json.keys(obj) Console.print("Keys: " + toString(List.length(keys))) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Keys: 2"); } #[test] fn test_js_json_type_of() { let source = r#" fn main(): Unit with {Console} = { let arr = [1, 2, 3] let obj = { x: 1 } let num = 42 Console.print("array: " + Json.typeOf(arr)) Console.print("object: " + Json.typeOf(obj)) Console.print("number: " + Json.typeOf(num)) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "array: array\nobject: object\nnumber: number"); } #[test] fn test_js_json_pretty_print() { let source = r#" fn main(): Unit with {Console} = { let obj = { name: "Test" } let pretty = Json.prettyPrint(obj) // Pretty print should have newlines if String.contains(pretty, "\n") then Console.print("has newlines") else Console.print("no newlines") } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "has newlines"); } #[test] fn test_js_json_is_array() { let source = r#" fn main(): Unit with {Console} = { let arr = [1, 2, 3] let obj = { x: 1 } if Json.isArray(arr) then Console.print("arr is array") else Console.print("arr is not array") if Json.isArray(obj) then Console.print("obj is array") else Console.print("obj is not array") } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "arr is array\nobj is not array"); } // ======================================================================== // DOM Effect Tests (Node.js stubs - returns None/null for missing document) // ======================================================================== #[test] fn test_js_dom_query_selector_stub() { // In Node.js, document is undefined, so querySelector returns None let source = r##" fn main(): Unit with {Console, Dom} = { match Dom.querySelector("#test") { Some(el) => Console.print("Found element"), None => Console.print("No element (expected in Node)") } } "##; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "No element (expected in Node)"); } #[test] fn test_js_dom_get_element_by_id_stub() { let source = r#" fn main(): Unit with {Console, Dom} = { match Dom.getElementById("app") { Some(el) => Console.print("Found"), None => Console.print("Not found (Node.js)") } } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Not found (Node.js)"); } #[test] fn test_js_dom_create_element_stub() { // createElement returns null in Node.js but should still be callable let source = r#" fn main(): Unit with {Console, Dom} = { let el = Dom.createElement("div") Console.print("createElement called") } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "createElement called"); } #[test] fn test_js_dom_window_size_stub() { let source = r#" fn main(): Unit with {Console, Dom} = { let size = Dom.getWindowSize() // In Node.js, returns { width: 0, height: 0 } Console.print("Width: " + toString(size.width)) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Width: 0"); } #[test] fn test_js_dom_get_body_stub() { let source = r#" fn main(): Unit with {Console, Dom} = { let body = Dom.getBody() // In Node.js, returns null Console.print("getBody called") } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "getBody called"); } #[test] fn test_js_dom_query_selector_all_stub() { let source = r#" fn main(): Unit with {Console, Dom} = { let elements = Dom.querySelectorAll("div") Console.print("Found: " + toString(List.length(elements))) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Found: 0"); } // ======================================================================== // Html Module Tests // ======================================================================== #[test] fn test_js_html_text() { let source = r#" fn main(): Unit with {Console} = { let node = Html.text("Hello, World!") Console.print("Tag: " + node.tag) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Tag: text"); } #[test] fn test_js_html_element() { let source = r#" fn main(): Unit with {Console} = { let node = Html.div([], []) Console.print("Tag: " + node.tag) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Tag: div"); } #[test] fn test_js_html_element_with_children() { let source = r#" fn main(): Unit with {Console} = { let child1 = Html.text("Hello") let child2 = Html.span([], [Html.text("World")]) let node = Html.div([], [child1, child2]) Console.print("Children: " + toString(List.length(node.children))) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Children: 2"); } #[test] fn test_js_html_attributes() { let source = r#" fn main(): Unit with {Console} = { let node = Html.div([Html.class("container"), Html.id("main")], []) Console.print("Attrs: " + toString(List.length(node.attrs))) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Attrs: 2"); } #[test] fn test_js_html_render_text() { let source = r#" fn main(): Unit with {Console} = { let node = Html.text("Hello") let html = Html.render(node) Console.print(html) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Hello"); } #[test] fn test_js_html_render_element() { let source = r#" fn main(): Unit with {Console} = { let node = Html.div([Html.class("test")], [Html.text("Content")]) let html = Html.render(node) Console.print(html) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert!(output.contains("")); } #[test] fn test_js_html_render_nested() { let source = r#" fn main(): Unit with {Console} = { let li1 = Html.li([], [Html.text("Item 1")]) let li2 = Html.li([], [Html.text("Item 2")]) let node = Html.ul([], [li1, li2]) let html = Html.render(node) if String.contains(html, "
    ") && String.contains(html, "
  • ") then Console.print("Nested OK") else Console.print("Nested failed") } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Nested OK"); } #[test] fn test_js_html_escape() { let source = r#" fn main(): Unit with {Console} = { let node = Html.text("") let html = Html.render(node) if String.contains(html, "<") then Console.print("Escaped") else Console.print("Not escaped") } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Escaped"); } #[test] fn test_js_html_fragment() { let source = r#" fn main(): Unit with {Console} = { let t1 = Html.text("Hello ") let t2 = Html.text("World") let node = Html.fragment([t1, t2]) let html = Html.render(node) Console.print(html) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Hello World"); } #[test] fn test_js_html_self_closing() { let source = r#" fn main(): Unit with {Console} = { let br = Html.br() let html = Html.render(br) if String.contains(html, "/>") then Console.print("Self-closing") else Console.print("Not self-closing") } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Self-closing"); } #[test] fn test_js_html_button() { let source = r#" fn main(): Unit with {Console} = { let attrs = [Html.class("btn")] let children = [Html.text("Click me")] let node = Html.button(attrs, children) let html = Html.render(node) if String.contains(html, " n } fn view(model: Model): String = "
    " + toString(getCount(model)) + "
    " fn main(): Unit with {Console} = { let m = init() let html = view(m) Console.print(html) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "
    0
    "); } #[test] fn test_js_html_with_event_handler() { // Test that event handlers are properly generated let source = r#" fn doNothing(): Unit = () fn main(): Unit with {Console} = { let btn = Html.button([Html.onClick(doNothing)], [Html.text("Click")]) Console.print("Tag: " + btn.tag) } "#; let output = compile_and_run(source).expect("Should compile and run"); assert_eq!(output, "Tag: button"); } }