Skip to content

Commit 10bc8c6

Browse files
committed
Implement multiple ES proposals and fix TypedArray.prototype.slice
- Add ArrayBuffer.prototype.transfer, transferToFixedLength, and detached getter (arraybuffer-transfer) - Add Uint8Array.fromBase64, fromHex, toBase64, toHex, setFromBase64, setFromHex (uint8array-base64) - Add Math.sumPrecise with Shewchuk exact-sum algorithm - Add Atomics.pause feature probe - Add error-cause support for Error and native error constructors - Add well-formed-json-stringify: escape lone surrogates in JSON.stringify - Add json-superset: allow U+2028/U+2029 in string literals - Add Symbol.isConcatSpreadable as a well-known symbol - Add Date.prototype.getYear, setYear, toGMTString (Annex B) - Fix TypedArray.prototype.slice to skip detached-buffer check when count is 0
1 parent 327940a commit 10bc8c6

16 files changed

Lines changed: 1445 additions & 28 deletions

ci/feature_probes/Atomics.pause.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Feature probe: Atomics.pause
2+
try {
3+
if (typeof Atomics !== 'object') throw new Error('Atomics missing');
4+
if (typeof Atomics.pause !== 'function') throw new Error('pause missing');
5+
Atomics.pause();
6+
console.log('OK');
7+
} catch(e) {
8+
console.log('NO');
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Feature probe: Math.sumPrecise
2+
try {
3+
if (typeof Math.sumPrecise !== 'function') throw new Error('sumPrecise missing');
4+
var result = Math.sumPrecise([1, 2, 3]);
5+
if (result !== 6) throw new Error('bad result: ' + result);
6+
console.log('OK');
7+
} catch(e) {
8+
console.log('NO');
9+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Feature probe: Symbol.isConcatSpreadable
2+
try {
3+
if (typeof Symbol.isConcatSpreadable !== 'symbol') throw new Error('missing');
4+
var obj = { length: 2, 0: 'a', 1: 'b', [Symbol.isConcatSpreadable]: true };
5+
var result = [].concat(obj);
6+
if (result.length !== 2 || result[0] !== 'a' || result[1] !== 'b') throw new Error('bad concat');
7+
console.log('OK');
8+
} catch(e) {
9+
console.log('NO');
10+
}

ci/feature_probes/arraybuffer-transfer.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ try {
33
if (typeof ArrayBuffer !== 'function') throw new Error('ArrayBuffer missing');
44
if (typeof ArrayBuffer.prototype.transfer !== 'function') throw new Error('transfer missing');
55
if (typeof ArrayBuffer.prototype.transferToFixedLength !== 'function') throw new Error('transferToFixedLength missing');
6+
var desc = Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, 'detached');
7+
if (!desc || typeof desc.get !== 'function') throw new Error('detached getter missing');
68
console.log('OK');
79
} catch (_) {
810
console.log('NO');
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Feature probe: uint8array-base64
2+
try {
3+
if (typeof Uint8Array.fromBase64 !== 'function') throw new Error('fromBase64 missing');
4+
if (typeof Uint8Array.fromHex !== 'function') throw new Error('fromHex missing');
5+
var u = new Uint8Array([72, 101, 108, 108, 111]);
6+
if (typeof u.toBase64 !== 'function') throw new Error('toBase64 missing');
7+
if (typeof u.toHex !== 'function') throw new Error('toHex missing');
8+
if (typeof u.setFromBase64 !== 'function') throw new Error('setFromBase64 missing');
9+
if (typeof u.setFromHex !== 'function') throw new Error('setFromHex missing');
10+
console.log('OK');
11+
} catch(e) {
12+
console.log('NO');
13+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Feature probe: well-formed-json-stringify
2+
// JSON.stringify should escape lone surrogates as \uXXXX
3+
try {
4+
var s = JSON.stringify('\uD800');
5+
if (s !== '"\\ud800"') throw new Error('lone high surrogate not escaped: ' + s);
6+
var s2 = JSON.stringify('\uDFFF');
7+
if (s2 !== '"\\udfff"') throw new Error('lone low surrogate not escaped: ' + s2);
8+
console.log('OK');
9+
} catch(e) {
10+
console.log('NO');
11+
}

src/core/eval.rs

Lines changed: 155 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12440,6 +12440,13 @@ pub fn evaluate_call_dispatch<'gc>(
1244012440
} else if let Some(method) = name.strip_prefix("TypedArray.prototype.") {
1244112441
let this_v = this_val.unwrap_or(&Value::Undefined);
1244212442
Ok(crate::js_typedarray::handle_typedarray_method(mc, this_v, method, eval_args, env)?)
12443+
} else if let Some(method) = name.strip_prefix("Uint8Array.prototype.") {
12444+
let this_v = this_val.unwrap_or(&Value::Undefined);
12445+
Ok(crate::js_typedarray::handle_uint8array_proto_method(
12446+
mc, this_v, method, eval_args, env,
12447+
)?)
12448+
} else if let Some(method) = name.strip_prefix("Uint8Array.") {
12449+
Ok(crate::js_typedarray::handle_uint8array_static_method(mc, method, eval_args, env)?)
1244312450
} else if name == "TypedArrayIterator.prototype.next" {
1244412451
let this_v = this_val.unwrap_or(&Value::Undefined);
1244512452
Ok(crate::js_typedarray::handle_typedarray_iterator_next(mc, this_v)?)
@@ -13476,16 +13483,33 @@ pub fn evaluate_call_dispatch<'gc>(
1347613483
Value::String(crate::unicode::utf8_to_utf16(&value_to_string(&prim)))
1347713484
};
1347813485
// The constructor's "prototype" property points to the error prototype
13479-
if let Some(prototype_rc) = object_get_key_value(obj, "prototype")
13486+
let err = if let Some(prototype_rc) = object_get_key_value(obj, "prototype")
1348013487
&& let Value::Object(proto_ptr) = &*prototype_rc.borrow()
1348113488
{
13482-
let err = crate::core::create_error(mc, Some(*proto_ptr), msg_val)?;
13483-
Ok(err)
13489+
crate::core::create_error(mc, Some(*proto_ptr), msg_val)?
1348413490
} else {
13485-
// Fallback: create error with no prototype
13486-
let err = crate::core::create_error(mc, None, msg_val)?;
13487-
Ok(err)
13491+
crate::core::create_error(mc, None, msg_val)?
13492+
};
13493+
// error-cause: InstallErrorCause(O, options) per §20.5.8.1
13494+
if let Some(options_val) = eval_args.get(1)
13495+
&& let Value::Object(options_obj) = options_val
13496+
{
13497+
let has_cause = if let Some(proxy_cell) = crate::core::slot_get(options_obj, &InternalSlot::Proxy)
13498+
&& let Value::Proxy(proxy) = &*proxy_cell.borrow()
13499+
{
13500+
crate::js_proxy::proxy_has_property(mc, proxy, "cause")?
13501+
} else {
13502+
crate::core::has_property_key(options_obj, "cause")
13503+
};
13504+
if has_cause {
13505+
let cause_val = crate::core::eval::get_property_with_accessors(mc, env, options_obj, "cause")?;
13506+
if let Value::Object(err_obj) = &err {
13507+
object_set_key_value(mc, err_obj, "cause", &cause_val)?;
13508+
err_obj.borrow_mut(mc).set_non_enumerable("cause");
13509+
}
13510+
}
1348813511
}
13512+
Ok(err)
1348913513
} else if name == crate::unicode::utf8_to_utf16("Promise") {
1349013514
// Promise() called without new must throw TypeError per §27.2.3.1 step 1
1349113515
Err(raise_type_error!("Promise constructor cannot be invoked without 'new'").into())
@@ -16953,6 +16977,7 @@ fn evaluate_expr_property<'gc>(
1695316977
| "Math.floor"
1695416978
| "Math.fround"
1695516979
| "Math.f16round"
16980+
| "Math.sumPrecise"
1695616981
| "Math.log"
1695716982
| "Math.log1p"
1695816983
| "Math.log2"
@@ -16971,6 +16996,10 @@ fn evaluate_expr_property<'gc>(
1697116996
| "Reflect.preventExtensions"
1697216997
| "ArrayBuffer.isView"
1697316998
| "ArrayBuffer.prototype.resize"
16999+
| "Uint8Array.fromBase64"
17000+
| "Uint8Array.fromHex"
17001+
| "Uint8Array.prototype.setFromBase64"
17002+
| "Uint8Array.prototype.setFromHex"
1697417003
| "RegExp.prototype.exec"
1697517004
| "RegExp.prototype.test"
1697617005
| "RegExp.prototype.match"
@@ -17055,6 +17084,8 @@ fn evaluate_expr_property<'gc>(
1705517084
| "String.prototype.substring"
1705617085
| "String.prototype.substr" => 2.0,
1705717086
"Function.prototype.[Symbol.hasInstance]" | "SharedArrayBuffer.prototype.grow" => 1.0,
17087+
"Uint8Array.prototype.toBase64" | "Uint8Array.prototype.toHex" => 0.0,
17088+
"ArrayBuffer.prototype.transfer" | "ArrayBuffer.prototype.transferToFixedLength" => 0.0,
1705817089
"Object.defineProperty" | "JSON.stringify" | "Reflect.apply" | "Reflect.defineProperty" | "Reflect.set" => 3.0,
1705917090
_ => {
1706017091
if func_name.starts_with("DataView.prototype.get") {
@@ -17687,6 +17718,26 @@ fn evaluate_expr_delete<'gc>(mc: &MutationContext<'gc>, env: &JSObjectDataPtr<'g
1768717718
return Ok(Value::Boolean(deleted));
1768817719
}
1768917720

17721+
// TypedArray [[Delete]]: numeric indices cannot be deleted
17722+
if let Some(ta_val) = slot_get(&obj, &InternalSlot::TypedArray)
17723+
&& let Value::TypedArray(ta) = &*ta_val.borrow()
17724+
&& let Some(numeric_index) = crate::js_typedarray::canonical_numeric_index_string(key)
17725+
{
17726+
// If detached, return true
17727+
if ta.buffer.borrow().detached {
17728+
return Ok(Value::Boolean(true));
17729+
}
17730+
// If not a valid index, return true
17731+
if !crate::js_typedarray::is_valid_integer_index(ta, numeric_index) {
17732+
return Ok(Value::Boolean(true));
17733+
}
17734+
// Valid index on non-detached buffer: cannot delete → false
17735+
if env_get_strictness(env) {
17736+
return Err(crate::raise_type_error!(format!("Cannot delete property '{key}' of a TypedArray")).into());
17737+
}
17738+
return Ok(Value::Boolean(false));
17739+
}
17740+
1769017741
if obj.borrow().non_configurable.contains(&key_val) {
1769117742
if env_get_strictness(env) {
1769217743
Err(crate::raise_type_error!(format!("Cannot delete non-configurable property '{key}'",)).into())
@@ -17766,6 +17817,29 @@ fn evaluate_expr_delete<'gc>(mc: &MutationContext<'gc>, env: &JSObjectDataPtr<'g
1776617817
}
1776717818
return Ok(Value::Boolean(true));
1776817819
}
17820+
// TypedArray [[Delete]]: numeric indices cannot be deleted
17821+
if let Some(ta_val) = slot_get(&obj, &InternalSlot::TypedArray)
17822+
&& let Value::TypedArray(ta) = &*ta_val.borrow()
17823+
{
17824+
let key_str = match &key {
17825+
PropertyKey::String(s) => Some(s.clone()),
17826+
_ => None,
17827+
};
17828+
if let Some(ref s) = key_str
17829+
&& let Some(numeric_index) = crate::js_typedarray::canonical_numeric_index_string(s)
17830+
{
17831+
if ta.buffer.borrow().detached {
17832+
return Ok(Value::Boolean(true));
17833+
}
17834+
if !crate::js_typedarray::is_valid_integer_index(ta, numeric_index) {
17835+
return Ok(Value::Boolean(true));
17836+
}
17837+
if env_get_strictness(env) {
17838+
return Err(crate::raise_type_error!(format!("Cannot delete property '{}' of a TypedArray", s)).into());
17839+
}
17840+
return Ok(Value::Boolean(false));
17841+
}
17842+
}
1776917843
if obj.borrow().non_configurable.contains(&key) {
1777017844
if env_get_strictness(env) {
1777117845
Err(crate::raise_type_error!(format!(
@@ -18367,6 +18441,10 @@ fn evaluate_expr_index<'gc>(
1836718441
| "Object.prototype.__lookupSetter__"
1836818442
| "ArrayBuffer.isView"
1836918443
| "ArrayBuffer.prototype.resize"
18444+
| "Uint8Array.fromBase64"
18445+
| "Uint8Array.fromHex"
18446+
| "Uint8Array.prototype.setFromBase64"
18447+
| "Uint8Array.prototype.setFromHex"
1837018448
| "Array.prototype.sort"
1837118449
| "encodeURI"
1837218450
| "encodeURIComponent"
@@ -18425,6 +18503,7 @@ fn evaluate_expr_index<'gc>(
1842518503
| "Math.floor"
1842618504
| "Math.fround"
1842718505
| "Math.f16round"
18506+
| "Math.sumPrecise"
1842818507
| "Math.log"
1842918508
| "Math.log1p"
1843018509
| "Math.log2"
@@ -20506,6 +20585,35 @@ pub fn call_native_function<'gc>(
2050620585
}
2050720586
}
2050820587

20588+
// arraybuffer-transfer: transfer, transferToFixedLength, detached
20589+
if name == "ArrayBuffer.prototype.transfer" || name == "ArrayBuffer.prototype.transferToFixedLength" {
20590+
let this_v = this_val.unwrap_or(&Value::Undefined);
20591+
let method_name = if name.ends_with("transferToFixedLength") {
20592+
"transferToFixedLength"
20593+
} else {
20594+
"transfer"
20595+
};
20596+
if let Value::Object(obj) = this_v {
20597+
return Ok(Some(crate::js_typedarray::handle_arraybuffer_method(
20598+
mc,
20599+
env,
20600+
obj,
20601+
method_name,
20602+
args,
20603+
)?));
20604+
} else {
20605+
return Err(raise_type_error!(format!("ArrayBuffer.prototype.{} called on non-object", method_name)).into());
20606+
}
20607+
}
20608+
20609+
if name == "get detached" || name == "ArrayBuffer.prototype.detached" {
20610+
let this_v = this_val.unwrap_or(&Value::Undefined);
20611+
if let Value::Object(obj) = this_v {
20612+
return Ok(Some(crate::js_typedarray::handle_arraybuffer_accessor(mc, obj, "detached")?));
20613+
}
20614+
return Err(raise_type_error!("Method ArrayBuffer.prototype.detached called on incompatible receiver").into());
20615+
}
20616+
2050920617
if name == "SharedArrayBuffer.prototype.byteLength" {
2051020618
let this_v = this_val.unwrap_or(&Value::Undefined);
2051120619
if let Value::Object(obj) = this_v {
@@ -20565,6 +20673,18 @@ pub fn call_native_function<'gc>(
2056520673
}
2056620674
}
2056720675

20676+
// Uint8Array prototype methods (toBase64, toHex, setFromBase64, setFromHex)
20677+
if let Some(method) = name.strip_prefix("Uint8Array.prototype.") {
20678+
let this_v = this_val.unwrap_or(&Value::Undefined);
20679+
return Ok(Some(crate::js_typedarray::handle_uint8array_proto_method(
20680+
mc, this_v, method, args, env,
20681+
)?));
20682+
}
20683+
// Uint8Array static methods (fromBase64, fromHex)
20684+
if let Some(method) = name.strip_prefix("Uint8Array.") {
20685+
return Ok(Some(crate::js_typedarray::handle_uint8array_static_method(mc, method, args, env)?));
20686+
}
20687+
2056820688
// %TypedArray%.species — get [Symbol.species]() { return this; }
2056920689
if name == "TypedArray.species" {
2057020690
let this_v = this_val.unwrap_or(&Value::Undefined);
@@ -22670,7 +22790,17 @@ fn evaluate_expr_new<'gc>(
2267022790
name_str.as_str(),
2267122791
"Error" | "ReferenceError" | "TypeError" | "RangeError" | "SyntaxError" | "EvalError" | "URIError"
2267222792
) {
22673-
let msg = eval_args.first().cloned().unwrap_or(Value::Undefined);
22793+
let raw_msg = eval_args.first().cloned().unwrap_or(Value::Undefined);
22794+
// ToString(message) per spec §20.5.1.1 step 3
22795+
let msg = if matches!(raw_msg, Value::Undefined) {
22796+
Value::Undefined
22797+
} else {
22798+
let prim = crate::core::to_primitive(mc, &raw_msg, "string", env)?;
22799+
if matches!(prim, Value::Symbol(_)) {
22800+
return Err(raise_type_error!("Cannot convert a Symbol value to a string").into());
22801+
}
22802+
Value::String(crate::unicode::utf8_to_utf16(&value_to_string(&prim)))
22803+
};
2267422804
let prototype = if let Some(proto_val) = object_get_key_value(&obj, "prototype") {
2267522805
let prototype_val = match &*proto_val.borrow() {
2267622806
Value::Property { value: Some(v), .. } => v.borrow().clone(),
@@ -22688,6 +22818,24 @@ fn evaluate_expr_new<'gc>(
2268822818
let err_val = crate::core::js_error::create_error(mc, prototype, msg)?;
2268922819
if let Value::Object(err_obj) = &err_val {
2269022820
object_set_key_value(mc, err_obj, "name", &Value::String(name.clone()))?;
22821+
// error-cause: InstallErrorCause(O, options) per §20.5.8.1
22822+
if let Some(options_val) = eval_args.get(1)
22823+
&& let Value::Object(options_obj) = options_val
22824+
{
22825+
// HasProperty(options, "cause") — must go through Proxy [[HasProperty]]
22826+
let has_cause = if let Some(proxy_cell) = crate::core::slot_get(options_obj, &InternalSlot::Proxy)
22827+
&& let Value::Proxy(proxy) = &*proxy_cell.borrow()
22828+
{
22829+
crate::js_proxy::proxy_has_property(mc, proxy, "cause")?
22830+
} else {
22831+
crate::core::has_property_key(options_obj, "cause")
22832+
};
22833+
if has_cause {
22834+
let cause_val = crate::core::eval::get_property_with_accessors(mc, env, options_obj, "cause")?;
22835+
object_set_key_value(mc, err_obj, "cause", &cause_val)?;
22836+
err_obj.borrow_mut(mc).set_non_enumerable("cause");
22837+
}
22838+
}
2269122839
}
2269222840
return Ok(err_val);
2269322841
} else if name_str == "AggregateError" {

src/core/token.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2005,9 +2005,8 @@ fn parse_string_literal(
20052005
}
20062006
} else {
20072007
// Check for unescaped line terminators in string literals (but not template literals)
2008-
if (end_char == '"' || end_char == '\'')
2009-
&& (chars[*start] == '\n' || chars[*start] == '\r' || chars[*start] == '\u{2028}' || chars[*start] == '\u{2029}')
2010-
{
2008+
// Per ES2019 (json-superset), U+2028 and U+2029 are allowed inside string literals.
2009+
if (end_char == '"' || end_char == '\'') && (chars[*start] == '\n' || chars[*start] == '\r') {
20112010
return Err(raise_tokenize_error!(
20122011
"Unterminated string literal (newline in string)",
20132012
current_line,

src/core/value.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1862,6 +1862,12 @@ pub fn get_own_property<'gc>(obj: &JSObjectDataPtr<'gc>, key: impl Into<Property
18621862
obj.borrow().properties.get(&key).cloned()
18631863
}
18641864

1865+
/// OrdinaryHasProperty: check own properties + prototype chain.
1866+
/// For Proxy objects, callers must check InternalSlot::Proxy and use proxy_has_property instead.
1867+
pub fn has_property_key<'gc>(obj: &JSObjectDataPtr<'gc>, key: impl Into<PropertyKey<'gc>>) -> bool {
1868+
object_get_key_value(obj, key).is_some()
1869+
}
1870+
18651871
pub fn object_set_key_value<'gc>(
18661872
mc: &MutationContext<'gc>,
18671873
obj: &JSObjectDataPtr<'gc>,

0 commit comments

Comments
 (0)