diff --git a/crates/zapcode-core/src/compiler/instruction.rs b/crates/zapcode-core/src/compiler/instruction.rs index c8b8ef0..e55e087 100644 --- a/crates/zapcode-core/src/compiler/instruction.rs +++ b/crates/zapcode-core/src/compiler/instruction.rs @@ -60,6 +60,8 @@ pub enum Instruction { Call(usize), Return, CallExternal(String, usize), + /// Call a global builtin function by name (e.g. parseInt, isNaN, Number). + CallBuiltin(String, usize), // Control flow Jump(usize), diff --git a/crates/zapcode-core/src/compiler/mod.rs b/crates/zapcode-core/src/compiler/mod.rs index 9d2284e..ced4930 100644 --- a/crates/zapcode-core/src/compiler/mod.rs +++ b/crates/zapcode-core/src/compiler/mod.rs @@ -34,6 +34,15 @@ struct Compiler { external_functions: HashSet, } +/// Global functions dispatched via `Instruction::CallBuiltin`. Kept in sync with +/// `builtins::call_global_function`. +fn is_global_builtin_fn(name: &str) -> bool { + matches!( + name, + "parseInt" | "parseFloat" | "isNaN" | "isFinite" | "String" | "Number" | "Boolean" + ) +} + struct LoopInfo { break_patches: Vec, continue_patches: Vec, @@ -919,6 +928,17 @@ impl Compiler { return Ok(()); } } + // Direct call to a global builtin function (parseInt, Number, …), + // unless shadowed by a local of the same name. + if let Expr::Ident(name) = callee.as_ref() { + if self.resolve_local(name).is_none() && is_global_builtin_fn(name) { + for arg in args { + self.compile_expr(arg)?; + } + self.emit(Instruction::CallBuiltin(name.clone(), args.len())); + return Ok(()); + } + } self.compile_expr(callee)?; for arg in args { self.compile_expr(arg)?; diff --git a/crates/zapcode-core/src/vm/builtins.rs b/crates/zapcode-core/src/vm/builtins.rs index 2b20d44..7313dcc 100644 --- a/crates/zapcode-core/src/vm/builtins.rs +++ b/crates/zapcode-core/src/vm/builtins.rs @@ -707,6 +707,136 @@ fn call_array_method(arr: &[Value], method: &str, args: &[Value]) -> Result Option { + let value = match name { + "parseInt" => parse_int(args), + "parseFloat" => parse_float(args), + "isNaN" => Value::Bool(arg_num(args, 0).is_nan()), + "isFinite" => Value::Bool(arg_num(args, 0).is_finite()), + "String" => match args.first() { + Some(v) => Value::String(Arc::from(v.to_js_string().as_str())), + None => Value::String(Arc::from("")), + }, + "Number" => match args.first() { + Some(v) => narrow_number(v.to_number()), + None => Value::Int(0), + }, + "Boolean" => Value::Bool(args.first().map(|v| v.is_truthy()).unwrap_or(false)), + _ => return None, + }; + Some(value) +} + +/// Represent a finite integral number as `Int` (nicer `===`), else `Float`. +fn narrow_number(n: f64) -> Value { + if n.is_finite() && n.fract() == 0.0 && n.abs() < i64::MAX as f64 { + Value::Int(n as i64) + } else { + Value::Float(n) + } +} + +fn parse_int(args: &[Value]) -> Value { + let s = args.first().map(|v| v.to_js_string()).unwrap_or_default(); + let chars: Vec = s.trim_start().chars().collect(); + let mut i = 0; + let mut sign = 1.0; + if i < chars.len() && (chars[i] == '+' || chars[i] == '-') { + if chars[i] == '-' { + sign = -1.0; + } + i += 1; + } + + let radix_arg = args.get(1).map(|v| v.to_number()).unwrap_or(f64::NAN); + let mut radix: i64 = if radix_arg.is_nan() || radix_arg == 0.0 { + 0 + } else { + radix_arg as i64 + }; + if radix != 0 && !(2..=36).contains(&radix) { + return Value::Float(f64::NAN); + } + if (radix == 0 || radix == 16) + && i + 1 < chars.len() + && chars[i] == '0' + && (chars[i + 1] == 'x' || chars[i + 1] == 'X') + { + i += 2; + radix = 16; + } + if radix == 0 { + radix = 10; + } + + let mut value = 0.0; + let mut any = false; + while i < chars.len() { + match chars[i].to_digit(36) { + Some(d) if (d as i64) < radix => { + value = value * radix as f64 + d as f64; + any = true; + i += 1; + } + _ => break, + } + } + if !any { + return Value::Float(f64::NAN); + } + narrow_number(sign * value) +} + +fn parse_float(args: &[Value]) -> Value { + let s = args.first().map(|v| v.to_js_string()).unwrap_or_default(); + let s = s.trim_start(); + if s.starts_with("Infinity") || s.starts_with("+Infinity") { + return Value::Float(f64::INFINITY); + } + if s.starts_with("-Infinity") { + return Value::Float(f64::NEG_INFINITY); + } + let b = s.as_bytes(); + let n = b.len(); + let mut i = 0; + if i < n && (b[i] == b'+' || b[i] == b'-') { + i += 1; + } + let mut saw_digit = false; + while i < n && b[i].is_ascii_digit() { + i += 1; + saw_digit = true; + } + if i < n && b[i] == b'.' { + i += 1; + while i < n && b[i].is_ascii_digit() { + i += 1; + saw_digit = true; + } + } + if saw_digit && i < n && (b[i] == b'e' || b[i] == b'E') { + let mut j = i + 1; + if j < n && (b[j] == b'+' || b[j] == b'-') { + j += 1; + } + if j < n && b[j].is_ascii_digit() { + while j < n && b[j].is_ascii_digit() { + j += 1; + } + i = j; + } + } + if !saw_digit { + return Value::Float(f64::NAN); + } + s[..i] + .parse::() + .map(Value::Float) + .unwrap_or(Value::Float(f64::NAN)) +} + // ── Object static methods ──────────────────────────────────────────── fn call_object_method(method: &str, args: &[Value]) -> Result> { diff --git a/crates/zapcode-core/src/vm/mod.rs b/crates/zapcode-core/src/vm/mod.rs index f0f39d9..eb918db 100644 --- a/crates/zapcode-core/src/vm/mod.rs +++ b/crates/zapcode-core/src/vm/mod.rs @@ -1808,6 +1808,22 @@ impl Vm { snapshot, })); } + Instruction::CallBuiltin(name, arg_count) => { + let mut args = Vec::with_capacity(arg_count); + for _ in 0..arg_count { + args.push(self.pop()?); + } + args.reverse(); + match builtins::call_global_function(&name, &args) { + Some(val) => self.push(val)?, + None => { + return Err(ZapcodeError::TypeError(format!( + "{} is not a function", + name + ))) + } + } + } // Control flow Instruction::Jump(target) => { diff --git a/crates/zapcode-core/tests/builtins.rs b/crates/zapcode-core/tests/builtins.rs index c69bb02..2651c70 100644 --- a/crates/zapcode-core/tests/builtins.rs +++ b/crates/zapcode-core/tests/builtins.rs @@ -574,3 +574,43 @@ fn test_array_some_empty() { let result = eval_ts("[].some((x) => x > 0)").unwrap(); assert_eq!(result, Value::Bool(false)); } + +// ── global builtin functions (regression: parseInt/parseFloat were missing) ── + +#[test] +fn test_parse_int() { + assert_eq!(eval_ts("parseInt('42px')").unwrap(), Value::Int(42)); + assert_eq!(eval_ts("parseInt(' -7')").unwrap(), Value::Int(-7)); + assert_eq!(eval_ts("parseInt('0xFF')").unwrap(), Value::Int(255)); + assert_eq!(eval_ts("parseInt('101', 2)").unwrap(), Value::Int(5)); + assert_eq!(eval_ts("parseInt('ff', 16)").unwrap(), Value::Int(255)); + assert!(matches!(eval_ts("parseInt('abc')").unwrap(), Value::Float(f) if f.is_nan())); +} + +#[test] +fn test_parse_float() { + assert_eq!(eval_ts("parseFloat('2.5xyz')").unwrap(), Value::Float(2.5)); + assert_eq!( + eval_ts("parseFloat(' 1e3')").unwrap(), + Value::Float(1000.0) + ); + assert!(matches!(eval_ts("parseFloat('nope')").unwrap(), Value::Float(f) if f.is_nan())); +} + +#[test] +fn test_isnan_isfinite_number_string_boolean() { + assert_eq!(eval_ts("isNaN(NaN)").unwrap(), Value::Bool(true)); + assert_eq!(eval_ts("isNaN(3)").unwrap(), Value::Bool(false)); + assert_eq!(eval_ts("isFinite(1/0)").unwrap(), Value::Bool(false)); + assert_eq!(eval_ts("isFinite(42)").unwrap(), Value::Bool(true)); + assert_eq!(eval_ts("Number('42')").unwrap(), Value::Int(42)); + assert_eq!(eval_ts("String(42)").unwrap(), Value::String("42".into())); + assert_eq!(eval_ts("Boolean(0)").unwrap(), Value::Bool(false)); +} + +#[test] +fn test_global_builtin_shadowed_by_local() { + // A local named `Number` must win over the builtin. + let r = eval_ts("const Number = (x) => x + 1; Number(41)").unwrap(); + assert_eq!(r, Value::Int(42)); +}