feat: TA 统一数值键门助手与单测钉

This commit is contained in:
2026-09-22 20:01:01 +00:00
parent dce132acec
commit 687a0b436a
2 changed files with 279 additions and 38 deletions
+50 -38
View File
@@ -111,7 +111,7 @@ pub fn number_is_finite<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
///
/// 与 Rust `char::is_whitespace` 的差异:规范集合不含 U+0085(NEL),手工
/// 按白名单匹配避免误剥。
fn is_js_ws(c: char) -> bool {
pub(crate) fn is_js_ws(c: char) -> bool {
matches!(
c,
'\u{0009}' | '\u{000B}' | '\u{000C}' | '\u{0020}' | '\u{00A0}' | '\u{1680}' | '\u{2000}'
@@ -245,41 +245,17 @@ pub fn number_parse_int<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
}
/// `parseFloat(string)`:解析字符串前缀为浮点数。
/// 按 StrDecimalLiteral 文法扫描最长合法十进制数字前缀:数字 + 至多一个
/// '.'('.' 后必须有数字,整数部分可为空)+ 可选指数(e/E 可带符号,
/// mantissa 须先有数字,指数须有数字)。
///
/// 规范白名单 trim 后读 `+`/`-` 符号,特判精确大小写的 `Infinity`;随后按
/// StrDecimalLiteral 文法扫描最长合法十进制前缀(整数 + 可选小数 + 可选
/// 指数,`0x10` 在 'x' 处停止得 0,`1.2.3` 得 1.2),前缀子串交给
/// fast_float 解析(溢出归 ±Infinity),无合法前缀返回 NaN。
pub fn number_parse_float<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.len() < 2 {
return NativeResult::Ok(JsValue::float(f64::NAN));
}
// 参数按 ToString 完整转换:对象经 ToPrimitive(string hint),Symbol 抛
// TypeError;对象方法抛出的原生异常原样传播。
let s = match oxide_runtime_api::to_string_full(vm.reg(args[1]), vm) {
Ok(s) => s,
Err(_) => {
if let Some(exc) = vm.take_uncaught_value() {
return NativeResult::Err(exc);
}
return NativeResult::Err(crate::error::create_type_error(vm, "Cannot convert value to a string"));
}
};
let s = s.trim_start_matches(is_js_ws).trim_end_matches(is_js_ws);
// 读符号;`Infinity` 大小写敏感,其后可带任意后缀(取最长合法前缀)。
let (neg, rest) = match s.as_bytes().first() {
Some(b'-') => (true, &s[1..]),
Some(b'+') => (false, &s[1..]),
_ => (false, s),
};
if rest.starts_with("Infinity") {
return NativeResult::Ok(JsValue::float(if neg { f64::NEG_INFINITY } else { f64::INFINITY }));
}
// 扫描 mantissa:数字与至多一个 '.'('.' 后必须有数字,整数部分可为空)。
let b = rest.as_bytes();
/// # 边界与前提
/// - 前缀值经 fast_float 正确舍入解析(溢出饱和为 ±Infinity);
/// - 无数字前缀(空串、'.'、'abc'、'1e')返回 None;
/// - 消费长度是前缀的字节长,调用方自取前缀语义(parseFloat)或全串语义
/// (消费长度须等于全长,如 ToNumber 门)。
pub(crate) fn parse_decimal_prefix(text: &str) -> Option<(f64, usize)> {
let b = text.as_bytes();
let mut i = 0usize;
let mut dot = false;
let mut mantissa_digits = 0usize;
@@ -311,12 +287,48 @@ pub fn number_parse_float<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
}
if mantissa_digits == 0 {
return None;
}
match fast_float::parse::<f64, _>(&text[..end]) {
Ok(v) => Some((v, end)),
Err(_) => None,
}
}
/// `parseFloat(string)`:解析字符串前缀为浮点数。
///
/// 规范白名单 trim 后读 `+`/`-` 符号,特判精确大小写的 `Infinity`;随后取
/// 共享前缀扫描核([`parse_decimal_prefix`])的最长合法十进制前缀(`0x10`
/// 在 'x' 处停止得 0,`1.2.3` 得 1.2),无合法前缀返回 NaN。
pub fn number_parse_float<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.len() < 2 {
return NativeResult::Ok(JsValue::float(f64::NAN));
}
// 参数按 ToString 完整转换:对象经 ToPrimitive(string hint),Symbol 抛
// TypeError;对象方法抛出的原生异常原样传播。
let s = match oxide_runtime_api::to_string_full(vm.reg(args[1]), vm) {
Ok(s) => s,
Err(_) => {
if let Some(exc) = vm.take_uncaught_value() {
return NativeResult::Err(exc);
}
return NativeResult::Err(crate::error::create_type_error(vm, "Cannot convert value to a string"));
}
};
let s = s.trim_start_matches(is_js_ws).trim_end_matches(is_js_ws);
match fast_float::parse::<f64, _>(&rest[..end]) {
Ok(v) => NativeResult::Ok(JsValue::float(if neg { -v } else { v })),
Err(_) => NativeResult::Ok(JsValue::float(f64::NAN)),
// 读符号;`Infinity` 大小写敏感,其后可带任意后缀(取最长合法前缀)。
let (neg, rest) = match s.as_bytes().first() {
Some(b'-') => (true, &s[1..]),
Some(b'+') => (false, &s[1..]),
_ => (false, s),
};
if rest.starts_with("Infinity") {
return NativeResult::Ok(JsValue::float(if neg { f64::NEG_INFINITY } else { f64::INFINITY }));
}
match parse_decimal_prefix(rest) {
Some((value, _)) => NativeResult::Ok(JsValue::float(if neg { -value } else { value })),
None => NativeResult::Ok(JsValue::float(f64::NAN)),
}
}
+229
View File
@@ -298,6 +298,110 @@ pub fn typed_array_integer_index<H: VmHost>(vm: &H, obj: &JsObject, prop_name_si
Some((index as usize, view.length))
}
// ── 统一数值键门 ─────────────────────────────────────────────────────────
/// TypedArray 统一数值键门三态:`Ordinary` 非数字串(走普通属性路径);
/// `NumericInvalid` 数字无效(负/分数/±Infinity/NaN/越界,含 "-0" 特例);
/// `NumericValid` 界内整数索引(携带索引)。
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum TaIndexGate {
Ordinary,
NumericInvalid,
NumericValid(u32),
}
/// 统一数值键门:消费方唯一入口。谓词 `P === ToString(ToNumber(P))` 全形态
/// round-trip("NaN"/±"Infinity" 天然归数字无效臂),`"-0"`(trim 后)独立
/// 特例归数字无效;整数键免字符串解析短路。内部取 length 值传递,消费方
/// 零视图接触。
///
/// # 边界与前提
/// - 非 TypedArray 对象、symbol 键与查不到的字符串键一律 `Ordinary`(防御,
/// 调用方均已先判)。
pub fn ta_index_gate<H: VmHost>(vm: &H, obj: &JsObject, key_si: u32) -> TaIndexGate {
if !obj.is_typed_array_obj() {
return TaIndexGate::Ordinary;
}
let length = ta_view_length(vm, obj);
if is_int_key(key_si) {
return ta_index_gate_int(int_key_value(key_si), length);
}
let Some(key) = vm.kernel_core().perm_interner().lookup(key_si) else {
return TaIndexGate::Ordinary;
};
ta_index_gate_from_text(key, length)
}
/// 取 TypedArray 视图长度(供只需 length 不需门的枚举面消费方);
/// 非 TypedArray 或内部状态无效返回 0。
pub(crate) fn ta_view_length<H: VmHost>(_vm: &H, obj: &JsObject) -> usize {
let Some(ptr) = typed_array_data_ptr(obj) else {
return 0;
};
if ptr.is_null() {
return 0;
}
// SAFETY: ptr 非空,为 TypedArray 对象 native_fn 槽内的 Box<TypedArrayData>,
// 与对象同生命周期,此处只读拷贝视图字段。
let view = unsafe { *ptr };
view.length
}
/// 纯核(无 VM)整数键臂:界内有效,越界数字无效(length 0 时全无效)。
pub(crate) fn ta_index_gate_int(index: u32, length: usize) -> TaIndexGate {
if (index as usize) < length {
TaIndexGate::NumericValid(index)
} else {
TaIndexGate::NumericInvalid
}
}
/// 纯核(无 VM)文本键臂:先 "-0" 特例,再 round-trip 判定;命中数字臂且
/// 界内整数时携带索引。
pub(crate) fn ta_index_gate_from_text(text: &str, length: usize) -> TaIndexGate {
let t = text
.trim_start_matches(crate::number::is_js_ws)
.trim_end_matches(crate::number::is_js_ws);
if t == "-0" {
return TaIndexGate::NumericInvalid;
}
let n = ta_to_number_text(t);
// round-trip 对原文比较(含空白):`" 1"`/`"1 "` 不成立归 Ordinary。
if text != oxide_runtime_api::js_number_to_string(n) {
return TaIndexGate::Ordinary;
}
if n.is_nan() || n.is_infinite() || n.fract() != 0.0 || n < 0.0 || n >= length as f64 {
return TaIndexGate::NumericInvalid;
}
TaIndexGate::NumericValid(n as u32)
}
/// 纯文本的整串 ToNumber(供门 round-trip):空串 +0;`±Infinity` 前缀取
/// 专名;十进制前缀须消费整串否则 NaN(hex/八/二进制与下划线文本全 NaN)。
fn ta_to_number_text(t: &str) -> f64 {
if t.is_empty() {
return 0.0;
}
let (neg, rest) = match t.as_bytes().first() {
Some(b'-') => (true, &t[1..]),
Some(b'+') => (false, &t[1..]),
_ => (false, t),
};
if rest.starts_with("Infinity") {
return if neg { f64::NEG_INFINITY } else { f64::INFINITY };
}
match crate::number::parse_decimal_prefix(rest) {
Some((value, consumed)) if consumed == rest.len() => {
if neg {
-value
} else {
value
}
}
_ => f64::NAN,
}
}
/// 写 TypedArray 指定整数索引的元素(供 VM 普通属性 set 的 typed 分支使用)。
/// 越界忽略(不创建属性、不报错);内部状态非法返回 Err(String)。
pub fn typed_array_element_set<H: VmHost>(
@@ -2085,3 +2189,128 @@ pub fn uint8array_from_hex<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let buffer = JsValue::from_js_object(new_array_buffer(vm, bytes));
NativeResult::Ok(JsValue::from_js_object(create_typed_array(vm, TypedArrayKind::Uint8, buffer, 0, len)))
}
#[cfg(test)]
mod tests {
use super::*;
fn gate(text: &str, length: usize) -> TaIndexGate {
ta_index_gate_from_text(text, length)
}
// A 整数键(length 除注明外取 2)。
#[test]
fn gate_text_integer_keys() {
assert_eq!(gate("0", 2), TaIndexGate::NumericValid(0));
assert_eq!(gate("1", 2), TaIndexGate::NumericValid(1));
assert_eq!(gate("0", 1), TaIndexGate::NumericValid(0));
assert_eq!(gate("2", 2), TaIndexGate::NumericInvalid);
assert_eq!(gate("4294967295", 2), TaIndexGate::NumericInvalid);
assert_eq!(gate("4294967296", 2), TaIndexGate::NumericInvalid);
assert_eq!(gate("007", 2), TaIndexGate::Ordinary);
}
// B 小数键:round-trip 成立的非整数归数字无效,round-trip 失败归 Ordinary。
#[test]
fn gate_text_fractional_keys() {
assert_eq!(gate("1.1", 2), TaIndexGate::NumericInvalid);
assert_eq!(gate("0.0001", 2), TaIndexGate::NumericInvalid);
assert_eq!(gate("0.1", 2), TaIndexGate::NumericInvalid);
assert_eq!(gate("0.000001", 2), TaIndexGate::NumericInvalid);
assert_eq!(gate("0.30000000000000004", 2), TaIndexGate::NumericInvalid);
assert_eq!(gate("1e-7", 2), TaIndexGate::NumericInvalid);
assert_eq!(gate("1e-20", 2), TaIndexGate::NumericInvalid);
assert_eq!(gate("1e-21", 2), TaIndexGate::NumericInvalid);
assert_eq!(gate("-9007199254740992", 2), TaIndexGate::NumericInvalid);
assert_eq!(gate("1.0", 2), TaIndexGate::Ordinary);
assert_eq!(gate("+1", 2), TaIndexGate::Ordinary);
assert_eq!(gate("-1.0", 2), TaIndexGate::Ordinary);
assert_eq!(gate("0.0000001", 2), TaIndexGate::Ordinary);
assert_eq!(gate("1000000000000000000000", 2), TaIndexGate::Ordinary);
assert_eq!(gate(" 1", 2), TaIndexGate::Ordinary);
assert_eq!(gate("1 ", 2), TaIndexGate::Ordinary);
assert_eq!(gate(".5", 2), TaIndexGate::Ordinary);
assert_eq!(gate("5.", 2), TaIndexGate::Ordinary);
assert_eq!(gate("1e2", 2), TaIndexGate::Ordinary);
assert_eq!(gate("1e20", 2), TaIndexGate::Ordinary);
assert_eq!(gate("1.7976931348623157e308", 2), TaIndexGate::Ordinary);
assert_eq!(gate("1.7976931348623159e308", 2), TaIndexGate::Ordinary);
assert_eq!(gate("1e400", 2), TaIndexGate::Ordinary);
assert_eq!(gate("2e308", 2), TaIndexGate::Ordinary);
assert_eq!(gate("9007199254740993", 2), TaIndexGate::Ordinary);
assert_eq!(gate("123456789012345678901234567890", 2), TaIndexGate::Ordinary);
}
// C 负数键:round-trip 成立的负数归数字无效,ToNumber=NaN 归 Ordinary。
#[test]
fn gate_text_negative_keys() {
assert_eq!(gate("-1", 2), TaIndexGate::NumericInvalid);
assert_eq!(gate("-1", 0), TaIndexGate::NumericInvalid);
assert_eq!(gate("-0.5", 2), TaIndexGate::NumericInvalid);
assert_eq!(gate("--1", 2), TaIndexGate::Ordinary);
assert_eq!(gate("-", 2), TaIndexGate::Ordinary);
}
// D 十六/八/二进制与下划线键:数字对象 ToString 永不输出 0x 前缀,
// 全部不 round-trip,归 Ordinary。
#[test]
fn gate_text_radix_and_underscore_keys() {
assert_eq!(gate("0x1", 2), TaIndexGate::Ordinary);
assert_eq!(gate("0X1", 2), TaIndexGate::Ordinary);
assert_eq!(gate("0o17", 2), TaIndexGate::Ordinary);
assert_eq!(gate("0b1", 2), TaIndexGate::Ordinary);
assert_eq!(gate("1_000", 2), TaIndexGate::Ordinary);
assert_eq!(gate("0x1.8p1", 2), TaIndexGate::Ordinary);
}
// E "-0" 特例:字符串相归数字无效(不得 Valid(0)),round-trip 失败的
// 变体归 Ordinary。
#[test]
fn gate_text_minus_zero_special() {
assert_eq!(gate("-0", 2), TaIndexGate::NumericInvalid);
assert_eq!(gate("-0", 0), TaIndexGate::NumericInvalid);
assert_eq!(gate("-0.0", 2), TaIndexGate::Ordinary);
assert_eq!(gate("- 0", 2), TaIndexGate::Ordinary);
assert_eq!(gate("+0", 2), TaIndexGate::Ordinary);
}
// F 保留字:NaN/±Infinity 走数字路径(round-trip 成立),拼写偏差归
// Ordinary。
#[test]
fn gate_text_reserved_words() {
assert_eq!(gate("NaN", 2), TaIndexGate::NumericInvalid);
assert_eq!(gate("Infinity", 2), TaIndexGate::NumericInvalid);
assert_eq!(gate("-Infinity", 2), TaIndexGate::NumericInvalid);
assert_eq!(gate("inf", 2), TaIndexGate::Ordinary);
assert_eq!(gate("INFINITY", 2), TaIndexGate::Ordinary);
assert_eq!(gate("NaNx", 2), TaIndexGate::Ordinary);
assert_eq!(gate("Infinity1", 2), TaIndexGate::Ordinary);
}
// G 空串与怪文本:ToNumber(+0) 的 round-trip "0" 与原文不等,归 Ordinary。
#[test]
fn gate_text_empty_and_odd() {
assert_eq!(gate("", 2), TaIndexGate::Ordinary);
assert_eq!(gate(" ", 2), TaIndexGate::Ordinary);
assert_eq!(gate("\u{3000}", 2), TaIndexGate::Ordinary);
}
// H 2^32 溢出与最大长度边界(最大长度取 1 字节 TA 上界 2^30)。
#[test]
fn gate_text_2pow32_and_max_length() {
assert_eq!(gate("4294967295", 1073741824), TaIndexGate::NumericInvalid);
assert_eq!(gate("1073741823", 1073741824), TaIndexGate::NumericValid(1073741823));
assert_eq!(gate("1073741824", 1073741824), TaIndexGate::NumericInvalid);
// 2^64 的规范 ToString 是最短回环式 "18446744073709552000"(V8 实测),
// 与原文不等,round-trip 不成立归 Ordinary(建真实属性)。
assert_eq!(gate("18446744073709551616", 2), TaIndexGate::Ordinary);
assert_eq!(gate("99999999999999999999999", 2), TaIndexGate::Ordinary);
}
// I int-key 臂(纯核)。
#[test]
fn gate_int_arm() {
assert_eq!(ta_index_gate_int(0, 2), TaIndexGate::NumericValid(0));
assert_eq!(ta_index_gate_int(2, 2), TaIndexGate::NumericInvalid);
}
}