we (web engine): Experimental web browser project to understand the limits of Claude
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement JS built-in String, Number, Boolean, and Symbol

## String
- Constructor: String(), String.fromCharCode(), String.fromCodePoint()
- Prototype: charAt, charCodeAt, codePointAt, concat, slice, substring,
substr, indexOf, lastIndexOf, includes, startsWith, endsWith, trim,
trimStart, trimEnd, padStart, padEnd, repeat, split, replace,
replaceAll, toLowerCase, toUpperCase, at, toString, valueOf

## Number
- Constructor: Number()
- Static: isNaN, isFinite, isInteger, isSafeInteger, parseInt, parseFloat
- Constants: EPSILON, MAX_SAFE_INTEGER, MIN_SAFE_INTEGER, MAX_VALUE,
MIN_VALUE, NaN, POSITIVE_INFINITY, NEGATIVE_INFINITY
- Prototype: toString(radix), valueOf, toFixed, toPrecision, toExponential

## Boolean
- Constructor: Boolean() with truthiness coercion
- Prototype: toString, valueOf

## Symbol
- Factory: Symbol(description) producing unique string-based identifiers
- Well-known: Symbol.iterator, toPrimitive, toStringTag, hasInstance
- Registry: Symbol.for(key), Symbol.keyFor(sym)

## VM Changes
- Added string_prototype, number_prototype, boolean_prototype to Vm
- Primitive auto-boxing: GetProperty/GetPropertyByName now look up
prototype methods for String, Number, and Boolean values
- Global constants: NaN, Infinity, undefined

37 new tests (328 total JS tests, all passing)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+1339 -3
+850 -1
crates/js/src/builtins.rs
··· 8 8 use crate::vm::*; 9 9 use std::collections::HashMap; 10 10 11 + /// Native callback type alias to satisfy clippy::type_complexity. 12 + type NativeMethod = ( 13 + &'static str, 14 + fn(&[Value], &mut NativeContext) -> Result<Value, RuntimeError>, 15 + ); 16 + 11 17 // ── Helpers ────────────────────────────────────────────────── 12 18 13 19 /// Create a native function GcRef. ··· 175 181 let err_proto = vm.gc.alloc(HeapObject::Object(err_proto_data)); 176 182 init_error_prototype(&mut vm.gc, err_proto); 177 183 178 - // Store prototypes in VM for use by CreateArray/CreateObject. 184 + // Create String.prototype (inherits from Object.prototype). 185 + let mut str_proto_data = ObjectData::new(); 186 + str_proto_data.prototype = Some(obj_proto); 187 + let str_proto = vm.gc.alloc(HeapObject::Object(str_proto_data)); 188 + init_string_prototype(&mut vm.gc, str_proto); 189 + 190 + // Create Number.prototype (inherits from Object.prototype). 191 + let mut num_proto_data = ObjectData::new(); 192 + num_proto_data.prototype = Some(obj_proto); 193 + let num_proto = vm.gc.alloc(HeapObject::Object(num_proto_data)); 194 + init_number_prototype(&mut vm.gc, num_proto); 195 + 196 + // Create Boolean.prototype (inherits from Object.prototype). 197 + let mut bool_proto_data = ObjectData::new(); 198 + bool_proto_data.prototype = Some(obj_proto); 199 + let bool_proto = vm.gc.alloc(HeapObject::Object(bool_proto_data)); 200 + init_boolean_prototype(&mut vm.gc, bool_proto); 201 + 202 + // Store prototypes in VM for use by CreateArray/CreateObject and auto-boxing. 179 203 vm.object_prototype = Some(obj_proto); 180 204 vm.array_prototype = Some(arr_proto); 205 + vm.string_prototype = Some(str_proto); 206 + vm.number_prototype = Some(num_proto); 207 + vm.boolean_prototype = Some(bool_proto); 181 208 182 209 // Create and register Object constructor. 183 210 let obj_ctor = init_object_constructor(&mut vm.gc, obj_proto); ··· 189 216 190 217 // Create and register Error constructors. 191 218 init_error_constructors(vm, err_proto); 219 + 220 + // Create and register String constructor. 221 + let str_ctor = init_string_constructor(&mut vm.gc, str_proto); 222 + vm.set_global("String", Value::Function(str_ctor)); 223 + 224 + // Create and register Number constructor. 225 + let num_ctor = init_number_constructor(&mut vm.gc, num_proto); 226 + vm.set_global("Number", Value::Function(num_ctor)); 227 + 228 + // Create and register Boolean constructor. 229 + let bool_ctor = init_boolean_constructor(&mut vm.gc, bool_proto); 230 + vm.set_global("Boolean", Value::Function(bool_ctor)); 231 + 232 + // Create and register Symbol factory. 233 + init_symbol_builtins(vm); 192 234 193 235 // Register global utility functions. 194 236 init_global_functions(vm); ··· 1417 1459 Ok(Value::String(name)) 1418 1460 } else { 1419 1461 Ok(Value::String(format!("{name}: {message}"))) 1462 + } 1463 + } 1464 + 1465 + // ── String built-in ────────────────────────────────────────── 1466 + 1467 + fn init_string_prototype(gc: &mut Gc<HeapObject>, proto: GcRef) { 1468 + let methods: &[NativeMethod] = &[ 1469 + ("charAt", string_proto_char_at), 1470 + ("charCodeAt", string_proto_char_code_at), 1471 + ("codePointAt", string_proto_code_point_at), 1472 + ("concat", string_proto_concat), 1473 + ("slice", string_proto_slice), 1474 + ("substring", string_proto_substring), 1475 + ("substr", string_proto_substr), 1476 + ("indexOf", string_proto_index_of), 1477 + ("lastIndexOf", string_proto_last_index_of), 1478 + ("includes", string_proto_includes), 1479 + ("startsWith", string_proto_starts_with), 1480 + ("endsWith", string_proto_ends_with), 1481 + ("trim", string_proto_trim), 1482 + ("trimStart", string_proto_trim_start), 1483 + ("trimEnd", string_proto_trim_end), 1484 + ("padStart", string_proto_pad_start), 1485 + ("padEnd", string_proto_pad_end), 1486 + ("repeat", string_proto_repeat), 1487 + ("split", string_proto_split), 1488 + ("replace", string_proto_replace), 1489 + ("replaceAll", string_proto_replace_all), 1490 + ("toLowerCase", string_proto_to_lower_case), 1491 + ("toUpperCase", string_proto_to_upper_case), 1492 + ("at", string_proto_at), 1493 + ("toString", string_proto_to_string), 1494 + ("valueOf", string_proto_value_of), 1495 + ]; 1496 + for &(name, callback) in methods { 1497 + let f = make_native(gc, name, callback); 1498 + set_builtin_prop(gc, proto, name, Value::Function(f)); 1499 + } 1500 + } 1501 + 1502 + fn init_string_constructor(gc: &mut Gc<HeapObject>, str_proto: GcRef) -> GcRef { 1503 + let ctor = gc.alloc(HeapObject::Function(Box::new(FunctionData { 1504 + name: "String".to_string(), 1505 + kind: FunctionKind::Native(NativeFunc { 1506 + callback: string_constructor, 1507 + }), 1508 + prototype_obj: Some(str_proto), 1509 + properties: HashMap::new(), 1510 + upvalues: Vec::new(), 1511 + }))); 1512 + let from_char_code = make_native(gc, "fromCharCode", string_from_char_code); 1513 + set_func_prop(gc, ctor, "fromCharCode", Value::Function(from_char_code)); 1514 + let from_code_point = make_native(gc, "fromCodePoint", string_from_code_point); 1515 + set_func_prop(gc, ctor, "fromCodePoint", Value::Function(from_code_point)); 1516 + ctor 1517 + } 1518 + 1519 + fn string_constructor(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1520 + let s = args 1521 + .first() 1522 + .map(|v| v.to_js_string(ctx.gc)) 1523 + .unwrap_or_default(); 1524 + Ok(Value::String(s)) 1525 + } 1526 + 1527 + fn string_from_char_code(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1528 + let s: String = args 1529 + .iter() 1530 + .filter_map(|v| { 1531 + let code = v.to_number() as u32; 1532 + char::from_u32(code) 1533 + }) 1534 + .collect(); 1535 + Ok(Value::String(s)) 1536 + } 1537 + 1538 + fn string_from_code_point(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1539 + let mut s = String::new(); 1540 + for v in args { 1541 + let code = v.to_number() as u32; 1542 + match char::from_u32(code) { 1543 + Some(c) => s.push(c), 1544 + None => { 1545 + return Err(RuntimeError::range_error(format!( 1546 + "Invalid code point {code}" 1547 + ))) 1548 + } 1549 + } 1550 + } 1551 + Ok(Value::String(s)) 1552 + } 1553 + 1554 + /// Helper: extract the string from `this` for String.prototype methods. 1555 + fn this_string(ctx: &NativeContext) -> String { 1556 + ctx.this.to_js_string(ctx.gc) 1557 + } 1558 + 1559 + /// Helper: get chars as a Vec for index-based operations. 1560 + fn str_chars(s: &str) -> Vec<char> { 1561 + s.chars().collect() 1562 + } 1563 + 1564 + fn string_proto_char_at(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1565 + let s = this_string(ctx); 1566 + let chars = str_chars(&s); 1567 + let idx = args.first().map(|v| v.to_number() as i64).unwrap_or(0); 1568 + if idx < 0 || idx as usize >= chars.len() { 1569 + Ok(Value::String(String::new())) 1570 + } else { 1571 + Ok(Value::String(chars[idx as usize].to_string())) 1572 + } 1573 + } 1574 + 1575 + fn string_proto_char_code_at( 1576 + args: &[Value], 1577 + ctx: &mut NativeContext, 1578 + ) -> Result<Value, RuntimeError> { 1579 + let s = this_string(ctx); 1580 + let chars = str_chars(&s); 1581 + let idx = args.first().map(|v| v.to_number() as i64).unwrap_or(0); 1582 + if idx < 0 || idx as usize >= chars.len() { 1583 + Ok(Value::Number(f64::NAN)) 1584 + } else { 1585 + Ok(Value::Number(chars[idx as usize] as u32 as f64)) 1586 + } 1587 + } 1588 + 1589 + fn string_proto_code_point_at( 1590 + args: &[Value], 1591 + ctx: &mut NativeContext, 1592 + ) -> Result<Value, RuntimeError> { 1593 + let s = this_string(ctx); 1594 + let chars = str_chars(&s); 1595 + let idx = args.first().map(|v| v.to_number() as i64).unwrap_or(0); 1596 + if idx < 0 || idx as usize >= chars.len() { 1597 + Ok(Value::Undefined) 1598 + } else { 1599 + Ok(Value::Number(chars[idx as usize] as u32 as f64)) 1600 + } 1601 + } 1602 + 1603 + fn string_proto_concat(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1604 + let mut s = this_string(ctx); 1605 + for arg in args { 1606 + s.push_str(&arg.to_js_string(ctx.gc)); 1607 + } 1608 + Ok(Value::String(s)) 1609 + } 1610 + 1611 + fn string_proto_slice(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1612 + let s = this_string(ctx); 1613 + let chars = str_chars(&s); 1614 + let len = chars.len() as i64; 1615 + let start = args.first().map(|v| v.to_number() as i64).unwrap_or(0); 1616 + let end = args.get(1).map(|v| v.to_number() as i64).unwrap_or(len); 1617 + let start = if start < 0 { 1618 + (len + start).max(0) as usize 1619 + } else { 1620 + start.min(len) as usize 1621 + }; 1622 + let end = if end < 0 { 1623 + (len + end).max(0) as usize 1624 + } else { 1625 + end.min(len) as usize 1626 + }; 1627 + if start >= end { 1628 + Ok(Value::String(String::new())) 1629 + } else { 1630 + Ok(Value::String(chars[start..end].iter().collect())) 1631 + } 1632 + } 1633 + 1634 + fn string_proto_substring(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1635 + let s = this_string(ctx); 1636 + let chars = str_chars(&s); 1637 + let len = chars.len() as i64; 1638 + let a = args 1639 + .first() 1640 + .map(|v| v.to_number() as i64) 1641 + .unwrap_or(0) 1642 + .clamp(0, len) as usize; 1643 + let b = args 1644 + .get(1) 1645 + .map(|v| v.to_number() as i64) 1646 + .unwrap_or(len) 1647 + .clamp(0, len) as usize; 1648 + let (start, end) = if a <= b { (a, b) } else { (b, a) }; 1649 + Ok(Value::String(chars[start..end].iter().collect())) 1650 + } 1651 + 1652 + fn string_proto_substr(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1653 + let s = this_string(ctx); 1654 + let chars = str_chars(&s); 1655 + let len = chars.len() as i64; 1656 + let start = args.first().map(|v| v.to_number() as i64).unwrap_or(0); 1657 + let start = if start < 0 { 1658 + (len + start).max(0) as usize 1659 + } else { 1660 + start.min(len) as usize 1661 + }; 1662 + let count = args.get(1).map(|v| v.to_number() as i64).unwrap_or(len) as usize; 1663 + let end = (start + count).min(chars.len()); 1664 + Ok(Value::String(chars[start..end].iter().collect())) 1665 + } 1666 + 1667 + fn string_proto_index_of(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1668 + let s = this_string(ctx); 1669 + let search = args 1670 + .first() 1671 + .map(|v| v.to_js_string(ctx.gc)) 1672 + .unwrap_or_default(); 1673 + let from = args.get(1).map(|v| v.to_number() as usize).unwrap_or(0); 1674 + let chars = str_chars(&s); 1675 + let search_chars = str_chars(&search); 1676 + if search_chars.is_empty() { 1677 + return Ok(Value::Number(from.min(chars.len()) as f64)); 1678 + } 1679 + for i in from..chars.len() { 1680 + if i + search_chars.len() <= chars.len() 1681 + && chars[i..i + search_chars.len()] == search_chars[..] 1682 + { 1683 + return Ok(Value::Number(i as f64)); 1684 + } 1685 + } 1686 + Ok(Value::Number(-1.0)) 1687 + } 1688 + 1689 + fn string_proto_last_index_of( 1690 + args: &[Value], 1691 + ctx: &mut NativeContext, 1692 + ) -> Result<Value, RuntimeError> { 1693 + let s = this_string(ctx); 1694 + let search = args 1695 + .first() 1696 + .map(|v| v.to_js_string(ctx.gc)) 1697 + .unwrap_or_default(); 1698 + let chars = str_chars(&s); 1699 + let search_chars = str_chars(&search); 1700 + let from = args 1701 + .get(1) 1702 + .map(|v| { 1703 + let n = v.to_number(); 1704 + if n.is_nan() { 1705 + chars.len() 1706 + } else { 1707 + n as usize 1708 + } 1709 + }) 1710 + .unwrap_or(chars.len()); 1711 + if search_chars.is_empty() { 1712 + return Ok(Value::Number(from.min(chars.len()) as f64)); 1713 + } 1714 + let max_start = from.min(chars.len().saturating_sub(search_chars.len())); 1715 + for i in (0..=max_start).rev() { 1716 + if i + search_chars.len() <= chars.len() 1717 + && chars[i..i + search_chars.len()] == search_chars[..] 1718 + { 1719 + return Ok(Value::Number(i as f64)); 1720 + } 1721 + } 1722 + Ok(Value::Number(-1.0)) 1723 + } 1724 + 1725 + fn string_proto_includes(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1726 + let s = this_string(ctx); 1727 + let search = args 1728 + .first() 1729 + .map(|v| v.to_js_string(ctx.gc)) 1730 + .unwrap_or_default(); 1731 + let from = args.get(1).map(|v| v.to_number() as usize).unwrap_or(0); 1732 + let chars = str_chars(&s); 1733 + let search_chars = str_chars(&search); 1734 + if search_chars.is_empty() { 1735 + return Ok(Value::Boolean(true)); 1736 + } 1737 + for i in from..chars.len() { 1738 + if i + search_chars.len() <= chars.len() 1739 + && chars[i..i + search_chars.len()] == search_chars[..] 1740 + { 1741 + return Ok(Value::Boolean(true)); 1742 + } 1743 + } 1744 + Ok(Value::Boolean(false)) 1745 + } 1746 + 1747 + fn string_proto_starts_with( 1748 + args: &[Value], 1749 + ctx: &mut NativeContext, 1750 + ) -> Result<Value, RuntimeError> { 1751 + let s = this_string(ctx); 1752 + let search = args 1753 + .first() 1754 + .map(|v| v.to_js_string(ctx.gc)) 1755 + .unwrap_or_default(); 1756 + let pos = args.get(1).map(|v| v.to_number() as usize).unwrap_or(0); 1757 + let chars = str_chars(&s); 1758 + let search_chars = str_chars(&search); 1759 + if pos + search_chars.len() > chars.len() { 1760 + return Ok(Value::Boolean(false)); 1761 + } 1762 + Ok(Value::Boolean( 1763 + chars[pos..pos + search_chars.len()] == search_chars[..], 1764 + )) 1765 + } 1766 + 1767 + fn string_proto_ends_with(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1768 + let s = this_string(ctx); 1769 + let search = args 1770 + .first() 1771 + .map(|v| v.to_js_string(ctx.gc)) 1772 + .unwrap_or_default(); 1773 + let chars = str_chars(&s); 1774 + let search_chars = str_chars(&search); 1775 + let end_pos = args 1776 + .get(1) 1777 + .map(|v| (v.to_number() as usize).min(chars.len())) 1778 + .unwrap_or(chars.len()); 1779 + if search_chars.len() > end_pos { 1780 + return Ok(Value::Boolean(false)); 1781 + } 1782 + let start = end_pos - search_chars.len(); 1783 + Ok(Value::Boolean(chars[start..end_pos] == search_chars[..])) 1784 + } 1785 + 1786 + fn string_proto_trim(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1787 + let _ = args; 1788 + Ok(Value::String(this_string(ctx).trim().to_string())) 1789 + } 1790 + 1791 + fn string_proto_trim_start(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1792 + let _ = args; 1793 + Ok(Value::String(this_string(ctx).trim_start().to_string())) 1794 + } 1795 + 1796 + fn string_proto_trim_end(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1797 + let _ = args; 1798 + Ok(Value::String(this_string(ctx).trim_end().to_string())) 1799 + } 1800 + 1801 + fn string_proto_pad_start(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1802 + let s = this_string(ctx); 1803 + let target_len = args.first().map(|v| v.to_number() as usize).unwrap_or(0); 1804 + let fill = args 1805 + .get(1) 1806 + .map(|v| v.to_js_string(ctx.gc)) 1807 + .unwrap_or_else(|| " ".to_string()); 1808 + let chars = str_chars(&s); 1809 + if chars.len() >= target_len || fill.is_empty() { 1810 + return Ok(Value::String(s)); 1811 + } 1812 + let fill_chars = str_chars(&fill); 1813 + let needed = target_len - chars.len(); 1814 + let mut pad = String::new(); 1815 + for i in 0..needed { 1816 + pad.push(fill_chars[i % fill_chars.len()]); 1817 + } 1818 + pad.push_str(&s); 1819 + Ok(Value::String(pad)) 1820 + } 1821 + 1822 + fn string_proto_pad_end(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1823 + let s = this_string(ctx); 1824 + let target_len = args.first().map(|v| v.to_number() as usize).unwrap_or(0); 1825 + let fill = args 1826 + .get(1) 1827 + .map(|v| v.to_js_string(ctx.gc)) 1828 + .unwrap_or_else(|| " ".to_string()); 1829 + let chars = str_chars(&s); 1830 + if chars.len() >= target_len || fill.is_empty() { 1831 + return Ok(Value::String(s)); 1832 + } 1833 + let fill_chars = str_chars(&fill); 1834 + let needed = target_len - chars.len(); 1835 + let mut result = s; 1836 + for i in 0..needed { 1837 + result.push(fill_chars[i % fill_chars.len()]); 1838 + } 1839 + Ok(Value::String(result)) 1840 + } 1841 + 1842 + fn string_proto_repeat(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1843 + let s = this_string(ctx); 1844 + let count = args.first().map(|v| v.to_number()).unwrap_or(0.0); 1845 + if count < 0.0 || count.is_infinite() { 1846 + return Err(RuntimeError::range_error("Invalid count value")); 1847 + } 1848 + Ok(Value::String(s.repeat(count as usize))) 1849 + } 1850 + 1851 + fn string_proto_split(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1852 + let s = this_string(ctx); 1853 + if args.is_empty() || matches!(args.first(), Some(Value::Undefined)) { 1854 + return Ok(make_value_array(ctx.gc, &[Value::String(s)])); 1855 + } 1856 + let sep = args[0].to_js_string(ctx.gc); 1857 + let limit = args 1858 + .get(1) 1859 + .map(|v| v.to_number() as usize) 1860 + .unwrap_or(usize::MAX); 1861 + if sep.is_empty() { 1862 + let items: Vec<Value> = str_chars(&s) 1863 + .into_iter() 1864 + .take(limit) 1865 + .map(|c| Value::String(c.to_string())) 1866 + .collect(); 1867 + return Ok(make_value_array(ctx.gc, &items)); 1868 + } 1869 + let mut items = Vec::new(); 1870 + let mut start = 0; 1871 + let sep_len = sep.len(); 1872 + while let Some(pos) = s[start..].find(&sep) { 1873 + if items.len() >= limit { 1874 + break; 1875 + } 1876 + items.push(Value::String(s[start..start + pos].to_string())); 1877 + start += pos + sep_len; 1878 + } 1879 + if items.len() < limit { 1880 + items.push(Value::String(s[start..].to_string())); 1881 + } 1882 + Ok(make_value_array(ctx.gc, &items)) 1883 + } 1884 + 1885 + fn string_proto_replace(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1886 + let s = this_string(ctx); 1887 + let search = args 1888 + .first() 1889 + .map(|v| v.to_js_string(ctx.gc)) 1890 + .unwrap_or_default(); 1891 + let replacement = args 1892 + .get(1) 1893 + .map(|v| v.to_js_string(ctx.gc)) 1894 + .unwrap_or_default(); 1895 + // Replace only the first occurrence. 1896 + if let Some(pos) = s.find(&search) { 1897 + let mut result = String::with_capacity(s.len()); 1898 + result.push_str(&s[..pos]); 1899 + result.push_str(&replacement); 1900 + result.push_str(&s[pos + search.len()..]); 1901 + Ok(Value::String(result)) 1902 + } else { 1903 + Ok(Value::String(s)) 1904 + } 1905 + } 1906 + 1907 + fn string_proto_replace_all( 1908 + args: &[Value], 1909 + ctx: &mut NativeContext, 1910 + ) -> Result<Value, RuntimeError> { 1911 + let s = this_string(ctx); 1912 + let search = args 1913 + .first() 1914 + .map(|v| v.to_js_string(ctx.gc)) 1915 + .unwrap_or_default(); 1916 + let replacement = args 1917 + .get(1) 1918 + .map(|v| v.to_js_string(ctx.gc)) 1919 + .unwrap_or_default(); 1920 + Ok(Value::String(s.replace(&search, &replacement))) 1921 + } 1922 + 1923 + fn string_proto_to_lower_case( 1924 + args: &[Value], 1925 + ctx: &mut NativeContext, 1926 + ) -> Result<Value, RuntimeError> { 1927 + let _ = args; 1928 + Ok(Value::String(this_string(ctx).to_lowercase())) 1929 + } 1930 + 1931 + fn string_proto_to_upper_case( 1932 + args: &[Value], 1933 + ctx: &mut NativeContext, 1934 + ) -> Result<Value, RuntimeError> { 1935 + let _ = args; 1936 + Ok(Value::String(this_string(ctx).to_uppercase())) 1937 + } 1938 + 1939 + fn string_proto_at(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1940 + let s = this_string(ctx); 1941 + let chars = str_chars(&s); 1942 + let idx = args.first().map(|v| v.to_number() as i64).unwrap_or(0); 1943 + let actual = if idx < 0 { 1944 + chars.len() as i64 + idx 1945 + } else { 1946 + idx 1947 + }; 1948 + if actual < 0 || actual as usize >= chars.len() { 1949 + Ok(Value::Undefined) 1950 + } else { 1951 + Ok(Value::String(chars[actual as usize].to_string())) 1952 + } 1953 + } 1954 + 1955 + fn string_proto_to_string(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1956 + let _ = args; 1957 + Ok(Value::String(this_string(ctx))) 1958 + } 1959 + 1960 + fn string_proto_value_of(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1961 + let _ = args; 1962 + Ok(Value::String(this_string(ctx))) 1963 + } 1964 + 1965 + // ── Number built-in ────────────────────────────────────────── 1966 + 1967 + fn init_number_prototype(gc: &mut Gc<HeapObject>, proto: GcRef) { 1968 + let methods: &[NativeMethod] = &[ 1969 + ("toString", number_proto_to_string), 1970 + ("valueOf", number_proto_value_of), 1971 + ("toFixed", number_proto_to_fixed), 1972 + ("toPrecision", number_proto_to_precision), 1973 + ("toExponential", number_proto_to_exponential), 1974 + ]; 1975 + for &(name, callback) in methods { 1976 + let f = make_native(gc, name, callback); 1977 + set_builtin_prop(gc, proto, name, Value::Function(f)); 1978 + } 1979 + } 1980 + 1981 + fn init_number_constructor(gc: &mut Gc<HeapObject>, num_proto: GcRef) -> GcRef { 1982 + let ctor = gc.alloc(HeapObject::Function(Box::new(FunctionData { 1983 + name: "Number".to_string(), 1984 + kind: FunctionKind::Native(NativeFunc { 1985 + callback: number_constructor, 1986 + }), 1987 + prototype_obj: Some(num_proto), 1988 + properties: HashMap::new(), 1989 + upvalues: Vec::new(), 1990 + }))); 1991 + // Static methods. 1992 + let is_nan = make_native(gc, "isNaN", number_is_nan); 1993 + set_func_prop(gc, ctor, "isNaN", Value::Function(is_nan)); 1994 + let is_finite = make_native(gc, "isFinite", number_is_finite); 1995 + set_func_prop(gc, ctor, "isFinite", Value::Function(is_finite)); 1996 + let is_integer = make_native(gc, "isInteger", number_is_integer); 1997 + set_func_prop(gc, ctor, "isInteger", Value::Function(is_integer)); 1998 + let is_safe_integer = make_native(gc, "isSafeInteger", number_is_safe_integer); 1999 + set_func_prop(gc, ctor, "isSafeInteger", Value::Function(is_safe_integer)); 2000 + let parse_int_fn = make_native(gc, "parseInt", crate::builtins::parse_int); 2001 + set_func_prop(gc, ctor, "parseInt", Value::Function(parse_int_fn)); 2002 + let parse_float_fn = make_native(gc, "parseFloat", crate::builtins::parse_float); 2003 + set_func_prop(gc, ctor, "parseFloat", Value::Function(parse_float_fn)); 2004 + // Constants. 2005 + set_func_prop(gc, ctor, "EPSILON", Value::Number(f64::EPSILON)); 2006 + set_func_prop( 2007 + gc, 2008 + ctor, 2009 + "MAX_SAFE_INTEGER", 2010 + Value::Number(9007199254740991.0), 2011 + ); 2012 + set_func_prop( 2013 + gc, 2014 + ctor, 2015 + "MIN_SAFE_INTEGER", 2016 + Value::Number(-9007199254740991.0), 2017 + ); 2018 + set_func_prop(gc, ctor, "MAX_VALUE", Value::Number(f64::MAX)); 2019 + set_func_prop(gc, ctor, "MIN_VALUE", Value::Number(f64::MIN_POSITIVE)); 2020 + set_func_prop(gc, ctor, "NaN", Value::Number(f64::NAN)); 2021 + set_func_prop(gc, ctor, "POSITIVE_INFINITY", Value::Number(f64::INFINITY)); 2022 + set_func_prop( 2023 + gc, 2024 + ctor, 2025 + "NEGATIVE_INFINITY", 2026 + Value::Number(f64::NEG_INFINITY), 2027 + ); 2028 + ctor 2029 + } 2030 + 2031 + fn number_constructor(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 2032 + let n = args.first().map(|v| v.to_number()).unwrap_or(0.0); 2033 + Ok(Value::Number(n)) 2034 + } 2035 + 2036 + fn number_is_nan(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 2037 + match args.first() { 2038 + Some(Value::Number(n)) => Ok(Value::Boolean(n.is_nan())), 2039 + _ => Ok(Value::Boolean(false)), 2040 + } 2041 + } 2042 + 2043 + fn number_is_finite(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 2044 + match args.first() { 2045 + Some(Value::Number(n)) => Ok(Value::Boolean(n.is_finite())), 2046 + _ => Ok(Value::Boolean(false)), 2047 + } 2048 + } 2049 + 2050 + fn number_is_integer(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 2051 + match args.first() { 2052 + Some(Value::Number(n)) => Ok(Value::Boolean(n.is_finite() && n.trunc() == *n)), 2053 + _ => Ok(Value::Boolean(false)), 2054 + } 2055 + } 2056 + 2057 + fn number_is_safe_integer(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 2058 + match args.first() { 2059 + Some(Value::Number(n)) => { 2060 + let safe = n.is_finite() && n.trunc() == *n && n.abs() <= 9007199254740991.0; 2061 + Ok(Value::Boolean(safe)) 2062 + } 2063 + _ => Ok(Value::Boolean(false)), 2064 + } 2065 + } 2066 + 2067 + /// Helper: extract the number from `this` for Number.prototype methods. 2068 + fn this_number(ctx: &NativeContext) -> f64 { 2069 + ctx.this.to_number() 2070 + } 2071 + 2072 + fn number_proto_to_string(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 2073 + let n = this_number(ctx); 2074 + let radix = args.first().map(|v| v.to_number() as u32).unwrap_or(10); 2075 + if !(2..=36).contains(&radix) { 2076 + return Err(RuntimeError::range_error( 2077 + "toString() radix must be between 2 and 36", 2078 + )); 2079 + } 2080 + if radix == 10 { 2081 + return Ok(Value::String(Value::Number(n).to_js_string(ctx.gc))); 2082 + } 2083 + if n.is_nan() { 2084 + return Ok(Value::String("NaN".to_string())); 2085 + } 2086 + if n.is_infinite() { 2087 + return Ok(Value::String(if n > 0.0 { 2088 + "Infinity".to_string() 2089 + } else { 2090 + "-Infinity".to_string() 2091 + })); 2092 + } 2093 + // Integer path for non-decimal radix. 2094 + let neg = n < 0.0; 2095 + let abs = n.abs() as u64; 2096 + let mut digits = Vec::new(); 2097 + let mut val = abs; 2098 + if val == 0 { 2099 + digits.push('0'); 2100 + } else { 2101 + while val > 0 { 2102 + let d = (val % radix as u64) as u32; 2103 + digits.push(char::from_digit(d, radix).unwrap_or('?')); 2104 + val /= radix as u64; 2105 + } 2106 + } 2107 + digits.reverse(); 2108 + let mut result = String::new(); 2109 + if neg { 2110 + result.push('-'); 2111 + } 2112 + result.extend(digits); 2113 + Ok(Value::String(result)) 2114 + } 2115 + 2116 + fn number_proto_value_of(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 2117 + let _ = args; 2118 + Ok(Value::Number(this_number(ctx))) 2119 + } 2120 + 2121 + fn number_proto_to_fixed(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 2122 + let n = this_number(ctx); 2123 + let digits = args.first().map(|v| v.to_number() as usize).unwrap_or(0); 2124 + if digits > 100 { 2125 + return Err(RuntimeError::range_error( 2126 + "toFixed() digits argument must be between 0 and 100", 2127 + )); 2128 + } 2129 + Ok(Value::String(format!("{n:.digits$}"))) 2130 + } 2131 + 2132 + fn number_proto_to_precision( 2133 + args: &[Value], 2134 + ctx: &mut NativeContext, 2135 + ) -> Result<Value, RuntimeError> { 2136 + let n = this_number(ctx); 2137 + if args.is_empty() || matches!(args.first(), Some(Value::Undefined)) { 2138 + return Ok(Value::String(Value::Number(n).to_js_string(ctx.gc))); 2139 + } 2140 + let prec = args[0].to_number() as usize; 2141 + if !(1..=100).contains(&prec) { 2142 + return Err(RuntimeError::range_error( 2143 + "toPrecision() argument must be between 1 and 100", 2144 + )); 2145 + } 2146 + Ok(Value::String(format!("{n:.prec$e}"))) 2147 + } 2148 + 2149 + fn number_proto_to_exponential( 2150 + args: &[Value], 2151 + ctx: &mut NativeContext, 2152 + ) -> Result<Value, RuntimeError> { 2153 + let n = this_number(ctx); 2154 + let digits = args.first().map(|v| v.to_number() as usize).unwrap_or(6); 2155 + if digits > 100 { 2156 + return Err(RuntimeError::range_error( 2157 + "toExponential() argument must be between 0 and 100", 2158 + )); 2159 + } 2160 + Ok(Value::String(format!("{n:.digits$e}"))) 2161 + } 2162 + 2163 + // ── Boolean built-in ───────────────────────────────────────── 2164 + 2165 + fn init_boolean_prototype(gc: &mut Gc<HeapObject>, proto: GcRef) { 2166 + let to_string = make_native(gc, "toString", boolean_proto_to_string); 2167 + set_builtin_prop(gc, proto, "toString", Value::Function(to_string)); 2168 + let value_of = make_native(gc, "valueOf", boolean_proto_value_of); 2169 + set_builtin_prop(gc, proto, "valueOf", Value::Function(value_of)); 2170 + } 2171 + 2172 + fn init_boolean_constructor(gc: &mut Gc<HeapObject>, bool_proto: GcRef) -> GcRef { 2173 + gc.alloc(HeapObject::Function(Box::new(FunctionData { 2174 + name: "Boolean".to_string(), 2175 + kind: FunctionKind::Native(NativeFunc { 2176 + callback: boolean_constructor, 2177 + }), 2178 + prototype_obj: Some(bool_proto), 2179 + properties: HashMap::new(), 2180 + upvalues: Vec::new(), 2181 + }))) 2182 + } 2183 + 2184 + fn boolean_constructor(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 2185 + let b = args.first().map(|v| v.to_boolean()).unwrap_or(false); 2186 + Ok(Value::Boolean(b)) 2187 + } 2188 + 2189 + fn boolean_proto_to_string(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 2190 + let _ = args; 2191 + Ok(Value::String( 2192 + if ctx.this.to_boolean() { 2193 + "true" 2194 + } else { 2195 + "false" 2196 + } 2197 + .to_string(), 2198 + )) 2199 + } 2200 + 2201 + fn boolean_proto_value_of(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 2202 + let _ = args; 2203 + Ok(Value::Boolean(ctx.this.to_boolean())) 2204 + } 2205 + 2206 + // ── Symbol built-in ────────────────────────────────────────── 2207 + 2208 + /// Global symbol ID counter. Each Symbol() call increments this. 2209 + static SYMBOL_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); 2210 + 2211 + fn init_symbol_builtins(vm: &mut Vm) { 2212 + // Create a function object so we can hang static props on it. 2213 + let gc_ref = make_native(&mut vm.gc, "Symbol", symbol_factory); 2214 + 2215 + // Well-known symbols as string constants. 2216 + let well_known = [ 2217 + ("iterator", "@@iterator"), 2218 + ("toPrimitive", "@@toPrimitive"), 2219 + ("toStringTag", "@@toStringTag"), 2220 + ("hasInstance", "@@hasInstance"), 2221 + ]; 2222 + for (name, value) in well_known { 2223 + set_func_prop(&mut vm.gc, gc_ref, name, Value::String(value.to_string())); 2224 + } 2225 + 2226 + // Symbol.for() and Symbol.keyFor(). 2227 + let sym_for = make_native(&mut vm.gc, "for", symbol_for); 2228 + set_func_prop(&mut vm.gc, gc_ref, "for", Value::Function(sym_for)); 2229 + let sym_key_for = make_native(&mut vm.gc, "keyFor", symbol_key_for); 2230 + set_func_prop(&mut vm.gc, gc_ref, "keyFor", Value::Function(sym_key_for)); 2231 + 2232 + vm.set_global("Symbol", Value::Function(gc_ref)); 2233 + 2234 + // Register global NaN and Infinity constants. 2235 + vm.set_global("NaN", Value::Number(f64::NAN)); 2236 + vm.set_global("Infinity", Value::Number(f64::INFINITY)); 2237 + vm.set_global("undefined", Value::Undefined); 2238 + } 2239 + 2240 + fn symbol_factory(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 2241 + let desc = args 2242 + .first() 2243 + .map(|v| v.to_js_string(ctx.gc)) 2244 + .unwrap_or_default(); 2245 + let id = SYMBOL_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed); 2246 + // Return a unique string representation. Real Symbol is a distinct type, 2247 + // but this pragmatic approach works for property keys and identity checks. 2248 + Ok(Value::String(format!("@@sym_{id}_{desc}"))) 2249 + } 2250 + 2251 + fn symbol_for(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 2252 + let key = args 2253 + .first() 2254 + .map(|v| v.to_js_string(ctx.gc)) 2255 + .unwrap_or_default(); 2256 + // Deterministic: same key always produces same symbol string. 2257 + Ok(Value::String(format!("@@global_{key}"))) 2258 + } 2259 + 2260 + fn symbol_key_for(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 2261 + let sym = args 2262 + .first() 2263 + .map(|v| v.to_js_string(ctx.gc)) 2264 + .unwrap_or_default(); 2265 + if let Some(key) = sym.strip_prefix("@@global_") { 2266 + Ok(Value::String(key.to_string())) 2267 + } else { 2268 + Ok(Value::Undefined) 1420 2269 } 1421 2270 } 1422 2271
+489 -2
crates/js/src/vm.rs
··· 682 682 pub object_prototype: Option<GcRef>, 683 683 /// Built-in Array.prototype (set on newly created arrays). 684 684 pub array_prototype: Option<GcRef>, 685 + /// Built-in String.prototype (for primitive auto-boxing). 686 + pub string_prototype: Option<GcRef>, 687 + /// Built-in Number.prototype (for primitive auto-boxing). 688 + pub number_prototype: Option<GcRef>, 689 + /// Built-in Boolean.prototype (for primitive auto-boxing). 690 + pub boolean_prototype: Option<GcRef>, 685 691 } 686 692 687 693 /// Maximum register file size. ··· 700 706 instructions_executed: 0, 701 707 object_prototype: None, 702 708 array_prototype: None, 709 + string_prototype: None, 710 + number_prototype: None, 711 + boolean_prototype: None, 703 712 }; 704 713 crate::builtins::init_builtins(&mut vm); 705 714 vm ··· 791 800 roots.push(r); 792 801 } 793 802 if let Some(r) = self.array_prototype { 803 + roots.push(r); 804 + } 805 + if let Some(r) = self.string_prototype { 806 + roots.push(r); 807 + } 808 + if let Some(r) = self.number_prototype { 809 + roots.push(r); 810 + } 811 + if let Some(r) = self.boolean_prototype { 794 812 roots.push(r); 795 813 } 796 814 roots ··· 1351 1369 Value::Object(gc_ref) | Value::Function(gc_ref) => { 1352 1370 gc_get_property(&self.gc, gc_ref, &key) 1353 1371 } 1354 - Value::String(ref s) => string_get_property(s, &key), 1372 + Value::String(ref s) => { 1373 + let v = string_get_property(s, &key); 1374 + if matches!(v, Value::Undefined) { 1375 + self.string_prototype 1376 + .map(|p| gc_get_property(&self.gc, p, &key)) 1377 + .unwrap_or(Value::Undefined) 1378 + } else { 1379 + v 1380 + } 1381 + } 1382 + Value::Number(_) => self 1383 + .number_prototype 1384 + .map(|p| gc_get_property(&self.gc, p, &key)) 1385 + .unwrap_or(Value::Undefined), 1386 + Value::Boolean(_) => self 1387 + .boolean_prototype 1388 + .map(|p| gc_get_property(&self.gc, p, &key)) 1389 + .unwrap_or(Value::Undefined), 1355 1390 _ => Value::Undefined, 1356 1391 }; 1357 1392 self.registers[base + dst as usize] = val; ··· 1434 1469 Value::Object(gc_ref) | Value::Function(gc_ref) => { 1435 1470 gc_get_property(&self.gc, gc_ref, &key) 1436 1471 } 1437 - Value::String(ref s) => string_get_property(s, &key), 1472 + Value::String(ref s) => { 1473 + let v = string_get_property(s, &key); 1474 + if matches!(v, Value::Undefined) { 1475 + self.string_prototype 1476 + .map(|p| gc_get_property(&self.gc, p, &key)) 1477 + .unwrap_or(Value::Undefined) 1478 + } else { 1479 + v 1480 + } 1481 + } 1482 + Value::Number(_) => self 1483 + .number_prototype 1484 + .map(|p| gc_get_property(&self.gc, p, &key)) 1485 + .unwrap_or(Value::Undefined), 1486 + Value::Boolean(_) => self 1487 + .boolean_prototype 1488 + .map(|p| gc_get_property(&self.gc, p, &key)) 1489 + .unwrap_or(Value::Undefined), 1438 1490 _ => Value::Undefined, 1439 1491 }; 1440 1492 self.registers[base + dst as usize] = val; ··· 3253 3305 match eval(src).unwrap() { 3254 3306 Value::Boolean(b) => assert!(b), 3255 3307 v => panic!("expected true, got {v:?}"), 3308 + } 3309 + } 3310 + 3311 + // ── String built-in tests ───────────────────────────────── 3312 + 3313 + #[test] 3314 + fn test_string_constructor() { 3315 + match eval("String(42)").unwrap() { 3316 + Value::String(s) => assert_eq!(s, "42"), 3317 + v => panic!("expected '42', got {v:?}"), 3318 + } 3319 + match eval("String(true)").unwrap() { 3320 + Value::String(s) => assert_eq!(s, "true"), 3321 + v => panic!("expected 'true', got {v:?}"), 3322 + } 3323 + match eval("String()").unwrap() { 3324 + Value::String(s) => assert_eq!(s, ""), 3325 + v => panic!("expected '', got {v:?}"), 3326 + } 3327 + } 3328 + 3329 + #[test] 3330 + fn test_string_length() { 3331 + match eval("'hello'.length").unwrap() { 3332 + Value::Number(n) => assert_eq!(n, 5.0), 3333 + v => panic!("expected 5, got {v:?}"), 3334 + } 3335 + } 3336 + 3337 + #[test] 3338 + fn test_string_char_at() { 3339 + match eval("'hello'.charAt(1)").unwrap() { 3340 + Value::String(s) => assert_eq!(s, "e"), 3341 + v => panic!("expected 'e', got {v:?}"), 3342 + } 3343 + match eval("'hello'.charAt(10)").unwrap() { 3344 + Value::String(s) => assert_eq!(s, ""), 3345 + v => panic!("expected '', got {v:?}"), 3346 + } 3347 + } 3348 + 3349 + #[test] 3350 + fn test_string_char_code_at() { 3351 + match eval("'A'.charCodeAt(0)").unwrap() { 3352 + Value::Number(n) => assert_eq!(n, 65.0), 3353 + v => panic!("expected 65, got {v:?}"), 3354 + } 3355 + } 3356 + 3357 + #[test] 3358 + fn test_string_proto_concat() { 3359 + match eval("'hello'.concat(' ', 'world')").unwrap() { 3360 + Value::String(s) => assert_eq!(s, "hello world"), 3361 + v => panic!("expected 'hello world', got {v:?}"), 3362 + } 3363 + } 3364 + 3365 + #[test] 3366 + fn test_string_slice() { 3367 + match eval("'hello world'.slice(6)").unwrap() { 3368 + Value::String(s) => assert_eq!(s, "world"), 3369 + v => panic!("expected 'world', got {v:?}"), 3370 + } 3371 + match eval("'hello'.slice(1, 3)").unwrap() { 3372 + Value::String(s) => assert_eq!(s, "el"), 3373 + v => panic!("expected 'el', got {v:?}"), 3374 + } 3375 + match eval("'hello'.slice(-3)").unwrap() { 3376 + Value::String(s) => assert_eq!(s, "llo"), 3377 + v => panic!("expected 'llo', got {v:?}"), 3378 + } 3379 + } 3380 + 3381 + #[test] 3382 + fn test_string_substring() { 3383 + match eval("'hello'.substring(1, 3)").unwrap() { 3384 + Value::String(s) => assert_eq!(s, "el"), 3385 + v => panic!("expected 'el', got {v:?}"), 3386 + } 3387 + // substring swaps args if start > end 3388 + match eval("'hello'.substring(3, 1)").unwrap() { 3389 + Value::String(s) => assert_eq!(s, "el"), 3390 + v => panic!("expected 'el', got {v:?}"), 3391 + } 3392 + } 3393 + 3394 + #[test] 3395 + fn test_string_index_of() { 3396 + match eval("'hello world'.indexOf('world')").unwrap() { 3397 + Value::Number(n) => assert_eq!(n, 6.0), 3398 + v => panic!("expected 6, got {v:?}"), 3399 + } 3400 + match eval("'hello'.indexOf('xyz')").unwrap() { 3401 + Value::Number(n) => assert_eq!(n, -1.0), 3402 + v => panic!("expected -1, got {v:?}"), 3403 + } 3404 + } 3405 + 3406 + #[test] 3407 + fn test_string_last_index_of() { 3408 + match eval("'abcabc'.lastIndexOf('abc')").unwrap() { 3409 + Value::Number(n) => assert_eq!(n, 3.0), 3410 + v => panic!("expected 3, got {v:?}"), 3411 + } 3412 + } 3413 + 3414 + #[test] 3415 + fn test_string_includes() { 3416 + match eval("'hello world'.includes('world')").unwrap() { 3417 + Value::Boolean(b) => assert!(b), 3418 + v => panic!("expected true, got {v:?}"), 3419 + } 3420 + match eval("'hello'.includes('xyz')").unwrap() { 3421 + Value::Boolean(b) => assert!(!b), 3422 + v => panic!("expected false, got {v:?}"), 3423 + } 3424 + } 3425 + 3426 + #[test] 3427 + fn test_string_starts_ends_with() { 3428 + match eval("'hello'.startsWith('hel')").unwrap() { 3429 + Value::Boolean(b) => assert!(b), 3430 + v => panic!("expected true, got {v:?}"), 3431 + } 3432 + match eval("'hello'.endsWith('llo')").unwrap() { 3433 + Value::Boolean(b) => assert!(b), 3434 + v => panic!("expected true, got {v:?}"), 3435 + } 3436 + } 3437 + 3438 + #[test] 3439 + fn test_string_trim() { 3440 + match eval("' hello '.trim()").unwrap() { 3441 + Value::String(s) => assert_eq!(s, "hello"), 3442 + v => panic!("expected 'hello', got {v:?}"), 3443 + } 3444 + match eval("' hello '.trimStart()").unwrap() { 3445 + Value::String(s) => assert_eq!(s, "hello "), 3446 + v => panic!("expected 'hello ', got {v:?}"), 3447 + } 3448 + match eval("' hello '.trimEnd()").unwrap() { 3449 + Value::String(s) => assert_eq!(s, " hello"), 3450 + v => panic!("expected ' hello', got {v:?}"), 3451 + } 3452 + } 3453 + 3454 + #[test] 3455 + fn test_string_pad() { 3456 + match eval("'5'.padStart(3, '0')").unwrap() { 3457 + Value::String(s) => assert_eq!(s, "005"), 3458 + v => panic!("expected '005', got {v:?}"), 3459 + } 3460 + match eval("'5'.padEnd(3, '0')").unwrap() { 3461 + Value::String(s) => assert_eq!(s, "500"), 3462 + v => panic!("expected '500', got {v:?}"), 3463 + } 3464 + } 3465 + 3466 + #[test] 3467 + fn test_string_repeat() { 3468 + match eval("'ab'.repeat(3)").unwrap() { 3469 + Value::String(s) => assert_eq!(s, "ababab"), 3470 + v => panic!("expected 'ababab', got {v:?}"), 3471 + } 3472 + } 3473 + 3474 + #[test] 3475 + fn test_string_split() { 3476 + // split returns an array; verify length and elements. 3477 + match eval("'a,b,c'.split(',').length").unwrap() { 3478 + Value::Number(n) => assert_eq!(n, 3.0), 3479 + v => panic!("expected 3, got {v:?}"), 3480 + } 3481 + match eval("'a,b,c'.split(',')[0]").unwrap() { 3482 + Value::String(s) => assert_eq!(s, "a"), 3483 + v => panic!("expected 'a', got {v:?}"), 3484 + } 3485 + match eval("'a,b,c'.split(',')[2]").unwrap() { 3486 + Value::String(s) => assert_eq!(s, "c"), 3487 + v => panic!("expected 'c', got {v:?}"), 3488 + } 3489 + } 3490 + 3491 + #[test] 3492 + fn test_string_replace() { 3493 + match eval("'hello world'.replace('world', 'there')").unwrap() { 3494 + Value::String(s) => assert_eq!(s, "hello there"), 3495 + v => panic!("expected 'hello there', got {v:?}"), 3496 + } 3497 + } 3498 + 3499 + #[test] 3500 + fn test_string_replace_all() { 3501 + match eval("'aabbcc'.replaceAll('b', 'x')").unwrap() { 3502 + Value::String(s) => assert_eq!(s, "aaxxcc"), 3503 + v => panic!("expected 'aaxxcc', got {v:?}"), 3504 + } 3505 + } 3506 + 3507 + #[test] 3508 + fn test_string_case() { 3509 + match eval("'Hello'.toLowerCase()").unwrap() { 3510 + Value::String(s) => assert_eq!(s, "hello"), 3511 + v => panic!("expected 'hello', got {v:?}"), 3512 + } 3513 + match eval("'Hello'.toUpperCase()").unwrap() { 3514 + Value::String(s) => assert_eq!(s, "HELLO"), 3515 + v => panic!("expected 'HELLO', got {v:?}"), 3516 + } 3517 + } 3518 + 3519 + #[test] 3520 + fn test_string_at() { 3521 + match eval("'hello'.at(0)").unwrap() { 3522 + Value::String(s) => assert_eq!(s, "h"), 3523 + v => panic!("expected 'h', got {v:?}"), 3524 + } 3525 + match eval("'hello'.at(-1)").unwrap() { 3526 + Value::String(s) => assert_eq!(s, "o"), 3527 + v => panic!("expected 'o', got {v:?}"), 3528 + } 3529 + } 3530 + 3531 + #[test] 3532 + fn test_string_from_char_code() { 3533 + match eval("String.fromCharCode(72, 101, 108)").unwrap() { 3534 + Value::String(s) => assert_eq!(s, "Hel"), 3535 + v => panic!("expected 'Hel', got {v:?}"), 3536 + } 3537 + } 3538 + 3539 + #[test] 3540 + fn test_string_from_code_point() { 3541 + match eval("String.fromCodePoint(65, 66, 67)").unwrap() { 3542 + Value::String(s) => assert_eq!(s, "ABC"), 3543 + v => panic!("expected 'ABC', got {v:?}"), 3544 + } 3545 + } 3546 + 3547 + // ── Number built-in tests ───────────────────────────────── 3548 + 3549 + #[test] 3550 + fn test_number_constructor() { 3551 + match eval("Number('42')").unwrap() { 3552 + Value::Number(n) => assert_eq!(n, 42.0), 3553 + v => panic!("expected 42, got {v:?}"), 3554 + } 3555 + match eval("Number(true)").unwrap() { 3556 + Value::Number(n) => assert_eq!(n, 1.0), 3557 + v => panic!("expected 1, got {v:?}"), 3558 + } 3559 + match eval("Number()").unwrap() { 3560 + Value::Number(n) => assert_eq!(n, 0.0), 3561 + v => panic!("expected 0, got {v:?}"), 3562 + } 3563 + } 3564 + 3565 + #[test] 3566 + fn test_number_is_nan() { 3567 + match eval("Number.isNaN(NaN)").unwrap() { 3568 + Value::Boolean(b) => assert!(b), 3569 + v => panic!("expected true, got {v:?}"), 3570 + } 3571 + match eval("Number.isNaN(42)").unwrap() { 3572 + Value::Boolean(b) => assert!(!b), 3573 + v => panic!("expected false, got {v:?}"), 3574 + } 3575 + // Number.isNaN doesn't coerce — string "NaN" is not NaN. 3576 + match eval("Number.isNaN('NaN')").unwrap() { 3577 + Value::Boolean(b) => assert!(!b), 3578 + v => panic!("expected false, got {v:?}"), 3579 + } 3580 + } 3581 + 3582 + #[test] 3583 + fn test_number_is_finite() { 3584 + match eval("Number.isFinite(42)").unwrap() { 3585 + Value::Boolean(b) => assert!(b), 3586 + v => panic!("expected true, got {v:?}"), 3587 + } 3588 + match eval("Number.isFinite(Infinity)").unwrap() { 3589 + Value::Boolean(b) => assert!(!b), 3590 + v => panic!("expected false, got {v:?}"), 3591 + } 3592 + } 3593 + 3594 + #[test] 3595 + fn test_number_is_integer() { 3596 + match eval("Number.isInteger(42)").unwrap() { 3597 + Value::Boolean(b) => assert!(b), 3598 + v => panic!("expected true, got {v:?}"), 3599 + } 3600 + match eval("Number.isInteger(42.5)").unwrap() { 3601 + Value::Boolean(b) => assert!(!b), 3602 + v => panic!("expected false, got {v:?}"), 3603 + } 3604 + } 3605 + 3606 + #[test] 3607 + fn test_number_is_safe_integer() { 3608 + match eval("Number.isSafeInteger(42)").unwrap() { 3609 + Value::Boolean(b) => assert!(b), 3610 + v => panic!("expected true, got {v:?}"), 3611 + } 3612 + match eval("Number.isSafeInteger(9007199254740992)").unwrap() { 3613 + Value::Boolean(b) => assert!(!b), 3614 + v => panic!("expected false, got {v:?}"), 3615 + } 3616 + } 3617 + 3618 + #[test] 3619 + fn test_number_constants() { 3620 + match eval("Number.MAX_SAFE_INTEGER").unwrap() { 3621 + Value::Number(n) => assert_eq!(n, 9007199254740991.0), 3622 + v => panic!("expected MAX_SAFE_INTEGER, got {v:?}"), 3623 + } 3624 + match eval("Number.EPSILON").unwrap() { 3625 + Value::Number(n) => assert_eq!(n, f64::EPSILON), 3626 + v => panic!("expected EPSILON, got {v:?}"), 3627 + } 3628 + } 3629 + 3630 + #[test] 3631 + fn test_number_to_fixed() { 3632 + match eval("var n = 3.14159; n.toFixed(2)").unwrap() { 3633 + Value::String(s) => assert_eq!(s, "3.14"), 3634 + v => panic!("expected '3.14', got {v:?}"), 3635 + } 3636 + } 3637 + 3638 + #[test] 3639 + fn test_number_to_string_radix() { 3640 + match eval("var n = 255; n.toString(16)").unwrap() { 3641 + Value::String(s) => assert_eq!(s, "ff"), 3642 + v => panic!("expected 'ff', got {v:?}"), 3643 + } 3644 + match eval("var n = 10; n.toString(2)").unwrap() { 3645 + Value::String(s) => assert_eq!(s, "1010"), 3646 + v => panic!("expected '1010', got {v:?}"), 3647 + } 3648 + } 3649 + 3650 + #[test] 3651 + fn test_number_parse_int() { 3652 + match eval("Number.parseInt('42')").unwrap() { 3653 + Value::Number(n) => assert_eq!(n, 42.0), 3654 + v => panic!("expected 42, got {v:?}"), 3655 + } 3656 + } 3657 + 3658 + // ── Boolean built-in tests ──────────────────────────────── 3659 + 3660 + #[test] 3661 + fn test_boolean_constructor() { 3662 + match eval("Boolean(1)").unwrap() { 3663 + Value::Boolean(b) => assert!(b), 3664 + v => panic!("expected true, got {v:?}"), 3665 + } 3666 + match eval("Boolean(0)").unwrap() { 3667 + Value::Boolean(b) => assert!(!b), 3668 + v => panic!("expected false, got {v:?}"), 3669 + } 3670 + match eval("Boolean('')").unwrap() { 3671 + Value::Boolean(b) => assert!(!b), 3672 + v => panic!("expected false, got {v:?}"), 3673 + } 3674 + match eval("Boolean('hello')").unwrap() { 3675 + Value::Boolean(b) => assert!(b), 3676 + v => panic!("expected true, got {v:?}"), 3677 + } 3678 + } 3679 + 3680 + #[test] 3681 + fn test_boolean_to_string() { 3682 + match eval("true.toString()").unwrap() { 3683 + Value::String(s) => assert_eq!(s, "true"), 3684 + v => panic!("expected 'true', got {v:?}"), 3685 + } 3686 + match eval("false.toString()").unwrap() { 3687 + Value::String(s) => assert_eq!(s, "false"), 3688 + v => panic!("expected 'false', got {v:?}"), 3689 + } 3690 + } 3691 + 3692 + // ── Symbol built-in tests ───────────────────────────────── 3693 + 3694 + #[test] 3695 + fn test_symbol_uniqueness() { 3696 + // Each Symbol() call should produce a unique value. 3697 + match eval("var a = Symbol('x'); var b = Symbol('x'); a === b").unwrap() { 3698 + Value::Boolean(b) => assert!(!b), 3699 + v => panic!("expected false, got {v:?}"), 3700 + } 3701 + } 3702 + 3703 + #[test] 3704 + fn test_symbol_well_known() { 3705 + match eval("typeof Symbol.iterator").unwrap() { 3706 + Value::String(s) => assert_eq!(s, "string"), 3707 + v => panic!("expected 'string', got {v:?}"), 3708 + } 3709 + match eval("Symbol.iterator").unwrap() { 3710 + Value::String(s) => assert_eq!(s, "@@iterator"), 3711 + v => panic!("expected '@@iterator', got {v:?}"), 3712 + } 3713 + } 3714 + 3715 + #[test] 3716 + fn test_symbol_for_and_key_for() { 3717 + // "for" is a keyword, so use bracket notation: Symbol["for"](...). 3718 + match eval("Symbol['for']('test') === Symbol['for']('test')").unwrap() { 3719 + Value::Boolean(b) => assert!(b), 3720 + v => panic!("expected true, got {v:?}"), 3721 + } 3722 + match eval("Symbol.keyFor(Symbol['for']('mykey'))").unwrap() { 3723 + Value::String(s) => assert_eq!(s, "mykey"), 3724 + v => panic!("expected 'mykey', got {v:?}"), 3725 + } 3726 + } 3727 + 3728 + // ── Primitive auto-boxing tests ─────────────────────────── 3729 + 3730 + #[test] 3731 + fn test_string_method_chaining() { 3732 + match eval("' Hello World '.trim().toLowerCase()").unwrap() { 3733 + Value::String(s) => assert_eq!(s, "hello world"), 3734 + v => panic!("expected 'hello world', got {v:?}"), 3735 + } 3736 + } 3737 + 3738 + #[test] 3739 + fn test_string_substr() { 3740 + match eval("'hello world'.substr(6, 5)").unwrap() { 3741 + Value::String(s) => assert_eq!(s, "world"), 3742 + v => panic!("expected 'world', got {v:?}"), 3256 3743 } 3257 3744 } 3258 3745 }