use std::cell::RefCell;
use std::rc::Rc;
use crate::builtins::number::{number_parse_float, number_parse_int};
use crate::error::{StatorError, StatorResult};
use crate::interpreter::GlobalEnv;
use crate::objects::value::JsValue;
pub const GLOBAL_UNDEFINED: JsValue = JsValue::Undefined;
pub const GLOBAL_NAN: f64 = f64::NAN;
pub const GLOBAL_INFINITY: f64 = f64::INFINITY;
pub fn global_is_nan(value: f64) -> bool {
value.is_nan()
}
pub fn global_is_finite(value: f64) -> bool {
value.is_finite()
}
pub fn global_parse_int(string: &str, radix: i32) -> f64 {
number_parse_int(string, radix)
}
pub fn global_parse_float(string: &str) -> f64 {
number_parse_float(string)
}
pub fn global_encode_uri(uri: &str) -> String {
percent_encode(uri, is_encode_uri_safe)
}
pub fn global_decode_uri(encoded_uri: &str) -> StatorResult<String> {
percent_decode(encoded_uri, is_decode_uri_reserved)
}
pub fn global_encode_uri_component(component: &str) -> String {
percent_encode(component, is_encode_uri_component_safe)
}
pub fn global_decode_uri_component(encoded_component: &str) -> StatorResult<String> {
percent_decode(encoded_component, |_| false)
}
pub fn global_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len() * 2);
for c in s.encode_utf16() {
let ch = char::from_u32(c as u32);
let is_safe = ch.is_some_and(|ch| {
matches!(ch,
'A'..='Z' | 'a'..='z' | '0'..='9'
| '@' | '*' | '_' | '+' | '-' | '.' | '/'
)
});
if is_safe {
out.push(ch.unwrap());
} else if c <= 0xFF {
out.push('%');
out.push(hex_digit((c as u8) >> 4));
out.push(hex_digit((c as u8) & 0xF));
} else {
out.push_str(&format!("%u{c:04X}"));
}
}
out
}
pub fn global_unescape(s: &str) -> String {
let bytes = s.as_bytes();
let mut out = String::with_capacity(s.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' {
if i + 5 < bytes.len()
&& bytes[i + 1] == b'u'
&& bytes[i + 2..i + 6].iter().all(|b| b.is_ascii_hexdigit())
{
let hex_str = std::str::from_utf8(&bytes[i + 2..i + 6]).unwrap_or("0000");
if let Ok(code) = u16::from_str_radix(hex_str, 16) {
let decoded = String::from_utf16_lossy(&[code]);
out.push_str(&decoded);
i += 6;
continue;
}
}
if i + 2 < bytes.len()
&& bytes[i + 1].is_ascii_hexdigit()
&& bytes[i + 2].is_ascii_hexdigit()
{
let hex_str = std::str::from_utf8(&bytes[i + 1..i + 3]).unwrap_or("00");
if let Ok(code) = u8::from_str_radix(hex_str, 16) {
out.push(code as char);
i += 3;
continue;
}
}
}
out.push(bytes[i] as char);
i += 1;
}
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EvalMode {
Direct,
Indirect,
}
struct EvalOutcome {
result: JsValue,
global_env: Rc<RefCell<GlobalEnv>>,
is_strict: bool,
}
type DirectEvalCapture = (JsValue, Rc<RefCell<GlobalEnv>>, bool);
pub fn global_eval(source: &str) -> StatorResult<JsValue> {
Ok(eval_core(source, EvalMode::Indirect, None, None)?.result)
}
pub fn global_eval_direct(
source: &str,
caller_env: Rc<RefCell<GlobalEnv>>,
) -> StatorResult<JsValue> {
global_eval_direct_with_scope(source, caller_env, None)
}
pub fn global_eval_indirect(source: &str) -> StatorResult<JsValue> {
Ok(eval_core(source, EvalMode::Indirect, None, None)?.result)
}
pub fn global_eval_strict(
source: &str,
caller_env: Rc<RefCell<GlobalEnv>>,
) -> StatorResult<JsValue> {
use crate::bytecode::bytecode_generator::BytecodeGenerator;
use crate::interpreter::{Interpreter, InterpreterFrame};
use crate::parser::parse;
let mut program = parse(source)?;
rewrite_last_expr_to_return(&mut program);
let bytecode = BytecodeGenerator::compile_program_with_source(&program, Some(source))?;
let child_env = caller_env.borrow().clone();
let child_rc = Rc::new(RefCell::new(child_env));
let mut frame = InterpreterFrame::new_with_globals(Rc::new(bytecode), vec![], child_rc);
Interpreter::run(&mut frame)
}
fn eval_core(
source: &str,
mode: EvalMode,
caller_env: Option<Rc<RefCell<GlobalEnv>>>,
caller_context: Option<JsValue>,
) -> StatorResult<EvalOutcome> {
use crate::bytecode::bytecode_generator::BytecodeGenerator;
use crate::interpreter::{Interpreter, InterpreterFrame, current_global_env};
use crate::parser::parse;
let mut program = parse(source)?;
rewrite_last_expr_to_return(&mut program);
let is_strict = program.is_strict;
let mut frame = match mode {
EvalMode::Direct if is_strict => {
let bytecode = BytecodeGenerator::compile_program_with_source(&program, Some(source))?;
let env = caller_env.expect("direct eval requires a caller environment");
let child_env = Rc::new(RefCell::new(env.borrow().clone()));
let mut frame =
InterpreterFrame::new_with_globals(Rc::new(bytecode), vec![], child_env);
frame.context = caller_context;
frame
}
EvalMode::Direct => {
let bytecode =
BytecodeGenerator::compile_eval_program_with_source(&program, Some(source))?;
let env = caller_env.expect("direct eval requires a caller environment");
let mut frame = InterpreterFrame::new_with_globals(Rc::new(bytecode), vec![], env);
frame.context = caller_context;
frame
}
EvalMode::Indirect => {
let bytecode = BytecodeGenerator::compile_program_with_source(&program, Some(source))?;
if let Some(env) = current_global_env() {
InterpreterFrame::new_with_globals(Rc::new(bytecode), vec![], env)
} else {
InterpreterFrame::new(Rc::new(bytecode), vec![])
}
}
};
let result = Interpreter::run(&mut frame)?;
Ok(EvalOutcome {
result,
global_env: Rc::clone(&frame.global_env),
is_strict,
})
}
pub(crate) fn global_eval_direct_with_scope(
source: &str,
caller_env: Rc<RefCell<GlobalEnv>>,
caller_context: Option<JsValue>,
) -> StatorResult<JsValue> {
Ok(global_eval_direct_with_scope_capture(source, caller_env, caller_context)?.0)
}
pub(crate) fn global_eval_direct_with_scope_capture(
source: &str,
caller_env: Rc<RefCell<GlobalEnv>>,
caller_context: Option<JsValue>,
) -> StatorResult<DirectEvalCapture> {
let outcome = eval_core(source, EvalMode::Direct, Some(caller_env), caller_context)?;
Ok((outcome.result, outcome.global_env, outcome.is_strict))
}
fn rewrite_last_expr_to_return(program: &mut crate::parser::ast::Program) {
use crate::parser::ast::{ProgramItem, ReturnStmt, Stmt};
if let Some(ProgramItem::Stmt(Stmt::Expr(expr_stmt))) = program.body.last_mut() {
let return_stmt = ReturnStmt {
loc: expr_stmt.loc,
argument: Some(expr_stmt.expr.clone()),
};
*program.body.last_mut().unwrap() = ProgramItem::Stmt(Stmt::Return(return_stmt));
}
}
fn is_encode_uri_safe(c: char) -> bool {
matches!(c,
'A'..='Z' | 'a'..='z' | '0'..='9'
| '-' | '_' | '.' | '!' | '~' | '*' | '\'' | '(' | ')'
| ';' | ',' | '/' | '?' | ':' | '@' | '&' | '=' | '+' | '$' | '#'
)
}
fn is_encode_uri_component_safe(c: char) -> bool {
matches!(c,
'A'..='Z' | 'a'..='z' | '0'..='9'
| '-' | '_' | '.' | '!' | '~' | '*' | '\'' | '(' | ')'
)
}
fn is_decode_uri_reserved(byte: u8) -> bool {
matches!(
byte,
b';' | b',' | b'/' | b'?' | b':' | b'@' | b'&' | b'=' | b'+' | b'$' | b'#'
)
}
fn percent_encode(input: &str, safe: fn(char) -> bool) -> String {
let mut out = String::with_capacity(input.len());
for c in input.chars() {
if safe(c) {
out.push(c);
} else {
let mut buf = [0u8; 4];
let bytes = c.encode_utf8(&mut buf);
for &b in bytes.as_bytes() {
out.push('%');
out.push(hex_digit(b >> 4));
out.push(hex_digit(b & 0xF));
}
}
}
out
}
fn percent_decode(input: &str, keep_reserved: fn(u8) -> bool) -> StatorResult<String> {
let bytes = input.as_bytes();
let mut out = String::with_capacity(input.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] != b'%' {
out.push(bytes[i] as char);
i += 1;
continue;
}
let b0 = decode_hex_byte(bytes, i)?;
let seq_len = utf8_seq_len(b0)?;
if seq_len == 1 {
if keep_reserved(b0) {
out.push('%');
out.push(bytes[i + 1] as char);
out.push(bytes[i + 2] as char);
} else {
out.push(b0 as char);
}
i += 3;
} else {
let mut buf = [0u8; 4];
buf[0] = b0;
for (k, cell) in buf.iter_mut().enumerate().take(seq_len).skip(1) {
*cell = decode_hex_byte(bytes, i + k * 3)?;
}
let s = std::str::from_utf8(&buf[..seq_len]).map_err(|_| {
StatorError::URIError("malformed URI: invalid UTF-8 sequence".into())
})?;
out.push_str(s);
i += seq_len * 3;
}
}
Ok(out)
}
fn decode_hex_byte(bytes: &[u8], pos: usize) -> StatorResult<u8> {
if pos + 2 >= bytes.len() {
return Err(StatorError::URIError(
"malformed URI: incomplete percent-encoded sequence".into(),
));
}
let hi = from_hex_digit(bytes[pos + 1])?;
let lo = from_hex_digit(bytes[pos + 2])?;
Ok((hi << 4) | lo)
}
fn utf8_seq_len(b: u8) -> StatorResult<usize> {
if b & 0x80 == 0 {
Ok(1)
} else if b & 0xE0 == 0xC0 {
Ok(2)
} else if b & 0xF0 == 0xE0 {
Ok(3)
} else if b & 0xF8 == 0xF0 {
Ok(4)
} else {
Err(StatorError::URIError(
"malformed URI: invalid UTF-8 leading byte".into(),
))
}
}
fn from_hex_digit(b: u8) -> StatorResult<u8> {
match b {
b'0'..=b'9' => Ok(b - b'0'),
b'A'..=b'F' => Ok(b - b'A' + 10),
b'a'..=b'f' => Ok(b - b'a' + 10),
_ => Err(StatorError::URIError(format!(
"malformed URI: invalid hex digit '{}'",
b as char
))),
}
}
fn hex_digit(n: u8) -> char {
match n {
0..=9 => (b'0' + n) as char,
_ => (b'A' + n - 10) as char,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_global_undefined_is_undefined() {
assert_eq!(GLOBAL_UNDEFINED, JsValue::Undefined);
}
#[test]
fn test_global_nan_is_nan() {
assert!(GLOBAL_NAN.is_nan());
}
#[test]
fn test_global_infinity_is_positive_infinity() {
assert!(GLOBAL_INFINITY.is_infinite() && GLOBAL_INFINITY > 0.0);
}
#[test]
fn test_global_is_nan_with_nan() {
assert!(global_is_nan(f64::NAN));
}
#[test]
fn test_global_is_nan_with_finite() {
assert!(!global_is_nan(0.0));
assert!(!global_is_nan(42.0));
assert!(!global_is_nan(-1.5));
}
#[test]
fn test_global_is_nan_with_infinity() {
assert!(!global_is_nan(f64::INFINITY));
assert!(!global_is_nan(f64::NEG_INFINITY));
}
#[test]
fn test_global_is_nan_coercion_via_parse() {
let coerced = "abc".parse::<f64>().unwrap_or(f64::NAN);
assert!(global_is_nan(coerced));
let coerced_num = "1".parse::<f64>().unwrap_or(f64::NAN);
assert!(!global_is_nan(coerced_num));
}
#[test]
fn test_global_is_finite_normal() {
assert!(global_is_finite(0.0));
assert!(global_is_finite(42.0));
assert!(global_is_finite(-1.5));
}
#[test]
fn test_global_is_finite_special() {
assert!(!global_is_finite(f64::NAN));
assert!(!global_is_finite(f64::INFINITY));
assert!(!global_is_finite(f64::NEG_INFINITY));
}
#[test]
fn test_global_is_finite_coercion_via_parse() {
let coerced = "1".parse::<f64>().unwrap_or(f64::NAN);
assert!(global_is_finite(coerced));
let coerced_nan = "abc".parse::<f64>().unwrap_or(f64::NAN);
assert!(!global_is_finite(coerced_nan));
}
#[test]
fn test_global_parse_int_decimal() {
assert_eq!(global_parse_int("42", 10), 42.0);
assert_eq!(global_parse_int("-7", 10), -7.0);
assert_eq!(global_parse_int("0", 10), 0.0);
}
#[test]
fn test_global_parse_int_hex_prefix() {
assert_eq!(global_parse_int("0xff", 0), 255.0);
assert_eq!(global_parse_int("0xFF", 0), 255.0);
}
#[test]
fn test_global_parse_int_radix() {
assert_eq!(global_parse_int("10", 2), 2.0);
assert_eq!(global_parse_int("ff", 16), 255.0);
assert_eq!(global_parse_int("z", 36), 35.0);
}
#[test]
fn test_global_parse_int_invalid() {
assert!(global_parse_int("xyz", 10).is_nan());
assert!(global_parse_int("", 10).is_nan());
}
#[test]
fn test_global_parse_int_leading_whitespace() {
assert_eq!(global_parse_int(" 42 ", 10), 42.0);
}
#[test]
fn test_global_parse_int_partial_match() {
assert_eq!(global_parse_int("123abc", 10), 123.0);
}
#[test]
fn test_global_parse_float_normal() {
assert_eq!(global_parse_float("3.14"), 3.14);
assert_eq!(global_parse_float("-2.5"), -2.5);
assert_eq!(global_parse_float("1e10"), 1e10);
}
#[test]
fn test_global_parse_float_whitespace() {
assert_eq!(global_parse_float(" 1.5 "), 1.5);
}
#[test]
fn test_global_parse_float_invalid() {
assert!(global_parse_float("abc").is_nan());
assert!(global_parse_float("").is_nan());
}
#[test]
fn test_global_parse_float_infinity() {
assert_eq!(global_parse_float("Infinity"), f64::INFINITY);
assert_eq!(global_parse_float("-Infinity"), f64::NEG_INFINITY);
}
#[test]
fn test_encode_uri_preserves_unreserved() {
assert_eq!(global_encode_uri("abcXYZ0-_.!~*'()"), "abcXYZ0-_.!~*'()");
}
#[test]
fn test_encode_uri_preserves_reserved() {
assert_eq!(global_encode_uri(";,/?:@&=+$#"), ";,/?:@&=+$#");
}
#[test]
fn test_encode_uri_encodes_space() {
assert_eq!(global_encode_uri("hello world"), "hello%20world");
}
#[test]
fn test_encode_uri_full_url() {
assert_eq!(
global_encode_uri("https://example.com/path?q=hello world"),
"https://example.com/path?q=hello%20world"
);
}
#[test]
fn test_encode_uri_non_ascii() {
assert_eq!(global_encode_uri("caf\u{00E9}"), "caf%C3%A9");
}
#[test]
fn test_encode_uri_empty() {
assert_eq!(global_encode_uri(""), "");
}
#[test]
fn test_decode_uri_space() {
assert_eq!(global_decode_uri("hello%20world").unwrap(), "hello world");
}
#[test]
fn test_decode_uri_preserves_reserved_sequences() {
assert_eq!(global_decode_uri("a%2Fb").unwrap(), "a%2Fb");
}
#[test]
fn test_decode_uri_non_ascii() {
assert_eq!(global_decode_uri("caf%C3%A9").unwrap(), "caf\u{00E9}");
}
#[test]
fn test_decode_uri_passthrough() {
assert_eq!(
global_decode_uri("https://example.com/path").unwrap(),
"https://example.com/path"
);
}
#[test]
fn test_decode_uri_malformed_sequence() {
assert!(global_decode_uri("%GG").is_err());
assert!(global_decode_uri("%2").is_err());
}
#[test]
fn test_encode_uri_component_preserves_unreserved() {
assert_eq!(
global_encode_uri_component("abcABC0-_.!~*'()"),
"abcABC0-_.!~*'()"
);
}
#[test]
fn test_encode_uri_component_encodes_reserved() {
assert_eq!(global_encode_uri_component("a=1&b=2"), "a%3D1%26b%3D2");
}
#[test]
fn test_encode_uri_component_encodes_slash() {
assert_eq!(global_encode_uri_component("foo/bar"), "foo%2Fbar");
}
#[test]
fn test_encode_uri_component_space() {
assert_eq!(global_encode_uri_component("hello world"), "hello%20world");
}
#[test]
fn test_encode_uri_component_non_ascii() {
assert_eq!(global_encode_uri_component("caf\u{00E9}"), "caf%C3%A9");
}
#[test]
fn test_encode_uri_component_empty() {
assert_eq!(global_encode_uri_component(""), "");
}
#[test]
fn test_decode_uri_component_space() {
assert_eq!(
global_decode_uri_component("hello%20world").unwrap(),
"hello world"
);
}
#[test]
fn test_decode_uri_component_decodes_all() {
assert_eq!(
global_decode_uri_component("a%3D1%26b%3D2").unwrap(),
"a=1&b=2"
);
}
#[test]
fn test_decode_uri_component_slash() {
assert_eq!(global_decode_uri_component("foo%2Fbar").unwrap(), "foo/bar");
}
#[test]
fn test_decode_uri_component_non_ascii() {
assert_eq!(
global_decode_uri_component("caf%C3%A9").unwrap(),
"caf\u{00E9}"
);
}
#[test]
fn test_decode_uri_component_malformed() {
assert!(global_decode_uri_component("%GG").is_err());
assert!(global_decode_uri_component("%2").is_err());
}
#[test]
fn test_global_eval_arithmetic() {
let result = global_eval("1 + 2").unwrap();
assert_eq!(result, JsValue::Smi(3));
}
#[test]
fn test_global_eval_number_expression() {
let result = global_eval("40 + 2").unwrap();
assert_eq!(result, JsValue::Smi(42));
}
#[test]
fn test_global_eval_boolean() {
let result = global_eval("true").unwrap();
assert_eq!(result, JsValue::Boolean(true));
}
#[test]
fn test_global_eval_complex_expression() {
let result = global_eval("(function() { var x = 10; return x * 2; })()").unwrap();
assert_eq!(result, JsValue::Smi(20));
}
#[test]
fn test_global_eval_syntax_error() {
let err = global_eval("(((").unwrap_err();
assert!(matches!(err, StatorError::SyntaxError(_)));
}
#[test]
fn test_global_eval_returns_undefined_for_empty() {
let result = global_eval("").unwrap();
assert_eq!(result, JsValue::Undefined);
}
#[test]
fn test_eval_direct_shares_caller_env() {
let mut ge = GlobalEnv::new();
crate::builtins::install_globals::install_globals(&mut ge.vars);
let env = Rc::new(RefCell::new(ge));
let result = global_eval_direct("var x = 99; x", Rc::clone(&env)).unwrap();
assert_eq!(result, JsValue::Smi(99));
assert!(env.borrow().contains_key("x"));
}
#[test]
fn test_eval_direct_reads_caller_vars() {
let mut ge = GlobalEnv::new();
crate::builtins::install_globals::install_globals(&mut ge.vars);
ge.insert("y".into(), JsValue::Smi(7));
let env = Rc::new(RefCell::new(ge));
let result = global_eval_direct("y + 3", Rc::clone(&env)).unwrap();
assert_eq!(result, JsValue::Smi(10));
}
#[test]
fn test_eval_indirect_fresh_scope() {
let result = global_eval_indirect("var z = 42; z").unwrap();
assert_eq!(result, JsValue::Smi(42));
}
#[test]
fn test_eval_indirect_arithmetic() {
let result = global_eval_indirect("2 + 3").unwrap();
assert_eq!(result, JsValue::Smi(5));
}
#[test]
fn test_eval_strict_isolates_vars() {
let mut ge = GlobalEnv::new();
crate::builtins::install_globals::install_globals(&mut ge.vars);
let env = Rc::new(RefCell::new(ge));
let result = global_eval_strict("var secret = 100; secret", Rc::clone(&env)).unwrap();
assert_eq!(result, JsValue::Smi(100));
assert!(!env.borrow().contains_key("secret"));
}
#[test]
fn test_eval_mode_debug_repr() {
assert_eq!(EvalMode::Direct, EvalMode::Direct);
assert_ne!(EvalMode::Direct, EvalMode::Indirect);
}
#[test]
fn test_escape_ascii_safe() {
assert_eq!(global_escape("abc"), "abc");
assert_eq!(global_escape("ABC"), "ABC");
assert_eq!(global_escape("019"), "019");
assert_eq!(global_escape("@*_+-./"), "@*_+-./");
}
#[test]
fn test_escape_space() {
assert_eq!(global_escape("hello world"), "hello%20world");
}
#[test]
fn test_escape_latin1() {
assert_eq!(global_escape("\u{00A9}"), "%A9");
}
#[test]
fn test_escape_empty() {
assert_eq!(global_escape(""), "");
}
#[test]
fn test_unescape_percent_xx() {
assert_eq!(global_unescape("hello%20world"), "hello world");
}
#[test]
fn test_unescape_percent_u() {
assert_eq!(global_unescape("%u00A9"), "\u{00A9}");
}
#[test]
fn test_unescape_passthrough() {
assert_eq!(global_unescape("abc"), "abc");
}
#[test]
fn test_escape_unescape_roundtrip() {
let original = "hello world ©";
assert_eq!(global_unescape(&global_escape(original)), original);
}
}