From 628e621550ae640ff25225dc57f236f8ad3a5fac Mon Sep 17 00:00:00 2001 From: Koji Ishimoto Date: Thu, 25 Jun 2026 23:42:55 +0900 Subject: [PATCH 1/4] runtime: migrate array-like lengths to Int64 --- interpreter/interpreter_test.mbt | 97 +++++++ interpreter/runtime/array_like.mbt | 271 +++++++++++------- interpreter/runtime/array_like_wbtest.mbt | 51 ++++ interpreter/runtime/call.mbt | 2 +- interpreter/runtime/conversions.mbt | 10 +- interpreter/runtime/conversions_wbtest.mbt | 32 +++ interpreter/runtime/iterators.mbt | 6 +- interpreter/runtime/own_property_keys.mbt | 20 +- interpreter/runtime/pkg.generated.mbti | 22 +- interpreter/runtime/property_own.mbt | 234 ++++++++------- interpreter/runtime/property_set.mbt | 45 ++- interpreter/stdlib/array_indexed_mutators.mbt | 178 ++++++++---- interpreter/stdlib/array_sort.mbt | 6 +- interpreter/stdlib/builtins_array_init.mbt | 190 ++++++------ interpreter/stdlib/builtins_arraybuffer.mbt | 10 +- interpreter/stdlib/builtins_dataview.mbt | 21 +- interpreter/stdlib/builtins_json.mbt | 2 +- .../stdlib/builtins_object_integrity.mbt | 2 +- interpreter/stdlib/builtins_reflect.mbt | 2 +- interpreter/stdlib/builtins_typedarray.mbt | 55 +++- scripts/test262_skip_metadata.json | 19 +- 21 files changed, 831 insertions(+), 444 deletions(-) diff --git a/interpreter/interpreter_test.mbt b/interpreter/interpreter_test.mbt index 44b65122..7c56a6e4 100644 --- a/interpreter/interpreter_test.mbt +++ b/interpreter/interpreter_test.mbt @@ -16456,6 +16456,56 @@ test "B.2: Array length set huge via defineProperty stays sparse" { json_inspect(output, content=["1000000", "1", "undefined"]) } +///| +test "array defineProperty sparse Int64 index stays sparse" { + let src = + #|var arr = []; + #|console.log(Reflect.defineProperty(arr, "2147483647", { value: "tail", configurable: true })); + #|console.log(arr.length); + #|console.log(arr["2147483647"]); + #|console.log(arr[0]); + let output = run_output(src) + json_inspect(output, content=["true", "2147483648", "tail", "undefined"]) +} + +///| +test "array defineProperty below non-writable Int64 length succeeds" { + let src = + #|var arr = []; + #|Object.defineProperty(arr, "length", { value: 3000000000 }); + #|Object.defineProperty(arr, "length", { writable: false }); + #|console.log(Reflect.defineProperty(arr, "0", { value: 1, configurable: true })); + #|console.log(arr[0]); + #|console.log(arr.length); + let output = run_output(src) + json_inspect(output, content=["true", "1", "3000000000"]) +} + +///| +test "array ownKeys orders sparse Int64 index before length" { + let src = + #|var arr = []; + #|arr[0] = "zero"; + #|Reflect.defineProperty(arr, "2147483648", { value: "tail", configurable: true }); + #|arr.name = "named"; + #|console.log(Reflect.ownKeys(arr).join("|")); + let output = run_output(src) + json_inspect(output, content=["0|2147483648|length|name"]) +} + +///| +test "array dense assignment updates stale sparse length override" { + let src = + #|var arr = []; + #|Object.defineProperty(arr, "length", { value: 150000 }); + #|arr[99999] = 1; + #|arr[150000] = 2; + #|console.log(arr.length); + #|console.log(arr[150000]); + let output = run_output(src) + json_inspect(output, content=["150001", "2"]) +} + ///| /// IteratorClose: iterator.return() called when for-of body throws (ForOfStmt) test "IteratorClose: body throw calls iterator return (ForOfStmt)" { @@ -19043,3 +19093,50 @@ test "Object.setPrototypeOf handles Map, Set, and Promise receivers (#452)" { "true", "undefined", "true", "undefined", "true", "true", ]) } + +///| +test "array includes uses Int64 ToLength and fromIndex near max safe integer" { + let src = + #|var obj = { + #| "9007199254740990": "needle", + #| length: 9007199254740991 + #|}; + #|console.log(Array.prototype.includes.call(obj, "needle", 9007199254740990)); + #|console.log(Array.prototype.includes.call(obj, "missing", 9007199254740990)); + #| + let output = run_output(src) + json_inspect(output, content=["true", "false"]) +} + +///| +test "array slice copies sparse properties near max safe integer" { + let src = + #|var obj = { + #| "9007199254740989": "a", + #| "9007199254740990": "b", + #| length: 9007199254740993 + #|}; + #|var result = Array.prototype.slice.call(obj, 9007199254740989); + #|console.log(result.length); + #|console.log(result[0] + "," + result[1]); + #| + let output = run_output(src) + json_inspect(output, content=["2", "a,b"]) +} + +///| +test "array toSpliced clamps ToLength before copying high sparse tail" { + let src = + #|var obj = { + #| "9007199254740989": "a", + #| "9007199254740990": "b", + #| "9007199254740991": "ignored", + #| length: 9007199254741012 + #|}; + #|var result = Array.prototype.toSpliced.call(obj, 0, 9007199254740989); + #|console.log(result.length); + #|console.log(result[0] + "," + result[1]); + #| + let output = run_output(src) + json_inspect(output, content=["2", "a,b"]) +} diff --git a/interpreter/runtime/array_like.mbt b/interpreter/runtime/array_like.mbt index ce96544d..215cf4c9 100644 --- a/interpreter/runtime/array_like.mbt +++ b/interpreter/runtime/array_like.mbt @@ -1,3 +1,18 @@ +///| +const JS_MAX_SAFE_INTEGER_I64 = 9007199254740991L + +///| +const JS_MAX_SAFE_INTEGER_DOUBLE = 9007199254740991.0 + +///| +const JS_MAX_SAFE_INTEGER_EXCLUSIVE_DOUBLE = 9007199254740992.0 + +///| +const JS_MAX_ARRAY_LENGTH_I64 = 4294967295L + +///| +const ARRAY_DENSE_MATERIALIZE_LIMIT_I64 = 10000000L + ///| /// Classification of an array-indexed lookup per ES2024 §10.1.5 /// OrdinaryGetOwnProperty. `Present` subsumes both "dense element, no @@ -61,6 +76,36 @@ fn array_index_lookup_result(data : ArrayData, i : Int) -> ArrayIndexHit { } } +///| +fn array_index_lookup_result64( + data : ArrayData, + index : Int64, +) -> ArrayIndexHit { + if index < 0L { + return OutOfRange + } + if index <= 0x7FFFFFFFL { + return array_index_lookup_result(data, index.to_int()) + } + let key = index.to_string() + match data.bag.descriptors.get(key) { + Some(desc) => + if desc.is_accessor { + OwnAccessor(desc.getter) + } else { + match data.bag.properties.get(key) { + Some(v) => Present(v) + None => OutOfRange + } + } + None => + match data.bag.properties.get(key) { + Some(v) => Present(v) + None => OutOfRange + } + } +} + ///| pub fn flatten_array_val( elements : Array[Value], @@ -81,14 +126,14 @@ pub fn flatten_array_val( } ///| -fn array_sparse_own_length_as_int(data : ArrayData) -> Int { - let mut observed = data.elements.length() +fn array_sparse_own_length(data : ArrayData) -> Int64 { + let mut observed = data.elements.length().to_int64() let include_key = fn(key : String) -> Unit { match array_index64_from_string(key) { - Some(idx64) if idx64 < 0x7FFFFFFFL => { + Some(idx64) => { let len64 = idx64 + 1L - if len64 > observed.to_int64() { - observed = len64.to_int() + if len64 > observed { + observed = len64 } } _ => () @@ -100,17 +145,17 @@ fn array_sparse_own_length_as_int(data : ArrayData) -> Int { } ///| -fn array_logical_length_as_int(data : ArrayData) -> Int { - let observed = array_sparse_own_length_as_int(data) +fn array_logical_length(data : ArrayData) -> Int64 { + let observed = array_sparse_own_length(data) match get_array_length_override(data) { - Some(n64) => if n64 <= 0x7FFFFFFFL { n64.to_int() } else { observed } + Some(n64) => n64 None => observed } } ///| /// ToLength: Get length from any value as per ECMAScript spec (array-like objects) -pub fn to_array_like_length(val : Value) -> Int raise { +pub fn to_array_like_length(val : Value) -> Int64 raise { match val { Null => raise @errors.TypeError( @@ -120,7 +165,7 @@ pub fn to_array_like_length(val : Value) -> Int raise { raise @errors.TypeError( message="Cannot convert undefined or null to object", ) - Array(data) => array_logical_length_as_int(data) + Array(data) => array_logical_length(data) Object(data) => { // Check for getter-based length first, then fall back to properties let length_val : Value? = match data.bag.properties.get("length") { @@ -145,8 +190,8 @@ pub fn to_array_like_length(val : Value) -> Int raise { } to_length_from_value(length_val) } - String_(s) => s.length() - _ => 0 + String_(s) => s.length().to_int64() + _ => 0L } } @@ -155,7 +200,7 @@ pub fn to_array_like_length(val : Value) -> Int raise { pub fn to_array_like_length_interp( val : Value, interp : Interpreter, -) -> Int raise { +) -> Int64 raise { match val { Null => raise @errors.TypeError( @@ -165,8 +210,8 @@ pub fn to_array_like_length_interp( raise @errors.TypeError( message="Cannot convert undefined or null to object", ) - Array(data) => array_logical_length_as_int(data) - String_(s) => s.length() + Array(data) => array_logical_length(data) + String_(s) => s.length().to_int64() _ => { // Use interpreter's get_property to properly handle getters and prototype chains let length_val = interp.get_property(val, "length", @token.Loc::default()) @@ -176,49 +221,37 @@ pub fn to_array_like_length_interp( } ///| -fn to_length_from_value(v : Value?, interp? : Interpreter? = None) -> Int raise { +fn to_length_number(n : Double) -> Int64 { + if n.is_nan() || n <= 0.0 { + 0L + } else if n.is_inf() || n >= JS_MAX_SAFE_INTEGER_DOUBLE { + JS_MAX_SAFE_INTEGER_I64 + } else { + n.floor().to_int64() + } +} + +///| +fn to_length_from_value( + v : Value?, + interp? : Interpreter? = None, +) -> Int64 raise { match v { - Some(Number(n)) => { - let len = n.to_int() - if len < 0 { - 0 - } else { - len - } - } - Some(v) => { - let n = to_number(v, interp~) - if n.is_nan() { - 0 - } else if n.is_inf() { - if n > 0.0 { - 0x7FFFFFFF - } else { - 0 - } - } else { - let i = n.to_int() - if i < 0 { - 0 - } else { - i - } - } - } - None => 0 + Some(Number(n)) => to_length_number(n) + Some(v) => to_length_number(to_number(v, interp~)) + None => 0L } } ///| /// Get indexed element from array-like value -pub fn get_array_like_element(val : Value, index : Int) -> Value { +pub fn get_array_like_element(val : Value, index : Int64) -> Value { let key = index.to_string() match val { Array(data) => - if index >= 0 && index < data.elements.length() { - data.elements[index] - } else { - Undefined + match array_index_lookup_result64(data, index) { + Present(v) => v + _ => Undefined } Object(data) => { // Check for accessor descriptor (getter) on own property first @@ -275,9 +308,10 @@ pub fn get_array_like_element(val : Value, index : Int) -> Value { } String_(s) => { let chars = s.to_array() - if index >= 0 && index < chars.length() { + if index >= 0L && index <= 0x7FFFFFFFL && index.to_int() < chars.length() { + let i = index.to_int() let buf = StringBuilder::new() - buf.write_char(chars[index]) + buf.write_char(chars[i]) String_(buf.to_string()) } else { Undefined @@ -292,11 +326,11 @@ pub fn get_array_like_element(val : Value, index : Int) -> Value { pub fn get_array_like_element_interp( interp : Interpreter, val : Value, - index : Int, + index : Int64, ) -> Value raise { match val { Array(data) => - match array_index_lookup_result(data, index) { + match array_index_lookup_result64(data, index) { Present(v) => v OwnAccessor(Some(getter)) => interp.call_value(getter, val, [], @token.Loc::default()) @@ -325,7 +359,7 @@ fn proto_chain_has_index( interp : Interpreter, proto : Value, key : String, - index : Int, + index : Int64, ) -> Bool { let mut current = proto while true { @@ -340,7 +374,7 @@ fn proto_chain_has_index( } } Array(arr_data) => - match array_index_lookup_result(arr_data, index) { + match array_index_lookup_result64(arr_data, index) { Present(_) | OwnAccessor(_) => return true Hole | OutOfRange => // Array-as-prototype: continue walking via the array's own @@ -360,12 +394,12 @@ fn proto_chain_has_index( pub fn has_array_like_element( interp : Interpreter, val : Value, - index : Int, + index : Int64, ) -> Bool { let key = index.to_string() match val { Array(data) => - match array_index_lookup_result(data, index) { + match array_index_lookup_result64(data, index) { Present(_) | OwnAccessor(_) => true Hole | OutOfRange => { let proto = get_array_prototype(interp.realm_state, data) @@ -400,7 +434,7 @@ pub fn has_array_like_element( // as a prototype only owns its dense slots; missing indices // must fall through to the next prototype (typically // Array.prototype, then Object.prototype). - match array_index_lookup_result(arr_data, index) { + match array_index_lookup_result64(arr_data, index) { Present(_) | OwnAccessor(_) => return true Hole | OutOfRange => current = get_array_prototype(interp.realm_state, arr_data) @@ -410,7 +444,9 @@ pub fn has_array_like_element( } false } - String_(s) => index >= 0 && index < s.length() + String_(s) => + index >= 0L && index <= 0x7FFFFFFFL && index.to_int() < s.length() + Proxy(_) => interp.has_property(val, key) catch { _ => false } _ => false } } @@ -488,13 +524,22 @@ fn cleanup_sparse_array_indices_above_length( ///| /// Set indexed element on array-like value -pub fn set_array_like_element(val : Value, index : Int, value : Value) -> Unit { +pub fn set_array_like_element( + val : Value, + index : Int64, + value : Value, +) -> Unit { let key = index.to_string() match val { Array(data) => - if index >= 0 && index < data.elements.length() { - data.elements[index] = value - data.holes.remove(index) + if index >= 0L && + index <= 0x7FFFFFFFL && + index.to_int() < data.elements.length() { + let i = index.to_int() + data.elements[i] = value + data.holes.remove(i) + } else if index >= 0L { + data.bag.properties[key] = value } Object(data) => data.bag.properties[key] = value _ => () @@ -503,14 +548,17 @@ pub fn set_array_like_element(val : Value, index : Int, value : Value) -> Unit { ///| /// Set the length property on an array-like value -pub fn set_array_like_length(val : Value, len : Int) -> Unit { +pub fn set_array_like_length(val : Value, len : Int64) -> Unit { match val { - Array(data) => - while data.elements.length() < len { - let new_idx = data.elements.length() - data.elements.push(Undefined) - data.holes[new_idx] = () + Array(data) => { + if len > data.elements.length().to_int64() { + set_array_length_override(data, len) + } else { + data.elements.truncate(len.to_int()) + clear_array_length_override(data) } + cleanup_holes_above_length(data.holes, len) + } Object(data) => data.bag.properties["length"] = Number(len.to_double()) _ => () } @@ -533,34 +581,42 @@ pub fn delete_array_like_element(val : Value, index : Int) -> Unit { pub fn create_data_property_or_throw( interp : Interpreter, target : Value, - index : Int, + index : Int64, value : Value, ) -> Unit raise { match target { - Array(data) => { - if index == data.elements.length() { - data.elements.push(value) - } else if index >= 0 && index < data.elements.length() { - data.elements[index] = value - } else { - // Sparse case: pad with Undefined up to index (these are holes), - // then set value at the target index (not a hole). - while data.elements.length() < index { - let pad_idx = data.elements.length() - data.elements.push(Undefined) - data.holes[pad_idx] = () + Array(data) => + if index >= 0L && index <= ARRAY_DENSE_MATERIALIZE_LIMIT_I64 { + let i = index.to_int() + if i == data.elements.length() { + data.elements.push(value) + } else if i >= 0 && i < data.elements.length() { + data.elements[i] = value + } else { + // Sparse case: pad with Undefined up to index (these are holes), + // then set value at the target index (not a hole). + while data.elements.length() < i { + let pad_idx = data.elements.length() + data.elements.push(Undefined) + data.holes[pad_idx] = () + } + data.elements.push(value) } - data.elements.push(value) + data.holes.remove(i) + } else { + let _ = interp.define_own_property( + target, + String_(index.to_string()), + PartialDescriptor::data_default(value), + @token.Loc::default(), + ) } - data.holes.remove(index) - } _ => { - let _ = interp.set_property( + let _ = interp.define_own_property( target, - index.to_string(), - value, + String_(index.to_string()), + PartialDescriptor::data_default(value), @token.Loc::default(), - strict=true, ) } } @@ -574,15 +630,35 @@ pub fn create_data_property_or_throw( pub fn array_species_create( interp : Interpreter, original : Value, - len : Int, + len : Int64, ) -> Value raise Error { - // Step 1: If originalArray is not an Array, return a plain array - let is_array = match original { - Array(_) => true - Object(data) => data.class_name == "Array" - _ => false + // Step 1: If originalArray is not an Array, return a plain array. + // IsArray unwraps proxies, so ArraySpeciesCreate must do the same before + // deciding whether to consult constructor/@@species. + let mut array_probe = original + let mut is_array = false + while true { + match array_probe { + Array(_) => { + is_array = true + break + } + Object(data) => { + is_array = data.class_name == "Array" + break + } + Proxy(proxy_data) => + match proxy_data.target { + Some(target) => array_probe = target + None => break + } + _ => break + } } if !is_array { + if len > JS_MAX_ARRAY_LENGTH_I64 { + raise @errors.RangeError(message="Invalid array length") + } return make_array([]) } let loc = @token.Loc::default() @@ -604,6 +680,9 @@ pub fn array_species_create( } // Step 5: If C is undefined, return plain array if species is Undefined { + if len > JS_MAX_ARRAY_LENGTH_I64 { + raise @errors.RangeError(message="Invalid array length") + } return make_array([]) } // Step 6: If IsConstructor(C) is false, throw TypeError @@ -619,7 +698,7 @@ pub fn array_species_create( pub fn to_array_like_elements(val : Value) -> Array[Value] raise { let len = to_array_like_length(val) let result : Array[Value] = [] - for i = 0; i < len; i = i + 1 { + for i = 0L; i < len; i = i + 1L { result.push(get_array_like_element(val, i)) } result diff --git a/interpreter/runtime/array_like_wbtest.mbt b/interpreter/runtime/array_like_wbtest.mbt index 3645dab0..60d6ffad 100644 --- a/interpreter/runtime/array_like_wbtest.mbt +++ b/interpreter/runtime/array_like_wbtest.mbt @@ -143,3 +143,54 @@ test "lookup: own write-only accessor returns OwnAccessor(None)" { _ => fail("expected OwnAccessor(None)") } } + +///| +test "to_array_like_length: keeps max-safe object length as Int64" { + let bag = PropertyBag::PropertyBag() + bag.properties["length"] = Number(9007199254740991.0) + let obj = Object({ + bag, + prototype: Null, + callable: None, + class_name: "Object", + extensible: true, + arraybuffer_state: None, + }) + let len = to_array_like_length(obj) + guard len == 9007199254740991L else { + fail("expected max-safe length, got " + len.to_string()) + } +} + +///| +test "to_array_like_length: sparse array index above Int max contributes to length" { + let bag = PropertyBag::PropertyBag() + bag.properties["2147483648"] = Number(1.0) + let data : ArrayData = { + elements: [], + bag, + length_writable: true, + holes: {}, + extensible: true, + } + let len = to_array_like_length(Array(data)) + guard len == 2147483649L else { + fail("expected sparse length above Int max, got " + len.to_string()) + } +} + +///| +test "to_array_like_length: array length override remains Int64" { + let data : ArrayData = { + elements: [], + bag: PropertyBag::PropertyBag(), + length_writable: true, + holes: {}, + extensible: true, + } + set_array_length_override(data, 9007199254740991L) + let len = to_array_like_length(Array(data)) + guard len == 9007199254740991L else { + fail("expected override length, got " + len.to_string()) + } +} diff --git a/interpreter/runtime/call.mbt b/interpreter/runtime/call.mbt index ff724e2f..3d2a4641 100644 --- a/interpreter/runtime/call.mbt +++ b/interpreter/runtime/call.mbt @@ -1059,7 +1059,7 @@ fn Interpreter::call_value_impl( _ => { // apply accepts any array-like object (including `arguments`). let arg_len = to_array_like_length(arg_array) - for i in 0..= 2^53. /// Returns 0 for undefined. -pub fn to_index(val : Value, interp? : Interpreter? = None) -> Int raise { +pub fn to_index(val : Value, interp? : Interpreter? = None) -> Int64 raise { match val { - Undefined => 0 + Undefined => 0L _ => { let n = to_number(val, interp~) let integer_index = if n.is_nan() || n == 0.0 { @@ -1015,10 +1015,10 @@ pub fn to_index(val : Value, interp? : Interpreter? = None) -> Int raise { } if integer_index.is_inf() || integer_index < 0.0 || - integer_index >= 9007199254740992.0 { + integer_index >= JS_MAX_SAFE_INTEGER_EXCLUSIVE_DOUBLE { raise @errors.RangeError(message="Invalid index") } - integer_index.to_int() + integer_index.to_int64() } } } @@ -1028,7 +1028,7 @@ pub fn to_index(val : Value, interp? : Interpreter? = None) -> Int raise { pub fn Interpreter::to_index( self : Interpreter, val : Value, -) -> Int raise Error { +) -> Int64 raise Error { to_index(val, interp=Some(self)) } diff --git a/interpreter/runtime/conversions_wbtest.mbt b/interpreter/runtime/conversions_wbtest.mbt index 407e1c27..e7476677 100644 --- a/interpreter/runtime/conversions_wbtest.mbt +++ b/interpreter/runtime/conversions_wbtest.mbt @@ -447,3 +447,35 @@ test "to_js_string - symbol raises TypeError" { fail("expected TypeError when converting Symbol to string") } } + +///| +test "to_index keeps max-safe index as Int64" { + let idx = to_index(Number(9007199254740991.0)) + guard idx == 9007199254740991L else { + fail("expected max-safe index, got " + idx.to_string()) + } +} + +///| +test "to_index rejects values at 2^53" { + let raised = try { + let _ = to_index(Number(9007199254740992.0)) + false + } catch { + @errors.RangeError(_) => true + _ => false + } + guard raised else { fail("expected RangeError for 2^53 index") } +} + +///| +test "to_index rejects negative indexes" { + let raised = try { + let _ = to_index(Number(-1.0)) + false + } catch { + @errors.RangeError(_) => true + _ => false + } + guard raised else { fail("expected RangeError for negative index") } +} diff --git a/interpreter/runtime/iterators.mbt b/interpreter/runtime/iterators.mbt index 14541da8..c20092f4 100644 --- a/interpreter/runtime/iterators.mbt +++ b/interpreter/runtime/iterators.mbt @@ -120,7 +120,7 @@ fn array_iterator_type_error() -> Value raise Error { ///| fn array_iterator_result_value( kind : ArrayIteratorKind, - current : Int, + current : Int64, target : Value, interp : Interpreter, ) -> Value raise Error { @@ -153,7 +153,7 @@ fn array_iterator_next( ) { (Some(Number(n)), Some(target), Some(kind_value)) => match array_iterator_kind_from_slot_value(kind_value) { - Some(kind) => (n.to_int(), target, kind) + Some(kind) => (n.to_int64(), target, kind) None => return array_iterator_type_error() } _ => return array_iterator_type_error() @@ -167,7 +167,7 @@ fn array_iterator_next( return make_iterator_result_object(Undefined, true) } data.bag.symbol_properties[ARRAY_ITERATOR_NEXT_INDEX_SYMBOL_ID] = Number( - (current + 1).to_double(), + (current + 1L).to_double(), ) let value = array_iterator_result_value(kind, current, target, interp) make_iterator_result_object(value, false) diff --git a/interpreter/runtime/own_property_keys.mbt b/interpreter/runtime/own_property_keys.mbt index f9c4617b..ed67d974 100644 --- a/interpreter/runtime/own_property_keys.mbt +++ b/interpreter/runtime/own_property_keys.mbt @@ -11,10 +11,8 @@ /// keys in insertion order, then symbol keys in insertion order; Array holes /// are omitted everywhere (§10.4.2.1). /// -/// Known limitation (pre-existing, out of scope for the #336 unification): -/// Array index keys beyond `Int` range (≥ 2^31, stored on the bag rather than -/// in `elements`) are emitted among the trailing string keys, after "length", -/// instead of merged into the ascending integer-index run. +/// Sparse Array index keys stored on the bag are merged into the ascending +/// integer-index run before the exotic "length" key. ///| /// Append the own symbol keys of a property bag (resolved id -> SymbolData) to @@ -56,11 +54,19 @@ fn append_array_own_keys( keys.push(Value::String_(i.to_string())) } } + let sorted_bag_keys = sort_property_keys(data.bag.properties) + for prop_name in sorted_bag_keys { + match array_index_of_key(prop_name) { + Some(idx) if idx >= element_len.to_int64() => + keys.push(Value::String_(prop_name)) + _ => () + } + } keys.push(Value::String_("length")) - for prop_name in sort_property_keys(data.bag.properties) { + for prop_name in sorted_bag_keys { match array_index_of_key(prop_name) { - Some(idx) if idx < element_len => continue - _ => keys.push(Value::String_(prop_name)) + Some(_) => () + None => keys.push(Value::String_(prop_name)) } } append_resolved_symbol_keys(keys, data.bag.symbol_properties, sym_state) diff --git a/interpreter/runtime/pkg.generated.mbti b/interpreter/runtime/pkg.generated.mbti index 6664cd4f..24c1da1f 100644 --- a/interpreter/runtime/pkg.generated.mbti +++ b/interpreter/runtime/pkg.generated.mbti @@ -27,7 +27,7 @@ pub fn apply_object_literal_proto_property(Value, Value) -> Unit raise pub fn apply_object_literal_static_data_property(Value, String, Value) -> Unit raise -pub fn array_species_create(Interpreter, Value, Int) -> Value raise +pub fn array_species_create(Interpreter, Value, Int64) -> Value raise pub fn builtin_method_desc() -> PropDescriptor @@ -43,7 +43,7 @@ pub fn collect_for_in_keys(Value, Interpreter) -> Array[String] raise pub fn compound_assign_binary_op(@ast.CompoundOp) -> @ast.BinOp? -pub fn create_data_property_or_throw(Interpreter, Value, Int, Value) -> Unit raise +pub fn create_data_property_or_throw(Interpreter, Value, Int64, Value) -> Unit raise pub fn create_iter_result(Value, Bool) -> Value @@ -81,9 +81,9 @@ pub fn get_array_iterator_override(ArrayData, well_known_symbols~ : WellKnownSym pub fn get_array_length_override(ArrayData) -> Int64? -pub fn get_array_like_element(Value, Int) -> Value +pub fn get_array_like_element(Value, Int64) -> Value -pub fn get_array_like_element_interp(Interpreter, Value, Int) -> Value raise +pub fn get_array_like_element_interp(Interpreter, Value, Int64) -> Value raise pub fn get_array_named_prop(ArrayData, String) -> Value? @@ -155,7 +155,7 @@ pub fn get_typedarray_byte_offset(ObjectData) -> Value? pub fn get_typedarray_viewed_buffer(ObjectData) -> Value? -pub fn has_array_like_element(Interpreter, Value, Int) -> Bool +pub fn has_array_like_element(Interpreter, Value, Int64) -> Bool pub fn has_parameter_expressions(Array[@ast.Param]) -> Bool @@ -295,9 +295,9 @@ pub fn set_array_iterator_override(ArrayData, well_known_symbols~ : WellKnownSym pub fn set_array_length_override(ArrayData, Int64) -> Unit -pub fn set_array_like_element(Value, Int, Value) -> Unit +pub fn set_array_like_element(Value, Int64, Value) -> Unit -pub fn set_array_like_length(Value, Int) -> Unit +pub fn set_array_like_length(Value, Int64) -> Unit pub fn set_array_named_prop(ArrayData, String, Value) -> Unit @@ -337,11 +337,11 @@ pub fn test_integrity_sealed(Value) -> Value pub fn to_array_like_elements(Value) -> Array[Value] raise -pub fn to_array_like_length(Value) -> Int raise +pub fn to_array_like_length(Value) -> Int64 raise -pub fn to_array_like_length_interp(Value, Interpreter) -> Int raise +pub fn to_array_like_length_interp(Value, Interpreter) -> Int64 raise -pub fn to_index(Value, interp? : Interpreter?) -> Int raise +pub fn to_index(Value, interp? : Interpreter?) -> Int64 raise pub fn to_int32(Double) -> Int @@ -668,7 +668,7 @@ pub fn Interpreter::run_timers(Self) -> Unit raise pub fn Interpreter::set_computed_property(Self, Value, Value, Value, @token.Loc, strict? : Bool, receiver? : Value) -> Value raise pub fn Interpreter::set_property(Self, Value, String, Value, @token.Loc, strict? : Bool, receiver? : Value) -> Value raise pub fn Interpreter::spread_iterable(Self, Value, @token.Loc) -> Array[Value] raise -pub fn Interpreter::to_index(Self, Value) -> Int raise +pub fn Interpreter::to_index(Self, Value) -> Int64 raise pub fn Interpreter::to_js_string(Self, Value) -> String raise pub fn Interpreter::to_number(Self, Value) -> Double raise pub fn Interpreter::to_object_literal_property_key(Self, Value) -> Value raise diff --git a/interpreter/runtime/property_own.mbt b/interpreter/runtime/property_own.mbt index 575eae2d..b3f869bc 100644 --- a/interpreter/runtime/property_own.mbt +++ b/interpreter/runtime/property_own.mbt @@ -2,15 +2,10 @@ /// If `key` is a canonical array-index string — a non-negative integer with no /// redundant representation (e.g. "0" or "42", but not "01", "-0", or "1.0") — /// return that index, else `None`. Mirrors the partition test used throughout -/// property enumeration. Indices beyond `Int` range are not represented here; -/// the 64-bit array-length path uses its own `Int64` check. -fn array_index_of_key(key : String) -> Int? { - let idx = @string.parse_int(key) catch { _ => -1 } - if idx >= 0 && idx.to_string() == key { - Some(idx) - } else { - None - } +/// property enumeration, including sparse array indices beyond MoonBit `Int` +/// range. +fn array_index_of_key(key : String) -> Int64? { + array_index64_from_string(key) } ///| @@ -27,9 +22,15 @@ pub fn sort_property_keys(props : Map[String, Value]) -> Array[String] { } }) int_keys.sort_by(fn(a, b) { - let na = @string.parse_int(a) catch { _ => 0 } - let nb = @string.parse_int(b) catch { _ => 0 } - na - nb + let na = array_index_of_key(a).unwrap_or(0L) + let nb = array_index_of_key(b).unwrap_or(0L) + if na < nb { + -1 + } else if na > nb { + 1 + } else { + 0 + } }) [..int_keys, ..str_keys] } @@ -997,86 +998,12 @@ pub fn Interpreter::array_define_own_property( if k == "length" { return self.array_set_length(arr, partial) } - // Is this an array index? - let idx = @string.parse_int(k) catch { _ => -1 } - let is_index = idx >= 0 && idx.to_string() == k - if is_index { - // Check length writable if we're extending. Compare against the - // logical length (override when sparse), not physical elements count. - let logical_len = match get_array_length_override(arr) { - Some(n64) => n64.to_int() - None => arr.elements.length() - } - if idx >= logical_len && !arr.length_writable { - return false - } - let had_index = idx < arr.elements.length() && !arr.holes.contains(idx) - if !had_index && !arr.extensible { - return false - } - // Grow elements to idx+1 with Undefined filler. Intermediate indices - // (old_length..idx) become spec-level holes; the target index `idx` - // is real and has its hole flag cleared below. - while arr.elements.length() < idx { - let pad_idx = arr.elements.length() - arr.elements.push(Undefined) - arr.holes[pad_idx] = () - } - while arr.elements.length() <= idx { - arr.elements.push(Undefined) - } - match arr.bag.descriptors.get(k) { - Some(current) => { - if !current.configurable { - if !is_compatible_with_non_configurable( - current, - partial, - arr.elements[idx], - ) { - return false - } - } - let merged = merge_partial_into(current, partial) - // Clear fields that become invalid after a descriptor-kind transition. - arr.bag.descriptors[k] = if !merged.is_accessor && - current.is_accessor { - { ..merged, getter: None, setter: None } - } else if merged.is_accessor && !current.is_accessor { - { ..merged, writable: false } - } else { - merged - } - } - None => - if had_index { - let current = { - writable: true, - enumerable: true, - configurable: true, - getter: None, - setter: None, - is_accessor: false, - } - arr.bag.descriptors[k] = merge_partial_into(current, partial) - } else { - arr.bag.descriptors[k] = complete_partial_for_new(partial) - } - } - arr.holes.remove(idx) - match partial.value { - Some(v) => arr.elements[idx] = v - None => () - } - return true - } - // Check for large valid array index: integers in [2^31, 2^32-2] that - // parse_int can't handle. Use Double to detect them, then store in the - // bag (can't materialise elements for a 4-billion-slot array) while - // still updating the logical length override. - let large_idx64 : Int64? = try { + // Is this an array index? Parse through Int64 so large sparse indices + // never wrap through MoonBit Int or force dense materialization. + let index64 : Int64? = try { let n = @string.parse_double(k) let i64 = n.to_int64() - if n >= 2147483648.0 && + if n >= 0.0 && n == i64.to_double() && i64 <= 4294967294L && i64.to_string() == k { @@ -1087,50 +1014,117 @@ pub fn Interpreter::array_define_own_property( } catch { _ => None } - match large_idx64 { + match index64 { Some(idx64) => { - let logical_len = match get_array_length_override(arr) { + let existing_override = get_array_length_override(arr) + let logical_len = match existing_override { Some(n64) => n64 None => arr.elements.length().to_int64() } if idx64 >= logical_len && !arr.length_writable { return false } - let existing = ordinary_get_own_string_desc(arr.bag, k) - match existing { - None => { - if !arr.extensible { - return false - } - let full = complete_partial_for_new(partial) - apply_descriptor_to_bag(arr.bag, key, full, partial.value) + if idx64 <= ARRAY_DENSE_MATERIALIZE_LIMIT_I64 { + let idx = idx64.to_int() + let had_index = idx < arr.elements.length() && + !arr.holes.contains(idx) + if !had_index && !arr.extensible { + return false } - Some(current) => { - if !current.configurable { - let cur_val = match arr.bag.properties.get(k) { - Some(v) => v - None => Undefined + // Grow elements to idx+1 with Undefined filler. Intermediate + // indices (old_length..idx) become spec-level holes; the target + // index `idx` is real and has its hole flag cleared below. + while arr.elements.length() < idx { + let pad_idx = arr.elements.length() + arr.elements.push(Undefined) + arr.holes[pad_idx] = () + } + while arr.elements.length() <= idx { + arr.elements.push(Undefined) + } + match arr.bag.descriptors.get(k) { + Some(current) => { + if !current.configurable { + if !is_compatible_with_non_configurable( + current, + partial, + arr.elements[idx], + ) { + return false + } + } + let merged = merge_partial_into(current, partial) + // Clear fields that become invalid after a descriptor-kind transition. + arr.bag.descriptors[k] = if !merged.is_accessor && + current.is_accessor { + { ..merged, getter: None, setter: None } + } else if merged.is_accessor && !current.is_accessor { + { ..merged, writable: false } + } else { + merged + } + } + None => + if had_index { + let current = { + writable: true, + enumerable: true, + configurable: true, + getter: None, + setter: None, + is_accessor: false, + } + arr.bag.descriptors[k] = merge_partial_into(current, partial) + } else { + arr.bag.descriptors[k] = complete_partial_for_new(partial) } - if !is_compatible_with_non_configurable( - current, partial, cur_val, - ) { + } + arr.holes.remove(idx) + match partial.value { + Some(v) => arr.elements[idx] = v + None => () + } + let new_len = idx64 + 1L + if existing_override is Some(_) && new_len > logical_len { + set_array_length_override(arr, new_len) + } + } else { + let existing = ordinary_get_own_string_desc(arr.bag, k) + match existing { + None => { + if !arr.extensible { return false } + let full = complete_partial_for_new(partial) + apply_descriptor_to_bag(arr.bag, key, full, partial.value) } - let merged = merge_partial_into(current, partial) - let final_desc = if !merged.is_accessor && current.is_accessor { - { ..merged, getter: None, setter: None } - } else if merged.is_accessor && !current.is_accessor { - { ..merged, writable: false } - } else { - merged + Some(current) => { + if !current.configurable { + let cur_val = match arr.bag.properties.get(k) { + Some(v) => v + None => Undefined + } + if !is_compatible_with_non_configurable( + current, partial, cur_val, + ) { + return false + } + } + let merged = merge_partial_into(current, partial) + let final_desc = if !merged.is_accessor && current.is_accessor { + { ..merged, getter: None, setter: None } + } else if merged.is_accessor && !current.is_accessor { + { ..merged, writable: false } + } else { + merged + } + apply_descriptor_to_bag(arr.bag, key, final_desc, partial.value) } - apply_descriptor_to_bag(arr.bag, key, final_desc, partial.value) } - } - let new_len = idx64 + 1L - if new_len > logical_len { - set_array_length_override(arr, new_len) + let new_len = idx64 + 1L + if new_len > logical_len { + set_array_length_override(arr, new_len) + } } return true } diff --git a/interpreter/runtime/property_set.mbt b/interpreter/runtime/property_set.mbt index 4e43f641..613e66f2 100644 --- a/interpreter/runtime/property_set.mbt +++ b/interpreter/runtime/property_set.mbt @@ -95,6 +95,21 @@ fn Interpreter::set_array_sparse_index64( value } +///| +fn update_array_length_after_dense_index_write( + data : ArrayData, + new_len : Int64, + logical_len64 : Int64, +) -> Unit { + if new_len > logical_len64 && + ( + get_array_length_override(data) is Some(_) || + new_len > data.elements.length().to_int64() + ) { + set_array_length_override(data, new_len) + } +} + ///| /// Stage B.1 [[Set]] dispatcher. /// @@ -425,11 +440,11 @@ pub fn Interpreter::set_property( } else { set_array_named_prop(data, prop, value) } - let new_len = i.to_int64() + 1L - if new_len > logical_len64 && - new_len > data.elements.length().to_int64() { - set_array_length_override(data, new_len) - } + update_array_length_after_dense_index_write( + data, + i.to_int64() + 1L, + logical_len64, + ) } else { match array_index64_from_string(prop) { Some(idx64) => @@ -1890,11 +1905,11 @@ pub fn Interpreter::set_computed_property( } else { set_array_named_prop(data, prop, value) } - let new_len = i.to_int64() + 1L - if new_len > logical_len64 && - new_len > data.elements.length().to_int64() { - set_array_length_override(data, new_len) - } + update_array_length_after_dense_index_write( + data, + i.to_int64() + 1L, + logical_len64, + ) } else { // Non-integer numeric key (e.g., 1.1, -0.5): delegate to set_property // to enforce descriptor, extensibility, and prototype-chain checks. @@ -2022,11 +2037,11 @@ pub fn Interpreter::set_computed_property( } else { set_array_named_prop(data, prop, value) } - let new_len = idx.to_int64() + 1L - if new_len > logical_len64 && - new_len > data.elements.length().to_int64() { - set_array_length_override(data, new_len) - } + update_array_length_after_dense_index_write( + data, + idx.to_int64() + 1L, + logical_len64, + ) handled = true } } diff --git a/interpreter/stdlib/array_indexed_mutators.mbt b/interpreter/stdlib/array_indexed_mutators.mbt index c390af14..df930715 100644 --- a/interpreter/stdlib/array_indexed_mutators.mbt +++ b/interpreter/stdlib/array_indexed_mutators.mbt @@ -2,7 +2,7 @@ fn array_mutator_set_index( interp : Interpreter, o : Value, - index : Int, + index : Int64, value : Value, ) -> Unit raise Error { array_mutator_set_property_key(interp, o, index.to_string(), value) @@ -12,7 +12,7 @@ fn array_mutator_set_index( fn array_mutator_delete_index( interp : Interpreter, o : Value, - index : Int, + index : Int64, ) -> Unit raise Error { array_mutator_delete_property_key(interp, o, index.to_string()) } @@ -21,7 +21,7 @@ fn array_mutator_delete_index( fn array_mutator_has_index( interp : Interpreter, o : Value, - index : Int, + index : Int64, ) -> Bool raise Error { interp.has_property(o, index.to_string()) } @@ -30,7 +30,7 @@ fn array_mutator_has_index( fn array_mutator_set_length( interp : Interpreter, o : Value, - len : Int, + len : Int64, ) -> Unit raise Error { array_mutator_set_length_number(interp, o, len.to_double()) } @@ -38,6 +38,69 @@ fn array_mutator_set_length( ///| const ARRAY_MAX_SAFE_LENGTH : Double = 9007199254740991.0 +///| +const ARRAY_MAX_SAFE_LENGTH_I64 = 9007199254740991L + +///| +const ARRAY_MAX_ARRAY_LENGTH_I64 = 4294967295L + +///| +const ARRAY_DENSE_MATERIALIZE_LIMIT_I64 = 10000000L + +///| +fn array_mutator_to_integer_index_from_number(n : Double) -> Int64 { + if n.is_nan() || n == 0.0 { + 0L + } else if n.is_inf() { + if n < 0.0 { + -ARRAY_MAX_SAFE_LENGTH_I64 + } else { + ARRAY_MAX_SAFE_LENGTH_I64 + } + } else { + let sign = if n < 0.0 { -1.0 } else { 1.0 } + let integer = n.abs().floor() * sign + if integer <= -ARRAY_MAX_SAFE_LENGTH { + -ARRAY_MAX_SAFE_LENGTH_I64 + } else if integer >= ARRAY_MAX_SAFE_LENGTH { + ARRAY_MAX_SAFE_LENGTH_I64 + } else { + integer.to_int64() + } + } +} + +///| +fn array_mutator_to_integer_index( + interp : Interpreter, + val : Value, +) -> Int64 raise Error { + array_mutator_to_integer_index_from_number(interp.to_number(val)) +} + +///| +fn array_mutator_relative_index64(index : Int64, len : Int64) -> Int64 { + if index < 0L { + let from_end = len + index + if from_end < 0L { + 0L + } else { + from_end + } + } else if index > len { + len + } else { + index + } +} + +///| +fn array_mutator_require_array_create_length(len : Int64) -> Unit raise Error { + if len > ARRAY_MAX_ARRAY_LENGTH_I64 { + raise @errors.RangeError(message="Invalid array length") + } +} + ///| fn array_mutator_array_index64_from_key(key : String) -> Int64? { let idx = @string.parse_int(key) catch { _ => -1 } @@ -421,9 +484,9 @@ fn array_reverse_indexed_properties( // ES2024 §23.1.3.28: query lower/upper presence separately, then use // Set/DeletePropertyOrThrow so explicit undefined and holes swap correctly. let len = @runtime.to_array_like_length_interp(o, interp) - let middle = len / 2 - for lower = 0; lower < middle; lower = lower + 1 { - let upper = len - lower - 1 + let middle = len / 2L + for lower = 0L; lower < middle; lower = lower + 1L { + let upper = len - lower - 1L let lower_exists = array_mutator_has_index(interp, o, lower) let lower_value = if lower_exists { Some(@runtime.get_array_like_element_interp(interp, o, lower)) @@ -465,19 +528,29 @@ fn array_copy_within_indexed_properties( // delete the target instead of materializing undefined. let len = @runtime.to_array_like_length_interp(o, interp) let mut to = if args.length() > 0 { - array_mutator_relative_index(interp.to_number(args[0]).to_int(), len) + array_mutator_relative_index64( + array_mutator_to_integer_index(interp, args[0]), + len, + ) } else { - 0 + 0L } let mut from = if args.length() > 1 { - array_mutator_relative_index(interp.to_number(args[1]).to_int(), len) + array_mutator_relative_index64( + array_mutator_to_integer_index(interp, args[1]), + len, + ) } else { - 0 + 0L } let final_index = if args.length() > 2 { match args[2] { Undefined => len - _ => array_mutator_relative_index(interp.to_number(args[2]).to_int(), len) + _ => + array_mutator_relative_index64( + array_mutator_to_integer_index(interp, args[2]), + len, + ) } } else { len @@ -490,13 +563,13 @@ fn array_copy_within_indexed_properties( if count <= 0 { return o } - let mut direction = 1 + let mut direction = 1L if from < to && to < from + count { - direction = -1 - from = from + count - 1 - to = to + count - 1 + direction = -1L + from = from + count - 1L + to = to + count - 1L } - while count > 0 { + while count > 0L { if array_mutator_has_index(interp, o, from) { let from_value = @runtime.get_array_like_element_interp(interp, o, from) array_mutator_set_index(interp, o, to, from_value) @@ -505,7 +578,7 @@ fn array_copy_within_indexed_properties( } from = from + direction to = to + direction - count = count - 1 + count = count - 1L } o } @@ -521,19 +594,26 @@ fn array_fill_indexed_properties( let fill_value = if args.length() > 0 { args[0] } else { Undefined } let len = @runtime.to_array_like_length_interp(o, interp) let start = if args.length() > 1 { - array_mutator_relative_index(interp.to_number(args[1]).to_int(), len) + array_mutator_relative_index64( + array_mutator_to_integer_index(interp, args[1]), + len, + ) } else { - 0 + 0L } let final_index = if args.length() > 2 { match args[2] { Undefined => len - _ => array_mutator_relative_index(interp.to_number(args[2]).to_int(), len) + _ => + array_mutator_relative_index64( + array_mutator_to_integer_index(interp, args[2]), + len, + ) } } else { len } - for k = start; k < final_index; k = k + 1 { + for k = start; k < final_index; k = k + 1L { array_mutator_set_index(interp, o, k, fill_value) } o @@ -743,36 +823,35 @@ fn array_splice_indexed_properties( ) -> Value raise Error { let alen = @runtime.to_array_like_length_interp(o, interp) let mut start = if args.length() > 0 { - interp.to_number(args[0]).to_int() + array_mutator_to_integer_index(interp, args[0]) } else { - 0 + 0L } - if start < 0 { + if start < 0L { start = alen + start - if start < 0 { - start = 0 + if start < 0L { + start = 0L } } if start > alen { start = alen } - let del_count = if args.length() > 1 { - let d_num = interp.to_number(args[1]) - if d_num.is_inf() || d_num >= (alen - start).to_double() { + let del_count = if args.length() == 0 { + 0L + } else if args.length() == 1 { + alen - start + } else { + let d = array_mutator_to_integer_index(interp, args[1]) + if d < 0L { + 0L + } else if d > alen - start { alen - start } else { - let d = d_num.to_int() - if d < 0 { - 0 - } else { - d - } + d } - } else { - alen - start } let removed = @runtime.array_species_create(interp, o, del_count) - for i = 0; i < del_count; i = i + 1 { + for i = 0L; i < del_count; i = i + 1L { if array_mutator_has_index(interp, o, start + i) { @runtime.create_data_property_or_throw( interp, @@ -787,10 +866,13 @@ fn array_splice_indexed_properties( for i = 2; i < args.length(); i = i + 1 { new_items.push(args[i]) } - let items_count = new_items.length() + let items_count = new_items.length().to_int64() let new_len = alen - del_count + items_count + if new_len > ARRAY_MAX_SAFE_LENGTH_I64 { + raise @errors.TypeError(message="Invalid array length") + } if items_count < del_count { - for i = start; i < alen - del_count; i = i + 1 { + for i = start; i < alen - del_count; i = i + 1L { let from = i + del_count let to = i + items_count if array_mutator_has_index(interp, o, from) { @@ -800,13 +882,13 @@ fn array_splice_indexed_properties( array_mutator_delete_index(interp, o, to) } } - for i = alen; i > new_len; i = i - 1 { - array_mutator_delete_index(interp, o, i - 1) + for i = alen; i > new_len; i = i - 1L { + array_mutator_delete_index(interp, o, i - 1L) } } else if items_count > del_count { - for i = alen - del_count; i > start; i = i - 1 { - let from = i + del_count - 1 - let to = i + items_count - 1 + for i = alen - del_count; i > start; i = i - 1L { + let from = i + del_count - 1L + let to = i + items_count - 1L if array_mutator_has_index(interp, o, from) { let from_value = @runtime.get_array_like_element_interp(interp, o, from) array_mutator_set_index(interp, o, to, from_value) @@ -815,8 +897,8 @@ fn array_splice_indexed_properties( } } } - for i = 0; i < items_count; i = i + 1 { - array_mutator_set_index(interp, o, start + i, new_items[i]) + for i = 0; i < new_items.length(); i = i + 1 { + array_mutator_set_index(interp, o, start + i.to_int64(), new_items[i]) } array_mutator_set_length(interp, o, new_len) removed diff --git a/interpreter/stdlib/array_sort.mbt b/interpreter/stdlib/array_sort.mbt index 59abba10..bacb27ba 100644 --- a/interpreter/stdlib/array_sort.mbt +++ b/interpreter/stdlib/array_sort.mbt @@ -109,16 +109,16 @@ fn array_sort_indexed_properties( let elements : Array[Value] = [] // SortIndexedProperties(..., skip-holes): only values for indices where // HasProperty is true enter the sortable list. Holes are deleted later. - for i = 0; i < len; i = i + 1 { + for i = 0L; i < len; i = i + 1L { if array_mutator_has_index(interp, o, i) { elements.push(@runtime.get_array_like_element_interp(interp, o, i)) } } array_sort_values(interp, elements, comparefn) for i = 0; i < elements.length(); i = i + 1 { - array_mutator_set_index(interp, o, i, elements[i]) + array_mutator_set_index(interp, o, i.to_int64(), elements[i]) } - for i = elements.length(); i < len; i = i + 1 { + for i = elements.length().to_int64(); i < len; i = i + 1L { array_mutator_delete_index(interp, o, i) } o diff --git a/interpreter/stdlib/builtins_array_init.mbt b/interpreter/stdlib/builtins_array_init.mbt index 73e61bee..544f417c 100644 --- a/interpreter/stdlib/builtins_array_init.mbt +++ b/interpreter/stdlib/builtins_array_init.mbt @@ -66,7 +66,7 @@ fn setup_array_builtins( "," } let parts : Array[String] = [] - for i = 0; i < len; i = i + 1 { + for i = 0L; i < len; i = i + 1L { let v = @runtime.get_array_like_element_interp(interp, o, i) match v { Null | Undefined => parts.push("") @@ -82,26 +82,26 @@ fn setup_array_builtins( fn(interp, this_val, args) raise { let o = to_object(this_val, interp) let len = @runtime.to_array_like_length_interp(o, interp) - if len == 0 { + if len == 0L { return Value::Number(-1.0) } let search = if args.length() > 0 { args[0] } else { Undefined } let from_idx = if args.length() > 1 { - interp.to_number(args[1]).to_int() + array_mutator_to_integer_index(interp, args[1]) } else { - 0 + 0L } - let start = if from_idx < 0 { + let start = if from_idx < 0L { let s = len + from_idx - if s < 0 { - 0 + if s < 0L { + 0L } else { s } } else { from_idx } - for i = start; i < len; i = i + 1 { + for i = start; i < len; i = i + 1L { if @runtime.has_array_like_element(interp, o, i) { if strict_equal_val( @runtime.get_array_like_element_interp(interp, o, i), @@ -120,23 +120,23 @@ fn setup_array_builtins( fn(interp, this_val, args) raise { let o = to_object(this_val, interp) let len = @runtime.to_array_like_length_interp(o, interp) - if len == 0 { + if len == 0L { return Value::Number(-1.0) } let search = if args.length() > 0 { args[0] } else { Undefined } let start = if args.length() > 1 { - interp.to_number(args[1]).to_int() + array_mutator_to_integer_index(interp, args[1]) } else { - len - 1 + len - 1L } - let from = if start < 0 { + let from = if start < 0L { len + start } else if start >= len { - len - 1 + len - 1L } else { start } - for i = from; i >= 0; i = i - 1 { + for i = from; i >= 0L; i = i - 1L { if @runtime.has_array_like_element(interp, o, i) { if strict_equal_val( @runtime.get_array_like_element_interp(interp, o, i), @@ -156,23 +156,23 @@ fn setup_array_builtins( let o = to_object(this_val, interp) let len = @runtime.to_array_like_length_interp(o, interp) // §22.1.3.11 step 3: ToInteger(fromIndex) is NOT called when len is 0. - guard len != 0 else { return Value::Bool(false) } + guard len != 0L else { return Value::Bool(false) } let (search, from_idx) = match args { - [s, fi, ..] => (s, interp.to_number(fi).to_int()) - [s] => (s, 0) - [] => (Undefined, 0) + [s, fi, ..] => (s, array_mutator_to_integer_index(interp, fi)) + [s] => (s, 0L) + [] => (Undefined, 0L) } - let start = if from_idx < 0 { + let start = if from_idx < 0L { let s = len + from_idx - if s < 0 { - 0 + if s < 0L { + 0L } else { s } } else { from_idx } - for i in start.. 0 { - interp.to_number(args[0]).to_int() + array_mutator_to_integer_index(interp, args[0]) } else { - 0 + 0L } let mut end = if args.length() > 1 { match args[1] { Undefined => alen - _ => interp.to_number(args[1]).to_int() + _ => array_mutator_to_integer_index(interp, args[1]) } } else { alen @@ -217,10 +217,10 @@ fn setup_array_builtins( if end > alen { end = alen } - let count = if end > start { end - start } else { 0 } + let count = if end > start { end - start } else { 0L } let a = @runtime.array_species_create(interp, o, count) - let mut n = 0 - for i = start; i < end; i = i + 1 { + let mut n = 0L + for i = start; i < end; i = i + 1L { if @runtime.has_array_like_element(interp, o, i) { @runtime.create_data_property_or_throw( interp, @@ -229,7 +229,7 @@ fn setup_array_builtins( @runtime.get_array_like_element_interp(interp, o, i), ) } - n += 1 + n += 1L } @runtime.set_array_like_length(a, n) a @@ -240,8 +240,8 @@ fn setup_array_builtins( length=1, fn(interp, this_val, args) raise { let o = to_object(this_val, interp) - let a = @runtime.array_species_create(interp, o, 0) - let mut n = 0 + let a = @runtime.array_species_create(interp, o, 0L) + let mut n = 0L let concat_sym_id = interp.realm_state.well_known_symbols.is_concat_spreadable.id // Process this value and each argument per ES2015 concat spec let items : Array[Value] = [o] @@ -292,7 +292,7 @@ fn setup_array_builtins( } if spreadable { let len = @runtime.to_array_like_length_interp(item, interp) - for i = 0; i < len; i = i + 1 { + for i = 0L; i < len; i = i + 1L { if @runtime.has_array_like_element(interp, item, i) { @runtime.create_data_property_or_throw( interp, @@ -357,12 +357,12 @@ fn setup_array_builtins( let o = to_object(this_val, interp) let len = @runtime.to_array_like_length_interp(o, interp) let idx = if args.length() > 0 { - interp.to_number(args[0]).to_int() + array_mutator_to_integer_index(interp, args[0]) } else { - 0 + 0L } - let actual_idx = if idx < 0 { len + idx } else { idx } - if actual_idx >= 0 && actual_idx < len { + let actual_idx = if idx < 0L { len + idx } else { idx } + if actual_idx >= 0L && actual_idx < len { @runtime.get_array_like_element_interp(interp, o, actual_idx) } else { Undefined @@ -376,7 +376,7 @@ fn setup_array_builtins( let o = to_object(this_val, interp) let len = @runtime.to_array_like_length_interp(o, interp) let parts : Array[String] = [] - for i = 0; i < len; i = i + 1 { + for i = 0L; i < len; i = i + 1L { let v = @runtime.get_array_like_element_interp(interp, o, i) match v { Null | Undefined => parts.push("") @@ -415,7 +415,7 @@ fn setup_array_builtins( } let this_arg = if args.length() > 1 { args[1] } else { Undefined } let loc = @token.Loc::default() - for i = 0; i < len; i = i + 1 { + for i = 0L; i < len; i = i + 1L { if @runtime.has_array_like_element(interp, o, i) { let _ = interp.call_value( callback, @@ -447,7 +447,7 @@ fn setup_array_builtins( let this_arg = if args.length() > 1 { args[1] } else { Undefined } let loc = @token.Loc::default() let a = @runtime.array_species_create(interp, o, len) - for i = 0; i < len; i = i + 1 { + for i = 0L; i < len; i = i + 1L { if @runtime.has_array_like_element(interp, o, i) { let val = interp.call_value( callback, @@ -480,9 +480,9 @@ fn setup_array_builtins( } let this_arg = if args.length() > 1 { args[1] } else { Undefined } let loc = @token.Loc::default() - let a = @runtime.array_species_create(interp, o, 0) - let mut to = 0 - for i = 0; i < len; i = i + 1 { + let a = @runtime.array_species_create(interp, o, 0L) + let mut to = 0L + for i = 0L; i < len; i = i + 1L { if @runtime.has_array_like_element(interp, o, i) { let el = @runtime.get_array_like_element_interp(interp, o, i) let val = interp.call_value( @@ -493,7 +493,7 @@ fn setup_array_builtins( ) if @runtime.is_truthy(val) { @runtime.create_data_property_or_throw(interp, a, to, el) - to += 1 + to += 1L } } } @@ -514,7 +514,7 @@ fn setup_array_builtins( } let loc = @token.Loc::default() let mut acc : Value = Undefined - let mut k = 0 + let mut k = 0L if args.length() > 1 { acc = args[1] } else { @@ -523,11 +523,11 @@ fn setup_array_builtins( while k < len { if @runtime.has_array_like_element(interp, o, k) { acc = @runtime.get_array_like_element_interp(interp, o, k) - k = k + 1 + k = k + 1L k_present = true break } - k = k + 1 + k = k + 1L } if !k_present { raise @errors.TypeError( @@ -549,7 +549,7 @@ fn setup_array_builtins( loc, ) } - k = k + 1 + k = k + 1L } acc }, @@ -568,20 +568,20 @@ fn setup_array_builtins( } let loc = @token.Loc::default() let mut acc : Value = Undefined - let mut k = len - 1 + let mut k = len - 1L if args.length() > 1 { acc = args[1] } else { // Per spec: find last present element as initial value (searching right to left) let mut k_present = false - while k >= 0 { + while k >= 0L { if @runtime.has_array_like_element(interp, o, k) { acc = @runtime.get_array_like_element_interp(interp, o, k) - k = k - 1 + k = k - 1L k_present = true break } - k = k - 1 + k = k - 1L } if !k_present { raise @errors.TypeError( @@ -589,7 +589,7 @@ fn setup_array_builtins( ) } } - while k >= 0 { + while k >= 0L { if @runtime.has_array_like_element(interp, o, k) { acc = interp.call_value( callback, @@ -603,7 +603,7 @@ fn setup_array_builtins( loc, ) } - k = k - 1 + k = k - 1L } acc }, @@ -622,7 +622,7 @@ fn setup_array_builtins( } let this_arg = if args.length() > 1 { args[1] } else { Undefined } let loc = @token.Loc::default() - for i = 0; i < len; i = i + 1 { + for i = 0L; i < len; i = i + 1L { let el = @runtime.get_array_like_element_interp(interp, o, i) let val = interp.call_value( callback, @@ -651,7 +651,7 @@ fn setup_array_builtins( } let this_arg = if args.length() > 1 { args[1] } else { Undefined } let loc = @token.Loc::default() - for i = 0; i < len; i = i + 1 { + for i = 0L; i < len; i = i + 1L { let el = @runtime.get_array_like_element_interp(interp, o, i) let val = interp.call_value( callback, @@ -680,7 +680,7 @@ fn setup_array_builtins( } let this_arg = if args.length() > 1 { args[1] } else { Undefined } let loc = @token.Loc::default() - for i = len - 1; i >= 0; i = i - 1 { + for i = len - 1L; i >= 0L; i = i - 1L { let el = @runtime.get_array_like_element_interp(interp, o, i) let val = interp.call_value( callback, @@ -709,7 +709,7 @@ fn setup_array_builtins( } let this_arg = if args.length() > 1 { args[1] } else { Undefined } let loc = @token.Loc::default() - for i = len - 1; i >= 0; i = i - 1 { + for i = len - 1L; i >= 0L; i = i - 1L { let el = @runtime.get_array_like_element_interp(interp, o, i) let val = interp.call_value( callback, @@ -738,7 +738,7 @@ fn setup_array_builtins( } let this_arg = if args.length() > 1 { args[1] } else { Undefined } let loc = @token.Loc::default() - for i = 0; i < len; i = i + 1 { + for i = 0L; i < len; i = i + 1L { if @runtime.has_array_like_element(interp, o, i) { let val = interp.call_value( callback, @@ -772,7 +772,7 @@ fn setup_array_builtins( } let this_arg = if args.length() > 1 { args[1] } else { Undefined } let loc = @token.Loc::default() - for i = 0; i < len; i = i + 1 { + for i = 0L; i < len; i = i + 1L { if @runtime.has_array_like_element(interp, o, i) { let val = interp.call_value( callback, @@ -817,11 +817,11 @@ fn setup_array_builtins( let elements = @runtime.to_array_like_elements(o) let flat_result : Array[Value] = [] @runtime.flatten_array_val(elements, depth, flat_result) - let a = @runtime.array_species_create(interp, o, 0) + let a = @runtime.array_species_create(interp, o, 0L) for i, val in flat_result { - @runtime.create_data_property_or_throw(interp, a, i, val) + @runtime.create_data_property_or_throw(interp, a, i.to_int64(), val) } - @runtime.set_array_like_length(a, flat_result.length()) + @runtime.set_array_like_length(a, flat_result.length().to_int64()) a }, ) @@ -839,9 +839,9 @@ fn setup_array_builtins( } let this_arg = if args.length() > 1 { args[1] } else { Undefined } let loc = @token.Loc::default() - let a = @runtime.array_species_create(interp, o, 0) - let mut n = 0 - for i = 0; i < len; i = i + 1 { + let a = @runtime.array_species_create(interp, o, 0L) + let mut n = 0L + for i = 0L; i < len; i = i + 1L { let el = @runtime.get_array_like_element_interp(interp, o, i) let val = interp.call_value( callback, @@ -853,11 +853,11 @@ fn setup_array_builtins( Array(inner) => for item in inner.elements { @runtime.create_data_property_or_throw(interp, a, n, item) - n += 1 + n += 1L } _ => { @runtime.create_data_property_or_throw(interp, a, n, val) - n += 1 + n += 1L } } } @@ -890,8 +890,9 @@ fn setup_array_builtins( fn(interp, this_val, _args) raise { let o = to_object(this_val, interp) let len = @runtime.to_array_like_length_interp(o, interp) + array_mutator_require_array_create_length(len) let result : Array[Value] = [] - for i = len - 1; i >= 0; i = i - 1 { + for i = len - 1L; i >= 0L; i = i - 1L { result.push(@runtime.get_array_like_element_interp(interp, o, i)) } @runtime.make_array(result) @@ -916,8 +917,9 @@ fn setup_array_builtins( } let o = to_object(this_val, interp) let len = @runtime.to_array_like_length_interp(o, interp) + array_mutator_require_array_create_length(len) let result : Array[Value] = [] - for i in 0.. 0 { @@ -1025,23 +1027,23 @@ fn setup_array_builtins( let o = to_object(this_val, interp) let len = @runtime.to_array_like_length_interp(o, interp) let mut start = if args.length() > 0 { - interp.to_number(args[0]).to_int() + array_mutator_to_integer_index(interp, args[0]) } else { - 0 + 0L } - if start < 0 { + if start < 0L { start = len + start - if start < 0 { - start = 0 + if start < 0L { + start = 0L } } if start > len { start = len } let del_count = if args.length() > 1 { - let d = interp.to_number(args[1]).to_int() - if d < 0 { - 0 + let d = array_mutator_to_integer_index(interp, args[1]) + if d < 0L { + 0L } else if d > len - start { len - start } else { @@ -1050,14 +1052,24 @@ fn setup_array_builtins( } else { len - start } + let insert_count = if args.length() > 2 { + (args.length() - 2).to_int64() + } else { + 0L + } + let new_len = len - del_count + insert_count + if new_len > ARRAY_MAX_SAFE_LENGTH_I64 { + raise @errors.TypeError(message="Invalid array length") + } + array_mutator_require_array_create_length(new_len) let result : Array[Value] = [] - for i = 0; i < start; i = i + 1 { + for i = 0L; i < start; i = i + 1L { result.push(@runtime.get_array_like_element_interp(interp, o, i)) } for i = 2; i < args.length(); i = i + 1 { result.push(args[i]) } - for i = start + del_count; i < len; i = i + 1 { + for i = start + del_count; i < len; i = i + 1L { result.push(@runtime.get_array_like_element_interp(interp, o, i)) } @runtime.make_array(result) @@ -1069,18 +1081,19 @@ fn setup_array_builtins( fn(interp, this_val, args) raise { let o = to_object(this_val, interp) let idx = if args.length() > 0 { - interp.to_number(args[0]).to_int() + array_mutator_to_integer_index(interp, args[0]) } else { - 0 + 0L } let value = if args.length() > 1 { args[1] } else { Undefined } let len = @runtime.to_array_like_length_interp(o, interp) - let actual_idx = if idx < 0 { len + idx } else { idx } - if actual_idx < 0 || actual_idx >= len { + let actual_idx = if idx < 0L { len + idx } else { idx } + if actual_idx < 0L || actual_idx >= len { raise @errors.RangeError(message="Invalid index") } + array_mutator_require_array_create_length(len) let result : Array[Value] = [] - for i = 0; i < len; i = i + 1 { + for i = 0L; i < len; i = i + 1L { if i == actual_idx { result.push(value) } else { @@ -1237,7 +1250,7 @@ fn setup_array_builtins( } let iterator = interp.call_value(iterator_method, source, [], loc) let next_method = interp.get_iterator_next_method(iterator, loc) - let mut k = 0 + let mut k = 0L while true { let iter_result = match interp.iterator_step_result(iterator, next_method, loc) { @@ -1263,7 +1276,7 @@ fn setup_array_builtins( None => value } elements.push(mapped) - k = k + 1 + k = k + 1L } } if !used_iterator { @@ -1304,7 +1317,7 @@ fn setup_array_builtins( _ => { let o = to_object(source, interp) let len = @runtime.to_array_like_length_interp(o, interp) - for i = 0; i < len; i = i + 1 { + for i = 0L; i < len; i = i + 1L { let el = interp.get_property(o, i.to_string(), loc) let mapped = match map_fn { Some(fn_val) => @@ -1401,7 +1414,8 @@ fn setup_array_builtins( // avoid OOM for pathological inputs; lengths above the cap are // tracked in ArrayData.bag so arr.length still // reports the correct value. - let elements : Array[Value] = if len64 <= 10000000L { + let elements : Array[Value] = if len64 <= + ARRAY_DENSE_MATERIALIZE_LIMIT_I64 { Array::make(len64.to_int(), Undefined) } else { [] diff --git a/interpreter/stdlib/builtins_arraybuffer.mbt b/interpreter/stdlib/builtins_arraybuffer.mbt index c0140d56..20de23cf 100644 --- a/interpreter/stdlib/builtins_arraybuffer.mbt +++ b/interpreter/stdlib/builtins_arraybuffer.mbt @@ -538,11 +538,17 @@ pub fn setup_arraybuffer_builtins( message="Constructor ArrayBuffer requires 'new'", ) } - let byte_length = if args.length() > 0 { + let byte_length64 = if args.length() > 0 { interp.to_index(args[0]) } else { - 0 + 0L } + if byte_length64 > MAX_ARRAYBUFFER_BYTE_LENGTH.to_int64() { + raise @errors.RangeError( + message="ArrayBuffer allocation size is too large", + ) + } + let byte_length = byte_length64.to_int() let loc = @token.Loc::default() let proto = match context.new_target() { Some(new_target) => { diff --git a/interpreter/stdlib/builtins_dataview.mbt b/interpreter/stdlib/builtins_dataview.mbt index 16f4ec76..760520c5 100644 --- a/interpreter/stdlib/builtins_dataview.mbt +++ b/interpreter/stdlib/builtins_dataview.mbt @@ -250,7 +250,7 @@ fn get_little_endian(args : Array[Value], arg_index : Int) -> Bool { /// Helper to validate DataView access. fn validate_dataview_access( data : ObjectData, - byte_offset : Int, + byte_offset : Int64, element_size : Int, realm_state : @runtime.RealmState, ) -> (Array[Int], Int) raise Error { @@ -280,7 +280,9 @@ fn validate_dataview_access( Some(Value::Number(n)) => n.to_int() _ => 0 } - if byte_offset < 0 || byte_offset + element_size > dv_byte_length { + let element_size64 = element_size.to_int64() + let dv_byte_length64 = dv_byte_length.to_int64() + if byte_offset < 0L || byte_offset + element_size64 > dv_byte_length64 { raise @errors.RangeError( message="Offset is outside the bounds of the DataView", ) @@ -292,7 +294,7 @@ fn validate_dataview_access( message="Cannot perform DataView operation on a detached ArrayBuffer", ) } - (bytes, dv_offset + byte_offset) + (bytes, (dv_offset.to_int64() + byte_offset).to_int()) } ///| @@ -937,11 +939,11 @@ pub fn setup_dataview_builtins( ) let byte_offset = if args.length() > 1 { match args[1] { - Undefined => 0 + Undefined => 0L _ => interp.to_index(args[1]) } } else { - 0 + 0L } if is_arraybuffer_state_detached(storage_state, buf_id) { raise @errors.TypeError( @@ -957,24 +959,25 @@ pub fn setup_dataview_builtins( } _ => 0 } - if byte_offset > buf_byte_length { + let buf_byte_length64 = buf_byte_length.to_int64() + if byte_offset > buf_byte_length64 { raise @errors.RangeError( message="Start offset \{byte_offset} is outside the bounds of the buffer", ) } let byte_length = if args.length() > 2 { match args[2] { - Undefined => buf_byte_length - byte_offset + Undefined => buf_byte_length64 - byte_offset _ => { let n = interp.to_index(args[2]) - if byte_offset + n > buf_byte_length { + if byte_offset + n > buf_byte_length64 { raise @errors.RangeError(message="Invalid DataView length \{n}") } n } } } else { - buf_byte_length - byte_offset + buf_byte_length64 - byte_offset } let loc = @token.Loc::default() let proto = match context.new_target() { diff --git a/interpreter/stdlib/builtins_json.mbt b/interpreter/stdlib/builtins_json.mbt index eaa4c2f4..3adb62bd 100644 --- a/interpreter/stdlib/builtins_json.mbt +++ b/interpreter/stdlib/builtins_json.mbt @@ -510,7 +510,7 @@ fn json_internalize( // Array branch: ? ToLength(? Get(val, "length")), then for each index: // delete or CreateDataProperty via [[DefineOwnProperty]]. let len = @runtime.to_array_like_length_interp(val, interp) - for i = 0; i < len; i = i + 1 { + for i = 0L; i < len; i = i + 1L { let idx = i.to_string() let new_val = json_internalize(val, idx, reviver, interp) match new_val { diff --git a/interpreter/stdlib/builtins_object_integrity.mbt b/interpreter/stdlib/builtins_object_integrity.mbt index 694a626c..9684fc0c 100644 --- a/interpreter/stdlib/builtins_object_integrity.mbt +++ b/interpreter/stdlib/builtins_object_integrity.mbt @@ -160,7 +160,7 @@ fn make_group_by_func() -> Value { let o = to_object(items, interp) let len = @runtime.to_array_like_length_interp(o, interp) let group_arrays : Map[String, Array[Value]] = {} - for k = 0; k < len; k = k + 1 { + for k = 0L; k < len; k = k + 1L { let kvalue = @runtime.get_array_like_element_interp(interp, o, k) let key = interp.call_value( callback, diff --git a/interpreter/stdlib/builtins_reflect.mbt b/interpreter/stdlib/builtins_reflect.mbt index 01cd7977..699fdeca 100644 --- a/interpreter/stdlib/builtins_reflect.mbt +++ b/interpreter/stdlib/builtins_reflect.mbt @@ -15,7 +15,7 @@ pub fn create_list_from_array_like( } let len = @runtime.to_array_like_length_interp(val, interp) let result : Array[Value] = [] - for i = 0; i < len; i = i + 1 { + for i = 0L; i < len; i = i + 1L { result.push(@runtime.get_array_like_element_interp(interp, val, i)) } result diff --git a/interpreter/stdlib/builtins_typedarray.mbt b/interpreter/stdlib/builtins_typedarray.mbt index b0980862..c85b0527 100644 --- a/interpreter/stdlib/builtins_typedarray.mbt +++ b/interpreter/stdlib/builtins_typedarray.mbt @@ -482,6 +482,14 @@ fn make_typedarray_from_buffer( }) } +///| +fn typedarray_index_to_int(index : Int64, message : String) -> Int raise Error { + if index > 0x7FFFFFFFL { + raise @errors.RangeError(message~) + } + index.to_int() +} + ///| fn typedarray_to_array_like_length( val : Value, @@ -1016,7 +1024,7 @@ pub fn setup_typedarray_builtins( let target_offset = if args.length() > 1 { interp.to_index(args[1]) } else { - 0 + 0L } if typedarray_is_detached(data, realm_state) { raise @errors.TypeError( @@ -1039,9 +1047,10 @@ pub fn setup_typedarray_builtins( Some(Value::Number(n)) => n.to_int() _ => 0 } - if target_offset + src_len > ta_length { + if target_offset + src_len.to_int64() > ta_length.to_int64() { raise @errors.RangeError(message="Source is too large") } + let target_offset_int = target_offset.to_int() // Buffer all source elements first to handle overlapping ArrayBuffers let temp : Array[Double] = [] for i = 0; i < src_len; i = i + 1 { @@ -1052,20 +1061,31 @@ pub fn setup_typedarray_builtins( } } for i = 0; i < src_len; i = i + 1 { - typedarray_set_index(data, target_offset + i, temp[i], realm_state) + typedarray_set_index( + data, + target_offset_int + i, + temp[i], + realm_state, + ) } Undefined } src => { let src_obj = to_object(src, interp) let src_len = @runtime.to_array_like_length_interp(src_obj, interp) - if target_offset + src_len > ta_length { + if target_offset + src_len > ta_length.to_int64() { raise @errors.RangeError(message="Source is too large") } - for i = 0; i < src_len; i = i + 1 { + let target_offset_int = target_offset.to_int() + for i = 0L; i < src_len; i = i + 1L { let val = @runtime.get_array_like_element_interp(interp, src_obj, i) let num = interp.to_number(val) - typedarray_set_index(data, target_offset + i, num, realm_state) + typedarray_set_index( + data, + target_offset_int + i.to_int(), + num, + realm_state, + ) } Undefined } @@ -2756,7 +2776,7 @@ pub fn setup_typedarray_builtins( Undefined | Null => { let array_like = to_object(source, interp) let len = @runtime.to_array_like_length_interp(array_like, interp) - for i = 0; i < len; i = i + 1 { + for i = 0L; i < len; i = i + 1L { source_values.push( interp.get_property(array_like, i.to_string(), loc), ) @@ -3056,7 +3076,10 @@ pub fn setup_typedarray_builtins( | Value::Null | Value::Undefined | Value::Symbol(_) => { - let length = interp.to_index(first) + let length = typedarray_index_to_int( + interp.to_index(first), + "Invalid typed array length", + ) make_typedarray_object( type_name, length, @@ -3102,12 +3125,10 @@ pub fn setup_typedarray_builtins( match args[1] { Undefined => 0 _ => { - let n = interp.to_index(args[1]) - if n < 0 { - raise @errors.RangeError( - message="Start offset \{n} is outside the bounds of the buffer", - ) - } + let n = typedarray_index_to_int( + interp.to_index(args[1]), + "Start offset is outside the bounds of the buffer", + ) n } } @@ -3134,7 +3155,11 @@ pub fn setup_typedarray_builtins( } else { (buf_byte_length - byte_offset) / bpe } - _ => interp.to_index(args[2]) + _ => + typedarray_index_to_int( + interp.to_index(args[2]), + "Invalid typed array length", + ) } } else if (buf_byte_length - byte_offset) % bpe != 0 { raise @errors.RangeError( diff --git a/scripts/test262_skip_metadata.json b/scripts/test262_skip_metadata.json index af1b9232..90529101 100644 --- a/scripts/test262_skip_metadata.json +++ b/scripts/test262_skip_metadata.json @@ -50,22 +50,5 @@ "CanBlockIsFalse", "CanBlockIsTrue" ], - "skip_path_suffixes": { - "built-ins/Array/prototype/includes/length-boundaries.js": "int64-array-index (#307)", - "built-ins/Array/prototype/reverse/length-exceeding-integer-limit-with-object.js": "int64-array-index (#307)", - "built-ins/Array/prototype/reverse/length-exceeding-integer-limit-with-proxy.js": "int64-array-index (#307)", - "built-ins/Array/prototype/slice/length-exceeding-integer-limit-proxied-array.js": "int64-array-index (#307)", - "built-ins/Array/prototype/slice/length-exceeding-integer-limit.js": "int64-array-index (#307)", - "built-ins/Array/prototype/splice/clamps-length-to-integer-limit.js": "int64-array-index (#307)", - "built-ins/Array/prototype/splice/create-species-length-exceeding-integer-limit.js": "int64-array-index (#307)", - "built-ins/Array/prototype/splice/length-and-deleteCount-exceeding-integer-limit.js": "int64-array-index (#307)", - "built-ins/Array/prototype/splice/length-exceeding-integer-limit-shrink-array.js": "int64-array-index (#307)", - "built-ins/Array/prototype/splice/length-near-integer-limit-grow-array.js": "int64-array-index (#307)", - "built-ins/Array/prototype/splice/throws-if-integer-limit-exceeded.js": "int64-array-index (#307)", - "built-ins/Array/prototype/toReversed/length-exceeding-array-length-limit.js": "int64-array-index (#307)", - "built-ins/Array/prototype/toSorted/length-exceeding-array-length-limit.js": "int64-array-index (#307)", - "built-ins/Array/prototype/toSpliced/length-clamped-to-2pow53minus1.js": "int64-array-index (#307)", - "built-ins/Array/prototype/toSpliced/length-exceeding-array-length-limit.js": "int64-array-index (#307)", - "built-ins/Array/prototype/with/length-exceeding-array-length-limit.js": "int64-array-index (#307)" - } + "skip_path_suffixes": {} } From a243dc57cd8ef33d76c87943a5323602696672bc Mon Sep 17 00:00:00 2001 From: Koji Ishimoto Date: Thu, 25 Jun 2026 23:56:55 +0900 Subject: [PATCH 2/4] fix: honor array species creation failures --- interpreter/interpreter_test.mbt | 46 +++++++++++++++++++ interpreter/runtime/array_like.mbt | 35 +++++++++----- interpreter/stdlib/array_indexed_mutators.mbt | 14 ++++-- 3 files changed, 79 insertions(+), 16 deletions(-) diff --git a/interpreter/interpreter_test.mbt b/interpreter/interpreter_test.mbt index 7c56a6e4..bcabd541 100644 --- a/interpreter/interpreter_test.mbt +++ b/interpreter/interpreter_test.mbt @@ -16506,6 +16506,52 @@ test "array dense assignment updates stale sparse length override" { json_inspect(output, content=["150001", "2"]) } +///| +test "array species result rejects CreateDataPropertyOrThrow" { + let src = + #|function attempt(label, thunk) { + #| try { thunk(); console.log(label + ":ok"); } + #| catch (e) { console.log(label + ":" + e.name); } + #|} + #|var source = [1]; + #|source.constructor = { + #| [Symbol.species]: function() { return Object.preventExtensions({}); } + #|}; + #|attempt("map", function() { source.map(function(x) { return x; }); }); + #|attempt("slice", function() { source.slice(0, 1); }); + #|attempt("concat", function() { source.concat([2]); }); + let output = run_output(src) + json_inspect(output, content=[ + "map:TypeError", "slice:TypeError", "concat:TypeError", + ]) +} + +///| +test "array splice length overflow throws before species" { + let src = + #|var calls = []; + #|var array = []; + #|array.constructor = {}; + #|Object.defineProperty(array.constructor, Symbol.species, { + #| get: function() { + #| calls.push("species"); + #| return function(n) { calls.push("ctor:" + n); return []; }; + #| } + #|}); + #|var source = new Proxy(array, { + #| get: function(t, k, r) { + #| if (k === "length") { calls.push("length"); return 9007199254740991; } + #| calls.push(String(k)); + #| return Reflect.get(t, k, r); + #| } + #|}); + #|try { Array.prototype.splice.call(source, 0, 0, "x"); console.log("ok"); } + #|catch (e) { console.log(e.name); } + #|console.log(calls.join("|")); + let output = run_output(src) + json_inspect(output, content=["TypeError", "length"]) +} + ///| /// IteratorClose: iterator.return() called when for-of body throws (ForOfStmt) test "IteratorClose: body throw calls iterator return (ForOfStmt)" { diff --git a/interpreter/runtime/array_like.mbt b/interpreter/runtime/array_like.mbt index 215cf4c9..d00e3b72 100644 --- a/interpreter/runtime/array_like.mbt +++ b/interpreter/runtime/array_like.mbt @@ -575,18 +575,23 @@ pub fn delete_array_like_element(val : Value, index : Int) -> Unit { } ///| -/// CreateDataPropertyOrThrow(O, P, V) per ES spec 7.3.5 -/// Sets an indexed property on a species result value, using the fast path -/// for plain Arrays and set_property for everything else. +/// CreateDataPropertyOrThrow(O, P, V) per ES spec 7.3.5. +/// Sets an indexed property on a species result value and throws if +/// `[[DefineOwnProperty]]` rejects the creation. pub fn create_data_property_or_throw( interp : Interpreter, target : Value, index : Int64, value : Value, ) -> Unit raise { - match target { + let key = index.to_string() + let ok = match target { Array(data) => - if index >= 0L && index <= ARRAY_DENSE_MATERIALIZE_LIMIT_I64 { + if index >= 0L && + index <= ARRAY_DENSE_MATERIALIZE_LIMIT_I64 && + data.extensible && + data.length_writable && + !data.bag.descriptors.contains(key) { let i = index.to_int() if i == data.elements.length() { data.elements.push(value) @@ -603,22 +608,30 @@ pub fn create_data_property_or_throw( data.elements.push(value) } data.holes.remove(i) + match get_array_length_override(data) { + Some(len) if index + 1L > len => + set_array_length_override(data, index + 1L) + _ => () + } + true } else { - let _ = interp.define_own_property( + interp.define_own_property( target, - String_(index.to_string()), + String_(key), PartialDescriptor::data_default(value), @token.Loc::default(), ) } - _ => { - let _ = interp.define_own_property( + _ => + interp.define_own_property( target, - String_(index.to_string()), + String_(key), PartialDescriptor::data_default(value), @token.Loc::default(), ) - } + } + if !ok { + raise @errors.TypeError(message="Cannot create data property") } } diff --git a/interpreter/stdlib/array_indexed_mutators.mbt b/interpreter/stdlib/array_indexed_mutators.mbt index df930715..456f3ab0 100644 --- a/interpreter/stdlib/array_indexed_mutators.mbt +++ b/interpreter/stdlib/array_indexed_mutators.mbt @@ -850,6 +850,15 @@ fn array_splice_indexed_properties( d } } + let items_count = if args.length() > 2 { + (args.length() - 2).to_int64() + } else { + 0L + } + let new_len = alen - del_count + items_count + if new_len > ARRAY_MAX_SAFE_LENGTH_I64 { + raise @errors.TypeError(message="Invalid array length") + } let removed = @runtime.array_species_create(interp, o, del_count) for i = 0L; i < del_count; i = i + 1L { if array_mutator_has_index(interp, o, start + i) { @@ -866,11 +875,6 @@ fn array_splice_indexed_properties( for i = 2; i < args.length(); i = i + 1 { new_items.push(args[i]) } - let items_count = new_items.length().to_int64() - let new_len = alen - del_count + items_count - if new_len > ARRAY_MAX_SAFE_LENGTH_I64 { - raise @errors.TypeError(message="Invalid array length") - } if items_count < del_count { for i = start; i < alen - del_count; i = i + 1L { let from = i + del_count From 7c6106304fce1f3b0d1a2976afd8929c8920a2e7 Mon Sep 17 00:00:00 2001 From: Koji Ishimoto Date: Fri, 26 Jun 2026 00:12:30 +0900 Subject: [PATCH 3/4] fix: preserve negative infinity relative indices --- interpreter/interpreter_test.mbt | 17 +++++++++++++++++ interpreter/stdlib/array_indexed_mutators.mbt | 9 ++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/interpreter/interpreter_test.mbt b/interpreter/interpreter_test.mbt index bcabd541..7866de02 100644 --- a/interpreter/interpreter_test.mbt +++ b/interpreter/interpreter_test.mbt @@ -19154,6 +19154,23 @@ test "array includes uses Int64 ToLength and fromIndex near max safe integer" { json_inspect(output, content=["true", "false"]) } +///| +test "array relative indices preserve negative infinity" { + let src = + #|var obj = { 0: "x", length: 9007199254740991 }; + #|console.log(Array.prototype.at.call(obj, -Infinity)); + #|console.log(Array.prototype.lastIndexOf.call(obj, "x", -Infinity)); + #|console.log(Array.prototype.includes.call(obj, "x", -Infinity)); + #|console.log(Array.prototype.at.call(obj, -9007199254740992)); + #|try { Array.prototype.with.call(obj, -Infinity, "y"); console.log("ok"); } + #|catch (e) { console.log(e.name); } + #| + let output = run_output(src) + json_inspect(output, content=[ + "undefined", "-1", "true", "undefined", "RangeError", + ]) +} + ///| test "array slice copies sparse properties near max safe integer" { let src = diff --git a/interpreter/stdlib/array_indexed_mutators.mbt b/interpreter/stdlib/array_indexed_mutators.mbt index 456f3ab0..c079c5ba 100644 --- a/interpreter/stdlib/array_indexed_mutators.mbt +++ b/interpreter/stdlib/array_indexed_mutators.mbt @@ -41,6 +41,9 @@ const ARRAY_MAX_SAFE_LENGTH : Double = 9007199254740991.0 ///| const ARRAY_MAX_SAFE_LENGTH_I64 = 9007199254740991L +///| +const ARRAY_BELOW_MIN_RELATIVE_INDEX_I64 = -9007199254740992L + ///| const ARRAY_MAX_ARRAY_LENGTH_I64 = 4294967295L @@ -53,15 +56,15 @@ fn array_mutator_to_integer_index_from_number(n : Double) -> Int64 { 0L } else if n.is_inf() { if n < 0.0 { - -ARRAY_MAX_SAFE_LENGTH_I64 + ARRAY_BELOW_MIN_RELATIVE_INDEX_I64 } else { ARRAY_MAX_SAFE_LENGTH_I64 } } else { let sign = if n < 0.0 { -1.0 } else { 1.0 } let integer = n.abs().floor() * sign - if integer <= -ARRAY_MAX_SAFE_LENGTH { - -ARRAY_MAX_SAFE_LENGTH_I64 + if integer < -ARRAY_MAX_SAFE_LENGTH { + ARRAY_BELOW_MIN_RELATIVE_INDEX_I64 } else if integer >= ARRAY_MAX_SAFE_LENGTH { ARRAY_MAX_SAFE_LENGTH_I64 } else { From 4a5d2d23d044f0c36c718d5f52d3a6139739be8d Mon Sep 17 00:00:00 2001 From: Koji Ishimoto Date: Fri, 26 Jun 2026 00:24:21 +0900 Subject: [PATCH 4/4] test262: update skip metadata fixture expectation --- tooling/test262_metadata/metadata_wbtest.mbt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tooling/test262_metadata/metadata_wbtest.mbt b/tooling/test262_metadata/metadata_wbtest.mbt index 697f5eb8..d36e860c 100644 --- a/tooling/test262_metadata/metadata_wbtest.mbt +++ b/tooling/test262_metadata/metadata_wbtest.mbt @@ -80,11 +80,7 @@ test "loads strict shared skip metadata JSON" { assert_true(metadata.features.contains("class-fields-private")) assert_true(metadata.flags.contains("CanBlockIsFalse")) assert_true(metadata.flags.contains("CanBlockIsTrue")) - assert_true( - metadata.path_suffixes.contains( - "built-ins/Array/prototype/includes/length-boundaries.js", - ), - ) + assert_eq(metadata.path_suffixes.length(), 0) } ///|