Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/zapcode-core/src/compiler/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
20 changes: 20 additions & 0 deletions crates/zapcode-core/src/compiler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ struct Compiler {
external_functions: HashSet<String>,
}

/// 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<usize>,
continue_patches: Vec<usize>,
Expand Down Expand Up @@ -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)?;
Expand Down
130 changes: 130 additions & 0 deletions crates/zapcode-core/src/vm/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,136 @@ fn call_array_method(arr: &[Value], method: &str, args: &[Value]) -> Result<Opti
Ok(Some(result))
}

/// Dispatch a global builtin function call (parseInt, isNaN, Number, …).
/// Returns `None` only for an unrecognized name (the compiler shouldn't emit one).
pub fn call_global_function(name: &str, args: &[Value]) -> Option<Value> {
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<char> = 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::<f64>()
.map(Value::Float)
.unwrap_or(Value::Float(f64::NAN))
}

// ── Object static methods ────────────────────────────────────────────

fn call_object_method(method: &str, args: &[Value]) -> Result<Option<Value>> {
Expand Down
16 changes: 16 additions & 0 deletions crates/zapcode-core/src/vm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
40 changes: 40 additions & 0 deletions crates/zapcode-core/tests/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}