Adds `extern fn` syntax for declaring external JavaScript functions: extern fn getElementById(id: String): Element extern fn getContext(el: Element, kind: String): CanvasCtx = "getContext" pub extern fn alert(msg: String): Unit Changes across 11 files: - Lexer: `extern` keyword - AST: `ExternFnDecl` struct + `Declaration::ExternFn` variant - Parser: parse `extern fn` with optional `= "jsName"` override - Typechecker: register extern fn type signatures - Interpreter: ExternFn value with clear error on call - JS backend: emit extern fn calls using JS name (no _lux suffix) - C backend: silently skips extern fns - Formatter, linter, modules, symbol_table: handle new variant Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
4203 lines
150 KiB
Rust
4203 lines
150 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>,
|
|
/// Effects actually used in the program (for tree-shaking runtime)
|
|
used_effects: HashSet<String>,
|
|
/// Extern function names mapped to their JS names
|
|
extern_fns: 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(),
|
|
used_effects: HashSet::new(),
|
|
extern_fns: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
/// Generate JavaScript code from a Lux program
|
|
pub fn generate(&mut self, program: &Program) -> Result<String, JsGenError> {
|
|
self.output.clear();
|
|
|
|
// First pass: collect all function names, types, and effects
|
|
for decl in &program.declarations {
|
|
match decl {
|
|
Declaration::Function(f) => {
|
|
self.functions.insert(f.name.name.clone());
|
|
if !f.effects.is_empty() {
|
|
self.effectful_functions.insert(f.name.name.clone());
|
|
}
|
|
}
|
|
Declaration::Type(t) => {
|
|
self.collect_type(t)?;
|
|
}
|
|
Declaration::ExternFn(ext) => {
|
|
let js_name = ext
|
|
.js_name
|
|
.clone()
|
|
.unwrap_or_else(|| ext.name.name.clone());
|
|
self.extern_fns.insert(ext.name.name.clone(), js_name);
|
|
self.functions.insert(ext.name.name.clone());
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
// Collect used effects for tree-shaking
|
|
self.collect_used_effects(program);
|
|
|
|
// Emit runtime helpers (tree-shaken based on used effects)
|
|
self.emit_runtime();
|
|
|
|
// Emit type constructors
|
|
for decl in &program.declarations {
|
|
if let Declaration::Type(t) = decl {
|
|
self.emit_type_constructors(t)?;
|
|
}
|
|
}
|
|
|
|
// Emit functions
|
|
for decl in &program.declarations {
|
|
if let Declaration::Function(f) = decl {
|
|
self.emit_function(f)?;
|
|
}
|
|
}
|
|
|
|
// Check if any top-level let calls main (to avoid double invocation)
|
|
let has_main_call = program.declarations.iter().any(|decl| {
|
|
if let Declaration::Let(l) = decl {
|
|
self.expr_calls_main(&l.value)
|
|
} else {
|
|
false
|
|
}
|
|
});
|
|
|
|
// Emit top-level let bindings and expressions
|
|
if program.declarations.iter().any(|d| matches!(d, Declaration::Let(_))) {
|
|
self.writeln("// Top-level bindings");
|
|
for decl in &program.declarations {
|
|
if let Declaration::Let(l) = decl {
|
|
self.emit_top_level_let(l)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Emit main function call if it exists and not already called
|
|
if self.functions.contains("main") && !has_main_call {
|
|
self.writeln("");
|
|
self.writeln("// Entry point");
|
|
if self.effectful_functions.contains("main") {
|
|
self.writeln("main_lux(Lux.defaultHandlers);");
|
|
} else {
|
|
self.writeln("main_lux();");
|
|
}
|
|
}
|
|
|
|
Ok(self.output.clone())
|
|
}
|
|
|
|
/// Collect all effects used in the program for runtime tree-shaking
|
|
fn collect_used_effects(&mut self, program: &Program) {
|
|
for decl in &program.declarations {
|
|
match decl {
|
|
Declaration::Function(f) => {
|
|
for effect in &f.effects {
|
|
self.used_effects.insert(effect.name.clone());
|
|
}
|
|
self.collect_effects_from_expr(&f.body);
|
|
}
|
|
Declaration::Let(l) => {
|
|
self.collect_effects_from_expr(&l.value);
|
|
}
|
|
Declaration::Handler(h) => {
|
|
self.used_effects.insert(h.effect.name.clone());
|
|
for imp in &h.implementations {
|
|
self.collect_effects_from_expr(&imp.body);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Recursively collect effect names from an expression
|
|
fn collect_effects_from_expr(&mut self, expr: &Expr) {
|
|
match expr {
|
|
Expr::EffectOp { effect, args, .. } => {
|
|
self.used_effects.insert(effect.name.clone());
|
|
for arg in args {
|
|
self.collect_effects_from_expr(arg);
|
|
}
|
|
}
|
|
Expr::Run { expr, handlers, .. } => {
|
|
self.collect_effects_from_expr(expr);
|
|
for (effect, handler) in handlers {
|
|
self.used_effects.insert(effect.name.clone());
|
|
self.collect_effects_from_expr(handler);
|
|
}
|
|
}
|
|
Expr::Call { func, args, .. } => {
|
|
self.collect_effects_from_expr(func);
|
|
for arg in args {
|
|
self.collect_effects_from_expr(arg);
|
|
}
|
|
}
|
|
Expr::Lambda { body, effects, .. } => {
|
|
for effect in effects {
|
|
self.used_effects.insert(effect.name.clone());
|
|
}
|
|
self.collect_effects_from_expr(body);
|
|
}
|
|
Expr::Let { value, body, .. } => {
|
|
self.collect_effects_from_expr(value);
|
|
self.collect_effects_from_expr(body);
|
|
}
|
|
Expr::If { condition, then_branch, else_branch, .. } => {
|
|
self.collect_effects_from_expr(condition);
|
|
self.collect_effects_from_expr(then_branch);
|
|
self.collect_effects_from_expr(else_branch);
|
|
}
|
|
Expr::Match { scrutinee, arms, .. } => {
|
|
self.collect_effects_from_expr(scrutinee);
|
|
for arm in arms {
|
|
self.collect_effects_from_expr(&arm.body);
|
|
if let Some(guard) = &arm.guard {
|
|
self.collect_effects_from_expr(guard);
|
|
}
|
|
}
|
|
}
|
|
Expr::Block { statements, result, .. } => {
|
|
for stmt in statements {
|
|
match stmt {
|
|
Statement::Expr(e) => self.collect_effects_from_expr(e),
|
|
Statement::Let { value, .. } => self.collect_effects_from_expr(value),
|
|
}
|
|
}
|
|
self.collect_effects_from_expr(result);
|
|
}
|
|
Expr::BinaryOp { left, right, .. } => {
|
|
self.collect_effects_from_expr(left);
|
|
self.collect_effects_from_expr(right);
|
|
}
|
|
Expr::UnaryOp { operand, .. } => {
|
|
self.collect_effects_from_expr(operand);
|
|
}
|
|
Expr::Field { object, .. } => {
|
|
self.collect_effects_from_expr(object);
|
|
}
|
|
Expr::TupleIndex { object, .. } => {
|
|
self.collect_effects_from_expr(object);
|
|
}
|
|
Expr::Record { spread, fields, .. } => {
|
|
if let Some(s) = spread {
|
|
self.collect_effects_from_expr(s);
|
|
}
|
|
for (_, expr) in fields {
|
|
self.collect_effects_from_expr(expr);
|
|
}
|
|
}
|
|
Expr::Tuple { elements, .. } | Expr::List { elements, .. } => {
|
|
for el in elements {
|
|
self.collect_effects_from_expr(el);
|
|
}
|
|
}
|
|
Expr::Resume { value, .. } => {
|
|
self.collect_effects_from_expr(value);
|
|
}
|
|
Expr::Literal(_) | Expr::Var(_) => {}
|
|
}
|
|
}
|
|
|
|
/// Emit the Lux runtime, tree-shaken based on used effects
|
|
fn emit_runtime(&mut self) {
|
|
let uses_console = self.used_effects.contains("Console");
|
|
let uses_random = self.used_effects.contains("Random");
|
|
let uses_time = self.used_effects.contains("Time");
|
|
let uses_http = self.used_effects.contains("Http");
|
|
let uses_dom = self.used_effects.contains("Dom");
|
|
let uses_html = self.used_effects.contains("Html") || uses_dom;
|
|
|
|
self.writeln("// Lux Runtime");
|
|
self.writeln("const Lux = {");
|
|
self.indent += 1;
|
|
|
|
// Core helpers — always emitted
|
|
self.writeln("Some: (value) => ({ tag: \"Some\", value }),");
|
|
self.writeln("None: () => ({ tag: \"None\" }),");
|
|
self.writeln("");
|
|
self.writeln("Ok: (value) => ({ tag: \"Ok\", value }),");
|
|
self.writeln("Err: (error) => ({ tag: \"Err\", error }),");
|
|
self.writeln("");
|
|
self.writeln("Cons: (head, tail) => [head, ...tail],");
|
|
self.writeln("Nil: () => [],");
|
|
self.writeln("");
|
|
|
|
// Default handlers — only include effects that are used
|
|
self.writeln("defaultHandlers: {");
|
|
self.indent += 1;
|
|
|
|
if uses_console {
|
|
self.emit_console_handler();
|
|
}
|
|
if uses_random {
|
|
self.emit_random_handler();
|
|
}
|
|
if uses_time {
|
|
self.emit_time_handler();
|
|
}
|
|
if uses_http {
|
|
self.emit_http_handler();
|
|
}
|
|
if uses_dom {
|
|
self.emit_dom_handler();
|
|
}
|
|
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
// HTML rendering — only if Html or Dom effects are used
|
|
if uses_html {
|
|
self.emit_html_helpers();
|
|
}
|
|
|
|
// TEA runtime — only if Dom is used
|
|
if uses_dom {
|
|
self.emit_tea_runtime();
|
|
}
|
|
|
|
self.indent -= 1;
|
|
self.writeln("};");
|
|
self.writeln("");
|
|
}
|
|
|
|
fn emit_console_handler(&mut self) {
|
|
self.writeln("Console: {");
|
|
self.indent += 1;
|
|
self.writeln("print: (msg) => console.log(msg),");
|
|
self.writeln("readLine: () => {");
|
|
self.indent += 1;
|
|
self.writeln("if (typeof require !== 'undefined') {");
|
|
self.indent += 1;
|
|
self.writeln("const readline = require('readline');");
|
|
self.writeln("const rl = readline.createInterface({ input: process.stdin, output: process.stdout });");
|
|
self.writeln("return new Promise(resolve => rl.question('', answer => { rl.close(); resolve(answer); }));");
|
|
self.indent -= 1;
|
|
self.writeln("}");
|
|
self.writeln("return prompt('') || '';");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
self.writeln("readInt: () => parseInt(Lux.defaultHandlers.Console.readLine(), 10)");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
}
|
|
|
|
fn emit_random_handler(&mut self) {
|
|
self.writeln("Random: {");
|
|
self.indent += 1;
|
|
self.writeln("int: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min,");
|
|
self.writeln("bool: () => Math.random() < 0.5,");
|
|
self.writeln("float: () => Math.random()");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
}
|
|
|
|
fn emit_time_handler(&mut self) {
|
|
self.writeln("Time: {");
|
|
self.indent += 1;
|
|
self.writeln("now: () => Date.now(),");
|
|
self.writeln("sleep: (ms) => new Promise(resolve => setTimeout(resolve, ms))");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
}
|
|
|
|
fn emit_http_handler(&mut self) {
|
|
self.writeln("Http: {");
|
|
self.indent += 1;
|
|
self.writeln("get: async (url) => {");
|
|
self.indent += 1;
|
|
self.writeln("try {");
|
|
self.indent += 1;
|
|
self.writeln("const response = await fetch(url);");
|
|
self.writeln("const body = await response.text();");
|
|
self.writeln("const headers = [];");
|
|
self.writeln("response.headers.forEach((v, k) => headers.push([k, v]));");
|
|
self.writeln("return Lux.Ok({ status: response.status, body, headers });");
|
|
self.indent -= 1;
|
|
self.writeln("} catch (e) {");
|
|
self.indent += 1;
|
|
self.writeln("return Lux.Err(e.message);");
|
|
self.indent -= 1;
|
|
self.writeln("}");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
self.writeln("post: async (url, body) => {");
|
|
self.indent += 1;
|
|
self.writeln("try {");
|
|
self.indent += 1;
|
|
self.writeln("const response = await fetch(url, { method: 'POST', body });");
|
|
self.writeln("const respBody = await response.text();");
|
|
self.writeln("const headers = [];");
|
|
self.writeln("response.headers.forEach((v, k) => headers.push([k, v]));");
|
|
self.writeln("return Lux.Ok({ status: response.status, body: respBody, headers });");
|
|
self.indent -= 1;
|
|
self.writeln("} catch (e) {");
|
|
self.indent += 1;
|
|
self.writeln("return Lux.Err(e.message);");
|
|
self.indent -= 1;
|
|
self.writeln("}");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
self.writeln("postJson: async (url, json) => {");
|
|
self.indent += 1;
|
|
self.writeln("try {");
|
|
self.indent += 1;
|
|
self.writeln("const response = await fetch(url, {");
|
|
self.indent += 1;
|
|
self.writeln("method: 'POST',");
|
|
self.writeln("headers: { 'Content-Type': 'application/json' },");
|
|
self.writeln("body: JSON.stringify(json)");
|
|
self.indent -= 1;
|
|
self.writeln("});");
|
|
self.writeln("const body = await response.text();");
|
|
self.writeln("const headers = [];");
|
|
self.writeln("response.headers.forEach((v, k) => headers.push([k, v]));");
|
|
self.writeln("return Lux.Ok({ status: response.status, body, headers });");
|
|
self.indent -= 1;
|
|
self.writeln("} catch (e) {");
|
|
self.indent += 1;
|
|
self.writeln("return Lux.Err(e.message);");
|
|
self.indent -= 1;
|
|
self.writeln("}");
|
|
self.indent -= 1;
|
|
self.writeln("}");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
}
|
|
|
|
fn emit_dom_handler(&mut self) {
|
|
self.writeln("Dom: {");
|
|
self.indent += 1;
|
|
|
|
// Query selectors
|
|
self.writeln("querySelector: (selector) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (typeof document === 'undefined') return Lux.None();");
|
|
self.writeln("const el = document.querySelector(selector);");
|
|
self.writeln("return el ? Lux.Some(el) : Lux.None();");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("querySelectorAll: (selector) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (typeof document === 'undefined') return [];");
|
|
self.writeln("return Array.from(document.querySelectorAll(selector));");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("getElementById: (id) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (typeof document === 'undefined') return Lux.None();");
|
|
self.writeln("const el = document.getElementById(id);");
|
|
self.writeln("return el ? Lux.Some(el) : Lux.None();");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("createElement: (tag) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (typeof document === 'undefined') return null;");
|
|
self.writeln("return document.createElement(tag);");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("createTextNode: (text) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (typeof document === 'undefined') return null;");
|
|
self.writeln("return document.createTextNode(text);");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("appendChild: (parent, child) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (parent && child) parent.appendChild(child);");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("removeChild: (parent, child) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (parent && child) parent.removeChild(child);");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("replaceChild: (parent, newChild, oldChild) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (parent && newChild && oldChild) parent.replaceChild(newChild, oldChild);");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("insertBefore: (parent, newNode, refNode) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (parent && newNode) parent.insertBefore(newNode, refNode);");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("setTextContent: (el, text) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (el) el.textContent = text;");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("getTextContent: (el) => {");
|
|
self.indent += 1;
|
|
self.writeln("return el ? el.textContent : '';");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("setInnerHtml: (el, html) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (el) el.innerHTML = html;");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("getInnerHtml: (el) => {");
|
|
self.indent += 1;
|
|
self.writeln("return el ? el.innerHTML : '';");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("setAttribute: (el, name, value) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (el) el.setAttribute(name, value);");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("getAttribute: (el, name) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (!el) return Lux.None();");
|
|
self.writeln("const val = el.getAttribute(name);");
|
|
self.writeln("return val !== null ? Lux.Some(val) : Lux.None();");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("removeAttribute: (el, name) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (el) el.removeAttribute(name);");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("hasAttribute: (el, name) => {");
|
|
self.indent += 1;
|
|
self.writeln("return el ? el.hasAttribute(name) : false;");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("addClass: (el, className) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (el) el.classList.add(className);");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("removeClass: (el, className) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (el) el.classList.remove(className);");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("toggleClass: (el, className) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (el) el.classList.toggle(className);");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("hasClass: (el, className) => {");
|
|
self.indent += 1;
|
|
self.writeln("return el ? el.classList.contains(className) : false;");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("setStyle: (el, property, value) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (el) el.style[property] = value;");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("getStyle: (el, property) => {");
|
|
self.indent += 1;
|
|
self.writeln("return el ? el.style[property] : '';");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("getValue: (el) => {");
|
|
self.indent += 1;
|
|
self.writeln("return el ? el.value : '';");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("setValue: (el, value) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (el) el.value = value;");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("isChecked: (el) => {");
|
|
self.indent += 1;
|
|
self.writeln("return el ? el.checked : false;");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("setChecked: (el, checked) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (el) el.checked = checked;");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("addEventListener: (el, event, handler) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (el) el.addEventListener(event, handler);");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("removeEventListener: (el, event, handler) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (el) el.removeEventListener(event, handler);");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("focus: (el) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (el && el.focus) el.focus();");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("blur: (el) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (el && el.blur) el.blur();");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("getBody: () => {");
|
|
self.indent += 1;
|
|
self.writeln("if (typeof document === 'undefined') return null;");
|
|
self.writeln("return document.body;");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("getHead: () => {");
|
|
self.indent += 1;
|
|
self.writeln("if (typeof document === 'undefined') return null;");
|
|
self.writeln("return document.head;");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("getWindow: () => {");
|
|
self.indent += 1;
|
|
self.writeln("if (typeof window === 'undefined') return null;");
|
|
self.writeln("return window;");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("alert: (msg) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (typeof alert !== 'undefined') alert(msg);");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("confirm: (msg) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (typeof confirm !== 'undefined') return confirm(msg);");
|
|
self.writeln("return false;");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("prompt: (msg, defaultValue) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (typeof prompt !== 'undefined') {");
|
|
self.indent += 1;
|
|
self.writeln("const result = prompt(msg, defaultValue || '');");
|
|
self.writeln("return result !== null ? Lux.Some(result) : Lux.None();");
|
|
self.indent -= 1;
|
|
self.writeln("}");
|
|
self.writeln("return Lux.None();");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("scrollTo: (x, y) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (typeof window !== 'undefined') window.scrollTo(x, y);");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("scrollIntoView: (el) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (el && el.scrollIntoView) el.scrollIntoView();");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("getBoundingClientRect: (el) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (!el) return { top: 0, left: 0, width: 0, height: 0, right: 0, bottom: 0 };");
|
|
self.writeln("const rect = el.getBoundingClientRect();");
|
|
self.writeln("return { top: rect.top, left: rect.left, width: rect.width, height: rect.height, right: rect.right, bottom: rect.bottom };");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("getWindowSize: () => {");
|
|
self.indent += 1;
|
|
self.writeln("if (typeof window === 'undefined') return { width: 0, height: 0 };");
|
|
self.writeln("return { width: window.innerWidth, height: window.innerHeight };");
|
|
self.indent -= 1;
|
|
self.writeln("}");
|
|
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
}
|
|
|
|
fn emit_html_helpers(&mut self) {
|
|
self.writeln("");
|
|
self.writeln("// HTML rendering");
|
|
self.writeln("renderHtml: (node) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (!node) return '';");
|
|
self.writeln("if (typeof node === 'string') return Lux.escapeHtml(node);");
|
|
self.writeln("if (node.tag === 'text') return Lux.escapeHtml(node.content);");
|
|
self.writeln("if (node.tag === 'raw') return node.html;");
|
|
self.writeln("if (node.tag === 'empty') return '';");
|
|
self.writeln("if (node.tag === 'fragment') return node.children.map(Lux.renderHtml).join('');");
|
|
self.writeln("");
|
|
self.writeln("const tag = node.tag;");
|
|
self.writeln("const attrs = (node.attrs || []).filter(a => a.attr).map(a => ` ${a.attr}=\"${Lux.escapeHtml(a.value)}\"`).join('');");
|
|
self.writeln("const events = (node.attrs || []).filter(a => a.event);");
|
|
self.writeln("");
|
|
self.writeln("// Self-closing tags");
|
|
self.writeln("if (['br', 'hr', 'img', 'input', 'meta', 'link'].includes(tag)) {");
|
|
self.indent += 1;
|
|
self.writeln("return `<${tag}${attrs} />`;");
|
|
self.indent -= 1;
|
|
self.writeln("}");
|
|
self.writeln("");
|
|
self.writeln("const children = (node.children || []).map(Lux.renderHtml).join('');");
|
|
self.writeln("return `<${tag}${attrs}>${children}</${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, '&')");
|
|
self.writeln(".replace(/</g, '<')");
|
|
self.writeln(".replace(/>/g, '>')");
|
|
self.writeln(".replace(/\"/g, '"')");
|
|
self.writeln(".replace(/'/g, ''');");
|
|
self.indent -= 1;
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("");
|
|
self.writeln("// Render HTML to DOM element");
|
|
self.writeln("renderToDom: (node) => {");
|
|
self.indent += 1;
|
|
self.writeln("if (typeof document === 'undefined') return null;");
|
|
self.writeln("if (!node) return null;");
|
|
self.writeln("if (typeof node === 'string') return document.createTextNode(node);");
|
|
self.writeln("if (node.tag === 'text') return document.createTextNode(node.content);");
|
|
self.writeln("if (node.tag === 'raw') {");
|
|
self.indent += 1;
|
|
self.writeln("const wrapper = document.createElement('div');");
|
|
self.writeln("wrapper.innerHTML = node.html;");
|
|
self.writeln("return wrapper.firstChild;");
|
|
self.indent -= 1;
|
|
self.writeln("}");
|
|
self.writeln("if (node.tag === 'empty') return document.createTextNode('');");
|
|
self.writeln("if (node.tag === 'fragment') {");
|
|
self.indent += 1;
|
|
self.writeln("const frag = document.createDocumentFragment();");
|
|
self.writeln("for (const child of node.children) {");
|
|
self.indent += 1;
|
|
self.writeln("const el = Lux.renderToDom(child);");
|
|
self.writeln("if (el) frag.appendChild(el);");
|
|
self.indent -= 1;
|
|
self.writeln("}");
|
|
self.writeln("return frag;");
|
|
self.indent -= 1;
|
|
self.writeln("}");
|
|
self.writeln("");
|
|
self.writeln("const el = document.createElement(node.tag);");
|
|
self.writeln("");
|
|
self.writeln("// Set attributes");
|
|
self.writeln("for (const attr of (node.attrs || [])) {");
|
|
self.indent += 1;
|
|
self.writeln("if (attr.attr) {");
|
|
self.indent += 1;
|
|
self.writeln("el.setAttribute(attr.attr, attr.value);");
|
|
self.indent -= 1;
|
|
self.writeln("}");
|
|
self.writeln("if (attr.event && attr.handler) {");
|
|
self.indent += 1;
|
|
self.writeln("el.addEventListener(attr.event, attr.handler);");
|
|
self.indent -= 1;
|
|
self.writeln("}");
|
|
self.indent -= 1;
|
|
self.writeln("}");
|
|
self.writeln("");
|
|
self.writeln("// Append children");
|
|
self.writeln("for (const child of (node.children || [])) {");
|
|
self.indent += 1;
|
|
self.writeln("const childEl = Lux.renderToDom(child);");
|
|
self.writeln("if (childEl) el.appendChild(childEl);");
|
|
self.indent -= 1;
|
|
self.writeln("}");
|
|
self.writeln("");
|
|
self.writeln("return el;");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
}
|
|
|
|
fn emit_tea_runtime(&mut self) {
|
|
self.writeln("");
|
|
self.writeln("// The Elm Architecture (TEA) runtime");
|
|
self.writeln("app: (config) => {");
|
|
self.indent += 1;
|
|
self.writeln("const { init, update, view, root } = config;");
|
|
self.writeln("let model = init;");
|
|
self.writeln("let rootEl = typeof root === 'string' ? document.querySelector(root) : root;");
|
|
self.writeln("let currentVdom = null;");
|
|
self.writeln("let currentDom = null;");
|
|
self.writeln("");
|
|
self.writeln("const dispatch = (msg) => {");
|
|
self.indent += 1;
|
|
self.writeln("model = update(model, msg);");
|
|
self.writeln("render();");
|
|
self.indent -= 1;
|
|
self.writeln("};");
|
|
self.writeln("");
|
|
self.writeln("const render = () => {");
|
|
self.indent += 1;
|
|
self.writeln("const newVdom = view(model, dispatch);");
|
|
self.writeln("const newDom = Lux.renderToDom(newVdom);");
|
|
self.writeln("if (currentDom) {");
|
|
self.indent += 1;
|
|
self.writeln("rootEl.replaceChild(newDom, currentDom);");
|
|
self.indent -= 1;
|
|
self.writeln("} else {");
|
|
self.indent += 1;
|
|
self.writeln("rootEl.innerHTML = '';");
|
|
self.writeln("rootEl.appendChild(newDom);");
|
|
self.indent -= 1;
|
|
self.writeln("}");
|
|
self.writeln("currentVdom = newVdom;");
|
|
self.writeln("currentDom = newDom;");
|
|
self.indent -= 1;
|
|
self.writeln("};");
|
|
self.writeln("");
|
|
self.writeln("// Initial render");
|
|
self.writeln("if (rootEl) render();");
|
|
self.writeln("");
|
|
self.writeln("return { dispatch, getModel: () => model };");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("");
|
|
self.writeln("// Simple TEA app (string-based view)");
|
|
self.writeln("simpleApp: (config) => {");
|
|
self.indent += 1;
|
|
self.writeln("const { init, update, view, root } = config;");
|
|
self.writeln("let model = init;");
|
|
self.writeln("let rootEl = typeof root === 'string' ? document.querySelector(root) : root;");
|
|
self.writeln("");
|
|
self.writeln("const dispatch = (msg) => {");
|
|
self.indent += 1;
|
|
self.writeln("model = update(model, msg);");
|
|
self.writeln("render();");
|
|
self.indent -= 1;
|
|
self.writeln("};");
|
|
self.writeln("");
|
|
self.writeln("// Make dispatch globally available for onclick handlers");
|
|
self.writeln("window.dispatch = dispatch;");
|
|
self.writeln("");
|
|
self.writeln("const render = () => {");
|
|
self.indent += 1;
|
|
self.writeln("if (rootEl) rootEl.innerHTML = view(model);");
|
|
self.indent -= 1;
|
|
self.writeln("};");
|
|
self.writeln("");
|
|
self.writeln("if (rootEl) render();");
|
|
self.writeln("return { dispatch, getModel: () => model };");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
|
|
self.writeln("");
|
|
self.writeln("// Basic diff - checks if model fields changed");
|
|
self.writeln("hasChanged: (oldModel, newModel, ...paths) => {");
|
|
self.indent += 1;
|
|
self.writeln("for (const path of paths) {");
|
|
self.indent += 1;
|
|
self.writeln("const parts = path.split('.');");
|
|
self.writeln("let oldVal = oldModel, newVal = newModel;");
|
|
self.writeln("for (const part of parts) {");
|
|
self.indent += 1;
|
|
self.writeln("oldVal = oldVal?.[part];");
|
|
self.writeln("newVal = newVal?.[part];");
|
|
self.indent -= 1;
|
|
self.writeln("}");
|
|
self.writeln("if (oldVal !== newVal) return true;");
|
|
self.indent -= 1;
|
|
self.writeln("}");
|
|
self.writeln("return false;");
|
|
self.indent -= 1;
|
|
self.writeln("},");
|
|
}
|
|
|
|
/// Collect type information from a type declaration
|
|
fn collect_type(&mut self, decl: &TypeDecl) -> Result<(), JsGenError> {
|
|
if let TypeDef::Enum(variants) = &decl.definition {
|
|
for variant in variants {
|
|
self.variant_to_type
|
|
.insert(variant.name.name.clone(), decl.name.name.clone());
|
|
|
|
// Store field types
|
|
let field_types: Vec<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;
|
|
|
|
// Save and clear var substitutions for this function scope
|
|
let saved_substitutions = self.var_substitutions.clone();
|
|
self.var_substitutions.clear();
|
|
|
|
// Emit function body
|
|
let body_code = self.emit_expr(&func.body)?;
|
|
self.writeln(&format!("return {};", body_code));
|
|
|
|
self.has_handlers = prev_has_handlers;
|
|
self.var_substitutions = saved_substitutions;
|
|
|
|
self.indent -= 1;
|
|
self.writeln("}");
|
|
self.writeln("");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Emit a top-level let binding
|
|
fn emit_top_level_let(&mut self, let_decl: &LetDecl) -> Result<(), JsGenError> {
|
|
let val = self.emit_expr(&let_decl.value)?;
|
|
let var_name = &let_decl.name.name;
|
|
|
|
if var_name == "_" {
|
|
// Wildcard binding: just execute for side effects
|
|
self.writeln(&format!("{};", val));
|
|
} else {
|
|
self.writeln(&format!("const {} = {};", var_name, val));
|
|
|
|
// Register the variable for future use
|
|
self.var_substitutions
|
|
.insert(var_name.clone(), var_name.clone());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Emit an expression and return the JavaScript code
|
|
fn emit_expr(&mut self, expr: &Expr) -> Result<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 | BinaryOp::Concat) {
|
|
if self.is_string_expr(left) || self.is_string_expr(right) {
|
|
return Ok(format!("({} + {})", l, r));
|
|
}
|
|
}
|
|
|
|
// ++ on lists: use .concat()
|
|
if matches!(op, BinaryOp::Concat) {
|
|
return Ok(format!("{}.concat({})", l, r));
|
|
}
|
|
|
|
let op_str = match op {
|
|
BinaryOp::Add => "+",
|
|
BinaryOp::Sub => "-",
|
|
BinaryOp::Mul => "*",
|
|
BinaryOp::Div => "/",
|
|
BinaryOp::Mod => "%",
|
|
BinaryOp::Eq => "===",
|
|
BinaryOp::Ne => "!==",
|
|
BinaryOp::Lt => "<",
|
|
BinaryOp::Le => "<=",
|
|
BinaryOp::Gt => ">",
|
|
BinaryOp::Ge => ">=",
|
|
BinaryOp::And => "&&",
|
|
BinaryOp::Or => "||",
|
|
BinaryOp::Concat => unreachable!("handled above"),
|
|
BinaryOp::Pipe => {
|
|
// Pipe operator: x |> f becomes f(x)
|
|
return Ok(format!("{}({})", r, l));
|
|
}
|
|
};
|
|
|
|
Ok(format!("({} {} {})", l, op_str, r))
|
|
}
|
|
|
|
Expr::UnaryOp { op, operand, .. } => {
|
|
let val = self.emit_expr(operand)?;
|
|
let op_str = match op {
|
|
UnaryOp::Neg => "-",
|
|
UnaryOp::Not => "!",
|
|
};
|
|
Ok(format!("({}{})", op_str, val))
|
|
}
|
|
|
|
Expr::If {
|
|
condition,
|
|
then_branch,
|
|
else_branch,
|
|
..
|
|
} => {
|
|
// Check if branches contain statements that need if-else instead of ternary
|
|
let needs_block = self.expr_has_statements(then_branch)
|
|
|| self.expr_has_statements(else_branch);
|
|
|
|
if needs_block {
|
|
// Use if-else statement for branches with statements
|
|
let result_var = format!("_if_result_{}", self.fresh_name());
|
|
let cond = self.emit_expr(condition)?;
|
|
|
|
self.writeln(&format!("let {};", result_var));
|
|
self.writeln(&format!("if ({}) {{", cond));
|
|
self.indent += 1;
|
|
let then_val = self.emit_expr(then_branch)?;
|
|
self.writeln(&format!("{} = {};", result_var, then_val));
|
|
self.indent -= 1;
|
|
self.writeln("} else {");
|
|
self.indent += 1;
|
|
let else_val = self.emit_expr(else_branch)?;
|
|
self.writeln(&format!("{} = {};", result_var, else_val));
|
|
self.indent -= 1;
|
|
self.writeln("}");
|
|
|
|
Ok(result_var)
|
|
} else {
|
|
// Simple ternary for pure expressions
|
|
let cond = self.emit_expr(condition)?;
|
|
let then_val = self.emit_expr(then_branch)?;
|
|
let else_val = self.emit_expr(else_branch)?;
|
|
Ok(format!("({} ? {} : {})", cond, then_val, else_val))
|
|
}
|
|
}
|
|
|
|
Expr::Let {
|
|
name, value, body, ..
|
|
} => {
|
|
let val = self.emit_expr(value)?;
|
|
|
|
if name.name == "_" {
|
|
// Wildcard binding: just execute for side effects
|
|
self.writeln(&format!("{};", val));
|
|
} else {
|
|
let var_name = format!("{}_{}", name.name, self.fresh_name());
|
|
|
|
self.writeln(&format!("const {} = {};", var_name, val));
|
|
|
|
// Add substitution
|
|
self.var_substitutions
|
|
.insert(name.name.clone(), var_name.clone());
|
|
}
|
|
|
|
let body_result = self.emit_expr(body)?;
|
|
|
|
// Remove substitution
|
|
if name.name != "_" {
|
|
self.var_substitutions.remove(&name.name);
|
|
}
|
|
|
|
Ok(body_result)
|
|
}
|
|
|
|
Expr::Call { func, args, .. } => {
|
|
// Check for List module calls
|
|
if let Expr::Field { object, field, .. } = func.as_ref() {
|
|
if let Expr::Var(module_name) = object.as_ref() {
|
|
if module_name.name == "List" {
|
|
return self.emit_list_operation(&field.name, args);
|
|
}
|
|
if module_name.name == "Map" {
|
|
return self.emit_map_operation(&field.name, args);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Int/Float module operations
|
|
if let Expr::Field { object, field, .. } = func.as_ref() {
|
|
if let Expr::Var(module_name) = object.as_ref() {
|
|
if module_name.name == "Int" {
|
|
let arg = self.emit_expr(&args[0])?;
|
|
match field.name.as_str() {
|
|
"toFloat" => return Ok(arg),
|
|
"toString" => return Ok(format!("String({})", arg)),
|
|
_ => {}
|
|
}
|
|
}
|
|
if module_name.name == "Float" {
|
|
let arg = self.emit_expr(&args[0])?;
|
|
match field.name.as_str() {
|
|
"toInt" => return Ok(format!("Math.trunc({})", arg)),
|
|
"toString" => return Ok(format!("String({})", arg)),
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for built-in functions
|
|
if let Expr::Var(ident) = func.as_ref() {
|
|
if ident.name == "toString" {
|
|
let arg = self.emit_expr(&args[0])?;
|
|
return Ok(format!("String({})", arg));
|
|
}
|
|
if ident.name == "print" {
|
|
let arg = self.emit_expr(&args[0])?;
|
|
return Ok(format!("console.log({})", arg));
|
|
}
|
|
}
|
|
|
|
let arg_strs: Result<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: Int module operations
|
|
if effect.name == "Int" {
|
|
let arg = self.emit_expr(&args[0])?;
|
|
match operation.name.as_str() {
|
|
"toFloat" => return Ok(arg), // JS numbers are already floats
|
|
"toString" => return Ok(format!("String({})", arg)),
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
// Special case: Float module operations
|
|
if effect.name == "Float" {
|
|
let arg = self.emit_expr(&args[0])?;
|
|
match operation.name.as_str() {
|
|
"toInt" => return Ok(format!("Math.trunc({})", arg)),
|
|
"toString" => return Ok(format!("String({})", arg)),
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
// Special case: Result module operations (not an effect)
|
|
if effect.name == "Result" {
|
|
return self.emit_result_operation(&operation.name, args);
|
|
}
|
|
|
|
// Special case: Json module operations (not an effect)
|
|
if effect.name == "Json" {
|
|
return self.emit_json_operation(&operation.name, args);
|
|
}
|
|
|
|
// Special case: Map module operations (not an effect)
|
|
if effect.name == "Map" {
|
|
return self.emit_map_operation(&operation.name, args);
|
|
}
|
|
|
|
// Special case: 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 state
|
|
let prev_has_handlers = self.has_handlers;
|
|
let saved_substitutions = self.var_substitutions.clone();
|
|
self.has_handlers = !effects.is_empty();
|
|
|
|
// Register lambda params as themselves (override any outer substitutions)
|
|
for p in &all_params {
|
|
self.var_substitutions.insert(p.clone(), p.clone());
|
|
}
|
|
|
|
// Capture any statements emitted during body evaluation
|
|
let output_start = self.output.len();
|
|
let prev_indent = self.indent;
|
|
self.indent += 1;
|
|
|
|
let body_code = self.emit_expr(body)?;
|
|
self.writeln(&format!("return {};", body_code));
|
|
|
|
// Extract body statements and restore output
|
|
let body_statements = self.output[output_start..].to_string();
|
|
self.output.truncate(output_start);
|
|
self.indent = prev_indent;
|
|
|
|
// Restore state
|
|
self.has_handlers = prev_has_handlers;
|
|
self.var_substitutions = saved_substitutions;
|
|
|
|
let indent_str = " ".repeat(self.indent);
|
|
Ok(format!(
|
|
"(function({}) {{\n{}{}}})",
|
|
all_params.join(", "),
|
|
body_statements,
|
|
indent_str,
|
|
))
|
|
}
|
|
|
|
Expr::Match {
|
|
scrutinee, arms, ..
|
|
} => self.emit_match(scrutinee, arms),
|
|
|
|
Expr::Block {
|
|
statements, result, ..
|
|
} => {
|
|
// Emit each statement
|
|
for stmt in statements {
|
|
match stmt {
|
|
Statement::Expr(expr) => {
|
|
let code = self.emit_expr(expr)?;
|
|
self.writeln(&format!("{};", code));
|
|
}
|
|
Statement::Let { name, value, .. } => {
|
|
let val = self.emit_expr(value)?;
|
|
if name.name == "_" {
|
|
self.writeln(&format!("{};", val));
|
|
} else {
|
|
let var_name =
|
|
format!("{}_{}", name.name, self.fresh_name());
|
|
self.writeln(&format!("const {} = {};", var_name, val));
|
|
self.var_substitutions
|
|
.insert(name.name.clone(), var_name.clone());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Emit result
|
|
self.emit_expr(result)
|
|
}
|
|
|
|
Expr::Record {
|
|
spread, fields, ..
|
|
} => {
|
|
let mut parts = Vec::new();
|
|
if let Some(spread_expr) = spread {
|
|
let spread_code = self.emit_expr(spread_expr)?;
|
|
parts.push(format!("...{}", spread_code));
|
|
}
|
|
for (name, expr) in fields {
|
|
let val = self.emit_expr(expr)?;
|
|
parts.push(format!("{}: {}", name.name, val));
|
|
}
|
|
Ok(format!("{{ {} }}", parts.join(", ")))
|
|
}
|
|
|
|
Expr::Tuple { elements, .. } => {
|
|
let elem_strs: Result<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
|
|
))
|
|
}
|
|
"sort" => {
|
|
let list = self.emit_expr(&args[0])?;
|
|
Ok(format!(
|
|
"[...{}].sort((a, b) => a < b ? -1 : a > b ? 1 : 0)",
|
|
list
|
|
))
|
|
}
|
|
"sortBy" => {
|
|
let list = self.emit_expr(&args[0])?;
|
|
let func = self.emit_expr(&args[1])?;
|
|
Ok(format!("[...{}].sort({})", list, func))
|
|
}
|
|
_ => 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 Map module operations using JS Map
|
|
fn emit_map_operation(
|
|
&mut self,
|
|
operation: &str,
|
|
args: &[Expr],
|
|
) -> Result<String, JsGenError> {
|
|
match operation {
|
|
"new" => Ok("new Map()".to_string()),
|
|
"set" => {
|
|
let map = self.emit_expr(&args[0])?;
|
|
let key = self.emit_expr(&args[1])?;
|
|
let val = self.emit_expr(&args[2])?;
|
|
Ok(format!(
|
|
"(function() {{ var m = new Map({}); m.set({}, {}); return m; }})()",
|
|
map, key, val
|
|
))
|
|
}
|
|
"get" => {
|
|
let map = self.emit_expr(&args[0])?;
|
|
let key = self.emit_expr(&args[1])?;
|
|
Ok(format!(
|
|
"({0}.has({1}) ? Lux.Some({0}.get({1})) : Lux.None())",
|
|
map, key
|
|
))
|
|
}
|
|
"contains" => {
|
|
let map = self.emit_expr(&args[0])?;
|
|
let key = self.emit_expr(&args[1])?;
|
|
Ok(format!("{}.has({})", map, key))
|
|
}
|
|
"remove" => {
|
|
let map = self.emit_expr(&args[0])?;
|
|
let key = self.emit_expr(&args[1])?;
|
|
Ok(format!(
|
|
"(function() {{ var m = new Map({}); m.delete({}); return m; }})()",
|
|
map, key
|
|
))
|
|
}
|
|
"keys" => {
|
|
let map = self.emit_expr(&args[0])?;
|
|
Ok(format!("Array.from({}.keys()).sort()", map))
|
|
}
|
|
"values" => {
|
|
let map = self.emit_expr(&args[0])?;
|
|
Ok(format!(
|
|
"Array.from({0}.entries()).sort(function(a,b) {{ return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0; }}).map(function(e) {{ return e[1]; }})",
|
|
map
|
|
))
|
|
}
|
|
"size" => {
|
|
let map = self.emit_expr(&args[0])?;
|
|
Ok(format!("{}.size", map))
|
|
}
|
|
"isEmpty" => {
|
|
let map = self.emit_expr(&args[0])?;
|
|
Ok(format!("({}.size === 0)", map))
|
|
}
|
|
"fromList" => {
|
|
let list = self.emit_expr(&args[0])?;
|
|
Ok(format!("new Map({}.map(function(t) {{ return [t[0], t[1]]; }}))", list))
|
|
}
|
|
"toList" => {
|
|
let map = self.emit_expr(&args[0])?;
|
|
Ok(format!(
|
|
"Array.from({}.entries()).sort(function(a,b) {{ return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0; }})",
|
|
map
|
|
))
|
|
}
|
|
"merge" => {
|
|
let m1 = self.emit_expr(&args[0])?;
|
|
let m2 = self.emit_expr(&args[1])?;
|
|
Ok(format!("new Map([...{}, ...{}])", m1, m2))
|
|
}
|
|
_ => Err(JsGenError {
|
|
message: format!("Unknown Map operation: {}", operation),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
/// 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 | BinaryOp::Concat)
|
|
&& (self.is_string_expr(left) || self.is_string_expr(right))
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Check if an expression contains statements that emit code before the result
|
|
fn expr_has_statements(&self, expr: &Expr) -> bool {
|
|
match expr {
|
|
Expr::Block { statements, .. } => !statements.is_empty(),
|
|
Expr::Let { .. } => true,
|
|
Expr::Match { .. } => true,
|
|
Expr::If { then_branch, else_branch, .. } => {
|
|
self.expr_has_statements(then_branch) || self.expr_has_statements(else_branch)
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Check if an expression calls the main function
|
|
fn expr_calls_main(&self, expr: &Expr) -> bool {
|
|
match expr {
|
|
Expr::Call { func, .. } => {
|
|
if let Expr::Var(ident) = func.as_ref() {
|
|
if ident.name == "main" {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
Expr::Run { expr, .. } => self.expr_calls_main(expr),
|
|
Expr::Let { value, body, .. } => {
|
|
self.expr_calls_main(value) || self.expr_calls_main(body)
|
|
}
|
|
Expr::Block { statements, result, .. } => {
|
|
for stmt in statements {
|
|
if let Statement::Let { value, .. } = stmt {
|
|
if self.expr_calls_main(value) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
self.expr_calls_main(result)
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Mangle a Lux name to a valid JavaScript name
|
|
fn mangle_name(&self, name: &str) -> String {
|
|
// Extern functions use their JS name directly (no mangling)
|
|
if let Some(js_name) = self.extern_fns.get(name) {
|
|
return js_name.clone();
|
|
}
|
|
format!("{}_lux", name)
|
|
}
|
|
|
|
/// Escape JavaScript reserved keywords
|
|
fn escape_js_keyword(&self, name: &str) -> String {
|
|
match name {
|
|
"break" | "case" | "catch" | "continue" | "debugger" | "default" | "delete" | "do"
|
|
| "else" | "finally" | "for" | "function" | "if" | "in" | "instanceof" | "new"
|
|
| "return" | "switch" | "this" | "throw" | "try" | "typeof" | "var" | "void"
|
|
| "while" | "with" | "class" | "const" | "enum" | "export" | "extends" | "import"
|
|
| "super" | "implements" | "interface" | "let" | "package" | "private"
|
|
| "protected" | "public" | "static" | "yield" | "await" | "async" => {
|
|
format!("{}_", name)
|
|
}
|
|
_ => name.to_string(),
|
|
}
|
|
}
|
|
|
|
/// Generate a fresh unique name
|
|
fn fresh_name(&mut self) -> usize {
|
|
let n = self.name_counter;
|
|
self.name_counter += 1;
|
|
n
|
|
}
|
|
|
|
/// Write a line with current indentation
|
|
fn writeln(&mut self, s: &str) {
|
|
for _ in 0..self.indent {
|
|
self.output.push_str(" ");
|
|
}
|
|
self.output.push_str(s);
|
|
self.output.push('\n');
|
|
}
|
|
}
|
|
|
|
impl Default for JsBackend {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::parser::Parser;
|
|
use std::process::Command;
|
|
|
|
#[test]
|
|
fn test_literal_int() {
|
|
let backend = JsBackend::new();
|
|
let lit = Literal {
|
|
kind: LiteralKind::Int(42),
|
|
span: Span::default(),
|
|
};
|
|
assert_eq!(backend.emit_literal(&lit).unwrap(), "42");
|
|
}
|
|
|
|
#[test]
|
|
fn test_literal_string() {
|
|
let backend = JsBackend::new();
|
|
let lit = Literal {
|
|
kind: LiteralKind::String("hello\nworld".to_string()),
|
|
span: Span::default(),
|
|
};
|
|
assert_eq!(backend.emit_literal(&lit).unwrap(), "\"hello\\nworld\"");
|
|
}
|
|
|
|
#[test]
|
|
fn test_literal_bool() {
|
|
let backend = JsBackend::new();
|
|
let lit = Literal {
|
|
kind: LiteralKind::Bool(true),
|
|
span: Span::default(),
|
|
};
|
|
assert_eq!(backend.emit_literal(&lit).unwrap(), "true");
|
|
}
|
|
|
|
#[test]
|
|
fn test_mangle_name() {
|
|
let backend = JsBackend::new();
|
|
assert_eq!(backend.mangle_name("foo"), "foo_lux");
|
|
assert_eq!(backend.mangle_name("main"), "main_lux");
|
|
}
|
|
|
|
#[test]
|
|
fn test_escape_keywords() {
|
|
let backend = JsBackend::new();
|
|
assert_eq!(backend.escape_js_keyword("class"), "class_");
|
|
assert_eq!(backend.escape_js_keyword("foo"), "foo");
|
|
}
|
|
|
|
/// Helper to compile Lux source to JS and run in Node.js
|
|
fn compile_and_run(source: &str) -> Result<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, "<") 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 core is always 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");
|
|
|
|
// Core runtime is always present
|
|
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");
|
|
|
|
// Console-only program should NOT include Dom, Html, or TEA sections
|
|
assert!(!js_code.contains("Dom:"), "Dom handler should not be in Console-only program");
|
|
assert!(!js_code.contains("renderHtml:"), "renderHtml should not be in Console-only program");
|
|
assert!(!js_code.contains("app:"), "TEA app should not be in Console-only program");
|
|
assert!(!js_code.contains("Http:"), "Http should not be in Console-only program");
|
|
|
|
// Console should be present
|
|
assert!(js_code.contains("Console:"), "Console handler should exist");
|
|
}
|
|
|
|
#[test]
|
|
fn test_js_runtime_tree_shaking_all_effects() {
|
|
// Test that all effects are included when all are used
|
|
use crate::parser::Parser;
|
|
|
|
let source = r#"
|
|
fn main(): Unit with {Console, Dom} = {
|
|
Console.print("Hello")
|
|
let _ = Dom.getElementById("app")
|
|
()
|
|
}
|
|
"#;
|
|
|
|
let program = Parser::parse_source(source).expect("Should parse");
|
|
let mut backend = JsBackend::new();
|
|
let js_code = backend.generate(&program).expect("Should generate");
|
|
|
|
assert!(js_code.contains("Console:"), "Console handler should exist");
|
|
assert!(js_code.contains("Dom:"), "Dom handler should exist");
|
|
assert!(js_code.contains("renderHtml:"), "renderHtml should be defined when Dom is used");
|
|
assert!(js_code.contains("renderToDom:"), "renderToDom should be defined when Dom is used");
|
|
assert!(js_code.contains("escapeHtml:"), "escapeHtml should be defined when Dom is used");
|
|
assert!(js_code.contains("app:"), "TEA app should be defined when Dom is used");
|
|
assert!(js_code.contains("simpleApp:"), "simpleApp should be defined when Dom is used");
|
|
assert!(js_code.contains("hasChanged:"), "hasChanged should be defined when Dom is used");
|
|
}
|
|
|
|
#[test]
|
|
fn test_js_runtime_default_handlers() {
|
|
// Test that only used effect handlers are 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");
|
|
|
|
// Only Console should be present
|
|
assert!(js_code.contains("Console:"), "Console handler should exist");
|
|
assert!(!js_code.contains("Random:"), "Random handler should not exist in Console-only program");
|
|
assert!(!js_code.contains("Time:"), "Time handler should not exist in Console-only program");
|
|
assert!(!js_code.contains("Http:"), "Http handler should not exist in Console-only program");
|
|
assert!(!js_code.contains("Dom:"), "Dom handler should not exist in Console-only program");
|
|
}
|
|
|
|
#[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");
|
|
}
|
|
}
|