-
Notifications
You must be signed in to change notification settings - Fork 6
feat: add global functions parseInt, parseFloat, isNaN, isFinite, Number, String, Boolean #57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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>, | ||
|
|
@@ -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)?; | ||
| } | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This compile-time rewrite bypasses normal lexical/global resolution. Both cases were verified at this PR head: const Number = value => value + 1;
const call = () => Number(41);
call();Expected: const parse = parseInt;
parse("42");Expected: Please register these as normal builtin function |
||
| self.emit(Instruction::CallBuiltin(name.clone(), args.len())); | ||
| return Ok(()); | ||
| } | ||
| } | ||
| self.compile_expr(callee)?; | ||
| for arg in args { | ||
| self.compile_expr(arg)?; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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() { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These conversions use the current generic [Number(""), Number(" 42 "), Number("0x10")]Expected: I also verified: parseInt("10", 4294967298)Expected: Please implement shared ECMAScript-style |
||
| 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(); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This parser clones the full input string and then materializes I deliberately did not run an unbounded stress payload during review. This finding is based on the allocation and control-flow path here. Please avoid materializing |
||
| 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>> { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instructionderives serde serialization and the completeCompiledProgramis stored in postcard snapshots. InsertingCallBuiltinhere shifts the enum tags for every following opcode (Jumponward), so a snapshot created by Zapcode 1.5.3 may decode old instructions as different instructions after an upgrade.Example scenario:
The persisted snapshot contains the post-suspension jump bytecode. After this insertion, those old enum tags no longer identify the same opcodes.
Please append
CallBuiltinat the end of the enum to preserve existing tags. Longer term, snapshots should use an explicit format version and legacy fixture tests, but preserving enum order is the minimal fix required in this PR.