feat: v2 built-in functions — 90+ native functions
Array: len, push, pop, filter, map, concat, contains, reverse, slice, indexOf, find, some, every, flat, sort (mutating ops re-trigger signal) Math: abs, min, max, floor, ceil, round, random, sqrt, pow, sin, cos, tan, atan2, clamp, lerp String: split, join, trim, upper, lower, replace, starts_with, ends_with, char_at, substring Conversion: int, float, string, bool Console: log, debug, warn Timer: delay Also adds ExprStatement support for top-level expressions (log, push, etc). 110 tests, 0 failures.
This commit is contained in:
parent
2aa2c7ad8e
commit
26d6c4f17a
4 changed files with 161 additions and 7 deletions
|
|
@ -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.indent -= 1;
|
||||||
self.emit_line("})();");
|
self.emit_line("})();");
|
||||||
|
|
||||||
|
|
@ -684,13 +692,98 @@ impl JsEmitter {
|
||||||
}
|
}
|
||||||
Expr::Call(name, args) => {
|
Expr::Call(name, args) => {
|
||||||
let args_js: Vec<String> = args.iter().map(|a| self.emit_expr(a)).collect();
|
let args_js: Vec<String> = args.iter().map(|a| self.emit_expr(a)).collect();
|
||||||
// Built-in functions map to DS.xxx()
|
|
||||||
let fn_name = match name.as_str() {
|
// ── Built-in function dispatch ──
|
||||||
"navigate" => "DS.navigate",
|
match name.as_str() {
|
||||||
"spring" => "DS.spring",
|
// Navigation & springs
|
||||||
_ => name,
|
"navigate" => format!("DS.navigate({})", args_js.join(", ")),
|
||||||
};
|
"spring" => format!("DS.spring({})", args_js.join(", ")),
|
||||||
format!("{}({})", fn_name, 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) => {
|
Expr::If(cond, then_b, else_b) => {
|
||||||
let c = self.emit_expr(cond);
|
let c = self.emit_expr(cond);
|
||||||
|
|
@ -819,6 +912,17 @@ impl JsEmitter {
|
||||||
format!("n{id}")
|
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) {
|
fn emit_line(&mut self, line: &str) {
|
||||||
for _ in 0..self.indent {
|
for _ in 0..self.indent {
|
||||||
self.output.push_str(" ");
|
self.output.push_str(" ");
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ pub enum Declaration {
|
||||||
Stream(StreamDecl),
|
Stream(StreamDecl),
|
||||||
/// `every 500 -> expr`
|
/// `every 500 -> expr`
|
||||||
Every(EveryDecl),
|
Every(EveryDecl),
|
||||||
|
/// Top-level expression statement: `log("hello")`, `push(items, x)`
|
||||||
|
ExprStatement(Expr),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `let count = 0` or `let doubled = count * 2`
|
/// `let count = 0` or `let doubled = count * 2`
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,11 @@ impl Parser {
|
||||||
TokenKind::Constrain => self.parse_constrain_decl(),
|
TokenKind::Constrain => self.parse_constrain_decl(),
|
||||||
TokenKind::Stream => self.parse_stream_decl(),
|
TokenKind::Stream => self.parse_stream_decl(),
|
||||||
TokenKind::Every => self.parse_every_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!(
|
_ => Err(self.error(format!(
|
||||||
"expected declaration (let, view, effect, on, component, route, constrain, stream, every), got {:?}",
|
"expected declaration (let, view, effect, on, component, route, constrain, stream, every), got {:?}",
|
||||||
self.peek()
|
self.peek()
|
||||||
|
|
|
||||||
43
examples/builtins.ds
Normal file
43
examples/builtins.ds
Normal file
|
|
@ -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)}"
|
||||||
|
]
|
||||||
Loading…
Add table
Reference in a new issue