diff --git a/SharpTS.Test262/baselines/compiled.txt b/SharpTS.Test262/baselines/compiled.txt index 074614e1..4aeaa500 100644 --- a/SharpTS.Test262/baselines/compiled.txt +++ b/SharpTS.Test262/baselines/compiled.txt @@ -1,7 +1,7 @@ # Test262 baseline — do not hand-edit. Regenerate with SHARPTS_TEST262_UPDATE_BASELINE=1. test/built-ins/Array/15.4.5-1.js Pass test/built-ins/Array/15.4.5.1-5-1.js RuntimeError -test/built-ins/Array/15.4.5.1-5-2.js TypeCheckError +test/built-ins/Array/15.4.5.1-5-2.js RuntimeError test/built-ins/Array/S15.4.1_A1.1_T1.js Pass test/built-ins/Array/S15.4.1_A1.1_T2.js Fail test/built-ins/Array/S15.4.1_A1.1_T3.js Pass diff --git a/SharpTS.Test262/baselines/interpreted.txt b/SharpTS.Test262/baselines/interpreted.txt index c772dbd2..21583ce6 100644 --- a/SharpTS.Test262/baselines/interpreted.txt +++ b/SharpTS.Test262/baselines/interpreted.txt @@ -1,7 +1,7 @@ # Test262 baseline — do not hand-edit. Regenerate with SHARPTS_TEST262_UPDATE_BASELINE=1. test/built-ins/Array/15.4.5-1.js Pass test/built-ins/Array/15.4.5.1-5-1.js RuntimeError -test/built-ins/Array/15.4.5.1-5-2.js TypeCheckError +test/built-ins/Array/15.4.5.1-5-2.js RuntimeError test/built-ins/Array/S15.4.1_A1.1_T1.js RuntimeError test/built-ins/Array/S15.4.1_A1.1_T2.js RuntimeError test/built-ins/Array/S15.4.1_A1.1_T3.js Pass diff --git a/SharpTS.Tests/TypeCheckerTests/TypeErrorTests.cs b/SharpTS.Tests/TypeCheckerTests/TypeErrorTests.cs index f25a1e36..4594ceba 100644 --- a/SharpTS.Tests/TypeCheckerTests/TypeErrorTests.cs +++ b/SharpTS.Tests/TypeCheckerTests/TypeErrorTests.cs @@ -617,4 +617,69 @@ function getProperty(obj: T, key: K): T[K] { } #endregion + + #region Array index range (ECMA-262) + + // ECMA-262 array indices are integers in [0, 2^32 - 2]. Numeric literals + // outside that range (e.g. 4294967295, -1) are regular property + // assignments per spec, not array-element writes — so the element-type + // check must not fire for them. Regression for issue #77. + + [Fact] + public void StringAssignToOutOfRangeUint32Index_OnNumberArray_Allowed() + { + var source = """ + var a: number[] = [0, 1, 2]; + a[4294967295] = "spec-legal"; + """; + + // Type-checker must not reject; runtime may still reject (separate + // sparse-array layer of work). + var ex = Record.Exception(() => TestHarness.RunInterpreted(source)); + Assert.False( + ex is not null && ex.Message.Contains("Type Error"), + $"unexpected type error: {ex?.Message}"); + } + + [Fact] + public void StringAssignToNegativeIndex_OnNumberArray_Allowed() + { + var source = """ + var a: number[] = [0, 1, 2]; + a[-1] = "not an array element"; + """; + + var ex = Record.Exception(() => TestHarness.RunInterpreted(source)); + Assert.False( + ex is not null && ex.Message.Contains("Type Error"), + $"unexpected type error: {ex?.Message}"); + } + + [Fact] + public void StringAssignToInRangeIndex_OnNumberArray_StillFails() + { + var source = """ + var a: number[] = [0, 1, 2]; + a[5] = "still wrong"; + """; + + var ex = Assert.ThrowsAny(() => TestHarness.RunInterpreted(source)); + Assert.Contains("Type Error", ex.Message); + Assert.Contains("array of", ex.Message); + } + + [Fact] + public void StringAssignToMaxValidIndex_OnNumberArray_StillFails() + { + var source = """ + var a: number[] = [0, 1, 2]; + a[4294967294] = "still wrong"; + """; + + var ex = Assert.ThrowsAny(() => TestHarness.RunInterpreted(source)); + Assert.Contains("Type Error", ex.Message); + Assert.Contains("array of", ex.Message); + } + + #endregion } diff --git a/TypeSystem/TypeChecker.Properties.Index.cs b/TypeSystem/TypeChecker.Properties.Index.cs index d08176cd..700ad3db 100644 --- a/TypeSystem/TypeChecker.Properties.Index.cs +++ b/TypeSystem/TypeChecker.Properties.Index.cs @@ -363,7 +363,12 @@ private TypeInfo CheckSetIndex(Expr.SetIndex setIndex) if (objType is TypeInfo.Array arrayType) { - if (!IsCompatible(arrayType.ElementType, valueType)) + // ECMA-262 array indices are integers in [0, 2^32 - 2]. Numeric + // literals outside that range (e.g. 4294967295, -1) are regular + // property assignments, not array-element writes — element-type + // compatibility doesn't apply. + if (IsArrayIndexInRange(setIndex.Index) + && !IsCompatible(arrayType.ElementType, valueType)) { throw new TypeCheckException($" Cannot assign '{valueType}' to array of '{arrayType.ElementType}'."); } @@ -553,6 +558,9 @@ or TypeInfo.WeakMap or TypeInfo.WeakSet or TypeInfo.Promise or TypeInfo.Function { if (objType is TypeInfo.Array arrayType) { + // Same out-of-range carve-out as CheckSetIndex above. + if (!IsArrayIndexInRange(setIndex.Index)) + return valueType; if (IsCompatible(arrayType.ElementType, valueType)) return valueType; return null; @@ -587,4 +595,25 @@ or TypeInfo.WeakMap or TypeInfo.WeakSet or TypeInfo.Promise or TypeInfo.Function return null; } + + /// + /// Returns true if the index expression is either non-literal (so the + /// strict element-type check should still apply) or is a numeric literal + /// that falls in the ECMA-262 array-index range [0, 2^32 - 2]. Numeric + /// literals outside that range are spec-equivalent to ordinary property + /// assignments and do not write into an array element slot. + /// + private static bool IsArrayIndexInRange(Expr indexExpr) + { + if (TryGetNumericLiteral(indexExpr) is not double n) return true; + return n >= 0 && n < (double)uint.MaxValue && n == Math.Floor(n); + } + + private static double? TryGetNumericLiteral(Expr e) + { + if (e is Expr.Literal { Value: double d }) return d; + if (e is Expr.Unary u && u.Operator.Type == TokenType.MINUS + && u.Right is Expr.Literal { Value: double d2 }) return -d2; + return null; + } }