Files
lux/src/codegen/js_backend.rs
Brandon Lucas 542255780d feat: add tuple index access, multiline args, and effect unification fix
- Tuple index: `pair.0`, `pair.1` syntax across parser, typechecker,
  interpreter, C/JS backends, formatter, linter, and symbol table
- Multi-line function args: allow newlines inside argument lists
- Fix effect unification for callback parameters (empty expected
  effects means "no constraint", not "must be pure")

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 16:21:48 -05:00

3824 lines
134 KiB
Rust

//! 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<T> | `Array` |
//! | Option<T> | `{tag: "Some", value: T} \| {tag: "None"}` |
//! | Result<T, E> | `{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<Span>,
}
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<String>,
/// Counter for generating unique names
name_counter: usize,
/// Mapping from variant names to their parent type name
variant_to_type: HashMap<String, String>,
/// Mapping from (type_name, variant_name) to field types
variant_field_types: HashMap<(String, String), Vec<String>>,
/// Functions that use effects (have handlers parameter)
effectful_functions: HashSet<String>,
/// Whether we're currently inside an effectful function
has_handlers: bool,
/// Variable substitutions for let binding
var_substitutions: HashMap<String, String>,
}
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(),
}
}
/// Generate JavaScript code from a Lux program
pub fn generate(&mut self, program: &Program) -> Result<String, JsGenError> {
self.output.clear();
// Emit runtime helpers
self.emit_runtime();
// 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)?;
}
_ => {}
}
}
// 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())
}
/// Emit the minimal Lux runtime
fn emit_runtime(&mut self) {
self.writeln("// Lux Runtime");
self.writeln("const Lux = {");
self.indent += 1;
// Option helpers
self.writeln("Some: (value) => ({ tag: \"Some\", value }),");
self.writeln("None: () => ({ tag: \"None\" }),");
self.writeln("");
// Result helpers
self.writeln("Ok: (value) => ({ tag: \"Ok\", value }),");
self.writeln("Err: (error) => ({ tag: \"Err\", error }),");
self.writeln("");
// List helpers
self.writeln("Cons: (head, tail) => [head, ...tail],");
self.writeln("Nil: () => [],");
self.writeln("");
// Default handlers for effects
self.writeln("defaultHandlers: {");
self.indent += 1;
// Console effect
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("},");
// Random effect
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("},");
// Time effect
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("},");
// Http effect (browser/Node compatible)
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("},");
// Dom effect (browser only - stubs for Node.js)
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("},");
// Element creation
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("},");
// DOM manipulation
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("},");
// Content
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("},");
// Attributes
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("},");
// Classes
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("},");
// Styles
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("},");
// Form elements
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("},");
// Events
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("},");
// Focus
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("},");
// Document
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("},");
// Window
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("},");
// Scroll
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("},");
// Dimensions
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("}");
self.indent -= 1;
self.writeln("},");
// HTML rendering helpers
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}</${tag}>`;");
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, '&amp;')");
self.writeln(".replace(/</g, '&lt;')");
self.writeln(".replace(/>/g, '&gt;')");
self.writeln(".replace(/\"/g, '&quot;')");
self.writeln(".replace(/'/g, '&#39;');");
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("},");
// TEA (The Elm Architecture) runtime
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("},");
// Simple app (for string-based views like the counter example)
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("},");
// Diff and patch (basic implementation for view_deps optimization)
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("}");
self.indent -= 1;
self.writeln("};");
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<String> = 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<String> = (0..types.len())
.map(|i| format!("field{}", i))
.collect();
let fields: Vec<String> = 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<String> =
fields.iter().map(|f| f.name.name.clone()).collect();
let field_inits: Vec<String> =
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<String> = 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;
// Clear var substitutions for this function
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.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;
// Check if this is a run expression (often results in undefined)
// We still want to execute it for its side effects
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<String, JsGenError> {
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 {
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) {
if self.is_string_expr(left) || self.is_string_expr(right) {
return Ok(format!("({} + {})", 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::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)?;
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
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);
}
}
}
// 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));
}
}
let arg_strs: Result<Vec<_>, _> = 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: 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: Html module operations (not an effect)
if effect.name == "Html" {
return self.emit_html_operation(&operation.name, args);
}
let arg_strs: Result<Vec<_>, _> = 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<String> =
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 handler state
let prev_has_handlers = self.has_handlers;
self.has_handlers = !effects.is_empty();
let body_code = self.emit_expr(body)?;
self.has_handlers = prev_has_handlers;
Ok(format!(
"(function({}) {{ return {}; }})",
all_params.join(", "),
body_code
))
}
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)?;
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 { fields, .. } => {
let field_strs: Result<Vec<_>, _> = fields
.iter()
.map(|(name, expr)| {
let val = self.emit_expr(expr)?;
Ok(format!("{}: {}", name.name, val))
})
.collect();
Ok(format!("{{ {} }}", field_strs?.join(", ")))
}
Expr::Tuple { elements, .. } => {
let elem_strs: Result<Vec<_>, _> =
elements.iter().map(|e| self.emit_expr(e)).collect();
Ok(format!("[{}]", elem_strs?.join(", ")))
}
Expr::List { elements, .. } => {
let elem_strs: Result<Vec<_>, _> =
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<Vec<_>, _> = 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<String, JsGenError> {
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<String, JsGenError> {
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<Vec<(String, String)>, 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<String, JsGenError> {
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
))
}
_ => 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<String, JsGenError> {
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<String, JsGenError> {
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<String, JsGenError> {
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<String, JsGenError> {
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<String, JsGenError> {
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 Html module operations for type-safe HTML construction
fn emit_html_operation(
&mut self,
operation: &str,
args: &[Expr],
) -> Result<String, JsGenError> {
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<String, JsGenError> {
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)
&& (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 {
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<String, String> {
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<Int> =
if b == 0 then None
else Some(a / b)
fn showResult(opt: Option<Int>): 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<Int> = 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<Int> = 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<Int> = 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<Int> =
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<Int> => 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<Int> = 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<Int, String> = Ok(42)
let err: Result<Int, String> = 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<Int, String> = 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<Int, String> = Ok(42)
let err: Result<Int, String> = 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<Int, String> = 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<Int, String> = 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<Int, String> =
if b == 0 then Err("division by zero")
else Ok(a / b)
fn main(): Unit with {Console} = {
let ok: Result<Int, String> = Ok(100)
let result = Result.flatMap(ok, fn(x: Int): Result<Int, String> => 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<Int, String> = Ok(42)
let err: Result<Int, String> = 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("<div"));
assert!(output.contains("class=\"test\""));
assert!(output.contains("Content"));
assert!(output.contains("</div>"));
}
#[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, "<ul>") && String.contains(html, "<li>") 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("<script>alert('xss')</script>")
let html = Html.render(node)
if String.contains(html, "&lt;") 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, "<button") && String.contains(html, "Click me") then Console.print("Button OK")
else Console.print("Button failed")
}
"#;
let output = compile_and_run(source).expect("Should compile and run");
assert_eq!(output, "Button OK");
}
#[test]
fn test_js_html_input() {
let source = r#"
fn main(): Unit with {Console} = {
let attrs = [Html.placeholder("Enter name")]
let node = Html.input(attrs, [])
let html = Html.render(node)
if String.contains(html, "input") && String.contains(html, "placeholder") then Console.print("Input OK")
else Console.print("Input failed")
}
"#;
let output = compile_and_run(source).expect("Should compile and run");
assert_eq!(output, "Input OK");
}
// ========================================================================
// TEA Runtime Tests (verify generated code includes runtime)
// ========================================================================
#[test]
fn test_js_runtime_generated() {
// Test that the Lux runtime is properly generated
use crate::parser::Parser;
let source = r#"
fn main(): Unit with {Console} = Console.print("Hello")
"#;
let program = Parser::parse_source(source).expect("Should parse");
let mut backend = JsBackend::new();
let js_code = backend.generate(&program).expect("Should generate");
// Check that Lux runtime includes key functions
assert!(js_code.contains("const Lux = {"), "Lux object should be defined");
assert!(js_code.contains("Some:"), "Option Some should be defined");
assert!(js_code.contains("None:"), "Option None should be defined");
assert!(js_code.contains("renderHtml:"), "renderHtml should be defined");
assert!(js_code.contains("renderToDom:"), "renderToDom should be defined");
assert!(js_code.contains("escapeHtml:"), "escapeHtml should be defined");
assert!(js_code.contains("app:"), "TEA app should be defined");
assert!(js_code.contains("simpleApp:"), "simpleApp should be defined");
assert!(js_code.contains("hasChanged:"), "hasChanged should be defined");
}
#[test]
fn test_js_runtime_default_handlers() {
// Test that default handlers are properly generated
use crate::parser::Parser;
let source = r#"
fn main(): Unit with {Console} = Console.print("Hello")
"#;
let program = Parser::parse_source(source).expect("Should parse");
let mut backend = JsBackend::new();
let js_code = backend.generate(&program).expect("Should generate");
// Check that default handlers include all effects
assert!(js_code.contains("Console:"), "Console handler should exist");
assert!(js_code.contains("Random:"), "Random handler should exist");
assert!(js_code.contains("Time:"), "Time handler should exist");
assert!(js_code.contains("Http:"), "Http handler should exist");
assert!(js_code.contains("Dom:"), "Dom handler should exist");
}
#[test]
fn test_js_tea_simple_model_view_update() {
// Test a simple TEA-style program that compiles correctly
let source = r#"
type Model = | Counter(Int)
fn init(): Model = Counter(0)
fn getCount(m: Model): Int = match m { Counter(n) => n }
fn view(model: Model): String =
"<div>" + toString(getCount(model)) + "</div>"
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, "<div>0</div>");
}
#[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");
}
}