From 937c295057b36e60a7154c018c41b18edb9f1fd2 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Fri, 1 May 2026 15:29:56 -0700 Subject: [PATCH] fix: type checker accepts out-of-range numeric indices on arrays (#77) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ECMA-262 array indices are integers in [0, 2^32 - 2]. Numeric literals outside that range — e.g. `a[4294967295]` or `a[-1]` — are spec-equivalent to ordinary string-keyed property assignments and do not write into an array element slot. The element-type compatibility check in CheckSetIndex / CheckSetIndexOnType incorrectly fired for these, blocking Test262 cases like `built-ins/Array/15.4.5.1-5-2.js` from ever reaching the runtime. Gate the element-type throw on a new `IsArrayIndexInRange` helper that returns false for numeric literals (including unary-minus literals) outside the valid array-index range. Non-literal indices keep the strict check — TypeScript-default behavior is preserved for typical code. Test262 baseline movement: `built-ins/Array/15.4.5.1-5-2.js` TypeCheckError → RuntimeError in both interpreted and compiled buckets, matching the issue's stated acceptance ("some to RuntimeError where a distinct bug is exposed"). The remaining RangeError on assignment to index 2^32-1 is a runtime-side concern (next-layer sparse array work), not in scope for this issue. --- SharpTS.Test262/baselines/compiled.txt | 2 +- SharpTS.Test262/baselines/interpreted.txt | 2 +- .../TypeCheckerTests/TypeErrorTests.cs | 65 +++++++++++++++++++ TypeSystem/TypeChecker.Properties.Index.cs | 31 ++++++++- 4 files changed, 97 insertions(+), 3 deletions(-) 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; + } }