diff --git a/compiler/ds-codegen/src/js_emitter.rs b/compiler/ds-codegen/src/js_emitter.rs index 225317e..8fa5685 100644 --- a/compiler/ds-codegen/src/js_emitter.rs +++ b/compiler/ds-codegen/src/js_emitter.rs @@ -267,6 +267,14 @@ impl JsEmitter { } } + // Phase 9: Top-level expression statements (log, push, etc.) + for decl in &program.declarations { + if let Declaration::ExprStatement(expr) = decl { + let js = self.emit_expr(expr); + self.emit_line(&format!("{};", js)); + } + } + self.indent -= 1; self.emit_line("})();"); @@ -684,13 +692,98 @@ impl JsEmitter { } Expr::Call(name, args) => { let args_js: Vec = args.iter().map(|a| self.emit_expr(a)).collect(); - // Built-in functions map to DS.xxx() - let fn_name = match name.as_str() { - "navigate" => "DS.navigate", - "spring" => "DS.spring", - _ => name, - }; - format!("{}({})", fn_name, args_js.join(", ")) + + // ── Built-in function dispatch ── + match name.as_str() { + // Navigation & springs + "navigate" => format!("DS.navigate({})", args_js.join(", ")), + "spring" => format!("DS.spring({})", args_js.join(", ")), + + // ── Array operations ── + "len" if args.len() == 1 => format!("{}.length", args_js[0]), + "push" if args.len() == 2 => { + // push mutates array, need to get signal root name + let root = self.get_signal_root_name(&args[0]); + format!("(() => {{ {}.push({}); {root}.value = [...{root}.value]; return {root}.value; }})()", + args_js[0], args_js[1], ) + } + "pop" if args.len() == 1 => { + let root = self.get_signal_root_name(&args[0]); + format!("(() => {{ const _v = {}.pop(); {root}.value = [...{root}.value]; return _v; }})()", + args_js[0]) + } + "filter" if args.len() == 2 => format!("{}.filter({})", args_js[0], args_js[1]), + "map" if args.len() == 2 => format!("{}.map({})", args_js[0], args_js[1]), + "concat" if args.len() == 2 => format!("[...{}, ...{}]", args_js[0], args_js[1]), + "contains" if args.len() == 2 => format!("{}.includes({})", args_js[0], args_js[1]), + "reverse" if args.len() == 1 => { + let root = self.get_signal_root_name(&args[0]); + format!("(() => {{ {}.reverse(); {root}.value = [...{root}.value]; return {root}.value; }})()", + args_js[0]) + } + "slice" if args.len() >= 2 => format!("{}.slice({})", args_js[0], args_js[1..].join(", ")), + "indexOf" if args.len() == 2 => format!("{}.indexOf({})", args_js[0], args_js[1]), + "find" if args.len() == 2 => format!("{}.find({})", args_js[0], args_js[1]), + "some" if args.len() == 2 => format!("{}.some({})", args_js[0], args_js[1]), + "every" if args.len() == 2 => format!("{}.every({})", args_js[0], args_js[1]), + "flat" if args.len() == 1 => format!("{}.flat()", args_js[0]), + "sort" if args.len() >= 1 => { + let root = self.get_signal_root_name(&args[0]); + if args.len() == 2 { + format!("(() => {{ {}.sort({}); {root}.value = [...{root}.value]; return {root}.value; }})()", + args_js[0], args_js[1]) + } else { + format!("(() => {{ {}.sort(); {root}.value = [...{root}.value]; return {root}.value; }})()", + args_js[0]) + } + } + + // ── Math operations ── + "abs" if args.len() == 1 => format!("Math.abs({})", args_js[0]), + "min" => format!("Math.min({})", args_js.join(", ")), + "max" => format!("Math.max({})", args_js.join(", ")), + "floor" if args.len() == 1 => format!("Math.floor({})", args_js[0]), + "ceil" if args.len() == 1 => format!("Math.ceil({})", args_js[0]), + "round" if args.len() == 1 => format!("Math.round({})", args_js[0]), + "random" if args.is_empty() => "Math.random()".to_string(), + "sqrt" if args.len() == 1 => format!("Math.sqrt({})", args_js[0]), + "pow" if args.len() == 2 => format!("Math.pow({}, {})", args_js[0], args_js[1]), + "sin" if args.len() == 1 => format!("Math.sin({})", args_js[0]), + "cos" if args.len() == 1 => format!("Math.cos({})", args_js[0]), + "tan" if args.len() == 1 => format!("Math.tan({})", args_js[0]), + "atan2" if args.len() == 2 => format!("Math.atan2({}, {})", args_js[0], args_js[1]), + "clamp" if args.len() == 3 => format!("Math.min(Math.max({}, {}), {})", args_js[0], args_js[1], args_js[2]), + "lerp" if args.len() == 3 => format!("({} + ({} - {}) * {})", args_js[0], args_js[1], args_js[0], args_js[2]), + + // ── String operations ── + "split" if args.len() == 2 => format!("{}.split({})", args_js[0], args_js[1]), + "join" if args.len() == 2 => format!("{}.join({})", args_js[0], args_js[1]), + "trim" if args.len() == 1 => format!("{}.trim()", args_js[0]), + "upper" if args.len() == 1 => format!("{}.toUpperCase()", args_js[0]), + "lower" if args.len() == 1 => format!("{}.toLowerCase()", args_js[0]), + "replace" if args.len() == 3 => format!("{}.replace({}, {})", args_js[0], args_js[1], args_js[2]), + "starts_with" if args.len() == 2 => format!("{}.startsWith({})", args_js[0], args_js[1]), + "ends_with" if args.len() == 2 => format!("{}.endsWith({})", args_js[0], args_js[1]), + "char_at" if args.len() == 2 => format!("{}.charAt({})", args_js[0], args_js[1]), + "substring" if args.len() == 3 => format!("{}.substring({}, {})", args_js[0], args_js[1], args_js[2]), + + // ── Conversion ── + "int" if args.len() == 1 => format!("parseInt({})", args_js[0]), + "float" if args.len() == 1 => format!("parseFloat({})", args_js[0]), + "string" if args.len() == 1 => format!("String({})", args_js[0]), + "bool" if args.len() == 1 => format!("Boolean({})", args_js[0]), + + // ── Console / debug ── + "log" => format!("console.log({})", args_js.join(", ")), + "debug" => format!("console.debug({})", args_js.join(", ")), + "warn" => format!("console.warn({})", args_js.join(", ")), + + // ── Timer ── + "delay" if args.len() == 2 => format!("setTimeout(() => {{ {} }}, {})", args_js[0], args_js[1]), + + // ── Fallback: user-defined function ── + _ => format!("{}({})", name, args_js.join(", ")), + } } Expr::If(cond, then_b, else_b) => { let c = self.emit_expr(cond); @@ -819,6 +912,17 @@ impl JsEmitter { format!("n{id}") } + /// Extract the root signal name from an expression. + /// `Ident("todos")` → "todos", `DotAccess(Ident("a"), "b")` → "a", fallback → "_arr" + fn get_signal_root_name(&self, expr: &Expr) -> String { + match expr { + Expr::Ident(name) => name.clone(), + Expr::DotAccess(base, _) => self.get_signal_root_name(base), + Expr::Index(base, _) => self.get_signal_root_name(base), + _ => "_arr".to_string(), + } + } + fn emit_line(&mut self, line: &str) { for _ in 0..self.indent { self.output.push_str(" "); diff --git a/compiler/ds-parser/src/ast.rs b/compiler/ds-parser/src/ast.rs index d369c18..a9d3dff 100644 --- a/compiler/ds-parser/src/ast.rs +++ b/compiler/ds-parser/src/ast.rs @@ -28,6 +28,8 @@ pub enum Declaration { Stream(StreamDecl), /// `every 500 -> expr` Every(EveryDecl), + /// Top-level expression statement: `log("hello")`, `push(items, x)` + ExprStatement(Expr), } /// `let count = 0` or `let doubled = count * 2` diff --git a/compiler/ds-parser/src/parser.rs b/compiler/ds-parser/src/parser.rs index 817dbff..4c42dcf 100644 --- a/compiler/ds-parser/src/parser.rs +++ b/compiler/ds-parser/src/parser.rs @@ -99,6 +99,11 @@ impl Parser { TokenKind::Constrain => self.parse_constrain_decl(), TokenKind::Stream => self.parse_stream_decl(), TokenKind::Every => self.parse_every_decl(), + // Expression statement: `log("hello")`, `push(items, x)` + TokenKind::Ident(_) => { + let expr = self.parse_expr()?; + Ok(Declaration::ExprStatement(expr)) + } _ => Err(self.error(format!( "expected declaration (let, view, effect, on, component, route, constrain, stream, every), got {:?}", self.peek() diff --git a/examples/builtins.ds b/examples/builtins.ds new file mode 100644 index 0000000..162b787 --- /dev/null +++ b/examples/builtins.ds @@ -0,0 +1,43 @@ +-- DreamStack built-in functions test +-- Tests array, math, string, and conversion builtins + +let items = [1, 2, 3, 4, 5] +let name = "DreamStack" + +-- Array operations +let count = len(items) +let has_three = contains(items, 3) + +-- Math operations +let x = floor(3.7) +let y = ceil(2.1) +let small = min(10, 5) +let big = max(10, 5) +let clamped = clamp(15, 0, 10) +let dist = sqrt(pow(3, 2) + pow(4, 2)) + +-- String operations +let upper_name = upper(name) +let lower_name = lower(name) + +-- Console +log("count:", count) +log("distance:", dist) + +view main = + column [ + text "Built-in Functions" + text "Items: {count}" + text "Has 3: {has_three}" + text "floor(3.7) = {x}" + text "ceil(2.1) = {y}" + text "min(10,5) = {small}" + text "max(10,5) = {big}" + text "clamp(15,0,10) = {clamped}" + text "sqrt(3²+4²) = {dist}" + text "upper = {upper_name}" + text "lower = {lower_name}" + button "Push 6" { click: push(items, 6) } + button "Pop" { click: pop(items) } + text "Length: {len(items)}" + ]